Squashed onto current main. Adds src/engine/common/Assert.h (EV_PARAM_CHECK / EV_ASSERT backed by zeroerr), an EVENGINE_ENABLE_ASSERTS option (non-Debug builds strip ZEROERR_NO_ASSERT unless enabled), parameter checks at key entry points, Doxygen comments across headers, Doxyfile + make docs / CMake docs targets, and Windows setup + code conventions in AGENTS.md.
225 lines
8.7 KiB
C++
225 lines
8.7 KiB
C++
#pragma once
|
|
|
|
#include "common/Export.h"
|
|
#include "devtools/CallGraph.hpp"
|
|
|
|
#include <atomic>
|
|
#include <cstdint>
|
|
#include <functional>
|
|
#include <mutex>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
struct SQVM;
|
|
typedef struct SQVM* HSQUIRRELVM;
|
|
|
|
namespace eve::dev {
|
|
|
|
enum class PauseReason : uint8_t {
|
|
None = 0,
|
|
PauseKey,
|
|
Breakpoint,
|
|
Step,
|
|
Exception,
|
|
Snapshot,
|
|
};
|
|
|
|
enum class RunMode : uint8_t {
|
|
Running = 0,
|
|
Paused, // frame-level or script-level pause
|
|
StepFrame, // run one game frame then pause
|
|
StepInto, // next script line (any call depth) — DAP stepIn
|
|
StepOver, // next script line at ≤ start depth — DAP next
|
|
StepOut, // next script line at < start depth — DAP stepOut
|
|
};
|
|
|
|
struct EVENGINE_API Breakpoint {
|
|
std::string source; // normalized path or basename
|
|
int line = 0;
|
|
bool enabled = true;
|
|
bool verified = false;
|
|
std::string condition; // optional GDScript-style condition; empty = always
|
|
int id = 0;
|
|
};
|
|
|
|
struct EVENGINE_API WatchEntry {
|
|
std::string expression;
|
|
std::string value; // last evaluated (display)
|
|
bool ok = false;
|
|
};
|
|
|
|
struct EVENGINE_API StackFrameInfo {
|
|
int id = 0;
|
|
SourceLoc loc;
|
|
std::string name;
|
|
};
|
|
|
|
struct EVENGINE_API VariableInfo {
|
|
std::string name;
|
|
std::string value;
|
|
std::string type;
|
|
bool expensive = false;
|
|
bool expandable = false; // container / instance: children available
|
|
int childCount = -1; // -1 = unknown
|
|
};
|
|
|
|
/** @brief Where a variable tree node is rooted (used by containerChildren/resolvePath). */
|
|
enum class VarKind : uint8_t {
|
|
Locals = 0, // frame locals
|
|
Globals = 1, // roottable
|
|
};
|
|
|
|
/**
|
|
* @brief Script + frame debugger: pause/step, breakpoints, watches.
|
|
*
|
|
* Frame pause: game loop skips eve_update (see load.nut + shouldRunUpdate).
|
|
* Script pause: Squirrel line hook blocks until resume/step (waitWhilePaused).
|
|
*
|
|
* Stepping (script, when stopped on a line):
|
|
* - stepInto — stop on the next line event (enter calls)
|
|
* - stepOver — stop on the next line at the same / outer stack depth
|
|
* - stepOut — stop after returning to the caller
|
|
*/
|
|
class EVENGINE_API Debugger {
|
|
public:
|
|
static Debugger& instance();
|
|
|
|
Debugger(const Debugger&) = delete;
|
|
Debugger& operator=(const Debugger&) = delete;
|
|
|
|
void attach(HSQUIRRELVM vm);
|
|
void detach();
|
|
bool isAttached() const { return vm_ != nullptr; }
|
|
HSQUIRRELVM vm() const { return vm_; }
|
|
|
|
// ---- run control ----
|
|
void pause(PauseReason reason = PauseReason::PauseKey);
|
|
void resume();
|
|
void stepFrame();
|
|
/** @brief Enter calls: stop on the next script line at any depth. */
|
|
void stepInto();
|
|
/** @brief Skip calls: stop on the next line at ≤ current stack depth. */
|
|
void stepOver();
|
|
/** @brief Finish current function: stop when stack depth drops. */
|
|
void stepOut();
|
|
/** @brief Alias for stepInto (historical name). */
|
|
void stepLine() { stepInto(); }
|
|
/**
|
|
* @brief Convenience: script stepOver when mid-hook; otherwise one game frame.
|
|
* Prefer stepInto/stepOver/stepOut from DAP / UI.
|
|
*/
|
|
void step();
|
|
bool isPaused() const { return mode_.load() == RunMode::Paused; }
|
|
RunMode mode() const { return mode_.load(); }
|
|
PauseReason lastPauseReason() const { return reason_.load(); }
|
|
const SourceLoc& pauseLocation() const { return pauseLoc_; }
|
|
/** @brief Current Squirrel call depth (1 = topmost script frame). 0 if none. */
|
|
int scriptStackDepth() const;
|
|
|
|
/** @brief Frame loop: true ⇒ call eve_update this frame. Consumes StepFrame. */
|
|
bool shouldRunUpdate();
|
|
/** @brief After a frame when StepFrame was active → return to Paused. */
|
|
void notifyFrameDone();
|
|
|
|
/**
|
|
* @brief Called from Squirrel line debug hook.
|
|
* Returns true if execution should block (breakpoint / step).
|
|
*/
|
|
bool onScriptLine(const SourceLoc& loc);
|
|
/** @brief Block until resume/step/detach (processes external poll callbacks). */
|
|
void waitWhilePaused(const std::function<void()>& pump = {});
|
|
|
|
// ---- breakpoints ----
|
|
int setBreakpoint(std::string source, int line, bool enabled = true,
|
|
std::string condition = {});
|
|
bool clearBreakpoint(std::string source, int line);
|
|
void clearBreakpoints(const std::string& source = {});
|
|
std::vector<Breakpoint> breakpoints() const;
|
|
bool hasBreakpoint(const std::string& source, int line) const;
|
|
|
|
// ---- watches ----
|
|
void addWatch(std::string expression);
|
|
bool removeWatch(const std::string& expression);
|
|
void clearWatches();
|
|
std::vector<WatchEntry> watches() const;
|
|
/** @brief Re-evaluate all watches against current VM (paused preferred). */
|
|
void refreshWatches();
|
|
/**
|
|
* @brief Evaluate an expression in the given frame's scope.
|
|
* Understands plain names, `a.b` paths, and full Squirrel expressions
|
|
* (arithmetic / calls / indexing) compiled against a locals+roottable env.
|
|
*/
|
|
VariableInfo evaluate(const std::string& expression, int frameLevel = 0) const;
|
|
/** @brief Locals of the given call-stack level (0 = current script frame). */
|
|
std::vector<VariableInfo> locals(int stackLevel = 0) const;
|
|
/** @brief Root-table slots (globals). */
|
|
std::vector<VariableInfo> globals() const;
|
|
/**
|
|
* @brief Children of a container variable. `path` is the key chain that locates
|
|
* the container from its root (locals frame / roottable / upvalues).
|
|
* Empty `path` lists the root entries themselves.
|
|
*/
|
|
std::vector<VariableInfo> containerChildren(VarKind kind, int frame,
|
|
const std::vector<std::string>& path) const;
|
|
std::vector<StackFrameInfo> stackTrace(int maxFrames = 32) const;
|
|
|
|
// ---- error / breakpoint policy ----
|
|
/** @brief Break on script errors (Godot "Break on Error"). Default off. */
|
|
void setBreakOnError(bool on) { breakOnError_.store(on); }
|
|
bool breakOnError() const { return breakOnError_.load(); }
|
|
/** @brief Master switch for all breakpoints ("skip all breakpoints"). */
|
|
void setBreakpointsEnabled(bool on) { bpsEnabled_.store(on); }
|
|
bool breakpointsEnabled() const { return bpsEnabled_.load(); }
|
|
/** @brief Called once when a breakpoint line is first observed by the line hook. */
|
|
using BreakpointEventFn = std::function<void(int id, const std::string& source, int line,
|
|
bool verified)>;
|
|
void setBreakpointEventFn(BreakpointEventFn fn) { bpEventFn_ = std::move(fn); }
|
|
|
|
using PumpFn = std::function<void()>;
|
|
void setPump(PumpFn pump) { pump_ = std::move(pump); }
|
|
|
|
/** @brief Normalize source paths for breakpoint matching (basename fallback). */
|
|
static std::string normalizeSource(std::string source);
|
|
/** @brief Basename of a normalized path (empty-safe). */
|
|
static std::string sourceBasename(const std::string& source);
|
|
/**
|
|
* @brief True when two source paths refer to the same script file.
|
|
* Matches exact path, basename, or one path as a suffix of the other.
|
|
*/
|
|
static bool sourcesMatch(const std::string& a, const std::string& b);
|
|
|
|
private:
|
|
Debugger() = default;
|
|
|
|
bool matchBreakpoint(const std::string& source, int line) const;
|
|
bool matchBreakpointAny(const std::string& source, int line) const;
|
|
bool conditionHolds(const Breakpoint& bp) const;
|
|
bool pushPathValue(HSQUIRRELVM vm, VarKind kind, int frame,
|
|
const std::vector<std::string>& path) const;
|
|
VariableInfo readLocal(HSQUIRRELVM vm, unsigned level, const std::string& name) const;
|
|
VariableInfo readRoot(HSQUIRRELVM vm, const std::string& name) const;
|
|
VariableInfo evaluateLegacy(const std::string& expression) const;
|
|
bool pushLocalValue(HSQUIRRELVM vm, unsigned level, const std::string& name) const;
|
|
void beginScriptStep(RunMode mode);
|
|
|
|
HSQUIRRELVM vm_ = nullptr;
|
|
std::atomic<RunMode> mode_{RunMode::Running};
|
|
std::atomic<PauseReason> reason_{PauseReason::None};
|
|
std::atomic<bool> breakOnError_{false};
|
|
std::atomic<bool> bpsEnabled_{true};
|
|
SourceLoc pauseLoc_;
|
|
mutable std::mutex mu_;
|
|
std::vector<Breakpoint> bps_;
|
|
std::vector<std::string> watchExprs_;
|
|
std::vector<WatchEntry> watchCache_;
|
|
int nextBpId_ = 1;
|
|
PumpFn pump_;
|
|
bool stepFrameArmed_ = false;
|
|
int stepStartDepth_ = 0;
|
|
/** @brief While set, step filters ignore this exact source+line (multi-_OP_LINE). */
|
|
SourceLoc stepSkipLoc_;
|
|
BreakpointEventFn bpEventFn_;
|
|
};
|
|
|
|
} // namespace eve::dev
|