godot-cli-control 0.2.18__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.18 → godot_cli_control-0.3.0}/PKG-INFO +11 -1
  2. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/addons/godot_cli_control/CHANGELOG.md +21 -0
  3. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/addons/godot_cli_control/README.md +9 -7
  4. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/python/README.md +10 -0
  5. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/python/godot_cli_control/_version.py +2 -2
  6. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/python/godot_cli_control/bridge.py +14 -6
  7. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/python/godot_cli_control/cli.py +440 -96
  8. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/python/godot_cli_control/client.py +8 -3
  9. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/python/godot_cli_control/daemon.py +214 -29
  10. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/python/godot_cli_control/pytest_plugin.py +180 -16
  11. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/python/godot_cli_control/registry.py +53 -15
  12. {godot_cli_control-0.2.18 → 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.18 → 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.18 → godot_cli_control-0.3.0}/python/tests/test_cli.py +979 -16
  17. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/python/tests/test_client.py +32 -0
  18. {godot_cli_control-0.2.18 → 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.18 → godot_cli_control-0.3.0}/python/tests/test_e2e_scene.py +5 -1
  21. {godot_cli_control-0.2.18 → 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.18/python/tests/test_registry.py +0 -128
  24. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/.gitignore +0 -0
  25. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/LICENSE +0 -0
  26. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/addons/godot_cli_control/LICENSE +0 -0
  27. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/addons/godot_cli_control/bin/run_cli_control.ps1 +0 -0
  28. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/addons/godot_cli_control/bin/run_cli_control.sh +0 -0
  29. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/addons/godot_cli_control/bridge/capture_logger.gd +0 -0
  30. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/addons/godot_cli_control/bridge/diagnostics_api.gd +0 -0
  31. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/addons/godot_cli_control/bridge/error_codes.gd +0 -0
  32. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/addons/godot_cli_control/bridge/game_bridge.gd +0 -0
  33. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/addons/godot_cli_control/bridge/game_bridge.gd.uid +0 -0
  34. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/addons/godot_cli_control/bridge/input_simulation_api.gd +0 -0
  35. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/addons/godot_cli_control/bridge/input_simulation_api.gd.uid +0 -0
  36. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/addons/godot_cli_control/bridge/low_level_api.gd +0 -0
  37. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/addons/godot_cli_control/bridge/low_level_api.gd.uid +0 -0
  38. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/addons/godot_cli_control/bridge/render_api.gd +0 -0
  39. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/addons/godot_cli_control/bridge/scene_api.gd +0 -0
  40. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/addons/godot_cli_control/bridge/time_api.gd +0 -0
  41. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/addons/godot_cli_control/bridge/variant_codec.gd +0 -0
  42. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/addons/godot_cli_control/bridge/wait_api.gd +0 -0
  43. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/addons/godot_cli_control/plugin.cfg +0 -0
  44. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/addons/godot_cli_control/plugin.gd +0 -0
  45. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/addons/godot_cli_control/plugin.gd.uid +0 -0
  46. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/addons/godot_cli_control/tests/gut/test_diagnostics_api.gd +0 -0
  47. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/addons/godot_cli_control/tests/gut/test_game_bridge.gd +0 -0
  48. {godot_cli_control-0.2.18 → 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.18 → 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.18 → godot_cli_control-0.3.0}/addons/godot_cli_control/tests/gut/test_render_api.gd +0 -0
  51. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/addons/godot_cli_control/tests/gut/test_scene_api.gd +0 -0
  52. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/addons/godot_cli_control/tests/gut/test_time_api.gd +0 -0
  53. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/addons/godot_cli_control/tests/gut/test_variant_codec.gd +0 -0
  54. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/addons/godot_cli_control/tests/gut/test_wait_api.gd +0 -0
  55. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/addons/godot_cli_control/tests/run_gut.py +0 -0
  56. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/addons/godot_cli_control/tests/run_gut.sh +0 -0
  57. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/addons/godot_cli_control/tests/test_init_walkthrough.py +0 -0
  58. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/addons/godot_cli_control/tests/test_walkthrough.sh +0 -0
  59. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/pyproject.toml +0 -0
  60. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/python/godot_cli_control/__init__.py +0 -0
  61. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/python/godot_cli_control/__main__.py +0 -0
  62. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/python/godot_cli_control/_duration.py +0 -0
  63. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/python/godot_cli_control/init_cmd.py +0 -0
  64. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/python/godot_cli_control/runner.py +0 -0
  65. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/python/godot_cli_control/skills_install.py +0 -0
  66. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/python/godot_cli_control/templates/__init__.py +0 -0
  67. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/python/godot_cli_control/templates/skill/__init__.py +0 -0
  68. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/python/tests/__init__.py +0 -0
  69. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/python/tests/test_cli_helpers.py +0 -0
  70. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/python/tests/test_duration.py +0 -0
  71. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/python/tests/test_e2e_client_direct.py +0 -0
  72. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/python/tests/test_e2e_diag.py +0 -0
  73. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/python/tests/test_e2e_example.py +0 -0
  74. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/python/tests/test_e2e_input.py +0 -0
  75. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/python/tests/test_e2e_record.py +0 -0
  76. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/python/tests/test_e2e_render.py +0 -0
  77. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/python/tests/test_e2e_screenshot_gui.py +0 -0
  78. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/python/tests/test_e2e_time.py +0 -0
  79. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/python/tests/test_init.py +0 -0
  80. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/python/tests/test_issue_fixes.py +0 -0
  81. {godot_cli_control-0.2.18 → godot_cli_control-0.3.0}/python/tests/test_runner.py +0 -0
  82. {godot_cli_control-0.2.18 → 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.18
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
@@ -120,6 +120,15 @@ def test_jump(godot_daemon, bridge):
120
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
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
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).
123
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.
124
133
 
125
134
  CLI options:
@@ -129,6 +138,7 @@ CLI options:
129
138
  --godot-cli-no-headless # open a real Godot window
130
139
  --godot-cli-project-root=DIR # default: pytest rootdir
131
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)
132
142
  ```
133
143
 
134
144
  ## CLI
@@ -6,6 +6,27 @@
6
6
 
7
7
  ## [Unreleased]
8
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
+
9
30
  ## [0.2.17] - 2026-06-06
10
31
 
11
32
  ### Fixed
@@ -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
 
@@ -83,6 +83,15 @@ def test_jump(godot_daemon, bridge):
83
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
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
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).
86
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.
87
96
 
88
97
  CLI options:
@@ -92,6 +101,7 @@ CLI options:
92
101
  --godot-cli-no-headless # open a real Godot window
93
102
  --godot-cli-project-root=DIR # default: pytest rootdir
94
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)
95
105
  ```
96
106
 
97
107
  ## CLI
@@ -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.18'
22
- __version_tuple__ = version_tuple = (0, 2, 18)
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())