@satelliteoflove/godot-mcp 3.15.0 → 3.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/commands/input_commands.gd +4 -2
- package/addon/commands/runtime_state_commands.gd +2 -1
- package/addon/game_bridge/joy_names.gd +68 -0
- package/addon/game_bridge/joy_names.gd.uid +1 -0
- package/addon/game_bridge/mcp_game_bridge.gd +197 -71
- package/addon/game_bridge/mcp_runtime_state_sampler.gd +172 -5
- package/addon/plugin.cfg +1 -1
- package/dist/__tests__/tools/game-time.test.js +41 -0
- package/dist/__tests__/tools/game-time.test.js.map +1 -1
- package/dist/__tests__/tools/input.test.js +142 -2
- package/dist/__tests__/tools/input.test.js.map +1 -1
- package/dist/__tests__/tools/runtime-state.test.js +247 -5
- package/dist/__tests__/tools/runtime-state.test.js.map +1 -1
- package/dist/tools/game-time.d.ts +106 -4
- package/dist/tools/game-time.d.ts.map +1 -1
- package/dist/tools/game-time.js +13 -9
- package/dist/tools/game-time.js.map +1 -1
- package/dist/tools/input.d.ts +112 -4
- package/dist/tools/input.d.ts.map +1 -1
- package/dist/tools/input.js +132 -9
- package/dist/tools/input.js.map +1 -1
- package/dist/tools/runtime-state.d.ts +34 -3
- package/dist/tools/runtime-state.d.ts.map +1 -1
- package/dist/tools/runtime-state.js +129 -17
- package/dist/tools/runtime-state.js.map +1 -1
- package/package.json +1 -1
|
@@ -137,10 +137,12 @@ func _event_to_string(event: InputEvent) -> String:
|
|
|
137
137
|
return "Mouse Button %d" % mouse_event.button_index
|
|
138
138
|
elif event is InputEventJoypadButton:
|
|
139
139
|
var joy_event := event as InputEventJoypadButton
|
|
140
|
-
return "Joypad Button %d" % joy_event.button_index
|
|
140
|
+
return "Joypad Button %d (%s)" % [joy_event.button_index, MCPJoyNames.button_name(joy_event.button_index)]
|
|
141
141
|
elif event is InputEventJoypadMotion:
|
|
142
|
+
# The signed axis_value is the direction bit an agent needs to lift the
|
|
143
|
+
# binding straight into an injection (e.g. move_left = left_x, value -1.0).
|
|
142
144
|
var joy_motion := event as InputEventJoypadMotion
|
|
143
|
-
return "Joypad Axis %d" % joy_motion.axis
|
|
145
|
+
return "Joypad Axis %d (%s, value %+.1f)" % [joy_motion.axis, MCPJoyNames.axis_name(joy_motion.axis), joy_motion.axis_value]
|
|
144
146
|
return event.as_text()
|
|
145
147
|
|
|
146
148
|
|
|
@@ -28,7 +28,8 @@ func watch_start(params: Dictionary) -> Dictionary:
|
|
|
28
28
|
var specs: Array = params.get("specs", [])
|
|
29
29
|
var hz: int = params.get("hz", 20)
|
|
30
30
|
var duration_ms: int = params.get("duration_ms", 1000)
|
|
31
|
-
var
|
|
31
|
+
var sigs: Array = params.get("signals", [])
|
|
32
|
+
var result = await _send_and_wait("watch_start", [specs, hz, duration_ms, sigs])
|
|
32
33
|
if result == null:
|
|
33
34
|
return _last_error
|
|
34
35
|
if result is Dictionary:
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
@tool
|
|
2
|
+
class_name MCPJoyNames
|
|
3
|
+
extends RefCounted
|
|
4
|
+
## Canonical joypad name<->index tables, shared by the game bridge (event
|
|
5
|
+
## injection) and the editor input commands (get_input_map display) so the
|
|
6
|
+
## wire vocabulary and the displayed names can never drift apart.
|
|
7
|
+
|
|
8
|
+
const BUTTONS := {
|
|
9
|
+
"a": JOY_BUTTON_A,
|
|
10
|
+
"b": JOY_BUTTON_B,
|
|
11
|
+
"x": JOY_BUTTON_X,
|
|
12
|
+
"y": JOY_BUTTON_Y,
|
|
13
|
+
"back": JOY_BUTTON_BACK,
|
|
14
|
+
"guide": JOY_BUTTON_GUIDE,
|
|
15
|
+
"start": JOY_BUTTON_START,
|
|
16
|
+
"left_stick": JOY_BUTTON_LEFT_STICK,
|
|
17
|
+
"right_stick": JOY_BUTTON_RIGHT_STICK,
|
|
18
|
+
"left_shoulder": JOY_BUTTON_LEFT_SHOULDER,
|
|
19
|
+
"right_shoulder": JOY_BUTTON_RIGHT_SHOULDER,
|
|
20
|
+
"dpad_up": JOY_BUTTON_DPAD_UP,
|
|
21
|
+
"dpad_down": JOY_BUTTON_DPAD_DOWN,
|
|
22
|
+
"dpad_left": JOY_BUTTON_DPAD_LEFT,
|
|
23
|
+
"dpad_right": JOY_BUTTON_DPAD_RIGHT,
|
|
24
|
+
"misc1": JOY_BUTTON_MISC1,
|
|
25
|
+
"paddle1": JOY_BUTTON_PADDLE1,
|
|
26
|
+
"paddle2": JOY_BUTTON_PADDLE2,
|
|
27
|
+
"paddle3": JOY_BUTTON_PADDLE3,
|
|
28
|
+
"paddle4": JOY_BUTTON_PADDLE4,
|
|
29
|
+
"touchpad": JOY_BUTTON_TOUCHPAD,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const AXES := {
|
|
33
|
+
"left_x": JOY_AXIS_LEFT_X,
|
|
34
|
+
"left_y": JOY_AXIS_LEFT_Y,
|
|
35
|
+
"right_x": JOY_AXIS_RIGHT_X,
|
|
36
|
+
"right_y": JOY_AXIS_RIGHT_Y,
|
|
37
|
+
"trigger_left": JOY_AXIS_TRIGGER_LEFT,
|
|
38
|
+
"trigger_right": JOY_AXIS_TRIGGER_RIGHT,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
## Resolve a wire value (name string, or raw index as an escape hatch) to a
|
|
43
|
+
## JoyButton index. Returns -1 for anything unknown.
|
|
44
|
+
static func button_index(value: Variant) -> int:
|
|
45
|
+
if value is int or value is float:
|
|
46
|
+
var i := int(value)
|
|
47
|
+
return i if i >= 0 and i < JOY_BUTTON_SDL_MAX else -1
|
|
48
|
+
return int(BUTTONS.get(str(value), -1))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
## Resolve a wire axis name to a JoyAxis index. Names only — the name itself
|
|
52
|
+
## is what tells an agent trigger (0..1) from stick (-1..1). Returns -1 if unknown.
|
|
53
|
+
static func axis_index(value: Variant) -> int:
|
|
54
|
+
return int(AXES.get(str(value), -1))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
static func button_name(idx: int) -> String:
|
|
58
|
+
for n in BUTTONS:
|
|
59
|
+
if int(BUTTONS[n]) == idx:
|
|
60
|
+
return n
|
|
61
|
+
return "button_%d" % idx
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
static func axis_name(idx: int) -> String:
|
|
65
|
+
for n in AXES:
|
|
66
|
+
if int(AXES[n]) == idx:
|
|
67
|
+
return n
|
|
68
|
+
return "axis_%d" % idx
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://row4hmo5gdem
|
|
@@ -137,16 +137,7 @@ func _sequence_process(delta: float) -> void:
|
|
|
137
137
|
|
|
138
138
|
while _sequence_events.size() > 0 and _sequence_events[0].time <= elapsed:
|
|
139
139
|
var seq_event: Dictionary = _sequence_events.pop_front()
|
|
140
|
-
|
|
141
|
-
input_event.action = seq_event.action
|
|
142
|
-
input_event.pressed = seq_event.is_press
|
|
143
|
-
input_event.strength = 1.0 if seq_event.is_press else 0.0
|
|
144
|
-
Input.parse_input_event(input_event)
|
|
145
|
-
if seq_event.is_press:
|
|
146
|
-
_held_actions[seq_event.action] = true
|
|
147
|
-
else:
|
|
148
|
-
_held_actions.erase(seq_event.action)
|
|
149
|
-
_actions_completed += 1
|
|
140
|
+
_actions_completed += _inject_timeline_event(seq_event)
|
|
150
141
|
|
|
151
142
|
# Trigger any frame captures whose offset has arrived (#239). Capture is
|
|
152
143
|
# deferred to frame_post_draw, so it completes a frame or two later; the
|
|
@@ -191,6 +182,7 @@ func _emit_sequence_result() -> void:
|
|
|
191
182
|
"frozen": _frozen,
|
|
192
183
|
"gameplay_ms": roundi(_sequence_gameplay_ms),
|
|
193
184
|
"wall_ms": Time.get_ticks_msec() - _sequence_start_time,
|
|
185
|
+
"input_kinds": _sequence_input_kinds,
|
|
194
186
|
}
|
|
195
187
|
|
|
196
188
|
if not _sequence_report.is_empty():
|
|
@@ -262,6 +254,11 @@ var _sequence_start_time: int = 0
|
|
|
262
254
|
var _sequence_running: bool = false
|
|
263
255
|
var _actions_completed: int = 0
|
|
264
256
|
var _actions_total: int = 0
|
|
257
|
+
# Entry counts by kind ({action, joy_button, axis}) for the current sequence /
|
|
258
|
+
# step window. Echoed in results: its PRESENCE is the version-skew signal a new
|
|
259
|
+
# server uses to detect an old bridge that silently dropped joypad entries.
|
|
260
|
+
var _sequence_input_kinds: Dictionary = {}
|
|
261
|
+
var _step_input_kinds: Dictionary = {}
|
|
265
262
|
# Game time (unpaused, scaled) accumulated across the sequence window — compared
|
|
266
263
|
# against wall time in the result to flag a sequence that ran under a pause/freeze.
|
|
267
264
|
var _sequence_gameplay_ms: float = 0.0
|
|
@@ -295,13 +292,19 @@ const SEQUENCE_MAX_CAPTURE_OFFSET_MS := 300000
|
|
|
295
292
|
# (new sequence) or the node leaves the tree — otherwise the dropped release
|
|
296
293
|
# latches the action "pressed" in the Input singleton (the stuck-held bug).
|
|
297
294
|
var _held_actions: Dictionary = {}
|
|
295
|
+
# Same guarantee for joypad state (#233): buttons whose press has fired
|
|
296
|
+
# (key "device:button") and axes whose last-set value is nonzero
|
|
297
|
+
# (key "device:axis") — a dropped end event would otherwise latch the polled
|
|
298
|
+
# Input singletons (is_joy_button_pressed / get_joy_axis) until game restart.
|
|
299
|
+
var _held_joy_buttons: Dictionary = {}
|
|
300
|
+
var _active_axes: Dictionary = {}
|
|
298
301
|
|
|
299
302
|
|
|
300
|
-
# Release any action still held
|
|
301
|
-
#
|
|
302
|
-
# when nothing is held.
|
|
303
|
+
# Release any action/button still held and re-zero any active axis from an
|
|
304
|
+
# interrupted sequence. A release here is a guaranteed cleanup, never a queued
|
|
305
|
+
# step that a clear could drop. Safe to call when nothing is held.
|
|
303
306
|
func _release_held_actions() -> void:
|
|
304
|
-
if _held_actions.is_empty():
|
|
307
|
+
if _held_actions.is_empty() and _held_joy_buttons.is_empty() and _active_axes.is_empty():
|
|
305
308
|
return
|
|
306
309
|
for action in _held_actions.keys():
|
|
307
310
|
var release := InputEventAction.new()
|
|
@@ -309,10 +312,26 @@ func _release_held_actions() -> void:
|
|
|
309
312
|
release.pressed = false
|
|
310
313
|
release.strength = 0.0
|
|
311
314
|
Input.parse_input_event(release)
|
|
315
|
+
for bkey in _held_joy_buttons.keys():
|
|
316
|
+
var binfo = _held_joy_buttons[bkey]
|
|
317
|
+
var brel := InputEventJoypadButton.new()
|
|
318
|
+
brel.device = int(binfo["device"])
|
|
319
|
+
brel.button_index = int(binfo["button"]) as JoyButton
|
|
320
|
+
brel.pressed = false
|
|
321
|
+
Input.parse_input_event(brel)
|
|
322
|
+
for akey in _active_axes.keys():
|
|
323
|
+
var ainfo = _active_axes[akey]
|
|
324
|
+
var azero := InputEventJoypadMotion.new()
|
|
325
|
+
azero.device = int(ainfo["device"])
|
|
326
|
+
azero.axis = int(ainfo["axis"]) as JoyAxis
|
|
327
|
+
azero.axis_value = 0.0
|
|
328
|
+
Input.parse_input_event(azero)
|
|
312
329
|
# Flush so the release takes effect immediately — _exit_tree may not get
|
|
313
330
|
# another frame, and a cleanup should be deterministic, not deferred.
|
|
314
331
|
Input.flush_buffered_events()
|
|
315
332
|
_held_actions.clear()
|
|
333
|
+
_held_joy_buttons.clear()
|
|
334
|
+
_active_axes.clear()
|
|
316
335
|
|
|
317
336
|
|
|
318
337
|
func _on_debugger_message(message: String, data: Array) -> bool:
|
|
@@ -1063,23 +1082,26 @@ func _handle_watch_start(data: Array) -> void:
|
|
|
1063
1082
|
var specs: Array = data[0] if data.size() > 0 else []
|
|
1064
1083
|
var hz: int = data[1] if data.size() > 1 else 20
|
|
1065
1084
|
var duration_ms: int = data[2] if data.size() > 2 else 1000
|
|
1066
|
-
var
|
|
1085
|
+
var signal_specs: Array = data[3] if data.size() > 3 else []
|
|
1086
|
+
var start_result := _sampler.start(specs, hz, duration_ms, signal_specs)
|
|
1067
1087
|
EngineDebugger.send_message("godot_mcp:game_response", ["watch_start", {
|
|
1068
1088
|
"started": true,
|
|
1069
1089
|
"resolved_fields": start_result.get("resolved_fields", 0),
|
|
1090
|
+
"connected_signals": start_result.get("connected_signals", 0),
|
|
1091
|
+
"unresolved_signals": start_result.get("unresolved_signals", []),
|
|
1070
1092
|
}])
|
|
1071
1093
|
|
|
1072
1094
|
|
|
1073
1095
|
func _handle_watch_collect() -> void:
|
|
1074
1096
|
if _sampler == null:
|
|
1075
|
-
EngineDebugger.send_message("godot_mcp:game_response", ["watch_collect", {"window_ms": 0, "sample_count": 0, "fields": {}}])
|
|
1097
|
+
EngineDebugger.send_message("godot_mcp:game_response", ["watch_collect", {"window_ms": 0, "sample_count": 0, "fields": {}, "events": [], "events_truncated": false}])
|
|
1076
1098
|
return
|
|
1077
1099
|
EngineDebugger.send_message("godot_mcp:game_response", ["watch_collect", _sampler.collect()])
|
|
1078
1100
|
|
|
1079
1101
|
|
|
1080
1102
|
func _handle_watch_stop() -> void:
|
|
1081
1103
|
if _sampler == null:
|
|
1082
|
-
EngineDebugger.send_message("godot_mcp:game_response", ["watch_stop", {"window_ms": 0, "sample_count": 0, "fields": {}}])
|
|
1104
|
+
EngineDebugger.send_message("godot_mcp:game_response", ["watch_stop", {"window_ms": 0, "sample_count": 0, "fields": {}, "events": [], "events_truncated": false}])
|
|
1083
1105
|
return
|
|
1084
1106
|
EngineDebugger.send_message("godot_mcp:game_response", ["watch_stop", _sampler.stop()])
|
|
1085
1107
|
|
|
@@ -1166,10 +1188,12 @@ func _event_to_string(event: InputEvent) -> String:
|
|
|
1166
1188
|
return "Mouse Button %d" % mouse_event.button_index
|
|
1167
1189
|
elif event is InputEventJoypadButton:
|
|
1168
1190
|
var joy_event := event as InputEventJoypadButton
|
|
1169
|
-
return "Joypad Button %d" % joy_event.button_index
|
|
1191
|
+
return "Joypad Button %d (%s)" % [joy_event.button_index, MCPJoyNames.button_name(joy_event.button_index)]
|
|
1170
1192
|
elif event is InputEventJoypadMotion:
|
|
1193
|
+
# The signed axis_value is the direction bit an agent needs to lift the
|
|
1194
|
+
# binding straight into an injection (e.g. move_left = left_x, value -1.0).
|
|
1171
1195
|
var joy_motion := event as InputEventJoypadMotion
|
|
1172
|
-
return "Joypad Axis %d" % joy_motion.axis
|
|
1196
|
+
return "Joypad Axis %d (%s, value %+.1f)" % [joy_motion.axis, MCPJoyNames.axis_name(joy_motion.axis), joy_motion.axis_value]
|
|
1173
1197
|
return event.as_text()
|
|
1174
1198
|
|
|
1175
1199
|
|
|
@@ -1179,6 +1203,11 @@ func _handle_execute_input_sequence(data: Array) -> void:
|
|
|
1179
1203
|
var screenshot_offsets: Array = data[2] if data.size() > 2 and data[2] is Array else []
|
|
1180
1204
|
var cap_max_width: int = int(data[3]) if data.size() > 3 else 640
|
|
1181
1205
|
|
|
1206
|
+
# Reset the skew echo up front: the result's input_kinds must reflect THIS
|
|
1207
|
+
# call's compile, never a stale value from a prior sequence (its absence is
|
|
1208
|
+
# how a new server detects an old bridge — a stale dict would mask that).
|
|
1209
|
+
_sequence_input_kinds = {"action": 0, "joy_button": 0, "axis": 0}
|
|
1210
|
+
|
|
1182
1211
|
if inputs.is_empty():
|
|
1183
1212
|
EngineDebugger.send_message("godot_mcp:input_sequence_result", [{
|
|
1184
1213
|
"error": "No inputs provided",
|
|
@@ -1230,34 +1259,14 @@ func _handle_execute_input_sequence(data: Array) -> void:
|
|
|
1230
1259
|
_sequence_captures_pending = 0
|
|
1231
1260
|
_sequence_capture_max_width = cap_max_width
|
|
1232
1261
|
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
if not InputMap.has_action(action_name):
|
|
1242
|
-
EngineDebugger.send_message("godot_mcp:input_sequence_result", [{
|
|
1243
|
-
"error": "Unknown action: %s" % action_name,
|
|
1244
|
-
}])
|
|
1245
|
-
return
|
|
1246
|
-
|
|
1247
|
-
_sequence_events.append({
|
|
1248
|
-
"time": start_ms,
|
|
1249
|
-
"action": action_name,
|
|
1250
|
-
"is_press": true,
|
|
1251
|
-
})
|
|
1252
|
-
_sequence_events.append({
|
|
1253
|
-
"time": start_ms + duration_ms,
|
|
1254
|
-
"action": action_name,
|
|
1255
|
-
"is_press": false,
|
|
1256
|
-
})
|
|
1257
|
-
|
|
1258
|
-
_sequence_events.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
|
1259
|
-
return a.time < b.time
|
|
1260
|
-
)
|
|
1262
|
+
var compiled := _compile_input_events(inputs)
|
|
1263
|
+
if compiled.has("error"):
|
|
1264
|
+
EngineDebugger.send_message("godot_mcp:input_sequence_result", [{
|
|
1265
|
+
"error": compiled["error"],
|
|
1266
|
+
}])
|
|
1267
|
+
return
|
|
1268
|
+
_sequence_events = compiled["events"]
|
|
1269
|
+
_sequence_input_kinds = compiled["kinds"]
|
|
1261
1270
|
|
|
1262
1271
|
# Baseline the effect probe at the last possible moment before any input fires.
|
|
1263
1272
|
_sequence_report = report_compiled
|
|
@@ -1511,6 +1520,10 @@ func _handle_game_time_step(data: Array) -> void:
|
|
|
1511
1520
|
_send_game_time_response("game_time_step", {"error": "Step already in progress"})
|
|
1512
1521
|
return
|
|
1513
1522
|
|
|
1523
|
+
# Reset the skew echo up front (see _handle_execute_input_sequence): the
|
|
1524
|
+
# step result's input_kinds must reflect this call, never a prior step's.
|
|
1525
|
+
_step_input_kinds = {"action": 0, "joy_button": 0, "axis": 0}
|
|
1526
|
+
|
|
1514
1527
|
var duration_ms: int = int(params.get("duration_ms", 0))
|
|
1515
1528
|
var frames: int = int(params.get("frames", 0))
|
|
1516
1529
|
if duration_ms <= 0 and frames <= 0:
|
|
@@ -1523,10 +1536,11 @@ func _handle_game_time_step(data: Array) -> void:
|
|
|
1523
1536
|
# from window start). Inputs must ride inside the step: an event injected
|
|
1524
1537
|
# while frozen lands on a frame gameplay never processes, so its
|
|
1525
1538
|
# is_action_just_pressed edge would be silently missed.
|
|
1526
|
-
var compiled :=
|
|
1539
|
+
var compiled := _compile_input_events(params.get("inputs", []))
|
|
1527
1540
|
if compiled.has("error"):
|
|
1528
1541
|
_send_game_time_response("game_time_step", {"error": compiled["error"]})
|
|
1529
1542
|
return
|
|
1543
|
+
_step_input_kinds = compiled["kinds"]
|
|
1530
1544
|
|
|
1531
1545
|
# Step from a running game is allowed — it freezes first, so "advance
|
|
1532
1546
|
# 500ms then wait for me" is a single atomic call.
|
|
@@ -1561,24 +1575,136 @@ func _handle_game_time_step(data: Array) -> void:
|
|
|
1561
1575
|
_update_processing()
|
|
1562
1576
|
|
|
1563
1577
|
|
|
1564
|
-
func
|
|
1565
|
-
# Builds the
|
|
1566
|
-
#
|
|
1578
|
+
func _compile_input_events(inputs: Array) -> Dictionary:
|
|
1579
|
+
# Builds the typed input timeline shared by input sequences and game-time
|
|
1580
|
+
# steps (#233). Each entry is discriminated by which key it carries:
|
|
1581
|
+
# `axis` (analog joypad axis), `joy_button`, or `action_name` (with optional
|
|
1582
|
+
# fractional `strength`). Returns {"events": [...], "kinds": {...}} or
|
|
1583
|
+
# {"error": ...} on an unknown action/button/axis. Every event carries:
|
|
1584
|
+
# time - ms offset from sequence/window start
|
|
1585
|
+
# phase - 0 = release/zero-set, 1 = press/set (the equal-time tie-break)
|
|
1586
|
+
# complete - completion credit, counted when the event fires (1 on ends)
|
|
1567
1587
|
var events: Array = []
|
|
1588
|
+
var kinds := {"action": 0, "joy_button": 0, "axis": 0}
|
|
1568
1589
|
for input in inputs:
|
|
1569
|
-
var action_name: String = input.get("action_name", "")
|
|
1570
|
-
if action_name.is_empty():
|
|
1571
|
-
continue
|
|
1572
|
-
if not InputMap.has_action(action_name):
|
|
1573
|
-
return {"error": "Unknown action: %s" % action_name}
|
|
1574
1590
|
var start_ms: int = int(input.get("start_ms", 0))
|
|
1575
1591
|
var dur: int = int(input.get("duration_ms", 0))
|
|
1576
|
-
|
|
1577
|
-
|
|
1592
|
+
# An instant tap (duration 0 — the schema default) must still emit its end
|
|
1593
|
+
# event STRICTLY AFTER its start, or the equal-time (time, phase) sort below
|
|
1594
|
+
# orders the release/zero-set before the press/set and the input latches
|
|
1595
|
+
# (press never paired with a release). One ms is enough: the time sort then
|
|
1596
|
+
# fires start-before-end even when both land in the same process frame.
|
|
1597
|
+
var end_ms: int = start_ms + maxi(dur, 1)
|
|
1598
|
+
if input.has("axis"):
|
|
1599
|
+
var axis := MCPJoyNames.axis_index(input["axis"])
|
|
1600
|
+
if axis < 0:
|
|
1601
|
+
return {"error": "Unknown joypad axis: %s (valid: %s)" % [str(input["axis"]), ", ".join(MCPJoyNames.AXES.keys())]}
|
|
1602
|
+
var device: int = int(input.get("device", 0))
|
|
1603
|
+
var value: float = clampf(float(input.get("value", 0.0)), -1.0, 1.0)
|
|
1604
|
+
kinds["axis"] += 1
|
|
1605
|
+
events.append({"time": start_ms, "phase": 1, "complete": 0,
|
|
1606
|
+
"kind": "axis", "axis": axis, "device": device, "value": value})
|
|
1607
|
+
events.append({"time": end_ms, "phase": 0, "complete": 1,
|
|
1608
|
+
"kind": "axis", "axis": axis, "device": device, "value": 0.0})
|
|
1609
|
+
elif input.has("joy_button"):
|
|
1610
|
+
var button := MCPJoyNames.button_index(input["joy_button"])
|
|
1611
|
+
if button < 0:
|
|
1612
|
+
return {"error": "Unknown joypad button: %s (valid: %s, or a raw index)" % [str(input["joy_button"]), ", ".join(MCPJoyNames.BUTTONS.keys())]}
|
|
1613
|
+
var bdevice: int = int(input.get("device", 0))
|
|
1614
|
+
kinds["joy_button"] += 1
|
|
1615
|
+
events.append({"time": start_ms, "phase": 1, "complete": 0,
|
|
1616
|
+
"kind": "joy_button", "button": button, "device": bdevice, "is_press": true})
|
|
1617
|
+
events.append({"time": end_ms, "phase": 0, "complete": 1,
|
|
1618
|
+
"kind": "joy_button", "button": button, "device": bdevice, "is_press": false})
|
|
1619
|
+
else:
|
|
1620
|
+
var action_name: String = input.get("action_name", "")
|
|
1621
|
+
if action_name.is_empty():
|
|
1622
|
+
continue
|
|
1623
|
+
if not InputMap.has_action(action_name):
|
|
1624
|
+
return {"error": "Unknown action: %s" % action_name}
|
|
1625
|
+
var strength: float = clampf(float(input.get("strength", 1.0)), 0.0, 1.0)
|
|
1626
|
+
kinds["action"] += 1
|
|
1627
|
+
events.append({"time": start_ms, "phase": 1, "complete": 0,
|
|
1628
|
+
"kind": "action", "action": action_name, "strength": strength, "is_press": true})
|
|
1629
|
+
events.append({"time": end_ms, "phase": 0, "complete": 1,
|
|
1630
|
+
"kind": "action", "action": action_name, "strength": strength, "is_press": false})
|
|
1631
|
+
# Releases/zero-sets fire before presses/sets at equal time, so a same-time
|
|
1632
|
+
# axis zero can never clobber a follow-on set of the same axis.
|
|
1578
1633
|
events.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
|
1579
|
-
|
|
1634
|
+
if a.time != b.time:
|
|
1635
|
+
return a.time < b.time
|
|
1636
|
+
return a.phase < b.phase
|
|
1580
1637
|
)
|
|
1581
|
-
|
|
1638
|
+
_cancel_redundant_axis_zeroes(events)
|
|
1639
|
+
return {"events": events, "kinds": kinds}
|
|
1640
|
+
|
|
1641
|
+
|
|
1642
|
+
func _cancel_redundant_axis_zeroes(events: Array) -> void:
|
|
1643
|
+
# Abutting same-axis entries (sweep ramps) must not bounce through zero:
|
|
1644
|
+
# both the zero-set and the next set pop in the same frame's while-loop, so
|
|
1645
|
+
# sort order alone cannot hide the transient zero from InputMap edge
|
|
1646
|
+
# detection (is_action_just_released would fire mid-sweep). Drop a zero-set
|
|
1647
|
+
# that lands at the same time as a follow-on set of the same (device, axis),
|
|
1648
|
+
# moving its completion credit to the survivor so counts stay exact.
|
|
1649
|
+
# Actions and buttons are deliberately NOT cancelled: release+press at the
|
|
1650
|
+
# same ms is a legitimate double-tap edge.
|
|
1651
|
+
var i := 0
|
|
1652
|
+
while i < events.size():
|
|
1653
|
+
var ev: Dictionary = events[i]
|
|
1654
|
+
if ev.kind == "axis" and ev.phase == 0:
|
|
1655
|
+
var j := i + 1
|
|
1656
|
+
while j < events.size() and events[j].time == ev.time:
|
|
1657
|
+
var nx: Dictionary = events[j]
|
|
1658
|
+
if nx.kind == "axis" and nx.phase == 1 and nx.axis == ev.axis and nx.device == ev.device:
|
|
1659
|
+
nx.complete = int(nx.complete) + int(ev.complete)
|
|
1660
|
+
events.remove_at(i)
|
|
1661
|
+
i -= 1
|
|
1662
|
+
break
|
|
1663
|
+
j += 1
|
|
1664
|
+
i += 1
|
|
1665
|
+
|
|
1666
|
+
|
|
1667
|
+
func _inject_timeline_event(ev: Dictionary) -> int:
|
|
1668
|
+
# Build and parse the engine event for one timeline entry, maintaining the
|
|
1669
|
+
# held-state registries that _release_held_actions uses for guaranteed
|
|
1670
|
+
# cleanup. Returns the event's completion credit. Joypad events drive the
|
|
1671
|
+
# polled Input singletons too (get_joy_axis / is_joy_button_pressed): unlike
|
|
1672
|
+
# the mouse cursor, parse_input_event updates joypad state for any device id
|
|
1673
|
+
# with no physical pad required.
|
|
1674
|
+
match str(ev.kind):
|
|
1675
|
+
"action":
|
|
1676
|
+
var ae := InputEventAction.new()
|
|
1677
|
+
ae.action = ev.action
|
|
1678
|
+
ae.pressed = ev.is_press
|
|
1679
|
+
ae.strength = float(ev.strength) if ev.is_press else 0.0
|
|
1680
|
+
Input.parse_input_event(ae)
|
|
1681
|
+
if ev.is_press:
|
|
1682
|
+
_held_actions[ev.action] = true
|
|
1683
|
+
else:
|
|
1684
|
+
_held_actions.erase(ev.action)
|
|
1685
|
+
"joy_button":
|
|
1686
|
+
var be := InputEventJoypadButton.new()
|
|
1687
|
+
be.device = ev.device
|
|
1688
|
+
be.button_index = ev.button as JoyButton
|
|
1689
|
+
be.pressed = ev.is_press
|
|
1690
|
+
Input.parse_input_event(be)
|
|
1691
|
+
var bkey := "%d:%d" % [ev.device, ev.button]
|
|
1692
|
+
if ev.is_press:
|
|
1693
|
+
_held_joy_buttons[bkey] = {"device": ev.device, "button": ev.button}
|
|
1694
|
+
else:
|
|
1695
|
+
_held_joy_buttons.erase(bkey)
|
|
1696
|
+
"axis":
|
|
1697
|
+
var me := InputEventJoypadMotion.new()
|
|
1698
|
+
me.device = ev.device
|
|
1699
|
+
me.axis = ev.axis as JoyAxis
|
|
1700
|
+
me.axis_value = ev.value
|
|
1701
|
+
Input.parse_input_event(me)
|
|
1702
|
+
var akey := "%d:%d" % [ev.device, ev.axis]
|
|
1703
|
+
if absf(float(ev.value)) > 0.0001:
|
|
1704
|
+
_active_axes[akey] = {"device": ev.device, "axis": ev.axis}
|
|
1705
|
+
else:
|
|
1706
|
+
_active_axes.erase(akey)
|
|
1707
|
+
return int(ev.get("complete", 0))
|
|
1582
1708
|
|
|
1583
1709
|
|
|
1584
1710
|
func _build_predicate_context() -> Dictionary:
|
|
@@ -1657,6 +1783,9 @@ func _handle_game_time_step_until(data: Array) -> void:
|
|
|
1657
1783
|
_send_game_time_response("game_time_step_until", {"error": "Step already in progress"})
|
|
1658
1784
|
return
|
|
1659
1785
|
|
|
1786
|
+
# Reset the skew echo up front (see _handle_execute_input_sequence).
|
|
1787
|
+
_step_input_kinds = {"action": 0, "joy_button": 0, "axis": 0}
|
|
1788
|
+
|
|
1660
1789
|
var src: String = str(params.get("until", "")).strip_edges()
|
|
1661
1790
|
if src.is_empty():
|
|
1662
1791
|
_send_game_time_response("game_time_step_until", {"error": "step_until requires a non-empty `until` expression"})
|
|
@@ -1690,14 +1819,17 @@ func _handle_game_time_step_until(data: Array) -> void:
|
|
|
1690
1819
|
return
|
|
1691
1820
|
var report_compiled: Array = report_result["report"]
|
|
1692
1821
|
|
|
1693
|
-
var compiled :=
|
|
1822
|
+
var compiled := _compile_input_events(params.get("inputs", []))
|
|
1694
1823
|
if compiled.has("error"):
|
|
1695
1824
|
_send_game_time_response("game_time_step_until", {"error": compiled["error"]})
|
|
1696
1825
|
return
|
|
1826
|
+
_step_input_kinds = compiled["kinds"]
|
|
1697
1827
|
|
|
1698
1828
|
_engage_freeze()
|
|
1699
1829
|
|
|
1700
1830
|
# Predicate already holds: advance nothing, stay frozen, report it.
|
|
1831
|
+
# input_kinds still rides along — its absence is the version-skew signal a
|
|
1832
|
+
# new server reads, so every success shape must carry it.
|
|
1701
1833
|
if bool(first_value):
|
|
1702
1834
|
var sc_result: Dictionary = {
|
|
1703
1835
|
"completed": true,
|
|
@@ -1708,6 +1840,7 @@ func _handle_game_time_step_until(data: Array) -> void:
|
|
|
1708
1840
|
"physics_ticks": 0,
|
|
1709
1841
|
"game_paused": _game_paused,
|
|
1710
1842
|
"predicate_met": true,
|
|
1843
|
+
"input_kinds": _step_input_kinds,
|
|
1711
1844
|
}
|
|
1712
1845
|
if not report_compiled.is_empty():
|
|
1713
1846
|
sc_result["report"] = _evaluate_report(report_compiled, ctx_inputs)
|
|
@@ -1769,17 +1902,9 @@ func _step_process(delta: float) -> void:
|
|
|
1769
1902
|
|
|
1770
1903
|
while _step_events.size() > 0 and _step_events[0].time <= _step_elapsed_ms:
|
|
1771
1904
|
var ev: Dictionary = _step_events.pop_front()
|
|
1772
|
-
|
|
1773
|
-
input_event.action = ev.action
|
|
1774
|
-
input_event.pressed = ev.is_press
|
|
1775
|
-
input_event.strength = 1.0 if ev.is_press else 0.0
|
|
1776
|
-
Input.parse_input_event(input_event)
|
|
1905
|
+
_inject_timeline_event(ev)
|
|
1777
1906
|
_step_events_fired += 1
|
|
1778
1907
|
_step_needs_settle = true
|
|
1779
|
-
if ev.is_press:
|
|
1780
|
-
_held_actions[ev.action] = true
|
|
1781
|
-
else:
|
|
1782
|
-
_held_actions.erase(ev.action)
|
|
1783
1908
|
|
|
1784
1909
|
var done := false
|
|
1785
1910
|
if _step_target_frames > 0:
|
|
@@ -1819,7 +1944,7 @@ func _step_process(delta: float) -> void:
|
|
|
1819
1944
|
func _finish_step() -> void:
|
|
1820
1945
|
# Releases are guaranteed cleanup, never queued steps: no holds survive
|
|
1821
1946
|
# across the freeze boundary (cross-step holds are a deliberate non-goal).
|
|
1822
|
-
var forced := _held_actions.size()
|
|
1947
|
+
var forced := _held_actions.size() + _held_joy_buttons.size() + _active_axes.size()
|
|
1823
1948
|
_release_held_actions()
|
|
1824
1949
|
var dropped := _step_events.size()
|
|
1825
1950
|
_step_events.clear()
|
|
@@ -1839,6 +1964,7 @@ func _finish_step() -> void:
|
|
|
1839
1964
|
"frames": _step_frames,
|
|
1840
1965
|
"physics_ticks": _step_physics_ticks,
|
|
1841
1966
|
"game_paused": _game_paused,
|
|
1967
|
+
"input_kinds": _step_input_kinds,
|
|
1842
1968
|
}
|
|
1843
1969
|
if _step_events_fired > 0:
|
|
1844
1970
|
result["events_fired"] = _step_events_fired
|