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.
Files changed (82) hide show
  1. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/PKG-INFO +38 -9
  2. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/CHANGELOG.md +143 -21
  3. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/README.md +9 -7
  4. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/README.md +37 -8
  5. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/godot_cli_control/_version.py +2 -2
  6. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/godot_cli_control/bridge.py +14 -6
  7. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/godot_cli_control/cli.py +440 -96
  8. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/godot_cli_control/client.py +8 -3
  9. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/godot_cli_control/daemon.py +214 -29
  10. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/godot_cli_control/pytest_plugin.py +180 -16
  11. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/godot_cli_control/registry.py +53 -15
  12. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/godot_cli_control/templates/skill/SKILL.md +126 -20
  13. godot_cli_control-0.3.0/python/tests/conftest.py +20 -0
  14. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/tests/test_bridge.py +111 -3
  15. godot_cli_control-0.3.0/python/tests/test_broadcast.py +605 -0
  16. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/tests/test_cli.py +979 -16
  17. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/tests/test_client.py +32 -0
  18. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/tests/test_daemon.py +288 -15
  19. godot_cli_control-0.3.0/python/tests/test_e2e_multi_instance.py +378 -0
  20. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/tests/test_e2e_scene.py +5 -1
  21. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/tests/test_pytest_plugin.py +325 -1
  22. godot_cli_control-0.3.0/python/tests/test_registry.py +221 -0
  23. godot_cli_control-0.2.17/python/tests/test_registry.py +0 -128
  24. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/.gitignore +0 -0
  25. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/LICENSE +0 -0
  26. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/LICENSE +0 -0
  27. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/bin/run_cli_control.ps1 +0 -0
  28. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/bin/run_cli_control.sh +0 -0
  29. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/bridge/capture_logger.gd +0 -0
  30. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/bridge/diagnostics_api.gd +0 -0
  31. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/bridge/error_codes.gd +0 -0
  32. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/bridge/game_bridge.gd +0 -0
  33. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/bridge/game_bridge.gd.uid +0 -0
  34. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/bridge/input_simulation_api.gd +0 -0
  35. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/bridge/input_simulation_api.gd.uid +0 -0
  36. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/bridge/low_level_api.gd +0 -0
  37. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/bridge/low_level_api.gd.uid +0 -0
  38. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/bridge/render_api.gd +0 -0
  39. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/bridge/scene_api.gd +0 -0
  40. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/bridge/time_api.gd +0 -0
  41. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/bridge/variant_codec.gd +0 -0
  42. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/bridge/wait_api.gd +0 -0
  43. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/plugin.cfg +0 -0
  44. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/plugin.gd +0 -0
  45. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/plugin.gd.uid +0 -0
  46. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/tests/gut/test_diagnostics_api.gd +0 -0
  47. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/tests/gut/test_game_bridge.gd +0 -0
  48. {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
  49. {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
  50. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/tests/gut/test_render_api.gd +0 -0
  51. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/tests/gut/test_scene_api.gd +0 -0
  52. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/tests/gut/test_time_api.gd +0 -0
  53. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/tests/gut/test_variant_codec.gd +0 -0
  54. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/tests/gut/test_wait_api.gd +0 -0
  55. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/tests/run_gut.py +0 -0
  56. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/tests/run_gut.sh +0 -0
  57. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/tests/test_init_walkthrough.py +0 -0
  58. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/addons/godot_cli_control/tests/test_walkthrough.sh +0 -0
  59. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/pyproject.toml +0 -0
  60. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/godot_cli_control/__init__.py +0 -0
  61. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/godot_cli_control/__main__.py +0 -0
  62. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/godot_cli_control/_duration.py +0 -0
  63. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/godot_cli_control/init_cmd.py +0 -0
  64. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/godot_cli_control/runner.py +0 -0
  65. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/godot_cli_control/skills_install.py +0 -0
  66. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/godot_cli_control/templates/__init__.py +0 -0
  67. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/godot_cli_control/templates/skill/__init__.py +0 -0
  68. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/tests/__init__.py +0 -0
  69. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/tests/test_cli_helpers.py +0 -0
  70. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/tests/test_duration.py +0 -0
  71. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/tests/test_e2e_client_direct.py +0 -0
  72. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/tests/test_e2e_diag.py +0 -0
  73. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/tests/test_e2e_example.py +0 -0
  74. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/tests/test_e2e_input.py +0 -0
  75. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/tests/test_e2e_record.py +0 -0
  76. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/tests/test_e2e_render.py +0 -0
  77. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/tests/test_e2e_screenshot_gui.py +0 -0
  78. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/tests/test_e2e_time.py +0 -0
  79. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/tests/test_init.py +0 -0
  80. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/tests/test_issue_fixes.py +0 -0
  81. {godot_cli_control-0.2.17 → godot_cli_control-0.3.0}/python/tests/test_runner.py +0 -0
  82. {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.2.17
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 calls `release_all()` so a `hold` left behind by one case can't bleed into the next, then closes the connection.
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 <node_path> [timeout] # exit 0=found, 1=timeout
169
- godot-cli-control wait-time <seconds>
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) or 2 (connection / usage)
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` / `wait-node` propagate their boolean result to the exit code, so shell `if` works:
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
- ### Fixed (BREAKING — exit code changes)
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
- - **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`。
74
+ - **examples/platformer-demo 可跑示例(#89)**:自带 Start 按钮 + 跳跃角色场景与 `drive.sh` 全链路演示,CI 下真 Godot 驱动防腐。
17
75
 
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", ...)`。
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
- - **feat: `get_properties` 多属性同帧原子读 + `get` 多属性形式(#100)**:`get <path> <prop1> <prop2>` 一次 RPC 同帧读取多个属性,不存在部分新鲜/部分旧数据的竞态;任一属性缺失整体失败(1002,message 点名所有缺失项)。sub-path 形式(`position:x`)在多属性列表中也支持。
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
- ### AI-friendliness review fixes (2026-05-11)
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
- #### Added
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
- #### Changed
91
- - **BREAKING (轻微)**:`daemon start` / `run` 默认 headless 行为改为基于 `sys.stdout.isatty()` 自动判定 —— pipe / CI / agent shell 默认 headless;交互终端默认开窗。新增 `--gui` 强制开窗 flag。`--headless` 仍可显式传,覆盖自动判。脚本里依赖 "默认会开窗" 的需要加 `--gui`。
212
+ ### Changed
213
+ - **`auto_enable_in_debug` 默认改为 true**(0.1.10):编辑器 F5 直接生效,不再需要手动激活(release build 仍无条件禁用)。
92
214
 
93
- ## [0.1.6] - Unreleased
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`), so a single-connection script connects to a running daemon without hardcoding a port.
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, or script missing `run(bridge)`). Always exits **64** (#82 / #111). |
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 the daemon for one specific project root. |
172
- | `daemon status` | Exit 0 = running, 1 = stopped. When stopped, the payload may carry `last_log` / `last_exit_code` to diagnose a crash. |
173
- | `daemon ls` | List every registered daemon (project root, pid, port). |
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 calls `release_all()` so a `hold` left behind by one case can't bleed into the next, then closes the connection.
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 <node_path> [timeout] # exit 0=found, 1=timeout
132
- godot-cli-control wait-time <seconds>
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) or 2 (connection / usage)
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` / `wait-node` propagate their boolean result to the exit code, so shell `if` works:
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.2.17'
22
- __version_tuple__ = version_tuple = (0, 2, 17)
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/port auto-discover(issue #91)。
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
- self._client = GameClient(port=port)
30
- self._loop.run_until_complete(
31
- self._client.connect(retries=15, backoff=1.0, total_timeout=60.0)
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())