godot-cli-control 0.2.5__tar.gz → 0.2.7__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.5 → godot_cli_control-0.2.7}/PKG-INFO +1 -1
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/addons/godot_cli_control/README.md +3 -2
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/addons/godot_cli_control/bridge/error_codes.gd +5 -0
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/addons/godot_cli_control/bridge/game_bridge.gd +29 -0
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/addons/godot_cli_control/bridge/low_level_api.gd +23 -7
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/addons/godot_cli_control/tests/gut/test_game_bridge.gd +23 -0
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/addons/godot_cli_control/tests/gut/test_low_level_api.gd +23 -0
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/python/godot_cli_control/_version.py +2 -2
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/python/godot_cli_control/cli.py +1 -1
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/python/godot_cli_control/templates/skill/SKILL.md +30 -6
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/.gitignore +0 -0
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/LICENSE +0 -0
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/addons/godot_cli_control/CHANGELOG.md +0 -0
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/addons/godot_cli_control/LICENSE +0 -0
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/addons/godot_cli_control/bin/run_cli_control.ps1 +0 -0
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/addons/godot_cli_control/bin/run_cli_control.sh +0 -0
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/addons/godot_cli_control/bridge/game_bridge.gd.uid +0 -0
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/addons/godot_cli_control/bridge/input_simulation_api.gd +0 -0
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/addons/godot_cli_control/bridge/input_simulation_api.gd.uid +0 -0
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/addons/godot_cli_control/bridge/low_level_api.gd.uid +0 -0
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/addons/godot_cli_control/plugin.cfg +0 -0
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/addons/godot_cli_control/plugin.gd +0 -0
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/addons/godot_cli_control/plugin.gd.uid +0 -0
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/addons/godot_cli_control/tests/gut/test_input_simulation_api.gd +0 -0
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/addons/godot_cli_control/tests/run_gut.sh +0 -0
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/addons/godot_cli_control/tests/test_init_walkthrough.py +0 -0
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/addons/godot_cli_control/tests/test_walkthrough.sh +0 -0
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/pyproject.toml +0 -0
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/python/README.md +0 -0
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/python/godot_cli_control/__init__.py +0 -0
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/python/godot_cli_control/__main__.py +0 -0
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/python/godot_cli_control/_duration.py +0 -0
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/python/godot_cli_control/bridge.py +0 -0
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/python/godot_cli_control/client.py +0 -0
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/python/godot_cli_control/daemon.py +0 -0
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/python/godot_cli_control/init_cmd.py +0 -0
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/python/godot_cli_control/pytest_plugin.py +0 -0
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/python/godot_cli_control/registry.py +0 -0
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/python/godot_cli_control/runner.py +0 -0
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/python/godot_cli_control/skills_install.py +0 -0
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/python/godot_cli_control/templates/__init__.py +0 -0
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/python/godot_cli_control/templates/skill/__init__.py +0 -0
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/python/tests/__init__.py +0 -0
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/python/tests/test_bridge.py +0 -0
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/python/tests/test_cli.py +0 -0
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/python/tests/test_cli_helpers.py +0 -0
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/python/tests/test_client.py +0 -0
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/python/tests/test_daemon.py +0 -0
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/python/tests/test_duration.py +0 -0
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/python/tests/test_init.py +0 -0
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/python/tests/test_pytest_plugin.py +0 -0
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/python/tests/test_registry.py +0 -0
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/python/tests/test_runner.py +0 -0
- {godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/python/tests/test_skills_install.py +0 -0
|
@@ -93,11 +93,12 @@ All methods callable via `godot-cli-control <method>` or `from godot_cli_control
|
|
|
93
93
|
| `action_press(action)` | `await client.action_press("jump")` |
|
|
94
94
|
| `action_release(action)` | `await client.action_release("jump")` |
|
|
95
95
|
| `action_tap(action, duration)` | `await client.action_tap("attack", 0.1)` |
|
|
96
|
-
| `input_get_pressed` (raw RPC) | `await client.request("input_get_pressed")` |
|
|
97
96
|
| `hold(action, duration)` | `await client.hold("run", 1.5)` |
|
|
98
97
|
| `combo(steps)` | `await client.combo([{"action": "jump", "duration": 0.1}])` |
|
|
99
98
|
| `combo_cancel()` | `await client.combo_cancel()` |
|
|
100
99
|
| `release_all()` | `await client.release_all()` |
|
|
100
|
+
| `get_pressed()` | `await client.get_pressed()` |
|
|
101
|
+
| `list_input_actions(include_builtin=False)` | `await client.list_input_actions()` |
|
|
101
102
|
|
|
102
103
|
### Error codes
|
|
103
104
|
|
|
@@ -110,7 +111,7 @@ Three numeric ranges share `error.code`; they never overlap, so a single field i
|
|
|
110
111
|
| `1003` | server | Method not found on the node (schema error, don't retry) |
|
|
111
112
|
| `1004` | server | Combo already in progress (call `combo-cancel` to retry) |
|
|
112
113
|
| `1005` | server | Scene tree too large (lower `depth` or pass `--max-nodes`) |
|
|
113
|
-
| `1006` | server | Resource transiently unavailable (e.g. screenshot
|
|
114
|
+
| `1006` | server | Resource transiently unavailable (e.g. screenshot during scene transition). Rare under normal use — GameBridge waits for viewport first-frame before listening, and `screenshot` retries internally. Safe to retry if you do hit it. |
|
|
114
115
|
| `-32600` | server | Malformed JSON-RPC request |
|
|
115
116
|
| `-32601` | server | Unknown method name |
|
|
116
117
|
| `-32602` | server | Invalid params (incl. blocked methods/properties from the security blacklist, or `set` value-type mismatch — e.g. `Vector2` property given an array of wrong length / non-numeric elements) |
|
{godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/addons/godot_cli_control/bridge/error_codes.gd
RENAMED
|
@@ -16,6 +16,11 @@ const COMBO_IN_PROGRESS: int = 1004
|
|
|
16
16
|
const SCENE_TREE_TOO_LARGE: int = 1005
|
|
17
17
|
# 资源 transient 不可用(screenshot viewport texture null 等)。
|
|
18
18
|
# 与 1003 拆开:1003 是 schema 错(永久),1006 是时机错(短重试可能成功)。
|
|
19
|
+
# issue #61 落地后语义:GameBridge 启动 gate(H)保证 client 连上时 viewport
|
|
20
|
+
# 至少画过一帧 + take_screenshot_async 内部循环(D)兜底动态 transient,
|
|
21
|
+
# 所以正常用法下 1006 不应触发。但它仍是 last-resort 合法信号 ——
|
|
22
|
+
# client 必须保留对 1006 的处理(不要假设它消失),未来若改为 fail-loud
|
|
23
|
+
# 会让 scene 切换瞬间的截图变成硬错。
|
|
19
24
|
const RESOURCE_UNAVAILABLE: int = 1006
|
|
20
25
|
|
|
21
26
|
const INVALID_PARAMS: int = -32602
|
{godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/addons/godot_cli_control/bridge/game_bridge.gd
RENAMED
|
@@ -6,6 +6,11 @@ const DEFAULT_PORT: int = 9877
|
|
|
6
6
|
const SETTING_AUTO_ENABLE: String = "godot_cli_control/auto_enable_in_debug"
|
|
7
7
|
const SETTING_OUTBOUND_BUFFER_MB: String = "godot_cli_control/outbound_buffer_mb"
|
|
8
8
|
const DEFAULT_OUTBOUND_BUFFER_MB: int = 10
|
|
9
|
+
# 启动 gate:listen 前等 viewport 首帧 ready 的上限帧数。
|
|
10
|
+
# 60fps 下 ~2s;shader 编译 / 大资源加载超过这个就走 fallback —— 仍开端口,
|
|
11
|
+
# 把后续 screenshot 的 transient 兜底交给 take_screenshot_async 的循环。
|
|
12
|
+
# 设计意图见 issue #61:根因消除(H)+ handler 内动态兜底(D)。
|
|
13
|
+
const FIRST_FRAME_READY_MAX_FRAMES: int = 120
|
|
9
14
|
|
|
10
15
|
var _tcp_server: TCPServer = TCPServer.new()
|
|
11
16
|
var _active_peer: WebSocketPeer = null
|
|
@@ -56,6 +61,13 @@ func _ready() -> void:
|
|
|
56
61
|
ProjectSettings.get_setting(SETTING_OUTBOUND_BUFFER_MB, DEFAULT_OUTBOUND_BUFFER_MB)
|
|
57
62
|
)
|
|
58
63
|
_outbound_buffer_size = max(1, mb) * 1024 * 1024
|
|
64
|
+
# 启动 gate:等 viewport 首帧 ready 后再开 listen。
|
|
65
|
+
# 根因:issue #61。screenshot 在「viewport 从未画过一次」时拿到 null
|
|
66
|
+
# texture → 报 1006 transient;让 client connect 上来这件事本身就承诺
|
|
67
|
+
# 「至少画过一帧」,把根因消除而不是各 handler 各自打补丁。
|
|
68
|
+
# 超时不阻塞 listen —— 端口冲突 / 启动诊断信号要保留(_tcp_server.listen
|
|
69
|
+
# 失败的 printerr 在端口冲突时是用户唯一的 root cause 提示)。
|
|
70
|
+
await _wait_first_frame_ready()
|
|
59
71
|
# 启动 TCP 服务器
|
|
60
72
|
_port = _parse_port_from_args()
|
|
61
73
|
# 安全:显式绑 127.0.0.1,避免 Godot TCPServer 默认 "*" 暴露到 LAN
|
|
@@ -81,6 +93,23 @@ func _ready() -> void:
|
|
|
81
93
|
print("GameBridge: idle-timeout %ds enabled" % _idle_timeout_secs)
|
|
82
94
|
|
|
83
95
|
|
|
96
|
+
func _wait_first_frame_ready() -> void:
|
|
97
|
+
# dummy renderer (--headless) 下 RenderingServer.frame_post_draw 永不发射;
|
|
98
|
+
# 与 take_screenshot_async 用同一套检测,dummy 路径走 process_frame ×2。
|
|
99
|
+
# 上限 FIRST_FRAME_READY_MAX_FRAMES 后无论 viewport 是否 ready 都返回,
|
|
100
|
+
# 让 listen 不被 GPU 卡死 / 大场景首帧拖住,避免 daemon 启动绑架到项目复杂度。
|
|
101
|
+
var dummy: bool = RenderingServer.get_rendering_device() == null
|
|
102
|
+
if dummy:
|
|
103
|
+
await get_tree().process_frame
|
|
104
|
+
await get_tree().process_frame
|
|
105
|
+
return
|
|
106
|
+
for _i in FIRST_FRAME_READY_MAX_FRAMES:
|
|
107
|
+
await RenderingServer.frame_post_draw
|
|
108
|
+
var image: Image = get_viewport().get_texture().get_image()
|
|
109
|
+
if image != null:
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
|
|
84
113
|
func _process(_delta: float) -> void:
|
|
85
114
|
if not _tcp_server.is_listening():
|
|
86
115
|
return
|
{godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/addons/godot_cli_control/bridge/low_level_api.gd
RENAMED
|
@@ -14,6 +14,10 @@ const _BUILD_TREE_HARD_LIMIT: int = 50
|
|
|
14
14
|
const _BUILD_TREE_NODE_LIMIT: int = 5000
|
|
15
15
|
# wait_game_time_async 防呆上限:防止误传 1e9 之类的数值挂死 session
|
|
16
16
|
const _MAX_WAIT_SECONDS: float = 3600.0
|
|
17
|
+
# take_screenshot_async 循环上限:常态下 GameBridge 启动 gate 已保证 viewport
|
|
18
|
+
# ready(issue #61 H 部分),这个循环只兜动态 transient(scene transition、
|
|
19
|
+
# 窗口 resize 一瞬)。30 帧 ~500ms @ 60fps,超时报 1006 给 client 兜底。
|
|
20
|
+
const SCREENSHOT_MAX_FRAMES: int = 30
|
|
17
21
|
|
|
18
22
|
# ProjectSettings 路径:第三方项目通过这两条额外补 ban 自家属性 / 方法。
|
|
19
23
|
# 合并到内置黑名单(去重,不能减只能加 —— 不开放 unban 是为了防止误删安全网)。
|
|
@@ -416,13 +420,25 @@ func take_screenshot_async() -> Dictionary:
|
|
|
416
420
|
# dummy renderer (--headless) 下 RenderingServer.frame_post_draw 永不发射,
|
|
417
421
|
# await 会永久挂死。RenderingServer.get_rendering_device() 在 dummy driver
|
|
418
422
|
# 下返回 null,用它检测后改走 process_frame 推进路径。
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
423
|
+
# windowed 下循环等到 ready 是 issue #61 的 D 部分:兜底动态 transient
|
|
424
|
+
# (scene 切换 / 窗口 resize 一瞬)。常态下 GameBridge._wait_first_frame_ready
|
|
425
|
+
# 已经保证 client 连上 = viewport 至少画过一帧(H),所以通常第一次就拿到 image。
|
|
426
|
+
# SCREENSHOT_MAX_FRAMES 后仍 null 才报 1006 —— 1006 是 last-resort 兜底,
|
|
427
|
+
# 仍是合法 transient(client 仍应处理)。
|
|
428
|
+
# dummy 路径只试一次:headless 下 viewport texture 永远拿不到 image(无真 GPU),
|
|
429
|
+
# 循环 N 次只会让 Godot 内部 "Parameter t is null" push_error 噪音放大 N 倍。
|
|
430
|
+
var dummy: bool = RenderingServer.get_rendering_device() == null
|
|
431
|
+
var max_iters: int = 1 if dummy else SCREENSHOT_MAX_FRAMES
|
|
432
|
+
var image: Image = null
|
|
433
|
+
for _i in max_iters:
|
|
434
|
+
if dummy:
|
|
435
|
+
await get_tree().process_frame
|
|
436
|
+
await get_tree().process_frame
|
|
437
|
+
else:
|
|
438
|
+
await RenderingServer.frame_post_draw
|
|
439
|
+
image = get_viewport().get_texture().get_image()
|
|
440
|
+
if image != null:
|
|
441
|
+
break
|
|
426
442
|
if image == null:
|
|
427
443
|
# 1006 (transient) ≠ 1003 (schema):agent 可短重试,等下一帧 viewport 就绪。
|
|
428
444
|
return _err(CliControlErrorCodes.RESOURCE_UNAVAILABLE, "Screenshot unavailable (viewport texture is null)")
|
|
@@ -405,3 +405,26 @@ func test_check_idle_resets_activity_when_busy() -> void:
|
|
|
405
405
|
_bridge._check_idle()
|
|
406
406
|
var now: int = Time.get_ticks_msec()
|
|
407
407
|
assert_almost_eq(_bridge._last_activity_ms, now, 200, "busy 时 _check_idle 必须把活动戳推到 ~now")
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
# ── 启动 gate (issue #61 H 部分) ─────────────────────────────────
|
|
411
|
+
# GUT 跑 --headless → RenderingServer.get_rendering_device() == null → dummy 路径。
|
|
412
|
+
# 真 windowed 路径无法在 GUT 内测(要真 GPU),靠 e2e 覆盖。
|
|
413
|
+
|
|
414
|
+
func test_wait_first_frame_ready_returns_under_headless() -> void:
|
|
415
|
+
# 关键防回归:dummy 路径不能 await frame_post_draw(永不发射 → 死锁)。
|
|
416
|
+
# 用 process_frame 推两帧应在毫秒级返回,否则函数被误改成走 windowed 分支。
|
|
417
|
+
# 必须 add_child 进 tree,否则 await get_tree().process_frame 拿不到 tree。
|
|
418
|
+
if _bridge.is_inside_tree():
|
|
419
|
+
_bridge.get_parent().remove_child(_bridge)
|
|
420
|
+
add_child_autofree(_bridge)
|
|
421
|
+
var t0: int = Time.get_ticks_msec()
|
|
422
|
+
await _bridge._wait_first_frame_ready()
|
|
423
|
+
var elapsed_ms: int = Time.get_ticks_msec() - t0
|
|
424
|
+
# 2s 余量,正常应在 < 100ms 返回
|
|
425
|
+
assert_lt(elapsed_ms, 2000, "_wait_first_frame_ready 在 headless 下必须秒返不死锁")
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
func test_first_frame_ready_max_frames_constant_is_positive() -> void:
|
|
429
|
+
# 防回归:常量被改成 0 或负数会让循环立刻退出 → 启动 gate 失效。
|
|
430
|
+
assert_gt(GameBridgeScript.FIRST_FRAME_READY_MAX_FRAMES, 0)
|
|
@@ -694,3 +694,26 @@ func test_node_exists_false_for_missing_path() -> void:
|
|
|
694
694
|
"path": "/root/__missing__",
|
|
695
695
|
})
|
|
696
696
|
assert_false(result.get("exists"))
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
# ── take_screenshot_async(issue #61 D 部分)──────────────────────
|
|
700
|
+
# GUT 跑在 --headless 下,RenderingServer.get_rendering_device() == null,
|
|
701
|
+
# 走 dummy 推帧路径。这两条测试主要防 await 死锁回归(早期版本一次没等
|
|
702
|
+
# 到 ready 就报 1006,绕过 H 启动 gate 后会再撞)。
|
|
703
|
+
|
|
704
|
+
func test_screenshot_returns_image_or_1006_under_headless() -> void:
|
|
705
|
+
# headless 下 viewport texture 可能是 null(dummy 无真实 RenderingDevice)。
|
|
706
|
+
# 合法结果:要么拿到 base64 image,要么循环跑满后报 1006。
|
|
707
|
+
# 关键不变量:函数必须返回(不死锁)、不抛、payload 形状正确。
|
|
708
|
+
var result: Dictionary = await _api.take_screenshot_async()
|
|
709
|
+
assert_true(result.has("image") or result.has("error"),
|
|
710
|
+
"take_screenshot_async must return image or error envelope")
|
|
711
|
+
if result.has("error"):
|
|
712
|
+
var err: Dictionary = result["error"] as Dictionary
|
|
713
|
+
assert_eq(int(err.get("code")), 1006,
|
|
714
|
+
"headless null viewport must report RESOURCE_UNAVAILABLE not other code")
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
func test_screenshot_max_frames_constant_is_positive() -> void:
|
|
718
|
+
# 防回归:常量被改成 0 或负数会让循环立刻退出 → 等同未修。
|
|
719
|
+
assert_gt(LowLevelApiScript.SCREENSHOT_MAX_FRAMES, 0)
|
|
@@ -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.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 2,
|
|
21
|
+
__version__ = version = '0.2.7'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 7)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -670,7 +670,7 @@ RPC_SPECS: tuple[RpcSpec, ...] = (
|
|
|
670
670
|
handler=cmd_wait_time,
|
|
671
671
|
description="按 game time 等待 N 秒(在 --write-movie 模式下与录像帧对齐)。",
|
|
672
672
|
positionals=(
|
|
673
|
-
Positional("seconds", None, "
|
|
673
|
+
Positional("seconds", None, "等待秒数(服务端范围 0 ≤ seconds ≤ 3600;client 在 ≤0 时短路返回成功)"),
|
|
674
674
|
),
|
|
675
675
|
example="wait-time 0.5",
|
|
676
676
|
text_formatter=_fmt_wait_time_text,
|
|
@@ -42,7 +42,8 @@ godot-cli-control daemon stop
|
|
|
42
42
|
|---|---|
|
|
43
43
|
| 0 | Success (or, for `exists` / `visible` / `wait-node`, the boolean was true / found) |
|
|
44
44
|
| 1 | RPC error (server returned `{"error":...}`); also `exists`/`visible`=false, `wait-node`=timeout, `daemon status`=stopped |
|
|
45
|
-
| 2 | Connection / IO / usage error (daemon not running, malformed `combo` input,
|
|
45
|
+
| 2 | Connection / IO / usage error (daemon not running, malformed `combo` input, script path not found). Also: **`daemon stop` returns 2** when the daemon stopped cleanly but `ffmpeg` transcode of the recorded `.avi`→`.mp4` failed — the raw `.avi` is kept and `.cli_control/ffmpeg.log` has the details. `run <script>` propagates this: a successful script + failed transcode still exits 2. |
|
|
46
|
+
| 3 | `daemon stop --all` partial failure: at least one daemon in the registry failed to stop. Per-record `rc` is in the JSON `result.stopped[]`. |
|
|
46
47
|
| 64 | Argparse usage error |
|
|
47
48
|
|
|
48
49
|
Shell-`if` works:
|
|
@@ -53,6 +54,22 @@ if godot-cli-control exists /root/Main/Boss; then
|
|
|
53
54
|
fi
|
|
54
55
|
```
|
|
55
56
|
|
|
57
|
+
## Daemon management
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
godot-cli-control daemon start # boot daemon for cwd project
|
|
61
|
+
godot-cli-control daemon status # exit 0 = running, 1 = stopped
|
|
62
|
+
godot-cli-control daemon stop # stop cwd-project daemon (rc 0; rc 2 = ffmpeg transcode failed)
|
|
63
|
+
godot-cli-control daemon stop --project /path/to/other/godot/project
|
|
64
|
+
godot-cli-control daemon stop --all # stop every registered daemon; exit 3 if any failed
|
|
65
|
+
godot-cli-control daemon ls # list all running daemons (cross-project, walks the registry)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
- **`daemon status` payload when running**: `{"state": "running", "pid": N, "port": M}`.
|
|
69
|
+
- **`daemon status` payload when stopped**: `{"state": "stopped"}`. If the previous launch wrote `.cli_control/godot.log` or recorded an exit code, the envelope also includes `"last_log": "<path>"` and/or `"last_exit_code": <int>` — use these to diagnose why the daemon died without manually grepping under `.cli_control/`.
|
|
70
|
+
- **`daemon ls` payload**: `{"daemons": [{"project_root", "pid", "port", "started_at", "godot_bin", "log_path"}, ...]}`. Dead records (PID gone) are auto-pruned on each call, so this is the canonical list of *actually-alive* daemons across all projects on the machine.
|
|
71
|
+
- **`daemon stop --all` payload**: `{"stopped": [{"project_root","pid","port","rc"[, "error"]}, ...], "rc": 0|3}`. Each entry's `rc` is the per-project stop result; the top-level `rc` is the aggregate exit code.
|
|
72
|
+
|
|
56
73
|
## JSON envelope examples
|
|
57
74
|
|
|
58
75
|
```bash
|
|
@@ -90,10 +107,10 @@ Three numeric ranges cohabit in `error.code`. Knowing which is which lets you de
|
|
|
90
107
|
|---|---|
|
|
91
108
|
| `1001` | Node not found at the given path. Most common — usually the agent passed a wrong / not-yet-loaded path. Retry after `wait-node`. |
|
|
92
109
|
| `1002` | Property not found on the node, or shape mismatch (e.g. `text` on a node that doesn't have it). Don't retry; inspect with `tree`. |
|
|
93
|
-
| `1003` | Method not found on the node. Schema error — don't retry
|
|
110
|
+
| `1003` | Method not found on the node, **or** unknown InputMap action passed to `press`/`release`/`tap`/`hold`/`combo` (`"Unknown action: <name>"`). Schema error — don't retry. For node methods inspect with `tree`; for missing actions run `actions` (or `actions --all`). |
|
|
94
111
|
| `1004` | Combo already in progress. Call `combo-cancel` (or `release-all`) and re-issue. Safe to retry after that. |
|
|
95
112
|
| `1005` | Scene tree too large to serialize (default safety limit). Pass `--max-nodes` or query a subtree with `children` / `tree <subpath>`. Don't retry as-is. |
|
|
96
|
-
| `1006` | Resource transiently unavailable (e.g. screenshot
|
|
113
|
+
| `1006` | Resource transiently unavailable (e.g. screenshot during scene transition / window resize). Rare under normal use: GameBridge waits for viewport first-frame before accepting connections, and `screenshot` retries internally up to ~30 frames (~500ms at 60 fps, ~1s at 30 fps, longer when `--write-movie` lowers the fixed fps). If you still see this, retry after `wait-time 0.05` or similar. |
|
|
97
114
|
|
|
98
115
|
**JSON-RPC standard — negative integers `-32xxx`:**
|
|
99
116
|
|
|
@@ -143,7 +160,7 @@ Server vs client ranges never overlap, so a single `code` field is unambiguous.
|
|
|
143
160
|
|
|
144
161
|
**Wait:**
|
|
145
162
|
- `wait-node <path> [timeout]` — block until node appears (exit 0=found, 1=timeout)
|
|
146
|
-
- `wait-time <seconds>` — wait N in-game seconds (matters for `--write-movie`)
|
|
163
|
+
- `wait-time <seconds>` — wait N in-game seconds (matters for `--write-movie`). Server bounds: `0 ≤ seconds ≤ 3600`; passing out-of-range gets `-32602 "seconds must be ..."`. Client short-circuits `seconds <= 0` without an RPC.
|
|
147
164
|
|
|
148
165
|
**Render:**
|
|
149
166
|
- `screenshot <path>` — write PNG (path is **required** as of 0.2.0)
|
|
@@ -190,7 +207,7 @@ godot-cli-control call /root/Game start_game 1 '"easy"' # int 1, string "easy
|
|
|
190
207
|
|
|
191
208
|
So `position '[100, 200]'` → `Vector2(100, 200)`, `transform '[1,0,0, 0,1,0, 0,0,1, 10,20,30]'` → `Transform3D(IDENTITY, (10,20,30))`. Wrong length or non-numeric elements fail loud with `-32602 "value type mismatch ..."` instead of silently setting `(0, 0)` like pre-0.2.5 versions did.
|
|
192
209
|
|
|
193
|
-
**Sub-path + Array also fails loud.** `set <node> transform:origin '[10, 20, 30]'` is rejected with `-32602 "sub-path + Array is not supported"`: Godot's `Object.set("transform:origin", Array)` silently drops the Array (origin stays at `(0,0,0)`) — same footgun
|
|
210
|
+
**Sub-path + Array also fails loud.** `set <node> transform:origin '[10, 20, 30]'` is rejected with `-32602 "sub-path + Array is not supported"`: Godot's `Object.set("transform:origin", Array)` silently drops the Array (origin stays at `(0,0,0)`) — same class of footgun as the strict Variant checks above, so the server pre-empts it. Sub-paths are scalar-only (`set <node> position:x 1.8`); to write a whole compound Variant use the top-level Array form above.
|
|
194
211
|
|
|
195
212
|
**Footgun**: bare `null` / `true` / `false` / numeric strings parse as JSON literals first, **not** as strings. If you actually mean the string `"null"`, wrap it explicitly:
|
|
196
213
|
|
|
@@ -294,7 +311,7 @@ Errors raise `RpcError(code, message)` (a `RuntimeError` subclass) that preserve
|
|
|
294
311
|
| `await client.is_visible(path)` | `visible <path>` |
|
|
295
312
|
| `await client.get_children(path)` | `children <path>` |
|
|
296
313
|
| `await client.screenshot()` | `screenshot <path>` |
|
|
297
|
-
| `await client.get_scene_tree(depth)` | `tree [depth]` |
|
|
314
|
+
| `await client.get_scene_tree(depth, max_nodes=None)` | `tree [depth] [--max-nodes N]` |
|
|
298
315
|
| `await client.wait_for_node(path, timeout)` | `wait-node <path> [timeout]` |
|
|
299
316
|
| `await client.wait_game_time(seconds)` | `wait-time <seconds>` |
|
|
300
317
|
| `await client.action_press(action)` | `press <action>` |
|
|
@@ -321,6 +338,12 @@ def run(bridge):
|
|
|
321
338
|
|
|
322
339
|
`bridge` is a synchronous wrapper around `GameClient` — same method names, no `await`. Sibling-imports work (the script's directory is on `sys.path`).
|
|
323
340
|
|
|
341
|
+
**Exit code (when `run` started the daemon itself):**
|
|
342
|
+
- `0` — script succeeded and daemon stopped cleanly.
|
|
343
|
+
- `1` — script raised (envelope carries `code: -1005` with the exception summary; full traceback on stderr).
|
|
344
|
+
- `2` — script-path / daemon-start failed, **or** the script succeeded but the auto-`daemon stop` afterwards hit an ffmpeg transcode failure (success envelope still emits, with `daemon_stop_warning` populated; raw `.avi` is preserved).
|
|
345
|
+
- `64` — argparse usage error (e.g. malformed `--idle-timeout`).
|
|
346
|
+
|
|
324
347
|
## pytest plugin (preferred for end-to-end test suites)
|
|
325
348
|
|
|
326
349
|
`pip install godot-cli-control[pytest]` registers a `pytest11` entry-point that exposes two fixtures, so a Godot e2e test is a one-liner:
|
|
@@ -367,6 +390,7 @@ pytest_plugins = ["godot_cli_control.pytest_plugin"]
|
|
|
367
390
|
- **`tree` returns `1005 "scene tree too large"`** — your scene has more than 5000 visible nodes (a Grid / spawned-bullets situation). Pass `--max-nodes 200` to cap, or `children <path>` for one specific subtree.
|
|
368
391
|
- **`set` with a string that *looks* like JSON** — value parser parses JSON first. To force a literal `"42"` string, pass `'"42"'`; to set a literal hash sign or array text, JSON-encode it.
|
|
369
392
|
- **`daemon start` opens a window when I expected headless** — your stdout is a TTY (interactive terminal). Pass `--headless` explicitly, or shell out from a context where stdout is piped.
|
|
393
|
+
- **`screenshot` used to fail with `1006` on the first call** — fixed. GameBridge now waits for the viewport's first frame before opening the port, so `connect succeeded` implies `viewport has rendered ≥ once`. The magic `bridge.wait(1.5)` before the first screenshot in older example scripts is no longer needed.
|
|
370
394
|
|
|
371
395
|
---
|
|
372
396
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/addons/godot_cli_control/bin/run_cli_control.ps1
RENAMED
|
File without changes
|
{godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/addons/godot_cli_control/bin/run_cli_control.sh
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/addons/godot_cli_control/tests/run_gut.sh
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/python/godot_cli_control/pytest_plugin.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/python/godot_cli_control/skills_install.py
RENAMED
|
File without changes
|
{godot_cli_control-0.2.5 → godot_cli_control-0.2.7}/python/godot_cli_control/templates/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|