godot-cli-control 0.2.4__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.4 → godot_cli_control-0.2.5}/PKG-INFO +1 -1
- {godot_cli_control-0.2.4 → godot_cli_control-0.2.5}/addons/godot_cli_control/CHANGELOG.md +26 -0
- {godot_cli_control-0.2.4 → 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.4 → 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.5/addons/godot_cli_control/tests/gut/test_low_level_api.gd +696 -0
- {godot_cli_control-0.2.4 → godot_cli_control-0.2.5}/python/godot_cli_control/_version.py +2 -2
- {godot_cli_control-0.2.4 → godot_cli_control-0.2.5}/python/godot_cli_control/bridge.py +14 -2
- {godot_cli_control-0.2.4 → godot_cli_control-0.2.5}/python/godot_cli_control/cli.py +391 -101
- {godot_cli_control-0.2.4 → godot_cli_control-0.2.5}/python/godot_cli_control/client.py +13 -2
- {godot_cli_control-0.2.4 → godot_cli_control-0.2.5}/python/godot_cli_control/init_cmd.py +74 -27
- {godot_cli_control-0.2.4 → godot_cli_control-0.2.5}/python/godot_cli_control/pytest_plugin.py +3 -3
- {godot_cli_control-0.2.4 → godot_cli_control-0.2.5}/python/godot_cli_control/templates/skill/SKILL.md +77 -5
- {godot_cli_control-0.2.4 → godot_cli_control-0.2.5}/python/tests/test_bridge.py +47 -2
- {godot_cli_control-0.2.4 → godot_cli_control-0.2.5}/python/tests/test_cli.py +541 -0
- {godot_cli_control-0.2.4 → godot_cli_control-0.2.5}/python/tests/test_cli_helpers.py +5 -3
- {godot_cli_control-0.2.4 → godot_cli_control-0.2.5}/python/tests/test_client.py +49 -0
- {godot_cli_control-0.2.4 → godot_cli_control-0.2.5}/python/tests/test_init.py +121 -0
- {godot_cli_control-0.2.4 → godot_cli_control-0.2.5}/python/tests/test_pytest_plugin.py +50 -0
- godot_cli_control-0.2.4/addons/godot_cli_control/bridge/low_level_api.gd +0 -280
- godot_cli_control-0.2.4/addons/godot_cli_control/tests/gut/test_low_level_api.gd +0 -194
- {godot_cli_control-0.2.4 → godot_cli_control-0.2.5}/.gitignore +0 -0
- {godot_cli_control-0.2.4 → godot_cli_control-0.2.5}/LICENSE +0 -0
- {godot_cli_control-0.2.4 → godot_cli_control-0.2.5}/addons/godot_cli_control/LICENSE +0 -0
- {godot_cli_control-0.2.4 → godot_cli_control-0.2.5}/addons/godot_cli_control/bin/run_cli_control.ps1 +0 -0
- {godot_cli_control-0.2.4 → godot_cli_control-0.2.5}/addons/godot_cli_control/bin/run_cli_control.sh +0 -0
- {godot_cli_control-0.2.4 → godot_cli_control-0.2.5}/addons/godot_cli_control/bridge/game_bridge.gd +0 -0
- {godot_cli_control-0.2.4 → godot_cli_control-0.2.5}/addons/godot_cli_control/bridge/game_bridge.gd.uid +0 -0
- {godot_cli_control-0.2.4 → godot_cli_control-0.2.5}/addons/godot_cli_control/bridge/input_simulation_api.gd.uid +0 -0
- {godot_cli_control-0.2.4 → godot_cli_control-0.2.5}/addons/godot_cli_control/bridge/low_level_api.gd.uid +0 -0
- {godot_cli_control-0.2.4 → godot_cli_control-0.2.5}/addons/godot_cli_control/plugin.cfg +0 -0
- {godot_cli_control-0.2.4 → godot_cli_control-0.2.5}/addons/godot_cli_control/plugin.gd +0 -0
- {godot_cli_control-0.2.4 → godot_cli_control-0.2.5}/addons/godot_cli_control/plugin.gd.uid +0 -0
- {godot_cli_control-0.2.4 → godot_cli_control-0.2.5}/addons/godot_cli_control/tests/gut/test_game_bridge.gd +0 -0
- {godot_cli_control-0.2.4 → godot_cli_control-0.2.5}/addons/godot_cli_control/tests/gut/test_input_simulation_api.gd +0 -0
- {godot_cli_control-0.2.4 → godot_cli_control-0.2.5}/addons/godot_cli_control/tests/run_gut.sh +0 -0
- {godot_cli_control-0.2.4 → godot_cli_control-0.2.5}/addons/godot_cli_control/tests/test_init_walkthrough.py +0 -0
- {godot_cli_control-0.2.4 → godot_cli_control-0.2.5}/addons/godot_cli_control/tests/test_walkthrough.sh +0 -0
- {godot_cli_control-0.2.4 → godot_cli_control-0.2.5}/pyproject.toml +0 -0
- {godot_cli_control-0.2.4 → godot_cli_control-0.2.5}/python/README.md +0 -0
- {godot_cli_control-0.2.4 → godot_cli_control-0.2.5}/python/godot_cli_control/__init__.py +0 -0
- {godot_cli_control-0.2.4 → godot_cli_control-0.2.5}/python/godot_cli_control/__main__.py +0 -0
- {godot_cli_control-0.2.4 → godot_cli_control-0.2.5}/python/godot_cli_control/_duration.py +0 -0
- {godot_cli_control-0.2.4 → godot_cli_control-0.2.5}/python/godot_cli_control/daemon.py +0 -0
- {godot_cli_control-0.2.4 → godot_cli_control-0.2.5}/python/godot_cli_control/registry.py +0 -0
- {godot_cli_control-0.2.4 → godot_cli_control-0.2.5}/python/godot_cli_control/runner.py +0 -0
- {godot_cli_control-0.2.4 → godot_cli_control-0.2.5}/python/godot_cli_control/skills_install.py +0 -0
- {godot_cli_control-0.2.4 → godot_cli_control-0.2.5}/python/godot_cli_control/templates/__init__.py +0 -0
- {godot_cli_control-0.2.4 → godot_cli_control-0.2.5}/python/godot_cli_control/templates/skill/__init__.py +0 -0
- {godot_cli_control-0.2.4 → godot_cli_control-0.2.5}/python/tests/__init__.py +0 -0
- {godot_cli_control-0.2.4 → godot_cli_control-0.2.5}/python/tests/test_daemon.py +0 -0
- {godot_cli_control-0.2.4 → godot_cli_control-0.2.5}/python/tests/test_duration.py +0 -0
- {godot_cli_control-0.2.4 → godot_cli_control-0.2.5}/python/tests/test_registry.py +0 -0
- {godot_cli_control-0.2.4 → godot_cli_control-0.2.5}/python/tests/test_runner.py +0 -0
- {godot_cli_control-0.2.4 → godot_cli_control-0.2.5}/python/tests/test_skills_install.py +0 -0
|
@@ -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
|
|
@@ -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
|
|