breakpoint-mcp 1.87.0 → 1.89.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/README.md +1 -1
- package/addon/breakpoint_mcp/bridge_secret.gd +37 -4
- package/addon/breakpoint_mcp/error_remedies.gd +5 -1
- package/addon/breakpoint_mcp/operations.gd +1 -1
- package/addon/breakpoint_mcp/plugin.cfg +1 -1
- package/addon/breakpoint_mcp/plugin.gd +17 -2
- package/addon/breakpoint_mcp/runtime_bridge.gd +289 -7
- package/dist/annotations.js +5 -5
- package/dist/annotations.js.map +1 -1
- package/dist/capabilities.js +1 -1
- package/dist/cli/doctor.js +1 -1
- package/dist/cli/init.js +1 -1
- package/dist/cli/tools.js +2 -2
- package/dist/index.js +5 -5
- package/dist/paths.js +4 -1
- package/dist/paths.js.map +1 -1
- package/dist/recipes.js +3 -3
- package/dist/recipes.js.map +1 -1
- package/dist/schemas.js +27 -1
- package/dist/schemas.js.map +1 -1
- package/dist/spawn-guard.js +10 -0
- package/dist/spawn-guard.js.map +1 -1
- package/dist/tool-census.js +2 -2
- package/dist/tools/cli.js +3 -1
- package/dist/tools/cli.js.map +1 -1
- package/dist/tools/processes.js +5 -3
- package/dist/tools/processes.js.map +1 -1
- package/dist/tools/runtime.js +112 -5
- package/dist/tools/runtime.js.map +1 -1
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
The MCP **host** for [Breakpoint MCP](https://github.com/jlivingston-Cipher/godot-breakpoint-mcp) —
|
|
4
4
|
a [Model Context Protocol](https://modelcontextprotocol.io) server that exposes the
|
|
5
5
|
Godot game engine to AI coding assistants across four planes: headless CLI, the live
|
|
6
|
-
editor, Godot's own LSP + DAP, and a runtime bridge inside the running game. **
|
|
6
|
+
editor, Godot's own LSP + DAP, and a runtime bridge inside the running game. **295 tools
|
|
7
7
|
+ 6 MCP resources**, built against the stable `@modelcontextprotocol/sdk` 1.x API.
|
|
8
8
|
Developed and tested with **Claude**; because MCP is an open protocol, other clients can
|
|
9
9
|
connect too (currently untested — reports welcome).
|
|
@@ -21,6 +21,14 @@ const SECRET_PATH := "res://.godot/breakpoint_mcp.secret"
|
|
|
21
21
|
## The shortest BREAKPOINT_RUNTIME_SECRET an opted-in debug export accepts (see
|
|
22
22
|
## runtime_listen_policy). The minted secret is 64 hex characters.
|
|
23
23
|
const EXPORT_SECRET_MIN_LEN := 32
|
|
24
|
+
## 🆕 BP-0037 — the project setting that turns strict mode on (default false, opt-in through
|
|
25
|
+
## 1.x). Under it the plugin registers no autoload, the game attaches runtime_bridge.gd
|
|
26
|
+
## itself from its own debug-only code, and the bridge listens only with the switch.
|
|
27
|
+
const STRICT_SETTING := "breakpoint_mcp/runtime/strict"
|
|
28
|
+
## 🆕 BP-0037 — the developer switch: this environment variable set to 1/true, or this user
|
|
29
|
+
## argument after `--` on the game's command line.
|
|
30
|
+
const SWITCH_ENV := "BREAKPOINT_RUNTIME"
|
|
31
|
+
const SWITCH_ARG := "--breakpoint-runtime"
|
|
24
32
|
|
|
25
33
|
|
|
26
34
|
## Return the shared secret, minting + persisting a fresh one if none exists.
|
|
@@ -69,14 +77,26 @@ static func load_or_mint() -> String:
|
|
|
69
77
|
## Pure — no OS calls, no files — so the decision is unit-tested headless; the caller
|
|
70
78
|
## passes the four facts in. `warn` is true only when someone ASKED for an exported
|
|
71
79
|
## bridge and is being refused; a plain shipped game stays silent.
|
|
72
|
-
|
|
80
|
+
##
|
|
81
|
+
## 🆕 BP-0037 — TWO MORE FACTS, AND ONE TEST MOVED TO THE FRONT. `debug_build` is now read
|
|
82
|
+
## FIRST, ahead of every other test: a build where OS.is_debug_build() is false never
|
|
83
|
+
## listens, whatever else is true of it (the only such build that reaches here is a release
|
|
84
|
+
## export, which already never listened, so nothing changes for a project that does not
|
|
85
|
+
## opt in). `strict` is the project setting `breakpoint_mcp/runtime/strict` (STRICT_SETTING,
|
|
86
|
+
## default false); when it is true the bridge listens only with the developer switch —
|
|
87
|
+
## BREAKPOINT_RUNTIME=1 in the environment or `--breakpoint-runtime` after `--` — which
|
|
88
|
+
## Breakpoint's own game launchers set (runtime_switch_on). Strict is opt-in through 1.x.
|
|
89
|
+
static func runtime_listen_policy(editor_build: bool, debug_build: bool, exported_optin: String, env_secret: String, strict: bool = false, switch_on: bool = false) -> Dictionary:
|
|
90
|
+
var optin := exported_optin.strip_edges().to_lower()
|
|
91
|
+
var asked := optin == "1" or optin == "true" or (strict and switch_on)
|
|
92
|
+
if not debug_build:
|
|
93
|
+
return {"listen": false, "secret_source": "", "warn": asked, "reason": "not a debug build (OS.is_debug_build() is false): the runtime bridge never listens in a release build, whatever its environment says"}
|
|
94
|
+
if strict and not switch_on:
|
|
95
|
+
return {"listen": false, "secret_source": "", "warn": optin == "1" or optin == "true", "reason": "strict mode (%s=true): the runtime bridge listens only with BREAKPOINT_RUNTIME=1 or --breakpoint-runtime" % STRICT_SETTING}
|
|
73
96
|
if editor_build:
|
|
74
97
|
return {"listen": true, "secret_source": "project", "warn": false, "reason": "development run (editor build)"}
|
|
75
|
-
var optin := exported_optin.strip_edges().to_lower()
|
|
76
98
|
if not (optin == "1" or optin == "true"):
|
|
77
99
|
return {"listen": false, "secret_source": "", "warn": false, "reason": "exported build: the runtime bridge listens only in development runs"}
|
|
78
|
-
if not debug_build:
|
|
79
|
-
return {"listen": false, "secret_source": "", "warn": true, "reason": "BREAKPOINT_RUNTIME_EXPORTED is honoured only in a debug export; a release export never listens"}
|
|
80
100
|
if env_secret.strip_edges().length() < EXPORT_SECRET_MIN_LEN:
|
|
81
101
|
return {"listen": false, "secret_source": "", "warn": true, "reason": "BREAKPOINT_RUNTIME_EXPORTED=1 needs BREAKPOINT_RUNTIME_SECRET of at least %d characters; an exported build never mints its own" % EXPORT_SECRET_MIN_LEN}
|
|
82
102
|
# The validated secret travels IN the decision, so nothing downstream re-reads the
|
|
@@ -84,6 +104,19 @@ static func runtime_listen_policy(editor_build: bool, debug_build: bool, exporte
|
|
|
84
104
|
return {"listen": true, "secret_source": "env", "secret": env_secret.strip_edges(), "warn": false, "reason": "debug export, opted in with BREAKPOINT_RUNTIME_EXPORTED"}
|
|
85
105
|
|
|
86
106
|
|
|
107
|
+
## 🆕 BP-0037 — whether the developer switch is set. Pure, like the policy: the caller passes
|
|
108
|
+
## OS.get_environment(SWITCH_ENV) and OS.get_cmdline_user_args().
|
|
109
|
+
static func runtime_switch_on(env_value: String, user_args: PackedStringArray) -> bool:
|
|
110
|
+
var v := env_value.strip_edges().to_lower()
|
|
111
|
+
return v == "1" or v == "true" or user_args.has(SWITCH_ARG)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
## 🆕 BP-0037 — whether this project is in strict mode. Strict only on a real `true`: a
|
|
115
|
+
## missing setting, or any other value, is the 1.x default (not strict).
|
|
116
|
+
static func runtime_strict(setting_value: Variant) -> bool:
|
|
117
|
+
return typeof(setting_value) == TYPE_BOOL and bool(setting_value)
|
|
118
|
+
|
|
119
|
+
|
|
87
120
|
## A development run is an EDITOR build that is NOT an export TEMPLATE. Both are compiled-in
|
|
88
121
|
## feature tags, and the second condition is what makes the first trustworthy: custom
|
|
89
122
|
## features — an export preset's list, or `_custom_features` in an override.cfg, which the
|
|
@@ -93,8 +93,9 @@ const RUNTIME_REMEDIES := {
|
|
|
93
93
|
"emit_failed": "Check the argument count and types against the signal's declaration, then emit again.",
|
|
94
94
|
"bad_action": "Call `inputmap_list` for the actions the project declares before injecting one.",
|
|
95
95
|
"bad_kind": "Pass one of the kinds the message lists to `runtime_inject_input`.",
|
|
96
|
+
"bad_event": "Pass the fields the message names for that kind to `runtime_inject_input`; a pointer's x and y are pixels of the frame `runtime_screenshot` returns.",
|
|
96
97
|
"bad_regex": "Simplify the pattern and call again — it did not compile as a Godot RegEx.",
|
|
97
|
-
"bad_reference": "Pass a reference image the project can load — `
|
|
98
|
+
"bad_reference": "Pass a reference image the project can load — `runtime_screenshot_save` writes one you can diff against later.",
|
|
98
99
|
"not_animation_player": "Call `runtime_get_tree` and pass the path of an AnimationPlayer node.",
|
|
99
100
|
"no_animation": "Call `runtime_anim_get_state` for the animations the player actually holds.",
|
|
100
101
|
"bad_scene": "Pass a res:// .tscn path that loads in the running project; `scene_get_dependencies` reports what a scene needs.",
|
|
@@ -108,6 +109,9 @@ const RUNTIME_REMEDIES := {
|
|
|
108
109
|
"set_ignored": "Check the property exists on this node with `runtime_get_property`, and that its setter accepts the value — nothing changed.",
|
|
109
110
|
"bad_event_field": "Send the field tagged — `{\"__type__\": \"Vector2\", \"x\": .., \"y\": ..}` — or omit it and let the event land at its default position.",
|
|
110
111
|
"bad_params": "Fix the parameter named above and call again — the message carries the accepted shape.",
|
|
112
|
+
"no_state_registry": "Set Engine metadata \"breakpoint_state\" to a Dictionary of name -> Callable from the game's own debug code, then call again.",
|
|
113
|
+
"unknown_state": "Call `runtime_read_state` without a name for the names the game registered, then pass one of them.",
|
|
114
|
+
"state_reader_gone": "Set the reader again from an object that is still alive, then call again.",
|
|
111
115
|
}
|
|
112
116
|
|
|
113
117
|
|
|
@@ -8,7 +8,7 @@ extends RefCounted
|
|
|
8
8
|
|
|
9
9
|
const Codec := preload("res://addons/breakpoint_mcp/variant_json.gd")
|
|
10
10
|
const Remedies := preload("res://addons/breakpoint_mcp/error_remedies.gd")
|
|
11
|
-
const ADDON_VERSION := "1.
|
|
11
|
+
const ADDON_VERSION := "1.19.0"
|
|
12
12
|
|
|
13
13
|
var _plugin: EditorPlugin
|
|
14
14
|
|
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
name="Breakpoint MCP"
|
|
4
4
|
description="Exposes the live Godot editor and the running game to an MCP host so an AI assistant can drive them: scene/node/resource CRUD with full undo/redo, project settings, ClassDB, editor + in-game screenshots, and an in-game runtime bridge (live SceneTree, property get/set, method calls, signals, input injection, Performance monitors). Pairs with the breakpoint-mcp MCP host."
|
|
5
5
|
author="James Livingston"
|
|
6
|
-
version="1.
|
|
6
|
+
version="1.19.0"
|
|
7
7
|
script="plugin.gd"
|
|
@@ -14,6 +14,7 @@ extends EditorPlugin
|
|
|
14
14
|
const BridgeServer := preload("res://addons/breakpoint_mcp/bridge_server.gd")
|
|
15
15
|
const StatusDock := preload("res://addons/breakpoint_mcp/status_dock.gd")
|
|
16
16
|
const PauseLatch := preload("res://addons/breakpoint_mcp/pause_latch.gd")
|
|
17
|
+
const BridgeSecret := preload("res://addons/breakpoint_mcp/bridge_secret.gd")
|
|
17
18
|
const RUNTIME_AUTOLOAD := "BreakpointRuntimeBridge"
|
|
18
19
|
const RUNTIME_SCRIPT := "res://addons/breakpoint_mcp/runtime_bridge.gd"
|
|
19
20
|
|
|
@@ -36,7 +37,15 @@ func _enter_tree() -> void:
|
|
|
36
37
|
_dock = dock
|
|
37
38
|
# Inject the runtime bridge into every run of the project so the runtime_*
|
|
38
39
|
# tools work as soon as the game starts. Removed cleanly on disable.
|
|
39
|
-
|
|
40
|
+
# 🆕 BP-0037 — STRICT MODE (`breakpoint_mcp/runtime/strict=true`, opt-in through 1.x):
|
|
41
|
+
# the plugin neither adds nor removes the autoload. The game attaches the bridge itself,
|
|
42
|
+
# by path, from its own debug-only code (runtime_bridge.gd's header), so an export that
|
|
43
|
+
# leaves this folder out keeps no autoload entry pointing into it.
|
|
44
|
+
if _strict():
|
|
45
|
+
if ProjectSettings.has_setting("autoload/" + RUNTIME_AUTOLOAD):
|
|
46
|
+
push_warning("[breakpoint_mcp] strict mode is on but project.godot still registers the %s autoload; remove it (Project Settings > Globals > Autoload) and attach the bridge by path — strict mode never edits it" % RUNTIME_AUTOLOAD)
|
|
47
|
+
else:
|
|
48
|
+
add_autoload_singleton(RUNTIME_AUTOLOAD, RUNTIME_SCRIPT)
|
|
40
49
|
# D3: emit resources/updated triggers. Selection + edited scene are reflected
|
|
41
50
|
# in godot://editor-state; the edited tree in godot://scene-tree.
|
|
42
51
|
_selection = EditorInterface.get_selection()
|
|
@@ -60,7 +69,8 @@ func _exit_tree() -> void:
|
|
|
60
69
|
_selection = null
|
|
61
70
|
if scene_changed.is_connected(_on_scene_changed):
|
|
62
71
|
scene_changed.disconnect(_on_scene_changed)
|
|
63
|
-
|
|
72
|
+
if not _strict():
|
|
73
|
+
remove_autoload_singleton(RUNTIME_AUTOLOAD)
|
|
64
74
|
if _server:
|
|
65
75
|
_server.shutdown()
|
|
66
76
|
_server.queue_free()
|
|
@@ -70,6 +80,11 @@ func _exit_tree() -> void:
|
|
|
70
80
|
print("[breakpoint_mcp] plugin disabled")
|
|
71
81
|
|
|
72
82
|
|
|
83
|
+
## 🆕 BP-0037 — whether this project opted into strict mode (bridge_secret.gd).
|
|
84
|
+
func _strict() -> bool:
|
|
85
|
+
return BridgeSecret.runtime_strict(ProjectSettings.get_setting(BridgeSecret.STRICT_SETTING, false))
|
|
86
|
+
|
|
87
|
+
|
|
73
88
|
## D3: the editor selection changed — godot://editor-state reflects the selection.
|
|
74
89
|
func _on_selection_changed() -> void:
|
|
75
90
|
if _server:
|
|
@@ -15,6 +15,17 @@ extends Node
|
|
|
15
15
|
## An EXPORTED game carries this autoload too, and there it stays closed: no socket, no
|
|
16
16
|
## secret written, no logger hooked (325, BP-0023 — see bridge_secret.gd's
|
|
17
17
|
## runtime_listen_policy for the one opt-in, debug exports only).
|
|
18
|
+
##
|
|
19
|
+
## STRICT MODE (BP-0037, opt-in through 1.x): a project that sets
|
|
20
|
+
## `breakpoint_mcp/runtime/strict=true` gets no autoload from the plugin. The game attaches
|
|
21
|
+
## this script itself, from its own debug-only code, by path — so an export preset that
|
|
22
|
+
## leaves `addons/breakpoint_mcp/` out leaves no reference behind:
|
|
23
|
+
## const BP := "res://addons/breakpoint_mcp/runtime_bridge.gd"
|
|
24
|
+
## if OS.is_debug_build() and ResourceLoader.exists(BP):
|
|
25
|
+
## var bridge: Node = load(BP).new()
|
|
26
|
+
## bridge.name = "BreakpointRuntimeBridge"
|
|
27
|
+
## get_tree().root.add_child.call_deferred(bridge)
|
|
28
|
+
## and the bridge then listens only with BREAKPOINT_RUNTIME=1 or `--breakpoint-runtime`.
|
|
18
29
|
|
|
19
30
|
## The addon version this GAME is running, answered on `ping`.
|
|
20
31
|
##
|
|
@@ -25,7 +36,7 @@ extends Node
|
|
|
25
36
|
## and the process that is already running keeps the addon it loaded. Held in lockstep
|
|
26
37
|
## with `plugin.cfg` and with `operations.gd`'s copy by contract_check check 14, which
|
|
27
38
|
## exists because two of those literals disagreed for two releases.
|
|
28
|
-
const ADDON_VERSION := "1.
|
|
39
|
+
const ADDON_VERSION := "1.19.0"
|
|
29
40
|
const Codec := preload("res://addons/breakpoint_mcp/variant_json.gd")
|
|
30
41
|
const Remedies := preload("res://addons/breakpoint_mcp/error_remedies.gd")
|
|
31
42
|
const DEFAULT_PORT := 9081
|
|
@@ -76,6 +87,14 @@ const BridgeSecret := preload("res://addons/breakpoint_mcp/bridge_secret.gd")
|
|
|
76
87
|
const PauseLatch := preload("res://addons/breakpoint_mcp/pause_latch.gd")
|
|
77
88
|
# F4: methods dispatched on the async lane (they await frames); see _handle_line / _dispatch_async.
|
|
78
89
|
const ASYNC_METHODS := ["runtime.step_frames"]
|
|
90
|
+
# 🆕 BP-0041 (1.19.0): the input kinds runtime.inject_input answers, in the order the
|
|
91
|
+
# refusal lists them. `pointer` alone rides the async lane (a tap or drag spans frames).
|
|
92
|
+
const INPUT_KINDS := ["action", "key", "mouse_button", "mouse_motion", "joypad_button", "joypad_motion", "pointer"]
|
|
93
|
+
const POINTER_ACTIONS := ["press", "move", "release", "tap", "drag"]
|
|
94
|
+
# The pointer's `button` is a MouseButton index, as on mouse_button: 1 left, 2 right, 3 middle.
|
|
95
|
+
const POINTER_MASKS := {MOUSE_BUTTON_LEFT: MOUSE_BUTTON_MASK_LEFT, MOUSE_BUTTON_RIGHT: MOUSE_BUTTON_MASK_RIGHT, MOUSE_BUTTON_MIDDLE: MOUSE_BUTTON_MASK_MIDDLE}
|
|
96
|
+
const STRETCH_MODES := ["disabled", "canvas_items", "viewport"]
|
|
97
|
+
const STRETCH_ASPECTS := ["ignore", "keep", "keep_width", "keep_height", "expand"]
|
|
79
98
|
# F4: default salient fields captured by runtime.state_digest when no explicit list is given.
|
|
80
99
|
const _DIGEST_DEFAULT_FIELDS := ["position", "global_position", "rotation", "scale", "visible", "modulate"]
|
|
81
100
|
|
|
@@ -96,9 +115,14 @@ var _in_capture: bool = false
|
|
|
96
115
|
## 🔴 325 (BP-0023): the listen decision, taken first in _ready (bridge_secret.gd's
|
|
97
116
|
## runtime_listen_policy). Read by _setup_auth for where the secret comes from.
|
|
98
117
|
var _policy: Dictionary = {}
|
|
118
|
+
## 🆕 BP-0041: where the injected pointer last was and which buttons it holds, so a
|
|
119
|
+
## `press`, `move` and `release` sent as three calls carry the relative motion and the
|
|
120
|
+
## button_mask one real gesture would.
|
|
121
|
+
var _pointer_at := Vector2.ZERO
|
|
122
|
+
var _pointer_mask: int = 0
|
|
99
123
|
|
|
100
124
|
|
|
101
|
-
## The
|
|
125
|
+
## The facts the listen policy is decided on. A method and not inline so the
|
|
102
126
|
## headless smoke can stand an exported build up inside an editor binary — the only
|
|
103
127
|
## binary CI has — by overriding this one function and nothing else.
|
|
104
128
|
func _policy_facts() -> Dictionary:
|
|
@@ -107,6 +131,9 @@ func _policy_facts() -> Dictionary:
|
|
|
107
131
|
"debug_build": OS.is_debug_build(),
|
|
108
132
|
"exported_optin": OS.get_environment("BREAKPOINT_RUNTIME_EXPORTED"),
|
|
109
133
|
"env_secret": OS.get_environment("BREAKPOINT_RUNTIME_SECRET"),
|
|
134
|
+
# 🆕 BP-0037 — strict mode and its developer switch (bridge_secret.gd).
|
|
135
|
+
"strict": BridgeSecret.runtime_strict(ProjectSettings.get_setting(BridgeSecret.STRICT_SETTING, false)),
|
|
136
|
+
"switch": BridgeSecret.runtime_switch_on(OS.get_environment(BridgeSecret.SWITCH_ENV), OS.get_cmdline_user_args()),
|
|
110
137
|
}
|
|
111
138
|
|
|
112
139
|
|
|
@@ -117,7 +144,7 @@ func _ready() -> void:
|
|
|
117
144
|
# that calls it — and a shipped game prints nothing unless someone asked for the
|
|
118
145
|
# bridge and is being refused.
|
|
119
146
|
var f := _policy_facts()
|
|
120
|
-
_policy = BridgeSecret.runtime_listen_policy(bool(f["editor_build"]), bool(f["debug_build"]), String(f["exported_optin"]), String(f["env_secret"]))
|
|
147
|
+
_policy = BridgeSecret.runtime_listen_policy(bool(f["editor_build"]), bool(f["debug_build"]), String(f["exported_optin"]), String(f["env_secret"]), bool(f.get("strict", false)), bool(f.get("switch", false)))
|
|
121
148
|
if not bool(_policy["listen"]):
|
|
122
149
|
set_process(false)
|
|
123
150
|
if bool(_policy["warn"]):
|
|
@@ -294,7 +321,7 @@ func _handle_line(c: Dictionary, line: String) -> void:
|
|
|
294
321
|
# F4: async lane — methods that await frames can't use the synchronous dispatch (which sends
|
|
295
322
|
# its response immediately). Start a coroutine that sends the id'd response when done; the host
|
|
296
323
|
# correlates responses by id, so an out-of-order async response is fine.
|
|
297
|
-
if method in ASYNC_METHODS:
|
|
324
|
+
if method in ASYNC_METHODS or _is_pointer(method, params):
|
|
298
325
|
_dispatch_async(c, id, method, params)
|
|
299
326
|
return
|
|
300
327
|
# D1a: the engine-error echo. The ring already receives every push_error /
|
|
@@ -471,6 +498,8 @@ func _dispatch(method: String, params: Dictionary) -> Dictionary:
|
|
|
471
498
|
return _state_digest(params)
|
|
472
499
|
"runtime.seed_rng":
|
|
473
500
|
return _seed_rng(params)
|
|
501
|
+
"runtime.read_state":
|
|
502
|
+
return _read_state(params)
|
|
474
503
|
"runtime.step_frames":
|
|
475
504
|
# Dispatched on the async lane (see _handle_line / _dispatch_async); this placeholder
|
|
476
505
|
# keeps the static contract check (which scans _dispatch case labels) aware of the
|
|
@@ -714,32 +743,242 @@ func _inject_input(params: Dictionary) -> Dictionary:
|
|
|
714
743
|
Input.action_release(action)
|
|
715
744
|
return _ok({"injected": true, "kind": kind})
|
|
716
745
|
"key":
|
|
746
|
+
# 🆕 BP-0041: keys bound in the editor's Input Map are stored by PHYSICAL key (the
|
|
747
|
+
# dialog's default), and an event carrying only `keycode` never matches one — so
|
|
748
|
+
# today's key kind reached `_input` but not the actions. A key with none of the
|
|
749
|
+
# three codes is KEY_NONE, which matches nothing, so it is refused, not injected.
|
|
750
|
+
if not (ev.has("keycode") or ev.has("physical_keycode") or ev.has("key_label")):
|
|
751
|
+
return _err("bad_event", "A key event needs at least one of event.keycode, event.physical_keycode or event.key_label; with none it is KEY_NONE and matches no key and no action.")
|
|
717
752
|
var k := InputEventKey.new()
|
|
718
753
|
k.keycode = int(ev.get("keycode", 0))
|
|
754
|
+
k.physical_keycode = int(ev.get("physical_keycode", 0))
|
|
755
|
+
k.key_label = int(ev.get("key_label", 0))
|
|
756
|
+
k.unicode = int(ev.get("unicode", 0))
|
|
757
|
+
k.echo = bool(ev.get("echo", false))
|
|
719
758
|
k.pressed = bool(ev.get("pressed", true))
|
|
759
|
+
_apply_modifiers(k, ev)
|
|
720
760
|
Input.parse_input_event(k)
|
|
721
761
|
return _ok({"injected": true, "kind": kind})
|
|
722
762
|
"mouse_button":
|
|
723
763
|
var mb := InputEventMouseButton.new()
|
|
724
764
|
mb.button_index = int(ev.get("button", 1))
|
|
725
765
|
mb.pressed = bool(ev.get("pressed", true))
|
|
726
|
-
|
|
766
|
+
mb.double_click = bool(ev.get("double_click", false))
|
|
767
|
+
var in_frame := String(ev.get("space", "window")) == "frame"
|
|
768
|
+
var pos_err := _apply_vector2(ev, "position", func(v: Vector2) -> void: mb.position = _frame_to_window(v) if in_frame else v)
|
|
727
769
|
if not pos_err.is_empty():
|
|
728
770
|
return pos_err
|
|
771
|
+
mb.global_position = mb.position
|
|
772
|
+
_apply_modifiers(mb, ev)
|
|
729
773
|
Input.parse_input_event(mb)
|
|
730
774
|
return _ok({"injected": true, "kind": kind})
|
|
731
775
|
"mouse_motion":
|
|
732
776
|
var mm := InputEventMouseMotion.new()
|
|
733
|
-
var
|
|
777
|
+
var m_in_frame := String(ev.get("space", "window")) == "frame"
|
|
778
|
+
var mpos_err := _apply_vector2(ev, "position", func(v: Vector2) -> void: mm.position = _frame_to_window(v) if m_in_frame else v)
|
|
734
779
|
if not mpos_err.is_empty():
|
|
735
780
|
return mpos_err
|
|
736
781
|
var rel_err := _apply_vector2(ev, "relative", func(v: Vector2) -> void: mm.relative = v)
|
|
737
782
|
if not rel_err.is_empty():
|
|
738
783
|
return rel_err
|
|
784
|
+
mm.global_position = mm.position
|
|
785
|
+
mm.button_mask = int(ev.get("button_mask", 0))
|
|
786
|
+
_apply_modifiers(mm, ev)
|
|
739
787
|
Input.parse_input_event(mm)
|
|
740
788
|
return _ok({"injected": true, "kind": kind})
|
|
789
|
+
"joypad_button":
|
|
790
|
+
if not ev.has("button"):
|
|
791
|
+
return _err("bad_event", "A joypad_button event needs event.button, Godot's JoyButton index (0 is the bottom face button).")
|
|
792
|
+
var jb := InputEventJoypadButton.new()
|
|
793
|
+
jb.device = int(ev.get("device", 0))
|
|
794
|
+
jb.button_index = int(ev.get("button"))
|
|
795
|
+
jb.pressed = bool(ev.get("pressed", true))
|
|
796
|
+
jb.pressure = float(ev.get("pressure", 1.0 if jb.pressed else 0.0))
|
|
797
|
+
Input.parse_input_event(jb)
|
|
798
|
+
return _ok({"injected": true, "kind": kind})
|
|
799
|
+
"joypad_motion":
|
|
800
|
+
if not ev.has("axis"):
|
|
801
|
+
return _err("bad_event", "A joypad_motion event needs event.axis, Godot's JoyAxis index (0 to 5), and event.value from -1 to 1.")
|
|
802
|
+
var jm := InputEventJoypadMotion.new()
|
|
803
|
+
jm.device = int(ev.get("device", 0))
|
|
804
|
+
jm.axis = int(ev.get("axis"))
|
|
805
|
+
jm.axis_value = clampf(float(ev.get("value", 0.0)), -1.0, 1.0)
|
|
806
|
+
Input.parse_input_event(jm)
|
|
807
|
+
return _ok({"injected": true, "kind": kind})
|
|
808
|
+
"pointer":
|
|
809
|
+
# Reached only from a caller that bypassed _handle_line's routing; the lane is async.
|
|
810
|
+
return _err("async_only", "runtime.inject_input with kind pointer is dispatched on the async lane")
|
|
741
811
|
_:
|
|
742
|
-
return _err("bad_kind", "Unknown input kind: %s" % kind)
|
|
812
|
+
return _err("bad_kind", "Unknown input kind: %s. The kinds are %s." % [kind, ", ".join(INPUT_KINDS)])
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
## 🆕 BP-0041 — the four modifier flags, on any event that carries them.
|
|
816
|
+
func _apply_modifiers(e: InputEventWithModifiers, ev: Dictionary) -> void:
|
|
817
|
+
e.shift_pressed = bool(ev.get("shift", false))
|
|
818
|
+
e.ctrl_pressed = bool(ev.get("ctrl", false))
|
|
819
|
+
e.alt_pressed = bool(ev.get("alt", false))
|
|
820
|
+
e.meta_pressed = bool(ev.get("meta", false))
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
## 🆕 BP-0041 — true when this call is a `pointer` gesture, which awaits frames and so
|
|
824
|
+
## rides the async lane like runtime.step_frames.
|
|
825
|
+
static func _is_pointer(method: String, params: Dictionary) -> bool:
|
|
826
|
+
if method != "runtime.inject_input":
|
|
827
|
+
return false
|
|
828
|
+
var ev: Variant = params.get("event")
|
|
829
|
+
return typeof(ev) == TYPE_DICTIONARY and String((ev as Dictionary).get("kind", "")) == "pointer"
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
## 🆕 BP-0041 — the geometry that relates `runtime.screenshot`'s frame to the window.
|
|
833
|
+
## `texture` is the frame's own pixel size (the image's, NOT `ViewportTexture.get_size()`:
|
|
834
|
+
## measured windowed on 4.7 under canvas_items, that answered 685x386 for an 888x500 frame); `visible_rect` is the viewport's coordinate
|
|
835
|
+
## space (what input events and Controls use); `window` is the OS window's client size;
|
|
836
|
+
## the stretch fields are the root window's content scale settings.
|
|
837
|
+
func _viewport_block(vp: Viewport, frame_size: Vector2) -> Dictionary:
|
|
838
|
+
var tex_size := frame_size
|
|
839
|
+
var vis := vp.get_visible_rect()
|
|
840
|
+
var win: Window = vp as Window if vp is Window else vp.get_window()
|
|
841
|
+
var block := {
|
|
842
|
+
"texture": [tex_size.x, tex_size.y],
|
|
843
|
+
"visible_rect": [vis.position.x, vis.position.y, vis.size.x, vis.size.y],
|
|
844
|
+
"window": [vis.size.x, vis.size.y],
|
|
845
|
+
"stretch_mode": "disabled",
|
|
846
|
+
"stretch_aspect": "ignore",
|
|
847
|
+
"content_scale_factor": 1.0,
|
|
848
|
+
}
|
|
849
|
+
if win != null:
|
|
850
|
+
block["window"] = [float(win.size.x), float(win.size.y)]
|
|
851
|
+
block["stretch_mode"] = STRETCH_MODES[clampi(int(win.content_scale_mode), 0, STRETCH_MODES.size() - 1)]
|
|
852
|
+
block["stretch_aspect"] = STRETCH_ASPECTS[clampi(int(win.content_scale_aspect), 0, STRETCH_ASPECTS.size() - 1)]
|
|
853
|
+
block["content_scale_factor"] = win.content_scale_factor
|
|
854
|
+
return block
|
|
855
|
+
|
|
856
|
+
|
|
857
|
+
## 🆕 BP-0041 — the transform that carries a pixel of the frame `runtime.screenshot`
|
|
858
|
+
## returns to a window pixel, in all three stretch modes.
|
|
859
|
+
##
|
|
860
|
+
## Godot's root window maps viewport coordinates to the window with
|
|
861
|
+
## `get_final_transform()` = window_transform * stretch_transform * global_canvas_transform
|
|
862
|
+
## (window.cpp, 4.3-4.7), and `Viewport._make_input_local` INVERTS exactly that for every
|
|
863
|
+
## event. The frame is the viewport's render target, whose pixels are viewport coordinates
|
|
864
|
+
## times the stretch scale — so frame -> window is `window_transform` alone: the letterbox
|
|
865
|
+
## offset under canvas_items, offset and upscale under viewport, identity when disabled.
|
|
866
|
+
## Measured windowed on the Mac (4.7, 1000x500 window, 1152x648 base, aspect keep): taps and
|
|
867
|
+
## drags at frame pixels landed on their targets under all three modes.
|
|
868
|
+
func _frame_transform() -> Transform2D:
|
|
869
|
+
var vp := get_viewport()
|
|
870
|
+
if vp == null:
|
|
871
|
+
return Transform2D.IDENTITY
|
|
872
|
+
if vp.has_method("get_stretch_transform"): # bound from Godot 4.4
|
|
873
|
+
var inner: Transform2D = vp.call("get_stretch_transform") * vp.global_canvas_transform
|
|
874
|
+
return vp.get_final_transform() * inner.affine_inverse()
|
|
875
|
+
# 4.2 / 4.3 bind no stretch accessor, so the frame's own size says the stretch scale.
|
|
876
|
+
var tex := vp.get_texture()
|
|
877
|
+
var img: Image = tex.get_image() if tex != null else null
|
|
878
|
+
if img == null or img.get_width() <= 0 or img.get_height() <= 0:
|
|
879
|
+
return Transform2D.IDENTITY
|
|
880
|
+
var vis := vp.get_visible_rect()
|
|
881
|
+
var to_local := Transform2D(0.0, vis.size / Vector2(img.get_width(), img.get_height()), 0.0, vis.position)
|
|
882
|
+
return vp.get_final_transform() * to_local
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
func _frame_to_window(f: Vector2) -> Vector2:
|
|
886
|
+
return _frame_transform() * f
|
|
887
|
+
|
|
888
|
+
|
|
889
|
+
## 🆕 BP-0041 — the pointer kind: press / move / release / tap / drag, at points given in
|
|
890
|
+
## the frame's pixels (`space` "frame", the default) or the window's (`space` "window").
|
|
891
|
+
## The reply carries the window points actually sent, so a miss is visible in the reply.
|
|
892
|
+
func _inject_pointer(ev_in: Variant) -> Dictionary:
|
|
893
|
+
if typeof(ev_in) != TYPE_DICTIONARY:
|
|
894
|
+
return _err("bad_event", "event must be an object.")
|
|
895
|
+
var ev: Dictionary = ev_in
|
|
896
|
+
var action := String(ev.get("action", ""))
|
|
897
|
+
if not action in POINTER_ACTIONS:
|
|
898
|
+
return _err("bad_event", "A pointer event needs event.action, one of %s; got %s." % [", ".join(POINTER_ACTIONS), JSON.stringify(action)])
|
|
899
|
+
if not (ev.has("x") and ev.has("y")):
|
|
900
|
+
return _err("bad_event", "A pointer event needs event.x and event.y, pixels of the frame runtime_screenshot returns.")
|
|
901
|
+
if action == "drag" and not (ev.has("to_x") and ev.has("to_y")):
|
|
902
|
+
return _err("bad_event", "A pointer drag needs event.to_x and event.to_y, where the button is released.")
|
|
903
|
+
var bname := int(ev.get("button", MOUSE_BUTTON_LEFT))
|
|
904
|
+
if not POINTER_MASKS.has(bname):
|
|
905
|
+
return _err("bad_event", "A pointer's event.button is a MouseButton index: 1 left, 2 right or 3 middle; got %s." % JSON.stringify(ev.get("button")))
|
|
906
|
+
var in_frame := String(ev.get("space", "frame")) != "window"
|
|
907
|
+
var warp := bool(ev.get("warp", false))
|
|
908
|
+
var per_step := clampi(int(ev.get("frames_per_step", 1)), 1, 10)
|
|
909
|
+
var xf := _frame_transform() if in_frame else Transform2D.IDENTITY
|
|
910
|
+
var start := xf * Vector2(float(ev.get("x")), float(ev.get("y")))
|
|
911
|
+
var points: Array = []
|
|
912
|
+
var frames := 0
|
|
913
|
+
match action:
|
|
914
|
+
"press":
|
|
915
|
+
_pointer_button(start, bname, true, warp)
|
|
916
|
+
points.append(start)
|
|
917
|
+
"release":
|
|
918
|
+
_pointer_button(start, bname, false, warp)
|
|
919
|
+
points.append(start)
|
|
920
|
+
"move":
|
|
921
|
+
_pointer_motion(start, warp)
|
|
922
|
+
points.append(start)
|
|
923
|
+
"tap":
|
|
924
|
+
_pointer_button(start, bname, true, warp)
|
|
925
|
+
points.append(start)
|
|
926
|
+
frames += await _pointer_wait(per_step)
|
|
927
|
+
_pointer_button(start, bname, false, warp)
|
|
928
|
+
points.append(start)
|
|
929
|
+
"drag":
|
|
930
|
+
var end := xf * Vector2(float(ev.get("to_x")), float(ev.get("to_y")))
|
|
931
|
+
var steps := clampi(int(ev.get("steps", 8)), 1, 60)
|
|
932
|
+
_pointer_button(start, bname, true, warp)
|
|
933
|
+
points.append(start)
|
|
934
|
+
for i in range(1, steps + 1):
|
|
935
|
+
frames += await _pointer_wait(per_step)
|
|
936
|
+
var p := start.lerp(end, float(i) / float(steps))
|
|
937
|
+
_pointer_motion(p, warp)
|
|
938
|
+
points.append(p)
|
|
939
|
+
frames += await _pointer_wait(per_step)
|
|
940
|
+
_pointer_button(end, bname, false, warp)
|
|
941
|
+
points.append(end)
|
|
942
|
+
var sent: Array = []
|
|
943
|
+
for p in points:
|
|
944
|
+
sent.append([(p as Vector2).x, (p as Vector2).y])
|
|
945
|
+
return _ok({"injected": true, "kind": "pointer", "window_points": sent, "frames": frames})
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
func _pointer_wait(n: int) -> int:
|
|
949
|
+
var tree := get_tree()
|
|
950
|
+
for _i in n:
|
|
951
|
+
await tree.process_frame
|
|
952
|
+
return n
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
func _pointer_button(at: Vector2, bname: int, pressed: bool, warp: bool) -> void:
|
|
956
|
+
if warp:
|
|
957
|
+
Input.warp_mouse(at)
|
|
958
|
+
if pressed:
|
|
959
|
+
_pointer_mask |= int(POINTER_MASKS[bname])
|
|
960
|
+
else:
|
|
961
|
+
_pointer_mask &= ~int(POINTER_MASKS[bname])
|
|
962
|
+
var mb := InputEventMouseButton.new()
|
|
963
|
+
mb.button_index = bname
|
|
964
|
+
mb.pressed = pressed
|
|
965
|
+
mb.position = at
|
|
966
|
+
mb.global_position = at
|
|
967
|
+
mb.button_mask = _pointer_mask
|
|
968
|
+
Input.parse_input_event(mb)
|
|
969
|
+
_pointer_at = at
|
|
970
|
+
|
|
971
|
+
|
|
972
|
+
func _pointer_motion(at: Vector2, warp: bool) -> void:
|
|
973
|
+
if warp:
|
|
974
|
+
Input.warp_mouse(at)
|
|
975
|
+
var mm := InputEventMouseMotion.new()
|
|
976
|
+
mm.position = at
|
|
977
|
+
mm.global_position = at
|
|
978
|
+
mm.relative = at - _pointer_at
|
|
979
|
+
mm.button_mask = _pointer_mask
|
|
980
|
+
Input.parse_input_event(mm)
|
|
981
|
+
_pointer_at = at
|
|
743
982
|
|
|
744
983
|
|
|
745
984
|
## Apply an OPTIONAL Vector2 field of a synthetic input event, or say why not.
|
|
@@ -825,6 +1064,7 @@ func _screenshot() -> Dictionary:
|
|
|
825
1064
|
"base64": Marshalls.raw_to_base64(buf),
|
|
826
1065
|
"width": img.get_width(),
|
|
827
1066
|
"height": img.get_height(),
|
|
1067
|
+
"viewport": _viewport_block(vp, Vector2(img.get_width(), img.get_height())),
|
|
828
1068
|
})
|
|
829
1069
|
|
|
830
1070
|
|
|
@@ -1172,6 +1412,46 @@ func _anim_get_state(params: Dictionary) -> Dictionary:
|
|
|
1172
1412
|
return _ok(_anim_state(ap))
|
|
1173
1413
|
|
|
1174
1414
|
|
|
1415
|
+
# ------------------------------------------- BP-0037 D: state the game exposes ----
|
|
1416
|
+
|
|
1417
|
+
## The Engine metadata key a game registers its state readers under. The game needs no
|
|
1418
|
+
## reference to this folder or any class in it — it writes a Dictionary of
|
|
1419
|
+
## name -> Callable to Engine metadata from its own debug-only code:
|
|
1420
|
+
## if OS.is_debug_build():
|
|
1421
|
+
## var reg: Dictionary = Engine.get_meta(&"breakpoint_state", {})
|
|
1422
|
+
## reg["mixer"] = _debug_mixer_state
|
|
1423
|
+
## Engine.set_meta(&"breakpoint_state", reg)
|
|
1424
|
+
## so it compiles and runs unchanged in an export that leaves addons/breakpoint_mcp/ out.
|
|
1425
|
+
const STATE_META := &"breakpoint_state"
|
|
1426
|
+
|
|
1427
|
+
|
|
1428
|
+
## runtime.read_state — read-only by construction: it reads the registry and calls only
|
|
1429
|
+
## what the game chose to register there. No `name` lists the registered names.
|
|
1430
|
+
func _read_state(params: Dictionary) -> Dictionary:
|
|
1431
|
+
if not Engine.has_meta(STATE_META) or typeof(Engine.get_meta(STATE_META)) != TYPE_DICTIONARY:
|
|
1432
|
+
return _err("no_state_registry", "The game registered no state readers: Engine metadata \"breakpoint_state\" is not a Dictionary of name -> Callable.")
|
|
1433
|
+
var reg: Dictionary = Engine.get_meta(STATE_META)
|
|
1434
|
+
var names: Array = []
|
|
1435
|
+
var by_name := {}
|
|
1436
|
+
for k in reg.keys():
|
|
1437
|
+
names.append(String(k))
|
|
1438
|
+
by_name[String(k)] = reg[k]
|
|
1439
|
+
names.sort()
|
|
1440
|
+
var state_name := String(params.get("name", ""))
|
|
1441
|
+
if state_name == "":
|
|
1442
|
+
return _ok({"names": names})
|
|
1443
|
+
if not by_name.has(state_name):
|
|
1444
|
+
return _err("unknown_state", "No state reader named \"%s\". Registered: %s" % [state_name, ", ".join(PackedStringArray(names)) if not names.is_empty() else "(none)"])
|
|
1445
|
+
var reader: Variant = by_name[state_name]
|
|
1446
|
+
if typeof(reader) != TYPE_CALLABLE or not (reader as Callable).is_valid():
|
|
1447
|
+
return _err("state_reader_gone", "The reader registered as \"%s\" is no longer callable — the object behind it was freed." % state_name)
|
|
1448
|
+
var args: Array = []
|
|
1449
|
+
for a in params.get("args", []):
|
|
1450
|
+
args.append(Codec.decode(a))
|
|
1451
|
+
var value: Variant = (reader as Callable).callv(args)
|
|
1452
|
+
return _ok({"names": names, "name": state_name, "value": Codec.encode(value)})
|
|
1453
|
+
|
|
1454
|
+
|
|
1175
1455
|
# ----------------------------------------------------- F8: node lifecycle ----
|
|
1176
1456
|
|
|
1177
1457
|
func _node_add(params: Dictionary) -> Dictionary:
|
|
@@ -1258,6 +1538,8 @@ func _dispatch_async(c: Dictionary, id: Variant, method: String, params: Diction
|
|
|
1258
1538
|
match method:
|
|
1259
1539
|
"runtime.step_frames":
|
|
1260
1540
|
result = await _step_frames(params)
|
|
1541
|
+
"runtime.inject_input":
|
|
1542
|
+
result = await _inject_pointer(params.get("event", {}))
|
|
1261
1543
|
_:
|
|
1262
1544
|
result = _err("async_only", "not an async method: %s" % method)
|
|
1263
1545
|
var peer: StreamPeerTCP = c.get("peer")
|
package/dist/annotations.js
CHANGED
|
@@ -14,7 +14,7 @@ const READ_ONLY = [
|
|
|
14
14
|
"resource_load", "runtime_anim_get_state", "runtime_assert_node_state", "runtime_assert_perf",
|
|
15
15
|
"runtime_assert_scene_structure", "runtime_assert_screen_text", "runtime_await_condition", "runtime_get_log",
|
|
16
16
|
"runtime_get_monitors", "runtime_get_property", "runtime_get_tree", "runtime_screenshot",
|
|
17
|
-
"runtime_peers_digest", "runtime_screenshot_diff", "runtime_state_digest", "scene_get_dependencies",
|
|
17
|
+
"runtime_peers_digest", "runtime_read_state", "runtime_screenshot_diff", "runtime_state_digest", "scene_get_dependencies",
|
|
18
18
|
"scene_get_tree", "scene_list_open", "screenshot_editor", "selection_get", "signal_list",
|
|
19
19
|
"signal_list_connections", "test_detect", "test_list", "tilemap_get_cell", "vcs_blame", "vcs_branch_list",
|
|
20
20
|
"vcs_diff", "vcs_log", "vcs_show", "vcs_status",
|
|
@@ -85,8 +85,8 @@ const IDEMPOTENT = [
|
|
|
85
85
|
"resource_set_import_settings", "resource_set_property", "rigidbody_set_properties", "runtime_anim_get_state",
|
|
86
86
|
"runtime_anim_stop", "runtime_assert_node_state", "runtime_assert_perf", "runtime_assert_scene_structure",
|
|
87
87
|
"runtime_assert_screen_text", "runtime_await_condition", "runtime_get_log", "runtime_get_monitors",
|
|
88
|
-
"runtime_get_property", "runtime_get_tree", "runtime_node_remove", "runtime_peer_stop", "runtime_peers_digest",
|
|
89
|
-
"runtime_screenshot", "runtime_screenshot_diff", "runtime_seed_rng", "runtime_set_property",
|
|
88
|
+
"runtime_get_property", "runtime_get_tree", "runtime_node_remove", "runtime_peer_stop", "runtime_peers_digest", "runtime_read_state",
|
|
89
|
+
"runtime_screenshot", "runtime_screenshot_diff", "runtime_screenshot_save", "runtime_seed_rng", "runtime_set_property",
|
|
90
90
|
"runtime_state_digest", "runtime_time_scale", "scene_get_dependencies", "scene_get_tree", "scene_list_open", "scene_new", "scene_open",
|
|
91
91
|
"scene_pack", "scene_reload", "scene_save", "scene_save_as", "screenshot_editor", "selection_get",
|
|
92
92
|
"selection_set", "shader_create", "shader_set_code", "shadermaterial_create", "shadermaterial_set_param",
|
|
@@ -156,8 +156,8 @@ export const ALL_ANNOTATED = [
|
|
|
156
156
|
"runtime_anim_play", "runtime_anim_stop", "runtime_assert_node_state", "runtime_assert_perf",
|
|
157
157
|
"runtime_assert_scene_structure", "runtime_assert_screen_text", "runtime_await_condition", "runtime_call_method",
|
|
158
158
|
"runtime_emit_signal", "runtime_get_log", "runtime_get_monitors", "runtime_get_property", "runtime_get_tree",
|
|
159
|
-
"runtime_inject_input", "runtime_node_add", "runtime_node_remove", "runtime_peer_stop", "runtime_peers_digest",
|
|
160
|
-
"runtime_screenshot", "runtime_screenshot_diff", "runtime_seed_rng", "runtime_set_property",
|
|
159
|
+
"runtime_inject_input", "runtime_node_add", "runtime_node_remove", "runtime_peer_stop", "runtime_peers_digest", "runtime_read_state",
|
|
160
|
+
"runtime_screenshot", "runtime_screenshot_diff", "runtime_screenshot_save", "runtime_seed_rng", "runtime_set_property",
|
|
161
161
|
"runtime_spawn_peers", "runtime_state_digest", "runtime_step_frames", "runtime_time_scale", "scene_close",
|
|
162
162
|
"scene_get_dependencies", "scene_get_tree",
|
|
163
163
|
"scene_list_open", "scene_new", "scene_open", "scene_pack", "scene_reload", "scene_save", "scene_save_as",
|