@baixfeng/godot-mcp-cli 1.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +187 -0
- package/addons/godot_mcp/command_handler.gd +161 -0
- package/addons/godot_mcp/command_handler.gd.uid +1 -0
- package/addons/godot_mcp/commands/base_command_processor.gd +221 -0
- package/addons/godot_mcp/commands/base_command_processor.gd.uid +1 -0
- package/addons/godot_mcp/commands/debugger_commands.gd +221 -0
- package/addons/godot_mcp/commands/debugger_commands.gd.uid +1 -0
- package/addons/godot_mcp/commands/editor_commands.gd +237 -0
- package/addons/godot_mcp/commands/editor_commands.gd.uid +1 -0
- package/addons/godot_mcp/commands/editor_script_commands.gd +365 -0
- package/addons/godot_mcp/commands/editor_script_commands.gd.uid +1 -0
- package/addons/godot_mcp/commands/input_commands.gd +337 -0
- package/addons/godot_mcp/commands/input_commands.gd.uid +1 -0
- package/addons/godot_mcp/commands/node_commands.gd +222 -0
- package/addons/godot_mcp/commands/node_commands.gd.uid +1 -0
- package/addons/godot_mcp/commands/project_commands.gd +298 -0
- package/addons/godot_mcp/commands/project_commands.gd.uid +1 -0
- package/addons/godot_mcp/commands/scene_commands.gd +337 -0
- package/addons/godot_mcp/commands/scene_commands.gd.uid +1 -0
- package/addons/godot_mcp/commands/script_commands.gd +349 -0
- package/addons/godot_mcp/commands/script_commands.gd.uid +1 -0
- package/addons/godot_mcp/mcp_asset_commands.gd +153 -0
- package/addons/godot_mcp/mcp_asset_commands.gd.uid +1 -0
- package/addons/godot_mcp/mcp_debug_output_publisher.gd +1669 -0
- package/addons/godot_mcp/mcp_debug_output_publisher.gd.uid +1 -0
- package/addons/godot_mcp/mcp_debugger_bridge.gd +1455 -0
- package/addons/godot_mcp/mcp_debugger_bridge.gd.uid +1 -0
- package/addons/godot_mcp/mcp_enhanced_commands.gd +1083 -0
- package/addons/godot_mcp/mcp_enhanced_commands.gd.uid +1 -0
- package/addons/godot_mcp/mcp_input_handler.gd +545 -0
- package/addons/godot_mcp/mcp_input_handler.gd.uid +1 -0
- package/addons/godot_mcp/mcp_runtime_debugger_bridge.gd +464 -0
- package/addons/godot_mcp/mcp_runtime_debugger_bridge.gd.uid +1 -0
- package/addons/godot_mcp/mcp_script_resource_commands.gd +165 -0
- package/addons/godot_mcp/mcp_script_resource_commands.gd.uid +1 -0
- package/addons/godot_mcp/mcp_server.gd +260 -0
- package/addons/godot_mcp/mcp_server.gd.uid +1 -0
- package/addons/godot_mcp/plugin.cfg +7 -0
- package/addons/godot_mcp/runtime_debugger.gd +81 -0
- package/addons/godot_mcp/runtime_debugger.gd.uid +1 -0
- package/addons/godot_mcp/ui/mcp_panel.gd +94 -0
- package/addons/godot_mcp/ui/mcp_panel.gd.uid +1 -0
- package/addons/godot_mcp/ui/mcp_panel.tscn +96 -0
- package/addons/godot_mcp/utils/node_utils.gd +82 -0
- package/addons/godot_mcp/utils/node_utils.gd.uid +1 -0
- package/addons/godot_mcp/utils/resource_utils.gd +81 -0
- package/addons/godot_mcp/utils/resource_utils.gd.uid +1 -0
- package/addons/godot_mcp/utils/script_utils.gd +114 -0
- package/addons/godot_mcp/utils/script_utils.gd.uid +1 -0
- package/addons/godot_mcp/websocket_server.gd +197 -0
- package/addons/godot_mcp/websocket_server.gd.uid +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +561 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +156 -0
- package/dist/index.js.map +1 -0
- package/dist/resources/asset_resources.d.ts +29 -0
- package/dist/resources/asset_resources.js +145 -0
- package/dist/resources/asset_resources.js.map +1 -0
- package/dist/resources/debug_resources.d.ts +11 -0
- package/dist/resources/debug_resources.js +106 -0
- package/dist/resources/debug_resources.js.map +1 -0
- package/dist/resources/debugger_resources.d.ts +62 -0
- package/dist/resources/debugger_resources.js +201 -0
- package/dist/resources/debugger_resources.js.map +1 -0
- package/dist/resources/editor_resources.d.ts +47 -0
- package/dist/resources/editor_resources.js +155 -0
- package/dist/resources/editor_resources.js.map +1 -0
- package/dist/resources/project_resources.d.ts +33 -0
- package/dist/resources/project_resources.js +137 -0
- package/dist/resources/project_resources.js.map +1 -0
- package/dist/resources/scene_resources.d.ts +33 -0
- package/dist/resources/scene_resources.js +160 -0
- package/dist/resources/scene_resources.js.map +1 -0
- package/dist/resources/script_resources.d.ts +51 -0
- package/dist/resources/script_resources.js +203 -0
- package/dist/resources/script_resources.js.map +1 -0
- package/dist/tools/asset_tools.d.ts +5 -0
- package/dist/tools/asset_tools.js +125 -0
- package/dist/tools/asset_tools.js.map +1 -0
- package/dist/tools/debugger_tools.d.ts +2 -0
- package/dist/tools/debugger_tools.js +342 -0
- package/dist/tools/debugger_tools.js.map +1 -0
- package/dist/tools/editor_tools.d.ts +2 -0
- package/dist/tools/editor_tools.js +165 -0
- package/dist/tools/editor_tools.js.map +1 -0
- package/dist/tools/enhanced_tools.d.ts +5 -0
- package/dist/tools/enhanced_tools.js +706 -0
- package/dist/tools/enhanced_tools.js.map +1 -0
- package/dist/tools/input_tools.d.ts +2 -0
- package/dist/tools/input_tools.js +408 -0
- package/dist/tools/input_tools.js.map +1 -0
- package/dist/tools/node_tools.d.ts +5 -0
- package/dist/tools/node_tools.js +217 -0
- package/dist/tools/node_tools.js.map +1 -0
- package/dist/tools/project_tools.d.ts +5 -0
- package/dist/tools/project_tools.js +162 -0
- package/dist/tools/project_tools.js.map +1 -0
- package/dist/tools/scene_tools.d.ts +5 -0
- package/dist/tools/scene_tools.js +260 -0
- package/dist/tools/scene_tools.js.map +1 -0
- package/dist/tools/script_resource_tools.d.ts +5 -0
- package/dist/tools/script_resource_tools.js +5 -0
- package/dist/tools/script_resource_tools.js.map +1 -0
- package/dist/tools/script_tools.d.ts +5 -0
- package/dist/tools/script_tools.js +154 -0
- package/dist/tools/script_tools.js.map +1 -0
- package/dist/utils/godot_connection.d.ts +30 -0
- package/dist/utils/godot_connection.js +285 -0
- package/dist/utils/godot_connection.js.map +1 -0
- package/dist/utils/types.d.ts +16 -0
- package/dist/utils/types.js +2 -0
- package/dist/utils/types.js.map +1 -0
- package/package.json +58 -0
|
@@ -0,0 +1,1455 @@
|
|
|
1
|
+
@tool
|
|
2
|
+
class_name MCPDebuggerBridge
|
|
3
|
+
extends EditorDebuggerPlugin
|
|
4
|
+
|
|
5
|
+
# Signals for debugger events
|
|
6
|
+
signal breakpoint_hit(session_id: int, script_path: String, line: int, stack_info: Dictionary)
|
|
7
|
+
signal execution_paused(session_id: int, reason: String)
|
|
8
|
+
signal execution_resumed(session_id: int)
|
|
9
|
+
signal stack_frame_changed(session_id: int, frame_info: Dictionary)
|
|
10
|
+
signal breakpoint_set(session_id: int, script_path: String, line: int, success: bool)
|
|
11
|
+
signal breakpoint_removed(session_id: int, script_path: String, line: int, success: bool)
|
|
12
|
+
|
|
13
|
+
# Debugger state tracking
|
|
14
|
+
var _active_sessions: Dictionary = {}
|
|
15
|
+
var _breakpoints: Dictionary = {}
|
|
16
|
+
var _current_client_id: int = -1
|
|
17
|
+
var _websocket_server = null
|
|
18
|
+
var _session_breakpoints: Dictionary = {} # Track breakpoints per session
|
|
19
|
+
var _session_stack_cache: Dictionary = {}
|
|
20
|
+
|
|
21
|
+
# Constants for message throttling
|
|
22
|
+
const MAX_STACK_FRAMES: int = 50
|
|
23
|
+
const EVENT_THROTTLE_MS: int = 100
|
|
24
|
+
var _last_event_time: int = 0
|
|
25
|
+
const STACK_CAPTURE_NAMES := ["stack", "call_stack", "callstack", "stack_dump"]
|
|
26
|
+
const DEBUGGER_CAPTURE_NAMES := ["mcp_debugger", "breakpoint", "debugger"]
|
|
27
|
+
|
|
28
|
+
func _init():
|
|
29
|
+
_active_sessions.clear()
|
|
30
|
+
_breakpoints.clear()
|
|
31
|
+
_session_breakpoints.clear()
|
|
32
|
+
_session_stack_cache.clear()
|
|
33
|
+
|
|
34
|
+
func set_websocket_server(server):
|
|
35
|
+
_websocket_server = server
|
|
36
|
+
|
|
37
|
+
func set_client_id(client_id: int):
|
|
38
|
+
_current_client_id = client_id
|
|
39
|
+
|
|
40
|
+
func _normalize_capture_name(value: String) -> String:
|
|
41
|
+
return value.to_lower()
|
|
42
|
+
|
|
43
|
+
func _is_stack_capture_name(name: String) -> bool:
|
|
44
|
+
return name in STACK_CAPTURE_NAMES
|
|
45
|
+
|
|
46
|
+
func _is_debugger_capture_name(name: String) -> bool:
|
|
47
|
+
return name in DEBUGGER_CAPTURE_NAMES
|
|
48
|
+
|
|
49
|
+
func _has_capture(capture: String) -> bool:
|
|
50
|
+
# We handle multiple captures to integrate with Godot's debugger system
|
|
51
|
+
var capture_lc = _normalize_capture_name(capture)
|
|
52
|
+
return _is_debugger_capture_name(capture_lc) or _is_stack_capture_name(capture_lc)
|
|
53
|
+
|
|
54
|
+
func _build_stack_capture_payload(data: Array) -> Dictionary:
|
|
55
|
+
if not data.is_empty() and typeof(data[0]) == TYPE_DICTIONARY:
|
|
56
|
+
return data[0]
|
|
57
|
+
return {"stack": data}
|
|
58
|
+
|
|
59
|
+
func _capture(message: String, data: Array, session_id: int) -> bool:
|
|
60
|
+
var normalized_session_id = _normalize_session_id(session_id)
|
|
61
|
+
_trace("Debugger capture received: %s for session %s" % [message, normalized_session_id])
|
|
62
|
+
|
|
63
|
+
var message_lc = _normalize_capture_name(message)
|
|
64
|
+
if _is_stack_capture_name(message_lc):
|
|
65
|
+
var payload := _build_stack_capture_payload(data)
|
|
66
|
+
_handle_stack_dump(normalized_session_id, payload)
|
|
67
|
+
return true
|
|
68
|
+
|
|
69
|
+
# Handle different Godot debugger messages
|
|
70
|
+
match message_lc:
|
|
71
|
+
"breakpoint":
|
|
72
|
+
_handle_native_breakpoint(normalized_session_id, data)
|
|
73
|
+
"debugger":
|
|
74
|
+
_handle_debugger_message(normalized_session_id, data)
|
|
75
|
+
"mcp_debugger":
|
|
76
|
+
_handle_mcp_debugger_message(normalized_session_id, data)
|
|
77
|
+
_:
|
|
78
|
+
return false
|
|
79
|
+
|
|
80
|
+
return true
|
|
81
|
+
|
|
82
|
+
func _handle_native_breakpoint(session_id: int, data: Array) -> void:
|
|
83
|
+
# Handle native Godot breakpoint events
|
|
84
|
+
if data.size() > 0:
|
|
85
|
+
var breakpoint_data = data[0]
|
|
86
|
+
if typeof(breakpoint_data) == TYPE_DICTIONARY:
|
|
87
|
+
var script_path = breakpoint_data.get("script_path", "")
|
|
88
|
+
var line = breakpoint_data.get("line", -1)
|
|
89
|
+
var reason = breakpoint_data.get("reason", "breakpoint_hit")
|
|
90
|
+
|
|
91
|
+
_trace("Native breakpoint hit: %s:%d" % [script_path, line])
|
|
92
|
+
|
|
93
|
+
# Update session state
|
|
94
|
+
_ensure_session_state(session_id, true)
|
|
95
|
+
_active_sessions[session_id]["paused"] = true
|
|
96
|
+
_active_sessions[session_id]["current_script"] = script_path
|
|
97
|
+
_active_sessions[session_id]["current_line"] = line
|
|
98
|
+
|
|
99
|
+
# Get stack info
|
|
100
|
+
var stack_info = _get_session_stack_info(session_id)
|
|
101
|
+
|
|
102
|
+
# Send event to MCP client
|
|
103
|
+
_handle_breakpoint_hit(session_id, {
|
|
104
|
+
"script_path": script_path,
|
|
105
|
+
"line": line,
|
|
106
|
+
"stack_info": stack_info
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
func _handle_debugger_message(session_id: int, data: Array) -> void:
|
|
110
|
+
# Handle general debugger messages (pause, resume, etc.)
|
|
111
|
+
if data.size() > 0:
|
|
112
|
+
var message_data = data[0]
|
|
113
|
+
if typeof(message_data) == TYPE_DICTIONARY:
|
|
114
|
+
var event_type = message_data.get("type", "")
|
|
115
|
+
match event_type:
|
|
116
|
+
"paused":
|
|
117
|
+
_handle_execution_paused(session_id, message_data)
|
|
118
|
+
"resumed":
|
|
119
|
+
_handle_execution_resumed(session_id)
|
|
120
|
+
"stack_frame":
|
|
121
|
+
_handle_stack_frame_changed(session_id, message_data)
|
|
122
|
+
"stack_dump":
|
|
123
|
+
_handle_stack_dump(session_id, message_data)
|
|
124
|
+
|
|
125
|
+
func _handle_mcp_debugger_message(session_id: int, data: Array) -> void:
|
|
126
|
+
# Handle our custom MCP debugger messages
|
|
127
|
+
if data.is_empty():
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
var event_data = data[0]
|
|
131
|
+
if typeof(event_data) != TYPE_DICTIONARY:
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
var event_type = event_data.get("type", "")
|
|
135
|
+
match event_type:
|
|
136
|
+
"breakpoint_hit":
|
|
137
|
+
_handle_breakpoint_hit(session_id, event_data)
|
|
138
|
+
"execution_paused":
|
|
139
|
+
_handle_execution_paused(session_id, event_data)
|
|
140
|
+
"execution_resumed":
|
|
141
|
+
_handle_execution_resumed(session_id)
|
|
142
|
+
"stack_frame_changed":
|
|
143
|
+
_handle_stack_frame_changed(session_id, event_data)
|
|
144
|
+
_:
|
|
145
|
+
_trace("Unknown MCP debugger event type: %s" % event_type)
|
|
146
|
+
|
|
147
|
+
func _setup_session(session_id: int) -> void:
|
|
148
|
+
session_id = _normalize_session_id(session_id)
|
|
149
|
+
_trace("Setting up debugger session %s" % session_id)
|
|
150
|
+
|
|
151
|
+
var session = _get_session_instance(session_id)
|
|
152
|
+
if session:
|
|
153
|
+
# Diagnostic: Log available methods and properties
|
|
154
|
+
_diagnose_session_capabilities(session)
|
|
155
|
+
|
|
156
|
+
session.started.connect(_on_session_started.bind(session_id), CONNECT_DEFERRED)
|
|
157
|
+
session.stopped.connect(_on_session_stopped.bind(session_id), CONNECT_DEFERRED)
|
|
158
|
+
session.breaked.connect(_on_session_breaked.bind(session_id), CONNECT_DEFERRED)
|
|
159
|
+
|
|
160
|
+
if session.is_active():
|
|
161
|
+
_ensure_session_state(session_id, true)
|
|
162
|
+
# Restore any existing breakpoints for this session
|
|
163
|
+
_setup_session_breakpoints(session_id)
|
|
164
|
+
|
|
165
|
+
func _normalize_session_id(value) -> Variant:
|
|
166
|
+
var value_type := typeof(value)
|
|
167
|
+
if value_type == TYPE_INT:
|
|
168
|
+
return value
|
|
169
|
+
if value_type == TYPE_FLOAT:
|
|
170
|
+
return int(value)
|
|
171
|
+
if value_type == TYPE_STRING:
|
|
172
|
+
var text: String = value
|
|
173
|
+
if text.is_valid_int():
|
|
174
|
+
return int(text)
|
|
175
|
+
return value
|
|
176
|
+
|
|
177
|
+
# Session tracking helpers
|
|
178
|
+
func _is_empty_session_request(value) -> bool:
|
|
179
|
+
if value == null:
|
|
180
|
+
return true
|
|
181
|
+
var value_type := typeof(value)
|
|
182
|
+
match value_type:
|
|
183
|
+
TYPE_INT:
|
|
184
|
+
return value < 0
|
|
185
|
+
TYPE_FLOAT:
|
|
186
|
+
return int(value) < 0
|
|
187
|
+
TYPE_STRING:
|
|
188
|
+
return String(value).strip_edges().is_empty()
|
|
189
|
+
_:
|
|
190
|
+
return false
|
|
191
|
+
|
|
192
|
+
func _collect_session_candidate_ids(requested_session_id) -> Array:
|
|
193
|
+
var candidates: Array = []
|
|
194
|
+
var use_active_sessions := _is_empty_session_request(requested_session_id)
|
|
195
|
+
|
|
196
|
+
if not use_active_sessions:
|
|
197
|
+
candidates.append(requested_session_id)
|
|
198
|
+
|
|
199
|
+
if use_active_sessions:
|
|
200
|
+
var active_ids = _get_active_session_ids()
|
|
201
|
+
for session_id in active_ids:
|
|
202
|
+
if session_id not in candidates:
|
|
203
|
+
candidates.append(session_id)
|
|
204
|
+
|
|
205
|
+
return candidates
|
|
206
|
+
|
|
207
|
+
func _find_tracked_session_key(session_id) -> Variant:
|
|
208
|
+
if _active_sessions.has(session_id):
|
|
209
|
+
return session_id
|
|
210
|
+
var normalized_id = _normalize_session_id(session_id)
|
|
211
|
+
if _active_sessions.has(normalized_id):
|
|
212
|
+
return normalized_id
|
|
213
|
+
for key in _active_sessions.keys():
|
|
214
|
+
if _normalize_session_id(key) == normalized_id:
|
|
215
|
+
return key
|
|
216
|
+
return null
|
|
217
|
+
|
|
218
|
+
func _resolve_session_index(session_identifier) -> int:
|
|
219
|
+
var normalized = _normalize_session_id(session_identifier)
|
|
220
|
+
|
|
221
|
+
var sessions := get_sessions()
|
|
222
|
+
for i in range(sessions.size()):
|
|
223
|
+
var session = sessions[i]
|
|
224
|
+
if session == null:
|
|
225
|
+
continue
|
|
226
|
+
|
|
227
|
+
var reported_id = session.get_session_id() if session.has_method("get_session_id") else i
|
|
228
|
+
var reported_normalized = _normalize_session_id(reported_id)
|
|
229
|
+
if reported_normalized == normalized:
|
|
230
|
+
return i
|
|
231
|
+
if typeof(normalized) == TYPE_STRING:
|
|
232
|
+
var reported_text := str(reported_normalized)
|
|
233
|
+
if reported_text == normalized:
|
|
234
|
+
return i
|
|
235
|
+
|
|
236
|
+
if typeof(normalized) == TYPE_INT and normalized >= 0 and normalized < sessions.size():
|
|
237
|
+
return normalized
|
|
238
|
+
|
|
239
|
+
return -1
|
|
240
|
+
|
|
241
|
+
func _get_session_instance(session_identifier):
|
|
242
|
+
var session_index := _resolve_session_index(session_identifier)
|
|
243
|
+
if session_index < 0:
|
|
244
|
+
return null
|
|
245
|
+
return get_session(session_index)
|
|
246
|
+
|
|
247
|
+
func _move_dict_key(target: Dictionary, old_key, new_key) -> void:
|
|
248
|
+
if old_key == null or new_key == null or old_key == new_key:
|
|
249
|
+
return
|
|
250
|
+
if not target.has(old_key):
|
|
251
|
+
return
|
|
252
|
+
if target.has(new_key):
|
|
253
|
+
target.erase(old_key)
|
|
254
|
+
return
|
|
255
|
+
target[new_key] = target[old_key]
|
|
256
|
+
target.erase(old_key)
|
|
257
|
+
|
|
258
|
+
func _rekey_session_data(old_key, new_key) -> void:
|
|
259
|
+
if old_key == null or new_key == null or old_key == new_key:
|
|
260
|
+
return
|
|
261
|
+
_move_dict_key(_active_sessions, old_key, new_key)
|
|
262
|
+
_move_dict_key(_session_breakpoints, old_key, new_key)
|
|
263
|
+
_move_dict_key(_session_stack_cache, old_key, new_key)
|
|
264
|
+
|
|
265
|
+
func _sync_tracked_session(session_id) -> bool:
|
|
266
|
+
var normalized_id = _normalize_session_id(session_id)
|
|
267
|
+
if _active_sessions.has(normalized_id):
|
|
268
|
+
return true
|
|
269
|
+
|
|
270
|
+
var existing_key = _find_tracked_session_key(normalized_id)
|
|
271
|
+
if existing_key != null:
|
|
272
|
+
_rekey_session_data(existing_key, normalized_id)
|
|
273
|
+
return true
|
|
274
|
+
|
|
275
|
+
# Refresh session tracking in case the debugger state changed
|
|
276
|
+
_get_active_session_ids()
|
|
277
|
+
existing_key = _find_tracked_session_key(normalized_id)
|
|
278
|
+
if existing_key != null:
|
|
279
|
+
_rekey_session_data(existing_key, normalized_id)
|
|
280
|
+
return true
|
|
281
|
+
|
|
282
|
+
return false
|
|
283
|
+
|
|
284
|
+
func _ensure_session_state(session_id, mark_active: bool = false) -> void:
|
|
285
|
+
var normalized_id = _normalize_session_id(session_id)
|
|
286
|
+
var tracked_key = _find_tracked_session_key(normalized_id)
|
|
287
|
+
|
|
288
|
+
if tracked_key == null:
|
|
289
|
+
_active_sessions[normalized_id] = {
|
|
290
|
+
"active": mark_active,
|
|
291
|
+
"paused": false,
|
|
292
|
+
"current_script": "",
|
|
293
|
+
"current_line": -1,
|
|
294
|
+
"breakpoints": []
|
|
295
|
+
}
|
|
296
|
+
elif tracked_key != normalized_id:
|
|
297
|
+
_rekey_session_data(tracked_key, normalized_id)
|
|
298
|
+
|
|
299
|
+
if mark_active and _active_sessions.has(normalized_id):
|
|
300
|
+
_active_sessions[normalized_id]["active"] = true
|
|
301
|
+
|
|
302
|
+
func _track_local_breakpoint(script_path: String, line: int) -> void:
|
|
303
|
+
if not _breakpoints.has(script_path):
|
|
304
|
+
_breakpoints[script_path] = []
|
|
305
|
+
|
|
306
|
+
var script_breakpoints: Array = _breakpoints[script_path]
|
|
307
|
+
if line not in script_breakpoints:
|
|
308
|
+
script_breakpoints.append(line)
|
|
309
|
+
|
|
310
|
+
func _untrack_local_breakpoint(script_path: String, line: int) -> void:
|
|
311
|
+
if not _breakpoints.has(script_path):
|
|
312
|
+
return
|
|
313
|
+
|
|
314
|
+
var script_breakpoints: Array = _breakpoints[script_path]
|
|
315
|
+
script_breakpoints.erase(line)
|
|
316
|
+
if script_breakpoints.is_empty():
|
|
317
|
+
_breakpoints.erase(script_path)
|
|
318
|
+
|
|
319
|
+
func _ensure_session_breakpoint_storage(session_id) -> Dictionary:
|
|
320
|
+
var normalized_id = _normalize_session_id(session_id)
|
|
321
|
+
if not _session_breakpoints.has(normalized_id):
|
|
322
|
+
_session_breakpoints[normalized_id] = {}
|
|
323
|
+
|
|
324
|
+
return _session_breakpoints[normalized_id]
|
|
325
|
+
|
|
326
|
+
func _session_breakpoint_lines(session_id, script_path: String) -> Array:
|
|
327
|
+
var storage = _ensure_session_breakpoint_storage(session_id)
|
|
328
|
+
if not storage.has(script_path):
|
|
329
|
+
storage[script_path] = []
|
|
330
|
+
|
|
331
|
+
return storage[script_path]
|
|
332
|
+
|
|
333
|
+
func _get_primary_session_info() -> Dictionary:
|
|
334
|
+
var active_sessions = _get_active_session_ids()
|
|
335
|
+
if active_sessions.is_empty():
|
|
336
|
+
return {}
|
|
337
|
+
|
|
338
|
+
var session_id = active_sessions[0]
|
|
339
|
+
var session = _get_session_instance(session_id)
|
|
340
|
+
if not session or not session.is_active():
|
|
341
|
+
return {"error": "Debugger session not active"}
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
"id": session_id,
|
|
345
|
+
"session": session
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
func _with_primary_session(error_message: String, action: Callable) -> Dictionary:
|
|
349
|
+
var info = _get_primary_session_info()
|
|
350
|
+
if info.is_empty():
|
|
351
|
+
return {"success": false, "message": error_message}
|
|
352
|
+
|
|
353
|
+
if info.has("error"):
|
|
354
|
+
return {"success": false, "message": info["error"]}
|
|
355
|
+
|
|
356
|
+
return action.call(info["id"], info["session"])
|
|
357
|
+
|
|
358
|
+
func _active_session_objects() -> Array:
|
|
359
|
+
var sessions: Array = []
|
|
360
|
+
for session_id in _get_active_session_ids():
|
|
361
|
+
var session = _get_session_instance(session_id)
|
|
362
|
+
if session and session.is_active():
|
|
363
|
+
sessions.append({
|
|
364
|
+
"id": session_id,
|
|
365
|
+
"session": session
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
return sessions
|
|
369
|
+
|
|
370
|
+
func _add_breakpoint_source(target: Dictionary, sources: Dictionary, source_name: String, breakpoint_map: Dictionary) -> void:
|
|
371
|
+
if breakpoint_map.is_empty():
|
|
372
|
+
return
|
|
373
|
+
|
|
374
|
+
_merge_breakpoint_map(target, breakpoint_map)
|
|
375
|
+
sources[source_name] = breakpoint_map
|
|
376
|
+
|
|
377
|
+
func _send_session_command(command: String, trace_label: String) -> Dictionary:
|
|
378
|
+
return _with_primary_session("No active debugger session", func(session_id: int, session):
|
|
379
|
+
if not session or not session.has_method("send_message"):
|
|
380
|
+
return {"success": false, "message": "Debugger session not active"}
|
|
381
|
+
|
|
382
|
+
session.send_message(command, [])
|
|
383
|
+
_trace(trace_label)
|
|
384
|
+
return {"success": true, "session_id": session_id}
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
# Breakpoint management
|
|
388
|
+
func set_breakpoint(script_path: String, line: int) -> Dictionary:
|
|
389
|
+
_track_local_breakpoint(script_path, line)
|
|
390
|
+
|
|
391
|
+
return _with_primary_session(
|
|
392
|
+
"No active debugger session. Start the project with debugging first.",
|
|
393
|
+
func(session_id: int, session):
|
|
394
|
+
var session_lines: Array = _session_breakpoint_lines(session_id, script_path)
|
|
395
|
+
if line not in session_lines:
|
|
396
|
+
session_lines.append(line)
|
|
397
|
+
|
|
398
|
+
var success = _set_native_breakpoint(session, script_path, line)
|
|
399
|
+
if success:
|
|
400
|
+
breakpoint_set.emit(session_id, script_path, line, true)
|
|
401
|
+
_trace("Breakpoint set successfully: %s:%d" % [script_path, line])
|
|
402
|
+
|
|
403
|
+
return {
|
|
404
|
+
"success": true,
|
|
405
|
+
"session_id": session_id,
|
|
406
|
+
"script_path": script_path,
|
|
407
|
+
"line": line
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return {
|
|
411
|
+
"success": false,
|
|
412
|
+
"message": "Failed to set breakpoint in Godot's debugger"
|
|
413
|
+
}
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
func remove_breakpoint(script_path: String, line: int) -> Dictionary:
|
|
418
|
+
_untrack_local_breakpoint(script_path, line)
|
|
419
|
+
|
|
420
|
+
return _with_primary_session("No active debugger session", func(session_id: int, session):
|
|
421
|
+
if _session_breakpoints.has(session_id) and _session_breakpoints[session_id].has(script_path):
|
|
422
|
+
_session_breakpoints[session_id][script_path].erase(line)
|
|
423
|
+
if _session_breakpoints[session_id][script_path].is_empty():
|
|
424
|
+
_session_breakpoints[session_id].erase(script_path)
|
|
425
|
+
|
|
426
|
+
var success = _remove_native_breakpoint(session, script_path, line)
|
|
427
|
+
if success:
|
|
428
|
+
breakpoint_removed.emit(session_id, script_path, line, true)
|
|
429
|
+
_trace("Breakpoint removed successfully: %s:%d" % [script_path, line])
|
|
430
|
+
|
|
431
|
+
return {"success": true, "session_id": session_id}
|
|
432
|
+
|
|
433
|
+
return {
|
|
434
|
+
"success": false,
|
|
435
|
+
"message": "Failed to remove breakpoint in Godot's debugger"
|
|
436
|
+
}
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
func get_breakpoints() -> Dictionary:
|
|
441
|
+
var aggregated: Dictionary = {}
|
|
442
|
+
var sources: Dictionary = {}
|
|
443
|
+
|
|
444
|
+
_add_breakpoint_source(aggregated, sources, "mcp_tracked", _breakpoints.duplicate(true))
|
|
445
|
+
_add_breakpoint_source(aggregated, sources, "session_tracked", _collect_tracked_session_breakpoints())
|
|
446
|
+
_add_breakpoint_source(aggregated, sources, "session_reported", _collect_active_session_breakpoints())
|
|
447
|
+
_add_breakpoint_source(aggregated, sources, "editor", _collect_editor_breakpoints())
|
|
448
|
+
|
|
449
|
+
return {
|
|
450
|
+
"breakpoints": aggregated,
|
|
451
|
+
"sources": sources
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
func clear_all_breakpoints() -> Dictionary:
|
|
455
|
+
var old_breakpoints = _breakpoints.duplicate(true)
|
|
456
|
+
var old_session_breakpoints = _session_breakpoints.duplicate(true)
|
|
457
|
+
|
|
458
|
+
# Clear local tracking dictionaries
|
|
459
|
+
_breakpoints.clear()
|
|
460
|
+
_session_breakpoints.clear()
|
|
461
|
+
|
|
462
|
+
# Actually remove breakpoints from Godot's debugger system
|
|
463
|
+
var cleared_count = 0
|
|
464
|
+
|
|
465
|
+
for session_info in _active_session_objects():
|
|
466
|
+
var session_id: int = session_info["id"]
|
|
467
|
+
var session = session_info["session"]
|
|
468
|
+
|
|
469
|
+
# Clear breakpoints from this session using native methods
|
|
470
|
+
if old_session_breakpoints.has(session_id):
|
|
471
|
+
var session_bps = old_session_breakpoints[session_id]
|
|
472
|
+
for script_path in session_bps:
|
|
473
|
+
for line in session_bps[script_path]:
|
|
474
|
+
var success = _remove_native_breakpoint(session, script_path, line)
|
|
475
|
+
if success:
|
|
476
|
+
cleared_count += 1
|
|
477
|
+
_trace("Cleared breakpoint %s:%d from session %s" % [script_path, line, session_id])
|
|
478
|
+
|
|
479
|
+
# Also try to clear any remaining breakpoints using direct methods
|
|
480
|
+
_clear_all_native_breakpoints(session)
|
|
481
|
+
|
|
482
|
+
return {
|
|
483
|
+
"success": true,
|
|
484
|
+
"cleared_breakpoints": old_breakpoints,
|
|
485
|
+
"cleared_count": cleared_count
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
# Execution control
|
|
489
|
+
func pause_execution() -> Dictionary:
|
|
490
|
+
return _send_session_command("break", "Sent break command to debugger")
|
|
491
|
+
|
|
492
|
+
func resume_execution() -> Dictionary:
|
|
493
|
+
return _send_session_command("continue", "Sent continue command to debugger")
|
|
494
|
+
|
|
495
|
+
func step_over() -> Dictionary:
|
|
496
|
+
return _send_session_command("next", "Sent next (step over) command to debugger")
|
|
497
|
+
|
|
498
|
+
func step_into() -> Dictionary:
|
|
499
|
+
return _send_session_command("step", "Sent step (step into) command to debugger")
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
# Call stack and inspection
|
|
503
|
+
func get_call_stack(session_id = null, timeout_ms: int = 750) -> Dictionary:
|
|
504
|
+
var candidate_ids := _collect_session_candidate_ids(session_id)
|
|
505
|
+
if candidate_ids.is_empty():
|
|
506
|
+
return {
|
|
507
|
+
"error": "session_not_found",
|
|
508
|
+
"message": "No active debugger session available"
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
var last_error: Dictionary = {}
|
|
512
|
+
for candidate in candidate_ids:
|
|
513
|
+
var normalized_id = _normalize_session_id(candidate)
|
|
514
|
+
if not _sync_tracked_session(normalized_id):
|
|
515
|
+
if _resolve_session_index(normalized_id) < 0:
|
|
516
|
+
last_error = {
|
|
517
|
+
"error": "session_not_found",
|
|
518
|
+
"session_id": normalized_id
|
|
519
|
+
}
|
|
520
|
+
continue
|
|
521
|
+
_ensure_session_state(normalized_id, true)
|
|
522
|
+
|
|
523
|
+
var session = _get_session_instance(normalized_id)
|
|
524
|
+
if session == null or not session.is_active():
|
|
525
|
+
last_error = {
|
|
526
|
+
"error": "session_not_active",
|
|
527
|
+
"session_id": normalized_id
|
|
528
|
+
}
|
|
529
|
+
continue
|
|
530
|
+
|
|
531
|
+
var request_timestamp := Time.get_ticks_msec()
|
|
532
|
+
session.send_message("get_stack_dump", [])
|
|
533
|
+
|
|
534
|
+
var stack_info := await _wait_for_stack_dump(normalized_id, request_timestamp, timeout_ms)
|
|
535
|
+
if stack_info.has("error"):
|
|
536
|
+
last_error = stack_info
|
|
537
|
+
continue
|
|
538
|
+
|
|
539
|
+
return stack_info
|
|
540
|
+
|
|
541
|
+
if last_error.is_empty():
|
|
542
|
+
return {
|
|
543
|
+
"error": "session_not_found",
|
|
544
|
+
"message": "No debugger session responded"
|
|
545
|
+
}
|
|
546
|
+
return last_error
|
|
547
|
+
|
|
548
|
+
func get_cached_stack_info(session_id) -> Dictionary:
|
|
549
|
+
var normalized_id = _normalize_session_id(session_id)
|
|
550
|
+
if typeof(normalized_id) == TYPE_INT and normalized_id < 0:
|
|
551
|
+
return {"error": "invalid_session"}
|
|
552
|
+
if _session_stack_cache.has(normalized_id):
|
|
553
|
+
return _session_stack_cache[normalized_id].duplicate(true)
|
|
554
|
+
var stack_info := _update_session_stack_cache(normalized_id)
|
|
555
|
+
if stack_info.has("error"):
|
|
556
|
+
return stack_info
|
|
557
|
+
return stack_info.duplicate(true)
|
|
558
|
+
|
|
559
|
+
func get_current_state() -> Dictionary:
|
|
560
|
+
var active_sessions = _get_active_session_ids()
|
|
561
|
+
var diagnostics := _collect_session_diagnostics(active_sessions)
|
|
562
|
+
if active_sessions.is_empty():
|
|
563
|
+
return {
|
|
564
|
+
"active_sessions": [],
|
|
565
|
+
"total_breakpoints": 0,
|
|
566
|
+
"debugger_active": false,
|
|
567
|
+
"diagnostics": diagnostics
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
var session_id = active_sessions[0]
|
|
571
|
+
var state = _active_sessions.get(session_id, {})
|
|
572
|
+
|
|
573
|
+
return {
|
|
574
|
+
"active_sessions": active_sessions,
|
|
575
|
+
"current_session_id": session_id,
|
|
576
|
+
"total_breakpoints": _count_total_breakpoints(),
|
|
577
|
+
"debugger_active": true,
|
|
578
|
+
"paused": state.get("paused", false),
|
|
579
|
+
"current_script": state.get("current_script", ""),
|
|
580
|
+
"current_line": state.get("current_line", -1),
|
|
581
|
+
"breakpoints": _breakpoints.duplicate(true),
|
|
582
|
+
"diagnostics": diagnostics
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
# Private methods
|
|
586
|
+
# All handlers below assume session_id has already been normalized.
|
|
587
|
+
func _handle_breakpoint_hit(session_id: int, event_data: Dictionary) -> void:
|
|
588
|
+
var script_path = event_data.get("script_path", "")
|
|
589
|
+
var line = event_data.get("line", -1)
|
|
590
|
+
var stack_info = event_data.get("stack_info", {})
|
|
591
|
+
var sanitized_stack := _update_session_stack_cache(session_id, stack_info)
|
|
592
|
+
|
|
593
|
+
# Update session state
|
|
594
|
+
_ensure_session_state(session_id, true)
|
|
595
|
+
_active_sessions[session_id]["paused"] = true
|
|
596
|
+
_active_sessions[session_id]["current_script"] = script_path
|
|
597
|
+
_active_sessions[session_id]["current_line"] = line
|
|
598
|
+
_session_stack_cache[session_id] = sanitized_stack.duplicate(true)
|
|
599
|
+
|
|
600
|
+
# Throttle events if needed
|
|
601
|
+
if not _should_send_event():
|
|
602
|
+
return
|
|
603
|
+
|
|
604
|
+
# Send to MCP client
|
|
605
|
+
_send_debugger_event("breakpoint_hit", {
|
|
606
|
+
"session_id": session_id,
|
|
607
|
+
"script_path": script_path,
|
|
608
|
+
"line": line,
|
|
609
|
+
"stack_info": sanitized_stack
|
|
610
|
+
})
|
|
611
|
+
|
|
612
|
+
breakpoint_hit.emit(session_id, script_path, line, stack_info)
|
|
613
|
+
|
|
614
|
+
func _handle_execution_paused(session_id: int, event_data: Dictionary) -> void:
|
|
615
|
+
var reason = event_data.get("reason", "unknown")
|
|
616
|
+
|
|
617
|
+
_ensure_session_state(session_id, true)
|
|
618
|
+
_active_sessions[session_id]["paused"] = true
|
|
619
|
+
_update_session_stack_cache(session_id)
|
|
620
|
+
|
|
621
|
+
if not _should_send_event():
|
|
622
|
+
return
|
|
623
|
+
|
|
624
|
+
_send_debugger_event("execution_paused", {
|
|
625
|
+
"session_id": session_id,
|
|
626
|
+
"reason": reason
|
|
627
|
+
})
|
|
628
|
+
|
|
629
|
+
execution_paused.emit(session_id, reason)
|
|
630
|
+
|
|
631
|
+
func _handle_execution_resumed(session_id: int) -> void:
|
|
632
|
+
_ensure_session_state(session_id, true)
|
|
633
|
+
_active_sessions[session_id]["paused"] = false
|
|
634
|
+
_active_sessions[session_id]["current_script"] = ""
|
|
635
|
+
_active_sessions[session_id]["current_line"] = -1
|
|
636
|
+
_session_stack_cache.erase(session_id)
|
|
637
|
+
|
|
638
|
+
if not _should_send_event():
|
|
639
|
+
return
|
|
640
|
+
|
|
641
|
+
_send_debugger_event("execution_resumed", {
|
|
642
|
+
"session_id": session_id
|
|
643
|
+
})
|
|
644
|
+
|
|
645
|
+
execution_resumed.emit(session_id)
|
|
646
|
+
|
|
647
|
+
func _handle_stack_frame_changed(session_id: int, event_data: Dictionary) -> void:
|
|
648
|
+
var frame_info = event_data.get("frame_info", {})
|
|
649
|
+
|
|
650
|
+
if not _should_send_event():
|
|
651
|
+
return
|
|
652
|
+
|
|
653
|
+
_send_debugger_event("stack_frame_changed", {
|
|
654
|
+
"session_id": session_id,
|
|
655
|
+
"frame_info": frame_info
|
|
656
|
+
})
|
|
657
|
+
|
|
658
|
+
stack_frame_changed.emit(session_id, frame_info)
|
|
659
|
+
|
|
660
|
+
func _handle_stack_dump(session_id: int, event_data: Dictionary) -> void:
|
|
661
|
+
var raw_frames = event_data.get("stack", event_data.get("frames", event_data.get("dump", [])))
|
|
662
|
+
var structured_frames: Array = []
|
|
663
|
+
|
|
664
|
+
if raw_frames is Array:
|
|
665
|
+
for frame in raw_frames:
|
|
666
|
+
if typeof(frame) == TYPE_DICTIONARY:
|
|
667
|
+
structured_frames.append(frame.duplicate(true))
|
|
668
|
+
elif frame is Array and frame.size() >= 3:
|
|
669
|
+
var script_path = String(frame[0])
|
|
670
|
+
var line_number = int(frame[1])
|
|
671
|
+
var function_name = String(frame[2])
|
|
672
|
+
structured_frames.append({
|
|
673
|
+
"script": script_path,
|
|
674
|
+
"file": script_path,
|
|
675
|
+
"line": line_number,
|
|
676
|
+
"function": function_name
|
|
677
|
+
})
|
|
678
|
+
else:
|
|
679
|
+
structured_frames.append({"raw": frame})
|
|
680
|
+
|
|
681
|
+
var stack_info := {
|
|
682
|
+
"frames": structured_frames,
|
|
683
|
+
"total_frames": structured_frames.size(),
|
|
684
|
+
"current_frame": event_data.get("current_frame", 0)
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if event_data.has("current_script"):
|
|
688
|
+
stack_info["current_script"] = event_data["current_script"]
|
|
689
|
+
if event_data.has("current_line"):
|
|
690
|
+
stack_info["current_line"] = event_data["current_line"]
|
|
691
|
+
|
|
692
|
+
_update_session_stack_cache(session_id, stack_info)
|
|
693
|
+
|
|
694
|
+
func _send_debugger_event(event_type: String, data: Dictionary) -> void:
|
|
695
|
+
if _websocket_server and _current_client_id >= 0:
|
|
696
|
+
var response = {
|
|
697
|
+
"status": "event",
|
|
698
|
+
"type": "debugger_event",
|
|
699
|
+
"event": event_type,
|
|
700
|
+
"data": data,
|
|
701
|
+
"timestamp": Time.get_ticks_msec()
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
_websocket_server.send_response(_current_client_id, response)
|
|
705
|
+
_trace("Sent debugger event: %s" % event_type)
|
|
706
|
+
|
|
707
|
+
func _should_send_event() -> bool:
|
|
708
|
+
var current_time = Time.get_ticks_msec()
|
|
709
|
+
if current_time - _last_event_time < EVENT_THROTTLE_MS:
|
|
710
|
+
return false
|
|
711
|
+
|
|
712
|
+
_last_event_time = current_time
|
|
713
|
+
return true
|
|
714
|
+
|
|
715
|
+
func _sanitize_stack_info(stack_info: Dictionary) -> Dictionary:
|
|
716
|
+
# Limit stack frames to prevent overwhelming the client
|
|
717
|
+
var frames = stack_info.get("frames", [])
|
|
718
|
+
if frames.size() > MAX_STACK_FRAMES:
|
|
719
|
+
stack_info["frames"] = frames.slice(0, MAX_STACK_FRAMES)
|
|
720
|
+
stack_info["truncated"] = true
|
|
721
|
+
stack_info["total_frames"] = frames.size()
|
|
722
|
+
|
|
723
|
+
return stack_info
|
|
724
|
+
|
|
725
|
+
func _update_session_stack_cache(session_id, stack_info = null) -> Dictionary:
|
|
726
|
+
session_id = _normalize_session_id(session_id)
|
|
727
|
+
var info: Dictionary
|
|
728
|
+
if typeof(stack_info) == TYPE_DICTIONARY:
|
|
729
|
+
info = stack_info.duplicate(true)
|
|
730
|
+
else:
|
|
731
|
+
info = _get_session_stack_info(session_id)
|
|
732
|
+
if typeof(info) != TYPE_DICTIONARY:
|
|
733
|
+
info = {}
|
|
734
|
+
if info.has("error"):
|
|
735
|
+
return info
|
|
736
|
+
var sanitized := _sanitize_stack_info(info)
|
|
737
|
+
sanitized["timestamp"] = Time.get_ticks_msec()
|
|
738
|
+
sanitized["session_id"] = session_id
|
|
739
|
+
_session_stack_cache[session_id] = sanitized.duplicate(true)
|
|
740
|
+
return sanitized
|
|
741
|
+
|
|
742
|
+
func _wait_for_stack_dump(session_id, request_timestamp: int, timeout_ms: int) -> Dictionary:
|
|
743
|
+
session_id = _normalize_session_id(session_id)
|
|
744
|
+
var cached := _duplicate_stack_cache(session_id)
|
|
745
|
+
if _stack_info_ready(cached, request_timestamp):
|
|
746
|
+
return cached
|
|
747
|
+
|
|
748
|
+
var loop := Engine.get_main_loop()
|
|
749
|
+
if loop == null or not (loop is SceneTree):
|
|
750
|
+
return {
|
|
751
|
+
"error": "stack_dump_unavailable",
|
|
752
|
+
"session_id": session_id,
|
|
753
|
+
"message": "Editor tree unavailable while waiting for stack dump."
|
|
754
|
+
}
|
|
755
|
+
var tree: SceneTree = loop
|
|
756
|
+
|
|
757
|
+
var deadline := Time.get_ticks_msec() + timeout_ms
|
|
758
|
+
while Time.get_ticks_msec() <= deadline:
|
|
759
|
+
await tree.process_frame
|
|
760
|
+
cached = _duplicate_stack_cache(session_id)
|
|
761
|
+
if _stack_info_ready(cached, request_timestamp):
|
|
762
|
+
return cached
|
|
763
|
+
|
|
764
|
+
if not cached.is_empty():
|
|
765
|
+
if cached.get("frames", []).is_empty():
|
|
766
|
+
var fallback := _build_frames_from_debug_output()
|
|
767
|
+
if not fallback.is_empty():
|
|
768
|
+
fallback["timestamp"] = Time.get_ticks_msec()
|
|
769
|
+
fallback["session_id"] = session_id
|
|
770
|
+
fallback["warning"] = "stack_dump_timeout"
|
|
771
|
+
_session_stack_cache[session_id] = fallback.duplicate(true)
|
|
772
|
+
return fallback
|
|
773
|
+
return cached
|
|
774
|
+
|
|
775
|
+
var fallback_result := _build_frames_from_debug_output()
|
|
776
|
+
if not fallback_result.is_empty():
|
|
777
|
+
fallback_result["timestamp"] = Time.get_ticks_msec()
|
|
778
|
+
fallback_result["session_id"] = session_id
|
|
779
|
+
fallback_result["warning"] = "stack_dump_timeout"
|
|
780
|
+
_session_stack_cache[session_id] = fallback_result.duplicate(true)
|
|
781
|
+
return fallback_result
|
|
782
|
+
|
|
783
|
+
return {
|
|
784
|
+
"error": "stack_dump_timeout",
|
|
785
|
+
"session_id": session_id,
|
|
786
|
+
"message": "Timed out waiting for debugger stack dump."
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
func _duplicate_stack_cache(session_id) -> Dictionary:
|
|
790
|
+
session_id = _normalize_session_id(session_id)
|
|
791
|
+
if not _session_stack_cache.has(session_id):
|
|
792
|
+
return {}
|
|
793
|
+
var cached = _session_stack_cache[session_id]
|
|
794
|
+
if typeof(cached) != TYPE_DICTIONARY:
|
|
795
|
+
return {}
|
|
796
|
+
return cached.duplicate(true)
|
|
797
|
+
|
|
798
|
+
func _stack_info_ready(info: Dictionary, request_timestamp: int) -> bool:
|
|
799
|
+
if info.is_empty():
|
|
800
|
+
return false
|
|
801
|
+
if info.has("error"):
|
|
802
|
+
return true
|
|
803
|
+
if not info.has("frames"):
|
|
804
|
+
return false
|
|
805
|
+
var frames = info.get("frames", [])
|
|
806
|
+
if typeof(frames) != TYPE_ARRAY:
|
|
807
|
+
return false
|
|
808
|
+
var info_timestamp := int(info.get("timestamp", 0))
|
|
809
|
+
if info_timestamp < request_timestamp:
|
|
810
|
+
return false
|
|
811
|
+
return true
|
|
812
|
+
|
|
813
|
+
func _count_total_breakpoints() -> int:
|
|
814
|
+
var count = 0
|
|
815
|
+
for script_breakpoints in _breakpoints.values():
|
|
816
|
+
count += script_breakpoints.size()
|
|
817
|
+
return count
|
|
818
|
+
|
|
819
|
+
func _collect_session_diagnostics(active_sessions: Array) -> Dictionary:
|
|
820
|
+
var diagnostics := {
|
|
821
|
+
"active_session_ids": active_sessions.duplicate(),
|
|
822
|
+
"tracked_sessions": _active_sessions.keys(),
|
|
823
|
+
"godot_session_objects": [],
|
|
824
|
+
"godot_session_count": 0
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
var sessions := get_sessions()
|
|
828
|
+
diagnostics["godot_session_count"] = sessions.size()
|
|
829
|
+
|
|
830
|
+
for i in range(sessions.size()):
|
|
831
|
+
var session = sessions[i]
|
|
832
|
+
if not session:
|
|
833
|
+
continue
|
|
834
|
+
|
|
835
|
+
var session_id := i
|
|
836
|
+
if session.has_method("get_session_id"):
|
|
837
|
+
session_id = session.get_session_id()
|
|
838
|
+
|
|
839
|
+
var session_info := {
|
|
840
|
+
"id": session_id,
|
|
841
|
+
"has_session": true,
|
|
842
|
+
"active": session.has_method("is_active") and session.is_active(),
|
|
843
|
+
"breaked": session.has_method("is_breaked") and session.is_breaked()
|
|
844
|
+
}
|
|
845
|
+
diagnostics["godot_session_objects"].append(session_info)
|
|
846
|
+
return diagnostics
|
|
847
|
+
|
|
848
|
+
func _collect_tracked_session_breakpoints() -> Dictionary:
|
|
849
|
+
var collected: Dictionary = {}
|
|
850
|
+
|
|
851
|
+
for session_id in _session_breakpoints.keys():
|
|
852
|
+
var session_breakpoints = _session_breakpoints[session_id]
|
|
853
|
+
if typeof(session_breakpoints) != TYPE_DICTIONARY:
|
|
854
|
+
continue
|
|
855
|
+
_merge_breakpoint_map(collected, session_breakpoints)
|
|
856
|
+
|
|
857
|
+
return collected
|
|
858
|
+
|
|
859
|
+
func _collect_active_session_breakpoints() -> Dictionary:
|
|
860
|
+
var collected: Dictionary = {}
|
|
861
|
+
var sessions := get_sessions()
|
|
862
|
+
|
|
863
|
+
for i in range(sessions.size()):
|
|
864
|
+
var session = sessions[i]
|
|
865
|
+
if not session or not session.has_method("is_active") or not session.is_active():
|
|
866
|
+
continue
|
|
867
|
+
|
|
868
|
+
var session_breakpoints = null
|
|
869
|
+
|
|
870
|
+
if session.has_method("get_breakpoints"):
|
|
871
|
+
session_breakpoints = session.get_breakpoints()
|
|
872
|
+
elif "breakpoints" in session and session.breakpoints and session.breakpoints.has_method("get_breakpoints"):
|
|
873
|
+
session_breakpoints = session.breakpoints.get_breakpoints()
|
|
874
|
+
|
|
875
|
+
if session_breakpoints != null:
|
|
876
|
+
var normalized := _convert_breakpoint_payload(session_breakpoints)
|
|
877
|
+
if not normalized.is_empty():
|
|
878
|
+
_merge_breakpoint_map(collected, normalized)
|
|
879
|
+
|
|
880
|
+
return collected
|
|
881
|
+
|
|
882
|
+
func _collect_editor_breakpoints() -> Dictionary:
|
|
883
|
+
if not Engine.has_singleton("EditorInterface"):
|
|
884
|
+
return {}
|
|
885
|
+
|
|
886
|
+
var editor_interface = Engine.get_singleton("EditorInterface")
|
|
887
|
+
if not editor_interface or not editor_interface.has_method("get_script_editor"):
|
|
888
|
+
return {}
|
|
889
|
+
|
|
890
|
+
var script_editor = editor_interface.get_script_editor()
|
|
891
|
+
if not script_editor or not script_editor.has_method("get_breakpoints"):
|
|
892
|
+
return {}
|
|
893
|
+
|
|
894
|
+
var editor_breakpoints = script_editor.get_breakpoints()
|
|
895
|
+
var normalized := _convert_breakpoint_payload(editor_breakpoints)
|
|
896
|
+
|
|
897
|
+
return normalized
|
|
898
|
+
|
|
899
|
+
func _convert_breakpoint_payload(payload) -> Dictionary:
|
|
900
|
+
var result: Dictionary = {}
|
|
901
|
+
if payload == null:
|
|
902
|
+
return result
|
|
903
|
+
|
|
904
|
+
match typeof(payload):
|
|
905
|
+
TYPE_DICTIONARY:
|
|
906
|
+
for script_path in payload.keys():
|
|
907
|
+
var line_values = payload[script_path]
|
|
908
|
+
var normalized_lines := _normalize_breakpoint_lines(line_values)
|
|
909
|
+
if normalized_lines.is_empty():
|
|
910
|
+
continue
|
|
911
|
+
result[script_path] = normalized_lines
|
|
912
|
+
TYPE_ARRAY, TYPE_PACKED_STRING_ARRAY:
|
|
913
|
+
for entry in payload:
|
|
914
|
+
var parsed := _parse_breakpoint_entry(entry)
|
|
915
|
+
if parsed.is_empty():
|
|
916
|
+
continue
|
|
917
|
+
var script_path: String = parsed.get("script_path", "")
|
|
918
|
+
var line: int = parsed.get("line", -1)
|
|
919
|
+
if script_path.is_empty() or line < 0:
|
|
920
|
+
continue
|
|
921
|
+
if not result.has(script_path):
|
|
922
|
+
result[script_path] = []
|
|
923
|
+
var lines: Array = result[script_path]
|
|
924
|
+
if line not in lines:
|
|
925
|
+
lines.append(line)
|
|
926
|
+
lines.sort()
|
|
927
|
+
_:
|
|
928
|
+
if typeof(payload) == TYPE_STRING:
|
|
929
|
+
var parsed := _parse_breakpoint_entry(payload)
|
|
930
|
+
if parsed.is_empty():
|
|
931
|
+
return result
|
|
932
|
+
result[parsed["script_path"]] = [parsed["line"]]
|
|
933
|
+
|
|
934
|
+
return result
|
|
935
|
+
|
|
936
|
+
func _parse_breakpoint_entry(entry) -> Dictionary:
|
|
937
|
+
if typeof(entry) == TYPE_DICTIONARY:
|
|
938
|
+
var script_path := ""
|
|
939
|
+
if entry.has("script_path"):
|
|
940
|
+
script_path = str(entry["script_path"])
|
|
941
|
+
elif entry.has("source"):
|
|
942
|
+
script_path = str(entry["source"])
|
|
943
|
+
elif entry.has("script"):
|
|
944
|
+
script_path = str(entry["script"])
|
|
945
|
+
|
|
946
|
+
var line := -1
|
|
947
|
+
if entry.has("line"):
|
|
948
|
+
line = _parse_line_value(entry["line"])
|
|
949
|
+
elif entry.has("line_number"):
|
|
950
|
+
line = _parse_line_value(entry["line_number"])
|
|
951
|
+
|
|
952
|
+
if script_path.is_empty() or line < 0:
|
|
953
|
+
return {}
|
|
954
|
+
|
|
955
|
+
return {
|
|
956
|
+
"script_path": script_path,
|
|
957
|
+
"line": line
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
if typeof(entry) == TYPE_ARRAY:
|
|
961
|
+
if entry.size() >= 2:
|
|
962
|
+
var script_path_entry = entry[0]
|
|
963
|
+
var line_entry = entry[1]
|
|
964
|
+
var script_path_value := str(script_path_entry)
|
|
965
|
+
var line_value := _parse_line_value(line_entry)
|
|
966
|
+
if not script_path_value.is_empty() and line_value >= 0:
|
|
967
|
+
return {
|
|
968
|
+
"script_path": script_path_value,
|
|
969
|
+
"line": line_value
|
|
970
|
+
}
|
|
971
|
+
return {}
|
|
972
|
+
|
|
973
|
+
if typeof(entry) == TYPE_STRING:
|
|
974
|
+
return _parse_breakpoint_string(entry)
|
|
975
|
+
|
|
976
|
+
if typeof(entry) == TYPE_INT:
|
|
977
|
+
return {}
|
|
978
|
+
|
|
979
|
+
return {}
|
|
980
|
+
|
|
981
|
+
func _parse_breakpoint_string(entry: String) -> Dictionary:
|
|
982
|
+
var separator_index := entry.rfind(":")
|
|
983
|
+
if separator_index == -1:
|
|
984
|
+
return {}
|
|
985
|
+
|
|
986
|
+
var script_path := entry.substr(0, separator_index).strip_edges()
|
|
987
|
+
var line_str := entry.substr(separator_index + 1, entry.length()).strip_edges()
|
|
988
|
+
|
|
989
|
+
if script_path.is_empty() or not line_str.is_valid_int():
|
|
990
|
+
return {}
|
|
991
|
+
|
|
992
|
+
return {
|
|
993
|
+
"script_path": script_path,
|
|
994
|
+
"line": int(line_str)
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
func _parse_line_value(value) -> int:
|
|
998
|
+
if value == null:
|
|
999
|
+
return -1
|
|
1000
|
+
|
|
1001
|
+
match typeof(value):
|
|
1002
|
+
TYPE_INT:
|
|
1003
|
+
return value
|
|
1004
|
+
TYPE_FLOAT:
|
|
1005
|
+
return int(value)
|
|
1006
|
+
TYPE_STRING, TYPE_STRING_NAME:
|
|
1007
|
+
var text_value := str(value)
|
|
1008
|
+
if text_value.is_valid_int():
|
|
1009
|
+
return int(text_value)
|
|
1010
|
+
return -1
|
|
1011
|
+
_:
|
|
1012
|
+
var text_value := str(value)
|
|
1013
|
+
if text_value.is_valid_int():
|
|
1014
|
+
return int(text_value)
|
|
1015
|
+
return -1
|
|
1016
|
+
|
|
1017
|
+
func _normalize_breakpoint_lines(line_values) -> Array:
|
|
1018
|
+
var result: Array = []
|
|
1019
|
+
|
|
1020
|
+
if line_values == null:
|
|
1021
|
+
return result
|
|
1022
|
+
|
|
1023
|
+
match typeof(line_values):
|
|
1024
|
+
TYPE_ARRAY:
|
|
1025
|
+
for value in line_values:
|
|
1026
|
+
var parsed_line := _parse_line_value(value)
|
|
1027
|
+
if parsed_line >= 0 and parsed_line not in result:
|
|
1028
|
+
result.append(parsed_line)
|
|
1029
|
+
TYPE_PACKED_INT32_ARRAY, TYPE_PACKED_INT64_ARRAY, TYPE_PACKED_FLOAT32_ARRAY:
|
|
1030
|
+
for value in line_values:
|
|
1031
|
+
var parsed_line := _parse_line_value(value)
|
|
1032
|
+
if parsed_line >= 0 and parsed_line not in result:
|
|
1033
|
+
result.append(parsed_line)
|
|
1034
|
+
TYPE_INT:
|
|
1035
|
+
if line_values >= 0:
|
|
1036
|
+
result.append(line_values)
|
|
1037
|
+
TYPE_STRING, TYPE_STRING_NAME:
|
|
1038
|
+
var parsed_line := _parse_line_value(line_values)
|
|
1039
|
+
if parsed_line >= 0 and parsed_line not in result:
|
|
1040
|
+
result.append(parsed_line)
|
|
1041
|
+
_:
|
|
1042
|
+
if typeof(line_values) == TYPE_DICTIONARY:
|
|
1043
|
+
for key in line_values.keys():
|
|
1044
|
+
var parsed_line := _parse_line_value(line_values[key])
|
|
1045
|
+
if parsed_line >= 0 and parsed_line not in result:
|
|
1046
|
+
result.append(parsed_line)
|
|
1047
|
+
|
|
1048
|
+
result.sort()
|
|
1049
|
+
return result
|
|
1050
|
+
|
|
1051
|
+
func _merge_breakpoint_map(target: Dictionary, source: Dictionary) -> void:
|
|
1052
|
+
for script_path in source.keys():
|
|
1053
|
+
var source_lines = source[script_path]
|
|
1054
|
+
var normalized_lines := _normalize_breakpoint_lines(source_lines)
|
|
1055
|
+
if normalized_lines.is_empty():
|
|
1056
|
+
continue
|
|
1057
|
+
|
|
1058
|
+
if not target.has(script_path):
|
|
1059
|
+
target[script_path] = []
|
|
1060
|
+
|
|
1061
|
+
var target_lines: Array = target[script_path]
|
|
1062
|
+
for line in normalized_lines:
|
|
1063
|
+
if line not in target_lines:
|
|
1064
|
+
target_lines.append(line)
|
|
1065
|
+
|
|
1066
|
+
target_lines.sort()
|
|
1067
|
+
target[script_path] = target_lines
|
|
1068
|
+
|
|
1069
|
+
func _get_active_session_ids() -> Array:
|
|
1070
|
+
var result: Array = []
|
|
1071
|
+
var sessions := get_sessions()
|
|
1072
|
+
|
|
1073
|
+
for i in range(sessions.size()):
|
|
1074
|
+
var session = sessions[i]
|
|
1075
|
+
if not session:
|
|
1076
|
+
continue
|
|
1077
|
+
|
|
1078
|
+
var session_id_value := i
|
|
1079
|
+
if session.has_method("get_session_id"):
|
|
1080
|
+
session_id_value = session.get_session_id()
|
|
1081
|
+
|
|
1082
|
+
var session_id = _normalize_session_id(session_id_value)
|
|
1083
|
+
|
|
1084
|
+
var session_active: bool = session.has_method("is_active") and session.is_active()
|
|
1085
|
+
var session_breaked: bool = session.has_method("is_breaked") and session.is_breaked()
|
|
1086
|
+
|
|
1087
|
+
if session_active or session_breaked:
|
|
1088
|
+
_ensure_session_state(session_id, true)
|
|
1089
|
+
if session_breaked:
|
|
1090
|
+
_active_sessions[session_id]["paused"] = true
|
|
1091
|
+
if session_id not in result:
|
|
1092
|
+
result.append(session_id)
|
|
1093
|
+
|
|
1094
|
+
# Fallback to tracked sessions when Godot doesn't report them via get_sessions().
|
|
1095
|
+
for session_key in _active_sessions.keys():
|
|
1096
|
+
var session_id = _normalize_session_id(session_key)
|
|
1097
|
+
var state: Dictionary = _active_sessions[session_key]
|
|
1098
|
+
if state.get("active", false) or state.get("paused", false):
|
|
1099
|
+
if session_id not in result:
|
|
1100
|
+
result.append(session_id)
|
|
1101
|
+
|
|
1102
|
+
return result
|
|
1103
|
+
|
|
1104
|
+
func _on_session_started(session_id: int) -> void:
|
|
1105
|
+
session_id = _normalize_session_id(session_id)
|
|
1106
|
+
_ensure_session_state(session_id, true)
|
|
1107
|
+
_trace("Debugger session %s started" % session_id)
|
|
1108
|
+
# Restore any existing breakpoints for this session
|
|
1109
|
+
_setup_session_breakpoints(session_id)
|
|
1110
|
+
|
|
1111
|
+
func _on_session_stopped(session_id: int) -> void:
|
|
1112
|
+
session_id = _normalize_session_id(session_id)
|
|
1113
|
+
_ensure_session_state(session_id)
|
|
1114
|
+
_active_sessions[session_id]["active"] = false
|
|
1115
|
+
_active_sessions[session_id]["paused"] = false
|
|
1116
|
+
_session_stack_cache.erase(session_id)
|
|
1117
|
+
_trace("Debugger session %s stopped" % session_id)
|
|
1118
|
+
|
|
1119
|
+
func _on_session_breaked(can_debug: bool, session_id: int) -> void:
|
|
1120
|
+
session_id = _normalize_session_id(session_id)
|
|
1121
|
+
_ensure_session_state(session_id)
|
|
1122
|
+
_active_sessions[session_id]["paused"] = can_debug
|
|
1123
|
+
_trace("Debugger session %s breaked, can_debug=%s" % [session_id, can_debug])
|
|
1124
|
+
|
|
1125
|
+
# Native breakpoint integration
|
|
1126
|
+
func _set_native_breakpoint(session: EditorDebuggerSession, script_path: String, line: int) -> bool:
|
|
1127
|
+
# Try different approaches to set breakpoints in Godot's debugger system
|
|
1128
|
+
|
|
1129
|
+
# Method 1: Try to use the session's breakpoint methods directly
|
|
1130
|
+
if session and session.has_method("breakpoints"):
|
|
1131
|
+
var breakpoints = session.breakpoints
|
|
1132
|
+
if breakpoints.has_method("set_line_breakpoint"):
|
|
1133
|
+
# This might be the correct method for setting breakpoints
|
|
1134
|
+
breakpoints.set_line_breakpoint(script_path, line)
|
|
1135
|
+
_trace("Set breakpoint using session.breakpoints.set_line_breakpoint: %s:%d" % [script_path, line])
|
|
1136
|
+
return true
|
|
1137
|
+
elif breakpoints.has_method("add_breakpoint"):
|
|
1138
|
+
breakpoints.add_breakpoint(script_path, line)
|
|
1139
|
+
_trace("Set breakpoint using session.breakpoints.add_breakpoint: %s:%d" % [script_path, line])
|
|
1140
|
+
return true
|
|
1141
|
+
|
|
1142
|
+
# Method 2: Try direct session methods
|
|
1143
|
+
if session and session.has_method("set_breakpoint"):
|
|
1144
|
+
# Try different parameter formats for set_breakpoint
|
|
1145
|
+
# Based on error, Godot expects exactly 3 arguments with String first
|
|
1146
|
+
var attempts = [
|
|
1147
|
+
[script_path, line, true], # script_path, line, enabled (most likely correct)
|
|
1148
|
+
[script_path, line, 1], # script_path, line, enabled (as int)
|
|
1149
|
+
]
|
|
1150
|
+
|
|
1151
|
+
for attempt in attempts:
|
|
1152
|
+
# Use callv to safely call with parameter array
|
|
1153
|
+
var result = session.callv("set_breakpoint", attempt)
|
|
1154
|
+
_trace("Set breakpoint using session.set_breakpoint with params %s: %s:%d" % [attempt, script_path, line])
|
|
1155
|
+
return true
|
|
1156
|
+
|
|
1157
|
+
# Method 3: Try standard Godot debugger protocol
|
|
1158
|
+
if session and session.has_method("send_message"):
|
|
1159
|
+
# Try different message formats that Godot might understand
|
|
1160
|
+
# Using the format from DEBUGGER_FIX_GUIDE.md
|
|
1161
|
+
var attempts = [
|
|
1162
|
+
["breakpoint:insert", [{"script_path": script_path, "line": line, "enabled": true}]],
|
|
1163
|
+
["breakpoint:insert", [{"source": script_path, "line": line, "enabled": true}]],
|
|
1164
|
+
["breakpoint:set", [{"script": script_path, "line": line, "enabled": true}]],
|
|
1165
|
+
["debugger:breakpoint", [{"script_path": script_path, "line": line, "enabled": true}]]
|
|
1166
|
+
]
|
|
1167
|
+
|
|
1168
|
+
for attempt in attempts:
|
|
1169
|
+
session.send_message(attempt[0], attempt[1])
|
|
1170
|
+
_trace("Attempted breakpoint set with format %s: %s:%d" % [attempt[0], script_path, line])
|
|
1171
|
+
|
|
1172
|
+
return true
|
|
1173
|
+
|
|
1174
|
+
return false
|
|
1175
|
+
|
|
1176
|
+
func _clear_all_native_breakpoints(session: EditorDebuggerSession) -> void:
|
|
1177
|
+
_trace("Attempting to clear all native breakpoints from session")
|
|
1178
|
+
|
|
1179
|
+
# Method 1: Try to access and clear breakpoints through the session object
|
|
1180
|
+
if session and session.has_method("get_breakpoints"):
|
|
1181
|
+
var current_breakpoints = session.get_breakpoints()
|
|
1182
|
+
_trace("Current breakpoints from session before clear: %s" % current_breakpoints)
|
|
1183
|
+
|
|
1184
|
+
if current_breakpoints and current_breakpoints is Dictionary:
|
|
1185
|
+
# Try to clear using session methods
|
|
1186
|
+
if session.has_method("clear_breakpoints"):
|
|
1187
|
+
session.clear_breakpoints()
|
|
1188
|
+
_trace("Called session.clear_breakpoints()")
|
|
1189
|
+
elif session.get("breakpoints") and session.breakpoints.has_method("clear"):
|
|
1190
|
+
session.breakpoints.clear()
|
|
1191
|
+
_trace("Called session.breakpoints.clear()")
|
|
1192
|
+
|
|
1193
|
+
# Method 2: Try different clear message formats
|
|
1194
|
+
if session and session.has_method("send_message"):
|
|
1195
|
+
var clear_attempts = [
|
|
1196
|
+
["breakpoint:clear_all", []],
|
|
1197
|
+
["breakpoint:clear", []],
|
|
1198
|
+
["debugger:clear_breakpoints", []],
|
|
1199
|
+
["debugger:breakpoints_clear", []]
|
|
1200
|
+
]
|
|
1201
|
+
|
|
1202
|
+
for attempt in clear_attempts:
|
|
1203
|
+
session.send_message(attempt[0], attempt[1])
|
|
1204
|
+
_trace("Sent clear breakpoints message: %s" % attempt[0])
|
|
1205
|
+
|
|
1206
|
+
func _remove_native_breakpoint(session: EditorDebuggerSession, script_path: String, line: int) -> bool:
|
|
1207
|
+
# Try different approaches to remove breakpoints in Godot's debugger system
|
|
1208
|
+
_trace("Attempting to remove breakpoint at %s:%d" % [script_path, line])
|
|
1209
|
+
|
|
1210
|
+
# Method 1: Try the direct message approach with "breakpoint:remove" first (most reliable)
|
|
1211
|
+
if session and session.has_method("send_message"):
|
|
1212
|
+
# Try different message formats that Godot might understand for removal
|
|
1213
|
+
var attempts = [
|
|
1214
|
+
["breakpoint:remove", [{"source": script_path, "line": line}]],
|
|
1215
|
+
["breakpoint:remove", [{"script_path": script_path, "line": line}]],
|
|
1216
|
+
["breakpoint:remove", [{"script": script_path, "line": line}]],
|
|
1217
|
+
["debugger:breakpoint_remove", [{"script_path": script_path, "line": line}]]
|
|
1218
|
+
]
|
|
1219
|
+
|
|
1220
|
+
for attempt in attempts:
|
|
1221
|
+
session.send_message(attempt[0], attempt[1])
|
|
1222
|
+
_trace("Sent breakpoint removal message %s for %s:%d" % [attempt[0], script_path, line])
|
|
1223
|
+
|
|
1224
|
+
# Method 2: Try to use the session's breakpoint methods directly
|
|
1225
|
+
if session and session.has_method("set_breakpoint"):
|
|
1226
|
+
# Some versions of Godot might support setting breakpoints with enabled=false to remove them
|
|
1227
|
+
var attempts = [
|
|
1228
|
+
[script_path, line, false], # script_path, line, enabled=false
|
|
1229
|
+
[script_path, line, 0], # script_path, line, enabled=0
|
|
1230
|
+
]
|
|
1231
|
+
|
|
1232
|
+
for attempt in attempts:
|
|
1233
|
+
var result = session.callv("set_breakpoint", attempt)
|
|
1234
|
+
_trace("Attempted removal using set_breakpoint with params %s: %s:%d" % [attempt, script_path, line])
|
|
1235
|
+
|
|
1236
|
+
# Method 3: Try using the breakpoints object if it exists
|
|
1237
|
+
if session and session.get("breakpoints"):
|
|
1238
|
+
var breakpoints = session.breakpoints
|
|
1239
|
+
_trace("Accessing session.breakpoints property: %s" % breakpoints)
|
|
1240
|
+
|
|
1241
|
+
if breakpoints:
|
|
1242
|
+
# Try to get current breakpoints and rebuild without the target line
|
|
1243
|
+
var current_breakpoints = session.get_breakpoints()
|
|
1244
|
+
_trace("Current breakpoints from session: %s" % current_breakpoints)
|
|
1245
|
+
|
|
1246
|
+
if current_breakpoints and current_breakpoints is Dictionary:
|
|
1247
|
+
if current_breakpoints.has(script_path):
|
|
1248
|
+
var script_bps = current_breakpoints[script_path]
|
|
1249
|
+
if script_bps is Array:
|
|
1250
|
+
# Remove the target line if it exists
|
|
1251
|
+
var new_bps = []
|
|
1252
|
+
var found_and_removed = false
|
|
1253
|
+
for bp_line in script_bps:
|
|
1254
|
+
if bp_line != line:
|
|
1255
|
+
new_bps.append(bp_line)
|
|
1256
|
+
else:
|
|
1257
|
+
found_and_removed = true
|
|
1258
|
+
|
|
1259
|
+
if found_and_removed:
|
|
1260
|
+
# Update the breakpoints array
|
|
1261
|
+
current_breakpoints[script_path] = new_bps
|
|
1262
|
+
_trace("Removed line %d from breakpoints for %s, new list: %s" % [line, script_path, new_bps])
|
|
1263
|
+
|
|
1264
|
+
# Try to clear and re-set all breakpoints
|
|
1265
|
+
if breakpoints.has_method("clear"):
|
|
1266
|
+
breakpoints.clear()
|
|
1267
|
+
_trace("Cleared all breakpoints, will re-add")
|
|
1268
|
+
|
|
1269
|
+
# Re-add all remaining breakpoints
|
|
1270
|
+
for sp in current_breakpoints:
|
|
1271
|
+
var bps = current_breakpoints[sp]
|
|
1272
|
+
if bps is Array:
|
|
1273
|
+
for bp_line in bps:
|
|
1274
|
+
_set_native_breakpoint(session, sp, bp_line)
|
|
1275
|
+
return true
|
|
1276
|
+
|
|
1277
|
+
# Method 4: Last resort - try to access EditorInterface and modify breakpoints directly
|
|
1278
|
+
if Engine.has_singleton("EditorInterface"):
|
|
1279
|
+
var editor_interface = Engine.get_singleton("EditorInterface")
|
|
1280
|
+
if editor_interface and editor_interface.has_method("get_script_editor"):
|
|
1281
|
+
var script_editor = editor_interface.get_script_editor()
|
|
1282
|
+
if script_editor and script_editor.has_method("get_current_script"):
|
|
1283
|
+
# Try to find and remove the breakpoint through the script editor
|
|
1284
|
+
_trace("Attempting to remove breakpoint through script editor interface")
|
|
1285
|
+
# This is a fallback method and may not work in all Godot versions
|
|
1286
|
+
|
|
1287
|
+
_trace("All breakpoint removal methods attempted for %s:%d" % [script_path, line])
|
|
1288
|
+
return true # Return true to indicate we attempted removal
|
|
1289
|
+
|
|
1290
|
+
func _get_session_stack_info(session_id) -> Dictionary:
|
|
1291
|
+
session_id = _normalize_session_id(session_id)
|
|
1292
|
+
var session = _get_session_instance(session_id)
|
|
1293
|
+
if not session or not session.is_active():
|
|
1294
|
+
return {"frames": [], "total_frames": 0}
|
|
1295
|
+
|
|
1296
|
+
# Try to get stack info from the session
|
|
1297
|
+
var stack_frames := []
|
|
1298
|
+
|
|
1299
|
+
# Method 1: Try to get stack via send_message and wait for response
|
|
1300
|
+
if session.has_method("send_message"):
|
|
1301
|
+
session.send_message("get_stack_dump", [])
|
|
1302
|
+
|
|
1303
|
+
# Note: This is async - we'll need to handle the response in the capture method
|
|
1304
|
+
# For now, return basic info
|
|
1305
|
+
var current_script = _active_sessions.get(session_id, {}).get("current_script", "")
|
|
1306
|
+
var current_line = _active_sessions.get(session_id, {}).get("current_line", -1)
|
|
1307
|
+
|
|
1308
|
+
if not current_script.is_empty() and current_line >= 0:
|
|
1309
|
+
var frame_info := {
|
|
1310
|
+
"script": current_script,
|
|
1311
|
+
"line": current_line,
|
|
1312
|
+
"function": "_process",
|
|
1313
|
+
"file": current_script
|
|
1314
|
+
}
|
|
1315
|
+
stack_frames.append(frame_info)
|
|
1316
|
+
|
|
1317
|
+
return {
|
|
1318
|
+
"frames": stack_frames,
|
|
1319
|
+
"total_frames": stack_frames.size(),
|
|
1320
|
+
"current_frame": 0
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
func _setup_session_breakpoints(session_id) -> void:
|
|
1324
|
+
session_id = _normalize_session_id(session_id)
|
|
1325
|
+
# When a new session starts, set up all existing breakpoints for it
|
|
1326
|
+
if not _session_breakpoints.has(session_id):
|
|
1327
|
+
return
|
|
1328
|
+
|
|
1329
|
+
var session = _get_session_instance(session_id)
|
|
1330
|
+
if not session or not session.is_active():
|
|
1331
|
+
return
|
|
1332
|
+
|
|
1333
|
+
var session_bps = _session_breakpoints[session_id]
|
|
1334
|
+
for script_path in session_bps:
|
|
1335
|
+
for line in session_bps[script_path]:
|
|
1336
|
+
_set_native_breakpoint(session, script_path, line)
|
|
1337
|
+
_trace("Restored breakpoint %s:%d for session %s" % [script_path, line, session_id])
|
|
1338
|
+
|
|
1339
|
+
func _diagnose_session_capabilities(session: EditorDebuggerSession) -> void:
|
|
1340
|
+
_trace("=== EditorDebuggerSession Capabilities ===")
|
|
1341
|
+
_trace("Session type: %s" % session.get_class())
|
|
1342
|
+
_trace("Session active: %s" % session.is_active())
|
|
1343
|
+
|
|
1344
|
+
# List all available methods
|
|
1345
|
+
var methods = session.get_method_list()
|
|
1346
|
+
_trace("Available methods:")
|
|
1347
|
+
for method in methods:
|
|
1348
|
+
_trace(" - %s" % method.name)
|
|
1349
|
+
|
|
1350
|
+
# Check for breakpoint-related properties
|
|
1351
|
+
if session.has_method("get_property_list"):
|
|
1352
|
+
var props = session.get_property_list()
|
|
1353
|
+
_trace("Available properties:")
|
|
1354
|
+
for prop in props:
|
|
1355
|
+
if prop is Dictionary and prop.has("name"):
|
|
1356
|
+
var prop_name = prop["name"]
|
|
1357
|
+
if "breakpoint" in prop_name.to_lower() or "debug" in prop_name.to_lower():
|
|
1358
|
+
_trace(" - %s: %s" % [prop_name, prop.get("type", "unknown")])
|
|
1359
|
+
|
|
1360
|
+
# Try to access breakpoints property if it exists
|
|
1361
|
+
if "breakpoints" in session:
|
|
1362
|
+
var breakpoints = session.breakpoints
|
|
1363
|
+
_trace("Breakpoints property type: %s" % breakpoints.get_class())
|
|
1364
|
+
if breakpoints.has_method("get_method_list"):
|
|
1365
|
+
var bp_methods = breakpoints.get_method_list()
|
|
1366
|
+
_trace("Breakpoints methods:")
|
|
1367
|
+
for method in bp_methods:
|
|
1368
|
+
_trace(" - %s" % method.name)
|
|
1369
|
+
|
|
1370
|
+
_trace("=== End Capabilities ===")
|
|
1371
|
+
|
|
1372
|
+
func _trace(text: String) -> void:
|
|
1373
|
+
if OS.is_stdout_verbose():
|
|
1374
|
+
print("[MCPDebugger] %s" % text)
|
|
1375
|
+
|
|
1376
|
+
func _build_frames_from_debug_output() -> Dictionary:
|
|
1377
|
+
var frames := _parse_frames_from_debug_output()
|
|
1378
|
+
if frames.is_empty():
|
|
1379
|
+
return {}
|
|
1380
|
+
return {
|
|
1381
|
+
"frames": frames,
|
|
1382
|
+
"total_frames": frames.size(),
|
|
1383
|
+
"current_frame": 0,
|
|
1384
|
+
"source": "debug_output"
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
func _parse_frames_from_debug_output() -> Array:
|
|
1388
|
+
var publisher = _get_debug_output_publisher()
|
|
1389
|
+
if publisher == null or not publisher.has_method("get_full_log_text"):
|
|
1390
|
+
return []
|
|
1391
|
+
var text = publisher.get_full_log_text()
|
|
1392
|
+
if text.is_empty():
|
|
1393
|
+
return []
|
|
1394
|
+
var lines = text.split("\n", false)
|
|
1395
|
+
var frame_lines: Array = []
|
|
1396
|
+
var collecting := false
|
|
1397
|
+
for i in range(lines.size() - 1, -1, -1):
|
|
1398
|
+
var line := String(lines[i]).strip_edges()
|
|
1399
|
+
if line.begins_with("Frame "):
|
|
1400
|
+
collecting = true
|
|
1401
|
+
frame_lines.push_front(line)
|
|
1402
|
+
elif collecting:
|
|
1403
|
+
break
|
|
1404
|
+
if frame_lines.is_empty():
|
|
1405
|
+
return []
|
|
1406
|
+
var frames := []
|
|
1407
|
+
for line in frame_lines:
|
|
1408
|
+
var frame_dict := _parse_print_stack_line(line)
|
|
1409
|
+
if not frame_dict.is_empty():
|
|
1410
|
+
frames.append(frame_dict)
|
|
1411
|
+
return frames
|
|
1412
|
+
|
|
1413
|
+
func _parse_print_stack_line(line: String) -> Dictionary:
|
|
1414
|
+
var trimmed := line.strip_edges()
|
|
1415
|
+
if not trimmed.begins_with("Frame "):
|
|
1416
|
+
return {}
|
|
1417
|
+
var dash_index := trimmed.find(" - ")
|
|
1418
|
+
if dash_index == -1:
|
|
1419
|
+
return {}
|
|
1420
|
+
var index_text := trimmed.substr(6, dash_index - 6).strip_edges()
|
|
1421
|
+
var index_value := 0
|
|
1422
|
+
if index_text.is_valid_int():
|
|
1423
|
+
index_value = int(index_text)
|
|
1424
|
+
var rest := trimmed.substr(dash_index + 3).strip_edges()
|
|
1425
|
+
var func_name := ""
|
|
1426
|
+
var location := rest
|
|
1427
|
+
var at_index := rest.find("@")
|
|
1428
|
+
if at_index != -1:
|
|
1429
|
+
location = rest.substr(0, at_index).strip_edges()
|
|
1430
|
+
func_name = rest.substr(at_index + 1).strip_edges()
|
|
1431
|
+
if func_name.ends_with("()"):
|
|
1432
|
+
func_name = func_name.substr(0, func_name.length() - 2)
|
|
1433
|
+
var script_path := ""
|
|
1434
|
+
var line_number := -1
|
|
1435
|
+
var colon_index := location.rfind(":")
|
|
1436
|
+
if colon_index != -1:
|
|
1437
|
+
var line_text := location.substr(colon_index + 1).strip_edges()
|
|
1438
|
+
if line_text.is_valid_int():
|
|
1439
|
+
line_number = int(line_text)
|
|
1440
|
+
script_path = location.substr(0, colon_index).strip_edges()
|
|
1441
|
+
else:
|
|
1442
|
+
script_path = location
|
|
1443
|
+
return {
|
|
1444
|
+
"index": index_value,
|
|
1445
|
+
"function": func_name,
|
|
1446
|
+
"script": script_path,
|
|
1447
|
+
"file": script_path,
|
|
1448
|
+
"line": line_number,
|
|
1449
|
+
"location": location
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
func _get_debug_output_publisher():
|
|
1453
|
+
if Engine.has_meta("MCPDebugOutputPublisher"):
|
|
1454
|
+
return Engine.get_meta("MCPDebugOutputPublisher")
|
|
1455
|
+
return null
|