#pragma once #include "common/Export.h" #include "devtools/CallGraph.hpp" #include #include #include #include #include #include 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& 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 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 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 locals(int stackLevel = 0) const; /** @brief Root-table slots (globals). */ std::vector 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 containerChildren(VarKind kind, int frame, const std::vector& path) const; std::vector 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 setBreakpointEventFn(BreakpointEventFn fn) { bpEventFn_ = std::move(fn); } using PumpFn = std::function; 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& 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 mode_{RunMode::Running}; std::atomic reason_{PauseReason::None}; std::atomic breakOnError_{false}; std::atomic bpsEnabled_{true}; SourceLoc pauseLoc_; mutable std::mutex mu_; std::vector bps_; std::vector watchExprs_; std::vector 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