godot-cli-control 0.2.13__tar.gz → 0.2.15__tar.gz

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.
Files changed (81) hide show
  1. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/PKG-INFO +23 -4
  2. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/addons/godot_cli_control/CHANGELOG.md +20 -0
  3. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/addons/godot_cli_control/README.md +35 -10
  4. godot_cli_control-0.2.15/addons/godot_cli_control/bridge/capture_logger.gd +57 -0
  5. godot_cli_control-0.2.15/addons/godot_cli_control/bridge/diagnostics_api.gd +124 -0
  6. godot_cli_control-0.2.15/addons/godot_cli_control/bridge/error_codes.gd +50 -0
  7. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/addons/godot_cli_control/bridge/game_bridge.gd +52 -8
  8. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/addons/godot_cli_control/bridge/input_simulation_api.gd +17 -4
  9. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/addons/godot_cli_control/bridge/low_level_api.gd +100 -34
  10. godot_cli_control-0.2.15/addons/godot_cli_control/bridge/render_api.gd +197 -0
  11. godot_cli_control-0.2.15/addons/godot_cli_control/bridge/scene_api.gd +87 -0
  12. godot_cli_control-0.2.15/addons/godot_cli_control/bridge/time_api.gd +94 -0
  13. godot_cli_control-0.2.15/addons/godot_cli_control/bridge/variant_codec.gd +141 -0
  14. godot_cli_control-0.2.15/addons/godot_cli_control/bridge/wait_api.gd +401 -0
  15. godot_cli_control-0.2.15/addons/godot_cli_control/tests/gut/test_diagnostics_api.gd +133 -0
  16. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/addons/godot_cli_control/tests/gut/test_game_bridge.gd +73 -7
  17. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/addons/godot_cli_control/tests/gut/test_input_simulation_api.gd +55 -0
  18. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/addons/godot_cli_control/tests/gut/test_low_level_api.gd +192 -0
  19. godot_cli_control-0.2.15/addons/godot_cli_control/tests/gut/test_render_api.gd +203 -0
  20. godot_cli_control-0.2.15/addons/godot_cli_control/tests/gut/test_scene_api.gd +66 -0
  21. godot_cli_control-0.2.15/addons/godot_cli_control/tests/gut/test_time_api.gd +122 -0
  22. godot_cli_control-0.2.15/addons/godot_cli_control/tests/gut/test_variant_codec.gd +115 -0
  23. godot_cli_control-0.2.15/addons/godot_cli_control/tests/gut/test_wait_api.gd +438 -0
  24. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/pyproject.toml +46 -0
  25. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/python/README.md +3 -3
  26. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/python/godot_cli_control/_version.py +2 -2
  27. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/python/godot_cli_control/bridge.py +64 -3
  28. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/python/godot_cli_control/cli.py +612 -29
  29. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/python/godot_cli_control/client.py +134 -3
  30. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/python/godot_cli_control/daemon.py +8 -0
  31. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/python/godot_cli_control/init_cmd.py +10 -4
  32. godot_cli_control-0.2.15/python/godot_cli_control/pytest_plugin.py +224 -0
  33. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/python/godot_cli_control/templates/skill/SKILL.md +105 -11
  34. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/python/tests/test_bridge.py +233 -2
  35. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/python/tests/test_cli.py +931 -76
  36. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/python/tests/test_cli_helpers.py +248 -4
  37. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/python/tests/test_client.py +496 -0
  38. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/python/tests/test_daemon.py +82 -0
  39. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/python/tests/test_e2e_client_direct.py +87 -0
  40. godot_cli_control-0.2.15/python/tests/test_e2e_diag.py +176 -0
  41. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/python/tests/test_e2e_example.py +3 -2
  42. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/python/tests/test_e2e_input.py +68 -1
  43. godot_cli_control-0.2.15/python/tests/test_e2e_render.py +221 -0
  44. godot_cli_control-0.2.15/python/tests/test_e2e_scene.py +149 -0
  45. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/python/tests/test_e2e_screenshot_gui.py +49 -0
  46. godot_cli_control-0.2.15/python/tests/test_e2e_time.py +163 -0
  47. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/python/tests/test_init.py +50 -2
  48. godot_cli_control-0.2.15/python/tests/test_pytest_plugin.py +887 -0
  49. godot_cli_control-0.2.13/addons/godot_cli_control/bridge/error_codes.gd +0 -28
  50. godot_cli_control-0.2.13/python/godot_cli_control/pytest_plugin.py +0 -109
  51. godot_cli_control-0.2.13/python/tests/test_pytest_plugin.py +0 -384
  52. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/.gitignore +0 -0
  53. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/LICENSE +0 -0
  54. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/addons/godot_cli_control/LICENSE +0 -0
  55. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/addons/godot_cli_control/bin/run_cli_control.ps1 +0 -0
  56. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/addons/godot_cli_control/bin/run_cli_control.sh +0 -0
  57. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/addons/godot_cli_control/bridge/game_bridge.gd.uid +0 -0
  58. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/addons/godot_cli_control/bridge/input_simulation_api.gd.uid +0 -0
  59. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/addons/godot_cli_control/bridge/low_level_api.gd.uid +0 -0
  60. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/addons/godot_cli_control/plugin.cfg +0 -0
  61. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/addons/godot_cli_control/plugin.gd +0 -0
  62. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/addons/godot_cli_control/plugin.gd.uid +0 -0
  63. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/addons/godot_cli_control/tests/run_gut.py +0 -0
  64. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/addons/godot_cli_control/tests/run_gut.sh +0 -0
  65. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/addons/godot_cli_control/tests/test_init_walkthrough.py +0 -0
  66. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/addons/godot_cli_control/tests/test_walkthrough.sh +0 -0
  67. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/python/godot_cli_control/__init__.py +0 -0
  68. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/python/godot_cli_control/__main__.py +0 -0
  69. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/python/godot_cli_control/_duration.py +0 -0
  70. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/python/godot_cli_control/registry.py +0 -0
  71. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/python/godot_cli_control/runner.py +0 -0
  72. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/python/godot_cli_control/skills_install.py +0 -0
  73. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/python/godot_cli_control/templates/__init__.py +0 -0
  74. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/python/godot_cli_control/templates/skill/__init__.py +0 -0
  75. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/python/tests/__init__.py +0 -0
  76. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/python/tests/test_duration.py +0 -0
  77. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/python/tests/test_e2e_record.py +0 -0
  78. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/python/tests/test_issue_fixes.py +0 -0
  79. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/python/tests/test_registry.py +0 -0
  80. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/python/tests/test_runner.py +0 -0
  81. {godot_cli_control-0.2.13 → godot_cli_control-0.2.15}/python/tests/test_skills_install.py +0 -0
@@ -1,10 +1,29 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: godot-cli-control
3
- Version: 0.2.13
3
+ Version: 0.2.15
4
4
  Summary: WebSocket bridge for headless / scripted control of Godot scenes.
5
+ Project-URL: Homepage, https://github.com/ClaymanTwinkle/godot-cli-control
6
+ Project-URL: Repository, https://github.com/ClaymanTwinkle/godot-cli-control
7
+ Project-URL: Documentation, https://github.com/ClaymanTwinkle/godot-cli-control#readme
8
+ Project-URL: Changelog, https://github.com/ClaymanTwinkle/godot-cli-control/blob/main/addons/godot_cli_control/CHANGELOG.md
9
+ Project-URL: Issues, https://github.com/ClaymanTwinkle/godot-cli-control/issues
5
10
  Author: kesar
6
11
  License: MIT
7
12
  License-File: LICENSE
13
+ Keywords: ai-agent,automation,cli,game-testing,gdscript,godot,godot-plugin,godot4,headless,json-rpc,llm,pytest,screenshot,websocket
14
+ Classifier: Development Status :: 3 - Alpha
15
+ Classifier: Environment :: Console
16
+ Classifier: Framework :: Pytest
17
+ Classifier: Intended Audience :: Developers
18
+ Classifier: License :: OSI Approved :: MIT License
19
+ Classifier: Operating System :: OS Independent
20
+ Classifier: Programming Language :: Python :: 3
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Programming Language :: Python :: 3.13
25
+ Classifier: Topic :: Games/Entertainment
26
+ Classifier: Topic :: Software Development :: Testing
8
27
  Requires-Python: >=3.10
9
28
  Requires-Dist: websockets<16,>=14
10
29
  Provides-Extra: pytest
@@ -26,7 +45,7 @@ WebSocket bridge for headless / scripted control of Godot 4 scenes — Python cl
26
45
  pipx install godot-cli-control
27
46
 
28
47
  # or, for unreleased main:
29
- pipx install "git+https://github.com/ClaymanTwinkle/godot-cli-control.git#subdirectory=python"
48
+ pipx install "git+https://github.com/ClaymanTwinkle/godot-cli-control.git"
30
49
  ```
31
50
 
32
51
  The wheel ships the Godot plugin source so the `init` command can drop it into your project.
@@ -43,7 +62,7 @@ godot-cli-control tree 3
43
62
  godot-cli-control daemon stop
44
63
  ```
45
64
 
46
- `init` is idempotent running it twice on the same project does nothing the second time. Pass `--force` to overwrite an existing `addons/godot_cli_control/`.
65
+ Re-running `init` refreshes both `addons/godot_cli_control/` and the SKILL.md files to match the installed CLI version (the plugin directory is wiped and re-copied; `project.godot` patching stays idempotent). Pass `--keep-addon` to keep an existing `addons/godot_cli_control/` untouched.
47
66
 
48
67
  `init` also writes `.claude/skills/godot-cli-control/SKILL.md` and `.codex/skills/godot-cli-control/SKILL.md` so AI agents working in your Godot project can pick up this CLI surface automatically. Use `--no-skills` to skip, or `--skills-only` to refresh just those files after a CLI upgrade. See the [top-level README](../README.md#agent-integration) for details.
49
68
 
@@ -114,7 +133,7 @@ The CLI is the canonical surface — every `GameClient` method has a one-line eq
114
133
 
115
134
  ```bash
116
135
  # Lifecycle
117
- godot-cli-control init [--path DIR] [--force]
136
+ godot-cli-control init [--path DIR] [--keep-addon]
118
137
  godot-cli-control daemon start [--headless | --gui] [--port N --idle-timeout 30m]
119
138
  godot-cli-control daemon start --record --movie-path X [--fps N] # 录制需真实渲染器,不能与 --headless 同用
120
139
  godot-cli-control daemon stop [--all | --project PATH]
@@ -2,11 +2,31 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ### Added
6
+ - **#110 `wait_signal` 超时与节点释放可区分**:未命中时返回体新增 `reason` 字段,值为 `"timeout"`(信号在超时前未发射)或 `"node_freed"`(等待期间目标节点被释放)。命中路径(`emitted: true`)不含 `reason`,对齐 `wait_property` matched 时无 reason 的约定。非 BREAKING(新增可选字段)。
7
+
8
+ ### Fixed (BREAKING — exit code changes)
9
+
10
+ - **#111 argparse 用法错 exit 2 → exit 64**: 所有 argparse 层错误(缺位置参数、非法 choices、未知子命令等)现在统一输出 `-1003` 信封并以 exit 64 退出。之前是 exit 2(argparse 默认)。影响:脚本若用 `[ $? -eq 2 ]` 判 argparse 错需改为 `[ $? -eq 64 ]`。
11
+ - **#92 `run <script>` 前置错误拆分**:
12
+ - 脚本路径不存在 / 脚本缺 `run(bridge)` 函数:由旧的 exit 2(或 exit 1)改为 exit 64,错误码由 `-1003` 保持不变。这是用法错,应修正调用而非重试。
13
+ - `daemon start`(`run` 自动启停 / `daemon start` 命令)/ `daemon stop` 系统级失败(端口冲突、找不到 Godot binary、PID 文件丢失等):错误码由 `-1003` 改为 **新码 `-1006` (PRECONDITION)**,exit code 保持 exit 2。影响:`run`/`daemon` 失败时,通过 `error.code` 区分用法错(`-1003` → 修调用)与基础设施错(`-1006` → 修环境)现在变得无歧义。
14
+
15
+ ### Added
16
+ - **feat(wait): `wait-prop` / `wait-signal` / `wait-frames` 条件等待原语 + 错误码 1007(#96)**: 三个新 CLI 子命令。`wait-prop <path> <prop> <value> [--op eq|ne|gt|lt|ge|le] [--timeout] [--tolerance]` 等属性满足条件(exit 0=matched, 1=timeout);`wait-signal <path> <signal> [--timeout]` 等信号发射(exit 0=emitted, 1=timeout);`wait-frames <N> [--physics]` 推进 N 帧。服务端错误码 1007 SIGNAL_NOT_FOUND(wait-signal 传未知信号名,永久性 schema 错,不应 retry)。Python `GameClient` 对应方法:`wait_property` / `wait_signal` / `wait_frames`。
17
+
18
+ ### Changed
19
+ - **BREAKING** `get` 复合 Variant 返回从旧版 `"(x, y)"` 字符串改为 set-schema 数组 + type 字段;信封 result 从裸值变 `{"value": <array-or-scalar>, "type"?: "<GodotType>"}` 对象(#99)。写侧 `set` 接受的 Array layout 完全一致,get→set round-trip 无需转换。Python API `get_property` / `get_properties` 只返回裸 value(type 已剥离),要拿 type 字段走 CLI `get` 或底层 `client.request("get_property", ...)`。
20
+
21
+ ### Added
22
+ - **feat: `get_properties` 多属性同帧原子读 + `get` 多属性形式(#100)**:`get <path> <prop1> <prop2>` 一次 RPC 同帧读取多个属性,不存在部分新鲜/部分旧数据的竞态;任一属性缺失整体失败(1002,message 点名所有缺失项)。sub-path 形式(`position:x`)在多属性列表中也支持。
23
+
5
24
  ### Fixed
6
25
  - **#52 `set` 走 JSON Array 喂 Vector/Color/Rect 时静默失败**:`set zoom '[1.8, 1.8]'` 等价于 `node.set("zoom", [1.8, 1.8])`,Godot 隐式构造失败 → 实际值是 `Vector2(0,0)` 或被 clamp 到 `0.00001`,但服务端仍返 `{success: true}`。`handle_set_property` 现在查 `get_property_list()` 拿声明类型,把 numeric Array 转成对应 Variant。长度不匹配或元素非数字时 fail-loud 返 `-32602 "value type mismatch ..."`,不再 silent corruption。
7
26
  - **sub-path 标量赋值现在真的会写入**:之前 `set <node> position:x 1.8` 调的是 `Object.set("position:x", 1.8)`,Godot 4 的 `Object.set` 把整串当字面属性名找不到就 silent no-op(依旧返 `{success: true}` 但 `position.x` 不变)。改用 `Object.set_indexed(NodePath, value)` 才会按 sub-path 写入。是 #54 review 阶段被新加的 `test_set_subpath_scalar_still_works` 捕获的隐藏 silent-fail。
8
27
 
9
28
  ### Added
29
+ - **feat(input): press/tap/hold/combo 注入 InputEventAction 进事件管线,_input/_unhandled_input 可见(#97)**:之前的实现只调 `Input.action_press/release`,仅翻轮询状态位,事件回调收不到。现改为 `Input.parse_input_event(InputEventAction)`,轮询(`is_action_pressed`/`get_vector`)与事件回调(`_input`/`_unhandled_input`)两种游戏写法均可感知注入输入。边界:`InputEventAction` 不携带鼠标坐标,依赖位置的 `_gui_input` 控件仍需用 `click`。
10
30
  - **#54 全部复合 Variant 都支持 Array → Variant coerce**:从 4-float 简单类型到 16-float 矩阵,全部按 axis-vector 顺序的 flat numeric Array 写入(每 N 元素 = 一个 Vector 轴)。覆盖:`Vector2/2i/3/3i/4/4i`、`Rect2/2i`、`Color`(3-element=RGB / 4-element=RGBA)、`Plane(a,b,c,d)`、`Quaternion(x,y,z,w)`、`AABB(pos 3, size 3)`、`Basis(9 axis-vector)`、`Transform2D(xaxis 2, yaxis 2, origin 2)`、`Transform3D(basis 9, origin 3)`、`Projection(16 axis-vector)`。具体 layout 见 SKILL.md。`Plane.normal` 和 `Quaternion` 不会自动归一化(与 Godot ctor 一致)。
11
31
  - **#54 防御性 fallback**:未在 coerce 名单也不在 `_ARRAY_PASSTHROUGH_SAFE_TYPES`(基本类型 / Object / 集合 / Packed*Array)白名单的声明类型 + Array 输入会 fail-loud,避免未来 Godot 加新 compound Variant 时 silent-corrupt 回归。
12
32
  - **#54 sub-path + Array 现在 fail-loud**:`set <node> transform:origin '[10, 20, 30]'` 在 Godot 里会 silent-corrupt(Vector3 leaf 不会从 Array 隐式构造,origin 仍是 (0,0,0)),server 现在主动返 `-32602 "sub-path + Array is not supported"` 提示走 top-level 形式(如 `set <node> transform '[basis 9, origin 3]'`)。sub-path 标量赋值(`position:x 1.8`)不变。
@@ -72,7 +72,7 @@ GODOT_BIN=/path/to/godot ./addons/godot_cli_control/tests/run_gut.sh
72
72
  GODOT_BIN=/path/to/godot python addons/godot_cli_control/tests/run_gut.py
73
73
  ```
74
74
 
75
- The runner builds a throwaway Godot project, `git clone`s a pinned [GUT](https://github.com/bitwes/Gut) release into `addons/gut/`, copies this plugin in, and runs the test files under `addons/godot_cli_control/tests/gut/`. Coverage today is `LowLevelApi` handler boundaries (blacklist, missing-property, node-not-found) and `InputSimulationApi` state machine (combo / press / release / tap / release_all).
75
+ The runner builds a throwaway Godot project, `git clone`s a pinned [GUT](https://github.com/bitwes/Gut) release into `addons/gut/`, copies this plugin in, and runs the test files under `addons/godot_cli_control/tests/gut/`. Coverage today is `LowLevelApi` handler boundaries (blacklist, missing-property, node-not-found), `InputSimulationApi` state machine (combo / press / release / tap / release_all), and `WaitApi` wait primitives (frames, property polling, signal capture, setup-guard).
76
76
 
77
77
  GUT itself is **not** vendored into the repo or shipped with the wheel/AssetLib zip — it's a dev-only dependency.
78
78
 
@@ -83,7 +83,8 @@ All methods callable via `godot-cli-control <method>` or `from godot_cli_control
83
83
  | Method | Example |
84
84
  |---|---|
85
85
  | `click(path)` | `await client.click("/root/MyScene/Button")` |
86
- | `get_property(path, property)` | `await client.get_property("/root/Player", "position")` |
86
+ | `get_property(path, property)` | `await client.get_property("/root/Player", "position")` — returns bare value; CLI `get` returns `{"value": ..., "type"?: ...}` shape |
87
+ | `get_properties(path, properties)` | `await client.get_properties("/root/Player", ["position", "health"])` — multi-property atomic read; returns `{prop: bare_value, ...}` dict |
87
88
  | `set_property(path, property, value)` | `await client.set_property("/root/Player", "visible", False)` |
88
89
  | `call_method(path, method, args)` | `await client.call_method("/root/Player", "take_damage", [10])` |
89
90
  | `get_text(path)` | `await client.get_text("/root/UI/Label")` |
@@ -91,9 +92,14 @@ All methods callable via `godot-cli-control <method>` or `from godot_cli_control
91
92
  | `is_visible(path)` | `await client.is_visible("/root/UI/Panel")` |
92
93
  | `get_children(path, type_filter?)` | `await client.get_children("/root/Map", "Node2D")` |
93
94
  | `get_scene_tree(depth)` | `await client.get_scene_tree(depth=3)` |
94
- | `screenshot()` | `png_bytes = await client.screenshot()` |
95
+ | `screenshot(node=None)` | `png_bytes = await client.screenshot()`; pass `node="/root/Game/Sprite"` to crop to that node's screen-space AABB; CLI: `screenshot <path> [--node <node-path>]` (errors: `1010` bounds undeterminable, `1011` off-screen) |
96
+ | `sprite_info(path)` | `await client.sprite_info("/root/Game/Sprite")` — aggregate render-state query for `Sprite2D` / `AnimatedSprite2D` / `TextureRect` (texture, `effective_region`, `frame_texture`, flips, frame, modulate); headless-safe; CLI: `sprite-info <node-path>`; error `1010` for other node types |
97
+ | `errors(since=0, limit=100)` | `await client.errors(since=marker)` — structured `push_error`/`push_warning` capture (ring of last 1000, cursor pagination via `marker`); needs Godot 4.5+ `Logger` (else `1012`); CLI: `errors [--since MARKER] [--limit N]` |
95
98
  | `wait_for_node(path, timeout)` | `await client.wait_for_node("/root/Boss", timeout=10.0)` |
96
99
  | `wait_game_time(seconds)` | `await client.wait_game_time(2.5)` |
100
+ | `wait_property(path, prop, value, op, timeout, tolerance)` | `await client.wait_property("/root/Player", "position:x", 500, op="gt", timeout=3.0)` |
101
+ | `wait_signal(path, signal, timeout)` | `await client.wait_signal("/root/Game", "level_complete", timeout=10.0)` — miss result includes `reason: "timeout"\|"node_freed"` |
102
+ | `wait_frames(frames, physics)` | `await client.wait_frames(4)` |
97
103
  | `action_press(action)` | `await client.action_press("jump")` |
98
104
  | `action_release(action)` | `await client.action_release("jump")` |
99
105
  | `action_tap(action, duration)` | `await client.action_tap("attack", 0.1)` |
@@ -103,13 +109,25 @@ All methods callable via `godot-cli-control <method>` or `from godot_cli_control
103
109
  | `release_all()` | `await client.release_all()` |
104
110
  | `get_pressed()` | `await client.get_pressed()` |
105
111
  | `list_input_actions(include_builtin=False)` | `await client.list_input_actions()` |
112
+ | `scene_reload(timeout)` | `await client.scene_reload(timeout=10.0)` — reload current scene, block until ready; CLI: `scene-reload [--timeout N]` |
113
+ | `scene_change(path, timeout)` | `await client.scene_change("res://levels/l2.tscn", timeout=10.0)` — switch scene, block until ready; CLI: `scene-change <res://path.tscn> [--timeout N]` |
114
+ | `time_scale(value=None)` | `await client.time_scale()` — read `Engine.time_scale`; `await client.time_scale(5.0)` — set to 5×; CLI: `time-scale [value]`; valid range `(0, 100]` |
115
+ | `pause()` | `await client.pause()` — freeze scene tree (`get_tree().paused = true`); idempotent; CLI: `pause` |
116
+ | `unpause()` | `await client.unpause()` — resume scene tree; idempotent; CLI: `unpause` |
117
+ | `step_frames(frames, physics=False)` | `await client.step_frames(10)` — while paused, advance exactly N frames (1..3600) then stop; CLI: `step-frames <n> [--physics]`; error `1009` if tree not paused |
106
118
 
107
119
  > **CLI note**: as a shell subcommand, `screenshot` **requires an output path** — `godot-cli-control screenshot /tmp/shot.png` — and writes the PNG to that file. Returning base64 over stdout is intentionally unsupported, to keep large binary payloads out of an automating agent's context window.
108
120
 
109
121
  > **`--wait`**: `tap` / `hold` / `combo` return as soon as the input is armed, *before* the motion finishes. Add `--wait` (e.g. `hold run 1.5 --wait`) to block until the action's duration elapses (game-time) so the next read sees the settled state — equivalent to following the command with `wait-time <duration>` on the same connection.
110
122
 
123
+ > **Event pipeline**: `press` / `tap` / `hold` / `combo` inject an `InputEventAction` through the engine's event pipeline — both polling (`is_action_pressed`, `get_vector`) **and** event callbacks (`_input`, `_unhandled_input`) see the input. `InputEventAction` carries no mouse coordinates; position-dependent `_gui_input` widgets need `click` instead.
124
+
111
125
  > **No-arg `GameClient()` / `GameBridge()`**: with no `port` argument they auto-discover the daemon's port from `.cli_control/port` (falling back to `9877`), so a single-connection script connects to a running daemon without hardcoding a port.
112
126
 
127
+ ### Variant encoding depth limit
128
+
129
+ `get` encodes property values recursively. Containers nested deeper than **64 levels**, or self-referencing containers, are replaced with the sentinel string `"<max-depth-exceeded>"`. If you see it, narrow your read using a sub-path (`get /root/Node mydict:somekey`).
130
+
113
131
  ### Error codes
114
132
 
115
133
  Three numeric ranges share `error.code`; they never overlap, so a single field is unambiguous.
@@ -122,14 +140,21 @@ Three numeric ranges share `error.code`; they never overlap, so a single field i
122
140
  | `1004` | server | Combo already in progress (call `combo-cancel` to retry) |
123
141
  | `1005` | server | Scene tree too large (lower `depth` or pass `--max-nodes`) |
124
142
  | `1006` | server | Resource transiently unavailable (e.g. screenshot during scene transition). Rare under normal use — GameBridge waits for viewport first-frame before listening, and `screenshot` retries internally. Safe to retry if you do hit it. |
143
+ | `1007` | server | Signal not found on the node (`wait-signal` schema error — signal name typo or the node doesn't define it). Permanent — don't retry; inspect with `tree` or `children` to find valid signals. |
144
+ | `1008` | server | Scene unavailable (`scene-reload` / `scene-change`): no current scene, scene file missing / failed to load, or timed out waiting for the new scene to become ready. Fix the path (permanent) or inspect the daemon log (timeout). |
145
+ | `1009` | server | NOT_PAUSED: `step-frames` called while the scene tree is not paused. Call `pause` first. Precondition error — not a param error (distinct from `-32602`). |
146
+ | `1010` | server | UNSUPPORTED_NODE_TYPE: `sprite-info` on a non-sprite node, or `screenshot --node` on a node whose bounds can't be determined. Schema-class — pick another node/command, don't retry. |
147
+ | `1011` | server | NODE_NOT_ON_SCREEN: `screenshot --node` crop rect doesn't intersect the viewport (off-screen / zero size). State-class — bring the node into view, then retry. |
148
+ | `1012` | server | FEATURE_UNAVAILABLE: the hosting engine lacks an API this RPC needs (currently: `errors` requires Godot 4.5+ `Logger`). Permanent for that engine — upgrade Godot, don't retry. |
125
149
  | `-32600` | server | Malformed JSON-RPC request |
126
150
  | `-32601` | server | Unknown method name |
127
- | `-32602` | server | Invalid params (incl. blocked methods/properties from the security blacklist, `set` value-type mismatch — e.g. `Vector2` property given an array of wrong length / non-numeric elements, or `hold` given `duration ≤ 0` — use `press` for an indefinite hold) |
151
+ | `-32602` | server | Invalid params (incl. blocked methods/properties from the security blacklist, `set` value-type mismatch — e.g. `Vector2` property given an array of wrong length / non-numeric elements, or `hold` given `duration ≤ 0` — use `press` for an indefinite hold; also out-of-range values like a scene `timeout` outside `(0, 3600]` sent directly via `GameClient`) |
128
152
  | `-1001` | client | Connection failure (daemon not running, port wrong, proxy hijacking localhost) |
129
153
  | `-1002` | client | Timeout waiting for response |
130
- | `-1003` | client | CLI usage error (combo missing steps, malformed `--steps-json`, a non-numeric `tap`/`wait-time` arg, a `set`/`call` value that fails JSON parsing, …). From an RPC subcommand this always exits **64** (#82); top-level `run`/`daemon` precondition failures also use `-1003` but exit 2. |
154
+ | `-1003` | client | CLI usage error (combo missing steps, malformed `--steps-json`, a non-numeric `tap`/`wait-time` arg, a `scene-change` path not starting with `res://`/`uid://`, a `scene-reload`/`scene-change` `--timeout` outside `(0, 3600]`, a `set`/`call` value that fails JSON parsing, script path not found, or script missing `run(bridge)`). Always exits **64** (#82 / #111). |
131
155
  | `-1004` | client | Local file IO error (e.g. screenshot can't write the destination) |
132
156
  | `-1005` | client | `run <script>` user script raised an uncaught exception — fix the script |
157
+ | `-1006` | client | Infra pre-condition failure (`daemon start`/`stop`/`run` auto-start failed at OS level — port conflict, Godot binary not found, etc.). Always exits **2** (#92). |
133
158
  | `-1099` | client | Internal CLI bug — please file an issue |
134
159
 
135
160
  For full retry guidance see the SKILL.md shipped by `godot-cli-control init` (`.claude/skills/godot-cli-control/SKILL.md` in the target project).
@@ -140,7 +165,7 @@ The daemon is the long-lived Godot process the CLI talks to (one per project roo
140
165
 
141
166
  | Command | What it does |
142
167
  |---|---|
143
- | `daemon start [--headless\|--gui] [--record]` | Launch Godot with the bridge listening. Headless vs GUI is auto-detected from the TTY; `--record` forces GUI (Movie Maker needs a real renderer) and is **rejected with `--headless`** (exit 2). |
168
+ | `daemon start [--headless\|--gui] [--record] [--time-scale N]` | Launch Godot with the bridge listening. Headless vs GUI is auto-detected from the TTY; `--record` forces GUI (Movie Maker needs a real renderer) and is **rejected with `--headless`** (exit 2). `--time-scale N` sets `Engine.time_scale` from frame 0 (range `(0, 100]`). |
144
169
  | `daemon stop` | Stop this project's daemon. Exit 2 if it stopped cleanly but the `.avi`→`.mp4` ffmpeg transcode failed (raw `.avi` kept; see `.cli_control/ffmpeg.log`). |
145
170
  | `daemon stop --all` | Stop every registered daemon across all projects. **Exit 3** if at least one failed; per-record `rc` (and an `error` field on failures) is in `result.stopped[]`. |
146
171
  | `daemon stop --project <path>` | Stop the daemon for one specific project root. |
@@ -155,11 +180,11 @@ Semantic exit codes so `if godot-cli-control exists /root/Foo; then …` works i
155
180
 
156
181
  | Code | Meaning |
157
182
  |---|---|
158
- | 0 | Success (or boolean true for `exists` / `visible`, node found for `wait-node`) |
159
- | 1 | RPC error; also `exists`/`visible`=false, `wait-node` timeout, `daemon status`=stopped |
160
- | 2 | Connection / IO error, or `daemon stop` ffmpeg-transcode failure |
183
+ | 0 | Success (or boolean true for `exists` / `visible`, node found for `wait-node`, condition matched for `wait-prop` / `wait-signal`) |
184
+ | 1 | RPC error; also `exists`/`visible`=false, `wait-node`/`wait-prop`/`wait-signal` timeout, `daemon status`=stopped |
185
+ | 2 | Connection / IO error, infra pre-condition failure (`daemon start`/`stop` system error, carry `-1006`), or `daemon stop` ffmpeg-transcode failure |
161
186
  | 3 | `daemon stop --all` partial failure (≥1 daemon failed to stop) |
162
- | 64 | Usage error on an RPC subcommand bad argparse args, a pre-flight reject (`combo` with no steps / malformed `--steps-json`), a non-numeric `tap`/`wait-time` arg, or a `set`/`call` value that fails JSON parsing. These all carry client code `-1003` and now consistently exit 64 (#82; a few previously exited 2). |
187
+ | 64 | Usage error — argparse parse failure, a pre-flight reject (`combo` with no steps / malformed `--steps-json`), a non-numeric `tap`/`wait-time` arg, a `set`/`call` value that fails JSON parsing, `run <script>` with a non-existent path or missing `run(bridge)`. All carry client code `-1003` and exit 64 (#82 / #111). |
163
188
 
164
189
  ## Activation Modes
165
190
 
@@ -0,0 +1,57 @@
1
+ extends Logger
2
+ ## push_error / push_warning 拦截器(issue #103)。
3
+ ##
4
+ ## 单独成文件且**不带 class_name**:Logger 是 Godot 4.5+ API,本文件只能
5
+ ## 由 diagnostics_api.gd 在 ClassDB.class_exists("Logger") 通过后 load()。
6
+ ## 任何对本文件的静态 preload / class_name 全局注册都会让老引擎在编译
7
+ ## 阶段就炸掉整个 addon —— 动态 load 把兼容面隔离在这一个文件里。
8
+ ##
9
+ ## 实测形态(Godot 4.6.2):push_error("msg") 到达时 code="msg"、
10
+ ## rationale=""、function="push_error"、file 是 C++ 源文件;真正有用的
11
+ ## GDScript 调用位置在 script_backtraces 的首帧(release 构建下可能为空)。
12
+
13
+ ## 由 diagnostics_api 注入:func(entry: Dictionary) -> void。
14
+ ## _log_error 可能从任意线程进来,sink 的实现负责自己的线程安全。
15
+ var sink: Callable = Callable()
16
+
17
+ const _TYPE_NAMES: Dictionary = {
18
+ ERROR_TYPE_ERROR: "error",
19
+ ERROR_TYPE_WARNING: "warning",
20
+ ERROR_TYPE_SCRIPT: "script",
21
+ ERROR_TYPE_SHADER: "shader",
22
+ }
23
+
24
+
25
+ func _log_error(
26
+ function: String,
27
+ file: String,
28
+ line: int,
29
+ code: String,
30
+ rationale: String,
31
+ _editor_notify: bool,
32
+ error_type: int,
33
+ script_backtraces: Array[ScriptBacktrace],
34
+ ) -> void:
35
+ if not sink.is_valid():
36
+ return
37
+ # 引擎内部错误 message 在 rationale、push_error 的在 code —— 取非空者
38
+ var message: String = rationale if not rationale.is_empty() else code
39
+ var source: Variant = null
40
+ for bt: ScriptBacktrace in script_backtraces:
41
+ if bt != null and bt.get_frame_count() > 0:
42
+ source = "%s:%d @ %s" % [
43
+ bt.get_frame_file(0), bt.get_frame_line(0), bt.get_frame_function(0),
44
+ ]
45
+ break
46
+ sink.call({
47
+ "type": _TYPE_NAMES.get(error_type, "error"),
48
+ "message": message,
49
+ "function": function,
50
+ "file": file,
51
+ "line": line,
52
+ "source": source,
53
+ })
54
+
55
+
56
+ func _log_message(_message: String, _error: bool) -> void:
57
+ pass # 只收 error/warning;普通 print 进 ring 会把 buffer 冲成噪音
@@ -0,0 +1,124 @@
1
+ class_name DiagnosticsApi
2
+ extends Node
3
+ ## push_error / push_warning 结构化捕获 + errors RPC(issue #103)
4
+ ##
5
+ ## 动机:「游戏静默吞错」类 bug 在 e2e 里无法断言 —— e2e 全绿因为没有任何
6
+ ## 手段表达「本用例期间应零 push_error」。这里给出那条防线的服务端:
7
+ ## Logger(Godot 4.5+)→ ring buffer → errors RPC 按 seq 游标增量查询。
8
+ ##
9
+ ## - 线程安全:_log_error 可能从任意线程进来,ring 操作全程持 Mutex;
10
+ ## - 上下文友好(契约 6):消息截断 + 默认 limit + truncated/dropped 信号;
11
+ ## - 老引擎(< 4.5 无 Logger):errors RPC 返回 1012,capture_logger.gd
12
+ ## 靠 load() 动态加载隔离,不拖垮整个 addon 的编译。
13
+
14
+ const _CAPTURE_LOGGER_PATH := "res://addons/godot_cli_control/bridge/capture_logger.gd"
15
+ const _DEFAULT_LIMIT: int = 100
16
+ const _MAX_LIMIT: int = 1000
17
+ const _MAX_MESSAGE_LEN: int = 1024
18
+
19
+ # ring 容量。非 const:GUT 测试调小验证 overflow/dropped 路径。
20
+ var _ring_cap: int = 1000
21
+
22
+ var _logger: Object = null # capture_logger 实例;老引擎恒 null
23
+ var _mutex := Mutex.new()
24
+ var _entries: Array[Dictionary] = []
25
+ var _next_seq: int = 1
26
+
27
+
28
+ # 用 _enter_tree / _exit_tree 对称管理(_ready 只在首次进树触发,摘除重挂
29
+ # 后 logger 会悬空)。ring 内容跨摘挂保留 —— 只有捕获通道随树挂载开关。
30
+ func _enter_tree() -> void:
31
+ if not ClassDB.class_exists("Logger"):
32
+ return # 老引擎:handle_errors 走 1012
33
+ var script: GDScript = load(_CAPTURE_LOGGER_PATH)
34
+ _logger = script.new()
35
+ _logger.sink = _record
36
+ OS.add_logger(_logger)
37
+
38
+
39
+ func _exit_tree() -> void:
40
+ if _logger != null:
41
+ OS.remove_logger(_logger)
42
+ _logger = null
43
+
44
+
45
+ ## capture_logger 的 sink。可能在任意线程被调,除 Time 读数外全程持锁。
46
+ func _record(entry: Dictionary) -> void:
47
+ var msg: String = entry.get("message", "") as String
48
+ if msg.length() > _MAX_MESSAGE_LEN:
49
+ entry["message"] = msg.substr(0, _MAX_MESSAGE_LEN) + "...[truncated]"
50
+ entry["unix_time"] = Time.get_unix_time_from_system()
51
+ entry["ticks_msec"] = Time.get_ticks_msec()
52
+ _mutex.lock()
53
+ entry["seq"] = _next_seq
54
+ _next_seq += 1
55
+ _entries.append(entry)
56
+ if _entries.size() > _ring_cap:
57
+ _entries.pop_front()
58
+ _mutex.unlock()
59
+
60
+
61
+ ## errors RPC:{"since": int>=0, "limit": int 0.._MAX_LIMIT} 都可选。
62
+ ## 返回 {"errors": [...], "marker": int, "dropped": int, "truncated": bool}:
63
+ ## - marker:游标。无截断时 = 全局最新 seq;截断时 = 本批最后一条的 seq
64
+ ## (下次 since=marker 继续翻页)。limit=0 是纯基线查询(拿 marker 不拿数据)。
65
+ ## - dropped:since 之后、ring 已挤掉的条数(>0 说明错误风暴超过容量)。
66
+ func handle_errors(params: Dictionary) -> Dictionary:
67
+ if _logger == null:
68
+ return _err(
69
+ CliControlErrorCodes.FEATURE_UNAVAILABLE,
70
+ "errors capture requires Godot 4.5+ (Logger API); running %s"
71
+ % Engine.get_version_info().get("string", "unknown"),
72
+ )
73
+ var since_or_err: Variant = _parse_non_negative_int(params, "since", 0)
74
+ if since_or_err is Dictionary:
75
+ return since_or_err as Dictionary
76
+ var since: int = since_or_err as int
77
+ var limit_or_err: Variant = _parse_non_negative_int(params, "limit", _DEFAULT_LIMIT)
78
+ if limit_or_err is Dictionary:
79
+ return limit_or_err as Dictionary
80
+ var limit: int = limit_or_err as int
81
+ if limit > _MAX_LIMIT:
82
+ return _err(
83
+ CliControlErrorCodes.INVALID_PARAMS,
84
+ "'limit' must be 0..%d, got %d" % [_MAX_LIMIT, limit],
85
+ )
86
+
87
+ _mutex.lock()
88
+ var latest: int = _next_seq - 1
89
+ var oldest_available: int = _next_seq - _entries.size()
90
+ var matched: Array[Dictionary] = []
91
+ for e: Dictionary in _entries:
92
+ if int(e["seq"]) > since:
93
+ matched.append(e.duplicate())
94
+ _mutex.unlock()
95
+
96
+ var dropped: int = maxi(0, oldest_available - since - 1)
97
+ var truncated: bool = limit > 0 and matched.size() > limit
98
+ var out: Array[Dictionary] = matched.slice(0, limit)
99
+ var marker: int = latest
100
+ if truncated:
101
+ marker = int(out[-1]["seq"])
102
+ return {"errors": out, "marker": marker, "dropped": dropped, "truncated": truncated}
103
+
104
+
105
+ func _parse_non_negative_int(params: Dictionary, key: String, default_value: int) -> Variant:
106
+ var raw: Variant = params.get(key, default_value)
107
+ # JSON 数字经解析可能是 float;接受「数值上是整数」的 float
108
+ if raw is float and (raw as float) == floorf(raw as float):
109
+ raw = int(raw as float)
110
+ if not (raw is int):
111
+ return _err(
112
+ CliControlErrorCodes.INVALID_PARAMS,
113
+ "'%s' must be a non-negative integer, got %s" % [key, str(raw)],
114
+ )
115
+ if (raw as int) < 0:
116
+ return _err(
117
+ CliControlErrorCodes.INVALID_PARAMS,
118
+ "'%s' must be a non-negative integer, got %d" % [key, raw],
119
+ )
120
+ return raw as int
121
+
122
+
123
+ func _err(code: int, message: String) -> Dictionary:
124
+ return {"error": {"code": code, "message": message}}
@@ -0,0 +1,50 @@
1
+ class_name CliControlErrorCodes
2
+ extends RefCounted
3
+ ## 集中错误码常量。新加业务码必须在这里登记,
4
+ ## 避免 1004 那种隐式撞码(input_sim 用 "combo in progress",
5
+ ## low_level 又用 "scene tree too large")。
6
+ ##
7
+ ## 三段制(详见 SKILL.md 错误码表):
8
+ ## 1xxx 服务端业务码
9
+ ## -32xxx JSON-RPC 标准
10
+ ## -1xxx 客户端(Python)侧;GDScript 这边不会产出
11
+
12
+ const NODE_NOT_FOUND: int = 1001
13
+ const PROPERTY_NOT_FOUND: int = 1002 # 也用于 "node has no 'text' property"
14
+ const METHOD_NOT_FOUND: int = 1003
15
+ const COMBO_IN_PROGRESS: int = 1004
16
+ const SCENE_TREE_TOO_LARGE: int = 1005
17
+ # 资源 transient 不可用(screenshot viewport texture null 等)。
18
+ # 与 1003 拆开:1003 是 schema 错(永久),1006 是时机错(短重试可能成功)。
19
+ # issue #61 落地后语义:GameBridge 启动 gate(H)保证 client 连上时 viewport
20
+ # 至少画过一帧 + take_screenshot_async 内部循环(D)兜底动态 transient,
21
+ # 所以正常用法下 1006 不应触发。但它仍是 last-resort 合法信号 ——
22
+ # client 必须保留对 1006 的处理(不要假设它消失),未来若改为 fail-loud
23
+ # 会让 scene 切换瞬间的截图变成硬错。
24
+ const RESOURCE_UNAVAILABLE: int = 1006
25
+ # 信号不存在(wait_signal 的 schema 错,永久性——与 1003 method、1002 property 同族)
26
+ const SIGNAL_NOT_FOUND: int = 1007
27
+ # 场景不可用(issue #98 scene_reload/scene_change):reload 时无 current
28
+ # scene、change 的 res 路径不存在/加载失败、等新场景 ready 超时。
29
+ # 与 1006 拆开:1006 是「短重试可能成功」的 transient;1008 三种情形里
30
+ # 路径不存在是永久错,超时大概率是场景加载本身坏了——agent 应停下排查。
31
+ const SCENE_UNAVAILABLE: int = 1008
32
+ # step_frames 的状态前置错(issue #102):tree 未 paused 时调 step_frames。
33
+ # 与 -32602 区分:参数本身没问题,是世界状态不满足前置——agent 应先 pause。
34
+ const NOT_PAUSED: int = 1009
35
+ # 节点类型不支持该可视化操作(issue #101):sprite_info 打在非 sprite 类节点、
36
+ # screenshot --node 算不出节点边界(非 CanvasItem / 无法确定 local rect)。
37
+ # schema 类永久错(与 1002/1003/1007 同族)——agent 应换节点或换操作,重试无意义。
38
+ const UNSUPPORTED_NODE_TYPE: int = 1010
39
+ # screenshot --node 的裁剪框与视口交集为空(issue #101):节点在屏幕外或
40
+ # 变换后尺寸为零。状态类错(与 1009 同族)——参数没问题,是世界状态不满足;
41
+ # agent 应先把节点挪进视口(移动 camera / 改 position / 等动画到位)再截。
42
+ const NODE_NOT_ON_SCREEN: int = 1011
43
+ # 引擎能力缺失(issue #103):errors 捕获需要 Godot 4.5+ 的 Logger API,
44
+ # 老引擎上该 RPC 永久不可用。与 1006 区分:不是 transient,升级引擎前
45
+ # 重试无意义;与 1010 区分:错的不是目标节点,是宿主引擎版本。
46
+ const FEATURE_UNAVAILABLE: int = 1012
47
+
48
+ const INVALID_PARAMS: int = -32602
49
+ const INVALID_REQUEST: int = -32600
50
+ const METHOD_UNKNOWN: int = -32601
@@ -27,6 +27,11 @@ var _in_flight: int = 0
27
27
  var _outbound_buffer_size: int = DEFAULT_OUTBOUND_BUFFER_MB * 1024 * 1024
28
28
  var _low_level_api: LowLevelApi = null
29
29
  var _input_sim_api: InputSimulationApi = null
30
+ var _wait_api: WaitApi = null
31
+ var _scene_api: SceneApi = null
32
+ var _time_api: TimeApi = null
33
+ var _render_api: RenderApi = null
34
+ var _diag_api: DiagnosticsApi = null
30
35
  # 方法注册表:{method_name: {"callable": Callable, "kind": "sync"|"async"|"async_with_id"}}
31
36
  # - sync: handler(params) -> Dictionary,dispatcher 立即 send response
32
37
  # - async: handler(params) -> await Dictionary,dispatcher await 后 send response
@@ -54,6 +59,30 @@ func _ready() -> void:
54
59
  _input_sim_api.name = "InputSimulationApi"
55
60
  add_child(_input_sim_api)
56
61
  _input_sim_api.setup(_on_async_response)
62
+ _wait_api = WaitApi.new()
63
+ _wait_api.name = "WaitApi"
64
+ add_child(_wait_api)
65
+ _wait_api.setup(_low_level_api._read_property)
66
+ _scene_api = SceneApi.new()
67
+ _scene_api.name = "SceneApi"
68
+ add_child(_scene_api)
69
+ _time_api = TimeApi.new()
70
+ _time_api.name = "TimeApi"
71
+ add_child(_time_api)
72
+ _render_api = RenderApi.new()
73
+ _render_api.name = "RenderApi"
74
+ add_child(_render_api)
75
+ # DiagnosticsApi 尽早挂上:Logger 从注册那一刻起才开始捕获,挂得越早
76
+ # 「启动期 push_error」的可见窗口越大(issue #103)
77
+ _diag_api = DiagnosticsApi.new()
78
+ _diag_api.name = "DiagnosticsApi"
79
+ add_child(_diag_api)
80
+ # 启动倍速(issue #102):非法值 parse 内已 printerr + 忽略,不挡启动
81
+ # 必须在 await _wait_first_frame_ready() 之前,保证「第 0 帧即倍速」
82
+ var startup_scale: float = TimeApi.parse_cmdline_time_scale(OS.get_cmdline_args())
83
+ if startup_scale > 0.0:
84
+ Engine.time_scale = startup_scale
85
+ print("GameBridge: Engine.time_scale = %s (from --cli-time-scale)" % startup_scale)
57
86
  # 构建统一方法注册表
58
87
  _register_methods()
59
88
  # 缓存 outbound buffer 大小(ProjectSettings 可覆盖默认 10MB,至少 1MB)
@@ -176,6 +205,7 @@ func _register_methods() -> void:
176
205
  # 低层 API(同步)
177
206
  _methods["click"] = {"callable": _low_level_api.handle_click, "kind": "sync"}
178
207
  _methods["get_property"] = {"callable": _low_level_api.handle_get_property, "kind": "sync"}
208
+ _methods["get_properties"] = {"callable": _low_level_api.handle_get_properties, "kind": "sync"}
179
209
  _methods["set_property"] = {"callable": _low_level_api.handle_set_property, "kind": "sync"}
180
210
  _methods["call_method"] = {"callable": _low_level_api.handle_call_method, "kind": "sync"}
181
211
  _methods["get_text"] = {"callable": _low_level_api.handle_get_text, "kind": "sync"}
@@ -183,9 +213,12 @@ func _register_methods() -> void:
183
213
  _methods["is_visible"] = {"callable": _low_level_api.handle_is_visible, "kind": "sync"}
184
214
  _methods["get_children"] = {"callable": _low_level_api.handle_get_children, "kind": "sync"}
185
215
  _methods["get_scene_tree"] = {"callable": _low_level_api.handle_get_scene_tree, "kind": "sync"}
186
- # 低层 API(异步)
187
- _methods["wait_for_node"] = {"callable": _low_level_api.wait_for_node_async, "kind": "async"}
188
- _methods["wait_game_time"] = {"callable": _low_level_api.wait_game_time_async, "kind": "async"}
216
+ # Wait API(异步)
217
+ _methods["wait_for_node"] = {"callable": _wait_api.wait_for_node_async, "kind": "async"}
218
+ _methods["wait_game_time"] = {"callable": _wait_api.wait_game_time_async, "kind": "async"}
219
+ _methods["wait_frames"] = {"callable": _wait_api.wait_frames_async, "kind": "async"}
220
+ _methods["wait_property"] = {"callable": _wait_api.wait_property_async, "kind": "async"}
221
+ _methods["wait_signal"] = {"callable": _wait_api.wait_signal_async, "kind": "async"}
189
222
  _methods["screenshot"] = {"callable": _wrap_screenshot, "kind": "async"}
190
223
  # 输入模拟(同步)
191
224
  _methods["input_action_press"] = {
@@ -206,11 +239,22 @@ func _register_methods() -> void:
206
239
  }
207
240
  # 输入模拟(async_with_id:handler 自行通过 _on_async_response 回响)
208
241
  _methods["input_combo"] = {"callable": _input_sim_api.handle_combo, "kind": "async_with_id"}
209
-
210
-
211
- # screenshot wrapper:take_screenshot_async 不接 params,统一签名为 (params) -> Dictionary
212
- func _wrap_screenshot(_params: Dictionary) -> Dictionary:
213
- return await _low_level_api.take_screenshot_async()
242
+ # Scene API(异步)
243
+ _methods["scene_reload"] = {"callable": _scene_api.scene_reload_async, "kind": "async"}
244
+ _methods["scene_change"] = {"callable": _scene_api.scene_change_async, "kind": "async"}
245
+ # Time API(issue #102)
246
+ _methods["sprite_info"] = {"callable": _render_api.handle_sprite_info, "kind": "sync"}
247
+ _methods["errors"] = {"callable": _diag_api.handle_errors, "kind": "sync"}
248
+
249
+ _methods["time_scale"] = {"callable": _time_api.handle_time_scale, "kind": "sync"}
250
+ _methods["pause"] = {"callable": _time_api.handle_pause, "kind": "sync"}
251
+ _methods["unpause"] = {"callable": _time_api.handle_unpause, "kind": "sync"}
252
+ _methods["step_frames"] = {"callable": _time_api.step_frames_async, "kind": "async"}
253
+
254
+
255
+ # screenshot wrapper:params 透传(issue #101 起带可选 "node" 裁剪参数)
256
+ func _wrap_screenshot(params: Dictionary) -> Dictionary:
257
+ return await _low_level_api.take_screenshot_async(params)
214
258
 
215
259
 
216
260
  func _handle_message(raw: String) -> void:
@@ -270,14 +270,27 @@ func _process(delta: float) -> void:
270
270
 
271
271
  # ── 底层输入操作 ──
272
272
 
273
+ ## issue #97:走 Input.parse_input_event 而非 Input.action_press——
274
+ ## InputEventAction 经输入泵进 SceneTree 事件管线(_input / _unhandled_input
275
+ ## 可见),同时仍更新 action 状态位(is_action_pressed / get_vector 不回归)。
276
+ ## 注意:InputEventAction 无坐标,依赖鼠标位置的 _gui_input 控件请用 click。
273
277
  func _do_press(action: String) -> void:
274
- if InputMap.has_action(action):
275
- Input.action_press(action)
278
+ if not InputMap.has_action(action):
279
+ return
280
+ var ev: InputEventAction = InputEventAction.new()
281
+ ev.action = action
282
+ ev.pressed = true
283
+ ev.strength = 1.0
284
+ Input.parse_input_event(ev)
276
285
 
277
286
 
278
287
  func _do_release(action: String) -> void:
279
- if InputMap.has_action(action):
280
- Input.action_release(action)
288
+ if not InputMap.has_action(action):
289
+ return
290
+ var ev: InputEventAction = InputEventAction.new()
291
+ ev.action = action
292
+ ev.pressed = false
293
+ Input.parse_input_event(ev)
281
294
 
282
295
 
283
296
  func _err(code: int, message: String) -> Dictionary: