@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,1669 @@
|
|
|
1
|
+
@tool
|
|
2
|
+
class_name MCPDebugOutputPublisher
|
|
3
|
+
extends Node
|
|
4
|
+
|
|
5
|
+
const POLL_INTERVAL_SECONDS := 0.5
|
|
6
|
+
const OUTPUT_SCORE_THRESHOLD := 60
|
|
7
|
+
const OUTPUT_KEYWORDS := ["output", "console", "log", "stdout"]
|
|
8
|
+
const ERRORS_SCORE_THRESHOLD := 25
|
|
9
|
+
const ERRORS_KEYWORDS := [
|
|
10
|
+
"error", "errors", "warning", "warnings",
|
|
11
|
+
"stack", "trace", "gdscript", "issues"
|
|
12
|
+
]
|
|
13
|
+
const STACK_TRACE_SCORE_THRESHOLD := 65
|
|
14
|
+
const STACK_TRACE_KEYWORDS := [
|
|
15
|
+
"stack", "trace", "callstack", "call_stack", "stack trace", "call stack"
|
|
16
|
+
]
|
|
17
|
+
const STACK_FRAMES_SCORE_THRESHOLD := 65
|
|
18
|
+
const STACK_FRAMES_KEYWORDS := [
|
|
19
|
+
"stack frames",
|
|
20
|
+
"frames",
|
|
21
|
+
"frame list",
|
|
22
|
+
"call frames",
|
|
23
|
+
"call stack",
|
|
24
|
+
"stackframe",
|
|
25
|
+
"frames panel"
|
|
26
|
+
]
|
|
27
|
+
const SUMMARY_VALUE_MAX_LEN := 160
|
|
28
|
+
|
|
29
|
+
var websocket_server: MCPWebSocketServer
|
|
30
|
+
|
|
31
|
+
var _subscribers: Dictionary = {}
|
|
32
|
+
var _elapsed := 0.0
|
|
33
|
+
var _last_length := 0
|
|
34
|
+
var _cached_output_control: Control = null
|
|
35
|
+
var _last_capture_source := "unset"
|
|
36
|
+
var _last_capture_detail := ""
|
|
37
|
+
var _last_capture_timestamp := 0
|
|
38
|
+
var _last_control_class := ""
|
|
39
|
+
var _last_control_path := ""
|
|
40
|
+
var _last_log_file_path := ""
|
|
41
|
+
var _last_control_search_summary := ""
|
|
42
|
+
|
|
43
|
+
func _ready() -> void:
|
|
44
|
+
set_process(false)
|
|
45
|
+
|
|
46
|
+
func subscribe(client_id: int) -> void:
|
|
47
|
+
_subscribers[client_id] = true
|
|
48
|
+
_initialize_baseline()
|
|
49
|
+
if not is_processing():
|
|
50
|
+
set_process(true)
|
|
51
|
+
|
|
52
|
+
func unsubscribe(client_id: int) -> void:
|
|
53
|
+
if _subscribers.erase(client_id) and _subscribers.is_empty():
|
|
54
|
+
set_process(false)
|
|
55
|
+
|
|
56
|
+
func unsubscribe_all() -> void:
|
|
57
|
+
_subscribers.clear()
|
|
58
|
+
set_process(false)
|
|
59
|
+
|
|
60
|
+
func _process(delta: float) -> void:
|
|
61
|
+
if _subscribers.is_empty():
|
|
62
|
+
set_process(false)
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
_elapsed += delta
|
|
66
|
+
if _elapsed < POLL_INTERVAL_SECONDS:
|
|
67
|
+
return
|
|
68
|
+
_elapsed = 0.0
|
|
69
|
+
|
|
70
|
+
_publish_incremental_frame()
|
|
71
|
+
|
|
72
|
+
func _initialize_baseline() -> void:
|
|
73
|
+
var current_text := _fetch_log_text()
|
|
74
|
+
if current_text == null:
|
|
75
|
+
_last_length = 0
|
|
76
|
+
else:
|
|
77
|
+
_last_length = current_text.length()
|
|
78
|
+
|
|
79
|
+
func _publish_incremental_frame() -> void:
|
|
80
|
+
var text := _fetch_log_text()
|
|
81
|
+
if text == null:
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
var reset := false
|
|
85
|
+
if text.length() < _last_length:
|
|
86
|
+
reset = true
|
|
87
|
+
|
|
88
|
+
var chunk := ""
|
|
89
|
+
if reset:
|
|
90
|
+
chunk = text
|
|
91
|
+
else:
|
|
92
|
+
if text.length() == _last_length:
|
|
93
|
+
return
|
|
94
|
+
chunk = text.substr(_last_length, text.length() - _last_length)
|
|
95
|
+
|
|
96
|
+
_last_length = text.length()
|
|
97
|
+
|
|
98
|
+
if chunk.is_empty() and not reset:
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
var lines: Array = []
|
|
102
|
+
if not chunk.is_empty():
|
|
103
|
+
lines = chunk.split("\n", false)
|
|
104
|
+
# Drop trailing empty line caused by split behaviour.
|
|
105
|
+
if lines.size() > 0 and String(lines.back()).is_empty():
|
|
106
|
+
lines.pop_back()
|
|
107
|
+
|
|
108
|
+
var payload := {
|
|
109
|
+
"event": "debug_output_frame",
|
|
110
|
+
"data": {
|
|
111
|
+
"timestamp": Time.get_ticks_msec(),
|
|
112
|
+
"chunk": chunk,
|
|
113
|
+
"lines": lines,
|
|
114
|
+
"reset": reset
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
for client_id in _subscribers.keys():
|
|
119
|
+
_send_event_to_client(int(client_id), payload)
|
|
120
|
+
|
|
121
|
+
func _send_event_to_client(client_id: int, payload: Dictionary) -> void:
|
|
122
|
+
if websocket_server == null:
|
|
123
|
+
return
|
|
124
|
+
websocket_server.send_event(client_id, payload)
|
|
125
|
+
|
|
126
|
+
func _fetch_log_text() -> String:
|
|
127
|
+
var control := _get_output_control()
|
|
128
|
+
var detail_notes: Array = []
|
|
129
|
+
var text := ""
|
|
130
|
+
_last_control_class = ""
|
|
131
|
+
|
|
132
|
+
if is_instance_valid(control):
|
|
133
|
+
_last_control_class = control.get_class()
|
|
134
|
+
if control.is_inside_tree():
|
|
135
|
+
_last_control_path = String(control.get_path())
|
|
136
|
+
else:
|
|
137
|
+
_last_control_path = ""
|
|
138
|
+
|
|
139
|
+
text = _extract_text_from_control(control)
|
|
140
|
+
if not text.is_empty():
|
|
141
|
+
_record_capture("control", "class=%s len=%d" % [_last_control_class, text.length()])
|
|
142
|
+
return text
|
|
143
|
+
detail_notes.append("control(%s) empty" % _last_control_class)
|
|
144
|
+
else:
|
|
145
|
+
_last_control_path = ""
|
|
146
|
+
detail_notes.append("control missing")
|
|
147
|
+
|
|
148
|
+
text = _fetch_debugger_log_text()
|
|
149
|
+
if not text.is_empty():
|
|
150
|
+
_record_capture("debugger", "len=%d" % text.length())
|
|
151
|
+
return text
|
|
152
|
+
detail_notes.append("debugger empty")
|
|
153
|
+
|
|
154
|
+
text = _fetch_log_file_text()
|
|
155
|
+
if not text.is_empty():
|
|
156
|
+
var path_info := _last_log_file_path if not _last_log_file_path.is_empty() else "unknown"
|
|
157
|
+
_record_capture("file", "path=%s len=%d" % [path_info, text.length()])
|
|
158
|
+
return text
|
|
159
|
+
detail_notes.append("log file missing")
|
|
160
|
+
if not _last_control_search_summary.is_empty():
|
|
161
|
+
detail_notes.append(_last_control_search_summary)
|
|
162
|
+
|
|
163
|
+
_record_capture("none", "; ".join(detail_notes))
|
|
164
|
+
return ""
|
|
165
|
+
|
|
166
|
+
func _record_capture(source: String, detail: String) -> void:
|
|
167
|
+
_last_capture_source = source
|
|
168
|
+
_last_capture_detail = detail
|
|
169
|
+
_last_capture_timestamp = Time.get_ticks_msec()
|
|
170
|
+
|
|
171
|
+
func _extract_text_from_control(control: Object) -> String:
|
|
172
|
+
if not is_instance_valid(control):
|
|
173
|
+
return ""
|
|
174
|
+
|
|
175
|
+
if control.has_method("get_parsed_text"):
|
|
176
|
+
return String(control.call("get_parsed_text"))
|
|
177
|
+
if control.has_method("get_text"):
|
|
178
|
+
return String(control.call("get_text"))
|
|
179
|
+
if control.has_method("get_full_text"):
|
|
180
|
+
return String(control.call("get_full_text"))
|
|
181
|
+
if control.has_method("get_line_count") and control.has_method("get_line"):
|
|
182
|
+
var lines: Array = []
|
|
183
|
+
var count := int(control.call("get_line_count"))
|
|
184
|
+
for i in range(count):
|
|
185
|
+
lines.append(String(control.call("get_line", i)))
|
|
186
|
+
return "\n".join(lines)
|
|
187
|
+
return ""
|
|
188
|
+
|
|
189
|
+
func _fetch_debugger_log_text() -> String:
|
|
190
|
+
if Engine.has_singleton("EditorDebuggerNode"):
|
|
191
|
+
var debugger = Engine.get_singleton("EditorDebuggerNode")
|
|
192
|
+
if debugger and debugger.has_method("get_log"):
|
|
193
|
+
return String(debugger.call("get_log"))
|
|
194
|
+
if has_node("/root/EditorNode/DebuggerPanel"):
|
|
195
|
+
var debugger_panel = get_node("/root/EditorNode/DebuggerPanel")
|
|
196
|
+
if debugger_panel and debugger_panel.has_method("get_output"):
|
|
197
|
+
return String(debugger_panel.call("get_output"))
|
|
198
|
+
return ""
|
|
199
|
+
|
|
200
|
+
func _fetch_log_file_text() -> String:
|
|
201
|
+
if not Engine.is_editor_hint():
|
|
202
|
+
return ""
|
|
203
|
+
var user_dir := ProjectSettings.globalize_path("user://")
|
|
204
|
+
_last_log_file_path = ""
|
|
205
|
+
var path_candidates: Array = [
|
|
206
|
+
"editor/editor.log",
|
|
207
|
+
"user/editor/editor.log",
|
|
208
|
+
"logs/editor.log",
|
|
209
|
+
"editor.log"
|
|
210
|
+
]
|
|
211
|
+
|
|
212
|
+
for relative_path in path_candidates:
|
|
213
|
+
var candidate := user_dir.path_join(relative_path)
|
|
214
|
+
if FileAccess.file_exists(candidate):
|
|
215
|
+
_last_log_file_path = candidate
|
|
216
|
+
break
|
|
217
|
+
|
|
218
|
+
if _last_log_file_path.is_empty():
|
|
219
|
+
return ""
|
|
220
|
+
|
|
221
|
+
var file := FileAccess.open(_last_log_file_path, FileAccess.READ)
|
|
222
|
+
if file == null:
|
|
223
|
+
_last_log_file_path = ""
|
|
224
|
+
return ""
|
|
225
|
+
|
|
226
|
+
var content := file.get_as_text()
|
|
227
|
+
file.close()
|
|
228
|
+
return content
|
|
229
|
+
|
|
230
|
+
func _locate_output_control() -> Control:
|
|
231
|
+
var result := _locate_control_with_scoring(Callable(self, "_score_output_control"), OUTPUT_SCORE_THRESHOLD)
|
|
232
|
+
_last_control_search_summary = String(result.get("summary", ""))
|
|
233
|
+
return result.get("control")
|
|
234
|
+
|
|
235
|
+
func _locate_control_with_scoring(scoring_func: Callable, threshold: int) -> Dictionary:
|
|
236
|
+
var summary: Array = []
|
|
237
|
+
|
|
238
|
+
if not Engine.is_editor_hint():
|
|
239
|
+
return {
|
|
240
|
+
"control": null,
|
|
241
|
+
"summary": "editor_hint=false"
|
|
242
|
+
}
|
|
243
|
+
summary.append("editor_hint=true")
|
|
244
|
+
|
|
245
|
+
var base_control_result: Dictionary = {}
|
|
246
|
+
var base_control: Control = null
|
|
247
|
+
|
|
248
|
+
if Engine.has_meta("GodotMCPPlugin"):
|
|
249
|
+
var plugin = Engine.get_meta("GodotMCPPlugin")
|
|
250
|
+
if plugin and plugin is EditorPlugin:
|
|
251
|
+
var editor_interface = plugin.get_editor_interface()
|
|
252
|
+
if editor_interface and editor_interface.has_method("get_base_control"):
|
|
253
|
+
base_control = editor_interface.call("get_base_control")
|
|
254
|
+
if is_instance_valid(base_control):
|
|
255
|
+
summary.append("direct_base_control=valid")
|
|
256
|
+
base_control_result = _search_control_tree(base_control, scoring_func)
|
|
257
|
+
if not base_control_result.is_empty():
|
|
258
|
+
summary.append("direct_score=%d" % int(base_control_result.get("score", 0)))
|
|
259
|
+
summary.append("direct_visited=%d" % int(base_control_result.get("visited", 0)))
|
|
260
|
+
var direct_class := String(base_control_result.get("class", ""))
|
|
261
|
+
if not direct_class.is_empty():
|
|
262
|
+
summary.append("direct_class=%s" % direct_class)
|
|
263
|
+
var direct_path := String(base_control_result.get("path", ""))
|
|
264
|
+
if not direct_path.is_empty():
|
|
265
|
+
summary.append("direct_path=%s" % _summarize_summary_value(direct_path))
|
|
266
|
+
if int(base_control_result.get("score", 0)) >= threshold and base_control_result.get("control"):
|
|
267
|
+
return {
|
|
268
|
+
"control": base_control_result.get("control"),
|
|
269
|
+
"summary": "; ".join(summary)
|
|
270
|
+
}
|
|
271
|
+
else:
|
|
272
|
+
summary.append("direct_base_control=invalid")
|
|
273
|
+
else:
|
|
274
|
+
summary.append("direct_base_control=missing")
|
|
275
|
+
else:
|
|
276
|
+
summary.append("direct_plugin=invalid")
|
|
277
|
+
else:
|
|
278
|
+
summary.append("direct_plugin=missing")
|
|
279
|
+
|
|
280
|
+
var has_editor_node := Engine.has_singleton("EditorNode")
|
|
281
|
+
summary.append("editor_node_singleton=%s" % ("true" if has_editor_node else "false"))
|
|
282
|
+
if not has_editor_node:
|
|
283
|
+
return {"control": null, "summary": "; ".join(summary)}
|
|
284
|
+
|
|
285
|
+
var editor_node = Engine.get_singleton("EditorNode")
|
|
286
|
+
if editor_node == null:
|
|
287
|
+
summary.append("editor_node=null")
|
|
288
|
+
return {"control": null, "summary": "; ".join(summary)}
|
|
289
|
+
summary.append("editor_node=valid")
|
|
290
|
+
|
|
291
|
+
var search_roots: Array = []
|
|
292
|
+
var root_labels: Dictionary = {}
|
|
293
|
+
var root_ids: Dictionary = {}
|
|
294
|
+
|
|
295
|
+
if editor_node.has_method("get_log"):
|
|
296
|
+
var editor_log = editor_node.call("get_log")
|
|
297
|
+
var valid_log := is_instance_valid(editor_log)
|
|
298
|
+
summary.append("get_log=%s" % ("valid" if valid_log else "invalid"))
|
|
299
|
+
if valid_log:
|
|
300
|
+
_register_control_root(search_roots, root_labels, root_ids, editor_log, "editor_log")
|
|
301
|
+
else:
|
|
302
|
+
summary.append("get_log=missing")
|
|
303
|
+
|
|
304
|
+
if editor_node.has_method("get_gui_base"):
|
|
305
|
+
var gui_base = editor_node.call("get_gui_base")
|
|
306
|
+
var valid_gui := is_instance_valid(gui_base)
|
|
307
|
+
summary.append("get_gui_base=%s" % ("valid" if valid_gui else "invalid"))
|
|
308
|
+
if valid_gui:
|
|
309
|
+
_register_control_root(search_roots, root_labels, root_ids, gui_base, "gui_base")
|
|
310
|
+
else:
|
|
311
|
+
summary.append("get_gui_base=missing")
|
|
312
|
+
|
|
313
|
+
if is_instance_valid(base_control):
|
|
314
|
+
_register_control_root(search_roots, root_labels, root_ids, base_control, "base_control")
|
|
315
|
+
|
|
316
|
+
if Engine.has_meta("GodotMCPPlugin"):
|
|
317
|
+
var plugin_again = Engine.get_meta("GodotMCPPlugin")
|
|
318
|
+
if plugin_again and plugin_again is EditorPlugin:
|
|
319
|
+
var editor_interface_again = plugin_again.get_editor_interface()
|
|
320
|
+
if editor_interface_again:
|
|
321
|
+
if editor_interface_again.has_method("get_editor_main_screen"):
|
|
322
|
+
var main_screen = editor_interface_again.call("get_editor_main_screen")
|
|
323
|
+
var valid_main := is_instance_valid(main_screen)
|
|
324
|
+
summary.append("plugin_main_screen=%s" % ("valid" if valid_main else "invalid"))
|
|
325
|
+
if valid_main:
|
|
326
|
+
_register_control_root(search_roots, root_labels, root_ids, main_screen, "main_screen")
|
|
327
|
+
else:
|
|
328
|
+
summary.append("plugin_main_screen=missing")
|
|
329
|
+
else:
|
|
330
|
+
summary.append("editor_interface=null")
|
|
331
|
+
else:
|
|
332
|
+
summary.append("mcp_plugin=invalid")
|
|
333
|
+
else:
|
|
334
|
+
summary.append("mcp_plugin=missing")
|
|
335
|
+
|
|
336
|
+
var scene_tree := get_tree()
|
|
337
|
+
if scene_tree:
|
|
338
|
+
var tree_root := scene_tree.get_root()
|
|
339
|
+
var valid_root := is_instance_valid(tree_root)
|
|
340
|
+
summary.append("scene_tree_root=%s" % ("valid" if valid_root else "invalid"))
|
|
341
|
+
if valid_root:
|
|
342
|
+
_register_control_root(search_roots, root_labels, root_ids, tree_root, "scene_tree_root")
|
|
343
|
+
else:
|
|
344
|
+
summary.append("scene_tree=null")
|
|
345
|
+
|
|
346
|
+
var total_visited := 0
|
|
347
|
+
var best_control: Control = null
|
|
348
|
+
var best_score := 0
|
|
349
|
+
var best_label := ""
|
|
350
|
+
var best_class := ""
|
|
351
|
+
var best_path := ""
|
|
352
|
+
|
|
353
|
+
for root in search_roots:
|
|
354
|
+
var result := _search_control_tree(root, scoring_func)
|
|
355
|
+
var id = root.get_instance_id()
|
|
356
|
+
var label := String(root_labels.get(id, "root"))
|
|
357
|
+
total_visited += int(result.get("visited", 0))
|
|
358
|
+
var score := int(result.get("score", 0))
|
|
359
|
+
if score > best_score:
|
|
360
|
+
best_score = score
|
|
361
|
+
best_control = result.get("control")
|
|
362
|
+
best_label = label
|
|
363
|
+
best_class = String(result.get("class", ""))
|
|
364
|
+
best_path = String(result.get("path", ""))
|
|
365
|
+
|
|
366
|
+
summary.append("search_roots=%d" % search_roots.size())
|
|
367
|
+
summary.append("search_total_visited=%d" % total_visited)
|
|
368
|
+
if best_score > 0:
|
|
369
|
+
summary.append("search_best_score=%d" % best_score)
|
|
370
|
+
if not best_label.is_empty():
|
|
371
|
+
summary.append("search_best_label=%s" % best_label)
|
|
372
|
+
if not best_class.is_empty():
|
|
373
|
+
summary.append("search_best_class=%s" % best_class)
|
|
374
|
+
if not best_path.is_empty():
|
|
375
|
+
summary.append("search_best_path=%s" % _summarize_summary_value(best_path))
|
|
376
|
+
else:
|
|
377
|
+
summary.append("search_best_score=0")
|
|
378
|
+
|
|
379
|
+
var summary_text := "; ".join(summary)
|
|
380
|
+
if best_control and best_score >= threshold:
|
|
381
|
+
return {
|
|
382
|
+
"control": best_control,
|
|
383
|
+
"summary": summary_text
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
"control": null,
|
|
388
|
+
"summary": summary_text
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
func _search_control_tree(root: Node, scoring_func: Callable = Callable()) -> Dictionary:
|
|
392
|
+
var visited: Dictionary = {}
|
|
393
|
+
var queue: Array = []
|
|
394
|
+
var visited_count := 0
|
|
395
|
+
var best_control: Control = null
|
|
396
|
+
var best_score := 0
|
|
397
|
+
var best_class := ""
|
|
398
|
+
var best_path := ""
|
|
399
|
+
|
|
400
|
+
if is_instance_valid(root):
|
|
401
|
+
queue.append(root)
|
|
402
|
+
|
|
403
|
+
while queue.size() > 0:
|
|
404
|
+
var candidate = queue.pop_front()
|
|
405
|
+
if not (candidate is Node):
|
|
406
|
+
continue
|
|
407
|
+
|
|
408
|
+
var node: Node = candidate
|
|
409
|
+
var instance_id := node.get_instance_id()
|
|
410
|
+
if visited.has(instance_id):
|
|
411
|
+
continue
|
|
412
|
+
visited[instance_id] = true
|
|
413
|
+
visited_count += 1
|
|
414
|
+
|
|
415
|
+
if node is Control:
|
|
416
|
+
var control_node: Control = node
|
|
417
|
+
var score := 0
|
|
418
|
+
if scoring_func.is_valid():
|
|
419
|
+
score = int(scoring_func.call(control_node))
|
|
420
|
+
else:
|
|
421
|
+
score = _score_output_control(control_node)
|
|
422
|
+
if score > best_score:
|
|
423
|
+
best_score = score
|
|
424
|
+
best_control = control_node
|
|
425
|
+
best_class = control_node.get_class()
|
|
426
|
+
if control_node.is_inside_tree():
|
|
427
|
+
best_path = String(control_node.get_path())
|
|
428
|
+
else:
|
|
429
|
+
best_path = String(control_node.name) if control_node.has_method("get_name") else "<detached>"
|
|
430
|
+
|
|
431
|
+
for child in node.get_children():
|
|
432
|
+
queue.append(child)
|
|
433
|
+
|
|
434
|
+
return {
|
|
435
|
+
"control": best_control,
|
|
436
|
+
"score": best_score,
|
|
437
|
+
"class": best_class,
|
|
438
|
+
"path": best_path,
|
|
439
|
+
"visited": visited_count
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
func _register_control_root(search_roots: Array, root_labels: Dictionary, root_ids: Dictionary, node: Node, label: String) -> void:
|
|
443
|
+
if not is_instance_valid(node):
|
|
444
|
+
return
|
|
445
|
+
var id := node.get_instance_id()
|
|
446
|
+
if root_ids.has(id):
|
|
447
|
+
return
|
|
448
|
+
root_ids[id] = true
|
|
449
|
+
search_roots.append(node)
|
|
450
|
+
root_labels[id] = label
|
|
451
|
+
|
|
452
|
+
func _summarize_summary_value(text: String) -> String:
|
|
453
|
+
if text.is_empty():
|
|
454
|
+
return text
|
|
455
|
+
if text.length() <= SUMMARY_VALUE_MAX_LEN:
|
|
456
|
+
return text
|
|
457
|
+
return text.substr(0, SUMMARY_VALUE_MAX_LEN - 3) + "..."
|
|
458
|
+
|
|
459
|
+
func _score_output_control(node: Control) -> int:
|
|
460
|
+
return _score_control_with_keywords(node, OUTPUT_KEYWORDS)
|
|
461
|
+
|
|
462
|
+
func _score_errors_control(node: Control) -> int:
|
|
463
|
+
return _score_control_with_keywords(node, ERRORS_KEYWORDS)
|
|
464
|
+
|
|
465
|
+
func _score_control_with_keywords(node: Control, keywords: Array) -> int:
|
|
466
|
+
if node == null:
|
|
467
|
+
return 0
|
|
468
|
+
|
|
469
|
+
var score := 0
|
|
470
|
+
|
|
471
|
+
if node.is_class("RichTextLabel"):
|
|
472
|
+
score = max(score, 30)
|
|
473
|
+
elif node.is_class("TextEdit") or node.is_class("CodeEdit"):
|
|
474
|
+
score = max(score, 25)
|
|
475
|
+
else:
|
|
476
|
+
return 0
|
|
477
|
+
|
|
478
|
+
if _has_editor_log_ancestor(node):
|
|
479
|
+
score = max(score, 100)
|
|
480
|
+
|
|
481
|
+
var node_name := String(node.name).to_lower()
|
|
482
|
+
for keyword in keywords:
|
|
483
|
+
if node_name.find(keyword) != -1:
|
|
484
|
+
score = max(score, 80)
|
|
485
|
+
break
|
|
486
|
+
|
|
487
|
+
var theme_type := ""
|
|
488
|
+
if node.has_method("get_theme_type_variation"):
|
|
489
|
+
theme_type = String(node.call("get_theme_type_variation")).to_lower()
|
|
490
|
+
for keyword in keywords:
|
|
491
|
+
if theme_type.find(keyword) != -1:
|
|
492
|
+
score = max(score, 75)
|
|
493
|
+
break
|
|
494
|
+
|
|
495
|
+
var parent := node.get_parent()
|
|
496
|
+
var depth := 0
|
|
497
|
+
while parent and depth < 6:
|
|
498
|
+
var parent_name := String(parent.name).to_lower()
|
|
499
|
+
for keyword in keywords:
|
|
500
|
+
if parent_name.find(keyword) != -1:
|
|
501
|
+
var parent_score := 70 - depth * 5
|
|
502
|
+
if parent_score > score:
|
|
503
|
+
score = parent_score
|
|
504
|
+
break
|
|
505
|
+
if parent.is_class("EditorLog"):
|
|
506
|
+
score = max(score, 95)
|
|
507
|
+
parent = parent.get_parent()
|
|
508
|
+
depth += 1
|
|
509
|
+
|
|
510
|
+
if node.is_inside_tree():
|
|
511
|
+
var path_lower := String(node.get_path()).to_lower()
|
|
512
|
+
for keyword in keywords:
|
|
513
|
+
if path_lower.find(keyword) != -1:
|
|
514
|
+
score = max(score, 70)
|
|
515
|
+
break
|
|
516
|
+
|
|
517
|
+
return score
|
|
518
|
+
|
|
519
|
+
func _has_editor_log_ancestor(node: Node) -> bool:
|
|
520
|
+
var current := node.get_parent()
|
|
521
|
+
while current:
|
|
522
|
+
if current.is_class("EditorLog"):
|
|
523
|
+
return true
|
|
524
|
+
current = current.get_parent()
|
|
525
|
+
return false
|
|
526
|
+
|
|
527
|
+
func _get_output_control() -> Control:
|
|
528
|
+
if is_instance_valid(_cached_output_control):
|
|
529
|
+
return _cached_output_control
|
|
530
|
+
_cached_output_control = _locate_output_control()
|
|
531
|
+
return _cached_output_control
|
|
532
|
+
|
|
533
|
+
func get_full_log_text() -> String:
|
|
534
|
+
return _fetch_log_text()
|
|
535
|
+
|
|
536
|
+
func clear_log_output() -> Dictionary:
|
|
537
|
+
var diagnostics := {
|
|
538
|
+
"timestamp": Time.get_ticks_msec(),
|
|
539
|
+
"attempts": []
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if not Engine.is_editor_hint():
|
|
543
|
+
diagnostics["error"] = "not_in_editor"
|
|
544
|
+
return {
|
|
545
|
+
"cleared": false,
|
|
546
|
+
"method": "editor_only",
|
|
547
|
+
"diagnostics": diagnostics
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
var cleared := false
|
|
551
|
+
var method_used := ""
|
|
552
|
+
|
|
553
|
+
var editor_node = Engine.get_singleton("EditorNode") if Engine.has_singleton("EditorNode") else null
|
|
554
|
+
if editor_node and editor_node.has_method("get_log"):
|
|
555
|
+
var editor_log = editor_node.call("get_log")
|
|
556
|
+
if is_instance_valid(editor_log):
|
|
557
|
+
diagnostics["attempts"].append("editor_log")
|
|
558
|
+
if editor_log.has_method("clear"):
|
|
559
|
+
editor_log.call("clear")
|
|
560
|
+
cleared = true
|
|
561
|
+
method_used = "editor_log_clear"
|
|
562
|
+
elif editor_log.has_method("set_text"):
|
|
563
|
+
editor_log.call("set_text", "")
|
|
564
|
+
cleared = true
|
|
565
|
+
method_used = "editor_log_set_text"
|
|
566
|
+
|
|
567
|
+
if not cleared and Engine.has_singleton("EditorDebuggerNode"):
|
|
568
|
+
var debugger_node = Engine.get_singleton("EditorDebuggerNode")
|
|
569
|
+
if debugger_node and debugger_node.has_method("clear_log"):
|
|
570
|
+
diagnostics["attempts"].append("editor_debugger_node")
|
|
571
|
+
debugger_node.call("clear_log")
|
|
572
|
+
cleared = true
|
|
573
|
+
method_used = "editor_debugger_clear_log"
|
|
574
|
+
|
|
575
|
+
if not cleared:
|
|
576
|
+
var control := _get_output_control()
|
|
577
|
+
if is_instance_valid(control):
|
|
578
|
+
diagnostics["attempts"].append("output_control_%s" % control.get_class())
|
|
579
|
+
if control.has_method("clear"):
|
|
580
|
+
control.call("clear")
|
|
581
|
+
cleared = true
|
|
582
|
+
method_used = "control_clear"
|
|
583
|
+
elif control.has_method("set_text"):
|
|
584
|
+
control.call("set_text", "")
|
|
585
|
+
cleared = true
|
|
586
|
+
method_used = "control_set_text"
|
|
587
|
+
|
|
588
|
+
if cleared:
|
|
589
|
+
_last_length = 0
|
|
590
|
+
_broadcast_log_reset()
|
|
591
|
+
|
|
592
|
+
diagnostics["cleared"] = cleared
|
|
593
|
+
diagnostics["method"] = method_used
|
|
594
|
+
|
|
595
|
+
return {
|
|
596
|
+
"cleared": cleared,
|
|
597
|
+
"method": method_used,
|
|
598
|
+
"diagnostics": diagnostics
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
func _broadcast_log_reset() -> void:
|
|
602
|
+
if _subscribers.is_empty() or websocket_server == null:
|
|
603
|
+
return
|
|
604
|
+
|
|
605
|
+
var payload := {
|
|
606
|
+
"event": "debug_output_frame",
|
|
607
|
+
"data": {
|
|
608
|
+
"timestamp": Time.get_ticks_msec(),
|
|
609
|
+
"chunk": "",
|
|
610
|
+
"lines": [],
|
|
611
|
+
"reset": true
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
for client_id in _subscribers.keys():
|
|
616
|
+
_send_event_to_client(int(client_id), payload)
|
|
617
|
+
|
|
618
|
+
func get_errors_panel_snapshot() -> Dictionary:
|
|
619
|
+
return _capture_errors_tab_text()
|
|
620
|
+
|
|
621
|
+
func clear_errors_panel() -> Dictionary:
|
|
622
|
+
var diagnostics := {
|
|
623
|
+
"timestamp": Time.get_ticks_msec(),
|
|
624
|
+
"attempts": []
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
if not Engine.is_editor_hint():
|
|
628
|
+
diagnostics["error"] = "not_in_editor"
|
|
629
|
+
return {
|
|
630
|
+
"cleared": false,
|
|
631
|
+
"method": "editor_only",
|
|
632
|
+
"diagnostics": diagnostics
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
var cleared := false
|
|
636
|
+
var method_used := ""
|
|
637
|
+
|
|
638
|
+
var search_roots := _gather_editor_search_roots()
|
|
639
|
+
var tab_info := {}
|
|
640
|
+
for root in search_roots:
|
|
641
|
+
tab_info = _find_errors_tab_control(root)
|
|
642
|
+
if tab_info.has("control"):
|
|
643
|
+
break
|
|
644
|
+
|
|
645
|
+
if not tab_info.has("control"):
|
|
646
|
+
diagnostics["error"] = "errors_tab_not_found"
|
|
647
|
+
return {
|
|
648
|
+
"cleared": false,
|
|
649
|
+
"method": "not_found",
|
|
650
|
+
"diagnostics": diagnostics
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
var tab_control: Control = tab_info.get("control")
|
|
654
|
+
if not is_instance_valid(tab_control):
|
|
655
|
+
diagnostics["error"] = "tab_control_invalid"
|
|
656
|
+
return {
|
|
657
|
+
"cleared": false,
|
|
658
|
+
"method": "invalid_control",
|
|
659
|
+
"diagnostics": diagnostics
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
diagnostics["tab_title"] = tab_info.get("tab_title", "")
|
|
663
|
+
diagnostics["attempts"].append("found_tab=%s" % tab_info.get("tab_title", ""))
|
|
664
|
+
|
|
665
|
+
var tree := _find_descendant_tree(tab_control)
|
|
666
|
+
if is_instance_valid(tree):
|
|
667
|
+
diagnostics["attempts"].append("tree_control")
|
|
668
|
+
if tree.has_method("clear"):
|
|
669
|
+
tree.call("clear")
|
|
670
|
+
cleared = true
|
|
671
|
+
method_used = "tree_clear"
|
|
672
|
+
diagnostics["tree_path"] = String(tree.get_path()) if tree.is_inside_tree() else ""
|
|
673
|
+
|
|
674
|
+
if not cleared and tab_control.has_method("clear"):
|
|
675
|
+
diagnostics["attempts"].append("tab_control_clear")
|
|
676
|
+
tab_control.call("clear")
|
|
677
|
+
cleared = true
|
|
678
|
+
method_used = "tab_control_clear"
|
|
679
|
+
|
|
680
|
+
diagnostics["cleared"] = cleared
|
|
681
|
+
diagnostics["method"] = method_used
|
|
682
|
+
|
|
683
|
+
return {
|
|
684
|
+
"cleared": cleared,
|
|
685
|
+
"method": method_used,
|
|
686
|
+
"diagnostics": diagnostics
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
func get_stack_trace_snapshot(session_id: int = -1) -> Dictionary:
|
|
690
|
+
return _capture_stack_trace_panel(session_id)
|
|
691
|
+
|
|
692
|
+
func get_stack_frames_snapshot(session_id: int = -1) -> Dictionary:
|
|
693
|
+
return _capture_stack_frames_panel(session_id)
|
|
694
|
+
|
|
695
|
+
func _gather_editor_search_roots() -> Array:
|
|
696
|
+
var search_roots: Array = []
|
|
697
|
+
|
|
698
|
+
var editor_node = Engine.get_singleton("EditorNode") if Engine.has_singleton("EditorNode") else null
|
|
699
|
+
if editor_node:
|
|
700
|
+
if editor_node.has_method("get_log"):
|
|
701
|
+
var editor_log = editor_node.call("get_log")
|
|
702
|
+
if is_instance_valid(editor_log):
|
|
703
|
+
search_roots.append(editor_log)
|
|
704
|
+
search_roots.append(editor_node)
|
|
705
|
+
|
|
706
|
+
var plugin = Engine.get_meta("GodotMCPPlugin") if Engine.has_meta("GodotMCPPlugin") else null
|
|
707
|
+
if plugin and plugin is EditorPlugin:
|
|
708
|
+
var editor_interface = plugin.get_editor_interface()
|
|
709
|
+
if editor_interface and editor_interface.has_method("get_base_control"):
|
|
710
|
+
var base_control = editor_interface.call("get_base_control")
|
|
711
|
+
if is_instance_valid(base_control):
|
|
712
|
+
search_roots.append(base_control)
|
|
713
|
+
|
|
714
|
+
var scene_tree := get_tree()
|
|
715
|
+
if scene_tree:
|
|
716
|
+
var tree_root := scene_tree.get_root()
|
|
717
|
+
if is_instance_valid(tree_root):
|
|
718
|
+
search_roots.append(tree_root)
|
|
719
|
+
|
|
720
|
+
if Engine.has_singleton("EditorDebuggerNode"):
|
|
721
|
+
var debugger_node = Engine.get_singleton("EditorDebuggerNode")
|
|
722
|
+
if debugger_node and debugger_node is Node and is_instance_valid(debugger_node):
|
|
723
|
+
search_roots.append(debugger_node)
|
|
724
|
+
if debugger_node.has_method("get_gui_base"):
|
|
725
|
+
var debugger_gui = debugger_node.call("get_gui_base")
|
|
726
|
+
if is_instance_valid(debugger_gui):
|
|
727
|
+
search_roots.append(debugger_gui)
|
|
728
|
+
|
|
729
|
+
var debugger_panel := get_node_or_null("/root/EditorNode/DebuggerPanel")
|
|
730
|
+
if debugger_panel and is_instance_valid(debugger_panel):
|
|
731
|
+
search_roots.append(debugger_panel)
|
|
732
|
+
|
|
733
|
+
return search_roots
|
|
734
|
+
|
|
735
|
+
func _capture_errors_tab_text() -> Dictionary:
|
|
736
|
+
var diagnostics := {
|
|
737
|
+
"source": "errors_tab_lookup",
|
|
738
|
+
"timestamp": Time.get_ticks_msec(),
|
|
739
|
+
"control_found": false,
|
|
740
|
+
"search_summary": ""
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
if not Engine.is_editor_hint():
|
|
744
|
+
diagnostics["error"] = "not_in_editor"
|
|
745
|
+
return {
|
|
746
|
+
"text": "",
|
|
747
|
+
"lines": [],
|
|
748
|
+
"line_count": 0,
|
|
749
|
+
"diagnostics": diagnostics
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
var search_roots := _gather_editor_search_roots()
|
|
753
|
+
|
|
754
|
+
var aggregated_summary: Array = []
|
|
755
|
+
var tab_info := {}
|
|
756
|
+
for root in search_roots:
|
|
757
|
+
tab_info = _find_errors_tab_control(root)
|
|
758
|
+
var summary := String(tab_info.get("summary", ""))
|
|
759
|
+
if not summary.is_empty():
|
|
760
|
+
aggregated_summary.append(summary)
|
|
761
|
+
if tab_info.has("control"):
|
|
762
|
+
break
|
|
763
|
+
|
|
764
|
+
diagnostics["search_summary"] = " | ".join(aggregated_summary) if aggregated_summary.size() > 0 else ""
|
|
765
|
+
|
|
766
|
+
if tab_info.is_empty() or not tab_info.has("control"):
|
|
767
|
+
diagnostics["error"] = "errors_tab_not_found"
|
|
768
|
+
return {
|
|
769
|
+
"text": "",
|
|
770
|
+
"lines": [],
|
|
771
|
+
"line_count": 0,
|
|
772
|
+
"diagnostics": diagnostics
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
var tab_control: Control = tab_info.get("control")
|
|
776
|
+
if not is_instance_valid(tab_control):
|
|
777
|
+
diagnostics["error"] = "tab_control_invalid"
|
|
778
|
+
return {
|
|
779
|
+
"text": "",
|
|
780
|
+
"lines": [],
|
|
781
|
+
"line_count": 0,
|
|
782
|
+
"diagnostics": diagnostics
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
diagnostics["control_found"] = true
|
|
786
|
+
diagnostics["tab_title"] = tab_info.get("tab_title", "")
|
|
787
|
+
if tab_control.is_inside_tree():
|
|
788
|
+
diagnostics["control_path"] = String(tab_control.get_path())
|
|
789
|
+
else:
|
|
790
|
+
diagnostics["control_path"] = ""
|
|
791
|
+
|
|
792
|
+
var tree := _find_descendant_tree(tab_control)
|
|
793
|
+
var lines: Array = []
|
|
794
|
+
var text := ""
|
|
795
|
+
|
|
796
|
+
if tree:
|
|
797
|
+
diagnostics["tree_path"] = String(tree.get_path()) if tree.is_inside_tree() else ""
|
|
798
|
+
lines = _collect_tree_lines(tree)
|
|
799
|
+
text = "\n".join(lines)
|
|
800
|
+
else:
|
|
801
|
+
text = _extract_text_from_control(tab_control)
|
|
802
|
+
if not text.is_empty():
|
|
803
|
+
lines = text.split("\n", false)
|
|
804
|
+
|
|
805
|
+
return {
|
|
806
|
+
"text": text,
|
|
807
|
+
"lines": lines,
|
|
808
|
+
"line_count": lines.size(),
|
|
809
|
+
"diagnostics": diagnostics
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
func _capture_stack_trace_panel(session_id: int) -> Dictionary:
|
|
813
|
+
var diagnostics := {
|
|
814
|
+
"source": "stack_trace_lookup",
|
|
815
|
+
"timestamp": Time.get_ticks_msec(),
|
|
816
|
+
"control_found": false,
|
|
817
|
+
"search_summary": ""
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
if not Engine.is_editor_hint():
|
|
821
|
+
diagnostics["error"] = "not_in_editor"
|
|
822
|
+
return {
|
|
823
|
+
"text": "",
|
|
824
|
+
"lines": [],
|
|
825
|
+
"line_count": 0,
|
|
826
|
+
"frames": [],
|
|
827
|
+
"diagnostics": diagnostics
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
var search_roots := _gather_editor_search_roots()
|
|
831
|
+
if search_roots.is_empty():
|
|
832
|
+
diagnostics["error"] = "search_roots_empty"
|
|
833
|
+
return {
|
|
834
|
+
"text": "",
|
|
835
|
+
"lines": [],
|
|
836
|
+
"line_count": 0,
|
|
837
|
+
"frames": [],
|
|
838
|
+
"diagnostics": diagnostics
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
var aggregated_summary: Array = []
|
|
842
|
+
var panel_info := {}
|
|
843
|
+
for root in search_roots:
|
|
844
|
+
panel_info = _find_stack_trace_control(root)
|
|
845
|
+
var summary := String(panel_info.get("summary", ""))
|
|
846
|
+
if not summary.is_empty():
|
|
847
|
+
aggregated_summary.append(summary)
|
|
848
|
+
if panel_info.has("control"):
|
|
849
|
+
break
|
|
850
|
+
|
|
851
|
+
if aggregated_summary.size() > 0:
|
|
852
|
+
diagnostics["search_summary"] = " | ".join(aggregated_summary)
|
|
853
|
+
|
|
854
|
+
if panel_info.is_empty() or not panel_info.has("control"):
|
|
855
|
+
diagnostics["error"] = "stack_trace_panel_not_found"
|
|
856
|
+
return {
|
|
857
|
+
"text": "",
|
|
858
|
+
"lines": [],
|
|
859
|
+
"line_count": 0,
|
|
860
|
+
"frames": [],
|
|
861
|
+
"diagnostics": diagnostics
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
var panel_control: Control = panel_info.get("control")
|
|
865
|
+
if not is_instance_valid(panel_control):
|
|
866
|
+
diagnostics["error"] = "panel_control_invalid"
|
|
867
|
+
return {
|
|
868
|
+
"text": "",
|
|
869
|
+
"lines": [],
|
|
870
|
+
"line_count": 0,
|
|
871
|
+
"frames": [],
|
|
872
|
+
"diagnostics": diagnostics
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
diagnostics["control_found"] = true
|
|
876
|
+
diagnostics["control_class"] = panel_control.get_class()
|
|
877
|
+
diagnostics["control_path"] = String(panel_control.get_path()) if panel_control.is_inside_tree() else ""
|
|
878
|
+
if panel_info.has("tab_title"):
|
|
879
|
+
diagnostics["tab_title"] = panel_info.get("tab_title")
|
|
880
|
+
if panel_info.has("score"):
|
|
881
|
+
diagnostics["match_score"] = panel_info.get("score")
|
|
882
|
+
if panel_info.has("fallback_source"):
|
|
883
|
+
diagnostics["fallback_source"] = panel_info.get("fallback_source")
|
|
884
|
+
diagnostics["panel_children"] = _summarize_control_structure(panel_control, 2, 64)
|
|
885
|
+
|
|
886
|
+
var lines: Array = []
|
|
887
|
+
var frames: Array = []
|
|
888
|
+
var text := ""
|
|
889
|
+
|
|
890
|
+
var tree: Tree = null
|
|
891
|
+
if panel_info.has("tree"):
|
|
892
|
+
tree = panel_info.get("tree")
|
|
893
|
+
if not is_instance_valid(tree) and panel_control is Tree:
|
|
894
|
+
tree = panel_control
|
|
895
|
+
|
|
896
|
+
if is_instance_valid(tree):
|
|
897
|
+
diagnostics["tree_path"] = String(tree.get_path()) if tree.is_inside_tree() else ""
|
|
898
|
+
diagnostics["tree_item_count"] = _count_tree_items(tree)
|
|
899
|
+
lines = _collect_tree_lines(tree)
|
|
900
|
+
text = "\n".join(lines)
|
|
901
|
+
frames = _collect_stack_tree_frames(tree)
|
|
902
|
+
if lines.is_empty() and text.is_empty():
|
|
903
|
+
var fallback_text_control := _find_descendant_text_control(panel_control)
|
|
904
|
+
if is_instance_valid(fallback_text_control):
|
|
905
|
+
diagnostics["text_control_path"] = String(fallback_text_control.get_path()) if fallback_text_control.is_inside_tree() else ""
|
|
906
|
+
diagnostics["text_control_class"] = fallback_text_control.get_class()
|
|
907
|
+
text = _extract_text_from_control(fallback_text_control)
|
|
908
|
+
if not text.is_empty():
|
|
909
|
+
lines = text.split("\n", false)
|
|
910
|
+
frames = _derive_frames_from_lines(lines)
|
|
911
|
+
if frames.is_empty():
|
|
912
|
+
diagnostics["fallback_text_source"] = "stack_panel_text_control"
|
|
913
|
+
else:
|
|
914
|
+
var text_control := _find_descendant_text_control(panel_control)
|
|
915
|
+
var capture_control: Control = panel_control
|
|
916
|
+
if is_instance_valid(text_control):
|
|
917
|
+
capture_control = text_control
|
|
918
|
+
diagnostics["text_control_path"] = String(text_control.get_path()) if text_control.is_inside_tree() else ""
|
|
919
|
+
diagnostics["text_control_class"] = text_control.get_class()
|
|
920
|
+
|
|
921
|
+
text = _extract_text_from_control(capture_control)
|
|
922
|
+
if not text.is_empty():
|
|
923
|
+
lines = text.split("\n", false)
|
|
924
|
+
frames = _derive_frames_from_lines(lines)
|
|
925
|
+
|
|
926
|
+
return {
|
|
927
|
+
"text": text,
|
|
928
|
+
"lines": lines,
|
|
929
|
+
"line_count": lines.size(),
|
|
930
|
+
"frames": frames,
|
|
931
|
+
"diagnostics": diagnostics
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
func _capture_stack_frames_panel(session_id: int) -> Dictionary:
|
|
935
|
+
var diagnostics := {
|
|
936
|
+
"source": "stack_frames_lookup",
|
|
937
|
+
"timestamp": Time.get_ticks_msec(),
|
|
938
|
+
"control_found": false,
|
|
939
|
+
"search_summary": ""
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
if not Engine.is_editor_hint():
|
|
943
|
+
diagnostics["error"] = "not_in_editor"
|
|
944
|
+
return {
|
|
945
|
+
"text": "",
|
|
946
|
+
"lines": [],
|
|
947
|
+
"line_count": 0,
|
|
948
|
+
"frames": [],
|
|
949
|
+
"diagnostics": diagnostics
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
var search_roots := _gather_editor_search_roots()
|
|
953
|
+
if search_roots.is_empty():
|
|
954
|
+
diagnostics["error"] = "search_roots_empty"
|
|
955
|
+
return {
|
|
956
|
+
"text": "",
|
|
957
|
+
"lines": [],
|
|
958
|
+
"line_count": 0,
|
|
959
|
+
"frames": [],
|
|
960
|
+
"diagnostics": diagnostics
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
var aggregated_summary: Array = []
|
|
964
|
+
var panel_info := {}
|
|
965
|
+
for root in search_roots:
|
|
966
|
+
panel_info = _find_stack_frames_control(root)
|
|
967
|
+
var summary := String(panel_info.get("summary", ""))
|
|
968
|
+
if not summary.is_empty():
|
|
969
|
+
aggregated_summary.append(summary)
|
|
970
|
+
if panel_info.has("control"):
|
|
971
|
+
break
|
|
972
|
+
|
|
973
|
+
if aggregated_summary.size() > 0:
|
|
974
|
+
diagnostics["search_summary"] = " | ".join(aggregated_summary)
|
|
975
|
+
|
|
976
|
+
if panel_info.is_empty() or not panel_info.has("control"):
|
|
977
|
+
diagnostics["error"] = "stack_frames_panel_not_found"
|
|
978
|
+
return {
|
|
979
|
+
"text": "",
|
|
980
|
+
"lines": [],
|
|
981
|
+
"line_count": 0,
|
|
982
|
+
"frames": [],
|
|
983
|
+
"diagnostics": diagnostics
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
var panel_control: Control = panel_info.get("control")
|
|
987
|
+
if not is_instance_valid(panel_control):
|
|
988
|
+
diagnostics["error"] = "panel_control_invalid"
|
|
989
|
+
return {
|
|
990
|
+
"text": "",
|
|
991
|
+
"lines": [],
|
|
992
|
+
"line_count": 0,
|
|
993
|
+
"frames": [],
|
|
994
|
+
"diagnostics": diagnostics
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
diagnostics["control_found"] = true
|
|
998
|
+
diagnostics["control_class"] = panel_control.get_class()
|
|
999
|
+
diagnostics["control_path"] = String(panel_control.get_path()) if panel_control.is_inside_tree() else ""
|
|
1000
|
+
if panel_info.has("tab_title"):
|
|
1001
|
+
diagnostics["tab_title"] = panel_info.get("tab_title")
|
|
1002
|
+
if panel_info.has("score"):
|
|
1003
|
+
diagnostics["match_score"] = panel_info.get("score")
|
|
1004
|
+
diagnostics["panel_children"] = _summarize_control_structure(panel_control, 2, 64)
|
|
1005
|
+
|
|
1006
|
+
var lines: Array = []
|
|
1007
|
+
var text := ""
|
|
1008
|
+
var frames: Array = []
|
|
1009
|
+
|
|
1010
|
+
var tree: Tree = null
|
|
1011
|
+
if panel_info.has("tree"):
|
|
1012
|
+
tree = panel_info.get("tree")
|
|
1013
|
+
if not is_instance_valid(tree) and panel_control is Tree:
|
|
1014
|
+
tree = panel_control
|
|
1015
|
+
|
|
1016
|
+
if is_instance_valid(tree):
|
|
1017
|
+
diagnostics["tree_path"] = String(tree.get_path()) if tree.is_inside_tree() else ""
|
|
1018
|
+
diagnostics["tree_item_count"] = _count_tree_items(tree)
|
|
1019
|
+
lines = _collect_tree_lines(tree)
|
|
1020
|
+
text = "\n".join(lines)
|
|
1021
|
+
frames = _collect_stack_tree_frames(tree)
|
|
1022
|
+
if lines.is_empty() and frames.is_empty():
|
|
1023
|
+
var text_control := _find_descendant_text_control(panel_control)
|
|
1024
|
+
if is_instance_valid(text_control):
|
|
1025
|
+
diagnostics["text_control_path"] = String(text_control.get_path()) if text_control.is_inside_tree() else ""
|
|
1026
|
+
diagnostics["text_control_class"] = text_control.get_class()
|
|
1027
|
+
text = _extract_text_from_control(text_control)
|
|
1028
|
+
if not text.is_empty():
|
|
1029
|
+
lines = text.split("\n", false)
|
|
1030
|
+
frames = _derive_frames_from_lines(lines)
|
|
1031
|
+
if frames.is_empty():
|
|
1032
|
+
diagnostics["fallback_text_source"] = "stack_frames_text_control"
|
|
1033
|
+
else:
|
|
1034
|
+
var text_control := _find_descendant_text_control(panel_control)
|
|
1035
|
+
var capture_control: Control = panel_control
|
|
1036
|
+
if is_instance_valid(text_control):
|
|
1037
|
+
capture_control = text_control
|
|
1038
|
+
diagnostics["text_control_path"] = String(text_control.get_path()) if text_control.is_inside_tree() else ""
|
|
1039
|
+
diagnostics["text_control_class"] = text_control.get_class()
|
|
1040
|
+
|
|
1041
|
+
text = _extract_text_from_control(capture_control)
|
|
1042
|
+
if not text.is_empty():
|
|
1043
|
+
lines = text.split("\n", false)
|
|
1044
|
+
frames = _derive_frames_from_lines(lines)
|
|
1045
|
+
|
|
1046
|
+
return {
|
|
1047
|
+
"text": text,
|
|
1048
|
+
"lines": lines,
|
|
1049
|
+
"line_count": lines.size(),
|
|
1050
|
+
"frames": frames,
|
|
1051
|
+
"diagnostics": diagnostics
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
func _find_errors_tab_control(root: Node) -> Dictionary:
|
|
1055
|
+
var queue: Array = []
|
|
1056
|
+
var summary := []
|
|
1057
|
+
if is_instance_valid(root):
|
|
1058
|
+
queue.append(root)
|
|
1059
|
+
else:
|
|
1060
|
+
return {}
|
|
1061
|
+
|
|
1062
|
+
var visited := 0
|
|
1063
|
+
|
|
1064
|
+
while queue.size() > 0:
|
|
1065
|
+
var candidate = queue.pop_front()
|
|
1066
|
+
if not is_instance_valid(candidate):
|
|
1067
|
+
continue
|
|
1068
|
+
visited += 1
|
|
1069
|
+
|
|
1070
|
+
if candidate is TabContainer:
|
|
1071
|
+
var tab_container: TabContainer = candidate
|
|
1072
|
+
var tab_count := 0
|
|
1073
|
+
if tab_container.has_method("get_tab_count"):
|
|
1074
|
+
tab_count = tab_container.get_tab_count()
|
|
1075
|
+
else:
|
|
1076
|
+
tab_count = tab_container.get_child_count()
|
|
1077
|
+
|
|
1078
|
+
for i in range(tab_count):
|
|
1079
|
+
var title := ""
|
|
1080
|
+
if tab_container.has_method("get_tab_title"):
|
|
1081
|
+
title = String(tab_container.get_tab_title(i))
|
|
1082
|
+
var title_lower := title.to_lower()
|
|
1083
|
+
if title_lower.find("error") != -1:
|
|
1084
|
+
var tab_control: Control = null
|
|
1085
|
+
if tab_container.has_method("get_tab_control"):
|
|
1086
|
+
tab_control = tab_container.get_tab_control(i)
|
|
1087
|
+
if not is_instance_valid(tab_control):
|
|
1088
|
+
# Fallback: try to get nth child
|
|
1089
|
+
if i < tab_container.get_child_count():
|
|
1090
|
+
var child = tab_container.get_child(i)
|
|
1091
|
+
if child is Control:
|
|
1092
|
+
tab_control = child
|
|
1093
|
+
|
|
1094
|
+
if is_instance_valid(tab_control):
|
|
1095
|
+
tab_control = _unwrap_tab_content(tab_control)
|
|
1096
|
+
summary.append("tab_found=%s" % title)
|
|
1097
|
+
summary.append("visited=%d" % visited)
|
|
1098
|
+
return {
|
|
1099
|
+
"control": tab_control,
|
|
1100
|
+
"tab_title": title,
|
|
1101
|
+
"summary": "; ".join(summary)
|
|
1102
|
+
}
|
|
1103
|
+
for child in candidate.get_children():
|
|
1104
|
+
if child is Node:
|
|
1105
|
+
queue.append(child)
|
|
1106
|
+
|
|
1107
|
+
return {"summary": "; ".join(summary)}
|
|
1108
|
+
|
|
1109
|
+
func _find_stack_trace_control(root: Node) -> Dictionary:
|
|
1110
|
+
var queue: Array = []
|
|
1111
|
+
var summary := []
|
|
1112
|
+
if is_instance_valid(root):
|
|
1113
|
+
queue.append(root)
|
|
1114
|
+
else:
|
|
1115
|
+
return {}
|
|
1116
|
+
|
|
1117
|
+
var visited := 0
|
|
1118
|
+
var max_nodes := 8192
|
|
1119
|
+
|
|
1120
|
+
while queue.size() > 0 and visited < max_nodes:
|
|
1121
|
+
var candidate = queue.pop_front()
|
|
1122
|
+
if not is_instance_valid(candidate):
|
|
1123
|
+
continue
|
|
1124
|
+
visited += 1
|
|
1125
|
+
|
|
1126
|
+
if candidate is TabContainer:
|
|
1127
|
+
var tab_container: TabContainer = candidate
|
|
1128
|
+
var tab_count := 0
|
|
1129
|
+
if tab_container.has_method("get_tab_count"):
|
|
1130
|
+
tab_count = tab_container.get_tab_count()
|
|
1131
|
+
else:
|
|
1132
|
+
tab_count = tab_container.get_child_count()
|
|
1133
|
+
for i in range(tab_count):
|
|
1134
|
+
var title := ""
|
|
1135
|
+
if tab_container.has_method("get_tab_title"):
|
|
1136
|
+
title = String(tab_container.get_tab_title(i))
|
|
1137
|
+
var title_lower := title.to_lower()
|
|
1138
|
+
for keyword in STACK_TRACE_KEYWORDS:
|
|
1139
|
+
if title_lower.find(keyword) != -1:
|
|
1140
|
+
var tab_control: Control = null
|
|
1141
|
+
if tab_container.has_method("get_tab_control"):
|
|
1142
|
+
tab_control = tab_container.get_tab_control(i)
|
|
1143
|
+
if not is_instance_valid(tab_control) and i < tab_container.get_child_count():
|
|
1144
|
+
var child = tab_container.get_child(i)
|
|
1145
|
+
if child is Control:
|
|
1146
|
+
tab_control = child
|
|
1147
|
+
|
|
1148
|
+
if is_instance_valid(tab_control):
|
|
1149
|
+
tab_control = _unwrap_tab_content(tab_control)
|
|
1150
|
+
var tree := _find_descendant_tree(tab_control)
|
|
1151
|
+
summary.append("tab_found=%s" % title)
|
|
1152
|
+
summary.append("visited=%d" % visited)
|
|
1153
|
+
return {
|
|
1154
|
+
"control": tab_control,
|
|
1155
|
+
"tree": tree,
|
|
1156
|
+
"tab_title": title,
|
|
1157
|
+
"summary": "; ".join(summary)
|
|
1158
|
+
}
|
|
1159
|
+
break
|
|
1160
|
+
|
|
1161
|
+
if candidate is Control:
|
|
1162
|
+
var control_score := _score_stack_trace_candidate(candidate)
|
|
1163
|
+
if control_score >= STACK_TRACE_SCORE_THRESHOLD:
|
|
1164
|
+
var matched_control: Control = candidate
|
|
1165
|
+
var tree_control: Tree = candidate if candidate is Tree else _find_descendant_tree(candidate)
|
|
1166
|
+
var info := {
|
|
1167
|
+
"control": matched_control,
|
|
1168
|
+
"score": control_score,
|
|
1169
|
+
"summary": "score=%d name=%s class=%s visited=%d" % [control_score, candidate.name, candidate.get_class(), visited]
|
|
1170
|
+
}
|
|
1171
|
+
if tree_control:
|
|
1172
|
+
info["tree"] = tree_control
|
|
1173
|
+
return info
|
|
1174
|
+
|
|
1175
|
+
for child in candidate.get_children():
|
|
1176
|
+
if child is Node:
|
|
1177
|
+
queue.append(child)
|
|
1178
|
+
|
|
1179
|
+
summary.append("visited=%d" % visited)
|
|
1180
|
+
return {"summary": "; ".join(summary)}
|
|
1181
|
+
|
|
1182
|
+
func _find_stack_frames_control(root: Node) -> Dictionary:
|
|
1183
|
+
var queue: Array = []
|
|
1184
|
+
var summary := []
|
|
1185
|
+
if is_instance_valid(root):
|
|
1186
|
+
queue.append(root)
|
|
1187
|
+
else:
|
|
1188
|
+
return {}
|
|
1189
|
+
|
|
1190
|
+
var visited := 0
|
|
1191
|
+
var max_nodes := 8192
|
|
1192
|
+
|
|
1193
|
+
while queue.size() > 0 and visited < max_nodes:
|
|
1194
|
+
var candidate = queue.pop_front()
|
|
1195
|
+
if not is_instance_valid(candidate):
|
|
1196
|
+
continue
|
|
1197
|
+
visited += 1
|
|
1198
|
+
|
|
1199
|
+
if candidate is TabContainer:
|
|
1200
|
+
var tab_container: TabContainer = candidate
|
|
1201
|
+
var tab_count := 0
|
|
1202
|
+
if tab_container.has_method("get_tab_count"):
|
|
1203
|
+
tab_count = tab_container.get_tab_count()
|
|
1204
|
+
else:
|
|
1205
|
+
tab_count = tab_container.get_child_count()
|
|
1206
|
+
for i in range(tab_count):
|
|
1207
|
+
var title := ""
|
|
1208
|
+
if tab_container.has_method("get_tab_title"):
|
|
1209
|
+
title = String(tab_container.get_tab_title(i))
|
|
1210
|
+
var title_lower := title.to_lower()
|
|
1211
|
+
if _title_matches_stack_frames(title_lower):
|
|
1212
|
+
var tab_control: Control = null
|
|
1213
|
+
if tab_container.has_method("get_tab_control"):
|
|
1214
|
+
tab_control = tab_container.get_tab_control(i)
|
|
1215
|
+
if not is_instance_valid(tab_control) and i < tab_container.get_child_count():
|
|
1216
|
+
var child = tab_container.get_child(i)
|
|
1217
|
+
if child is Control:
|
|
1218
|
+
tab_control = child
|
|
1219
|
+
|
|
1220
|
+
if is_instance_valid(tab_control):
|
|
1221
|
+
tab_control = _unwrap_tab_content(tab_control)
|
|
1222
|
+
var tree := _find_descendant_tree(tab_control)
|
|
1223
|
+
summary.append("tab_found=%s" % title)
|
|
1224
|
+
summary.append("visited=%d" % visited)
|
|
1225
|
+
return {
|
|
1226
|
+
"control": tab_control,
|
|
1227
|
+
"tree": tree,
|
|
1228
|
+
"tab_title": title,
|
|
1229
|
+
"summary": "; ".join(summary)
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
if candidate is Control:
|
|
1233
|
+
var control_score := _score_stack_frames_candidate(candidate)
|
|
1234
|
+
if control_score >= STACK_FRAMES_SCORE_THRESHOLD:
|
|
1235
|
+
var matched_control: Control = candidate
|
|
1236
|
+
var tree_control: Tree = candidate if candidate is Tree else _find_descendant_tree(candidate)
|
|
1237
|
+
var info := {
|
|
1238
|
+
"control": matched_control,
|
|
1239
|
+
"score": control_score,
|
|
1240
|
+
"summary": "score=%d name=%s class=%s visited=%d" % [control_score, candidate.name, candidate.get_class(), visited]
|
|
1241
|
+
}
|
|
1242
|
+
if tree_control:
|
|
1243
|
+
info["tree"] = tree_control
|
|
1244
|
+
return info
|
|
1245
|
+
|
|
1246
|
+
for child in candidate.get_children():
|
|
1247
|
+
if child is Node:
|
|
1248
|
+
queue.append(child)
|
|
1249
|
+
|
|
1250
|
+
summary.append("visited=%d" % visited)
|
|
1251
|
+
var fallback_info := _find_stack_trace_control(root)
|
|
1252
|
+
if fallback_info.has("control"):
|
|
1253
|
+
fallback_info["fallback_source"] = "stack_trace_lookup"
|
|
1254
|
+
var combined_summary := "; ".join(summary)
|
|
1255
|
+
if fallback_info.has("summary") and not String(fallback_info["summary"]).is_empty():
|
|
1256
|
+
combined_summary = "%s | %s" % [combined_summary, fallback_info["summary"]]
|
|
1257
|
+
fallback_info["summary"] = combined_summary
|
|
1258
|
+
return fallback_info
|
|
1259
|
+
return {"summary": "; ".join(summary)}
|
|
1260
|
+
|
|
1261
|
+
func _score_stack_trace_candidate(node: Control) -> int:
|
|
1262
|
+
var score := 0
|
|
1263
|
+
var name_lower := String(node.name).to_lower()
|
|
1264
|
+
for keyword in STACK_TRACE_KEYWORDS:
|
|
1265
|
+
if name_lower.find(keyword) != -1:
|
|
1266
|
+
score += 35
|
|
1267
|
+
break
|
|
1268
|
+
|
|
1269
|
+
var class_lower := node.get_class().to_lower()
|
|
1270
|
+
if class_lower.find("stack") != -1 or class_lower.find("trace") != -1:
|
|
1271
|
+
score += 20
|
|
1272
|
+
|
|
1273
|
+
if node is Tree:
|
|
1274
|
+
score += 20
|
|
1275
|
+
|
|
1276
|
+
var parent := node.get_parent()
|
|
1277
|
+
var depth := 0
|
|
1278
|
+
while parent and depth < 4:
|
|
1279
|
+
var parent_name := String(parent.name).to_lower()
|
|
1280
|
+
if parent_name.find("debug") != -1 or parent_name.find("stack") != -1:
|
|
1281
|
+
score += 10
|
|
1282
|
+
break
|
|
1283
|
+
parent = parent.get_parent()
|
|
1284
|
+
depth += 1
|
|
1285
|
+
|
|
1286
|
+
if node.is_inside_tree():
|
|
1287
|
+
var path_lower := String(node.get_path()).to_lower()
|
|
1288
|
+
for keyword in STACK_TRACE_KEYWORDS:
|
|
1289
|
+
if path_lower.find(keyword) != -1:
|
|
1290
|
+
score += 10
|
|
1291
|
+
break
|
|
1292
|
+
|
|
1293
|
+
return score
|
|
1294
|
+
|
|
1295
|
+
func _score_stack_frames_candidate(node: Control) -> int:
|
|
1296
|
+
var score := 0
|
|
1297
|
+
var name_lower := String(node.name).to_lower()
|
|
1298
|
+
if name_lower.find("frame") != -1 and name_lower.find("stack") != -1:
|
|
1299
|
+
score += 35
|
|
1300
|
+
else:
|
|
1301
|
+
for keyword in STACK_FRAMES_KEYWORDS:
|
|
1302
|
+
if name_lower.find(keyword) != -1:
|
|
1303
|
+
score += 35
|
|
1304
|
+
break
|
|
1305
|
+
|
|
1306
|
+
var class_lower := node.get_class().to_lower()
|
|
1307
|
+
if class_lower.find("frame") != -1 and class_lower.find("stack") != -1:
|
|
1308
|
+
score += 20
|
|
1309
|
+
|
|
1310
|
+
if node is Tree:
|
|
1311
|
+
score += 20
|
|
1312
|
+
|
|
1313
|
+
var parent := node.get_parent()
|
|
1314
|
+
var depth := 0
|
|
1315
|
+
while parent and depth < 4:
|
|
1316
|
+
var parent_name := String(parent.name).to_lower()
|
|
1317
|
+
if parent_name.find("frame") != -1 and parent_name.find("stack") != -1:
|
|
1318
|
+
score += 10
|
|
1319
|
+
break
|
|
1320
|
+
if parent_name.find("call stack") != -1:
|
|
1321
|
+
score += 10
|
|
1322
|
+
break
|
|
1323
|
+
parent = parent.get_parent()
|
|
1324
|
+
depth += 1
|
|
1325
|
+
|
|
1326
|
+
if node.is_inside_tree():
|
|
1327
|
+
var path_lower := String(node.get_path()).to_lower()
|
|
1328
|
+
if path_lower.find("frame") != -1 and path_lower.find("stack") != -1:
|
|
1329
|
+
score += 10
|
|
1330
|
+
elif path_lower.find("call_stack") != -1 or path_lower.find("call stack") != -1:
|
|
1331
|
+
score += 10
|
|
1332
|
+
|
|
1333
|
+
return score
|
|
1334
|
+
|
|
1335
|
+
func _title_matches_stack_frames(title_lower: String) -> bool:
|
|
1336
|
+
if title_lower.is_empty():
|
|
1337
|
+
return false
|
|
1338
|
+
if title_lower.find("stack frames") != -1 or title_lower.find("stack frame") != -1:
|
|
1339
|
+
return true
|
|
1340
|
+
if title_lower.find("call stack") != -1:
|
|
1341
|
+
return true
|
|
1342
|
+
if title_lower.find("frame") != -1 and title_lower.find("stack") != -1:
|
|
1343
|
+
return true
|
|
1344
|
+
for keyword in STACK_FRAMES_KEYWORDS:
|
|
1345
|
+
if title_lower.find(keyword) != -1:
|
|
1346
|
+
return true
|
|
1347
|
+
return false
|
|
1348
|
+
|
|
1349
|
+
func _collect_stack_tree_frames(tree: Tree) -> Array:
|
|
1350
|
+
var frames: Array = []
|
|
1351
|
+
if not is_instance_valid(tree):
|
|
1352
|
+
return frames
|
|
1353
|
+
|
|
1354
|
+
var root := tree.get_root()
|
|
1355
|
+
if not root:
|
|
1356
|
+
return frames
|
|
1357
|
+
|
|
1358
|
+
var item := root.get_first_child()
|
|
1359
|
+
var fallback_index := 0
|
|
1360
|
+
while item:
|
|
1361
|
+
frames.append(_build_stack_frame_from_item(item, tree.columns, fallback_index))
|
|
1362
|
+
fallback_index += 1
|
|
1363
|
+
item = item.get_next()
|
|
1364
|
+
|
|
1365
|
+
return frames
|
|
1366
|
+
|
|
1367
|
+
func _build_stack_frame_from_item(item: TreeItem, column_count: int, fallback_index: int) -> Dictionary:
|
|
1368
|
+
var columns: Array = []
|
|
1369
|
+
for col in range(column_count):
|
|
1370
|
+
columns.append(String(item.get_text(col)))
|
|
1371
|
+
|
|
1372
|
+
var function_name := ""
|
|
1373
|
+
var location := ""
|
|
1374
|
+
|
|
1375
|
+
if columns.size() >= 2:
|
|
1376
|
+
function_name = columns[1]
|
|
1377
|
+
if columns.size() >= 3:
|
|
1378
|
+
location = columns[2]
|
|
1379
|
+
elif columns.size() >= 2:
|
|
1380
|
+
location = columns[1]
|
|
1381
|
+
|
|
1382
|
+
var script_path := ""
|
|
1383
|
+
var line_number := -1
|
|
1384
|
+
|
|
1385
|
+
if not location.is_empty():
|
|
1386
|
+
var split := location.rsplit(":", false, 1)
|
|
1387
|
+
if split.size() == 2:
|
|
1388
|
+
var potential_line := split[1].strip_edges()
|
|
1389
|
+
if potential_line.is_valid_int():
|
|
1390
|
+
line_number = int(potential_line)
|
|
1391
|
+
script_path = split[0]
|
|
1392
|
+
|
|
1393
|
+
var index_value := fallback_index
|
|
1394
|
+
if columns.size() > 0:
|
|
1395
|
+
var index_text := String(columns[0]).strip_edges()
|
|
1396
|
+
if index_text.begins_with("#"):
|
|
1397
|
+
index_text = index_text.substr(1).strip_edges()
|
|
1398
|
+
if index_text.is_valid_int():
|
|
1399
|
+
index_value = int(index_text)
|
|
1400
|
+
|
|
1401
|
+
return {
|
|
1402
|
+
"index": index_value,
|
|
1403
|
+
"function": function_name,
|
|
1404
|
+
"location": location,
|
|
1405
|
+
"script": script_path,
|
|
1406
|
+
"line": line_number,
|
|
1407
|
+
"columns": columns
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
func _derive_frames_from_lines(lines: Array) -> Array:
|
|
1411
|
+
var frames: Array = []
|
|
1412
|
+
var i := 0
|
|
1413
|
+
var fallback_index := 0
|
|
1414
|
+
while i < lines.size():
|
|
1415
|
+
var current_line := String(lines[i]).strip_edges()
|
|
1416
|
+
if current_line.is_empty():
|
|
1417
|
+
i += 1
|
|
1418
|
+
continue
|
|
1419
|
+
|
|
1420
|
+
var consumed_lines := 1
|
|
1421
|
+
var parsed := _parse_stack_line(current_line, fallback_index)
|
|
1422
|
+
|
|
1423
|
+
if parsed.is_empty():
|
|
1424
|
+
if current_line.begins_with("res://") or current_line.begins_with("user://"):
|
|
1425
|
+
var script_path := current_line
|
|
1426
|
+
var line_number := -1
|
|
1427
|
+
if i + 1 < lines.size():
|
|
1428
|
+
var next_line := String(lines[i + 1]).strip_edges()
|
|
1429
|
+
var prefix := "Line "
|
|
1430
|
+
if next_line.begins_with(prefix):
|
|
1431
|
+
var maybe_number := next_line.substr(prefix.length()).strip_edges()
|
|
1432
|
+
if maybe_number.is_valid_int():
|
|
1433
|
+
line_number = int(maybe_number)
|
|
1434
|
+
consumed_lines = 2
|
|
1435
|
+
var location := script_path
|
|
1436
|
+
if line_number >= 0:
|
|
1437
|
+
location = "%s:%d" % [script_path, line_number]
|
|
1438
|
+
parsed = {
|
|
1439
|
+
"index": fallback_index,
|
|
1440
|
+
"function": "",
|
|
1441
|
+
"location": location,
|
|
1442
|
+
"script": script_path,
|
|
1443
|
+
"line": line_number,
|
|
1444
|
+
"columns": [script_path]
|
|
1445
|
+
}
|
|
1446
|
+
if not parsed.is_empty():
|
|
1447
|
+
frames.append(parsed)
|
|
1448
|
+
fallback_index += 1
|
|
1449
|
+
i += consumed_lines
|
|
1450
|
+
|
|
1451
|
+
return frames
|
|
1452
|
+
|
|
1453
|
+
func _parse_stack_line(line: String, fallback_index: int) -> Dictionary:
|
|
1454
|
+
var trimmed := line.strip_edges()
|
|
1455
|
+
if trimmed.is_empty():
|
|
1456
|
+
return {}
|
|
1457
|
+
|
|
1458
|
+
var index_value := fallback_index
|
|
1459
|
+
if trimmed.begins_with("#"):
|
|
1460
|
+
var after_hash := trimmed.substr(1).strip_edges()
|
|
1461
|
+
var parts := after_hash.split(" ", false)
|
|
1462
|
+
if parts.size() > 0:
|
|
1463
|
+
var index_text := parts[0].strip_edges()
|
|
1464
|
+
if index_text.is_valid_int():
|
|
1465
|
+
index_value = int(index_text)
|
|
1466
|
+
|
|
1467
|
+
var script_path := ""
|
|
1468
|
+
var line_number := -1
|
|
1469
|
+
var colon_index := trimmed.rfind(":")
|
|
1470
|
+
if colon_index != -1 and colon_index + 1 < trimmed.length():
|
|
1471
|
+
var maybe_line := trimmed.substr(colon_index + 1).strip_edges()
|
|
1472
|
+
if maybe_line.is_valid_int():
|
|
1473
|
+
line_number = int(maybe_line)
|
|
1474
|
+
script_path = trimmed.substr(0, colon_index)
|
|
1475
|
+
return {
|
|
1476
|
+
"index": index_value,
|
|
1477
|
+
"function": "",
|
|
1478
|
+
"location": trimmed,
|
|
1479
|
+
"script": script_path,
|
|
1480
|
+
"line": line_number,
|
|
1481
|
+
"columns": [trimmed]
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
return {}
|
|
1485
|
+
|
|
1486
|
+
func _unwrap_tab_content(control: Control) -> Control:
|
|
1487
|
+
if not is_instance_valid(control):
|
|
1488
|
+
return control
|
|
1489
|
+
|
|
1490
|
+
# Godot editor wraps tab content inside MarginContainer -> VBox/HBox -> actual content.
|
|
1491
|
+
# Try to find the deepest Control that actually holds text.
|
|
1492
|
+
var current := control
|
|
1493
|
+
var safety := 0
|
|
1494
|
+
|
|
1495
|
+
while safety < 5 and current.get_child_count() == 1:
|
|
1496
|
+
var child = current.get_child(0)
|
|
1497
|
+
if child is Control:
|
|
1498
|
+
current = child
|
|
1499
|
+
safety += 1
|
|
1500
|
+
else:
|
|
1501
|
+
break
|
|
1502
|
+
|
|
1503
|
+
# If we ended up on a container with multiple children, try to pick the TextEdit/RichTextLabel.
|
|
1504
|
+
if current.get_child_count() > 1:
|
|
1505
|
+
for child in current.get_children():
|
|
1506
|
+
if child is Control and (_is_text_display_control(child)):
|
|
1507
|
+
return child
|
|
1508
|
+
|
|
1509
|
+
return current
|
|
1510
|
+
|
|
1511
|
+
func _is_text_display_control(control: Control) -> bool:
|
|
1512
|
+
if not is_instance_valid(control):
|
|
1513
|
+
return false
|
|
1514
|
+
if control.is_class("TextEdit") or control.is_class("CodeEdit") or control.is_class("RichTextLabel"):
|
|
1515
|
+
return true
|
|
1516
|
+
return false
|
|
1517
|
+
|
|
1518
|
+
func _find_descendant_tree(root: Node, max_nodes: int = 8192) -> Tree:
|
|
1519
|
+
if not is_instance_valid(root):
|
|
1520
|
+
return null
|
|
1521
|
+
var queue: Array = [root]
|
|
1522
|
+
var visited := 0
|
|
1523
|
+
while queue.size() > 0 and visited < max_nodes:
|
|
1524
|
+
var candidate = queue.pop_front()
|
|
1525
|
+
visited += 1
|
|
1526
|
+
if not is_instance_valid(candidate):
|
|
1527
|
+
continue
|
|
1528
|
+
if candidate is Tree:
|
|
1529
|
+
return candidate
|
|
1530
|
+
for child in candidate.get_children():
|
|
1531
|
+
if child is Node:
|
|
1532
|
+
queue.append(child)
|
|
1533
|
+
return null
|
|
1534
|
+
|
|
1535
|
+
func _find_descendant_text_control(root: Node, max_nodes: int = 4096) -> Control:
|
|
1536
|
+
if not is_instance_valid(root):
|
|
1537
|
+
return null
|
|
1538
|
+
var queue: Array = [root]
|
|
1539
|
+
var visited := 0
|
|
1540
|
+
while queue.size() > 0 and visited < max_nodes:
|
|
1541
|
+
var candidate = queue.pop_front()
|
|
1542
|
+
if not is_instance_valid(candidate):
|
|
1543
|
+
continue
|
|
1544
|
+
visited += 1
|
|
1545
|
+
if candidate is Control and _is_text_display_control(candidate):
|
|
1546
|
+
return candidate
|
|
1547
|
+
for child in candidate.get_children():
|
|
1548
|
+
if child is Node:
|
|
1549
|
+
queue.append(child)
|
|
1550
|
+
return null
|
|
1551
|
+
|
|
1552
|
+
func _summarize_control_structure(root: Node, max_depth: int, max_nodes: int) -> Array:
|
|
1553
|
+
var summary: Array = []
|
|
1554
|
+
if not is_instance_valid(root):
|
|
1555
|
+
return summary
|
|
1556
|
+
var queue: Array = [{
|
|
1557
|
+
"node": root,
|
|
1558
|
+
"depth": 0
|
|
1559
|
+
}]
|
|
1560
|
+
var visited := 0
|
|
1561
|
+
while queue.size() > 0 and visited < max_nodes:
|
|
1562
|
+
var entry = queue.pop_front()
|
|
1563
|
+
var node = entry["node"]
|
|
1564
|
+
var depth = entry["depth"]
|
|
1565
|
+
if not is_instance_valid(node) or depth > max_depth:
|
|
1566
|
+
continue
|
|
1567
|
+
var prefix := ""
|
|
1568
|
+
for i in range(depth):
|
|
1569
|
+
prefix += "-"
|
|
1570
|
+
summary.append("%s%s (%s)" % [prefix, node.name, node.get_class()])
|
|
1571
|
+
visited += 1
|
|
1572
|
+
if depth == max_depth:
|
|
1573
|
+
continue
|
|
1574
|
+
for child in node.get_children():
|
|
1575
|
+
if child is Node:
|
|
1576
|
+
queue.append({
|
|
1577
|
+
"node": child,
|
|
1578
|
+
"depth": depth + 1
|
|
1579
|
+
})
|
|
1580
|
+
return summary
|
|
1581
|
+
|
|
1582
|
+
func _collect_tree_lines(tree: Tree) -> Array:
|
|
1583
|
+
var lines: Array = []
|
|
1584
|
+
if not is_instance_valid(tree):
|
|
1585
|
+
return lines
|
|
1586
|
+
var root := tree.get_root()
|
|
1587
|
+
if not root:
|
|
1588
|
+
return lines
|
|
1589
|
+
var column_count: int = tree.columns
|
|
1590
|
+
if tree.has_method("is_hide_root") and not tree.is_hide_root():
|
|
1591
|
+
_collect_tree_item_lines(root, lines, 0, column_count)
|
|
1592
|
+
var child := root.get_first_child()
|
|
1593
|
+
while child:
|
|
1594
|
+
_collect_tree_item_lines(child, lines, 1, column_count)
|
|
1595
|
+
child = child.get_next()
|
|
1596
|
+
else:
|
|
1597
|
+
var item := root.get_first_child()
|
|
1598
|
+
while item:
|
|
1599
|
+
_collect_tree_item_lines(item, lines, 0, column_count)
|
|
1600
|
+
item = item.get_next()
|
|
1601
|
+
return lines
|
|
1602
|
+
|
|
1603
|
+
func _count_tree_items(tree: Tree) -> int:
|
|
1604
|
+
if not is_instance_valid(tree):
|
|
1605
|
+
return 0
|
|
1606
|
+
var root := tree.get_root()
|
|
1607
|
+
if not root:
|
|
1608
|
+
return 0
|
|
1609
|
+
var count := 0
|
|
1610
|
+
if tree.has_method("is_hide_root") and not tree.is_hide_root():
|
|
1611
|
+
count += 1
|
|
1612
|
+
var child := root.get_first_child()
|
|
1613
|
+
while child:
|
|
1614
|
+
count += 1
|
|
1615
|
+
child = child.get_next()
|
|
1616
|
+
else:
|
|
1617
|
+
var item := root.get_first_child()
|
|
1618
|
+
while item:
|
|
1619
|
+
count += 1
|
|
1620
|
+
item = item.get_next()
|
|
1621
|
+
return count
|
|
1622
|
+
|
|
1623
|
+
func _collect_tree_item_lines(item: TreeItem, lines: Array, depth: int, column_count: int) -> void:
|
|
1624
|
+
if not is_instance_valid(item):
|
|
1625
|
+
return
|
|
1626
|
+
var parts: Array = []
|
|
1627
|
+
|
|
1628
|
+
if item.has_meta("_is_warning"):
|
|
1629
|
+
parts.append("[warning]")
|
|
1630
|
+
elif item.has_meta("_is_error"):
|
|
1631
|
+
parts.append("[error]")
|
|
1632
|
+
|
|
1633
|
+
var main_text := item.get_text(0)
|
|
1634
|
+
if not main_text.is_empty():
|
|
1635
|
+
parts.append(main_text)
|
|
1636
|
+
|
|
1637
|
+
for col in range(1, column_count):
|
|
1638
|
+
var extra := item.get_text(col)
|
|
1639
|
+
if not extra.is_empty():
|
|
1640
|
+
parts.append(extra)
|
|
1641
|
+
|
|
1642
|
+
if parts.size() > 0:
|
|
1643
|
+
var prefix := _make_indent(depth)
|
|
1644
|
+
lines.append(prefix + " ".join(parts))
|
|
1645
|
+
|
|
1646
|
+
var child := item.get_first_child()
|
|
1647
|
+
while child:
|
|
1648
|
+
_collect_tree_item_lines(child, lines, depth + 1, column_count)
|
|
1649
|
+
child = child.get_next()
|
|
1650
|
+
|
|
1651
|
+
func get_capture_diagnostics() -> Dictionary:
|
|
1652
|
+
return {
|
|
1653
|
+
"source": _last_capture_source,
|
|
1654
|
+
"detail": _last_capture_detail,
|
|
1655
|
+
"timestamp": _last_capture_timestamp,
|
|
1656
|
+
"control_class": _last_control_class,
|
|
1657
|
+
"control_path": _last_control_path,
|
|
1658
|
+
"log_file_path": _last_log_file_path,
|
|
1659
|
+
"control_search": _last_control_search_summary
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
func _make_indent(depth: int) -> String:
|
|
1663
|
+
if depth <= 0:
|
|
1664
|
+
return ""
|
|
1665
|
+
var spaces := depth * 2
|
|
1666
|
+
var builder := ""
|
|
1667
|
+
for i in range(spaces):
|
|
1668
|
+
builder += " "
|
|
1669
|
+
return builder
|