@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.
Files changed (70) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +925 -0
  3. package/addons/godot_agent_loop/LICENSE +23 -0
  4. package/addons/godot_agent_loop/README.md +31 -0
  5. package/addons/godot_agent_loop/plugin.cfg +8 -0
  6. package/addons/godot_agent_loop/plugin.gd +417 -0
  7. package/agent-plugin/.claude-plugin/plugin.json +19 -0
  8. package/agent-plugin/.codex-plugin/plugin.json +37 -0
  9. package/agent-plugin/.mcp.json +14 -0
  10. package/agent-plugin/adapter-manifest.json +45 -0
  11. package/agent-plugin/pi/extension.ts +136 -0
  12. package/agent-plugin/skills/build-godot-game/SKILL.md +41 -0
  13. package/agent-plugin/skills/debug-godot-game/SKILL.md +37 -0
  14. package/agent-plugin/skills/ship-godot-game/SKILL.md +46 -0
  15. package/agent-plugin/skills/ship-godot-game/agents/openai.yaml +4 -0
  16. package/agent-plugin/skills/verify-godot-change/SKILL.md +35 -0
  17. package/build/authoring-session-manager.js +237 -0
  18. package/build/domain-tool-registries.js +207 -0
  19. package/build/editor-bridge-protocol.js +1 -0
  20. package/build/editor-connection.js +112 -0
  21. package/build/editor-mutation-guard.js +30 -0
  22. package/build/editor-plugin-installer.js +220 -0
  23. package/build/engine-api/extension_api-4.7.stable.official.5b4e0cb0f.json +356830 -0
  24. package/build/game-command-service.js +49 -0
  25. package/build/game-connection.js +337 -0
  26. package/build/godot-executable.js +156 -0
  27. package/build/godot-process-manager.js +112 -0
  28. package/build/godot-subprocess.js +23 -0
  29. package/build/headless-operation-runner.js +68 -0
  30. package/build/headless-operation-service.js +65 -0
  31. package/build/index.js +478 -0
  32. package/build/interaction-server-installer.js +234 -0
  33. package/build/opencode-setup.js +199 -0
  34. package/build/project-support.js +219 -0
  35. package/build/runtime-protocol.js +202 -0
  36. package/build/scripts/godot_operations.gd +1534 -0
  37. package/build/scripts/mcp_editor_plugin.gd +417 -0
  38. package/build/scripts/mcp_interaction_server.gd +1255 -0
  39. package/build/scripts/mcp_runtime/audio_animation_domain.gd +568 -0
  40. package/build/scripts/mcp_runtime/command_params.gd +339 -0
  41. package/build/scripts/mcp_runtime/core_domain.gd +591 -0
  42. package/build/scripts/mcp_runtime/input_domain.gd +695 -0
  43. package/build/scripts/mcp_runtime/networking_domain.gd +356 -0
  44. package/build/scripts/mcp_runtime/physics_domain.gd +653 -0
  45. package/build/scripts/mcp_runtime/privileged_command_policy.gd +81 -0
  46. package/build/scripts/mcp_runtime/rendering_domain.gd +807 -0
  47. package/build/scripts/mcp_runtime/runtime_domain.gd +108 -0
  48. package/build/scripts/mcp_runtime/scene_2d_domain.gd +527 -0
  49. package/build/scripts/mcp_runtime/scene_3d_domain.gd +573 -0
  50. package/build/scripts/mcp_runtime/system_domain.gd +277 -0
  51. package/build/scripts/mcp_runtime/ui_domain.gd +619 -0
  52. package/build/scripts/mcp_runtime/variant_codec.gd +335 -0
  53. package/build/scripts/validate_script.gd +28 -0
  54. package/build/server-instructions.js +11 -0
  55. package/build/session-timing.js +20 -0
  56. package/build/tool-argument-validation.js +120 -0
  57. package/build/tool-definitions.js +2727 -0
  58. package/build/tool-handlers/game-tool-handlers.js +1345 -0
  59. package/build/tool-handlers/lifecycle-tool-handlers.js +346 -0
  60. package/build/tool-handlers/project-handler-services.js +1458 -0
  61. package/build/tool-handlers/project-tool-handlers.js +1506 -0
  62. package/build/tool-handlers/visual-regression-service.js +139 -0
  63. package/build/tool-manifest.js +1193 -0
  64. package/build/tool-mutation-policy.js +122 -0
  65. package/build/tool-registry.js +34 -0
  66. package/build/tool-surface.js +70 -0
  67. package/build/utils.js +273 -0
  68. package/package.json +110 -0
  69. package/product.json +52 -0
  70. 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)