Skip to content

Commit abd1a1d

Browse files
authored
Merge pull request #1099 from UE4SS-RE/luadebug
feat: adds Lua debugger GUI tab
2 parents f251fc6 + c18f643 commit abd1a1d

File tree

11 files changed

+4600
-14
lines changed

11 files changed

+4600
-14
lines changed

UE4SS/include/GUI/ImGuiUtility.hpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,18 @@
33
#include <JSON/JSON.hpp>
44
// #include <JSONParser/JSON.hpp>
55
#include <JSON/Parser/Parser.hpp>
6+
#include <UE4SSProgram.hpp>
7+
#include <SettingsManager.hpp>
68
#include <imgui.h>
79

810
namespace RC::GUI
911
{
12+
// Scale a pixel value by the GUI font scaling factor
13+
inline auto scaled(float value) -> float
14+
{
15+
return value * UE4SSProgram::settings_manager.Debug.DebugGUIFontScaling;
16+
}
17+
1018
auto ImGui_AutoScroll(const char* label, float* previous_max_scroll_y) -> void;
1119
auto ImGui_InputTextMultiline_WithAutoScroll(const char* label,
1220
char* buf,

UE4SS/include/GUI/LuaDebugger.hpp

Lines changed: 351 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,351 @@
1+
#pragma once
2+
3+
#include <atomic>
4+
#include <chrono>
5+
#include <condition_variable>
6+
#include <deque>
7+
#include <filesystem>
8+
#include <functional>
9+
#include <mutex>
10+
#include <set>
11+
#include <string>
12+
#include <unordered_map>
13+
#include <vector>
14+
15+
#include <File/Macros.hpp>
16+
#include <TextEditor.h>
17+
18+
struct lua_State;
19+
struct lua_Debug;
20+
21+
namespace RC::LuaMadeSimple
22+
{
23+
class Lua;
24+
}
25+
26+
namespace RC::GUI
27+
{
28+
// Represents a single entry in the Lua call stack
29+
struct LuaCallStackEntry
30+
{
31+
std::string function_name;
32+
std::string source;
33+
int line_number{0};
34+
int current_line{0};
35+
std::string what; // "Lua", "C", "main", "tail"
36+
std::string name_what; // "global", "local", "method", "field", ""
37+
};
38+
39+
// Represents a single stack slot value
40+
struct LuaStackSlot
41+
{
42+
int index{0};
43+
std::string type_name; // "nil", "boolean", "number", "string", "table", "function", "userdata", "thread", "lightuserdata"
44+
std::string value_preview; // Short string representation of the value
45+
bool is_table{false};
46+
bool is_userdata{false};
47+
};
48+
49+
// Represents a local variable at a stack frame
50+
struct LuaLocalVariable
51+
{
52+
std::string name;
53+
std::string type_name;
54+
std::string value_preview;
55+
bool is_table{false};
56+
bool is_userdata{false};
57+
};
58+
59+
// Represents a stack frame with its local variables
60+
struct LuaStackFrame
61+
{
62+
std::string function_name;
63+
std::string source;
64+
int line_number{0};
65+
int current_line{0};
66+
std::string what;
67+
std::vector<LuaLocalVariable> locals;
68+
};
69+
70+
// Represents a value in the variable tree (for table inspection)
71+
struct LuaValueNode
72+
{
73+
std::string key;
74+
std::string type_name;
75+
std::string value_preview;
76+
bool is_expandable{false}; // true for tables/userdata with metatable
77+
bool is_expanded{false};
78+
std::vector<LuaValueNode> children;
79+
int depth{0};
80+
std::string path; // Full path for fetching (e.g., "myTable.subTable.value")
81+
};
82+
83+
// Represents a breakpoint
84+
struct LuaBreakpoint
85+
{
86+
std::string source_file; // Source file path
87+
int line{0};
88+
bool enabled{true};
89+
std::string condition; // Optional condition expression
90+
int hit_count{0};
91+
};
92+
93+
// Represents a script file for viewing
94+
struct LuaScriptFile
95+
{
96+
std::string path;
97+
std::string content;
98+
std::vector<std::string> lines;
99+
bool loaded{false};
100+
};
101+
102+
// REPL history entry
103+
struct ReplHistoryEntry
104+
{
105+
std::string input;
106+
std::string output;
107+
bool is_error{false};
108+
std::chrono::system_clock::time_point timestamp;
109+
};
110+
111+
// Represents a captured Lua error with full context
112+
struct LuaErrorRecord
113+
{
114+
std::chrono::system_clock::time_point timestamp;
115+
std::string mod_name;
116+
std::string error_message;
117+
std::string traceback;
118+
std::vector<LuaCallStackEntry> call_stack;
119+
std::vector<LuaStackSlot> stack_snapshot;
120+
};
121+
122+
// Information about a tracked Lua state
123+
struct LuaStateInfo
124+
{
125+
lua_State* lua_state{nullptr};
126+
std::string mod_name;
127+
std::string state_type; // "main", "hook", "async"
128+
int current_stack_size{0};
129+
std::vector<LuaStackSlot> current_stack;
130+
bool is_active{true};
131+
};
132+
133+
class LuaDebugger
134+
{
135+
public:
136+
static constexpr size_t MAX_ERROR_HISTORY = 100;
137+
static constexpr size_t MAX_STACK_PREVIEW_LENGTH = 64;
138+
static constexpr size_t MAX_REPL_HISTORY = 100;
139+
static constexpr size_t MAX_TABLE_DEPTH = 10;
140+
141+
private:
142+
// All tracked Lua states
143+
std::unordered_map<lua_State*, LuaStateInfo> m_lua_states;
144+
mutable std::mutex m_states_mutex;
145+
146+
std::deque<LuaErrorRecord> m_error_history;
147+
mutable std::mutex m_errors_mutex;
148+
149+
// UI state
150+
bool m_auto_scroll_errors{true};
151+
int m_selected_error_index{-1};
152+
lua_State* m_selected_state{nullptr};
153+
bool m_show_stack_details{true};
154+
bool m_pause_on_error{false};
155+
std::string m_error_filter;
156+
157+
static LuaDebugger* s_instance;
158+
159+
// Breakpoints
160+
std::vector<LuaBreakpoint> m_breakpoints;
161+
mutable std::mutex m_breakpoints_mutex;
162+
std::atomic<bool> m_is_paused{false};
163+
std::atomic<bool> m_step_requested{false};
164+
std::atomic<bool> m_step_over_requested{false};
165+
std::atomic<bool> m_step_out_requested{false};
166+
std::atomic<bool> m_continue_requested{false};
167+
lua_State* m_paused_state{nullptr};
168+
int m_paused_line{0};
169+
std::string m_paused_source;
170+
std::condition_variable m_pause_cv;
171+
std::mutex m_pause_mutex;
172+
int m_step_start_depth{0};
173+
174+
// Cached paused state info (captured on Lua thread, read on GUI thread)
175+
// IMPORTANT: Never manipulate the Lua state from the GUI thread while paused!
176+
std::vector<LuaStackSlot> m_paused_stack_slots;
177+
std::vector<LuaCallStackEntry> m_paused_call_stack;
178+
std::vector<LuaStackFrame> m_paused_stack_frames; // Call stack with local variables
179+
mutable std::mutex m_paused_data_mutex;
180+
181+
// Track which states have debug hooks installed
182+
std::set<lua_State*> m_states_with_hooks;
183+
184+
// Script viewer (for Debug view)
185+
std::unordered_map<std::string, LuaScriptFile> m_script_cache;
186+
std::string m_current_script_path;
187+
int m_script_scroll_to_line{-1};
188+
mutable std::mutex m_scripts_mutex;
189+
190+
// Script editor state (for Script Editor tab)
191+
TextEditor m_script_editor;
192+
bool m_script_is_dirty{false};
193+
std::string m_script_edit_path;
194+
std::string m_script_original_content;
195+
196+
// Track which states had debug enabled (for auto-restore on reload)
197+
std::set<std::string> m_debug_enabled_mods;
198+
199+
// Cached loaded module paths per state (populated on Lua thread)
200+
std::unordered_map<lua_State*, std::vector<std::string>> m_loaded_modules_cache;
201+
mutable std::mutex m_loaded_modules_mutex;
202+
203+
// REPL
204+
std::string m_repl_input;
205+
std::deque<ReplHistoryEntry> m_repl_history;
206+
int m_repl_history_index{-1};
207+
std::vector<std::string> m_repl_input_history;
208+
mutable std::mutex m_repl_mutex;
209+
bool m_repl_pending{false};
210+
std::string m_repl_pending_result;
211+
bool m_repl_pending_is_error{false};
212+
213+
// Table inspection
214+
std::vector<LuaValueNode> m_globals_tree;
215+
std::set<std::string> m_expanded_paths;
216+
bool m_tree_refresh_requested{false};
217+
218+
// Stack frame expansion state (-1 = no change, 0 = collapse all, 1 = expand all)
219+
int m_stack_frames_expand_action{-1};
220+
221+
public:
222+
LuaDebugger();
223+
~LuaDebugger();
224+
225+
static auto get() -> LuaDebugger&;
226+
static auto has_instance() -> bool;
227+
228+
// State tracking
229+
auto register_lua_state(lua_State* L, const std::string& mod_name, const std::string& state_type) -> void;
230+
auto unregister_lua_state(lua_State* L) -> void;
231+
auto update_state_stack(lua_State* L) -> void;
232+
233+
// Error recording
234+
auto record_error(lua_State* L, const std::string& error_message, const std::string& traceback) -> void;
235+
auto clear_error_history() -> void;
236+
auto get_error_count() const -> size_t;
237+
238+
// Stack inspection utilities
239+
static auto get_stack_slots(lua_State* L) -> std::vector<LuaStackSlot>;
240+
static auto get_call_stack(lua_State* L) -> std::vector<LuaCallStackEntry>;
241+
static auto get_stack_frames_with_locals(lua_State* L) -> std::vector<LuaStackFrame>;
242+
static auto format_stack_value(lua_State* L, int index, size_t max_length = MAX_STACK_PREVIEW_LENGTH) -> std::string;
243+
static auto get_enhanced_traceback(lua_State* L, const std::string& message, int level = 0) -> std::string;
244+
static auto get_globals(lua_State* L, size_t max_entries = 100) -> std::vector<std::pair<std::string, LuaStackSlot>>;
245+
246+
// Breakpoint management
247+
auto add_breakpoint(const std::string& source, int line, const std::string& condition = "") -> void;
248+
auto remove_breakpoint(const std::string& source, int line) -> void;
249+
auto toggle_breakpoint(const std::string& source, int line) -> void;
250+
auto clear_all_breakpoints() -> void;
251+
auto has_breakpoint(const std::string& source, int line) const -> bool;
252+
auto is_paused() const -> bool { return m_is_paused.load(); }
253+
254+
// Debug control
255+
auto continue_execution() -> void;
256+
auto step_into() -> void;
257+
auto step_over() -> void;
258+
auto step_out() -> void;
259+
260+
// Debug hook (called from Lua)
261+
static auto debug_hook(lua_State* L, lua_Debug* ar) -> void;
262+
auto install_debug_hook(lua_State* L) -> void;
263+
auto uninstall_debug_hook(lua_State* L) -> void;
264+
auto has_debug_hook(lua_State* L) const -> bool;
265+
266+
// Script loading
267+
auto load_script(const std::string& path) -> const LuaScriptFile*;
268+
auto get_mod_scripts(lua_State* L) -> std::vector<std::string>;
269+
auto save_script(const std::string& path, const std::string& content) -> bool;
270+
auto reload_mod_for_state(lua_State* L) -> void;
271+
272+
// REPL
273+
auto execute_repl(lua_State* L, const std::string& code) -> void;
274+
275+
// Table inspection
276+
static auto get_table_children(lua_State* L, const std::string& path, int depth = 0) -> std::vector<LuaValueNode>;
277+
auto request_table_expand(const std::string& path) -> void;
278+
279+
// GUI rendering
280+
auto render() -> void;
281+
282+
private:
283+
auto render_state_list() -> void;
284+
auto render_stack_view() -> void;
285+
auto render_globals_view() -> void;
286+
auto render_error_log() -> void;
287+
auto render_error_details(const LuaErrorRecord& error) -> void;
288+
auto render_call_stack(const std::vector<LuaCallStackEntry>& call_stack) -> void;
289+
auto render_controls() -> void;
290+
auto render_debug_view() -> void;
291+
auto render_script_editor() -> void;
292+
auto render_mods_tab() -> void;
293+
auto render_breakpoints_panel() -> void;
294+
auto render_repl() -> void;
295+
auto render_value_tree(std::vector<LuaValueNode>& nodes) -> void;
296+
auto render_debug_controls() -> void;
297+
298+
auto find_mod_name_for_state(lua_State* L) const -> std::string;
299+
auto request_globals_refresh() -> void;
300+
auto request_loaded_modules_refresh() -> void;
301+
auto check_breakpoint(lua_State* L, lua_Debug* ar) -> bool;
302+
auto wait_for_continue() -> void;
303+
auto get_call_depth(lua_State* L) -> int;
304+
305+
// Filter for globals view
306+
std::string m_globals_filter;
307+
308+
// Cached globals (since we can't access Lua from GUI thread directly)
309+
std::vector<std::pair<std::string, LuaStackSlot>> m_cached_globals;
310+
lua_State* m_cached_globals_state{nullptr};
311+
bool m_globals_refresh_requested{false};
312+
mutable std::mutex m_globals_mutex;
313+
314+
// Tree view support for globals
315+
std::unordered_map<std::string, std::vector<std::pair<std::string, LuaStackSlot>>> m_globals_children_cache;
316+
std::set<std::string> m_pending_table_expansions;
317+
318+
// Helper for recursive tree rendering
319+
auto render_globals_tree_node(const std::vector<std::pair<std::string, LuaStackSlot>>& children, const std::string& parent_path, int depth) -> void;
320+
321+
// Helper to get table entries at a given path
322+
static auto get_table_entries_at_path(lua_State* L, const std::string& path) -> std::vector<std::pair<std::string, LuaStackSlot>>;
323+
324+
// Mod management
325+
struct ModInfo
326+
{
327+
std::string name;
328+
std::filesystem::path path;
329+
bool enabled_via_txt{false};
330+
bool enabled_via_mods_txt{false};
331+
bool is_running{false};
332+
333+
bool is_enabled() const { return enabled_via_txt || enabled_via_mods_txt; }
334+
};
335+
336+
std::vector<ModInfo> m_discovered_mods;
337+
bool m_mods_list_dirty{true};
338+
std::string m_new_mod_name;
339+
bool m_show_create_mod_popup{false};
340+
std::string m_new_file_name;
341+
bool m_show_create_file_popup{false};
342+
std::filesystem::path m_create_file_mod_path;
343+
int m_pending_editor_tab_switch{-1};
344+
345+
auto refresh_mods_list() -> void;
346+
auto create_new_mod(const std::string& name) -> bool;
347+
auto create_new_file(const std::string& mod_path, const std::string& filename) -> bool;
348+
auto set_mod_enabled(const std::filesystem::path& mod_path, bool enabled) -> void;
349+
};
350+
351+
} // namespace RC::GUI

UE4SS/include/Mod/Mod.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ namespace RC
4848

4949
public:
5050
auto get_name() const -> StringViewType;
51+
auto get_path() const -> const std::filesystem::path& { return m_mod_path; }
5152

5253
virtual auto start_mod() -> void = 0;
5354
virtual auto uninstall() -> void = 0;

UE4SS/include/UE4SSProgram.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ namespace RC
225225
RC_UE4SS_API auto get_working_directory() -> File::StringType;
226226
RC_UE4SS_API auto get_mods_directory() -> File::StringType;
227227
RC_UE4SS_API auto get_mods_directories() -> std::vector<std::filesystem::path>&;
228+
RC_UE4SS_API auto get_mods_txt_entries() -> std::unordered_map<std::string, bool>;
228229
[[nodiscard]] RC_UE4SS_API auto make_compatible_path(const std::filesystem::path&) const -> std::filesystem::path;
229230
RC_UE4SS_API auto insert_mods_directory(const std::filesystem::path&, int64_t index) -> void;
230231
RC_UE4SS_API auto add_mods_directory(const std::filesystem::path&) -> void;

0 commit comments

Comments
 (0)