godot-cli-control 0.2.17__tar.gz → 0.3.0__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.17 → godot_cli_control-0.3.0}/PKG-INFO +38 -9
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/CHANGELOG.md +143 -21
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/README.md +9 -7
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/README.md +37 -8
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/godot_cli_control/_version.py +2 -2
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/godot_cli_control/bridge.py +14 -6
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/godot_cli_control/cli.py +440 -96
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/godot_cli_control/client.py +8 -3
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/godot_cli_control/daemon.py +214 -29
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/godot_cli_control/pytest_plugin.py +180 -16
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/godot_cli_control/registry.py +53 -15
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/godot_cli_control/templates/skill/SKILL.md +126 -20
- godot_cli_control-0.3.0/python/tests/conftest.py +20 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/tests/test_bridge.py +111 -3
- godot_cli_control-0.3.0/python/tests/test_broadcast.py +605 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/tests/test_cli.py +979 -16
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/tests/test_client.py +32 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/tests/test_daemon.py +288 -15
- godot_cli_control-0.3.0/python/tests/test_e2e_multi_instance.py +378 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/tests/test_e2e_scene.py +5 -1
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/tests/test_pytest_plugin.py +325 -1
- godot_cli_control-0.3.0/python/tests/test_registry.py +221 -0
- godot_cli_control-0.2.17/python/tests/test_registry.py +0 -128
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/.gitignore +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/LICENSE +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/LICENSE +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/bin/run_cli_control.ps1 +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/bin/run_cli_control.sh +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/bridge/capture_logger.gd +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/bridge/diagnostics_api.gd +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/bridge/error_codes.gd +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/bridge/game_bridge.gd +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/bridge/game_bridge.gd.uid +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/bridge/input_simulation_api.gd +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/bridge/input_simulation_api.gd.uid +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/bridge/low_level_api.gd +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/bridge/low_level_api.gd.uid +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/bridge/render_api.gd +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/bridge/scene_api.gd +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/bridge/time_api.gd +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/bridge/variant_codec.gd +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/bridge/wait_api.gd +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/plugin.cfg +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/plugin.gd +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/plugin.gd.uid +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/tests/gut/test_diagnostics_api.gd +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/tests/gut/test_game_bridge.gd +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/tests/gut/test_input_simulation_api.gd +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/tests/gut/test_low_level_api.gd +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/tests/gut/test_render_api.gd +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/tests/gut/test_scene_api.gd +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/tests/gut/test_time_api.gd +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/tests/gut/test_variant_codec.gd +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/tests/gut/test_wait_api.gd +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/tests/run_gut.py +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/tests/run_gut.sh +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/tests/test_init_walkthrough.py +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/tests/test_walkthrough.sh +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/pyproject.toml +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/godot_cli_control/__init__.py +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/godot_cli_control/__main__.py +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/godot_cli_control/_duration.py +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/godot_cli_control/init_cmd.py +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/godot_cli_control/runner.py +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/godot_cli_control/skills_install.py +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/godot_cli_control/templates/__init__.py +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/godot_cli_control/templates/skill/__init__.py +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/tests/__init__.py +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/tests/test_cli_helpers.py +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/tests/test_duration.py +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/tests/test_e2e_client_direct.py +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/tests/test_e2e_diag.py +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/tests/test_e2e_example.py +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/tests/test_e2e_input.py +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/tests/test_e2e_record.py +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/tests/test_e2e_render.py +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/tests/test_e2e_screenshot_gui.py +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/tests/test_e2e_time.py +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/tests/test_init.py +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/tests/test_issue_fixes.py +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/tests/test_runner.py +0 -0
- {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/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.
|
|
3
|
+
Version: 0.3.0
|
|
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
|
|
@@ -117,7 +117,19 @@ def test_jump(godot_daemon, bridge):
|
|
|
117
117
|
```
|
|
118
118
|
|
|
119
119
|
- `godot_daemon` (session-scoped) starts headless Godot once and stops it after all tests; if a daemon is already running it's reused (and not stopped at teardown — keeps your IDE workflow alive).
|
|
120
|
-
- `bridge` (function-scoped) gives a fresh `GameBridge`; on teardown it
|
|
120
|
+
- `bridge` (function-scoped) gives a fresh `GameBridge`; on teardown it best-effort restores global engine state — `unpause()`, `time_scale` back to its setup-time snapshot, `release_all()` — so a `hold`/`pause`/speed-up left behind by one case can't bleed into the next, then closes the connection.
|
|
121
|
+
- `fresh_scene` (function-scoped, opt-in) calls `scene_reload()` at setup so the case starts on a pristine scene. Node references cached before the reload are invalid afterwards.
|
|
122
|
+
- `no_push_errors` (function-scoped, opt-in) records an `errors` marker at setup and fails the case if any new `push_error` was emitted during it (warnings don't fail). Requires Godot 4.5+ (Logger API) — older engines raise `RpcError` 1012 at setup, loudly.
|
|
123
|
+
- `godot_instances` (scope configurable, default function) is a multi-instance factory for multiplayer e2e — start a named server **and** clients inside one test and get connected `GameBridge` objects back; teardown stops everything the fixture started (and only that):
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
def test_join(godot_instances):
|
|
127
|
+
server = godot_instances.start("server")
|
|
128
|
+
client = godot_instances.start("client1")
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`start(name)` is idempotent get-or-start (`headless`/`time_scale` follow the global options, overridable per call; `port` always defaults to 0 = OS-assigned); `stop(name)` stops one instance mid-test (restartable); `daemon(name)` exposes the underlying `Daemon`. `--godot-cli-instances-scope session` shares one set of instances across the whole suite (faster, no state isolation between tests).
|
|
132
|
+
- On a test failure (non-headless daemon only) a best-effort screenshot is saved to `.cli_control/failures/<nodeid>.png` and the path is attached to the pytest report.
|
|
121
133
|
|
|
122
134
|
CLI options:
|
|
123
135
|
|
|
@@ -125,6 +137,8 @@ CLI options:
|
|
|
125
137
|
--godot-cli-port=N # GameBridge port (default: read from .cli_control/port)
|
|
126
138
|
--godot-cli-no-headless # open a real Godot window
|
|
127
139
|
--godot-cli-project-root=DIR # default: pytest rootdir
|
|
140
|
+
--godot-cli-time-scale=X # Engine.time_scale applied at daemon startup (e.g. 5 to speed up the suite)
|
|
141
|
+
--godot-cli-instances-scope=function|session # godot_instances fixture scope (default: function)
|
|
128
142
|
```
|
|
129
143
|
|
|
130
144
|
## CLI
|
|
@@ -139,17 +153,20 @@ godot-cli-control daemon start --record --movie-path X [--fps N] # 录制需
|
|
|
139
153
|
godot-cli-control daemon stop [--all | --project PATH]
|
|
140
154
|
godot-cli-control daemon status
|
|
141
155
|
godot-cli-control daemon ls # list running daemons across all projects
|
|
156
|
+
godot-cli-control daemon logs [--tail N] # last N lines of .cli_control/godot.log (works post-mortem)
|
|
142
157
|
godot-cli-control run <script.py> [--headless ...]
|
|
143
158
|
|
|
144
159
|
# Read
|
|
145
160
|
godot-cli-control tree [depth]
|
|
146
|
-
godot-cli-control get <node_path> <prop>
|
|
161
|
+
godot-cli-control get <node_path> <prop> [prop2 ...] # multi-prop = atomic same-frame read
|
|
147
162
|
godot-cli-control text <node_path>
|
|
148
163
|
godot-cli-control exists <node_path> # exit 0=true, 1=false, 2=infra
|
|
149
164
|
godot-cli-control visible <node_path> # exit 0=true, 1=false, 2=infra
|
|
150
165
|
godot-cli-control children <node_path> [type-filter]
|
|
151
166
|
godot-cli-control pressed
|
|
152
167
|
godot-cli-control actions [--all]
|
|
168
|
+
godot-cli-control sprite-info <node_path> # Sprite2D/AnimatedSprite2D/TextureRect render state in one call
|
|
169
|
+
godot-cli-control errors [--since MARKER] [--limit N] # structured push_error/push_warning log (Godot 4.5+)
|
|
153
170
|
|
|
154
171
|
# Write / call
|
|
155
172
|
godot-cli-control set <node_path> <prop> <json-value>
|
|
@@ -164,21 +181,33 @@ godot-cli-control combo --steps-json '[...]' # or `combo file.json` / `combo -
|
|
|
164
181
|
godot-cli-control combo-cancel
|
|
165
182
|
godot-cli-control release-all
|
|
166
183
|
|
|
167
|
-
# Wait
|
|
168
|
-
godot-cli-control wait-node
|
|
169
|
-
godot-cli-control wait-
|
|
184
|
+
# Wait (exit 0=hit, 1=timeout)
|
|
185
|
+
godot-cli-control wait-node <node_path> [timeout]
|
|
186
|
+
godot-cli-control wait-prop <node_path> <prop> <value> [--op gt|lt|ge|le|ne] [--timeout S] [--tolerance T]
|
|
187
|
+
godot-cli-control wait-signal <node_path> <signal> [timeout] # arm BEFORE triggering the action
|
|
188
|
+
godot-cli-control wait-frames <n> [--physics]
|
|
189
|
+
godot-cli-control wait-time <seconds> # game time — scales with time-scale
|
|
190
|
+
|
|
191
|
+
# Scene isolation
|
|
192
|
+
godot-cli-control scene-reload [--timeout S] # reload current scene, block until ready
|
|
193
|
+
godot-cli-control scene-change <res://path.tscn> # switch scene, block until ready
|
|
194
|
+
|
|
195
|
+
# Time control
|
|
196
|
+
godot-cli-control time-scale [value] # read (no arg) or set Engine.time_scale, (0, 100]
|
|
197
|
+
godot-cli-control pause | unpause
|
|
198
|
+
godot-cli-control step-frames <n> [--physics] # deterministic stepping while paused
|
|
170
199
|
|
|
171
200
|
# Render (path is required as of 0.2.0)
|
|
172
|
-
godot-cli-control screenshot <output.png>
|
|
201
|
+
godot-cli-control screenshot <output.png> [--node <node_path>] # --node crops to that node's screen rect
|
|
173
202
|
```
|
|
174
203
|
|
|
175
204
|
### Output contract
|
|
176
205
|
|
|
177
206
|
- success: `{"ok": true, "result": <data>}` on stdout, exit 0
|
|
178
|
-
- error: `{"ok": false, "error": {"code": N, "message": "..."}}` on stdout, exit 1 (RPC)
|
|
207
|
+
- error: `{"ok": false, "error": {"code": N, "message": "..."}}` on stdout, exit 1 (RPC), 2 (connection / infra), or 64 (usage)
|
|
179
208
|
- `--text` / `--no-json` switches back to the legacy human-readable strings; errors then go to stderr.
|
|
180
209
|
|
|
181
|
-
`exists` / `visible`
|
|
210
|
+
`exists` / `visible` and the `wait-*` commands propagate their boolean / hit-or-timeout result to the exit code, so shell `if` works:
|
|
182
211
|
|
|
183
212
|
```bash
|
|
184
213
|
if godot-cli-control exists /root/Main/Boss; then
|
|
@@ -1,35 +1,167 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
只记用户可感知的变更(feature / BREAKING / 行为修复);纯 CI、内部重构、文档微调不入账,明细见 git log。
|
|
4
|
+
版本由 git tag 派生(hatch-vcs)。发版时 `release.sh` 会校验本文件:`[Unreleased]` 非空时拒绝打 tag,
|
|
5
|
+
加 `--roll-changelog` 自动把 `[Unreleased]` 滚动为新版本段(issue #140)。
|
|
6
|
+
|
|
3
7
|
## [Unreleased]
|
|
4
8
|
|
|
9
|
+
## [0.3.0] - 2026-06-07
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- **同项目多实例(命名实例)**:`daemon start --name <inst>`(默认 `default`)可在同一项目下并行启动多个 Godot daemon;顶层 `--instance <name>` 对所有 RPC / run / daemon 子命令通用,daemon 子命令的 `--name` 是等价写法。
|
|
13
|
+
- **状态布局迁移 `.cli_control/instances/<name>/`**:daemon 的 pid / port / log 文件从项目根平铺改为按实例子目录存放。legacy 平铺路径(旧格式 `<hash>.json` 注册记录 + `.cli_control/port` 等)仅作只读 fallback 兼容,升级期间无需手工迁移;停掉旧 daemon 后文件自动收敛。硬编码读 `.cli_control/port` 的脚本应改读 `.cli_control/instances/default/port`,或改用 `GameClient()`/`GameBridge()` 自动发现(支持 `instance=` 参数)以免再次硬编码。
|
|
14
|
+
- **`daemon ls` 新增 instance 列**:JSON 每条 `result.daemons[]` 含 `"instance"` 字段;text 输出格式改为 `pid\tport\tinstance\tproject_root\tstarted_at`。
|
|
15
|
+
- **`daemon stop --all --project <path>`**:限定停某个项目下的全部实例(原 `--all` 不带 `--project` 仍停全局所有项目的所有实例)。
|
|
16
|
+
- **daemon 各子命令的 JSON envelope result 带 `"instance"` 字段**:`start` / `stop` / `status` / `logs` 均在 result 中增加 `"instance"` 键,便于 agent 确认操作目标。
|
|
17
|
+
- **`GameBridge(instance=...)` / `GameClient(instance=...)`**:`port=None` 时 `instance` 参数生效,自动查找对应命名实例的端口;显式 `port` 优先(向后兼容)。
|
|
18
|
+
- **pytest 多实例工厂 fixture `godot_instances`**(#143):联机 e2e 在单测里同时拿 server / client bridge——`godot_instances.start("server")` 幂等启动命名实例并返回已连接 `GameBridge`,teardown 自动停掉本 fixture 起的全部实例(已在跑的只连不杀);`stop(name)` 支持中途显式停(掉线场景)后重启,`daemon(name)` 暴露底层 `Daemon`。新增 pytest 选项 `--godot-cli-instances-scope=function|session`(默认 function 每用例隔离;session 整套共享省启动时间)。
|
|
19
|
+
- **`--instance all` 一条命令广播全部活实例**(#145):任一 RPC 子命令对 cwd 项目全部活实例并发执行,聚合信封 `{"instances":[{instance, ok, result|error, rc}...], "rc": 0|3}`(entry 复刻单命令信封);退出码全 0→0、任一非 0→3(沿 `daemon stop --all` 先例)。字符串参数中的 `{instance}` 逐实例替换;广播 `screenshot` 的路径缺 `{instance}` 时 preflight 报 -1003/64。`all` 成为保留实例名(`daemon start --name all` 拒绝);`run`/`daemon` 子命令不支持广播。
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- **多实例选靶语义(处处一致)**:0 个在跑 → 选 default(legacy fallback);1 个在跑 → 自动选中;≥2 个在跑且未指定 `--instance` / `--name` → `-1003` / exit 64,message 列出在跑实例名(`"multiple instances running: ... — pass --instance <name>"`)。显式 `--instance nope` 但该实例未在跑 → `-1003` / exit 64,message 附当前在跑实例列表。`--instance` 与 `--port` 互斥。
|
|
23
|
+
- **选中实例 live 但 port 文件尚不可读(daemon 启动中的瞬态)→ `-1003` / exit 64 并提示稍后重试**(#144):此前静默回退默认端口、连接失败干等 30s,agent 拿不到「正在启动,请重试」的信号。
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- **`daemon stop --all --project <path>` 对没有任何 daemon 在跑的项目不再伪造 `default` 条目**(#144):text 输出 `(no running daemons)`、JSON `{"stopped": [], "rc": 0}`,与 `--all` 全局空注册表的形状一致。
|
|
27
|
+
- **旧版本 CLI 启动的录像 daemon 用新版 `daemon stop` 停止时录像正常转码**(#144):此前 legacy 平铺路径的 `movie_path` 无人消费,录像静默残留;legacy `last_exit_code` 死文件也一并清理。
|
|
28
|
+
- **kill -9 留下的死实例目录 `.cli_control/instances/<name>/` 在状态文件清空后自动回收**(#144):`godot.log` 等诊断文件在场时目录保留不动。
|
|
29
|
+
|
|
30
|
+
## [0.2.17] - 2026-06-06
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
- **#137 `screenshot --node` 在 hiDPI / viewport stretch 下裁剪错位**(PR #138):`compute_node_screen_rect` 漏乘 viewport final transform——rect 算在画布(逻辑设计分辨率)坐标系,而取图侧 `get_image()` 是窗口物理像素系;平窗(scale=1)两系重合所以长期未暴露。Retina / `canvas_items` stretch 下裁出的区域偏移或截半。
|
|
34
|
+
|
|
35
|
+
## [0.2.16] - 2026-06-06
|
|
36
|
+
|
|
5
37
|
### Added
|
|
38
|
+
- **`find_godot_binary` PATH 候选名补 .NET/mono 版别名**(PR #134):`godot4-mono` / `godot-mono` / `godot_mono` / `Godot_mono`,排标准名之后(双版并存优先轻量标准版)。macOS `/Applications` 与 Windows Program Files 的 glob 天然覆盖 mono 包名,零改动。
|
|
39
|
+
- **#135 macOS 检测兜底扫描 `~/Applications`**(PR #136):`/Applications` 优先、用户级目录兜底,覆盖无管理员权限安装的场景。
|
|
40
|
+
|
|
41
|
+
## [0.2.15] - 2026-06-05
|
|
42
|
+
|
|
43
|
+
### Added
|
|
44
|
+
- **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`。
|
|
6
45
|
- **#110 `wait_signal` 超时与节点释放可区分**:未命中时返回体新增 `reason` 字段,值为 `"timeout"`(信号在超时前未发射)或 `"node_freed"`(等待期间目标节点被释放)。命中路径(`emitted: true`)不含 `reason`,对齐 `wait_property` matched 时无 reason 的约定。非 BREAKING(新增可选字段)。
|
|
46
|
+
- **feat: `get_properties` 多属性同帧原子读 + `get` 多属性形式(#100)**:`get <path> <prop1> <prop2>` 一次 RPC 同帧读取多个属性,不存在部分新鲜/部分旧数据的竞态;任一属性缺失整体失败(1002,message 点名所有缺失项)。sub-path 形式(`position:x`)在多属性列表中也支持。
|
|
47
|
+
- **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`。
|
|
48
|
+
- **feat(scene): `scene-reload` / `scene-change` + pytest `fresh_scene` fixture + 错误码 1008(#98,PR #121)**:重载当前场景 / 切换到指定场景并阻塞到新场景 ready(per-test 隔离原语);无 current scene、路径不存在、超时报 1008。`fresh_scene` fixture 让用例从干净场景开始;reload 后此前缓存的节点路径全部失效。
|
|
49
|
+
- **feat(time): `time-scale` / `pause` / `unpause` / `step-frames` + 错误码 1009(#102,PR #125)**:读写 `Engine.time_scale`(合法域 (0, 100],`wait-time` 按 game time 计、倍速后语义不变)、暂停/恢复 SceneTree、paused 下确定性推进 N 帧(物理断言原语)。未 pause 调 `step-frames` 报 1009。
|
|
50
|
+
- **feat(render): `sprite-info` + `screenshot --node` + 错误码 1010/1011(#101,PR #130)**:`sprite-info` 一次拿齐 Sprite2D / AnimatedSprite2D / TextureRect 的 texture、实际图集区域(effective_region / frame_texture)、翻转、帧号、modulate、visible(纯属性读,headless 可用);`screenshot --node` 按节点屏幕 AABB 裁剪小图供像素级断言。非 sprite 类 → 1010,屏幕外/零尺寸 → 1011。
|
|
51
|
+
- **feat(diag): `errors` 结构化查询 + `daemon logs --tail` + pytest 失败配套 + 错误码 1012(#103,PR #131)**:Logger 拦截 push_error / push_warning,seq 游标增量查询(`--since` 实现「本用例期间零错误」断言;需 Godot 4.5+,老引擎报 1012);`daemon logs --tail N` 直接读 `.cli_control/godot.log`(daemon 已退出也可用,post-mortem 排查);pytest 新增 `no_push_errors` fixture(用例期间出现新 push_error 即失败)与失败自动截图(非 headless 时存 `.cli_control/failures/<nodeid>.png`)。
|
|
52
|
+
- **feat(init): 默认覆盖 addon 目录 + `--keep-addon` 逃生口(PR #133)**:重跑 `init` 即同步 `addons/godot_cli_control/` 与 SKILL.md 到当前 CLI 版本(插件目录 wipe 后重拷,`project.godot` patch 保持幂等)。`--force` 降级为兼容 no-op(与 `--keep-addon` 互斥)。
|
|
7
53
|
|
|
8
|
-
###
|
|
54
|
+
### Changed
|
|
55
|
+
- **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", ...)`。
|
|
56
|
+
- **pytest `bridge` fixture teardown 兜底还原全局态(#124,PR #126)**:unpause + 把 `time_scale` 还原到 setup 时快照值(非盲写 1.0,不打断 `--godot-cli-time-scale` 整套加速)+ `release_all()`。上个用例崩溃残留的 pause / 倍速不再污染下一个。
|
|
9
57
|
|
|
58
|
+
### Fixed (BREAKING — exit code changes)
|
|
10
59
|
- **#111 argparse 用法错 exit 2 → exit 64**: 所有 argparse 层错误(缺位置参数、非法 choices、未知子命令等)现在统一输出 `-1003` 信封并以 exit 64 退出。之前是 exit 2(argparse 默认)。影响:脚本若用 `[ $? -eq 2 ]` 判 argparse 错需改为 `[ $? -eq 64 ]`。
|
|
11
60
|
- **#92 `run <script>` 前置错误拆分**:
|
|
12
61
|
- 脚本路径不存在 / 脚本缺 `run(bridge)` 函数:由旧的 exit 2(或 exit 1)改为 exit 64,错误码由 `-1003` 保持不变。这是用法错,应修正调用而非重试。
|
|
13
62
|
- `daemon start`(`run` 自动启停 / `daemon start` 命令)/ `daemon stop` 系统级失败(端口冲突、找不到 Godot binary、PID 文件丢失等):错误码由 `-1003` 改为 **新码 `-1006` (PRECONDITION)**,exit code 保持 exit 2。影响:`run`/`daemon` 失败时,通过 `error.code` 区分用法错(`-1003` → 修调用)与基础设施错(`-1006` → 修环境)现在变得无歧义。
|
|
14
63
|
|
|
64
|
+
### Performance
|
|
65
|
+
- **#109 `wait_property` 免物化直比 + 属性存在性 memo(PR #132)**:逐帧轮询不再每帧物化编码整个属性值;顺手修了 GDScript 跨类型 `==` 是运行期脚本错误(中止函数抛 Nil、非静默 false)的存量雷(`_safe_eq` 守卫)。
|
|
66
|
+
|
|
67
|
+
## [0.2.14] - 2026-06-03
|
|
68
|
+
|
|
69
|
+
仅打包元数据 / 文档:PyPI keywords / classifiers / urls、社区健康文件(CONTRIBUTING / SECURITY / issue & PR 模板)、修复失效的 git 安装命令。无代码行为变更。
|
|
70
|
+
|
|
71
|
+
## [0.2.13] - 2026-06-02
|
|
72
|
+
|
|
15
73
|
### Added
|
|
16
|
-
- **
|
|
74
|
+
- **examples/platformer-demo 可跑示例(#89)**:自带 Start 按钮 + 跳跃角色场景与 `drive.sh` 全链路演示,CI 下真 Godot 驱动防腐。
|
|
17
75
|
|
|
18
|
-
###
|
|
19
|
-
-
|
|
76
|
+
### Fixed
|
|
77
|
+
- **批量修复 10 个 issue**:CLI 子命令端口从 `.cli_control/port` 自动发现、`-1003` 用法错退出码统一为 64(#82)、`tap`/`hold` 加 `--wait` 阻塞选项、ruff 进 CI 门禁等。
|
|
78
|
+
|
|
79
|
+
## [0.2.12] - 2026-06-01
|
|
20
80
|
|
|
21
81
|
### Added
|
|
22
|
-
-
|
|
82
|
+
- **#80 `init` 把 `.cli_control/` 写进目标项目 `.gitignore`**,避免提交机器本地状态(port / pid / log)。
|
|
83
|
+
|
|
84
|
+
## [0.2.11] - 2026-05-31
|
|
85
|
+
|
|
86
|
+
### Fixed
|
|
87
|
+
- **#79 拒绝 `--record` + `--headless` 组合**:Godot Movie Maker 在 headless 下 SIGSEGV(上游问题),preflight 直接报用法错(下游 CultivationWorld #180 根因)。
|
|
88
|
+
|
|
89
|
+
## [0.2.10] - 2026-05-24
|
|
90
|
+
|
|
91
|
+
### Fixed
|
|
92
|
+
- Windows / 测试修补批次:`run_gut.py` 强制 UTF-8 stdout/stderr(#36 Windows GUT 中文崩溃)、Windows 注册表目录(#43)、`run` 收尾 SIGTERM 噪音消除(#67)、`_build_tree` LIMIT 常量消歧(#48)。
|
|
93
|
+
|
|
94
|
+
## [0.2.9] - 2026-05-23
|
|
95
|
+
|
|
96
|
+
### Fixed
|
|
97
|
+
- **输入持续性**:断连按 WebSocket close code 区分「清 / 不清」持有中的输入;`hold` duration 加校验。崩断的客户端不再把按住的方向键永久遗留在游戏里。
|
|
98
|
+
|
|
99
|
+
## [0.2.8] - 2026-05-16
|
|
100
|
+
|
|
101
|
+
### Added
|
|
102
|
+
- **`GODOT_CLI_LONG_OP_TIMEOUT=<seconds>`** 环境变量抬高 600s 客户端长操作上限(超 10 分钟的录制);`wait_game_time` / `combo` 去掉 wall-clock 上限,靠 ws 心跳兜底(#45)。
|
|
103
|
+
- **`run` 静态检测脚本里的 `screenshot` 自动 force GUI(#65)**:headless 截图必失败,提前规避。
|
|
104
|
+
|
|
105
|
+
## [0.2.7] - 2026-05-16
|
|
106
|
+
|
|
107
|
+
仅文档:SKILL.md / `-h` / 旁路文档以代码为标准对齐(11 处缺漏与不准确)。无代码行为变更。
|
|
108
|
+
|
|
109
|
+
## [0.2.6] - 2026-05-16
|
|
110
|
+
|
|
111
|
+
### Fixed
|
|
112
|
+
- **#61 `screenshot` 首帧 transient 失败根因消除 + 兜底**:viewport texture 未就绪时不再偶发 1006。
|
|
113
|
+
|
|
114
|
+
## [0.2.5] - 2026-05-16
|
|
23
115
|
|
|
24
116
|
### Fixed
|
|
25
117
|
- **#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。
|
|
26
118
|
- **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。
|
|
119
|
+
- **CLI flag position**: `--json` / `--text` / `--no-json` 现在在 RPC 子命令尾部也接受(之前只能写最前面)。argparse 子 parser 用 `default=argparse.SUPPRESS` + 顶层 `set_defaults` 兜底,避免子 parser 默认值覆盖父 parser 解析结果。
|
|
120
|
+
- **Error code 1004 collision**: `low_level_api.gd` 的 scene tree 超限改用新业务码 `1005 "scene tree too large"`,与 input_simulation `1004 "combo in progress"` 解耦。新增 `error_codes.gd` 集中常量。
|
|
121
|
+
- **pytest fixture default port**: `--godot-cli-port` 默认从 9877 改为 0(OS-assigned),与 `daemon start` 默认对齐;多项目并行测试不再撞端口。
|
|
122
|
+
- **Addon README error-code table**: 之前只列到 1003,补全 1004 / 1005 / 客户端 -1xxx 段。
|
|
123
|
+
- **Scene tree hard limit bypass (DoS fix)**: `handle_get_scene_tree` 入口现在把 `max_nodes` clamp 到硬墙 `_BUILD_TREE_MAX_NODES` (5000)。修复前客户端传 `max_nodes=999999` 会让服务端先把整棵超大树构造成 Dictionary 再被 1005 错误丢弃(内存浪费 / OOM 路径)。
|
|
124
|
+
- **Error code 1003 semantic split**: screenshot 在 viewport texture 为 null 时不再借用 `1003 METHOD_NOT_FOUND`,改用新业务码 `1006 RESOURCE_UNAVAILABLE`。1003 现在是纯 schema 错(不应 retry),1006 是 transient 错(短重试可能成功),agent 据此分别处置。
|
|
125
|
+
- **`cmd_run` / `cmd_init` 真正输出 JSON envelope(#50)**;`GameBridge` 补 `get_scene_tree` / `wait_game_time` / `action_tap` alias 兑现「方法名一致」承诺(#58 #60)。
|
|
27
126
|
|
|
28
127
|
### 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`。
|
|
30
128
|
- **#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 一致)。
|
|
31
129
|
- **#54 防御性 fallback**:未在 coerce 名单也不在 `_ARRAY_PASSTHROUGH_SAFE_TYPES`(基本类型 / Object / 集合 / Packed*Array)白名单的声明类型 + Array 输入会 fail-loud,避免未来 Godot 加新 compound Variant 时 silent-corrupt 回归。
|
|
32
130
|
- **#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`)不变。
|
|
131
|
+
- `tree --max-nodes <N>`(默认 200):节点数软上限;超出时响应含 `truncated: true` + `total_nodes`,agent 据此决定分子树。硬墙仍是 5000 节点 → `1005`。
|
|
132
|
+
- `set` / `call --text-value`:禁用 JSON 解析、把 value/args 强制按字符串处理,避开 `null` / `true` / `42` 这类字面量被解析成 Variant 类型的 footgun。
|
|
133
|
+
|
|
134
|
+
### Changed
|
|
135
|
+
- **BREAKING (轻微)**:`daemon start` / `run` 默认 headless 行为改为基于 `sys.stdout.isatty()` 自动判定 —— pipe / CI / agent shell 默认 headless;交互终端默认开窗。新增 `--gui` 强制开窗 flag。`--headless` 仍可显式传,覆盖自动判。脚本里依赖 "默认会开窗" 的需要加 `--gui`。
|
|
136
|
+
|
|
137
|
+
## [0.2.4] - 2026-05-01
|
|
138
|
+
|
|
139
|
+
### Changed
|
|
140
|
+
- **BREAKING (轻微):默认 daemon 端口从 9877 改为 OS-assigned**。实际端口写入 `.cli_control/port`,CLI 子命令与 `GameClient()`(不传 port)自动发现。硬编码 `127.0.0.1:9877` 的外部脚本要么读 `.cli_control/port`,要么显式传 `--port 9877`。
|
|
141
|
+
|
|
142
|
+
### Added
|
|
143
|
+
- **`daemon ls`**:跨项目列出运行中的 daemon(全局注册表 + 死记录自动清理)。
|
|
144
|
+
- **`daemon stop --all` / `--project <path>`**:批量 / 跨项目停止。
|
|
145
|
+
- **`daemon start --idle-timeout 30m`**:opt-in 空闲自动 shutdown;项目级默认可写 `.cli_control/config.json`(`{"idle_timeout": "30m"}`),显式 flag 优先。
|
|
146
|
+
- `daemon start` 前先 bind 校验端口空闲,被占立即报错。
|
|
147
|
+
|
|
148
|
+
## [0.2.3] - 2026-05-01
|
|
149
|
+
|
|
150
|
+
### Added
|
|
151
|
+
- **`GODOT_CLI_CONTROL=0` 强制禁用逃生口**:env var 一票否决,覆盖其它激活方式。
|
|
152
|
+
|
|
153
|
+
## [0.2.2] - 2026-04-29
|
|
154
|
+
|
|
155
|
+
### Security / Hardening
|
|
156
|
+
- **封堵 `set_property` 黑名单 NodePath sub-property 旁路**(`script:source_code` 类路径绕过黑名单)。
|
|
157
|
+
- handler 加固:async type-check、action 名校验、scene-tree 节点数上限。
|
|
158
|
+
- Python 测试覆盖率接入 80% 门禁。
|
|
159
|
+
|
|
160
|
+
## [0.2.1] - 2026-04-29
|
|
161
|
+
|
|
162
|
+
与 v0.2.0 同代码(发布管线重跑),无变更。
|
|
163
|
+
|
|
164
|
+
## [0.2.0] - 2026-04-29
|
|
33
165
|
|
|
34
166
|
### AI-friendly CLI 改造(多个 BREAKING change)
|
|
35
167
|
|
|
@@ -73,24 +205,14 @@
|
|
|
73
205
|
#### Out of scope (intentional)
|
|
74
206
|
- `run <script.py>` 子命令保留旧的人类可读 stderr 输出。它是交互式脚本宿主,不是 RPC,新的 JSON 信封契约只覆盖 RPC 子命令 + daemon 三命令。
|
|
75
207
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
#### Fixed
|
|
79
|
-
- **CLI flag position**: `--json` / `--text` / `--no-json` 现在在 RPC 子命令尾部也接受(之前只能写最前面)。argparse 子 parser 用 `default=argparse.SUPPRESS` + 顶层 `set_defaults` 兜底,避免子 parser 默认值覆盖父 parser 解析结果。
|
|
80
|
-
- **Error code 1004 collision**: `low_level_api.gd` 的 scene tree 超限改用新业务码 `1005 "scene tree too large"`,与 input_simulation `1004 "combo in progress"` 解耦。新增 `error_codes.gd` 集中常量。
|
|
81
|
-
- **pytest fixture default port**: `--godot-cli-port` 默认从 9877 改为 0(OS-assigned),与 `daemon start` 默认对齐;多项目并行测试不再撞端口。
|
|
82
|
-
- **Addon README error-code table**: 之前只列到 1003,补全 1004 / 1005 / 客户端 -1xxx 段。
|
|
83
|
-
- **Scene tree hard limit bypass (DoS fix)**: `handle_get_scene_tree` 入口现在把 `max_nodes` clamp 到硬墙 `_BUILD_TREE_MAX_NODES` (5000)。修复前客户端传 `max_nodes=999999` 会让服务端先把整棵超大树构造成 Dictionary 再被 1005 错误丢弃(内存浪费 / OOM 路径)。
|
|
84
|
-
- **Error code 1003 semantic split**: screenshot 在 viewport texture 为 null 时不再借用 `1003 METHOD_NOT_FOUND`,改用新业务码 `1006 RESOURCE_UNAVAILABLE`。1003 现在是纯 schema 错(不应 retry),1006 是 transient 错(短重试可能成功),agent 据此分别处置。
|
|
208
|
+
## [0.1.7] – [0.1.10] - 2026-04-29
|
|
85
209
|
|
|
86
|
-
|
|
87
|
-
- `tree --max-nodes <N>`(默认 200):节点数软上限;超出时响应含 `truncated: true` + `total_nodes`,agent 据此决定分子树。硬墙仍是 5000 节点 → `1005`。
|
|
88
|
-
- `set` / `call --text-value`:禁用 JSON 解析、把 value/args 强制按字符串处理,避开 `null` / `true` / `42` 这类字面量被解析成 Variant 类型的 footgun。
|
|
210
|
+
发布管线 / 打包修复批次:`pyproject.toml` 提升到仓库根(0.1.7)、`init` 重新导入项目以刷新 `global_script_class_cache`(0.1.9)。
|
|
89
211
|
|
|
90
|
-
|
|
91
|
-
-
|
|
212
|
+
### Changed
|
|
213
|
+
- **`auto_enable_in_debug` 默认改为 true**(0.1.10):编辑器 F5 直接生效,不再需要手动激活(release build 仍无条件禁用)。
|
|
92
214
|
|
|
93
|
-
## [0.1.6] -
|
|
215
|
+
## [0.1.6] - 2026-04-29
|
|
94
216
|
|
|
95
217
|
### Added
|
|
96
218
|
- `godot-cli-control init`: 一键 onboarding 子命令 + 跨平台 Python daemon (取代手写 wrapper)。
|
|
@@ -122,7 +122,7 @@ All methods callable via `godot-cli-control <method>` or `from godot_cli_control
|
|
|
122
122
|
|
|
123
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
124
|
|
|
125
|
-
> **No-arg `GameClient()` / `GameBridge()`**: with no `port` argument they auto-discover the daemon's port from `.cli_control/port` (falling back to `9877`)
|
|
125
|
+
> **No-arg `GameClient()` / `GameBridge()`**: with no `port` argument they auto-discover the daemon's port from `.cli_control/instances/<name>/port` (falling back to `9877`). Pass `instance="server"` to target a named instance when multiple are running (explicit `port` always wins over `instance`). A no-arg call in a single-instance environment still works as before.
|
|
126
126
|
|
|
127
127
|
### Variant encoding depth limit
|
|
128
128
|
|
|
@@ -151,7 +151,7 @@ Three numeric ranges share `error.code`; they never overlap, so a single field i
|
|
|
151
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`) |
|
|
152
152
|
| `-1001` | client | Connection failure (daemon not running, port wrong, proxy hijacking localhost) |
|
|
153
153
|
| `-1002` | client | Timeout waiting for response |
|
|
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,
|
|
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, script missing `run(bridge)`, **multi-instance ambiguity** (≥2 instances running and no `--instance`/`--name` given), **named instance not running** (explicit `--instance nope` but that instance isn't up), **instance port not readable yet** (alive but mid-startup — transient, retry), or `--instance` / `--name` conflict). Always exits **64** (#82 / #111). |
|
|
155
155
|
| `-1004` | client | Local file IO error (e.g. screenshot can't write the destination) |
|
|
156
156
|
| `-1005` | client | `run <script>` user script raised an uncaught exception — fix the script |
|
|
157
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). |
|
|
@@ -165,12 +165,14 @@ The daemon is the long-lived Godot process the CLI talks to (one per project roo
|
|
|
165
165
|
|
|
166
166
|
| Command | What it does |
|
|
167
167
|
|---|---|
|
|
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]`). |
|
|
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`). |
|
|
168
|
+
| `daemon start [--headless\|--gui] [--record] [--time-scale N] [--name <inst>]` | Launch Godot with the bridge listening. `--name` sets the instance name (default `default`); use different names to run multiple Godot daemons for the same project in parallel. 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]`). |
|
|
169
|
+
| `daemon stop [--name <inst>]` | Stop this project's daemon. With `--name`, stop the named instance. Exit 2 if it stopped cleanly but the `.avi`→`.mp4` ffmpeg transcode failed (raw `.avi` kept; see `.cli_control/ffmpeg.log`). |
|
|
170
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[]`. |
|
|
171
|
-
| `daemon stop --project <path>` | Stop
|
|
172
|
-
| `daemon
|
|
173
|
-
| `daemon
|
|
171
|
+
| `daemon stop --all --project <path>` | Stop all instances of one specific project. |
|
|
172
|
+
| `daemon stop --project <path>` | Stop the daemon for one specific project root (single-instance shorthand). |
|
|
173
|
+
| `daemon status [--name <inst>]` | Exit 0 = running, 1 = stopped. Response includes `"instance"` field. When stopped, the payload may carry `last_log` / `last_exit_code` to diagnose a crash. |
|
|
174
|
+
| `daemon ls` | List every registered daemon (project root, pid, port, **instance**). Text columns: `pid\tport\tinstance\tproject_root\tstarted_at`. |
|
|
175
|
+
| `daemon logs [--name <inst>] [--tail N]` | Print last N lines of godot.log for the given instance. |
|
|
174
176
|
|
|
175
177
|
**Project-level defaults**: drop a `.cli_control/config.json` with `{"idle_timeout": "30m"}` to give `daemon start` / `run` a default idle-timeout without passing `--idle-timeout` each time. It's consulted only when the flag is omitted (default `0`); an explicit `--idle-timeout` always wins. A long-running `wait-time` / `combo` / recording is bounded client-side by a 600s wall-time fail-safe — override it for legitimately long operations with `GODOT_CLI_LONG_OP_TIMEOUT=<seconds>`.
|
|
176
178
|
|
|
@@ -80,7 +80,19 @@ def test_jump(godot_daemon, bridge):
|
|
|
80
80
|
```
|
|
81
81
|
|
|
82
82
|
- `godot_daemon` (session-scoped) starts headless Godot once and stops it after all tests; if a daemon is already running it's reused (and not stopped at teardown — keeps your IDE workflow alive).
|
|
83
|
-
- `bridge` (function-scoped) gives a fresh `GameBridge`; on teardown it
|
|
83
|
+
- `bridge` (function-scoped) gives a fresh `GameBridge`; on teardown it best-effort restores global engine state — `unpause()`, `time_scale` back to its setup-time snapshot, `release_all()` — so a `hold`/`pause`/speed-up left behind by one case can't bleed into the next, then closes the connection.
|
|
84
|
+
- `fresh_scene` (function-scoped, opt-in) calls `scene_reload()` at setup so the case starts on a pristine scene. Node references cached before the reload are invalid afterwards.
|
|
85
|
+
- `no_push_errors` (function-scoped, opt-in) records an `errors` marker at setup and fails the case if any new `push_error` was emitted during it (warnings don't fail). Requires Godot 4.5+ (Logger API) — older engines raise `RpcError` 1012 at setup, loudly.
|
|
86
|
+
- `godot_instances` (scope configurable, default function) is a multi-instance factory for multiplayer e2e — start a named server **and** clients inside one test and get connected `GameBridge` objects back; teardown stops everything the fixture started (and only that):
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
def test_join(godot_instances):
|
|
90
|
+
server = godot_instances.start("server")
|
|
91
|
+
client = godot_instances.start("client1")
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`start(name)` is idempotent get-or-start (`headless`/`time_scale` follow the global options, overridable per call; `port` always defaults to 0 = OS-assigned); `stop(name)` stops one instance mid-test (restartable); `daemon(name)` exposes the underlying `Daemon`. `--godot-cli-instances-scope session` shares one set of instances across the whole suite (faster, no state isolation between tests).
|
|
95
|
+
- On a test failure (non-headless daemon only) a best-effort screenshot is saved to `.cli_control/failures/<nodeid>.png` and the path is attached to the pytest report.
|
|
84
96
|
|
|
85
97
|
CLI options:
|
|
86
98
|
|
|
@@ -88,6 +100,8 @@ CLI options:
|
|
|
88
100
|
--godot-cli-port=N # GameBridge port (default: read from .cli_control/port)
|
|
89
101
|
--godot-cli-no-headless # open a real Godot window
|
|
90
102
|
--godot-cli-project-root=DIR # default: pytest rootdir
|
|
103
|
+
--godot-cli-time-scale=X # Engine.time_scale applied at daemon startup (e.g. 5 to speed up the suite)
|
|
104
|
+
--godot-cli-instances-scope=function|session # godot_instances fixture scope (default: function)
|
|
91
105
|
```
|
|
92
106
|
|
|
93
107
|
## CLI
|
|
@@ -102,17 +116,20 @@ godot-cli-control daemon start --record --movie-path X [--fps N] # 录制需
|
|
|
102
116
|
godot-cli-control daemon stop [--all | --project PATH]
|
|
103
117
|
godot-cli-control daemon status
|
|
104
118
|
godot-cli-control daemon ls # list running daemons across all projects
|
|
119
|
+
godot-cli-control daemon logs [--tail N] # last N lines of .cli_control/godot.log (works post-mortem)
|
|
105
120
|
godot-cli-control run <script.py> [--headless ...]
|
|
106
121
|
|
|
107
122
|
# Read
|
|
108
123
|
godot-cli-control tree [depth]
|
|
109
|
-
godot-cli-control get <node_path> <prop>
|
|
124
|
+
godot-cli-control get <node_path> <prop> [prop2 ...] # multi-prop = atomic same-frame read
|
|
110
125
|
godot-cli-control text <node_path>
|
|
111
126
|
godot-cli-control exists <node_path> # exit 0=true, 1=false, 2=infra
|
|
112
127
|
godot-cli-control visible <node_path> # exit 0=true, 1=false, 2=infra
|
|
113
128
|
godot-cli-control children <node_path> [type-filter]
|
|
114
129
|
godot-cli-control pressed
|
|
115
130
|
godot-cli-control actions [--all]
|
|
131
|
+
godot-cli-control sprite-info <node_path> # Sprite2D/AnimatedSprite2D/TextureRect render state in one call
|
|
132
|
+
godot-cli-control errors [--since MARKER] [--limit N] # structured push_error/push_warning log (Godot 4.5+)
|
|
116
133
|
|
|
117
134
|
# Write / call
|
|
118
135
|
godot-cli-control set <node_path> <prop> <json-value>
|
|
@@ -127,21 +144,33 @@ godot-cli-control combo --steps-json '[...]' # or `combo file.json` / `combo -
|
|
|
127
144
|
godot-cli-control combo-cancel
|
|
128
145
|
godot-cli-control release-all
|
|
129
146
|
|
|
130
|
-
# Wait
|
|
131
|
-
godot-cli-control wait-node
|
|
132
|
-
godot-cli-control wait-
|
|
147
|
+
# Wait (exit 0=hit, 1=timeout)
|
|
148
|
+
godot-cli-control wait-node <node_path> [timeout]
|
|
149
|
+
godot-cli-control wait-prop <node_path> <prop> <value> [--op gt|lt|ge|le|ne] [--timeout S] [--tolerance T]
|
|
150
|
+
godot-cli-control wait-signal <node_path> <signal> [timeout] # arm BEFORE triggering the action
|
|
151
|
+
godot-cli-control wait-frames <n> [--physics]
|
|
152
|
+
godot-cli-control wait-time <seconds> # game time — scales with time-scale
|
|
153
|
+
|
|
154
|
+
# Scene isolation
|
|
155
|
+
godot-cli-control scene-reload [--timeout S] # reload current scene, block until ready
|
|
156
|
+
godot-cli-control scene-change <res://path.tscn> # switch scene, block until ready
|
|
157
|
+
|
|
158
|
+
# Time control
|
|
159
|
+
godot-cli-control time-scale [value] # read (no arg) or set Engine.time_scale, (0, 100]
|
|
160
|
+
godot-cli-control pause | unpause
|
|
161
|
+
godot-cli-control step-frames <n> [--physics] # deterministic stepping while paused
|
|
133
162
|
|
|
134
163
|
# Render (path is required as of 0.2.0)
|
|
135
|
-
godot-cli-control screenshot <output.png>
|
|
164
|
+
godot-cli-control screenshot <output.png> [--node <node_path>] # --node crops to that node's screen rect
|
|
136
165
|
```
|
|
137
166
|
|
|
138
167
|
### Output contract
|
|
139
168
|
|
|
140
169
|
- success: `{"ok": true, "result": <data>}` on stdout, exit 0
|
|
141
|
-
- error: `{"ok": false, "error": {"code": N, "message": "..."}}` on stdout, exit 1 (RPC)
|
|
170
|
+
- error: `{"ok": false, "error": {"code": N, "message": "..."}}` on stdout, exit 1 (RPC), 2 (connection / infra), or 64 (usage)
|
|
142
171
|
- `--text` / `--no-json` switches back to the legacy human-readable strings; errors then go to stderr.
|
|
143
172
|
|
|
144
|
-
`exists` / `visible`
|
|
173
|
+
`exists` / `visible` and the `wait-*` commands propagate their boolean / hit-or-timeout result to the exit code, so shell `if` works:
|
|
145
174
|
|
|
146
175
|
```bash
|
|
147
176
|
if godot-cli-control exists /root/Main/Boss; then
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.
|
|
22
|
-
__version_tuple__ = version_tuple = (0,
|
|
21
|
+
__version__ = version = '0.3.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 3, 0)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -21,15 +21,23 @@ from .client import GameClient
|
|
|
21
21
|
class GameBridge:
|
|
22
22
|
"""同步接口,内部通过事件循环调用异步 GameClient。"""
|
|
23
23
|
|
|
24
|
-
def __init__(self, port: int | None = None) -> None:
|
|
25
|
-
# port=None → GameClient 从 .cli_control/
|
|
24
|
+
def __init__(self, port: int | None = None, instance: str | None = None) -> None:
|
|
25
|
+
# port=None → GameClient 从 .cli_control/ auto-discover(issue #91)。
|
|
26
26
|
# daemon 默认 OS 自动分配端口,所以 ``GameBridge()`` 无参数也能连上正在
|
|
27
27
|
# 跑的 daemon,与 README 的单连接脚本示例一致。
|
|
28
|
+
# instance:命名实例名(如 "server"),透传给 GameClient.instance;
|
|
29
|
+
# 显式 port 优先,instance 仅在 port=None 时生效(见 client.py)。
|
|
28
30
|
self._loop = asyncio.new_event_loop()
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
# 构造/连接失败时回收 loop —— 多实例歧义(InstanceAmbiguityError)在
|
|
32
|
+
# __init__ 抛出是预期用法路径,不能让每次报错都泄漏一个 event loop。
|
|
33
|
+
try:
|
|
34
|
+
self._client = GameClient(port=port, instance=instance)
|
|
35
|
+
self._loop.run_until_complete(
|
|
36
|
+
self._client.connect(retries=15, backoff=1.0, total_timeout=60.0)
|
|
37
|
+
)
|
|
38
|
+
except BaseException:
|
|
39
|
+
self._loop.close()
|
|
40
|
+
raise
|
|
33
41
|
|
|
34
42
|
def close(self) -> None:
|
|
35
43
|
self._loop.run_until_complete(self._client.disconnect())
|