@beremaran/godot-agent-loop 1.0.0
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.
- package/LICENSE +23 -0
- package/README.md +925 -0
- package/addons/godot_agent_loop/LICENSE +23 -0
- package/addons/godot_agent_loop/README.md +31 -0
- package/addons/godot_agent_loop/plugin.cfg +8 -0
- package/addons/godot_agent_loop/plugin.gd +417 -0
- package/agent-plugin/.claude-plugin/plugin.json +19 -0
- package/agent-plugin/.codex-plugin/plugin.json +37 -0
- package/agent-plugin/.mcp.json +14 -0
- package/agent-plugin/adapter-manifest.json +45 -0
- package/agent-plugin/pi/extension.ts +136 -0
- package/agent-plugin/skills/build-godot-game/SKILL.md +41 -0
- package/agent-plugin/skills/debug-godot-game/SKILL.md +37 -0
- package/agent-plugin/skills/ship-godot-game/SKILL.md +46 -0
- package/agent-plugin/skills/ship-godot-game/agents/openai.yaml +4 -0
- package/agent-plugin/skills/verify-godot-change/SKILL.md +35 -0
- package/build/authoring-session-manager.js +237 -0
- package/build/domain-tool-registries.js +207 -0
- package/build/editor-bridge-protocol.js +1 -0
- package/build/editor-connection.js +112 -0
- package/build/editor-mutation-guard.js +30 -0
- package/build/editor-plugin-installer.js +220 -0
- package/build/engine-api/extension_api-4.7.stable.official.5b4e0cb0f.json +356830 -0
- package/build/game-command-service.js +49 -0
- package/build/game-connection.js +337 -0
- package/build/godot-executable.js +156 -0
- package/build/godot-process-manager.js +112 -0
- package/build/godot-subprocess.js +23 -0
- package/build/headless-operation-runner.js +68 -0
- package/build/headless-operation-service.js +65 -0
- package/build/index.js +478 -0
- package/build/interaction-server-installer.js +234 -0
- package/build/opencode-setup.js +199 -0
- package/build/project-support.js +219 -0
- package/build/runtime-protocol.js +202 -0
- package/build/scripts/godot_operations.gd +1534 -0
- package/build/scripts/mcp_editor_plugin.gd +417 -0
- package/build/scripts/mcp_interaction_server.gd +1255 -0
- package/build/scripts/mcp_runtime/audio_animation_domain.gd +568 -0
- package/build/scripts/mcp_runtime/command_params.gd +339 -0
- package/build/scripts/mcp_runtime/core_domain.gd +591 -0
- package/build/scripts/mcp_runtime/input_domain.gd +695 -0
- package/build/scripts/mcp_runtime/networking_domain.gd +356 -0
- package/build/scripts/mcp_runtime/physics_domain.gd +653 -0
- package/build/scripts/mcp_runtime/privileged_command_policy.gd +81 -0
- package/build/scripts/mcp_runtime/rendering_domain.gd +807 -0
- package/build/scripts/mcp_runtime/runtime_domain.gd +108 -0
- package/build/scripts/mcp_runtime/scene_2d_domain.gd +527 -0
- package/build/scripts/mcp_runtime/scene_3d_domain.gd +573 -0
- package/build/scripts/mcp_runtime/system_domain.gd +277 -0
- package/build/scripts/mcp_runtime/ui_domain.gd +619 -0
- package/build/scripts/mcp_runtime/variant_codec.gd +335 -0
- package/build/scripts/validate_script.gd +28 -0
- package/build/server-instructions.js +11 -0
- package/build/session-timing.js +20 -0
- package/build/tool-argument-validation.js +120 -0
- package/build/tool-definitions.js +2727 -0
- package/build/tool-handlers/game-tool-handlers.js +1345 -0
- package/build/tool-handlers/lifecycle-tool-handlers.js +346 -0
- package/build/tool-handlers/project-handler-services.js +1458 -0
- package/build/tool-handlers/project-tool-handlers.js +1506 -0
- package/build/tool-handlers/visual-regression-service.js +139 -0
- package/build/tool-manifest.js +1193 -0
- package/build/tool-mutation-policy.js +122 -0
- package/build/tool-registry.js +34 -0
- package/build/tool-surface.js +70 -0
- package/build/utils.js +273 -0
- package/package.json +110 -0
- package/product.json +52 -0
- package/scripts/prepare-package.js +42 -0
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
extends "res://mcp_runtime/runtime_domain.gd"
|
|
2
|
+
|
|
3
|
+
# UI domain: theme overrides, Control focus/layout, text controls, popups,
|
|
4
|
+
# Tree/ItemList/OptionButton items, tabs, menus, and Range/ColorPicker values.
|
|
5
|
+
|
|
6
|
+
const ANCHOR_PRESETS: Dictionary = {
|
|
7
|
+
"top_left": Control.PRESET_TOP_LEFT,
|
|
8
|
+
"top_right": Control.PRESET_TOP_RIGHT,
|
|
9
|
+
"bottom_left": Control.PRESET_BOTTOM_LEFT,
|
|
10
|
+
"bottom_right": Control.PRESET_BOTTOM_RIGHT,
|
|
11
|
+
"center_left": Control.PRESET_CENTER_LEFT,
|
|
12
|
+
"center_top": Control.PRESET_CENTER_TOP,
|
|
13
|
+
"center_right": Control.PRESET_CENTER_RIGHT,
|
|
14
|
+
"center_bottom": Control.PRESET_CENTER_BOTTOM,
|
|
15
|
+
"center": Control.PRESET_CENTER,
|
|
16
|
+
"left_wide": Control.PRESET_LEFT_WIDE,
|
|
17
|
+
"top_wide": Control.PRESET_TOP_WIDE,
|
|
18
|
+
"right_wide": Control.PRESET_RIGHT_WIDE,
|
|
19
|
+
"bottom_wide": Control.PRESET_BOTTOM_WIDE,
|
|
20
|
+
"vcenter_wide": Control.PRESET_VCENTER_WIDE,
|
|
21
|
+
"hcenter_wide": Control.PRESET_HCENTER_WIDE,
|
|
22
|
+
"full_rect": Control.PRESET_FULL_RECT,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
func register_commands() -> void:
|
|
27
|
+
register_command("ui_theme", _cmd_ui_theme)
|
|
28
|
+
register_command("ui_control", _cmd_ui_control)
|
|
29
|
+
register_command("ui_text", _cmd_ui_text)
|
|
30
|
+
register_command("ui_popup", _cmd_ui_popup)
|
|
31
|
+
register_command("ui_tree", _cmd_ui_tree)
|
|
32
|
+
register_command("ui_item_list", _cmd_ui_item_list)
|
|
33
|
+
register_command("ui_tabs", _cmd_ui_tabs)
|
|
34
|
+
register_command("ui_menu", _cmd_ui_menu)
|
|
35
|
+
register_command("ui_range", _cmd_ui_range)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# Records a structured failure when the resolved node is not the class the
|
|
39
|
+
# command drives; the caller still routes through params_invalid().
|
|
40
|
+
func _require_class(reader: CommandParams, node: Node, type_name: String) -> void:
|
|
41
|
+
if reader.failed() or node == null:
|
|
42
|
+
return
|
|
43
|
+
reader.fail("Node is not a %s: %s" % [type_name, node.get_class()],
|
|
44
|
+
{"param": "node_path", "reason": "invalid_value", "expected": type_name, "value": node.get_class()})
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
func _color_from(value: Variant) -> Color:
|
|
48
|
+
var source: Dictionary = CommandParams.as_dictionary(value)
|
|
49
|
+
return Color(
|
|
50
|
+
CommandParams.json_float(source, "r"),
|
|
51
|
+
CommandParams.json_float(source, "g"),
|
|
52
|
+
CommandParams.json_float(source, "b"),
|
|
53
|
+
CommandParams.json_float(source, "a", 1.0),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# --- Theme overrides ---
|
|
58
|
+
func _cmd_ui_theme(params: Dictionary) -> void:
|
|
59
|
+
var reader: CommandParams = CommandParams.new(params)
|
|
60
|
+
var node: Node = require_node(reader)
|
|
61
|
+
var overrides: Dictionary = reader.optional_dictionary("overrides")
|
|
62
|
+
if node != null and not node is Control:
|
|
63
|
+
_require_class(reader, node, "Control")
|
|
64
|
+
if params_invalid(reader):
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
var ctrl: Control = node as Control
|
|
68
|
+
var applied: Array = []
|
|
69
|
+
|
|
70
|
+
var colors: Dictionary = CommandParams.json_dictionary(overrides, "colors")
|
|
71
|
+
for color_name: Variant in colors:
|
|
72
|
+
var override: StringName = StringName(str(color_name))
|
|
73
|
+
ctrl.add_theme_color_override(override, _color_from(colors[color_name]))
|
|
74
|
+
applied.append("color:" + str(color_name))
|
|
75
|
+
|
|
76
|
+
var constants: Dictionary = CommandParams.json_dictionary(overrides, "constants")
|
|
77
|
+
for constant_name: Variant in constants:
|
|
78
|
+
var override: StringName = StringName(str(constant_name))
|
|
79
|
+
ctrl.add_theme_constant_override(override, CommandParams.to_int(constants[constant_name]))
|
|
80
|
+
applied.append("constant:" + str(constant_name))
|
|
81
|
+
|
|
82
|
+
var font_sizes: Dictionary = CommandParams.as_dictionary(overrides.get("fontSizes", overrides.get("font_sizes", {})))
|
|
83
|
+
for font_name: Variant in font_sizes:
|
|
84
|
+
var override: StringName = StringName(str(font_name))
|
|
85
|
+
ctrl.add_theme_font_size_override(override, CommandParams.to_int(font_sizes[font_name]))
|
|
86
|
+
applied.append("font_size:" + str(font_name))
|
|
87
|
+
|
|
88
|
+
respond({"success": true, "node_path": str(params.get("node_path", "")), "applied": applied})
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# --- Control focus and layout ---
|
|
92
|
+
func _cmd_ui_control(params: Dictionary) -> void:
|
|
93
|
+
var reader: CommandParams = CommandParams.new(params)
|
|
94
|
+
var node: Node = require_node(reader)
|
|
95
|
+
var action: String = reader.optional_enum("action", "get_info", ["grab_focus", "release_focus", "configure", "get_info"])
|
|
96
|
+
if node != null and not node is Control:
|
|
97
|
+
_require_class(reader, node, "Control")
|
|
98
|
+
if params_invalid(reader):
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
var ctrl: Control = node as Control
|
|
102
|
+
match action:
|
|
103
|
+
"grab_focus":
|
|
104
|
+
ctrl.grab_focus()
|
|
105
|
+
respond({"success": true, "action": "grab_focus"})
|
|
106
|
+
"release_focus":
|
|
107
|
+
ctrl.release_focus()
|
|
108
|
+
respond({"success": true, "action": "release_focus"})
|
|
109
|
+
"configure":
|
|
110
|
+
var applied: Array = []
|
|
111
|
+
if reader.has_param("tooltip"):
|
|
112
|
+
ctrl.tooltip_text = str(reader.raw("tooltip"))
|
|
113
|
+
applied.append("tooltip")
|
|
114
|
+
if reader.has_param("mouse_filter"):
|
|
115
|
+
var mouse_filter: String = reader.required_enum("mouse_filter", ["stop", "pass", "ignore"])
|
|
116
|
+
if params_invalid(reader):
|
|
117
|
+
return
|
|
118
|
+
match mouse_filter:
|
|
119
|
+
"stop": ctrl.mouse_filter = Control.MOUSE_FILTER_STOP
|
|
120
|
+
"pass": ctrl.mouse_filter = Control.MOUSE_FILTER_PASS
|
|
121
|
+
"ignore": ctrl.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
122
|
+
applied.append("mouse_filter")
|
|
123
|
+
if reader.has_param("min_size"):
|
|
124
|
+
var min_size: Dictionary = reader.required_dictionary("min_size")
|
|
125
|
+
if params_invalid(reader):
|
|
126
|
+
return
|
|
127
|
+
ctrl.custom_minimum_size = Vector2(CommandParams.json_float(min_size, "x", 0), CommandParams.json_float(min_size, "y", 0))
|
|
128
|
+
applied.append("min_size")
|
|
129
|
+
if reader.has_param("anchor_preset"):
|
|
130
|
+
var preset: int = _resolve_anchor_preset(reader.raw("anchor_preset"))
|
|
131
|
+
if preset < 0:
|
|
132
|
+
reader.fail("Invalid anchor_preset: %s" % str(reader.raw("anchor_preset")),
|
|
133
|
+
{"param": "anchor_preset", "reason": "invalid_value", "allowed": ANCHOR_PRESETS.keys(), "value": str(reader.raw("anchor_preset"))})
|
|
134
|
+
if params_invalid(reader):
|
|
135
|
+
return
|
|
136
|
+
ctrl.set_anchors_and_offsets_preset(preset as Control.LayoutPreset)
|
|
137
|
+
applied.append("anchor_preset")
|
|
138
|
+
respond({"success": true, "action": "configure", "applied": applied})
|
|
139
|
+
"get_info":
|
|
140
|
+
respond({"success": true, "size": variant_to_json(ctrl.size), "position": variant_to_json(ctrl.position), "has_focus": ctrl.has_focus(), "visible": ctrl.visible, "tooltip": ctrl.tooltip_text, "mouse_filter": ctrl.mouse_filter})
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
func _resolve_anchor_preset(value: Variant) -> int:
|
|
144
|
+
if value is int or value is float:
|
|
145
|
+
return CommandParams.to_int(value)
|
|
146
|
+
if value is String:
|
|
147
|
+
var text: String = value
|
|
148
|
+
var key: String = text.to_lower()
|
|
149
|
+
if ANCHOR_PRESETS.has(key):
|
|
150
|
+
return ANCHOR_PRESETS[key]
|
|
151
|
+
return -1
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# --- Text controls ---
|
|
155
|
+
func _cmd_ui_text(params: Dictionary) -> void:
|
|
156
|
+
var reader: CommandParams = CommandParams.new(params)
|
|
157
|
+
var node: Node = require_node(reader)
|
|
158
|
+
var action: String = reader.optional_enum("action", "get", ["get", "set", "append", "clear", "bbcode"])
|
|
159
|
+
if params_invalid(reader):
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
if action == "bbcode":
|
|
163
|
+
var bbcode: String = reader.required_string("text")
|
|
164
|
+
if not node is RichTextLabel:
|
|
165
|
+
_require_class(reader, node, "RichTextLabel")
|
|
166
|
+
if params_invalid(reader):
|
|
167
|
+
return
|
|
168
|
+
var rtl: RichTextLabel = node as RichTextLabel
|
|
169
|
+
rtl.bbcode_enabled = true
|
|
170
|
+
rtl.text = bbcode
|
|
171
|
+
respond({"success": true, "action": "bbcode"})
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
if not (node is LineEdit or node is TextEdit or node is RichTextLabel):
|
|
175
|
+
_require_class(reader, node, "text control (LineEdit/TextEdit/RichTextLabel)")
|
|
176
|
+
send_params_error(reader)
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
match action:
|
|
180
|
+
"get":
|
|
181
|
+
var text: String = ""
|
|
182
|
+
if node is LineEdit: text = (node as LineEdit).text
|
|
183
|
+
elif node is TextEdit: text = (node as TextEdit).text
|
|
184
|
+
elif node is RichTextLabel: text = (node as RichTextLabel).text
|
|
185
|
+
respond({"success": true, "text": text})
|
|
186
|
+
"set":
|
|
187
|
+
var text: String = reader.required_string("text")
|
|
188
|
+
if params_invalid(reader):
|
|
189
|
+
return
|
|
190
|
+
if node is LineEdit: (node as LineEdit).text = text
|
|
191
|
+
elif node is TextEdit: (node as TextEdit).text = text
|
|
192
|
+
elif node is RichTextLabel: (node as RichTextLabel).text = text
|
|
193
|
+
_apply_text_selection(reader, node)
|
|
194
|
+
if params_invalid(reader):
|
|
195
|
+
return
|
|
196
|
+
respond({"success": true, "action": "set"})
|
|
197
|
+
"append":
|
|
198
|
+
var text: String = reader.required_string("text")
|
|
199
|
+
if params_invalid(reader):
|
|
200
|
+
return
|
|
201
|
+
if node is TextEdit: (node as TextEdit).text += text
|
|
202
|
+
elif node is RichTextLabel: (node as RichTextLabel).append_text(text)
|
|
203
|
+
elif node is LineEdit: (node as LineEdit).text += text
|
|
204
|
+
_apply_text_selection(reader, node)
|
|
205
|
+
if params_invalid(reader):
|
|
206
|
+
return
|
|
207
|
+
respond({"success": true, "action": "append"})
|
|
208
|
+
"clear":
|
|
209
|
+
if node is LineEdit: (node as LineEdit).text = ""
|
|
210
|
+
elif node is TextEdit: (node as TextEdit).text = ""
|
|
211
|
+
elif node is RichTextLabel:
|
|
212
|
+
(node as RichTextLabel).clear()
|
|
213
|
+
(node as RichTextLabel).text = ""
|
|
214
|
+
respond({"success": true, "action": "clear"})
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
func _apply_text_selection(reader: CommandParams, node: Node) -> void:
|
|
218
|
+
var has_from: bool = reader.has_param("selection_from")
|
|
219
|
+
var has_to: bool = reader.has_param("selection_to")
|
|
220
|
+
if has_from != has_to:
|
|
221
|
+
reader.fail("selection_from and selection_to must be provided together",
|
|
222
|
+
{"param": "selection_from", "reason": "missing_pair"})
|
|
223
|
+
return
|
|
224
|
+
var caret: int = reader.optional_int("caret_position", -1, -1)
|
|
225
|
+
var selection_from: int = reader.optional_int("selection_from", 0, 0)
|
|
226
|
+
var selection_to: int = reader.optional_int("selection_to", 0, 0)
|
|
227
|
+
if reader.failed():
|
|
228
|
+
return
|
|
229
|
+
if node is LineEdit:
|
|
230
|
+
var line: LineEdit = node as LineEdit
|
|
231
|
+
if caret >= 0:
|
|
232
|
+
line.caret_column = mini(caret, line.text.length())
|
|
233
|
+
if has_from:
|
|
234
|
+
line.select(mini(selection_from, line.text.length()), mini(selection_to, line.text.length()))
|
|
235
|
+
elif node is TextEdit:
|
|
236
|
+
var edit: TextEdit = node as TextEdit
|
|
237
|
+
if caret >= 0:
|
|
238
|
+
edit.set_caret_column(caret)
|
|
239
|
+
if has_from:
|
|
240
|
+
edit.select(0, selection_from, 0, selection_to)
|
|
241
|
+
elif caret >= 0 or has_from:
|
|
242
|
+
reader.fail("caret and selection parameters require LineEdit or TextEdit",
|
|
243
|
+
{"param": "caret_position", "reason": "invalid_value", "expected": "LineEdit or TextEdit"})
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
# --- Popups and windows ---
|
|
247
|
+
func _cmd_ui_popup(params: Dictionary) -> void:
|
|
248
|
+
var reader: CommandParams = CommandParams.new(params)
|
|
249
|
+
var node: Node = require_node(reader)
|
|
250
|
+
var action: String = reader.optional_enum("action", "popup_centered", ["popup_centered", "popup", "hide", "get_info"])
|
|
251
|
+
if node != null and not node is Window:
|
|
252
|
+
_require_class(reader, node, "Window")
|
|
253
|
+
if params_invalid(reader):
|
|
254
|
+
return
|
|
255
|
+
|
|
256
|
+
var win: Window = node as Window
|
|
257
|
+
if reader.has_param("title"):
|
|
258
|
+
win.title = reader.required_string("title")
|
|
259
|
+
if reader.has_param("text"):
|
|
260
|
+
if win is AcceptDialog:
|
|
261
|
+
(win as AcceptDialog).dialog_text = reader.required_string("text")
|
|
262
|
+
else:
|
|
263
|
+
reader.fail("text requires an AcceptDialog", {"param": "text", "reason": "invalid_value", "expected": "AcceptDialog"})
|
|
264
|
+
if params_invalid(reader):
|
|
265
|
+
return
|
|
266
|
+
match action:
|
|
267
|
+
"popup_centered":
|
|
268
|
+
if reader.has_param("size"):
|
|
269
|
+
var size: Dictionary = reader.required_dictionary("size")
|
|
270
|
+
if params_invalid(reader):
|
|
271
|
+
return
|
|
272
|
+
var requested_size := Vector2i(CommandParams.json_int(size, "x", 200), CommandParams.json_int(size, "y", 100))
|
|
273
|
+
win.popup_centered(requested_size)
|
|
274
|
+
# Embedded headless windows clamp during centering; explicit sizing is authoritative.
|
|
275
|
+
win.size = requested_size
|
|
276
|
+
else:
|
|
277
|
+
win.popup_centered()
|
|
278
|
+
respond({"success": true, "action": "popup_centered"})
|
|
279
|
+
"popup":
|
|
280
|
+
win.popup()
|
|
281
|
+
respond({"success": true, "action": "popup"})
|
|
282
|
+
"hide":
|
|
283
|
+
win.hide()
|
|
284
|
+
respond({"success": true, "action": "hide"})
|
|
285
|
+
"get_info":
|
|
286
|
+
respond({"success": true, "visible": win.visible, "title": win.title, "size": variant_to_json(win.size)})
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
# --- Tree items ---
|
|
290
|
+
func _cmd_ui_tree(params: Dictionary) -> void:
|
|
291
|
+
var reader: CommandParams = CommandParams.new(params)
|
|
292
|
+
var node: Node = require_node(reader)
|
|
293
|
+
var action: String = reader.optional_enum("action", "get_items", ["get_items", "add", "select", "collapse", "expand", "remove"])
|
|
294
|
+
if node != null and not node is Tree:
|
|
295
|
+
_require_class(reader, node, "Tree")
|
|
296
|
+
if params_invalid(reader):
|
|
297
|
+
return
|
|
298
|
+
|
|
299
|
+
var tree_node: Tree = node as Tree
|
|
300
|
+
match action:
|
|
301
|
+
"get_items":
|
|
302
|
+
var items: Array = []
|
|
303
|
+
var tree_root: TreeItem = tree_node.get_root()
|
|
304
|
+
if tree_root != null:
|
|
305
|
+
_collect_tree_items(tree_root, items, 0, "")
|
|
306
|
+
respond({"success": true, "action": "get_items", "items": items})
|
|
307
|
+
"add":
|
|
308
|
+
var text: String = reader.required_string("text")
|
|
309
|
+
var column: int = reader.optional_int("column", 0, 0)
|
|
310
|
+
var tree_root: TreeItem = tree_node.get_root()
|
|
311
|
+
if tree_root == null:
|
|
312
|
+
tree_root = tree_node.create_item()
|
|
313
|
+
if column >= tree_node.columns:
|
|
314
|
+
reader.fail("column is out of bounds", {"param": "column", "reason": "out_of_range", "max": tree_node.columns - 1, "value": column})
|
|
315
|
+
var parent: TreeItem = _tree_item_at_path(reader, tree_node, reader.optional_string("item_path", ""), true)
|
|
316
|
+
if params_invalid(reader):
|
|
317
|
+
return
|
|
318
|
+
var item: TreeItem = tree_node.create_item(parent)
|
|
319
|
+
item.set_text(column, text)
|
|
320
|
+
respond({"success": true, "action": "add", "text": text})
|
|
321
|
+
"select", "collapse", "expand", "remove":
|
|
322
|
+
var item_path: String = reader.required_string("item_path")
|
|
323
|
+
var item: TreeItem = _tree_item_at_path(reader, tree_node, item_path, false)
|
|
324
|
+
var column: int = reader.optional_int("column", 0, 0)
|
|
325
|
+
if column >= tree_node.columns:
|
|
326
|
+
reader.fail("column is out of bounds", {"param": "column", "reason": "out_of_range", "max": tree_node.columns - 1, "value": column})
|
|
327
|
+
if params_invalid(reader):
|
|
328
|
+
return
|
|
329
|
+
match action:
|
|
330
|
+
"select": tree_node.set_selected(item, column)
|
|
331
|
+
"collapse": item.collapsed = true
|
|
332
|
+
"expand": item.collapsed = false
|
|
333
|
+
"remove": item.free()
|
|
334
|
+
respond({"success": true, "action": action, "item_path": item_path})
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
func _collect_tree_items(item: TreeItem, result: Array, depth: int, item_path: String) -> void:
|
|
338
|
+
var columns: Array = []
|
|
339
|
+
for column in item.get_tree().columns:
|
|
340
|
+
columns.append(item.get_text(column))
|
|
341
|
+
result.append({"path": item_path, "text": item.get_text(0), "columns": columns, "depth": depth, "collapsed": item.collapsed, "selected": item.is_selected(0)})
|
|
342
|
+
var child: TreeItem = item.get_first_child()
|
|
343
|
+
var child_index: int = 0
|
|
344
|
+
while child != null:
|
|
345
|
+
var child_path: String = str(child_index) if item_path.is_empty() else "%s/%d" % [item_path, child_index]
|
|
346
|
+
_collect_tree_items(child, result, depth + 1, child_path)
|
|
347
|
+
child = child.get_next()
|
|
348
|
+
child_index += 1
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
func _tree_item_at_path(reader: CommandParams, tree_node: Tree, item_path: String, allow_root: bool) -> TreeItem:
|
|
352
|
+
var item: TreeItem = tree_node.get_root()
|
|
353
|
+
if item == null:
|
|
354
|
+
reader.fail("Tree has no root item", {"param": "item_path", "reason": "not_found"})
|
|
355
|
+
return null
|
|
356
|
+
if item_path.is_empty():
|
|
357
|
+
if allow_root:
|
|
358
|
+
return item
|
|
359
|
+
reader.fail("item_path must identify a non-root item", {"param": "item_path", "reason": "invalid_value"})
|
|
360
|
+
return null
|
|
361
|
+
for segment: String in item_path.split("/"):
|
|
362
|
+
if not segment.is_valid_int() or segment.to_int() < 0:
|
|
363
|
+
reader.fail("item_path must contain non-negative child indices", {"param": "item_path", "reason": "invalid_value", "value": item_path})
|
|
364
|
+
return null
|
|
365
|
+
var child_index: int = segment.to_int()
|
|
366
|
+
item = item.get_child(child_index)
|
|
367
|
+
if item == null:
|
|
368
|
+
reader.fail("Tree item not found: %s" % item_path, {"param": "item_path", "reason": "not_found", "value": item_path})
|
|
369
|
+
return null
|
|
370
|
+
return item
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
# --- ItemList / OptionButton items ---
|
|
374
|
+
func _cmd_ui_item_list(params: Dictionary) -> void:
|
|
375
|
+
var reader: CommandParams = CommandParams.new(params)
|
|
376
|
+
var node: Node = require_node(reader)
|
|
377
|
+
if params_invalid(reader):
|
|
378
|
+
return
|
|
379
|
+
|
|
380
|
+
if node is ItemList:
|
|
381
|
+
var item_list: ItemList = node as ItemList
|
|
382
|
+
var action: String = reader.optional_enum("action", "get_items", ["get_items", "select", "add", "remove", "clear"])
|
|
383
|
+
if params_invalid(reader):
|
|
384
|
+
return
|
|
385
|
+
match action:
|
|
386
|
+
"get_items":
|
|
387
|
+
var items: Array = []
|
|
388
|
+
for i in item_list.item_count:
|
|
389
|
+
items.append({"index": i, "text": item_list.get_item_text(i), "selected": item_list.is_selected(i)})
|
|
390
|
+
respond({"success": true, "items": items})
|
|
391
|
+
"select":
|
|
392
|
+
var index: int = reader.required_int("index", 0)
|
|
393
|
+
if index >= item_list.item_count:
|
|
394
|
+
reader.fail("index is out of bounds", {"param": "index", "reason": "out_of_range", "max": item_list.item_count - 1, "value": index})
|
|
395
|
+
if params_invalid(reader):
|
|
396
|
+
return
|
|
397
|
+
item_list.select(index)
|
|
398
|
+
respond({"success": true, "action": "select"})
|
|
399
|
+
"add":
|
|
400
|
+
@warning_ignore("return_value_discarded")
|
|
401
|
+
item_list.add_item(reader.required_string("text"))
|
|
402
|
+
if params_invalid(reader):
|
|
403
|
+
return
|
|
404
|
+
respond({"success": true, "action": "add"})
|
|
405
|
+
"remove":
|
|
406
|
+
var index: int = reader.required_int("index", 0)
|
|
407
|
+
if index >= item_list.item_count:
|
|
408
|
+
reader.fail("index is out of bounds", {"param": "index", "reason": "out_of_range", "max": item_list.item_count - 1, "value": index})
|
|
409
|
+
if params_invalid(reader):
|
|
410
|
+
return
|
|
411
|
+
item_list.remove_item(index)
|
|
412
|
+
respond({"success": true, "action": "remove"})
|
|
413
|
+
"clear":
|
|
414
|
+
item_list.clear()
|
|
415
|
+
respond({"success": true, "action": "clear"})
|
|
416
|
+
elif node is OptionButton:
|
|
417
|
+
var option_button: OptionButton = node as OptionButton
|
|
418
|
+
var action: String = reader.optional_enum("action", "get_items", ["get_items", "select", "add", "remove", "clear"])
|
|
419
|
+
if params_invalid(reader):
|
|
420
|
+
return
|
|
421
|
+
match action:
|
|
422
|
+
"get_items":
|
|
423
|
+
var items: Array = []
|
|
424
|
+
for i in option_button.item_count:
|
|
425
|
+
items.append({"index": i, "text": option_button.get_item_text(i)})
|
|
426
|
+
respond({"success": true, "items": items, "selected": option_button.selected})
|
|
427
|
+
"select":
|
|
428
|
+
var index: int = reader.required_int("index", 0)
|
|
429
|
+
if index >= option_button.item_count:
|
|
430
|
+
reader.fail("index is out of bounds", {"param": "index", "reason": "out_of_range", "max": option_button.item_count - 1, "value": index})
|
|
431
|
+
if params_invalid(reader):
|
|
432
|
+
return
|
|
433
|
+
option_button.select(index)
|
|
434
|
+
respond({"success": true, "action": "select"})
|
|
435
|
+
"add":
|
|
436
|
+
option_button.add_item(reader.required_string("text"))
|
|
437
|
+
if params_invalid(reader):
|
|
438
|
+
return
|
|
439
|
+
respond({"success": true, "action": "add"})
|
|
440
|
+
"remove":
|
|
441
|
+
var index: int = reader.required_int("index", 0)
|
|
442
|
+
if index >= option_button.item_count:
|
|
443
|
+
reader.fail("index is out of bounds", {"param": "index", "reason": "out_of_range", "max": option_button.item_count - 1, "value": index})
|
|
444
|
+
if params_invalid(reader):
|
|
445
|
+
return
|
|
446
|
+
option_button.remove_item(index)
|
|
447
|
+
respond({"success": true, "action": "remove"})
|
|
448
|
+
"clear":
|
|
449
|
+
option_button.clear()
|
|
450
|
+
respond({"success": true, "action": "clear"})
|
|
451
|
+
else:
|
|
452
|
+
_require_class(reader, node, "ItemList or OptionButton")
|
|
453
|
+
send_params_error(reader)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
# --- Tabs ---
|
|
457
|
+
func _cmd_ui_tabs(params: Dictionary) -> void:
|
|
458
|
+
var reader: CommandParams = CommandParams.new(params)
|
|
459
|
+
var node: Node = require_node(reader)
|
|
460
|
+
if params_invalid(reader):
|
|
461
|
+
return
|
|
462
|
+
|
|
463
|
+
if node is TabContainer:
|
|
464
|
+
var tab_container: TabContainer = node as TabContainer
|
|
465
|
+
var action: String = reader.optional_enum("action", "get_tabs", ["get_tabs", "set_current", "set_title"])
|
|
466
|
+
if params_invalid(reader):
|
|
467
|
+
return
|
|
468
|
+
match action:
|
|
469
|
+
"get_tabs":
|
|
470
|
+
var tabs: Array = []
|
|
471
|
+
for i in tab_container.get_tab_count():
|
|
472
|
+
tabs.append({"index": i, "title": tab_container.get_tab_title(i)})
|
|
473
|
+
respond({"success": true, "tabs": tabs, "current": tab_container.current_tab})
|
|
474
|
+
"set_current":
|
|
475
|
+
var index: int = reader.required_int("index", 0)
|
|
476
|
+
if index >= tab_container.get_tab_count():
|
|
477
|
+
reader.fail("index is out of bounds", {"param": "index", "reason": "out_of_range", "max": tab_container.get_tab_count() - 1, "value": index})
|
|
478
|
+
if params_invalid(reader):
|
|
479
|
+
return
|
|
480
|
+
tab_container.current_tab = index
|
|
481
|
+
respond({"success": true, "action": "set_current"})
|
|
482
|
+
"set_title":
|
|
483
|
+
var index: int = reader.required_int("index", 0)
|
|
484
|
+
var title: String = reader.required_string("title")
|
|
485
|
+
if index >= tab_container.get_tab_count():
|
|
486
|
+
reader.fail("index is out of bounds", {"param": "index", "reason": "out_of_range", "max": tab_container.get_tab_count() - 1, "value": index})
|
|
487
|
+
if params_invalid(reader):
|
|
488
|
+
return
|
|
489
|
+
tab_container.set_tab_title(index, title)
|
|
490
|
+
respond({"success": true, "action": "set_title"})
|
|
491
|
+
elif node is TabBar:
|
|
492
|
+
var tab_bar: TabBar = node as TabBar
|
|
493
|
+
var action: String = reader.optional_enum("action", "get_tabs", ["get_tabs", "set_current", "set_title"])
|
|
494
|
+
if params_invalid(reader):
|
|
495
|
+
return
|
|
496
|
+
match action:
|
|
497
|
+
"get_tabs":
|
|
498
|
+
var tabs: Array = []
|
|
499
|
+
for i in tab_bar.tab_count:
|
|
500
|
+
tabs.append({"index": i, "title": tab_bar.get_tab_title(i)})
|
|
501
|
+
respond({"success": true, "tabs": tabs, "current": tab_bar.current_tab})
|
|
502
|
+
"set_current":
|
|
503
|
+
var index: int = reader.required_int("index", 0)
|
|
504
|
+
if index >= tab_bar.tab_count:
|
|
505
|
+
reader.fail("index is out of bounds", {"param": "index", "reason": "out_of_range", "max": tab_bar.tab_count - 1, "value": index})
|
|
506
|
+
if params_invalid(reader):
|
|
507
|
+
return
|
|
508
|
+
tab_bar.current_tab = index
|
|
509
|
+
respond({"success": true, "action": "set_current"})
|
|
510
|
+
"set_title":
|
|
511
|
+
var index: int = reader.required_int("index", 0)
|
|
512
|
+
var title: String = reader.required_string("title")
|
|
513
|
+
if index >= tab_bar.tab_count:
|
|
514
|
+
reader.fail("index is out of bounds", {"param": "index", "reason": "out_of_range", "max": tab_bar.tab_count - 1, "value": index})
|
|
515
|
+
if params_invalid(reader):
|
|
516
|
+
return
|
|
517
|
+
tab_bar.set_tab_title(index, title)
|
|
518
|
+
respond({"success": true, "action": "set_title"})
|
|
519
|
+
else:
|
|
520
|
+
_require_class(reader, node, "TabContainer or TabBar")
|
|
521
|
+
send_params_error(reader)
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
# --- Popup menus ---
|
|
525
|
+
func _cmd_ui_menu(params: Dictionary) -> void:
|
|
526
|
+
var reader: CommandParams = CommandParams.new(params)
|
|
527
|
+
var node: Node = require_node(reader)
|
|
528
|
+
var action: String = reader.optional_enum("action", "get_items", ["get_items", "add", "remove", "set_checked", "clear"])
|
|
529
|
+
if node != null and not node is PopupMenu:
|
|
530
|
+
_require_class(reader, node, "PopupMenu")
|
|
531
|
+
if params_invalid(reader):
|
|
532
|
+
return
|
|
533
|
+
|
|
534
|
+
var menu: PopupMenu = node as PopupMenu
|
|
535
|
+
match action:
|
|
536
|
+
"get_items":
|
|
537
|
+
var items: Array = []
|
|
538
|
+
for i in menu.item_count:
|
|
539
|
+
var shortcut: Shortcut = menu.get_item_shortcut(i)
|
|
540
|
+
items.append({"index": i, "text": menu.get_item_text(i), "checked": menu.is_item_checked(i), "disabled": menu.is_item_disabled(i), "id": menu.get_item_id(i), "shortcut": shortcut.get_as_text() if shortcut != null else ""})
|
|
541
|
+
respond({"success": true, "items": items})
|
|
542
|
+
"add":
|
|
543
|
+
var text: String = reader.required_string("text")
|
|
544
|
+
var id: int = reader.optional_int("id", -1)
|
|
545
|
+
if params_invalid(reader):
|
|
546
|
+
return
|
|
547
|
+
menu.add_item(text, id)
|
|
548
|
+
if reader.has_param("shortcut_key"):
|
|
549
|
+
var shortcut_key: String = reader.required_string("shortcut_key")
|
|
550
|
+
var keycode: Key = OS.find_keycode_from_string(shortcut_key)
|
|
551
|
+
if keycode == KEY_NONE:
|
|
552
|
+
reader.fail("Unknown shortcut key: %s" % shortcut_key, {"param": "shortcut_key", "reason": "invalid_value", "value": shortcut_key})
|
|
553
|
+
if params_invalid(reader):
|
|
554
|
+
menu.remove_item(menu.item_count - 1)
|
|
555
|
+
return
|
|
556
|
+
var event: InputEventKey = InputEventKey.new()
|
|
557
|
+
event.keycode = keycode
|
|
558
|
+
var shortcut: Shortcut = Shortcut.new()
|
|
559
|
+
shortcut.events = [event]
|
|
560
|
+
menu.set_item_shortcut(menu.item_count - 1, shortcut)
|
|
561
|
+
respond({"success": true, "action": "add"})
|
|
562
|
+
"remove":
|
|
563
|
+
var index: int = reader.required_int("index", 0)
|
|
564
|
+
if index >= menu.item_count:
|
|
565
|
+
reader.fail("index is out of bounds", {"param": "index", "reason": "out_of_range", "max": menu.item_count - 1, "value": index})
|
|
566
|
+
if params_invalid(reader):
|
|
567
|
+
return
|
|
568
|
+
menu.remove_item(index)
|
|
569
|
+
respond({"success": true, "action": "remove"})
|
|
570
|
+
"set_checked":
|
|
571
|
+
var index: int = reader.required_int("index", 0)
|
|
572
|
+
if index >= menu.item_count:
|
|
573
|
+
reader.fail("index is out of bounds", {"param": "index", "reason": "out_of_range", "max": menu.item_count - 1, "value": index})
|
|
574
|
+
if not reader.has_param("checked"):
|
|
575
|
+
reader.fail("checked is required", {"param": "checked", "reason": "missing"})
|
|
576
|
+
var checked: bool = reader.optional_bool("checked", false)
|
|
577
|
+
if params_invalid(reader):
|
|
578
|
+
return
|
|
579
|
+
menu.set_item_checked(index, checked)
|
|
580
|
+
respond({"success": true, "action": "set_checked"})
|
|
581
|
+
"clear":
|
|
582
|
+
menu.clear()
|
|
583
|
+
respond({"success": true, "action": "clear"})
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
# --- Range / ColorPicker values ---
|
|
587
|
+
func _cmd_ui_range(params: Dictionary) -> void:
|
|
588
|
+
var reader: CommandParams = CommandParams.new(params)
|
|
589
|
+
var node: Node = require_node(reader)
|
|
590
|
+
var action: String = reader.optional_enum("action", "get", ["get", "set"])
|
|
591
|
+
if params_invalid(reader):
|
|
592
|
+
return
|
|
593
|
+
|
|
594
|
+
if node is Range:
|
|
595
|
+
var range_node: Range = node as Range
|
|
596
|
+
if action == "get":
|
|
597
|
+
respond({"success": true, "value": range_node.value, "min": range_node.min_value, "max": range_node.max_value, "step": range_node.step})
|
|
598
|
+
return
|
|
599
|
+
if reader.has_param("min_value"): range_node.min_value = reader.required_number("min_value")
|
|
600
|
+
if reader.has_param("max_value"): range_node.max_value = reader.required_number("max_value")
|
|
601
|
+
if reader.has_param("step"): range_node.step = reader.required_number("step")
|
|
602
|
+
if reader.has_param("value"): range_node.value = reader.required_number("value")
|
|
603
|
+
if params_invalid(reader):
|
|
604
|
+
return
|
|
605
|
+
respond({"success": true, "action": "set", "value": range_node.value})
|
|
606
|
+
elif node is ColorPicker:
|
|
607
|
+
var picker: ColorPicker = node as ColorPicker
|
|
608
|
+
if action == "get":
|
|
609
|
+
var c: Color = picker.color
|
|
610
|
+
respond({"success": true, "color": {"r": c.r, "g": c.g, "b": c.b, "a": c.a}})
|
|
611
|
+
return
|
|
612
|
+
var color: Dictionary = reader.required_dictionary("color")
|
|
613
|
+
if params_invalid(reader):
|
|
614
|
+
return
|
|
615
|
+
picker.color = _color_from(color)
|
|
616
|
+
respond({"success": true, "action": "set"})
|
|
617
|
+
else:
|
|
618
|
+
_require_class(reader, node, "Range or ColorPicker")
|
|
619
|
+
send_params_error(reader)
|