godot-cli-control 0.2.14__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.
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/PKG-INFO +3 -3
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/addons/godot_cli_control/CHANGELOG.md +20 -0
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/addons/godot_cli_control/README.md +35 -10
- godot_cli_control-0.2.15/addons/godot_cli_control/bridge/capture_logger.gd +57 -0
- godot_cli_control-0.2.15/addons/godot_cli_control/bridge/diagnostics_api.gd +124 -0
- godot_cli_control-0.2.15/addons/godot_cli_control/bridge/error_codes.gd +50 -0
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/addons/godot_cli_control/bridge/game_bridge.gd +52 -8
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/addons/godot_cli_control/bridge/input_simulation_api.gd +17 -4
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/addons/godot_cli_control/bridge/low_level_api.gd +100 -34
- godot_cli_control-0.2.15/addons/godot_cli_control/bridge/render_api.gd +197 -0
- godot_cli_control-0.2.15/addons/godot_cli_control/bridge/scene_api.gd +87 -0
- godot_cli_control-0.2.15/addons/godot_cli_control/bridge/time_api.gd +94 -0
- godot_cli_control-0.2.15/addons/godot_cli_control/bridge/variant_codec.gd +141 -0
- godot_cli_control-0.2.15/addons/godot_cli_control/bridge/wait_api.gd +401 -0
- godot_cli_control-0.2.15/addons/godot_cli_control/tests/gut/test_diagnostics_api.gd +133 -0
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/addons/godot_cli_control/tests/gut/test_game_bridge.gd +73 -7
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/addons/godot_cli_control/tests/gut/test_input_simulation_api.gd +55 -0
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/addons/godot_cli_control/tests/gut/test_low_level_api.gd +192 -0
- godot_cli_control-0.2.15/addons/godot_cli_control/tests/gut/test_render_api.gd +203 -0
- godot_cli_control-0.2.15/addons/godot_cli_control/tests/gut/test_scene_api.gd +66 -0
- godot_cli_control-0.2.15/addons/godot_cli_control/tests/gut/test_time_api.gd +122 -0
- godot_cli_control-0.2.15/addons/godot_cli_control/tests/gut/test_variant_codec.gd +115 -0
- godot_cli_control-0.2.15/addons/godot_cli_control/tests/gut/test_wait_api.gd +438 -0
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/python/README.md +2 -2
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/python/godot_cli_control/_version.py +2 -2
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/python/godot_cli_control/bridge.py +64 -3
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/python/godot_cli_control/cli.py +612 -29
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/python/godot_cli_control/client.py +134 -3
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/python/godot_cli_control/daemon.py +8 -0
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/python/godot_cli_control/init_cmd.py +10 -4
- godot_cli_control-0.2.15/python/godot_cli_control/pytest_plugin.py +224 -0
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/python/godot_cli_control/templates/skill/SKILL.md +105 -11
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/python/tests/test_bridge.py +233 -2
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/python/tests/test_cli.py +931 -76
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/python/tests/test_cli_helpers.py +248 -4
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/python/tests/test_client.py +496 -0
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/python/tests/test_daemon.py +82 -0
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/python/tests/test_e2e_client_direct.py +87 -0
- godot_cli_control-0.2.15/python/tests/test_e2e_diag.py +176 -0
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/python/tests/test_e2e_example.py +3 -2
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/python/tests/test_e2e_input.py +68 -1
- godot_cli_control-0.2.15/python/tests/test_e2e_render.py +221 -0
- godot_cli_control-0.2.15/python/tests/test_e2e_scene.py +149 -0
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/python/tests/test_e2e_screenshot_gui.py +49 -0
- godot_cli_control-0.2.15/python/tests/test_e2e_time.py +163 -0
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/python/tests/test_init.py +50 -2
- godot_cli_control-0.2.15/python/tests/test_pytest_plugin.py +887 -0
- godot_cli_control-0.2.14/addons/godot_cli_control/bridge/error_codes.gd +0 -28
- godot_cli_control-0.2.14/python/godot_cli_control/pytest_plugin.py +0 -109
- godot_cli_control-0.2.14/python/tests/test_pytest_plugin.py +0 -384
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/.gitignore +0 -0
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/LICENSE +0 -0
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/addons/godot_cli_control/LICENSE +0 -0
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/addons/godot_cli_control/bin/run_cli_control.ps1 +0 -0
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/addons/godot_cli_control/bin/run_cli_control.sh +0 -0
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/addons/godot_cli_control/bridge/game_bridge.gd.uid +0 -0
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/addons/godot_cli_control/bridge/input_simulation_api.gd.uid +0 -0
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/addons/godot_cli_control/bridge/low_level_api.gd.uid +0 -0
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/addons/godot_cli_control/plugin.cfg +0 -0
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/addons/godot_cli_control/plugin.gd +0 -0
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/addons/godot_cli_control/plugin.gd.uid +0 -0
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/addons/godot_cli_control/tests/run_gut.py +0 -0
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/addons/godot_cli_control/tests/run_gut.sh +0 -0
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/addons/godot_cli_control/tests/test_init_walkthrough.py +0 -0
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/addons/godot_cli_control/tests/test_walkthrough.sh +0 -0
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/pyproject.toml +0 -0
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/python/godot_cli_control/__init__.py +0 -0
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/python/godot_cli_control/__main__.py +0 -0
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/python/godot_cli_control/_duration.py +0 -0
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/python/godot_cli_control/registry.py +0 -0
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/python/godot_cli_control/runner.py +0 -0
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/python/godot_cli_control/skills_install.py +0 -0
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/python/godot_cli_control/templates/__init__.py +0 -0
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/python/godot_cli_control/templates/skill/__init__.py +0 -0
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/python/tests/__init__.py +0 -0
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/python/tests/test_duration.py +0 -0
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/python/tests/test_e2e_record.py +0 -0
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/python/tests/test_issue_fixes.py +0 -0
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/python/tests/test_registry.py +0 -0
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/python/tests/test_runner.py +0 -0
- {godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/python/tests/test_skills_install.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: godot-cli-control
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.15
|
|
4
4
|
Summary: WebSocket bridge for headless / scripted control of Godot scenes.
|
|
5
5
|
Project-URL: Homepage, https://github.com/ClaymanTwinkle/godot-cli-control
|
|
6
6
|
Project-URL: Repository, https://github.com/ClaymanTwinkle/godot-cli-control
|
|
@@ -62,7 +62,7 @@ godot-cli-control tree 3
|
|
|
62
62
|
godot-cli-control daemon stop
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
-
`init`
|
|
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.
|
|
66
66
|
|
|
67
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.
|
|
68
68
|
|
|
@@ -133,7 +133,7 @@ The CLI is the canonical surface — every `GameClient` method has a one-line eq
|
|
|
133
133
|
|
|
134
134
|
```bash
|
|
135
135
|
# Lifecycle
|
|
136
|
-
godot-cli-control init [--path DIR] [--
|
|
136
|
+
godot-cli-control init [--path DIR] [--keep-addon]
|
|
137
137
|
godot-cli-control daemon start [--headless | --gui] [--port N --idle-timeout 30m]
|
|
138
138
|
godot-cli-control daemon start --record --movie-path X [--fps N] # 录制需真实渲染器,不能与 --headless 同用
|
|
139
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)
|
|
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,
|
|
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
|
|
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
|
{godot_cli_control-0.2.14 → godot_cli_control-0.2.15}/addons/godot_cli_control/bridge/game_bridge.gd
RENAMED
|
@@ -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
|
-
#
|
|
187
|
-
_methods["wait_for_node"] = {"callable":
|
|
188
|
-
_methods["wait_game_time"] = {"callable":
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|