godot-cli-control 0.2.3__tar.gz → 0.2.5__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.3 → godot_cli_control-0.2.5}/.gitignore +2 -0
- {godot_cli_control-0.2.3 → godot_cli_control-0.2.5}/PKG-INFO +7 -5
- {godot_cli_control-0.2.3 → godot_cli_control-0.2.5}/addons/godot_cli_control/CHANGELOG.md +26 -0
- {godot_cli_control-0.2.3 → godot_cli_control-0.2.5}/addons/godot_cli_control/README.md +23 -1
- godot_cli_control-0.2.5/addons/godot_cli_control/bridge/error_codes.gd +23 -0
- {godot_cli_control-0.2.3 → godot_cli_control-0.2.5}/addons/godot_cli_control/bridge/game_bridge.gd +71 -8
- {godot_cli_control-0.2.3 → godot_cli_control-0.2.5}/addons/godot_cli_control/bridge/input_simulation_api.gd +16 -12
- godot_cli_control-0.2.5/addons/godot_cli_control/bridge/low_level_api.gd +490 -0
- {godot_cli_control-0.2.3 → godot_cli_control-0.2.5}/addons/godot_cli_control/tests/gut/test_game_bridge.gd +61 -0
- godot_cli_control-0.2.5/addons/godot_cli_control/tests/gut/test_low_level_api.gd +696 -0
- {godot_cli_control-0.2.3 → godot_cli_control-0.2.5}/python/README.md +6 -4
- godot_cli_control-0.2.5/python/godot_cli_control/_duration.py +18 -0
- {godot_cli_control-0.2.3 → godot_cli_control-0.2.5}/python/godot_cli_control/_version.py +2 -2
- {godot_cli_control-0.2.3 → godot_cli_control-0.2.5}/python/godot_cli_control/bridge.py +14 -2
- {godot_cli_control-0.2.3 → godot_cli_control-0.2.5}/python/godot_cli_control/cli.py +546 -106
- {godot_cli_control-0.2.3 → godot_cli_control-0.2.5}/python/godot_cli_control/client.py +13 -2
- {godot_cli_control-0.2.3 → godot_cli_control-0.2.5}/python/godot_cli_control/daemon.py +216 -13
- {godot_cli_control-0.2.3 → godot_cli_control-0.2.5}/python/godot_cli_control/init_cmd.py +74 -27
- {godot_cli_control-0.2.3 → godot_cli_control-0.2.5}/python/godot_cli_control/pytest_plugin.py +3 -3
- godot_cli_control-0.2.5/python/godot_cli_control/registry.py +135 -0
- {godot_cli_control-0.2.3 → godot_cli_control-0.2.5}/python/godot_cli_control/templates/skill/SKILL.md +78 -6
- {godot_cli_control-0.2.3 → godot_cli_control-0.2.5}/python/tests/test_bridge.py +47 -2
- {godot_cli_control-0.2.3 → godot_cli_control-0.2.5}/python/tests/test_cli.py +832 -13
- {godot_cli_control-0.2.3 → godot_cli_control-0.2.5}/python/tests/test_cli_helpers.py +5 -3
- {godot_cli_control-0.2.3 → godot_cli_control-0.2.5}/python/tests/test_client.py +49 -0
- {godot_cli_control-0.2.3 → godot_cli_control-0.2.5}/python/tests/test_daemon.py +535 -1
- godot_cli_control-0.2.5/python/tests/test_duration.py +36 -0
- {godot_cli_control-0.2.3 → godot_cli_control-0.2.5}/python/tests/test_init.py +121 -0
- {godot_cli_control-0.2.3 → godot_cli_control-0.2.5}/python/tests/test_pytest_plugin.py +55 -1
- godot_cli_control-0.2.5/python/tests/test_registry.py +74 -0
- godot_cli_control-0.2.3/addons/godot_cli_control/bridge/low_level_api.gd +0 -280
- godot_cli_control-0.2.3/addons/godot_cli_control/tests/gut/test_low_level_api.gd +0 -194
- {godot_cli_control-0.2.3 → godot_cli_control-0.2.5}/LICENSE +0 -0
- {godot_cli_control-0.2.3 → godot_cli_control-0.2.5}/addons/godot_cli_control/LICENSE +0 -0
- {godot_cli_control-0.2.3 → godot_cli_control-0.2.5}/addons/godot_cli_control/bin/run_cli_control.ps1 +0 -0
- {godot_cli_control-0.2.3 → godot_cli_control-0.2.5}/addons/godot_cli_control/bin/run_cli_control.sh +0 -0
- {godot_cli_control-0.2.3 → godot_cli_control-0.2.5}/addons/godot_cli_control/bridge/game_bridge.gd.uid +0 -0
- {godot_cli_control-0.2.3 → godot_cli_control-0.2.5}/addons/godot_cli_control/bridge/input_simulation_api.gd.uid +0 -0
- {godot_cli_control-0.2.3 → godot_cli_control-0.2.5}/addons/godot_cli_control/bridge/low_level_api.gd.uid +0 -0
- {godot_cli_control-0.2.3 → godot_cli_control-0.2.5}/addons/godot_cli_control/plugin.cfg +0 -0
- {godot_cli_control-0.2.3 → godot_cli_control-0.2.5}/addons/godot_cli_control/plugin.gd +0 -0
- {godot_cli_control-0.2.3 → godot_cli_control-0.2.5}/addons/godot_cli_control/plugin.gd.uid +0 -0
- {godot_cli_control-0.2.3 → godot_cli_control-0.2.5}/addons/godot_cli_control/tests/gut/test_input_simulation_api.gd +0 -0
- {godot_cli_control-0.2.3 → godot_cli_control-0.2.5}/addons/godot_cli_control/tests/run_gut.sh +0 -0
- {godot_cli_control-0.2.3 → godot_cli_control-0.2.5}/addons/godot_cli_control/tests/test_init_walkthrough.py +0 -0
- {godot_cli_control-0.2.3 → godot_cli_control-0.2.5}/addons/godot_cli_control/tests/test_walkthrough.sh +0 -0
- {godot_cli_control-0.2.3 → godot_cli_control-0.2.5}/pyproject.toml +0 -0
- {godot_cli_control-0.2.3 → godot_cli_control-0.2.5}/python/godot_cli_control/__init__.py +0 -0
- {godot_cli_control-0.2.3 → godot_cli_control-0.2.5}/python/godot_cli_control/__main__.py +0 -0
- {godot_cli_control-0.2.3 → godot_cli_control-0.2.5}/python/godot_cli_control/runner.py +0 -0
- {godot_cli_control-0.2.3 → godot_cli_control-0.2.5}/python/godot_cli_control/skills_install.py +0 -0
- {godot_cli_control-0.2.3 → godot_cli_control-0.2.5}/python/godot_cli_control/templates/__init__.py +0 -0
- {godot_cli_control-0.2.3 → godot_cli_control-0.2.5}/python/godot_cli_control/templates/skill/__init__.py +0 -0
- {godot_cli_control-0.2.3 → godot_cli_control-0.2.5}/python/tests/__init__.py +0 -0
- {godot_cli_control-0.2.3 → godot_cli_control-0.2.5}/python/tests/test_runner.py +0 -0
- {godot_cli_control-0.2.3 → godot_cli_control-0.2.5}/python/tests/test_skills_install.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: godot-cli-control
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.5
|
|
4
4
|
Summary: WebSocket bridge for headless / scripted control of Godot scenes.
|
|
5
5
|
Author: kesar
|
|
6
6
|
License: MIT
|
|
@@ -53,7 +53,8 @@ import asyncio
|
|
|
53
53
|
from godot_cli_control import GameClient
|
|
54
54
|
|
|
55
55
|
async def main():
|
|
56
|
-
|
|
56
|
+
# Omitting port lets GameClient auto-discover from .cli_control/port (written by daemon start)
|
|
57
|
+
async with GameClient() as client:
|
|
57
58
|
tree = await client.get_scene_tree(depth=3)
|
|
58
59
|
await client.click("/root/MyScene/Button")
|
|
59
60
|
await client.action_press("jump")
|
|
@@ -101,7 +102,7 @@ def test_jump(godot_daemon, bridge):
|
|
|
101
102
|
CLI options:
|
|
102
103
|
|
|
103
104
|
```
|
|
104
|
-
--godot-cli-port=N # GameBridge port (default
|
|
105
|
+
--godot-cli-port=N # GameBridge port (default: read from .cli_control/port)
|
|
105
106
|
--godot-cli-no-headless # open a real Godot window
|
|
106
107
|
--godot-cli-project-root=DIR # default: pytest rootdir
|
|
107
108
|
```
|
|
@@ -113,9 +114,10 @@ The CLI is the canonical surface — every `GameClient` method has a one-line eq
|
|
|
113
114
|
```bash
|
|
114
115
|
# Lifecycle
|
|
115
116
|
godot-cli-control init [--path DIR] [--force]
|
|
116
|
-
godot-cli-control daemon start [--headless --record --movie-path X --fps N --port N]
|
|
117
|
-
godot-cli-control daemon stop
|
|
117
|
+
godot-cli-control daemon start [--headless --record --movie-path X --fps N --port N --idle-timeout 30m]
|
|
118
|
+
godot-cli-control daemon stop [--all | --project PATH]
|
|
118
119
|
godot-cli-control daemon status
|
|
120
|
+
godot-cli-control daemon ls # list running daemons across all projects
|
|
119
121
|
godot-cli-control run <script.py> [--headless ...]
|
|
120
122
|
|
|
121
123
|
# Read
|
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
### Fixed
|
|
6
|
+
- **#52 `set` 走 JSON Array 喂 Vector/Color/Rect 时静默失败**:`set zoom '[1.8, 1.8]'` 等价于 `node.set("zoom", [1.8, 1.8])`,Godot 隐式构造失败 → 实际值是 `Vector2(0,0)` 或被 clamp 到 `0.00001`,但服务端仍返 `{success: true}`。`handle_set_property` 现在查 `get_property_list()` 拿声明类型,把 numeric Array 转成对应 Variant。长度不匹配或元素非数字时 fail-loud 返 `-32602 "value type mismatch ..."`,不再 silent corruption。
|
|
7
|
+
- **sub-path 标量赋值现在真的会写入**:之前 `set <node> position:x 1.8` 调的是 `Object.set("position:x", 1.8)`,Godot 4 的 `Object.set` 把整串当字面属性名找不到就 silent no-op(依旧返 `{success: true}` 但 `position.x` 不变)。改用 `Object.set_indexed(NodePath, value)` 才会按 sub-path 写入。是 #54 review 阶段被新加的 `test_set_subpath_scalar_still_works` 捕获的隐藏 silent-fail。
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **#54 全部复合 Variant 都支持 Array → Variant coerce**:从 4-float 简单类型到 16-float 矩阵,全部按 axis-vector 顺序的 flat numeric Array 写入(每 N 元素 = 一个 Vector 轴)。覆盖:`Vector2/2i/3/3i/4/4i`、`Rect2/2i`、`Color`(3-element=RGB / 4-element=RGBA)、`Plane(a,b,c,d)`、`Quaternion(x,y,z,w)`、`AABB(pos 3, size 3)`、`Basis(9 axis-vector)`、`Transform2D(xaxis 2, yaxis 2, origin 2)`、`Transform3D(basis 9, origin 3)`、`Projection(16 axis-vector)`。具体 layout 见 SKILL.md。`Plane.normal` 和 `Quaternion` 不会自动归一化(与 Godot ctor 一致)。
|
|
11
|
+
- **#54 防御性 fallback**:未在 coerce 名单也不在 `_ARRAY_PASSTHROUGH_SAFE_TYPES`(基本类型 / Object / 集合 / Packed*Array)白名单的声明类型 + Array 输入会 fail-loud,避免未来 Godot 加新 compound Variant 时 silent-corrupt 回归。
|
|
12
|
+
- **#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`)不变。
|
|
13
|
+
|
|
5
14
|
### AI-friendly CLI 改造(多个 BREAKING change)
|
|
6
15
|
|
|
7
16
|
把 CLI 重定位成 AI agent 的一等接口:默认结构化输出、补齐读 / 写 / 发现的 shell 命令、明确退出码契约。shell-only 的 agent 现在不需要写 Python 脚本就能完成全部操作。
|
|
@@ -44,6 +53,23 @@
|
|
|
44
53
|
#### Out of scope (intentional)
|
|
45
54
|
- `run <script.py>` 子命令保留旧的人类可读 stderr 输出。它是交互式脚本宿主,不是 RPC,新的 JSON 信封契约只覆盖 RPC 子命令 + daemon 三命令。
|
|
46
55
|
|
|
56
|
+
### AI-friendliness review fixes (2026-05-11)
|
|
57
|
+
|
|
58
|
+
#### Fixed
|
|
59
|
+
- **CLI flag position**: `--json` / `--text` / `--no-json` 现在在 RPC 子命令尾部也接受(之前只能写最前面)。argparse 子 parser 用 `default=argparse.SUPPRESS` + 顶层 `set_defaults` 兜底,避免子 parser 默认值覆盖父 parser 解析结果。
|
|
60
|
+
- **Error code 1004 collision**: `low_level_api.gd` 的 scene tree 超限改用新业务码 `1005 "scene tree too large"`,与 input_simulation `1004 "combo in progress"` 解耦。新增 `error_codes.gd` 集中常量。
|
|
61
|
+
- **pytest fixture default port**: `--godot-cli-port` 默认从 9877 改为 0(OS-assigned),与 `daemon start` 默认对齐;多项目并行测试不再撞端口。
|
|
62
|
+
- **Addon README error-code table**: 之前只列到 1003,补全 1004 / 1005 / 客户端 -1xxx 段。
|
|
63
|
+
- **Scene tree hard limit bypass (DoS fix)**: `handle_get_scene_tree` 入口现在把 `max_nodes` clamp 到硬墙 `_BUILD_TREE_NODE_LIMIT` (5000)。修复前客户端传 `max_nodes=999999` 会让服务端先把整棵超大树构造成 Dictionary 再被 1005 错误丢弃(内存浪费 / OOM 路径)。
|
|
64
|
+
- **Error code 1003 semantic split**: screenshot 在 viewport texture 为 null 时不再借用 `1003 METHOD_NOT_FOUND`,改用新业务码 `1006 RESOURCE_UNAVAILABLE`。1003 现在是纯 schema 错(不应 retry),1006 是 transient 错(短重试可能成功),agent 据此分别处置。
|
|
65
|
+
|
|
66
|
+
#### Added
|
|
67
|
+
- `tree --max-nodes <N>`(默认 200):节点数软上限;超出时响应含 `truncated: true` + `total_nodes`,agent 据此决定分子树。硬墙仍是 5000 节点 → `1005`。
|
|
68
|
+
- `set` / `call --text-value`:禁用 JSON 解析、把 value/args 强制按字符串处理,避开 `null` / `true` / `42` 这类字面量被解析成 Variant 类型的 footgun。
|
|
69
|
+
|
|
70
|
+
#### Changed
|
|
71
|
+
- **BREAKING (轻微)**:`daemon start` / `run` 默认 headless 行为改为基于 `sys.stdout.isatty()` 自动判定 —— pipe / CI / agent shell 默认 headless;交互终端默认开窗。新增 `--gui` 强制开窗 flag。`--headless` 仍可显式传,覆盖自动判。脚本里依赖 "默认会开窗" 的需要加 `--gui`。
|
|
72
|
+
|
|
47
73
|
## [0.1.6] - Unreleased
|
|
48
74
|
|
|
49
75
|
### Added
|
|
@@ -99,7 +99,29 @@ All methods callable via `godot-cli-control <method>` or `from godot_cli_control
|
|
|
99
99
|
| `combo_cancel()` | `await client.combo_cancel()` |
|
|
100
100
|
| `release_all()` | `await client.release_all()` |
|
|
101
101
|
|
|
102
|
-
Error codes
|
|
102
|
+
### Error codes
|
|
103
|
+
|
|
104
|
+
Three numeric ranges share `error.code`; they never overlap, so a single field is unambiguous.
|
|
105
|
+
|
|
106
|
+
| Code | Source | Meaning |
|
|
107
|
+
|---|---|---|
|
|
108
|
+
| `1001` | server | Node not found at the given path |
|
|
109
|
+
| `1002` | server | Property not found / shape mismatch |
|
|
110
|
+
| `1003` | server | Method not found on the node (schema error, don't retry) |
|
|
111
|
+
| `1004` | server | Combo already in progress (call `combo-cancel` to retry) |
|
|
112
|
+
| `1005` | server | Scene tree too large (lower `depth` or pass `--max-nodes`) |
|
|
113
|
+
| `1006` | server | Resource transiently unavailable (e.g. screenshot before viewport ready) — safe to retry |
|
|
114
|
+
| `-32600` | server | Malformed JSON-RPC request |
|
|
115
|
+
| `-32601` | server | Unknown method name |
|
|
116
|
+
| `-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) |
|
|
117
|
+
| `-1001` | client | Connection failure (daemon not running, port wrong, proxy hijacking localhost) |
|
|
118
|
+
| `-1002` | client | Timeout waiting for response |
|
|
119
|
+
| `-1003` | client | CLI usage error (combo missing steps, malformed `--steps-json`, …) |
|
|
120
|
+
| `-1004` | client | Local file IO error (e.g. screenshot can't write the destination) |
|
|
121
|
+
| `-1005` | client | `run <script>` user script raised an uncaught exception — fix the script |
|
|
122
|
+
| `-1099` | client | Internal CLI bug — please file an issue |
|
|
123
|
+
|
|
124
|
+
For full retry guidance see the SKILL.md shipped by `godot-cli-control init` (`.claude/skills/godot-cli-control/SKILL.md` in the target project).
|
|
103
125
|
|
|
104
126
|
## Activation Modes
|
|
105
127
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
class_name CliControlErrorCodes
|
|
2
|
+
extends RefCounted
|
|
3
|
+
## 集中错误码常量。新加业务码必须在这里登记,
|
|
4
|
+
## 避免 1004 那种隐式撞码(input_sim 用 "combo in progress",
|
|
5
|
+
## low_level 又用 "scene tree too large")。
|
|
6
|
+
##
|
|
7
|
+
## 三段制(详见 SKILL.md 错误码表):
|
|
8
|
+
## 1xxx 服务端业务码
|
|
9
|
+
## -32xxx JSON-RPC 标准
|
|
10
|
+
## -1xxx 客户端(Python)侧;GDScript 这边不会产出
|
|
11
|
+
|
|
12
|
+
const NODE_NOT_FOUND: int = 1001
|
|
13
|
+
const PROPERTY_NOT_FOUND: int = 1002 # 也用于 "node has no 'text' property"
|
|
14
|
+
const METHOD_NOT_FOUND: int = 1003
|
|
15
|
+
const COMBO_IN_PROGRESS: int = 1004
|
|
16
|
+
const SCENE_TREE_TOO_LARGE: int = 1005
|
|
17
|
+
# 资源 transient 不可用(screenshot viewport texture null 等)。
|
|
18
|
+
# 与 1003 拆开:1003 是 schema 错(永久),1006 是时机错(短重试可能成功)。
|
|
19
|
+
const RESOURCE_UNAVAILABLE: int = 1006
|
|
20
|
+
|
|
21
|
+
const INVALID_PARAMS: int = -32602
|
|
22
|
+
const INVALID_REQUEST: int = -32600
|
|
23
|
+
const METHOD_UNKNOWN: int = -32601
|
{godot_cli_control-0.2.3 → godot_cli_control-0.2.5}/addons/godot_cli_control/bridge/game_bridge.gd
RENAMED
|
@@ -11,6 +11,14 @@ var _tcp_server: TCPServer = TCPServer.new()
|
|
|
11
11
|
var _active_peer: WebSocketPeer = null
|
|
12
12
|
var _active_stream: StreamPeerTCP = null
|
|
13
13
|
var _port: int = DEFAULT_PORT
|
|
14
|
+
var _idle_timeout_secs: int = 0
|
|
15
|
+
var _last_activity_ms: int = 0
|
|
16
|
+
# 正在处理中的请求数。> 0 表示 daemon 没闲着,idle 检查必须放过 —— 否则
|
|
17
|
+
# 一条 wait_game_time(3600) 会被 30m idle-timeout 半路打断,客户端拿不到响应。
|
|
18
|
+
# 入:消息通过参数校验、即将派发到 handler 时 +1。
|
|
19
|
+
# 出:_dispatch_result(sync/async/async_with_id 三条路径的最终响应点)-1。
|
|
20
|
+
# 校验失败的 _send_error 不计数(没进过 handler)。
|
|
21
|
+
var _in_flight: int = 0
|
|
14
22
|
var _outbound_buffer_size: int = DEFAULT_OUTBOUND_BUFFER_MB * 1024 * 1024
|
|
15
23
|
var _low_level_api: LowLevelApi = null
|
|
16
24
|
var _input_sim_api: InputSimulationApi = null
|
|
@@ -23,7 +31,12 @@ var _methods: Dictionary = {}
|
|
|
23
31
|
|
|
24
32
|
func _ready() -> void:
|
|
25
33
|
if not _should_activate():
|
|
26
|
-
print(
|
|
34
|
+
print(
|
|
35
|
+
(
|
|
36
|
+
"[Godot CLI Control] inactive — pass --cli-control, set GODOT_CLI_CONTROL=1, or enable %s in Project Settings (debug build only)"
|
|
37
|
+
% SETTING_AUTO_ENABLE
|
|
38
|
+
)
|
|
39
|
+
)
|
|
27
40
|
queue_free()
|
|
28
41
|
return
|
|
29
42
|
# 即使 SceneTree 暂停也要继续运行
|
|
@@ -39,7 +52,9 @@ func _ready() -> void:
|
|
|
39
52
|
# 构建统一方法注册表
|
|
40
53
|
_register_methods()
|
|
41
54
|
# 缓存 outbound buffer 大小(ProjectSettings 可覆盖默认 10MB,至少 1MB)
|
|
42
|
-
var mb: int = int(
|
|
55
|
+
var mb: int = int(
|
|
56
|
+
ProjectSettings.get_setting(SETTING_OUTBOUND_BUFFER_MB, DEFAULT_OUTBOUND_BUFFER_MB)
|
|
57
|
+
)
|
|
43
58
|
_outbound_buffer_size = max(1, mb) * 1024 * 1024
|
|
44
59
|
# 启动 TCP 服务器
|
|
45
60
|
_port = _parse_port_from_args()
|
|
@@ -54,6 +69,16 @@ func _ready() -> void:
|
|
|
54
69
|
printerr(msg)
|
|
55
70
|
return
|
|
56
71
|
print("GameBridge: Listening on ws://127.0.0.1:%d" % _port)
|
|
72
|
+
_idle_timeout_secs = _parse_idle_timeout_from_args()
|
|
73
|
+
_last_activity_ms = Time.get_ticks_msec()
|
|
74
|
+
if _idle_timeout_secs > 0:
|
|
75
|
+
var t: Timer = Timer.new()
|
|
76
|
+
t.wait_time = 1.0
|
|
77
|
+
t.autostart = true
|
|
78
|
+
t.process_mode = Node.PROCESS_MODE_ALWAYS
|
|
79
|
+
t.timeout.connect(_check_idle)
|
|
80
|
+
add_child(t)
|
|
81
|
+
print("GameBridge: idle-timeout %ds enabled" % _idle_timeout_secs)
|
|
57
82
|
|
|
58
83
|
|
|
59
84
|
func _process(_delta: float) -> void:
|
|
@@ -118,14 +143,22 @@ func _register_methods() -> void:
|
|
|
118
143
|
_methods["wait_game_time"] = {"callable": _low_level_api.wait_game_time_async, "kind": "async"}
|
|
119
144
|
_methods["screenshot"] = {"callable": _wrap_screenshot, "kind": "async"}
|
|
120
145
|
# 输入模拟(同步)
|
|
121
|
-
_methods["input_action_press"] = {
|
|
122
|
-
|
|
146
|
+
_methods["input_action_press"] = {
|
|
147
|
+
"callable": _input_sim_api.handle_action_press, "kind": "sync"
|
|
148
|
+
}
|
|
149
|
+
_methods["input_action_release"] = {
|
|
150
|
+
"callable": _input_sim_api.handle_action_release, "kind": "sync"
|
|
151
|
+
}
|
|
123
152
|
_methods["input_action_tap"] = {"callable": _input_sim_api.handle_action_tap, "kind": "sync"}
|
|
124
153
|
_methods["input_get_pressed"] = {"callable": _input_sim_api.handle_get_pressed, "kind": "sync"}
|
|
125
|
-
_methods["list_input_actions"] = {
|
|
154
|
+
_methods["list_input_actions"] = {
|
|
155
|
+
"callable": _input_sim_api.handle_list_input_actions, "kind": "sync"
|
|
156
|
+
}
|
|
126
157
|
_methods["input_hold"] = {"callable": _input_sim_api.handle_hold, "kind": "sync"}
|
|
127
158
|
_methods["input_release_all"] = {"callable": _input_sim_api.handle_release_all, "kind": "sync"}
|
|
128
|
-
_methods["input_combo_cancel"] = {
|
|
159
|
+
_methods["input_combo_cancel"] = {
|
|
160
|
+
"callable": _input_sim_api.handle_combo_cancel, "kind": "sync"
|
|
161
|
+
}
|
|
129
162
|
# 输入模拟(async_with_id:handler 自行通过 _on_async_response 回响)
|
|
130
163
|
_methods["input_combo"] = {"callable": _input_sim_api.handle_combo, "kind": "async_with_id"}
|
|
131
164
|
|
|
@@ -136,6 +169,7 @@ func _wrap_screenshot(_params: Dictionary) -> Dictionary:
|
|
|
136
169
|
|
|
137
170
|
|
|
138
171
|
func _handle_message(raw: String) -> void:
|
|
172
|
+
_last_activity_ms = Time.get_ticks_msec()
|
|
139
173
|
var parsed: Variant = JSON.parse_string(raw)
|
|
140
174
|
if parsed == null or not parsed is Dictionary:
|
|
141
175
|
_send_error("", -32600, "Invalid JSON")
|
|
@@ -170,6 +204,7 @@ func _handle_message(raw: String) -> void:
|
|
|
170
204
|
var entry: Dictionary = _methods[method] as Dictionary
|
|
171
205
|
var handler: Callable = entry["callable"] as Callable
|
|
172
206
|
var kind: String = entry["kind"] as String
|
|
207
|
+
_in_flight += 1
|
|
173
208
|
match kind:
|
|
174
209
|
"sync":
|
|
175
210
|
var result: Dictionary = handler.call(params)
|
|
@@ -186,12 +221,14 @@ func _run_async(id: String, handler: Callable, params: Dictionary) -> void:
|
|
|
186
221
|
# 客户端 await 挂死到 timeout。先收原 Variant 自己 type-check。
|
|
187
222
|
var raw: Variant = await handler.call(params)
|
|
188
223
|
if not raw is Dictionary:
|
|
224
|
+
_in_flight = max(0, _in_flight - 1)
|
|
189
225
|
_send_error(id, -32603, "internal: async handler returned non-dict")
|
|
190
226
|
return
|
|
191
227
|
_dispatch_result(id, raw as Dictionary)
|
|
192
228
|
|
|
193
229
|
|
|
194
230
|
func _dispatch_result(id: String, result: Dictionary) -> void:
|
|
231
|
+
_in_flight = max(0, _in_flight - 1)
|
|
195
232
|
if result.has("error"):
|
|
196
233
|
var err: Dictionary = result["error"] as Dictionary
|
|
197
234
|
_send_error(id, err["code"] as int, err["message"] as String)
|
|
@@ -253,9 +290,35 @@ func _parse_port_from_args() -> int:
|
|
|
253
290
|
var port: int = parts[1].to_int()
|
|
254
291
|
if port < 1 or port > 65535:
|
|
255
292
|
push_warning(
|
|
256
|
-
|
|
257
|
-
|
|
293
|
+
(
|
|
294
|
+
"GameBridge: Port %d out of range [1, 65535], falling back to %d"
|
|
295
|
+
% [port, DEFAULT_PORT]
|
|
296
|
+
)
|
|
258
297
|
)
|
|
259
298
|
return DEFAULT_PORT
|
|
260
299
|
return port
|
|
261
300
|
return DEFAULT_PORT
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
func _parse_idle_timeout_from_args() -> int:
|
|
304
|
+
for arg: String in OS.get_cmdline_args():
|
|
305
|
+
if arg.begins_with("--game-bridge-idle-timeout="):
|
|
306
|
+
var parts: PackedStringArray = arg.split("=", false, 1)
|
|
307
|
+
if parts.size() != 2 or not parts[1].is_valid_int():
|
|
308
|
+
push_warning("GameBridge: Invalid idle-timeout %s, disabling" % arg)
|
|
309
|
+
return 0
|
|
310
|
+
var secs: int = parts[1].to_int()
|
|
311
|
+
return secs if secs > 0 else 0
|
|
312
|
+
return 0
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
func _check_idle() -> void:
|
|
316
|
+
# 有正在处理的请求 → daemon 没闲着;把活动戳推到现在,等所有请求落地后
|
|
317
|
+
# 再开始计时。否则一个 wait_game_time(idle_timeout+1) 就会被半路 quit。
|
|
318
|
+
if _in_flight > 0:
|
|
319
|
+
_last_activity_ms = Time.get_ticks_msec()
|
|
320
|
+
return
|
|
321
|
+
var idle_ms: int = Time.get_ticks_msec() - _last_activity_ms
|
|
322
|
+
if idle_ms / 1000 >= _idle_timeout_secs:
|
|
323
|
+
print("GameBridge: idle for %ds, shutting down" % (idle_ms / 1000))
|
|
324
|
+
get_tree().quit()
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
class_name InputSimulationApi
|
|
2
2
|
extends Node
|
|
3
3
|
## 输入模拟 API:动作级按键、持续控制、组合序列
|
|
4
|
+
##
|
|
5
|
+
## 错误码常量来自 res://addons/godot_cli_control/bridge/error_codes.gd
|
|
6
|
+
## (class_name CliControlErrorCodes)。靠 Godot 全局 class 注册解析;若 GUT
|
|
7
|
+
## 测试跑前遇到 "Class 'CliControlErrorCodes' not found",先 import 一次。
|
|
4
8
|
|
|
5
9
|
# 手动按下的动作(无定时器)
|
|
6
10
|
var _pressed_actions: Dictionary = {}
|
|
@@ -46,10 +50,10 @@ func is_combo_active() -> bool:
|
|
|
46
50
|
|
|
47
51
|
func handle_action_press(params: Dictionary) -> Dictionary:
|
|
48
52
|
if _combo_active:
|
|
49
|
-
return _err(
|
|
53
|
+
return _err(CliControlErrorCodes.COMBO_IN_PROGRESS, "combo in progress")
|
|
50
54
|
var action: String = params.get("action", "") as String
|
|
51
55
|
if not InputMap.has_action(action):
|
|
52
|
-
return _err(
|
|
56
|
+
return _err(CliControlErrorCodes.METHOD_NOT_FOUND, "Unknown action: %s" % action)
|
|
53
57
|
_do_press(action)
|
|
54
58
|
_pressed_actions[action] = true
|
|
55
59
|
return {"success": true}
|
|
@@ -57,10 +61,10 @@ func handle_action_press(params: Dictionary) -> Dictionary:
|
|
|
57
61
|
|
|
58
62
|
func handle_action_release(params: Dictionary) -> Dictionary:
|
|
59
63
|
if _combo_active:
|
|
60
|
-
return _err(
|
|
64
|
+
return _err(CliControlErrorCodes.COMBO_IN_PROGRESS, "combo in progress")
|
|
61
65
|
var action: String = params.get("action", "") as String
|
|
62
66
|
if not InputMap.has_action(action):
|
|
63
|
-
return _err(
|
|
67
|
+
return _err(CliControlErrorCodes.METHOD_NOT_FOUND, "Unknown action: %s" % action)
|
|
64
68
|
_do_release(action)
|
|
65
69
|
_pressed_actions.erase(action)
|
|
66
70
|
_held_actions.erase(action)
|
|
@@ -71,10 +75,10 @@ func handle_action_release(params: Dictionary) -> Dictionary:
|
|
|
71
75
|
## 定时器到期后自动释放
|
|
72
76
|
func handle_action_tap(params: Dictionary) -> Dictionary:
|
|
73
77
|
if _combo_active:
|
|
74
|
-
return _err(
|
|
78
|
+
return _err(CliControlErrorCodes.COMBO_IN_PROGRESS, "combo in progress")
|
|
75
79
|
var action: String = params.get("action", "") as String
|
|
76
80
|
if not InputMap.has_action(action):
|
|
77
|
-
return _err(
|
|
81
|
+
return _err(CliControlErrorCodes.METHOD_NOT_FOUND, "Unknown action: %s" % action)
|
|
78
82
|
var duration: float = params.get("duration", 0.1) as float
|
|
79
83
|
_do_press(action)
|
|
80
84
|
_held_actions[action] = duration
|
|
@@ -104,10 +108,10 @@ func handle_list_input_actions(params: Dictionary) -> Dictionary:
|
|
|
104
108
|
|
|
105
109
|
func handle_hold(params: Dictionary) -> Dictionary:
|
|
106
110
|
if _combo_active:
|
|
107
|
-
return _err(
|
|
111
|
+
return _err(CliControlErrorCodes.COMBO_IN_PROGRESS, "combo in progress")
|
|
108
112
|
var action: String = params.get("action", "") as String
|
|
109
113
|
if not InputMap.has_action(action):
|
|
110
|
-
return _err(
|
|
114
|
+
return _err(CliControlErrorCodes.METHOD_NOT_FOUND, "Unknown action: %s" % action)
|
|
111
115
|
var duration: float = params.get("duration", 0.0) as float
|
|
112
116
|
_do_press(action)
|
|
113
117
|
_held_actions[action] = duration
|
|
@@ -137,7 +141,7 @@ func release_all() -> void:
|
|
|
137
141
|
func handle_combo(params: Dictionary, request_id: String) -> void:
|
|
138
142
|
if _combo_active:
|
|
139
143
|
if _send_response_callback.is_valid():
|
|
140
|
-
_send_response_callback.call(request_id, _err(
|
|
144
|
+
_send_response_callback.call(request_id, _err(CliControlErrorCodes.COMBO_IN_PROGRESS, "combo in progress"))
|
|
141
145
|
return
|
|
142
146
|
var steps: Array = params.get("steps", []) as Array
|
|
143
147
|
_combo_request_id = request_id
|
|
@@ -188,7 +192,7 @@ func _begin_combo_step() -> void:
|
|
|
188
192
|
var raw: Variant = _combo_steps[_combo_index]
|
|
189
193
|
if not raw is Dictionary:
|
|
190
194
|
_abort_combo_with_error(
|
|
191
|
-
|
|
195
|
+
CliControlErrorCodes.INVALID_PARAMS, "combo step must be object at index %d" % _combo_index
|
|
192
196
|
)
|
|
193
197
|
return
|
|
194
198
|
var step: Dictionary = raw as Dictionary
|
|
@@ -198,7 +202,7 @@ func _begin_combo_step() -> void:
|
|
|
198
202
|
var action: String = step["action"] as String
|
|
199
203
|
if not InputMap.has_action(action):
|
|
200
204
|
_abort_combo_with_error(
|
|
201
|
-
|
|
205
|
+
CliControlErrorCodes.METHOD_NOT_FOUND, "Unknown action at combo step %d: %s" % [_combo_index, action]
|
|
202
206
|
)
|
|
203
207
|
return
|
|
204
208
|
var duration: float = step.get("duration", 0.1) as float
|
|
@@ -207,7 +211,7 @@ func _begin_combo_step() -> void:
|
|
|
207
211
|
_combo_timer = duration
|
|
208
212
|
else:
|
|
209
213
|
_abort_combo_with_error(
|
|
210
|
-
|
|
214
|
+
CliControlErrorCodes.INVALID_PARAMS, "combo step missing 'wait' or 'action' at index %d" % _combo_index
|
|
211
215
|
)
|
|
212
216
|
|
|
213
217
|
|