@satelliteoflove/godot-mcp 3.10.0 → 3.10.1
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.
|
@@ -3,6 +3,11 @@ extends MCPBaseCommand
|
|
|
3
3
|
class_name MCPInputCommands
|
|
4
4
|
|
|
5
5
|
const INPUT_TIMEOUT := 30.0
|
|
6
|
+
# How long to wait for the game bridge to report it is ready to receive input
|
|
7
|
+
# before giving up. The natural workflow is run -> immediately drive the game;
|
|
8
|
+
# the session connects before the scene loads, so this short wait absorbs that
|
|
9
|
+
# gap (usually under a second) instead of dispatching input into a void (#241).
|
|
10
|
+
const READY_TIMEOUT := 10.0
|
|
6
11
|
|
|
7
12
|
var _input_map_result: Dictionary = {}
|
|
8
13
|
var _input_map_pending: bool = false
|
|
@@ -15,6 +20,9 @@ var _type_text_result: Dictionary = {}
|
|
|
15
20
|
var _type_text_pending: bool = false
|
|
16
21
|
|
|
17
22
|
|
|
23
|
+
const _BRIDGE_NOT_READY_MSG := "Game is running but its MCP bridge is not ready to receive input yet (no scene up, or the game just launched). This usually clears within a second of run — retry shortly."
|
|
24
|
+
|
|
25
|
+
|
|
18
26
|
func get_commands() -> Dictionary:
|
|
19
27
|
return {
|
|
20
28
|
"get_input_map": get_input_map,
|
|
@@ -23,6 +31,23 @@ func get_commands() -> Dictionary:
|
|
|
23
31
|
}
|
|
24
32
|
|
|
25
33
|
|
|
34
|
+
# Block until the running game's bridge reports it can consume input, bounded by
|
|
35
|
+
# READY_TIMEOUT. Returns true once ready, false if the game stops or never comes
|
|
36
|
+
# up in time. In the common case (game already running) this returns immediately
|
|
37
|
+
# without waiting a frame. Gating input on this is the fix for #241: the debug
|
|
38
|
+
# session connects before the main scene loads, so input dispatched on
|
|
39
|
+
# has_active_session() alone lands in a game with nothing to receive it.
|
|
40
|
+
func _await_bridge_ready(debugger_plugin) -> bool:
|
|
41
|
+
var start_time := Time.get_ticks_msec()
|
|
42
|
+
while not debugger_plugin.is_bridge_ready():
|
|
43
|
+
if not EditorInterface.is_playing_scene():
|
|
44
|
+
return false # game stopped or crashed while we waited
|
|
45
|
+
await Engine.get_main_loop().process_frame
|
|
46
|
+
if (Time.get_ticks_msec() - start_time) / 1000.0 > READY_TIMEOUT:
|
|
47
|
+
return false
|
|
48
|
+
return true
|
|
49
|
+
|
|
50
|
+
|
|
26
51
|
func get_input_map(_params: Dictionary) -> Dictionary:
|
|
27
52
|
if not EditorInterface.is_playing_scene():
|
|
28
53
|
return _get_editor_input_map()
|
|
@@ -113,8 +138,10 @@ func execute_input_sequence(params: Dictionary) -> Dictionary:
|
|
|
113
138
|
return _error("NOT_RUNNING", "No game is currently running")
|
|
114
139
|
|
|
115
140
|
var debugger_plugin = _plugin.get_debugger_plugin() if _plugin else null
|
|
116
|
-
if debugger_plugin == null
|
|
141
|
+
if debugger_plugin == null:
|
|
117
142
|
return _error("NO_SESSION", "No active debug session")
|
|
143
|
+
if not await _await_bridge_ready(debugger_plugin):
|
|
144
|
+
return _error("BRIDGE_NOT_READY", _BRIDGE_NOT_READY_MSG)
|
|
118
145
|
|
|
119
146
|
var max_end_time: float = 0.0
|
|
120
147
|
for input in inputs:
|
|
@@ -162,8 +189,10 @@ func type_text(params: Dictionary) -> Dictionary:
|
|
|
162
189
|
return _error("NOT_RUNNING", "No game is currently running")
|
|
163
190
|
|
|
164
191
|
var debugger_plugin = _plugin.get_debugger_plugin() if _plugin else null
|
|
165
|
-
if debugger_plugin == null
|
|
192
|
+
if debugger_plugin == null:
|
|
166
193
|
return _error("NO_SESSION", "No active debug session")
|
|
194
|
+
if not await _await_bridge_ready(debugger_plugin):
|
|
195
|
+
return _error("BRIDGE_NOT_READY", _BRIDGE_NOT_READY_MSG)
|
|
167
196
|
|
|
168
197
|
var timeout := max(INPUT_TIMEOUT, (text.length() * delay_ms / 1000.0) + 5.0)
|
|
169
198
|
|
|
@@ -10,8 +10,13 @@ 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
12
|
signal game_response(message_type: String, data: Variant)
|
|
13
|
+
signal bridge_ready()
|
|
13
14
|
|
|
14
15
|
var _active_session_id: int = -1
|
|
16
|
+
# True once the running game's bridge has announced it is ready to receive input
|
|
17
|
+
# (its main scene is up). The debug session connects before the scene loads, so
|
|
18
|
+
# has_active_session() alone is not enough to know input will land (#241).
|
|
19
|
+
var _bridge_ready: bool = false
|
|
15
20
|
var _pending_screenshot: bool = false
|
|
16
21
|
var _pending_debug_output: bool = false
|
|
17
22
|
var _pending_performance_metrics: bool = false
|
|
@@ -53,15 +58,30 @@ func _capture(message: String, data: Array, session_id: int) -> bool:
|
|
|
53
58
|
"godot_mcp:game_response":
|
|
54
59
|
_handle_game_response(data)
|
|
55
60
|
return true
|
|
61
|
+
"godot_mcp:bridge_ready":
|
|
62
|
+
_handle_bridge_ready(session_id)
|
|
63
|
+
return true
|
|
56
64
|
return false
|
|
57
65
|
|
|
58
66
|
|
|
67
|
+
func _handle_bridge_ready(session_id: int) -> void:
|
|
68
|
+
# Only the active session's bridge counts; ignore a late message from a prior
|
|
69
|
+
# run (a new _setup_session has already reset the flag for the current one).
|
|
70
|
+
if session_id != _active_session_id:
|
|
71
|
+
return
|
|
72
|
+
_bridge_ready = true
|
|
73
|
+
bridge_ready.emit()
|
|
74
|
+
|
|
75
|
+
|
|
59
76
|
func _setup_session(session_id: int) -> void:
|
|
60
77
|
_active_session_id = session_id
|
|
78
|
+
# New game session: its bridge has not announced readiness yet.
|
|
79
|
+
_bridge_ready = false
|
|
61
80
|
|
|
62
81
|
|
|
63
82
|
func _session_stopped() -> void:
|
|
64
83
|
_active_session_id = -1
|
|
84
|
+
_bridge_ready = false
|
|
65
85
|
if _pending_screenshot:
|
|
66
86
|
_pending_screenshot = false
|
|
67
87
|
screenshot_received.emit(false, "", 0, 0, "Game session ended")
|
|
@@ -97,6 +117,13 @@ func has_active_session() -> bool:
|
|
|
97
117
|
return true
|
|
98
118
|
|
|
99
119
|
|
|
120
|
+
# True only once the running game's bridge has reported its main scene is up and
|
|
121
|
+
# can consume input. Input commands gate on this (not just has_active_session) so
|
|
122
|
+
# a sequence injected right after run is not dispatched into a half-booted game.
|
|
123
|
+
func is_bridge_ready() -> bool:
|
|
124
|
+
return _bridge_ready and has_active_session()
|
|
125
|
+
|
|
126
|
+
|
|
100
127
|
func request_screenshot(max_width: int = 1024, quality: float = 0.75) -> void:
|
|
101
128
|
if _active_session_id < 0:
|
|
102
129
|
screenshot_received.emit(false, "", 0, 0, "No active game session")
|
|
@@ -5,10 +5,20 @@ const DEFAULT_MAX_WIDTH := 1024
|
|
|
5
5
|
const DEFAULT_JPEG_QUALITY := 0.75
|
|
6
6
|
const Onscreen := preload("onscreen.gd")
|
|
7
7
|
|
|
8
|
+
# Cap on frames waited for the main scene to appear before announcing ready
|
|
9
|
+
# anyway. The scene is normally added within a frame or two of the bridge
|
|
10
|
+
# autoload's _ready; the cap only matters for a scene-less run (a SceneTree-only
|
|
11
|
+
# tool), so it never blocks readiness forever. ~10s at 60 fps.
|
|
12
|
+
const READY_SCENE_WAIT_FRAMES := 600
|
|
13
|
+
|
|
8
14
|
var _logger: _MCPGameLogger
|
|
9
15
|
var _profiler: MCPFrameProfiler
|
|
10
16
|
var _sampler: MCPRuntimeStateSampler
|
|
11
17
|
|
|
18
|
+
# Set once the bridge has told the editor the game is ready to drive. Guards the
|
|
19
|
+
# announcement against firing twice and lets the headless test observe it.
|
|
20
|
+
var _ready_announced := false
|
|
21
|
+
|
|
12
22
|
|
|
13
23
|
func _ready() -> void:
|
|
14
24
|
# The bridge must keep processing while the scene tree is paused. Input
|
|
@@ -39,6 +49,10 @@ func _ready() -> void:
|
|
|
39
49
|
_engage_freeze()
|
|
40
50
|
MCPLog.info("Game bridge: launched frozen")
|
|
41
51
|
|
|
52
|
+
# Tell the editor when the game is actually drivable, so input injected right
|
|
53
|
+
# after `run` is not silently dropped into a half-booted game (see #241).
|
|
54
|
+
_announce_bridge_ready_when_drivable()
|
|
55
|
+
|
|
42
56
|
|
|
43
57
|
func _exit_tree() -> void:
|
|
44
58
|
# Guaranteed cleanup: never leave an action latched when the bridge node
|
|
@@ -50,6 +64,35 @@ func _exit_tree() -> void:
|
|
|
50
64
|
EngineDebugger.unregister_profiler("mcp_frame_profiler")
|
|
51
65
|
|
|
52
66
|
|
|
67
|
+
# The bridge autoload's _ready runs BEFORE the main scene is added to the tree,
|
|
68
|
+
# so the debug session is live (and the editor sees has_active_session) while
|
|
69
|
+
# current_scene is still null. Input injected in that window is dispatched into a
|
|
70
|
+
# game that has nothing to consume it — reported as executed, but a silent no-op
|
|
71
|
+
# (#241). Wait for the scene to exist plus one frame (so its own _ready/input
|
|
72
|
+
# wiring has run), then announce readiness; the editor gates input on this signal.
|
|
73
|
+
func _announce_bridge_ready_when_drivable() -> void:
|
|
74
|
+
var tree := get_tree()
|
|
75
|
+
if tree == null:
|
|
76
|
+
return
|
|
77
|
+
var frames := 0
|
|
78
|
+
while tree.current_scene == null and frames < READY_SCENE_WAIT_FRAMES:
|
|
79
|
+
await tree.process_frame
|
|
80
|
+
frames += 1
|
|
81
|
+
# One more frame so a freshly-added scene has had its first _ready/process pass.
|
|
82
|
+
# process_frame fires even while paused, so launch-frozen runs still report ready.
|
|
83
|
+
await tree.process_frame
|
|
84
|
+
var scene_path := tree.current_scene.scene_file_path if tree.current_scene else ""
|
|
85
|
+
_emit_bridge_ready(scene_path)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
func _emit_bridge_ready(scene_path: String) -> void:
|
|
89
|
+
if _ready_announced:
|
|
90
|
+
return
|
|
91
|
+
_ready_announced = true
|
|
92
|
+
EngineDebugger.send_message("godot_mcp:bridge_ready", [scene_path])
|
|
93
|
+
MCPLog.info("Game bridge: ready to drive (%s)" % scene_path)
|
|
94
|
+
|
|
95
|
+
|
|
53
96
|
func _process(delta: float) -> void:
|
|
54
97
|
_game_time_process(delta)
|
|
55
98
|
_sequence_process()
|
package/addon/plugin.cfg
CHANGED