@satelliteoflove/godot-mcp 2.16.0 → 2.17.0
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/addon/command_router.gd +1 -0
- package/addon/commands/debug_commands.gd +0 -36
- package/addon/commands/profiler_commands.gd +143 -0
- package/addon/core/mcp_debugger_plugin.gd +52 -0
- package/addon/game_bridge/mcp_frame_profiler.gd +61 -0
- package/addon/game_bridge/mcp_game_bridge.gd +170 -0
- package/addon/plugin.cfg +1 -1
- package/addon/websocket_server.gd +6 -78
- package/dist/__tests__/tools/profiler-actions.test.d.ts +2 -0
- package/dist/__tests__/tools/profiler-actions.test.d.ts.map +1 -0
- package/dist/__tests__/tools/profiler-actions.test.js +95 -0
- package/dist/__tests__/tools/profiler-actions.test.js.map +1 -0
- package/dist/__tests__/tools/profiler.test.d.ts +2 -0
- package/dist/__tests__/tools/profiler.test.d.ts.map +1 -0
- package/dist/__tests__/tools/profiler.test.js +163 -0
- package/dist/__tests__/tools/profiler.test.js.map +1 -0
- package/dist/connection/websocket.d.ts +1 -1
- package/dist/connection/websocket.d.ts.map +1 -1
- package/dist/connection/websocket.js +14 -0
- package/dist/connection/websocket.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -1
- package/dist/tools/editor.d.ts.map +1 -1
- package/dist/tools/editor.js +2 -1
- package/dist/tools/editor.js.map +1 -1
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +3 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/profiler.d.ts +54 -0
- package/dist/tools/profiler.d.ts.map +1 -0
- package/dist/tools/profiler.js +204 -0
- package/dist/tools/profiler.js.map +1 -0
- package/package.json +1 -1
package/addon/command_router.gd
CHANGED
|
@@ -20,6 +20,7 @@ func setup(plugin: EditorPlugin) -> void:
|
|
|
20
20
|
_register_handler(MCPResourceCommands.new(), plugin)
|
|
21
21
|
_register_handler(MCPScene3DCommands.new(), plugin)
|
|
22
22
|
_register_handler(MCPInputCommands.new(), plugin)
|
|
23
|
+
_register_handler(MCPProfilerCommands.new(), plugin)
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
func _register_handler(handler: MCPBaseCommand, plugin: EditorPlugin) -> void:
|
|
@@ -3,21 +3,16 @@ extends MCPBaseCommand
|
|
|
3
3
|
class_name MCPDebugCommands
|
|
4
4
|
|
|
5
5
|
const DEBUG_OUTPUT_TIMEOUT := 5.0
|
|
6
|
-
const PERFORMANCE_METRICS_TIMEOUT := 5.0
|
|
7
6
|
|
|
8
7
|
var _debug_output_result: PackedStringArray = []
|
|
9
8
|
var _debug_output_pending: bool = false
|
|
10
9
|
|
|
11
|
-
var _performance_metrics_result: Dictionary = {}
|
|
12
|
-
var _performance_metrics_pending: bool = false
|
|
13
|
-
|
|
14
10
|
|
|
15
11
|
func get_commands() -> Dictionary:
|
|
16
12
|
return {
|
|
17
13
|
"run_project": run_project,
|
|
18
14
|
"stop_project": stop_project,
|
|
19
15
|
"get_debug_output": get_debug_output,
|
|
20
|
-
"get_performance_metrics": get_performance_metrics,
|
|
21
16
|
"get_log_messages": get_log_messages,
|
|
22
17
|
"get_errors": get_errors,
|
|
23
18
|
"get_stack_trace": get_stack_trace,
|
|
@@ -100,37 +95,6 @@ func _on_debug_output_received(output: PackedStringArray) -> void:
|
|
|
100
95
|
_debug_output_result = output
|
|
101
96
|
|
|
102
97
|
|
|
103
|
-
func get_performance_metrics(_params: Dictionary) -> Dictionary:
|
|
104
|
-
if not EditorInterface.is_playing_scene():
|
|
105
|
-
return _error("NOT_RUNNING", "No game is currently running")
|
|
106
|
-
|
|
107
|
-
var debugger_plugin = _plugin.get_debugger_plugin() if _plugin else null
|
|
108
|
-
if debugger_plugin == null or not debugger_plugin.has_active_session():
|
|
109
|
-
return _error("NO_SESSION", "No active debug session")
|
|
110
|
-
|
|
111
|
-
_performance_metrics_pending = true
|
|
112
|
-
_performance_metrics_result = {}
|
|
113
|
-
|
|
114
|
-
debugger_plugin.performance_metrics_received.connect(_on_performance_metrics_received, CONNECT_ONE_SHOT)
|
|
115
|
-
debugger_plugin.request_performance_metrics()
|
|
116
|
-
|
|
117
|
-
var start_time := Time.get_ticks_msec()
|
|
118
|
-
while _performance_metrics_pending:
|
|
119
|
-
await Engine.get_main_loop().process_frame
|
|
120
|
-
if (Time.get_ticks_msec() - start_time) / 1000.0 > PERFORMANCE_METRICS_TIMEOUT:
|
|
121
|
-
_performance_metrics_pending = false
|
|
122
|
-
if debugger_plugin.performance_metrics_received.is_connected(_on_performance_metrics_received):
|
|
123
|
-
debugger_plugin.performance_metrics_received.disconnect(_on_performance_metrics_received)
|
|
124
|
-
return _error("TIMEOUT", "Timed out waiting for performance metrics")
|
|
125
|
-
|
|
126
|
-
return _success(_performance_metrics_result)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
func _on_performance_metrics_received(metrics: Dictionary) -> void:
|
|
130
|
-
_performance_metrics_pending = false
|
|
131
|
-
_performance_metrics_result = metrics
|
|
132
|
-
|
|
133
|
-
|
|
134
98
|
func get_log_messages(params: Dictionary) -> Dictionary:
|
|
135
99
|
var clear: bool = params.get("clear", false)
|
|
136
100
|
var limit: int = params.get("limit", 50)
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
@tool
|
|
2
|
+
extends MCPBaseCommand
|
|
3
|
+
class_name MCPProfilerCommands
|
|
4
|
+
|
|
5
|
+
const PROFILER_TIMEOUT := 5.0
|
|
6
|
+
const GENERIC_TIMEOUT := 5.0
|
|
7
|
+
|
|
8
|
+
var _performance_metrics_pending: bool = false
|
|
9
|
+
var _performance_metrics_result: Dictionary = {}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
func get_commands() -> Dictionary:
|
|
13
|
+
return {
|
|
14
|
+
"get_performance_metrics": get_performance_metrics,
|
|
15
|
+
"start_profiler": start_profiler,
|
|
16
|
+
"stop_profiler": stop_profiler,
|
|
17
|
+
"get_profiler_data": get_profiler_data,
|
|
18
|
+
"get_active_processes": get_active_processes,
|
|
19
|
+
"get_signal_connections": get_signal_connections,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
func get_performance_metrics(_params: Dictionary) -> Dictionary:
|
|
24
|
+
if not EditorInterface.is_playing_scene():
|
|
25
|
+
return _error("NOT_RUNNING", "No game is currently running")
|
|
26
|
+
|
|
27
|
+
var debugger_plugin = _plugin.get_debugger_plugin() if _plugin else null
|
|
28
|
+
if debugger_plugin == null or not debugger_plugin.has_active_session():
|
|
29
|
+
return _error("NO_SESSION", "No active debug session")
|
|
30
|
+
|
|
31
|
+
_performance_metrics_pending = true
|
|
32
|
+
_performance_metrics_result = {}
|
|
33
|
+
|
|
34
|
+
debugger_plugin.performance_metrics_received.connect(_on_performance_metrics_received, CONNECT_ONE_SHOT)
|
|
35
|
+
debugger_plugin.request_performance_metrics()
|
|
36
|
+
|
|
37
|
+
var start_time := Time.get_ticks_msec()
|
|
38
|
+
while _performance_metrics_pending:
|
|
39
|
+
await Engine.get_main_loop().process_frame
|
|
40
|
+
if (Time.get_ticks_msec() - start_time) / 1000.0 > PROFILER_TIMEOUT:
|
|
41
|
+
_performance_metrics_pending = false
|
|
42
|
+
if debugger_plugin.performance_metrics_received.is_connected(_on_performance_metrics_received):
|
|
43
|
+
debugger_plugin.performance_metrics_received.disconnect(_on_performance_metrics_received)
|
|
44
|
+
return _error("TIMEOUT", "Timed out waiting for performance metrics")
|
|
45
|
+
|
|
46
|
+
return _success(_performance_metrics_result)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
func _on_performance_metrics_received(metrics: Dictionary) -> void:
|
|
50
|
+
_performance_metrics_pending = false
|
|
51
|
+
_performance_metrics_result = metrics
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
func start_profiler(_params: Dictionary) -> Dictionary:
|
|
55
|
+
if not EditorInterface.is_playing_scene():
|
|
56
|
+
return _error("NOT_RUNNING", "No game is currently running")
|
|
57
|
+
|
|
58
|
+
var debugger_plugin = _plugin.get_debugger_plugin() if _plugin else null
|
|
59
|
+
if debugger_plugin == null or not debugger_plugin.has_active_session():
|
|
60
|
+
return _error("NO_SESSION", "No active debug session")
|
|
61
|
+
|
|
62
|
+
debugger_plugin.toggle_frame_profiler(true)
|
|
63
|
+
return _success({"message": "Frame profiler started"})
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
func stop_profiler(_params: Dictionary) -> Dictionary:
|
|
67
|
+
if not EditorInterface.is_playing_scene():
|
|
68
|
+
return _error("NOT_RUNNING", "No game is currently running")
|
|
69
|
+
|
|
70
|
+
var debugger_plugin = _plugin.get_debugger_plugin() if _plugin else null
|
|
71
|
+
if debugger_plugin == null or not debugger_plugin.has_active_session():
|
|
72
|
+
return _error("NO_SESSION", "No active debug session")
|
|
73
|
+
|
|
74
|
+
debugger_plugin.toggle_frame_profiler(false)
|
|
75
|
+
return _success({"message": "Frame profiler stopped"})
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
func get_profiler_data(_params: Dictionary) -> Dictionary:
|
|
79
|
+
var result = await _send_and_wait("get_profiler_data")
|
|
80
|
+
if result == null:
|
|
81
|
+
return _last_error
|
|
82
|
+
var result_dict: Dictionary
|
|
83
|
+
if result is Dictionary:
|
|
84
|
+
result_dict = result
|
|
85
|
+
else:
|
|
86
|
+
result_dict = {"data": result}
|
|
87
|
+
return _success(result_dict)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
func get_active_processes(_params: Dictionary) -> Dictionary:
|
|
91
|
+
var result = await _send_and_wait("get_active_processes")
|
|
92
|
+
if result == null:
|
|
93
|
+
return _last_error
|
|
94
|
+
var result_dict: Dictionary
|
|
95
|
+
if result is Dictionary:
|
|
96
|
+
result_dict = result
|
|
97
|
+
else:
|
|
98
|
+
result_dict = {"data": result}
|
|
99
|
+
return _success(result_dict)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
func get_signal_connections(params: Dictionary) -> Dictionary:
|
|
103
|
+
var node_path: String = params.get("node_path", "")
|
|
104
|
+
var result = await _send_and_wait("get_signal_connections", [node_path])
|
|
105
|
+
if result == null:
|
|
106
|
+
return _last_error
|
|
107
|
+
var result_dict: Dictionary
|
|
108
|
+
if result is Dictionary:
|
|
109
|
+
result_dict = result
|
|
110
|
+
else:
|
|
111
|
+
result_dict = {"data": result}
|
|
112
|
+
return _success(result_dict)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
var _last_error: Dictionary = {}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
func _send_and_wait(msg_type: String, args: Array = []):
|
|
119
|
+
if not EditorInterface.is_playing_scene():
|
|
120
|
+
_last_error = _error("NOT_RUNNING", "No game is currently running")
|
|
121
|
+
return null
|
|
122
|
+
|
|
123
|
+
var debugger_plugin = _plugin.get_debugger_plugin() if _plugin else null
|
|
124
|
+
if debugger_plugin == null or not debugger_plugin.has_active_session():
|
|
125
|
+
_last_error = _error("NO_SESSION", "No active debug session")
|
|
126
|
+
return null
|
|
127
|
+
|
|
128
|
+
var sent: bool = debugger_plugin.send_game_message(msg_type, args)
|
|
129
|
+
if not sent:
|
|
130
|
+
_last_error = _error("SEND_FAILED", "Failed to send message to game")
|
|
131
|
+
return null
|
|
132
|
+
|
|
133
|
+
var start_time := Time.get_ticks_msec()
|
|
134
|
+
while not debugger_plugin.has_response(msg_type):
|
|
135
|
+
await Engine.get_main_loop().process_frame
|
|
136
|
+
if (Time.get_ticks_msec() - start_time) / 1000.0 > GENERIC_TIMEOUT:
|
|
137
|
+
debugger_plugin.clear_response(msg_type)
|
|
138
|
+
_last_error = _error("TIMEOUT", "Timed out waiting for %s response" % msg_type)
|
|
139
|
+
return null
|
|
140
|
+
|
|
141
|
+
var response = debugger_plugin.get_response(msg_type)
|
|
142
|
+
debugger_plugin.clear_response(msg_type)
|
|
143
|
+
return response
|
|
@@ -9,6 +9,7 @@ signal find_nodes_received(matches: Array, count: int, error: String)
|
|
|
9
9
|
signal input_map_received(actions: Array, error: String)
|
|
10
10
|
signal input_sequence_completed(result: Dictionary)
|
|
11
11
|
signal type_text_completed(result: Dictionary)
|
|
12
|
+
signal game_response(message_type: String, data: Variant)
|
|
12
13
|
|
|
13
14
|
var _active_session_id: int = -1
|
|
14
15
|
var _pending_screenshot: bool = false
|
|
@@ -18,6 +19,8 @@ var _pending_find_nodes: bool = false
|
|
|
18
19
|
var _pending_input_map: bool = false
|
|
19
20
|
var _pending_input_sequence: bool = false
|
|
20
21
|
var _pending_type_text: bool = false
|
|
22
|
+
var _pending_requests: Dictionary = {}
|
|
23
|
+
var _responses: Dictionary = {}
|
|
21
24
|
|
|
22
25
|
|
|
23
26
|
func _has_capture(prefix: String) -> bool:
|
|
@@ -47,6 +50,9 @@ func _capture(message: String, data: Array, session_id: int) -> bool:
|
|
|
47
50
|
"godot_mcp:type_text_result":
|
|
48
51
|
_handle_type_text_result(data)
|
|
49
52
|
return true
|
|
53
|
+
"godot_mcp:game_response":
|
|
54
|
+
_handle_game_response(data)
|
|
55
|
+
return true
|
|
50
56
|
return false
|
|
51
57
|
|
|
52
58
|
|
|
@@ -77,6 +83,9 @@ func _session_stopped() -> void:
|
|
|
77
83
|
if _pending_type_text:
|
|
78
84
|
_pending_type_text = false
|
|
79
85
|
type_text_completed.emit({"error": "Game session ended"})
|
|
86
|
+
for msg_type in _pending_requests:
|
|
87
|
+
_responses[msg_type] = {}
|
|
88
|
+
_pending_requests.clear()
|
|
80
89
|
|
|
81
90
|
|
|
82
91
|
func has_active_session() -> bool:
|
|
@@ -229,3 +238,46 @@ func _handle_type_text_result(data: Array) -> void:
|
|
|
229
238
|
_pending_type_text = false
|
|
230
239
|
var result: Dictionary = data[0] if data.size() > 0 else {}
|
|
231
240
|
type_text_completed.emit(result)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
func send_game_message(msg_type: String, args: Array = []) -> bool:
|
|
244
|
+
if _active_session_id < 0:
|
|
245
|
+
return false
|
|
246
|
+
var session := get_session(_active_session_id)
|
|
247
|
+
if not session:
|
|
248
|
+
return false
|
|
249
|
+
_pending_requests[msg_type] = true
|
|
250
|
+
_responses.erase(msg_type)
|
|
251
|
+
session.send_message("godot_mcp:" + msg_type, args)
|
|
252
|
+
return true
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
func has_response(msg_type: String) -> bool:
|
|
256
|
+
return _responses.has(msg_type)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
func get_response(msg_type: String) -> Variant:
|
|
260
|
+
return _responses.get(msg_type)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
func clear_response(msg_type: String) -> void:
|
|
264
|
+
_responses.erase(msg_type)
|
|
265
|
+
_pending_requests.erase(msg_type)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
func _handle_game_response(data: Array) -> void:
|
|
269
|
+
if data.size() < 2:
|
|
270
|
+
return
|
|
271
|
+
var msg_type: String = data[0]
|
|
272
|
+
var response_data: Variant = data[1]
|
|
273
|
+
_pending_requests.erase(msg_type)
|
|
274
|
+
_responses[msg_type] = response_data
|
|
275
|
+
game_response.emit(msg_type, response_data)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
func toggle_frame_profiler(enable: bool) -> void:
|
|
279
|
+
if _active_session_id < 0:
|
|
280
|
+
return
|
|
281
|
+
var session := get_session(_active_session_id)
|
|
282
|
+
if session:
|
|
283
|
+
session.toggle_profiler("mcp_frame_profiler", enable)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
extends EngineProfiler
|
|
2
|
+
class_name MCPFrameProfiler
|
|
3
|
+
|
|
4
|
+
const MAX_FRAMES := 300
|
|
5
|
+
const MONITOR_SAMPLE_INTERVAL := 10
|
|
6
|
+
|
|
7
|
+
var _active := false
|
|
8
|
+
var _buffer: Array[Dictionary] = []
|
|
9
|
+
var _frame_index := 0
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
func _toggle(enable: bool, _options: Array) -> void:
|
|
13
|
+
_active = enable
|
|
14
|
+
if enable:
|
|
15
|
+
_buffer.clear()
|
|
16
|
+
_frame_index = 0
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
func _tick(frame_time: float, process_time: float, physics_time: float, physics_frame_time: float) -> void:
|
|
20
|
+
if not _active:
|
|
21
|
+
return
|
|
22
|
+
|
|
23
|
+
var entry := {
|
|
24
|
+
"ft": frame_time,
|
|
25
|
+
"pt": process_time,
|
|
26
|
+
"pht": physics_time,
|
|
27
|
+
"pft": physics_frame_time,
|
|
28
|
+
"i": _frame_index,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if _frame_index % MONITOR_SAMPLE_INTERVAL == 0:
|
|
32
|
+
entry["m"] = _snapshot_monitors()
|
|
33
|
+
|
|
34
|
+
_buffer.append(entry)
|
|
35
|
+
if _buffer.size() > MAX_FRAMES:
|
|
36
|
+
_buffer.pop_front()
|
|
37
|
+
|
|
38
|
+
_frame_index += 1
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
func get_buffer_data() -> Dictionary:
|
|
42
|
+
return {
|
|
43
|
+
"active": _active,
|
|
44
|
+
"frame_count": _buffer.size(),
|
|
45
|
+
"total_frames_collected": _frame_index,
|
|
46
|
+
"max_fps": Engine.max_fps,
|
|
47
|
+
"frames": _buffer.duplicate(),
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
func _snapshot_monitors() -> Dictionary:
|
|
52
|
+
return {
|
|
53
|
+
"fps": Performance.get_monitor(Performance.TIME_FPS),
|
|
54
|
+
"obj_count": int(Performance.get_monitor(Performance.OBJECT_COUNT)),
|
|
55
|
+
"node_count": int(Performance.get_monitor(Performance.OBJECT_NODE_COUNT)),
|
|
56
|
+
"orphan_nodes": int(Performance.get_monitor(Performance.OBJECT_ORPHAN_NODE_COUNT)),
|
|
57
|
+
"mem_static": int(Performance.get_monitor(Performance.MEMORY_STATIC)),
|
|
58
|
+
"render_objects": int(Performance.get_monitor(Performance.RENDER_TOTAL_OBJECTS_IN_FRAME)),
|
|
59
|
+
"render_draw_calls": int(Performance.get_monitor(Performance.RENDER_TOTAL_DRAW_CALLS_IN_FRAME)),
|
|
60
|
+
"render_primitives": int(Performance.get_monitor(Performance.RENDER_TOTAL_PRIMITIVES_IN_FRAME)),
|
|
61
|
+
}
|
|
@@ -4,6 +4,7 @@ class_name MCPGameBridge
|
|
|
4
4
|
const DEFAULT_MAX_WIDTH := 1920
|
|
5
5
|
|
|
6
6
|
var _logger: _MCPGameLogger
|
|
7
|
+
var _profiler: MCPFrameProfiler
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
func _ready() -> void:
|
|
@@ -11,6 +12,8 @@ func _ready() -> void:
|
|
|
11
12
|
return
|
|
12
13
|
_logger = _MCPGameLogger.new()
|
|
13
14
|
OS.add_logger(_logger)
|
|
15
|
+
_profiler = MCPFrameProfiler.new()
|
|
16
|
+
EngineDebugger.register_profiler("mcp_frame_profiler", _profiler)
|
|
14
17
|
EngineDebugger.register_message_capture("godot_mcp", _on_debugger_message)
|
|
15
18
|
MCPLog.info("Game bridge initialized")
|
|
16
19
|
|
|
@@ -18,6 +21,8 @@ func _ready() -> void:
|
|
|
18
21
|
func _exit_tree() -> void:
|
|
19
22
|
if EngineDebugger.is_active():
|
|
20
23
|
EngineDebugger.unregister_message_capture("godot_mcp")
|
|
24
|
+
if _profiler:
|
|
25
|
+
EngineDebugger.unregister_profiler("mcp_frame_profiler")
|
|
21
26
|
|
|
22
27
|
|
|
23
28
|
func _process(_delta: float) -> void:
|
|
@@ -75,6 +80,15 @@ func _on_debugger_message(message: String, data: Array) -> bool:
|
|
|
75
80
|
"type_text":
|
|
76
81
|
_handle_type_text(data)
|
|
77
82
|
return true
|
|
83
|
+
"get_profiler_data":
|
|
84
|
+
_handle_get_profiler_data()
|
|
85
|
+
return true
|
|
86
|
+
"get_active_processes":
|
|
87
|
+
_handle_get_active_processes()
|
|
88
|
+
return true
|
|
89
|
+
"get_signal_connections":
|
|
90
|
+
_handle_get_signal_connections(data)
|
|
91
|
+
return true
|
|
78
92
|
return false
|
|
79
93
|
|
|
80
94
|
|
|
@@ -191,19 +205,175 @@ func _handle_get_performance_metrics() -> void:
|
|
|
191
205
|
"render_objects": int(Performance.get_monitor(Performance.RENDER_TOTAL_OBJECTS_IN_FRAME)),
|
|
192
206
|
"render_draw_calls": int(Performance.get_monitor(Performance.RENDER_TOTAL_DRAW_CALLS_IN_FRAME)),
|
|
193
207
|
"render_primitives": int(Performance.get_monitor(Performance.RENDER_TOTAL_PRIMITIVES_IN_FRAME)),
|
|
208
|
+
"render_video_mem": int(Performance.get_monitor(Performance.RENDER_VIDEO_MEM_USED)),
|
|
209
|
+
"render_texture_mem": int(Performance.get_monitor(Performance.RENDER_TEXTURE_MEM_USED)),
|
|
210
|
+
"render_buffer_mem": int(Performance.get_monitor(Performance.RENDER_BUFFER_MEM_USED)),
|
|
194
211
|
"physics_2d_active_objects": int(Performance.get_monitor(Performance.PHYSICS_2D_ACTIVE_OBJECTS)),
|
|
195
212
|
"physics_2d_collision_pairs": int(Performance.get_monitor(Performance.PHYSICS_2D_COLLISION_PAIRS)),
|
|
196
213
|
"physics_2d_island_count": int(Performance.get_monitor(Performance.PHYSICS_2D_ISLAND_COUNT)),
|
|
214
|
+
"physics_3d_active_objects": int(Performance.get_monitor(Performance.PHYSICS_3D_ACTIVE_OBJECTS)),
|
|
215
|
+
"physics_3d_collision_pairs": int(Performance.get_monitor(Performance.PHYSICS_3D_COLLISION_PAIRS)),
|
|
216
|
+
"physics_3d_island_count": int(Performance.get_monitor(Performance.PHYSICS_3D_ISLAND_COUNT)),
|
|
217
|
+
"audio_output_latency": Performance.get_monitor(Performance.AUDIO_OUTPUT_LATENCY),
|
|
197
218
|
"object_count": int(Performance.get_monitor(Performance.OBJECT_COUNT)),
|
|
198
219
|
"object_resource_count": int(Performance.get_monitor(Performance.OBJECT_RESOURCE_COUNT)),
|
|
199
220
|
"object_node_count": int(Performance.get_monitor(Performance.OBJECT_NODE_COUNT)),
|
|
200
221
|
"object_orphan_node_count": int(Performance.get_monitor(Performance.OBJECT_ORPHAN_NODE_COUNT)),
|
|
201
222
|
"memory_static": int(Performance.get_monitor(Performance.MEMORY_STATIC)),
|
|
202
223
|
"memory_static_max": int(Performance.get_monitor(Performance.MEMORY_STATIC_MAX)),
|
|
224
|
+
"memory_msg_buffer_max": int(Performance.get_monitor(Performance.MEMORY_MESSAGE_BUFFER_MAX)),
|
|
225
|
+
"navigation_active_maps": int(Performance.get_monitor(Performance.NAVIGATION_ACTIVE_MAPS)),
|
|
226
|
+
"navigation_region_count": int(Performance.get_monitor(Performance.NAVIGATION_REGION_COUNT)),
|
|
227
|
+
"navigation_agent_count": int(Performance.get_monitor(Performance.NAVIGATION_AGENT_COUNT)),
|
|
228
|
+
"navigation_link_count": int(Performance.get_monitor(Performance.NAVIGATION_LINK_COUNT)),
|
|
229
|
+
"navigation_polygon_count": int(Performance.get_monitor(Performance.NAVIGATION_POLYGON_COUNT)),
|
|
230
|
+
"navigation_edge_count": int(Performance.get_monitor(Performance.NAVIGATION_EDGE_COUNT)),
|
|
231
|
+
"navigation_edge_merge_count": int(Performance.get_monitor(Performance.NAVIGATION_EDGE_MERGE_COUNT)),
|
|
232
|
+
"navigation_edge_connection_count": int(Performance.get_monitor(Performance.NAVIGATION_EDGE_CONNECTION_COUNT)),
|
|
233
|
+
"navigation_edge_free_count": int(Performance.get_monitor(Performance.NAVIGATION_EDGE_FREE_COUNT)),
|
|
234
|
+
"navigation_obstacle_count": int(Performance.get_monitor(Performance.NAVIGATION_OBSTACLE_COUNT)),
|
|
235
|
+
"pipeline_compilations_canvas": int(Performance.get_monitor(Performance.PIPELINE_COMPILATIONS_CANVAS)),
|
|
236
|
+
"pipeline_compilations_mesh": int(Performance.get_monitor(Performance.PIPELINE_COMPILATIONS_MESH)),
|
|
237
|
+
"pipeline_compilations_surface": int(Performance.get_monitor(Performance.PIPELINE_COMPILATIONS_SURFACE)),
|
|
238
|
+
"pipeline_compilations_draw": int(Performance.get_monitor(Performance.PIPELINE_COMPILATIONS_DRAW)),
|
|
239
|
+
"pipeline_compilations_specialization": int(Performance.get_monitor(Performance.PIPELINE_COMPILATIONS_SPECIALIZATION)),
|
|
203
240
|
}
|
|
241
|
+
|
|
242
|
+
var rid := get_viewport().get_viewport_rid()
|
|
243
|
+
metrics["viewport_render_cpu_ms"] = RenderingServer.viewport_get_measured_render_time_cpu(rid) + RenderingServer.viewport_get_measured_render_time_gpu(rid)
|
|
244
|
+
metrics["viewport_render_gpu_ms"] = RenderingServer.viewport_get_measured_render_time_gpu(rid)
|
|
245
|
+
|
|
204
246
|
EngineDebugger.send_message("godot_mcp:performance_metrics_result", [metrics])
|
|
205
247
|
|
|
206
248
|
|
|
249
|
+
func _handle_get_profiler_data() -> void:
|
|
250
|
+
var data := _profiler.get_buffer_data() if _profiler else {}
|
|
251
|
+
EngineDebugger.send_message("godot_mcp:game_response", ["get_profiler_data", data])
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
func _handle_get_active_processes() -> void:
|
|
255
|
+
var tree := get_tree()
|
|
256
|
+
var scene_root := tree.current_scene if tree else null
|
|
257
|
+
if not scene_root:
|
|
258
|
+
EngineDebugger.send_message("godot_mcp:game_response", ["get_active_processes", {"processes": []}])
|
|
259
|
+
return
|
|
260
|
+
|
|
261
|
+
var script_map: Dictionary = {}
|
|
262
|
+
_collect_processes(scene_root, scene_root, script_map)
|
|
263
|
+
|
|
264
|
+
var processes: Array = []
|
|
265
|
+
for script_path in script_map:
|
|
266
|
+
processes.append(script_map[script_path])
|
|
267
|
+
|
|
268
|
+
processes.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
|
269
|
+
return a.instance_count > b.instance_count
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
EngineDebugger.send_message("godot_mcp:game_response", ["get_active_processes", {"processes": processes}])
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
func _collect_processes(node: Node, scene_root: Node, script_map: Dictionary) -> void:
|
|
276
|
+
var is_proc := node.is_processing()
|
|
277
|
+
var is_phys := node.is_physics_processing()
|
|
278
|
+
|
|
279
|
+
if is_proc or is_phys:
|
|
280
|
+
var script_path := ""
|
|
281
|
+
var script := node.get_script()
|
|
282
|
+
if script and script is Script:
|
|
283
|
+
script_path = script.resource_path
|
|
284
|
+
if script_path.is_empty():
|
|
285
|
+
script_path = node.get_class()
|
|
286
|
+
|
|
287
|
+
if not script_map.has(script_path):
|
|
288
|
+
script_map[script_path] = {
|
|
289
|
+
"script_path": script_path,
|
|
290
|
+
"has_process": false,
|
|
291
|
+
"has_physics_process": false,
|
|
292
|
+
"instance_count": 0,
|
|
293
|
+
"example_paths": [],
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
var entry: Dictionary = script_map[script_path]
|
|
297
|
+
if is_proc:
|
|
298
|
+
entry.has_process = true
|
|
299
|
+
if is_phys:
|
|
300
|
+
entry.has_physics_process = true
|
|
301
|
+
entry.instance_count += 1
|
|
302
|
+
if entry.example_paths.size() < 3:
|
|
303
|
+
var path := "/root/" + scene_root.name
|
|
304
|
+
var relative := scene_root.get_path_to(node)
|
|
305
|
+
if relative != NodePath("."):
|
|
306
|
+
path += "/" + str(relative)
|
|
307
|
+
entry.example_paths.append(path)
|
|
308
|
+
|
|
309
|
+
for child in node.get_children():
|
|
310
|
+
_collect_processes(child, scene_root, script_map)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
func _handle_get_signal_connections(data: Array) -> void:
|
|
314
|
+
var node_path: String = data[0] if data.size() > 0 else ""
|
|
315
|
+
|
|
316
|
+
var tree := get_tree()
|
|
317
|
+
var scene_root := tree.current_scene if tree else null
|
|
318
|
+
if not scene_root:
|
|
319
|
+
EngineDebugger.send_message("godot_mcp:game_response", ["get_signal_connections", {"connections": []}])
|
|
320
|
+
return
|
|
321
|
+
|
|
322
|
+
var search_root: Node = scene_root
|
|
323
|
+
if not node_path.is_empty():
|
|
324
|
+
search_root = _get_node_from_path(node_path, scene_root)
|
|
325
|
+
if not search_root:
|
|
326
|
+
EngineDebugger.send_message("godot_mcp:game_response", ["get_signal_connections", {"connections": [], "error": "Node not found: " + node_path}])
|
|
327
|
+
return
|
|
328
|
+
|
|
329
|
+
var connections: Array = []
|
|
330
|
+
_collect_signal_connections(search_root, scene_root, connections, 0)
|
|
331
|
+
|
|
332
|
+
EngineDebugger.send_message("godot_mcp:game_response", ["get_signal_connections", {"connections": connections}])
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
const MAX_SIGNAL_CONNECTIONS := 200
|
|
336
|
+
const MAX_SIGNAL_DEPTH := 20
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
func _collect_signal_connections(node: Node, scene_root: Node, connections: Array, depth: int) -> void:
|
|
340
|
+
if connections.size() >= MAX_SIGNAL_CONNECTIONS or depth > MAX_SIGNAL_DEPTH:
|
|
341
|
+
return
|
|
342
|
+
|
|
343
|
+
var source_path := _node_path_string(node, scene_root)
|
|
344
|
+
|
|
345
|
+
for sig_info in node.get_signal_list():
|
|
346
|
+
var sig_name: String = sig_info.name
|
|
347
|
+
for conn in node.get_signal_connection_list(sig_name):
|
|
348
|
+
if connections.size() >= MAX_SIGNAL_CONNECTIONS:
|
|
349
|
+
return
|
|
350
|
+
var target: Object = conn.callable.get_object()
|
|
351
|
+
var target_path := ""
|
|
352
|
+
if target is Node:
|
|
353
|
+
target_path = _node_path_string(target as Node, scene_root)
|
|
354
|
+
else:
|
|
355
|
+
target_path = str(target)
|
|
356
|
+
connections.append({
|
|
357
|
+
"source_path": source_path,
|
|
358
|
+
"signal_name": sig_name,
|
|
359
|
+
"target_path": target_path,
|
|
360
|
+
"method_name": conn.callable.get_method(),
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
for child in node.get_children():
|
|
364
|
+
if connections.size() >= MAX_SIGNAL_CONNECTIONS:
|
|
365
|
+
return
|
|
366
|
+
_collect_signal_connections(child, scene_root, connections, depth + 1)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
func _node_path_string(node: Node, scene_root: Node) -> String:
|
|
370
|
+
var path := "/root/" + scene_root.name
|
|
371
|
+
var relative := scene_root.get_path_to(node)
|
|
372
|
+
if relative != NodePath("."):
|
|
373
|
+
path += "/" + str(relative)
|
|
374
|
+
return path
|
|
375
|
+
|
|
376
|
+
|
|
207
377
|
class _MCPGameLogger extends Logger:
|
|
208
378
|
var _output: PackedStringArray = []
|
|
209
379
|
var _max_lines := 1000
|