@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,1083 @@
|
|
|
1
|
+
@tool
|
|
2
|
+
class_name MCPEnhancedCommands
|
|
3
|
+
extends MCPBaseCommandProcessor
|
|
4
|
+
|
|
5
|
+
func process_command(client_id: int, command_type: String, params: Dictionary, command_id: String) -> bool:
|
|
6
|
+
match command_type:
|
|
7
|
+
"get_editor_scene_structure":
|
|
8
|
+
_handle_get_editor_scene_structure(client_id, params, command_id)
|
|
9
|
+
return true
|
|
10
|
+
"get_runtime_scene_structure":
|
|
11
|
+
_handle_get_runtime_scene_structure(client_id, params, command_id)
|
|
12
|
+
return true
|
|
13
|
+
"get_debug_output":
|
|
14
|
+
_handle_get_debug_output(client_id, params, command_id)
|
|
15
|
+
return true
|
|
16
|
+
"get_editor_errors":
|
|
17
|
+
_handle_get_editor_errors(client_id, params, command_id)
|
|
18
|
+
return true
|
|
19
|
+
"update_node_transform":
|
|
20
|
+
_handle_update_node_transform(client_id, params, command_id)
|
|
21
|
+
return true
|
|
22
|
+
"evaluate_runtime":
|
|
23
|
+
_handle_evaluate_runtime(client_id, params, command_id)
|
|
24
|
+
return true
|
|
25
|
+
"subscribe_debug_output":
|
|
26
|
+
_handle_subscribe_debug_output(client_id, command_id)
|
|
27
|
+
return true
|
|
28
|
+
"unsubscribe_debug_output":
|
|
29
|
+
_handle_unsubscribe_debug_output(client_id, command_id)
|
|
30
|
+
return true
|
|
31
|
+
"get_stack_trace_panel":
|
|
32
|
+
_handle_get_stack_trace_panel(client_id, params, command_id)
|
|
33
|
+
return true
|
|
34
|
+
"get_stack_frames_panel":
|
|
35
|
+
_handle_get_stack_frames_panel(client_id, params, command_id)
|
|
36
|
+
return true
|
|
37
|
+
"clear_debug_output":
|
|
38
|
+
_handle_clear_debug_output(client_id, command_id)
|
|
39
|
+
return true
|
|
40
|
+
"clear_editor_errors":
|
|
41
|
+
_handle_clear_editor_errors(client_id, command_id)
|
|
42
|
+
return true
|
|
43
|
+
|
|
44
|
+
# Command not handled by this processor
|
|
45
|
+
return false
|
|
46
|
+
|
|
47
|
+
# Helper function to get EditorInterface
|
|
48
|
+
func _get_editor_interface():
|
|
49
|
+
var plugin_instance = Engine.get_meta("GodotMCPPlugin") as EditorPlugin
|
|
50
|
+
if plugin_instance:
|
|
51
|
+
return plugin_instance.get_editor_interface()
|
|
52
|
+
return null
|
|
53
|
+
|
|
54
|
+
func _get_runtime_bridge() -> MCPRuntimeDebuggerBridge:
|
|
55
|
+
if Engine.has_meta("MCPRuntimeDebuggerBridge"):
|
|
56
|
+
return Engine.get_meta("MCPRuntimeDebuggerBridge") as MCPRuntimeDebuggerBridge
|
|
57
|
+
return null
|
|
58
|
+
|
|
59
|
+
func _get_debugger_bridge() -> MCPDebuggerBridge:
|
|
60
|
+
if Engine.has_meta("MCPDebuggerBridge"):
|
|
61
|
+
return Engine.get_meta("MCPDebuggerBridge") as MCPDebuggerBridge
|
|
62
|
+
if Engine.has_meta("GodotMCPPlugin"):
|
|
63
|
+
var plugin_instance = Engine.get_meta("GodotMCPPlugin")
|
|
64
|
+
if plugin_instance and plugin_instance.has_method("get_debugger_bridge"):
|
|
65
|
+
var bridge = plugin_instance.get_debugger_bridge()
|
|
66
|
+
if bridge and bridge is MCPDebuggerBridge:
|
|
67
|
+
return bridge
|
|
68
|
+
return null
|
|
69
|
+
|
|
70
|
+
# ---- Scene Structure Commands ----
|
|
71
|
+
|
|
72
|
+
func _handle_get_editor_scene_structure(client_id: int, params: Dictionary, command_id: String) -> void:
|
|
73
|
+
var options = _build_scene_options(params, false, false)
|
|
74
|
+
var result = _build_scene_structure_result(options)
|
|
75
|
+
_send_success(client_id, result, command_id)
|
|
76
|
+
|
|
77
|
+
func _handle_get_runtime_scene_structure(client_id: int, params: Dictionary, command_id: String) -> void:
|
|
78
|
+
var runtime_bridge := _get_runtime_bridge()
|
|
79
|
+
if runtime_bridge == null:
|
|
80
|
+
_send_success(client_id, { "error": "Runtime debugger bridge not available. Ensure the project is running." }, command_id)
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
var options = _build_scene_options(params, false, false)
|
|
84
|
+
var timeout_ms = params.get("timeout_ms", MCPRuntimeDebuggerBridge.DEFAULT_TIMEOUT_MS)
|
|
85
|
+
timeout_ms = int(timeout_ms)
|
|
86
|
+
if timeout_ms < 100:
|
|
87
|
+
timeout_ms = 100
|
|
88
|
+
elif timeout_ms > 5000:
|
|
89
|
+
timeout_ms = 5000
|
|
90
|
+
|
|
91
|
+
var request_info = runtime_bridge.request_runtime_scene_snapshot()
|
|
92
|
+
if request_info.has("error"):
|
|
93
|
+
_send_success(client_id, request_info, command_id)
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
var session_id: int = request_info.get("session_id", -1)
|
|
97
|
+
var baseline_version: int = request_info.get("baseline_version", 0)
|
|
98
|
+
var scene_tree := get_tree()
|
|
99
|
+
if scene_tree == null:
|
|
100
|
+
_send_success(client_id, { "error": "Scene tree unavailable for runtime polling." }, command_id)
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
var deadline: int = Time.get_ticks_msec() + timeout_ms
|
|
104
|
+
var snapshot: Dictionary = {}
|
|
105
|
+
|
|
106
|
+
while Time.get_ticks_msec() <= deadline:
|
|
107
|
+
if runtime_bridge.has_new_runtime_snapshot(session_id, baseline_version):
|
|
108
|
+
snapshot = runtime_bridge.build_runtime_snapshot(session_id, options)
|
|
109
|
+
if not snapshot.is_empty():
|
|
110
|
+
break
|
|
111
|
+
|
|
112
|
+
await scene_tree.process_frame
|
|
113
|
+
|
|
114
|
+
if snapshot.is_empty():
|
|
115
|
+
snapshot = {
|
|
116
|
+
"error": "Timed out waiting for runtime scene data.",
|
|
117
|
+
"hint": "Ensure the remote debugger supports scene tree capture; try opening the Remote Scene tab in Godot or enabling EngineDebugger.set_capture('scene', true) inside the running project."
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
_send_success(client_id, snapshot, command_id)
|
|
121
|
+
|
|
122
|
+
func _handle_evaluate_runtime(client_id: int, params: Dictionary, command_id: String) -> void:
|
|
123
|
+
var runtime_bridge := _get_runtime_bridge()
|
|
124
|
+
if runtime_bridge == null:
|
|
125
|
+
_send_success(client_id, {
|
|
126
|
+
"error": "Runtime debugger bridge not available. Ensure the project is running."
|
|
127
|
+
}, command_id)
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
var expression := ""
|
|
131
|
+
if params.has("expression"):
|
|
132
|
+
expression = str(params.get("expression", ""))
|
|
133
|
+
elif params.has("code"):
|
|
134
|
+
expression = str(params.get("code", ""))
|
|
135
|
+
|
|
136
|
+
if expression.strip_edges().is_empty():
|
|
137
|
+
_send_error(client_id, "Expression cannot be empty", command_id)
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
var options: Dictionary = {}
|
|
141
|
+
if params.has("context_path"):
|
|
142
|
+
options["node_path"] = str(params.get("context_path"))
|
|
143
|
+
elif params.has("node_path"):
|
|
144
|
+
options["node_path"] = str(params.get("node_path"))
|
|
145
|
+
|
|
146
|
+
if params.has("capture_prints"):
|
|
147
|
+
options["capture_prints"] = _coerce_bool(params.get("capture_prints"), true)
|
|
148
|
+
|
|
149
|
+
var timeout_ms := int(params.get("timeout_ms", MCPRuntimeDebuggerBridge.DEFAULT_EVAL_TIMEOUT_MS))
|
|
150
|
+
if timeout_ms < 100:
|
|
151
|
+
timeout_ms = 100
|
|
152
|
+
elif timeout_ms > 5000:
|
|
153
|
+
timeout_ms = 5000
|
|
154
|
+
|
|
155
|
+
var request_info := runtime_bridge.evaluate_runtime_expression(expression, options)
|
|
156
|
+
if request_info.has("error"):
|
|
157
|
+
_send_success(client_id, request_info, command_id)
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
var session_id: int = request_info.get("session_id", -1)
|
|
161
|
+
var request_id: int = request_info.get("request_id", -1)
|
|
162
|
+
if session_id < 0 or request_id < 0:
|
|
163
|
+
_send_success(client_id, { "error": "Failed to enqueue runtime evaluation request." }, command_id)
|
|
164
|
+
return
|
|
165
|
+
|
|
166
|
+
var scene_tree := get_tree()
|
|
167
|
+
if scene_tree == null:
|
|
168
|
+
_send_success(client_id, { "error": "Scene tree unavailable while waiting for runtime evaluation." }, command_id)
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
var deadline: int = Time.get_ticks_msec() + timeout_ms
|
|
172
|
+
var response: Dictionary = {}
|
|
173
|
+
|
|
174
|
+
while Time.get_ticks_msec() <= deadline:
|
|
175
|
+
if runtime_bridge.has_eval_result(session_id, request_id):
|
|
176
|
+
response = runtime_bridge.take_eval_result(session_id, request_id)
|
|
177
|
+
break
|
|
178
|
+
await scene_tree.process_frame
|
|
179
|
+
|
|
180
|
+
if response.is_empty():
|
|
181
|
+
response = {
|
|
182
|
+
"error": "Timed out waiting for runtime evaluation result.",
|
|
183
|
+
"hint": "Ensure the running project registers the mcp_eval debugger capture via EngineDebugger.register_message_capture."
|
|
184
|
+
}
|
|
185
|
+
elif not response.get("success", true) and not response.has("error"):
|
|
186
|
+
response["error"] = "Runtime evaluation failed."
|
|
187
|
+
|
|
188
|
+
_send_success(client_id, response, command_id)
|
|
189
|
+
|
|
190
|
+
func _build_scene_structure_result(options: Dictionary) -> Dictionary:
|
|
191
|
+
var editor_interface = _get_editor_interface()
|
|
192
|
+
if not editor_interface:
|
|
193
|
+
return { "error": "Could not access EditorInterface" }
|
|
194
|
+
|
|
195
|
+
var root = editor_interface.get_edited_scene_root()
|
|
196
|
+
if not root:
|
|
197
|
+
return { "error": "No scene is currently being edited" }
|
|
198
|
+
|
|
199
|
+
var scene_path = ""
|
|
200
|
+
|
|
201
|
+
if "scene_file_path" in root:
|
|
202
|
+
scene_path = root.scene_file_path
|
|
203
|
+
if typeof(scene_path) != TYPE_STRING:
|
|
204
|
+
scene_path = str(scene_path)
|
|
205
|
+
|
|
206
|
+
if scene_path.is_empty():
|
|
207
|
+
scene_path = "Unsaved Scene"
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
"scene_path": scene_path,
|
|
211
|
+
"path": scene_path,
|
|
212
|
+
"root_node_type": root.get_class(),
|
|
213
|
+
"root_node_name": root.name,
|
|
214
|
+
"structure": _build_node_info(root, options, 0)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
func _build_scene_options(params: Dictionary, include_properties_default: bool, include_scripts_default: bool) -> Dictionary:
|
|
218
|
+
var include_properties = include_properties_default
|
|
219
|
+
if params.has("include_properties"):
|
|
220
|
+
include_properties = _coerce_bool(params.get("include_properties"), include_properties_default)
|
|
221
|
+
|
|
222
|
+
var include_scripts = include_scripts_default
|
|
223
|
+
if params.has("include_scripts"):
|
|
224
|
+
include_scripts = _coerce_bool(params.get("include_scripts"), include_scripts_default)
|
|
225
|
+
|
|
226
|
+
var max_depth = -1
|
|
227
|
+
if params.has("max_depth"):
|
|
228
|
+
max_depth = int(params.get("max_depth"))
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
"include_properties": include_properties,
|
|
232
|
+
"include_scripts": include_scripts,
|
|
233
|
+
"max_depth": max_depth
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
func _coerce_bool(value, default: bool) -> bool:
|
|
237
|
+
if typeof(value) == TYPE_BOOL:
|
|
238
|
+
return value
|
|
239
|
+
if typeof(value) == TYPE_STRING:
|
|
240
|
+
var lowered = value.to_lower()
|
|
241
|
+
if lowered == "true":
|
|
242
|
+
return true
|
|
243
|
+
if lowered == "false":
|
|
244
|
+
return false
|
|
245
|
+
return bool(value) if value != null else default
|
|
246
|
+
|
|
247
|
+
func _build_node_info(node: Node, options: Dictionary, depth: int) -> Dictionary:
|
|
248
|
+
var info = {
|
|
249
|
+
"name": node.name,
|
|
250
|
+
"type": node.get_class(),
|
|
251
|
+
"path": node.get_path(),
|
|
252
|
+
"children": []
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if options.get("include_properties", false):
|
|
256
|
+
var properties = {}
|
|
257
|
+
if node.has_method("get_property_list"):
|
|
258
|
+
var props = node.get_property_list()
|
|
259
|
+
for prop in props:
|
|
260
|
+
if prop.usage & PROPERTY_USAGE_EDITOR and not (prop.usage & PROPERTY_USAGE_CATEGORY):
|
|
261
|
+
if prop.name in ["position", "rotation", "scale", "text", "visible"]:
|
|
262
|
+
properties[prop.name] = node.get(prop.name)
|
|
263
|
+
if properties.size() > 0:
|
|
264
|
+
info["properties"] = properties
|
|
265
|
+
|
|
266
|
+
if options.get("include_scripts", false):
|
|
267
|
+
var script = node.get_script()
|
|
268
|
+
if script:
|
|
269
|
+
var script_path = ""
|
|
270
|
+
var class_name_str = ""
|
|
271
|
+
|
|
272
|
+
if typeof(script) == TYPE_OBJECT:
|
|
273
|
+
if script.has_method("get_path") or "resource_path" in script:
|
|
274
|
+
script_path = script.resource_path if "resource_path" in script else ""
|
|
275
|
+
|
|
276
|
+
if script.has_method("get_instance_base_type"):
|
|
277
|
+
class_name_str = script.get_instance_base_type()
|
|
278
|
+
|
|
279
|
+
info["script"] = {
|
|
280
|
+
"path": script_path,
|
|
281
|
+
"class_name": class_name_str
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
var max_depth = options.get("max_depth", -1)
|
|
285
|
+
if max_depth >= 0 and depth >= max_depth:
|
|
286
|
+
return info
|
|
287
|
+
|
|
288
|
+
for child in node.get_children():
|
|
289
|
+
info["children"].append(_build_node_info(child, options, depth + 1))
|
|
290
|
+
|
|
291
|
+
return info
|
|
292
|
+
|
|
293
|
+
# ---- Debug Output Commands ----
|
|
294
|
+
|
|
295
|
+
func _handle_get_debug_output(client_id: int, _params: Dictionary, command_id: String) -> void:
|
|
296
|
+
var result = get_debug_output()
|
|
297
|
+
|
|
298
|
+
_send_success(client_id, result, command_id)
|
|
299
|
+
|
|
300
|
+
func _handle_get_editor_errors(client_id: int, _params: Dictionary, command_id: String) -> void:
|
|
301
|
+
var snapshot := _capture_editor_errors_snapshot()
|
|
302
|
+
var diagnostics := snapshot.get("diagnostics", {})
|
|
303
|
+
if diagnostics.has("error"):
|
|
304
|
+
_send_error(client_id, "Failed to read Errors tab: %s" % diagnostics["error"], command_id)
|
|
305
|
+
return
|
|
306
|
+
_send_success(client_id, snapshot, command_id)
|
|
307
|
+
|
|
308
|
+
func get_debug_output() -> Dictionary:
|
|
309
|
+
var output := ""
|
|
310
|
+
var publisher := _get_debug_output_publisher()
|
|
311
|
+
var diagnostics: Dictionary = {}
|
|
312
|
+
if publisher:
|
|
313
|
+
output = publisher.get_full_log_text()
|
|
314
|
+
|
|
315
|
+
if publisher.has_method("get_capture_diagnostics"):
|
|
316
|
+
diagnostics = publisher.get_capture_diagnostics()
|
|
317
|
+
else:
|
|
318
|
+
# Fallback to remote debugger log if publisher unavailable.
|
|
319
|
+
var source := "none"
|
|
320
|
+
var detail := ""
|
|
321
|
+
if Engine.has_singleton("EditorDebuggerNode"):
|
|
322
|
+
var debugger = Engine.get_singleton("EditorDebuggerNode")
|
|
323
|
+
if debugger and debugger.has_method("get_log"):
|
|
324
|
+
output = debugger.get_log()
|
|
325
|
+
source = "debugger_singleton"
|
|
326
|
+
detail = "len=%d" % output.length()
|
|
327
|
+
elif has_node("/root/EditorNode/DebuggerPanel"):
|
|
328
|
+
var debugger_panel = get_node("/root/EditorNode/DebuggerPanel")
|
|
329
|
+
if debugger_panel and debugger_panel.has_method("get_output"):
|
|
330
|
+
output = debugger_panel.get_output()
|
|
331
|
+
source = "debugger_panel"
|
|
332
|
+
detail = "len=%d" % output.length()
|
|
333
|
+
|
|
334
|
+
diagnostics = {
|
|
335
|
+
"source": source,
|
|
336
|
+
"detail": detail,
|
|
337
|
+
"timestamp": Time.get_ticks_msec()
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
"output": output,
|
|
342
|
+
"diagnostics": diagnostics
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
func _handle_subscribe_debug_output(client_id: int, command_id: String) -> void:
|
|
346
|
+
var publisher := _get_debug_output_publisher()
|
|
347
|
+
if publisher == null:
|
|
348
|
+
_send_error(client_id, "Debug output publisher unavailable.", command_id)
|
|
349
|
+
return
|
|
350
|
+
|
|
351
|
+
publisher.subscribe(client_id)
|
|
352
|
+
_send_success(client_id, {
|
|
353
|
+
"subscribed": true,
|
|
354
|
+
"message": "Live debug output streaming enabled. Future log frames will be delivered asynchronously."
|
|
355
|
+
}, command_id)
|
|
356
|
+
|
|
357
|
+
func _handle_unsubscribe_debug_output(client_id: int, command_id: String) -> void:
|
|
358
|
+
var publisher := _get_debug_output_publisher()
|
|
359
|
+
if publisher == null:
|
|
360
|
+
_send_error(client_id, "Debug output publisher unavailable.", command_id)
|
|
361
|
+
return
|
|
362
|
+
|
|
363
|
+
publisher.unsubscribe(client_id)
|
|
364
|
+
_send_success(client_id, {
|
|
365
|
+
"subscribed": false,
|
|
366
|
+
"message": "Live debug output streaming disabled for this client."
|
|
367
|
+
}, command_id)
|
|
368
|
+
|
|
369
|
+
func _handle_get_stack_trace_panel(client_id: int, params: Dictionary, command_id: String) -> void:
|
|
370
|
+
var publisher := _get_debug_output_publisher()
|
|
371
|
+
var snapshot: Dictionary = {
|
|
372
|
+
"text": "",
|
|
373
|
+
"lines": [],
|
|
374
|
+
"line_count": 0,
|
|
375
|
+
"frames": [],
|
|
376
|
+
"diagnostics": {
|
|
377
|
+
"error": "debug_output_publisher_unavailable",
|
|
378
|
+
"timestamp": Time.get_ticks_msec()
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if publisher and publisher.has_method("get_stack_trace_snapshot"):
|
|
383
|
+
var snapshot_session := int(params.get("session_id", -1))
|
|
384
|
+
snapshot = publisher.get_stack_trace_snapshot(snapshot_session)
|
|
385
|
+
|
|
386
|
+
var snapshot_diagnostics := snapshot.get("diagnostics", {})
|
|
387
|
+
if typeof(snapshot_diagnostics) != TYPE_DICTIONARY:
|
|
388
|
+
snapshot_diagnostics = {}
|
|
389
|
+
snapshot["diagnostics"] = snapshot_diagnostics
|
|
390
|
+
|
|
391
|
+
var debugger_state := {}
|
|
392
|
+
var requested_session := int(params.get("session_id", -1))
|
|
393
|
+
var resolved_session_id := requested_session
|
|
394
|
+
var debugger_error := ""
|
|
395
|
+
var debugger_bridge := _get_debugger_bridge()
|
|
396
|
+
|
|
397
|
+
if debugger_bridge:
|
|
398
|
+
debugger_state = debugger_bridge.get_current_state()
|
|
399
|
+
var active_sessions := debugger_state.get("active_sessions", [])
|
|
400
|
+
if resolved_session_id < 0 and active_sessions is Array and not active_sessions.is_empty():
|
|
401
|
+
resolved_session_id = debugger_state.get("current_session_id", -1)
|
|
402
|
+
if resolved_session_id < 0:
|
|
403
|
+
resolved_session_id = active_sessions[0]
|
|
404
|
+
if resolved_session_id >= 0:
|
|
405
|
+
var frames := []
|
|
406
|
+
if snapshot.has("frames") and snapshot["frames"] is Array:
|
|
407
|
+
frames = snapshot["frames"]
|
|
408
|
+
var lines := []
|
|
409
|
+
if snapshot.has("lines") and snapshot["lines"] is Array:
|
|
410
|
+
lines = snapshot["lines"]
|
|
411
|
+
var needs_stack := frames.is_empty()
|
|
412
|
+
var needs_lines := lines.is_empty()
|
|
413
|
+
if needs_stack or needs_lines:
|
|
414
|
+
if debugger_bridge.has_method("get_cached_stack_info"):
|
|
415
|
+
var fallback := debugger_bridge.get_cached_stack_info(resolved_session_id)
|
|
416
|
+
var fallback_frames := fallback.get("frames", [])
|
|
417
|
+
if fallback_frames is Array and not fallback_frames.is_empty():
|
|
418
|
+
snapshot["frames"] = fallback_frames
|
|
419
|
+
snapshot_diagnostics["fallback_source"] = "debugger_bridge"
|
|
420
|
+
snapshot_diagnostics["fallback_frame_count"] = fallback.get("total_frames", fallback_frames.size())
|
|
421
|
+
if needs_lines:
|
|
422
|
+
var formatted_lines := _format_frames_as_lines(fallback_frames)
|
|
423
|
+
snapshot["lines"] = formatted_lines
|
|
424
|
+
if snapshot.get("text", "") == "":
|
|
425
|
+
snapshot["text"] = "\n".join(formatted_lines)
|
|
426
|
+
elif fallback.has("error"):
|
|
427
|
+
snapshot_diagnostics["fallback_error"] = fallback["error"]
|
|
428
|
+
else:
|
|
429
|
+
debugger_error = "Debugger bridge unavailable"
|
|
430
|
+
|
|
431
|
+
var response := {
|
|
432
|
+
"stack_trace_panel": snapshot,
|
|
433
|
+
"session_id": resolved_session_id,
|
|
434
|
+
"debugger_state": debugger_state,
|
|
435
|
+
"timestamp": Time.get_ticks_msec()
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if not debugger_error.is_empty():
|
|
439
|
+
response["call_stack_error"] = debugger_error
|
|
440
|
+
|
|
441
|
+
_send_success(client_id, response, command_id)
|
|
442
|
+
|
|
443
|
+
func _handle_get_stack_frames_panel(client_id: int, params: Dictionary, command_id: String) -> void:
|
|
444
|
+
var publisher := _get_debug_output_publisher()
|
|
445
|
+
var snapshot: Dictionary = {
|
|
446
|
+
"text": "",
|
|
447
|
+
"lines": [],
|
|
448
|
+
"line_count": 0,
|
|
449
|
+
"frames": [],
|
|
450
|
+
"diagnostics": {
|
|
451
|
+
"error": "debug_output_publisher_unavailable",
|
|
452
|
+
"timestamp": Time.get_ticks_msec()
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if publisher and publisher.has_method("get_stack_frames_snapshot"):
|
|
457
|
+
var snapshot_session := int(params.get("session_id", -1))
|
|
458
|
+
snapshot = publisher.get_stack_frames_snapshot(snapshot_session)
|
|
459
|
+
|
|
460
|
+
var snapshot_diagnostics := snapshot.get("diagnostics", {})
|
|
461
|
+
if typeof(snapshot_diagnostics) != TYPE_DICTIONARY:
|
|
462
|
+
snapshot_diagnostics = {}
|
|
463
|
+
snapshot["diagnostics"] = snapshot_diagnostics
|
|
464
|
+
|
|
465
|
+
var debugger_state := {}
|
|
466
|
+
var requested_session := int(params.get("session_id", -1))
|
|
467
|
+
var resolved_session_id := requested_session
|
|
468
|
+
var debugger_error := ""
|
|
469
|
+
var debugger_bridge := _get_debugger_bridge()
|
|
470
|
+
|
|
471
|
+
var refresh_requested := bool(params.get("refresh", false))
|
|
472
|
+
if debugger_bridge:
|
|
473
|
+
debugger_state = debugger_bridge.get_current_state()
|
|
474
|
+
var active_sessions := debugger_state.get("active_sessions", [])
|
|
475
|
+
if resolved_session_id < 0 and active_sessions is Array and not active_sessions.is_empty():
|
|
476
|
+
resolved_session_id = debugger_state.get("current_session_id", -1)
|
|
477
|
+
if resolved_session_id < 0:
|
|
478
|
+
resolved_session_id = active_sessions[0]
|
|
479
|
+
if resolved_session_id >= 0:
|
|
480
|
+
var frames := []
|
|
481
|
+
if snapshot.has("frames") and snapshot["frames"] is Array:
|
|
482
|
+
frames = snapshot["frames"]
|
|
483
|
+
var needs_stack := frames.is_empty()
|
|
484
|
+
var needs_enrichment := _frames_need_enrichment(frames)
|
|
485
|
+
var existing_lines := []
|
|
486
|
+
if snapshot.has("lines") and snapshot["lines"] is Array:
|
|
487
|
+
existing_lines = snapshot["lines"]
|
|
488
|
+
var needs_lines := existing_lines.is_empty()
|
|
489
|
+
var require_debugger_frames := refresh_requested or needs_stack or needs_lines or needs_enrichment
|
|
490
|
+
if require_debugger_frames and debugger_bridge.has_method("get_cached_stack_info"):
|
|
491
|
+
var debugger_snapshot := await _fetch_debugger_stack_frames(debugger_bridge, resolved_session_id, refresh_requested)
|
|
492
|
+
var fallback_frames := debugger_snapshot.get("frames", [])
|
|
493
|
+
if fallback_frames is Array and not fallback_frames.is_empty():
|
|
494
|
+
snapshot["frames"] = fallback_frames
|
|
495
|
+
snapshot_diagnostics["fallback_source"] = "debugger_bridge"
|
|
496
|
+
snapshot_diagnostics["fallback_frame_count"] = debugger_snapshot.get("total_frames", fallback_frames.size())
|
|
497
|
+
if needs_lines or snapshot.get("lines", []).is_empty():
|
|
498
|
+
var formatted_lines := _format_stack_frames_panel_lines(fallback_frames)
|
|
499
|
+
if not formatted_lines.is_empty():
|
|
500
|
+
snapshot["lines"] = formatted_lines
|
|
501
|
+
snapshot["line_count"] = formatted_lines.size()
|
|
502
|
+
if snapshot.get("text", "") == "":
|
|
503
|
+
snapshot["text"] = "\n".join(formatted_lines)
|
|
504
|
+
needs_stack = false
|
|
505
|
+
needs_enrichment = false
|
|
506
|
+
needs_lines = false
|
|
507
|
+
elif debugger_snapshot.has("error"):
|
|
508
|
+
snapshot_diagnostics["fallback_error"] = debugger_snapshot["error"]
|
|
509
|
+
if needs_lines and snapshot.get("text", "") != "":
|
|
510
|
+
var fallback_lines := String(snapshot["text"]).split("\n", false)
|
|
511
|
+
if not fallback_lines.is_empty():
|
|
512
|
+
snapshot["lines"] = fallback_lines
|
|
513
|
+
snapshot["line_count"] = fallback_lines.size()
|
|
514
|
+
if needs_stack and not snapshot.has("frames"):
|
|
515
|
+
snapshot["frames"] = []
|
|
516
|
+
else:
|
|
517
|
+
debugger_error = "Debugger bridge unavailable"
|
|
518
|
+
|
|
519
|
+
var snapshot_frames := []
|
|
520
|
+
if snapshot.has("frames") and snapshot["frames"] is Array:
|
|
521
|
+
snapshot_frames = snapshot["frames"]
|
|
522
|
+
if snapshot_frames is Array and not snapshot_frames.is_empty():
|
|
523
|
+
var formatted_frame_lines := _format_stack_frames_panel_lines(snapshot_frames)
|
|
524
|
+
if not formatted_frame_lines.is_empty():
|
|
525
|
+
snapshot["lines"] = formatted_frame_lines
|
|
526
|
+
snapshot["line_count"] = formatted_frame_lines.size()
|
|
527
|
+
snapshot["text"] = "\n".join(formatted_frame_lines)
|
|
528
|
+
|
|
529
|
+
var response := {
|
|
530
|
+
"stack_frames_panel": snapshot,
|
|
531
|
+
"session_id": resolved_session_id,
|
|
532
|
+
"debugger_state": debugger_state,
|
|
533
|
+
"timestamp": Time.get_ticks_msec()
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if not debugger_error.is_empty():
|
|
537
|
+
response["stack_frames_error"] = debugger_error
|
|
538
|
+
|
|
539
|
+
_send_success(client_id, response, command_id)
|
|
540
|
+
|
|
541
|
+
func _fetch_debugger_stack_frames(debugger_bridge: MCPDebuggerBridge, session_id: int, refresh_requested: bool) -> Dictionary:
|
|
542
|
+
if debugger_bridge == null or session_id < 0:
|
|
543
|
+
return {}
|
|
544
|
+
var result := {}
|
|
545
|
+
if refresh_requested:
|
|
546
|
+
result = await debugger_bridge.get_call_stack(session_id)
|
|
547
|
+
if typeof(result) == TYPE_DICTIONARY and result.get("frames", []).size() > 0:
|
|
548
|
+
return result
|
|
549
|
+
|
|
550
|
+
result = debugger_bridge.get_cached_stack_info(session_id)
|
|
551
|
+
var frames := result.get("frames", [])
|
|
552
|
+
if frames is Array and not frames.is_empty():
|
|
553
|
+
return result
|
|
554
|
+
|
|
555
|
+
var refreshed_result = await debugger_bridge.get_call_stack(session_id)
|
|
556
|
+
if typeof(refreshed_result) == TYPE_DICTIONARY and refreshed_result.get("frames", []).is_empty() == false:
|
|
557
|
+
return refreshed_result
|
|
558
|
+
|
|
559
|
+
var log_frames := _build_frames_from_debug_output()
|
|
560
|
+
if not log_frames.is_empty():
|
|
561
|
+
return log_frames
|
|
562
|
+
|
|
563
|
+
return result
|
|
564
|
+
|
|
565
|
+
func _frames_need_enrichment(frames: Array) -> bool:
|
|
566
|
+
if frames.is_empty():
|
|
567
|
+
return false
|
|
568
|
+
for frame in frames:
|
|
569
|
+
if typeof(frame) != TYPE_DICTIONARY:
|
|
570
|
+
continue
|
|
571
|
+
var script := ""
|
|
572
|
+
if frame.has("script") and frame["script"] is String:
|
|
573
|
+
script = frame["script"]
|
|
574
|
+
elif frame.has("file") and frame["file"] is String:
|
|
575
|
+
script = frame["file"]
|
|
576
|
+
var line := -1
|
|
577
|
+
if frame.has("line") and frame["line"] is int:
|
|
578
|
+
line = frame["line"]
|
|
579
|
+
if script == "" or line < 0:
|
|
580
|
+
return true
|
|
581
|
+
return false
|
|
582
|
+
|
|
583
|
+
func _handle_clear_debug_output(client_id: int, command_id: String) -> void:
|
|
584
|
+
var publisher := _get_debug_output_publisher()
|
|
585
|
+
if publisher == null or not publisher.has_method("clear_log_output"):
|
|
586
|
+
_send_error(client_id, "Cannot clear Output panel because the debug output publisher is unavailable.", command_id)
|
|
587
|
+
return
|
|
588
|
+
|
|
589
|
+
var result := publisher.clear_log_output()
|
|
590
|
+
if not result.has("message"):
|
|
591
|
+
if result.get("cleared", false):
|
|
592
|
+
result["message"] = "Debug Output panel cleared."
|
|
593
|
+
else:
|
|
594
|
+
result["message"] = "Debug Output panel could not be cleared automatically."
|
|
595
|
+
|
|
596
|
+
_send_success(client_id, result, command_id)
|
|
597
|
+
|
|
598
|
+
func _handle_clear_editor_errors(client_id: int, command_id: String) -> void:
|
|
599
|
+
var publisher := _get_debug_output_publisher()
|
|
600
|
+
if publisher == null or not publisher.has_method("clear_errors_panel"):
|
|
601
|
+
_send_error(client_id, "Cannot clear Errors tab because the debug output publisher is unavailable.", command_id)
|
|
602
|
+
return
|
|
603
|
+
|
|
604
|
+
var result := publisher.clear_errors_panel()
|
|
605
|
+
if not result.has("message"):
|
|
606
|
+
if result.get("cleared", false):
|
|
607
|
+
result["message"] = "Errors tab cleared successfully."
|
|
608
|
+
else:
|
|
609
|
+
result["message"] = "Errors tab could not be cleared automatically."
|
|
610
|
+
|
|
611
|
+
_send_success(client_id, result, command_id)
|
|
612
|
+
|
|
613
|
+
func _get_debug_output_publisher() -> MCPDebugOutputPublisher:
|
|
614
|
+
if Engine.has_meta("MCPDebugOutputPublisher"):
|
|
615
|
+
var publisher = Engine.get_meta("MCPDebugOutputPublisher")
|
|
616
|
+
if publisher and publisher is MCPDebugOutputPublisher:
|
|
617
|
+
return publisher
|
|
618
|
+
return null
|
|
619
|
+
|
|
620
|
+
func _capture_editor_errors_snapshot() -> Dictionary:
|
|
621
|
+
var publisher := _get_debug_output_publisher()
|
|
622
|
+
if publisher and publisher.has_method("get_errors_panel_snapshot"):
|
|
623
|
+
return publisher.get_errors_panel_snapshot()
|
|
624
|
+
|
|
625
|
+
var diagnostics := {
|
|
626
|
+
"timestamp": Time.get_ticks_msec()
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if not Engine.is_editor_hint():
|
|
630
|
+
diagnostics["error"] = "editor_only"
|
|
631
|
+
return {
|
|
632
|
+
"text": "",
|
|
633
|
+
"lines": [],
|
|
634
|
+
"line_count": 0,
|
|
635
|
+
"diagnostics": diagnostics
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
var search_roots: Array = []
|
|
639
|
+
var editor_node = Engine.get_singleton("EditorNode") if Engine.has_singleton("EditorNode") else null
|
|
640
|
+
if editor_node:
|
|
641
|
+
if editor_node.has_method("get_log"):
|
|
642
|
+
var editor_log = editor_node.call("get_log")
|
|
643
|
+
if is_instance_valid(editor_log):
|
|
644
|
+
search_roots.append(editor_log)
|
|
645
|
+
search_roots.append(editor_node)
|
|
646
|
+
|
|
647
|
+
var plugin = Engine.get_meta("GodotMCPPlugin") if Engine.has_meta("GodotMCPPlugin") else null
|
|
648
|
+
if plugin and plugin is EditorPlugin:
|
|
649
|
+
var editor_interface = plugin.get_editor_interface()
|
|
650
|
+
if editor_interface and editor_interface.has_method("get_base_control"):
|
|
651
|
+
var base_control = editor_interface.call("get_base_control")
|
|
652
|
+
if is_instance_valid(base_control):
|
|
653
|
+
search_roots.append(base_control)
|
|
654
|
+
|
|
655
|
+
var scene_tree := get_tree()
|
|
656
|
+
if scene_tree:
|
|
657
|
+
var tree_root := scene_tree.get_root()
|
|
658
|
+
if is_instance_valid(tree_root):
|
|
659
|
+
search_roots.append(tree_root)
|
|
660
|
+
|
|
661
|
+
var aggregated_summary: Array = []
|
|
662
|
+
var tab_info := {}
|
|
663
|
+
for root in search_roots:
|
|
664
|
+
tab_info = _find_errors_tab_in_editor_log(root)
|
|
665
|
+
var summary := String(tab_info.get("summary", ""))
|
|
666
|
+
if not summary.is_empty():
|
|
667
|
+
aggregated_summary.append(summary)
|
|
668
|
+
if tab_info.has("control"):
|
|
669
|
+
break
|
|
670
|
+
if aggregated_summary.size() > 0:
|
|
671
|
+
diagnostics["search_summary"] = " | ".join(aggregated_summary)
|
|
672
|
+
else:
|
|
673
|
+
diagnostics["search_summary"] = ""
|
|
674
|
+
|
|
675
|
+
if tab_info.is_empty() or not tab_info.has("control"):
|
|
676
|
+
diagnostics["error"] = "errors_tab_not_found"
|
|
677
|
+
return {
|
|
678
|
+
"text": "",
|
|
679
|
+
"lines": [],
|
|
680
|
+
"line_count": 0,
|
|
681
|
+
"diagnostics": diagnostics
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
var tab_control: Control = tab_info.get("control")
|
|
685
|
+
if not is_instance_valid(tab_control):
|
|
686
|
+
diagnostics["error"] = "tab_control_invalid"
|
|
687
|
+
return {
|
|
688
|
+
"text": "",
|
|
689
|
+
"lines": [],
|
|
690
|
+
"line_count": 0,
|
|
691
|
+
"diagnostics": diagnostics
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
diagnostics["tab_title"] = tab_info.get("tab_title", "")
|
|
695
|
+
if tab_control.is_inside_tree():
|
|
696
|
+
diagnostics["control_path"] = String(tab_control.get_path())
|
|
697
|
+
else:
|
|
698
|
+
diagnostics["control_path"] = ""
|
|
699
|
+
|
|
700
|
+
var lines: Array = []
|
|
701
|
+
var text := ""
|
|
702
|
+
var tree := _locate_descendant_tree(tab_control)
|
|
703
|
+
if tree:
|
|
704
|
+
if tree.is_inside_tree():
|
|
705
|
+
diagnostics["tree_path"] = String(tree.get_path())
|
|
706
|
+
lines = _collect_tree_lines(tree)
|
|
707
|
+
text = "\n".join(lines)
|
|
708
|
+
else:
|
|
709
|
+
text = _extract_text_from_control_local(tab_control)
|
|
710
|
+
if not text.is_empty():
|
|
711
|
+
lines = text.split("\n", false)
|
|
712
|
+
|
|
713
|
+
return {
|
|
714
|
+
"text": text,
|
|
715
|
+
"lines": lines,
|
|
716
|
+
"line_count": lines.size(),
|
|
717
|
+
"diagnostics": diagnostics
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
func _find_errors_tab_in_editor_log(root: Node) -> Dictionary:
|
|
721
|
+
var queue: Array = []
|
|
722
|
+
var summary: Array = []
|
|
723
|
+
if is_instance_valid(root):
|
|
724
|
+
queue.append(root)
|
|
725
|
+
else:
|
|
726
|
+
return {}
|
|
727
|
+
|
|
728
|
+
var visited := 0
|
|
729
|
+
while queue.size() > 0:
|
|
730
|
+
var candidate = queue.pop_front()
|
|
731
|
+
if not is_instance_valid(candidate):
|
|
732
|
+
continue
|
|
733
|
+
visited += 1
|
|
734
|
+
|
|
735
|
+
if candidate is TabContainer:
|
|
736
|
+
var tab_container: TabContainer = candidate
|
|
737
|
+
var tab_count: int = tab_container.get_tab_count() if tab_container.has_method("get_tab_count") else tab_container.get_child_count()
|
|
738
|
+
for i in range(tab_count):
|
|
739
|
+
var title := ""
|
|
740
|
+
if tab_container.has_method("get_tab_title"):
|
|
741
|
+
title = String(tab_container.get_tab_title(i))
|
|
742
|
+
var title_lower := title.to_lower()
|
|
743
|
+
if title_lower.find("error") != -1:
|
|
744
|
+
var tab_control: Control = null
|
|
745
|
+
if tab_container.has_method("get_tab_control"):
|
|
746
|
+
tab_control = tab_container.get_tab_control(i)
|
|
747
|
+
if not is_instance_valid(tab_control) and i < tab_container.get_child_count():
|
|
748
|
+
var child = tab_container.get_child(i)
|
|
749
|
+
if child is Control:
|
|
750
|
+
tab_control = child
|
|
751
|
+
|
|
752
|
+
if is_instance_valid(tab_control):
|
|
753
|
+
tab_control = _unwrap_single_child_control(tab_control)
|
|
754
|
+
summary.append("tab_found=%s" % title)
|
|
755
|
+
summary.append("visited=%d" % visited)
|
|
756
|
+
return {
|
|
757
|
+
"control": tab_control,
|
|
758
|
+
"tab_title": title,
|
|
759
|
+
"summary": "; ".join(summary)
|
|
760
|
+
}
|
|
761
|
+
for child in candidate.get_children():
|
|
762
|
+
if child is Node:
|
|
763
|
+
queue.append(child)
|
|
764
|
+
|
|
765
|
+
return {"summary": "; ".join(summary)}
|
|
766
|
+
|
|
767
|
+
func _unwrap_single_child_control(control: Control) -> Control:
|
|
768
|
+
if not is_instance_valid(control):
|
|
769
|
+
return control
|
|
770
|
+
|
|
771
|
+
var current := control
|
|
772
|
+
var safety := 0
|
|
773
|
+
while safety < 5 and current.get_child_count() == 1:
|
|
774
|
+
var child = current.get_child(0)
|
|
775
|
+
if child is Control:
|
|
776
|
+
current = child
|
|
777
|
+
safety += 1
|
|
778
|
+
else:
|
|
779
|
+
break
|
|
780
|
+
|
|
781
|
+
if current.get_child_count() > 1:
|
|
782
|
+
for child in current.get_children():
|
|
783
|
+
if child is Control and (_is_text_display_control_local(child)):
|
|
784
|
+
return child
|
|
785
|
+
|
|
786
|
+
return current
|
|
787
|
+
|
|
788
|
+
func _locate_descendant_tree(root: Node, max_nodes: int = 8192) -> Tree:
|
|
789
|
+
if not is_instance_valid(root):
|
|
790
|
+
return null
|
|
791
|
+
var queue: Array = [root]
|
|
792
|
+
var visited := 0
|
|
793
|
+
while queue.size() > 0 and visited < max_nodes:
|
|
794
|
+
var candidate = queue.pop_front()
|
|
795
|
+
visited += 1
|
|
796
|
+
if not is_instance_valid(candidate):
|
|
797
|
+
continue
|
|
798
|
+
if candidate is Tree:
|
|
799
|
+
return candidate
|
|
800
|
+
for child in candidate.get_children():
|
|
801
|
+
if child is Node:
|
|
802
|
+
queue.append(child)
|
|
803
|
+
return null
|
|
804
|
+
|
|
805
|
+
func _collect_tree_lines(tree: Tree) -> Array:
|
|
806
|
+
var lines: Array = []
|
|
807
|
+
if not is_instance_valid(tree):
|
|
808
|
+
return lines
|
|
809
|
+
var root := tree.get_root()
|
|
810
|
+
if not root:
|
|
811
|
+
return lines
|
|
812
|
+
var item := root.get_first_child()
|
|
813
|
+
var column_count: int = tree.columns
|
|
814
|
+
while item:
|
|
815
|
+
_collect_tree_item_lines(item, lines, 0, column_count)
|
|
816
|
+
item = item.get_next()
|
|
817
|
+
return lines
|
|
818
|
+
|
|
819
|
+
func _collect_tree_item_lines(item: TreeItem, lines: Array, depth: int, column_count: int) -> void:
|
|
820
|
+
if not is_instance_valid(item):
|
|
821
|
+
return
|
|
822
|
+
|
|
823
|
+
var parts: Array = []
|
|
824
|
+
if item.has_meta("_is_warning"):
|
|
825
|
+
parts.append("[warning]")
|
|
826
|
+
elif item.has_meta("_is_error"):
|
|
827
|
+
parts.append("[error]")
|
|
828
|
+
|
|
829
|
+
var primary := item.get_text(0)
|
|
830
|
+
if not primary.is_empty():
|
|
831
|
+
parts.append(primary)
|
|
832
|
+
|
|
833
|
+
for col in range(1, column_count):
|
|
834
|
+
var extra := item.get_text(col)
|
|
835
|
+
if not extra.is_empty():
|
|
836
|
+
parts.append(extra)
|
|
837
|
+
|
|
838
|
+
if parts.size() > 0:
|
|
839
|
+
var prefix := _make_indent_local(depth)
|
|
840
|
+
lines.append(prefix + " ".join(parts))
|
|
841
|
+
|
|
842
|
+
var child := item.get_first_child()
|
|
843
|
+
while child:
|
|
844
|
+
_collect_tree_item_lines(child, lines, depth + 1, column_count)
|
|
845
|
+
child = child.get_next()
|
|
846
|
+
|
|
847
|
+
func _is_text_display_control_local(control: Control) -> bool:
|
|
848
|
+
if not is_instance_valid(control):
|
|
849
|
+
return false
|
|
850
|
+
return control.is_class("TextEdit") or control.is_class("CodeEdit") or control.is_class("RichTextLabel")
|
|
851
|
+
|
|
852
|
+
func _extract_text_from_control_local(control: Object) -> String:
|
|
853
|
+
if not is_instance_valid(control):
|
|
854
|
+
return ""
|
|
855
|
+
|
|
856
|
+
if control.has_method("get_parsed_text"):
|
|
857
|
+
return String(control.call("get_parsed_text"))
|
|
858
|
+
if control.has_method("get_text"):
|
|
859
|
+
return String(control.call("get_text"))
|
|
860
|
+
if control.has_method("get_full_text"):
|
|
861
|
+
return String(control.call("get_full_text"))
|
|
862
|
+
if control.has_method("get_line_count") and control.has_method("get_line"):
|
|
863
|
+
var lines: Array = []
|
|
864
|
+
var count := int(control.call("get_line_count"))
|
|
865
|
+
for i in range(count):
|
|
866
|
+
lines.append(String(control.call("get_line", i)))
|
|
867
|
+
return "\n".join(lines)
|
|
868
|
+
return ""
|
|
869
|
+
|
|
870
|
+
func _make_indent_local(depth: int) -> String:
|
|
871
|
+
if depth <= 0:
|
|
872
|
+
return ""
|
|
873
|
+
var spaces := depth * 2
|
|
874
|
+
var builder := ""
|
|
875
|
+
for i in range(spaces):
|
|
876
|
+
builder += " "
|
|
877
|
+
return builder
|
|
878
|
+
|
|
879
|
+
# ---- Node Transform Commands ----
|
|
880
|
+
|
|
881
|
+
func _handle_update_node_transform(client_id: int, params: Dictionary, command_id: String) -> void:
|
|
882
|
+
var node_path = params.get("node_path", "")
|
|
883
|
+
var position = params.get("position", null)
|
|
884
|
+
var rotation = params.get("rotation", null)
|
|
885
|
+
var scale = params.get("scale", null)
|
|
886
|
+
|
|
887
|
+
var result = update_node_transform(node_path, position, rotation, scale)
|
|
888
|
+
|
|
889
|
+
_send_success(client_id, result, command_id)
|
|
890
|
+
|
|
891
|
+
func update_node_transform(node_path: String, position, rotation, scale) -> Dictionary:
|
|
892
|
+
var editor_interface = _get_editor_interface()
|
|
893
|
+
|
|
894
|
+
if not editor_interface:
|
|
895
|
+
return { "error": "Could not access EditorInterface" }
|
|
896
|
+
|
|
897
|
+
var scene_root = editor_interface.get_edited_scene_root()
|
|
898
|
+
|
|
899
|
+
if not scene_root:
|
|
900
|
+
return { "error": "No scene open" }
|
|
901
|
+
|
|
902
|
+
var node = scene_root.get_node_or_null(node_path)
|
|
903
|
+
if not node:
|
|
904
|
+
return { "error": "Node not found" }
|
|
905
|
+
|
|
906
|
+
# Update all specified properties
|
|
907
|
+
if position != null and node.has_method("set_position"):
|
|
908
|
+
if position is Array and position.size() >= 2:
|
|
909
|
+
node.set_position(Vector2(position[0], position[1]))
|
|
910
|
+
elif typeof(position) == TYPE_DICTIONARY and "x" in position and "y" in position:
|
|
911
|
+
node.set_position(Vector2(position.x, position.y))
|
|
912
|
+
|
|
913
|
+
if rotation != null and node.has_method("set_rotation"):
|
|
914
|
+
node.set_rotation(rotation)
|
|
915
|
+
|
|
916
|
+
if scale != null and node.has_method("set_scale"):
|
|
917
|
+
if scale is Array and scale.size() >= 2:
|
|
918
|
+
node.set_scale(Vector2(scale[0], scale[1]))
|
|
919
|
+
elif typeof(scale) == TYPE_DICTIONARY and "x" in scale and "y" in scale:
|
|
920
|
+
node.set_scale(Vector2(scale.x, scale.y))
|
|
921
|
+
|
|
922
|
+
# Mark the scene as modified
|
|
923
|
+
editor_interface.mark_scene_as_unsaved()
|
|
924
|
+
|
|
925
|
+
return {
|
|
926
|
+
"success": true,
|
|
927
|
+
"node_path": node_path,
|
|
928
|
+
"updated": {
|
|
929
|
+
"position": position != null,
|
|
930
|
+
"rotation": rotation != null,
|
|
931
|
+
"scale": scale != null
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
func _format_frames_as_lines(frames: Array) -> Array:
|
|
936
|
+
var lines: Array = []
|
|
937
|
+
for i in range(frames.size()):
|
|
938
|
+
var frame = frames[i]
|
|
939
|
+
if typeof(frame) != TYPE_DICTIONARY:
|
|
940
|
+
continue
|
|
941
|
+
var index: int = i
|
|
942
|
+
if frame.has("index") and frame["index"] is int:
|
|
943
|
+
index = frame["index"]
|
|
944
|
+
var script: String = ""
|
|
945
|
+
if frame.has("script") and frame["script"] is String:
|
|
946
|
+
script = frame["script"]
|
|
947
|
+
elif frame.has("file") and frame["file"] is String:
|
|
948
|
+
script = frame["file"]
|
|
949
|
+
var line_num: int = -1
|
|
950
|
+
if frame.has("line") and frame["line"] is int:
|
|
951
|
+
line_num = frame["line"]
|
|
952
|
+
var function_name: String = ""
|
|
953
|
+
if frame.has("function") and frame["function"] is String:
|
|
954
|
+
function_name = frame["function"]
|
|
955
|
+
var location := ""
|
|
956
|
+
if script is String and not script.is_empty():
|
|
957
|
+
location = script
|
|
958
|
+
if line_num is int and line_num >= 0:
|
|
959
|
+
if location.is_empty():
|
|
960
|
+
location = ":%d" % line_num
|
|
961
|
+
else:
|
|
962
|
+
location += ":%d" % line_num
|
|
963
|
+
if location.is_empty():
|
|
964
|
+
location = String(frame.get("location", "unknown location"))
|
|
965
|
+
var fn_display := function_name if function_name is String and not function_name.is_empty() else "(anonymous)"
|
|
966
|
+
lines.append("#%d %s — %s" % [index, fn_display, location])
|
|
967
|
+
return lines
|
|
968
|
+
|
|
969
|
+
func _build_frames_from_debug_output() -> Dictionary:
|
|
970
|
+
var frames := _parse_frames_from_debug_output()
|
|
971
|
+
if frames.is_empty():
|
|
972
|
+
return {}
|
|
973
|
+
return {
|
|
974
|
+
"frames": frames,
|
|
975
|
+
"total_frames": frames.size(),
|
|
976
|
+
"current_frame": 0,
|
|
977
|
+
"source": "debug_output"
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
func _parse_frames_from_debug_output() -> Array:
|
|
981
|
+
var publisher := _get_debug_output_publisher()
|
|
982
|
+
if publisher == null or not publisher.has_method("get_full_log_text"):
|
|
983
|
+
return []
|
|
984
|
+
var text := publisher.get_full_log_text()
|
|
985
|
+
if text.is_empty():
|
|
986
|
+
return []
|
|
987
|
+
var lines := text.split("\n", false)
|
|
988
|
+
var frame_lines: Array = []
|
|
989
|
+
var collecting := false
|
|
990
|
+
for i in range(lines.size() - 1, -1, -1):
|
|
991
|
+
var line := String(lines[i]).strip_edges()
|
|
992
|
+
if line.begins_with("Frame "):
|
|
993
|
+
collecting = true
|
|
994
|
+
frame_lines.push_front(line)
|
|
995
|
+
elif collecting:
|
|
996
|
+
break
|
|
997
|
+
if frame_lines.is_empty():
|
|
998
|
+
return []
|
|
999
|
+
var frames := []
|
|
1000
|
+
for line in frame_lines:
|
|
1001
|
+
var frame_dict := _parse_print_stack_line(line)
|
|
1002
|
+
if not frame_dict.is_empty():
|
|
1003
|
+
frames.append(frame_dict)
|
|
1004
|
+
return frames
|
|
1005
|
+
|
|
1006
|
+
func _parse_print_stack_line(line: String) -> Dictionary:
|
|
1007
|
+
var trimmed := line.strip_edges()
|
|
1008
|
+
if not trimmed.begins_with("Frame "):
|
|
1009
|
+
return {}
|
|
1010
|
+
var dash_index := trimmed.find(" - ")
|
|
1011
|
+
if dash_index == -1:
|
|
1012
|
+
return {}
|
|
1013
|
+
var index_text := trimmed.substr(6, dash_index - 6).strip_edges()
|
|
1014
|
+
var index_value := 0
|
|
1015
|
+
if index_text.is_valid_int():
|
|
1016
|
+
index_value = int(index_text)
|
|
1017
|
+
var rest := trimmed.substr(dash_index + 3).strip_edges()
|
|
1018
|
+
var func_name := ""
|
|
1019
|
+
var location := rest
|
|
1020
|
+
var at_index := rest.find("@")
|
|
1021
|
+
if at_index != -1:
|
|
1022
|
+
location = rest.substr(0, at_index).strip_edges()
|
|
1023
|
+
func_name = rest.substr(at_index + 1).strip_edges()
|
|
1024
|
+
if func_name.ends_with("()"):
|
|
1025
|
+
func_name = func_name.substr(0, func_name.length() - 2)
|
|
1026
|
+
var script_path := ""
|
|
1027
|
+
var line_number := -1
|
|
1028
|
+
var colon_index := location.rfind(":")
|
|
1029
|
+
if colon_index != -1:
|
|
1030
|
+
var line_text := location.substr(colon_index + 1).strip_edges()
|
|
1031
|
+
if line_text.is_valid_int():
|
|
1032
|
+
line_number = int(line_text)
|
|
1033
|
+
script_path = location.substr(0, colon_index).strip_edges()
|
|
1034
|
+
else:
|
|
1035
|
+
script_path = location
|
|
1036
|
+
return {
|
|
1037
|
+
"index": index_value,
|
|
1038
|
+
"function": func_name,
|
|
1039
|
+
"script": script_path,
|
|
1040
|
+
"file": script_path,
|
|
1041
|
+
"line": line_number,
|
|
1042
|
+
"location": location
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
func _format_stack_frames_panel_lines(frames: Array) -> Array:
|
|
1046
|
+
var lines: Array = []
|
|
1047
|
+
for i in range(frames.size()):
|
|
1048
|
+
var frame = frames[i]
|
|
1049
|
+
if typeof(frame) != TYPE_DICTIONARY:
|
|
1050
|
+
continue
|
|
1051
|
+
var index_value := i
|
|
1052
|
+
if frame.has("index"):
|
|
1053
|
+
var raw_index = frame["index"]
|
|
1054
|
+
if typeof(raw_index) == TYPE_INT:
|
|
1055
|
+
index_value = raw_index
|
|
1056
|
+
elif typeof(raw_index) == TYPE_STRING:
|
|
1057
|
+
var index_text := String(raw_index).strip_edges()
|
|
1058
|
+
if index_text.is_valid_int():
|
|
1059
|
+
index_value = int(index_text)
|
|
1060
|
+
var script_path := ""
|
|
1061
|
+
if frame.has("script") and typeof(frame["script"]) == TYPE_STRING:
|
|
1062
|
+
script_path = frame["script"]
|
|
1063
|
+
elif frame.has("file") and typeof(frame["file"]) == TYPE_STRING:
|
|
1064
|
+
script_path = frame["file"]
|
|
1065
|
+
var line_number := -1
|
|
1066
|
+
if frame.has("line") and typeof(frame["line"]) == TYPE_INT:
|
|
1067
|
+
line_number = frame["line"]
|
|
1068
|
+
var location := ""
|
|
1069
|
+
if not script_path.is_empty():
|
|
1070
|
+
location = script_path
|
|
1071
|
+
if line_number >= 0:
|
|
1072
|
+
location += ":%d" % line_number
|
|
1073
|
+
else:
|
|
1074
|
+
location = String(frame.get("location", ""))
|
|
1075
|
+
if location.is_empty():
|
|
1076
|
+
location = "<unknown>"
|
|
1077
|
+
var function_name := ""
|
|
1078
|
+
if frame.has("function") and typeof(frame["function"]) == TYPE_STRING:
|
|
1079
|
+
function_name = frame["function"]
|
|
1080
|
+
if function_name.is_empty():
|
|
1081
|
+
function_name = "(anonymous)"
|
|
1082
|
+
lines.append("%d - %s - at function: %s" % [index_value, location, function_name])
|
|
1083
|
+
return lines
|