@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,591 @@
1
+ extends "res://mcp_runtime/runtime_domain.gd"
2
+
3
+ # Core scene, node, property, group, and signal commands. This domain owns
4
+ # scene-tree mutation and introspection while transport lifecycle stays in the
5
+ # composition root.
6
+
7
+ var _await_signal_received: bool = false
8
+ var _await_signal_args: Array = []
9
+
10
+ func register_commands() -> void:
11
+ register_command("get_scene_tree", _cmd_get_scene_tree)
12
+ register_command("get_property", _cmd_get_property)
13
+ register_command("set_property", _cmd_set_property)
14
+ register_command("call_method", _cmd_call_method)
15
+ register_command("get_node_info", _cmd_get_node_info)
16
+ register_command("instantiate_scene", _cmd_instantiate_scene)
17
+ register_command("remove_node", _cmd_remove_node)
18
+ register_command("change_scene", _cmd_change_scene)
19
+ register_command("connect_signal", _cmd_connect_signal)
20
+ register_command("disconnect_signal", _cmd_disconnect_signal)
21
+ register_command("emit_signal", _cmd_emit_signal)
22
+ register_command("get_nodes_in_group", _cmd_get_nodes_in_group)
23
+ register_command("find_nodes_by_class", _cmd_find_nodes_by_class)
24
+ register_command("reparent_node", _cmd_reparent_node)
25
+ register_command("spawn_node", _cmd_spawn_node)
26
+ register_command("manage_group", _cmd_manage_group)
27
+ register_command("list_signals", _cmd_list_signals)
28
+ register_command("await_signal", _cmd_await_signal)
29
+
30
+
31
+ func _cmd_get_scene_tree(params: Dictionary) -> void:
32
+ var reader: CommandParams = CommandParams.new(params)
33
+ var max_nodes: int = reader.optional_int("max_nodes", 1000, 1, 10000)
34
+ if params_invalid(reader):
35
+ return
36
+ var traversal: Dictionary = {"count": 0, "truncated": false, "max_nodes": max_nodes}
37
+ var tree: Dictionary = _build_tree_node(get_tree().root, traversal)
38
+ respond({"success": true, "tree": tree, "node_count": traversal["count"], "truncated": traversal["truncated"], "max_nodes": max_nodes})
39
+
40
+ func _build_tree_node(node: Node, traversal: Dictionary) -> Dictionary:
41
+ var count_value: Variant = traversal.get("count", 0)
42
+ var count: int = count_value if count_value is int else 0
43
+ traversal["count"] = count + 1
44
+ var info: Dictionary = {
45
+ "name": node.name,
46
+ "type": node.get_class(),
47
+ }
48
+ var children_arr: Array = []
49
+ for child in node.get_children():
50
+ var current_value: Variant = traversal.get("count", 0)
51
+ var maximum_value: Variant = traversal.get("max_nodes", 1000)
52
+ var current: int = current_value if current_value is int else 0
53
+ var maximum: int = maximum_value if maximum_value is int else 1000
54
+ if current >= maximum:
55
+ traversal["truncated"] = true
56
+ break
57
+ children_arr.append(_build_tree_node(child, traversal))
58
+ if children_arr.size() > 0:
59
+ info["children"] = children_arr
60
+ return info
61
+
62
+
63
+ func _cmd_get_property(params: Dictionary) -> void:
64
+ var reader: CommandParams = CommandParams.new(params)
65
+ var node_path: String = reader.required_node_path()
66
+ var property: String = reader.required_string("property")
67
+ var node: Node = require_node(reader)
68
+ if params_invalid(reader):
69
+ return
70
+ if not property in node:
71
+ reader.fail("Property not found: %s on node %s" % [property, node_path], {"param": "property", "reason": "property_not_found", "value": property})
72
+ send_params_error(reader)
73
+ return
74
+
75
+ var value: Variant = node.get(property)
76
+ respond({"success": true, "value": variant_to_json(value), "property": property, "node_path": node_path})
77
+
78
+
79
+ # --- Set Property ---
80
+
81
+ func _cmd_set_property(params: Dictionary) -> void:
82
+ var reader: CommandParams = CommandParams.new(params)
83
+ var node_path: String = reader.required_node_path()
84
+ var property: String = reader.required_string("property")
85
+ var type_hint: String = reader.optional_string("type_hint", "")
86
+ var node: Node = require_node(reader)
87
+ if params_invalid(reader):
88
+ return
89
+
90
+ var raw_value: Variant = reader.raw("value")
91
+ var value: Variant
92
+ if type_hint.is_empty():
93
+ value = json_to_variant_for_property(node, property, raw_value)
94
+ else:
95
+ value = json_to_variant(raw_value, type_hint)
96
+ node.set(property, value)
97
+ respond({"success": true, "node_path": node_path, "property": property, "value": variant_to_json(node.get(property))})
98
+
99
+
100
+ # --- Call Method ---
101
+
102
+ func _cmd_call_method(params: Dictionary) -> void:
103
+ var reader: CommandParams = CommandParams.new(params)
104
+ var node_path: String = reader.required_node_path()
105
+ var method_name: String = reader.required_string("method")
106
+ var args: Array = reader.optional_array("args")
107
+ var node: Node = require_node(reader)
108
+ if not reader.failed() and not node.has_method(method_name):
109
+ reader.fail("Method not found: %s on node %s" % [method_name, node_path], {"param": "method", "reason": "method_not_found", "value": method_name})
110
+ if params_invalid(reader):
111
+ return
112
+
113
+ var result: Variant = node.callv(method_name, args)
114
+ respond({"success": true, "result": variant_to_json(result)})
115
+
116
+
117
+ # --- Get Node Info ---
118
+
119
+ func _cmd_get_node_info(params: Dictionary) -> void:
120
+ var node_path: String = params.get("node_path", "")
121
+ if node_path.is_empty():
122
+ respond({"error": "node_path is required"})
123
+ return
124
+
125
+ var node: Node = get_tree().root.get_node_or_null(node_path)
126
+ if node == null:
127
+ respond({"error": "Node not found: %s" % node_path})
128
+ return
129
+
130
+ var properties: Array = []
131
+ for prop in node.get_property_list():
132
+ var prop_dict: Dictionary = prop
133
+ if prop_dict.get("usage", 0) & PROPERTY_USAGE_EDITOR:
134
+ var property_name: String = CommandParams.json_string(prop_dict, "name")
135
+ properties.append({
136
+ "name": property_name,
137
+ "type": prop_dict.get("type", 0),
138
+ "value": variant_to_json(node.get(property_name))
139
+ })
140
+
141
+ var signals: Array = []
142
+ for sig in node.get_signal_list():
143
+ var sig_dict: Dictionary = sig
144
+ signals.append(sig_dict.get("name", ""))
145
+
146
+ var methods: Array = []
147
+ for m in node.get_method_list():
148
+ var m_dict: Dictionary = m
149
+ if not str(m_dict.get("name", "")).begins_with("_"):
150
+ methods.append(m_dict.get("name", ""))
151
+
152
+ var children: Array = []
153
+ for child in node.get_children():
154
+ children.append({
155
+ "name": child.name,
156
+ "type": child.get_class(),
157
+ "path": str(child.get_path())
158
+ })
159
+
160
+ respond({
161
+ "success": true,
162
+ "class": node.get_class(),
163
+ "name": node.name,
164
+ "path": str(node.get_path()),
165
+ "properties": properties,
166
+ "signals": signals,
167
+ "methods": methods,
168
+ "children": children
169
+ })
170
+
171
+
172
+ # --- Instantiate Scene ---
173
+
174
+ func _cmd_instantiate_scene(params: Dictionary) -> void:
175
+ var reader: CommandParams = CommandParams.new(params)
176
+ var scene_path: String = reader.required_resource_path("scene_path")
177
+ var parent: Node = require_node(reader, "parent_path", "/root")
178
+ if params_invalid(reader):
179
+ return
180
+
181
+ var packed: PackedScene = load(scene_path) as PackedScene
182
+ if packed == null:
183
+ respond({"error": "Failed to load scene: %s" % scene_path, "error_data": {"param": "scene_path", "reason": "resource_not_found", "value": scene_path}})
184
+ return
185
+
186
+ var instance: Node = packed.instantiate()
187
+ parent.add_child(instance)
188
+ respond({"success": true, "instance_name": instance.name, "instance_path": str(instance.get_path())})
189
+
190
+
191
+ # --- Remove Node ---
192
+
193
+ func _cmd_remove_node(params: Dictionary) -> void:
194
+ var node_path: String = params.get("node_path", "")
195
+ if node_path.is_empty():
196
+ respond({"error": "node_path is required"})
197
+ return
198
+
199
+ var node: Node = get_tree().root.get_node_or_null(node_path)
200
+ if node == null:
201
+ respond({"error": "Node not found: %s" % node_path})
202
+ return
203
+
204
+ var node_name: String = node.name
205
+ node.queue_free()
206
+ respond({"success": true, "removed": node_name})
207
+
208
+
209
+ # --- Change Scene ---
210
+
211
+ func _cmd_change_scene(params: Dictionary) -> void:
212
+ var reader: CommandParams = CommandParams.new(params)
213
+ var scene_path: String = reader.required_resource_path("scene_path")
214
+ if params_invalid(reader):
215
+ return
216
+
217
+ var err: int = get_tree().change_scene_to_file(scene_path)
218
+ if err != OK:
219
+ respond({"error": "Failed to change scene to %s: %s" % [scene_path, error_string(err)], "error_data": godot_error_data(err)})
220
+ return
221
+
222
+ respond({"success": true, "scene": scene_path})
223
+
224
+
225
+ # --- Pause ---
226
+
227
+ func _cmd_connect_signal(params: Dictionary) -> void:
228
+ var reader := CommandParams.new(params)
229
+ var node_path: String = reader.required_node_path()
230
+ var signal_name: String = reader.required_string("signal_name")
231
+ var target_path: String = reader.required_node_path("target_path")
232
+ var method_name: String = reader.required_string("method")
233
+ var binds: Array = reader.optional_array("binds")
234
+ var deferred: bool = reader.optional_bool("deferred", false)
235
+ var one_shot: bool = reader.optional_bool("one_shot", false)
236
+ var reference_counted: bool = reader.optional_bool("reference_counted", false)
237
+ if params_invalid(reader):
238
+ return
239
+
240
+ var node: Node = get_tree().root.get_node_or_null(node_path)
241
+ if node == null:
242
+ respond({"error": "Source node not found: %s" % node_path})
243
+ return
244
+
245
+ var target: Node = get_tree().root.get_node_or_null(target_path)
246
+ if target == null:
247
+ respond({"error": "Target node not found: %s" % target_path})
248
+ return
249
+
250
+ if not node.has_signal(signal_name):
251
+ respond({"error": "Signal '%s' not found on node %s" % [signal_name, node_path]})
252
+ return
253
+
254
+ if not target.has_method(method_name):
255
+ respond({"error": "Method '%s' not found on target %s" % [method_name, target_path]})
256
+ return
257
+
258
+ var callable: Callable = Callable(target, method_name).bindv(binds)
259
+ if node.is_connected(signal_name, callable) and not reference_counted:
260
+ respond({"error": "Signal already connected"})
261
+ return
262
+
263
+ var flags: int = 0
264
+ if deferred:
265
+ flags |= CONNECT_DEFERRED
266
+ if one_shot:
267
+ flags |= CONNECT_ONE_SHOT
268
+ if reference_counted:
269
+ flags |= CONNECT_REFERENCE_COUNTED
270
+ var err: int = node.connect(signal_name, callable, flags)
271
+ if err != OK:
272
+ respond({"error": "Failed to connect signal: %s" % error_string(err), "error_data": godot_error_data(err)})
273
+ return
274
+ respond({"success": true, "signal": signal_name, "from": node_path, "to": target_path, "method": method_name, "bind_count": binds.size(), "flags": flags})
275
+
276
+
277
+ # --- Disconnect Signal ---
278
+
279
+ func _cmd_disconnect_signal(params: Dictionary) -> void:
280
+ var reader := CommandParams.new(params)
281
+ var node_path: String = reader.required_node_path()
282
+ var signal_name: String = reader.required_string("signal_name")
283
+ var target_path: String = reader.required_node_path("target_path")
284
+ var method_name: String = reader.required_string("method")
285
+ var binds: Array = reader.optional_array("binds")
286
+ if params_invalid(reader):
287
+ return
288
+
289
+ var node: Node = get_tree().root.get_node_or_null(node_path)
290
+ if node == null:
291
+ respond({"error": "Source node not found: %s" % node_path})
292
+ return
293
+
294
+ var target: Node = get_tree().root.get_node_or_null(target_path)
295
+ if target == null:
296
+ respond({"error": "Target node not found: %s" % target_path})
297
+ return
298
+
299
+ var callable: Callable = Callable(target, method_name).bindv(binds)
300
+ if not node.is_connected(signal_name, callable):
301
+ respond({"error": "Signal is not connected"})
302
+ return
303
+
304
+ node.disconnect(signal_name, callable)
305
+ respond({"success": true, "disconnected": signal_name, "from": node_path, "to": target_path, "method": method_name})
306
+
307
+
308
+ # --- Emit Signal ---
309
+
310
+ func _cmd_emit_signal(params: Dictionary) -> void:
311
+ var node_path: String = params.get("node_path", "")
312
+ var signal_name: String = params.get("signal_name", "")
313
+ if node_path.is_empty() or signal_name.is_empty():
314
+ respond({"error": "node_path and signal_name are required"})
315
+ return
316
+
317
+ var node: Node = get_tree().root.get_node_or_null(node_path)
318
+ if node == null:
319
+ respond({"error": "Node not found: %s" % node_path})
320
+ return
321
+
322
+ if not node.has_signal(signal_name):
323
+ respond({"error": "Signal '%s' not found on node %s" % [signal_name, node_path]})
324
+ return
325
+
326
+ var args: Array = params.get("args", [])
327
+ var call_args: Array = [signal_name]
328
+ call_args.append_array(args)
329
+ node.callv("emit_signal", call_args)
330
+ respond({"success": true, "emitted": signal_name, "node": node_path, "arg_count": args.size()})
331
+
332
+
333
+ # --- Play Animation ---
334
+
335
+ func _cmd_get_nodes_in_group(params: Dictionary) -> void:
336
+ var group_name: String = params.get("group", "")
337
+ if group_name.is_empty():
338
+ respond({"error": "group is required"})
339
+ return
340
+
341
+ var nodes: Array = get_tree().get_nodes_in_group(group_name)
342
+ var result: Array = []
343
+ for node: Node in nodes:
344
+ result.append({
345
+ "name": node.name,
346
+ "type": node.get_class(),
347
+ "path": str(node.get_path())
348
+ })
349
+ respond({"success": true, "group": group_name, "count": result.size(), "nodes": result})
350
+
351
+
352
+ # --- Find Nodes By Class ---
353
+
354
+ func _cmd_find_nodes_by_class(params: Dictionary) -> void:
355
+ var class_filter: String = params.get("class_name", "")
356
+ if class_filter.is_empty():
357
+ respond({"error": "class_name is required"})
358
+ return
359
+
360
+ var root_path: String = params.get("root_path", "/root")
361
+ var root_node: Node = get_tree().root.get_node_or_null(root_path)
362
+ if root_node == null:
363
+ respond({"error": "Root node not found: %s" % root_path})
364
+ return
365
+
366
+ var found: Array = []
367
+ _find_by_class_recursive(root_node, class_filter, found)
368
+ respond({"success": true, "class_name": class_filter, "count": found.size(), "nodes": found})
369
+
370
+ func _find_by_class_recursive(node: Node, class_filter: String, results: Array) -> void:
371
+ if node.get_class() == class_filter or node.is_class(class_filter):
372
+ results.append({
373
+ "name": node.name,
374
+ "type": node.get_class(),
375
+ "path": str(node.get_path())
376
+ })
377
+ for child in node.get_children():
378
+ _find_by_class_recursive(child, class_filter, results)
379
+
380
+
381
+ # --- Reparent Node ---
382
+
383
+ func _cmd_reparent_node(params: Dictionary) -> void:
384
+ var node_path: String = params.get("node_path", "")
385
+ var new_parent_path: String = params.get("new_parent_path", "")
386
+ if node_path.is_empty() or new_parent_path.is_empty():
387
+ respond({"error": "node_path and new_parent_path are required"})
388
+ return
389
+
390
+ var node: Node = get_tree().root.get_node_or_null(node_path)
391
+ if node == null:
392
+ respond({"error": "Node not found: %s" % node_path})
393
+ return
394
+
395
+ var new_parent: Node = get_tree().root.get_node_or_null(new_parent_path)
396
+ if new_parent == null:
397
+ respond({"error": "New parent not found: %s" % new_parent_path})
398
+ return
399
+
400
+ var keep_global: bool = params.get("keep_global_transform", true)
401
+ node.reparent(new_parent, keep_global)
402
+ respond({"success": true, "node": node.name, "new_parent": new_parent_path, "new_path": str(node.get_path())})
403
+
404
+
405
+ # --- Get Camera ---
406
+
407
+ func _cmd_spawn_node(params: Dictionary) -> void:
408
+ var type_name: String = params.get("type", "")
409
+ var node_name: String = params.get("name", "")
410
+ var parent_path: String = params.get("parent_path", "/root")
411
+
412
+ if type_name.is_empty():
413
+ respond({"error": "type is required"})
414
+ return
415
+
416
+ if not ClassDB.class_exists(type_name):
417
+ respond({"error": "Unknown class: %s" % type_name})
418
+ return
419
+
420
+ if not ClassDB.is_parent_class(type_name, "Node") and type_name != "Node":
421
+ respond({"error": "Class '%s' is not a Node type" % type_name})
422
+ return
423
+
424
+ var parent: Node = get_tree().root.get_node_or_null(parent_path)
425
+ if parent == null:
426
+ respond({"error": "Parent node not found: %s" % parent_path})
427
+ return
428
+
429
+ # A class that is not a Node (or does not exist) instantiates to a value this
430
+ # command cannot add to the tree.
431
+ var instantiated: Variant = ClassDB.instantiate(type_name)
432
+ if not instantiated is Node:
433
+ respond({"error": "Failed to instantiate: %s" % type_name})
434
+ return
435
+ var instance: Node = instantiated
436
+
437
+ if node_name.length() > 0:
438
+ instance.name = node_name
439
+
440
+ # Apply properties if provided
441
+ var properties: Dictionary = CommandParams.json_dictionary(params, "properties")
442
+ for prop_name: Variant in properties:
443
+ var property: String = str(prop_name)
444
+ var raw_value: Variant = properties[prop_name]
445
+ var value: Variant = json_to_variant_for_property(instance, property, raw_value)
446
+ instance.set(property, value)
447
+
448
+ parent.add_child(instance)
449
+ respond({"success": true, "name": instance.name, "type": type_name, "path": str(instance.get_path())})
450
+
451
+
452
+ # --- Set Shader Parameter ---
453
+
454
+ func _cmd_manage_group(params: Dictionary) -> void:
455
+ var action: String = params.get("action", "")
456
+ var group_name: String = params.get("group", "")
457
+
458
+ if action == "clear_group":
459
+ if group_name.is_empty():
460
+ respond({"error": "group is required for clear_group"})
461
+ return
462
+ var nodes: Array = get_tree().get_nodes_in_group(group_name)
463
+ for node: Node in nodes:
464
+ node.remove_from_group(group_name)
465
+ respond({"success": true, "action": "clear_group", "group": group_name, "removed_count": nodes.size()})
466
+ return
467
+
468
+ var node_path: String = params.get("node_path", "")
469
+ if node_path.is_empty():
470
+ respond({"error": "node_path is required"})
471
+ return
472
+
473
+ var node: Node = get_tree().root.get_node_or_null(node_path)
474
+ if node == null:
475
+ respond({"error": "Node not found: %s" % node_path})
476
+ return
477
+
478
+ match action:
479
+ "add":
480
+ if group_name.is_empty():
481
+ respond({"error": "group is required for add"})
482
+ return
483
+ node.add_to_group(group_name)
484
+ respond({"success": true, "action": "add", "node_path": node_path, "group": group_name})
485
+ "remove":
486
+ if group_name.is_empty():
487
+ respond({"error": "group is required for remove"})
488
+ return
489
+ node.remove_from_group(group_name)
490
+ respond({"success": true, "action": "remove", "node_path": node_path, "group": group_name})
491
+ "get_groups":
492
+ var groups: Array = []
493
+ for g in node.get_groups():
494
+ groups.append(str(g))
495
+ respond({"success": true, "action": "get_groups", "node_path": node_path, "groups": groups})
496
+ _:
497
+ respond({"error": "Unknown group action: %s. Use add, remove, get_groups, or clear_group" % action})
498
+
499
+
500
+ # --- Create Timer ---
501
+
502
+ func _cmd_list_signals(params: Dictionary) -> void:
503
+ var reader: CommandParams = CommandParams.new(params)
504
+ var node_path: String = reader.required_node_path()
505
+ var node: Node = require_node(reader)
506
+ if params_invalid(reader):
507
+ return
508
+ var signals: Array = []
509
+ for sig: Dictionary in node.get_signal_list():
510
+ var sig_name: String = CommandParams.json_string(sig, "name")
511
+ var connections: Array = []
512
+ for conn: Dictionary in node.get_signal_connection_list(sig_name):
513
+ connections.append({"callable": str(conn["callable"]), "flags": conn["flags"]})
514
+ signals.append({"name": sig_name, "args": str(sig["args"]), "connections": connections})
515
+ respond({"success": true, "node_path": node_path, "signals": signals})
516
+
517
+ func _cmd_await_signal(params: Dictionary) -> void:
518
+ var reader := CommandParams.new(params)
519
+ var node_path: String = reader.required_node_path()
520
+ var signal_name: String = reader.required_string("signal_name")
521
+ var timeout: float = reader.optional_number("timeout", 10.0, 0.01, 30.0)
522
+ var node: Node = require_node(reader)
523
+ if params_invalid(reader):
524
+ return
525
+ if not node.has_signal(signal_name):
526
+ reader.fail("Signal not found", {"param": "signal_name", "reason": "signal_not_found", "value": signal_name})
527
+ send_params_error(reader)
528
+ return
529
+ var argument_count: int = 0
530
+ for signal_info: Dictionary in node.get_signal_list():
531
+ if CommandParams.json_string(signal_info, "name") == signal_name:
532
+ argument_count = CommandParams.json_array(signal_info, "args").size()
533
+ break
534
+ if argument_count > 8:
535
+ reader.fail("Signal has too many arguments to await", {"param": "signal_name", "reason": "limit_exceeded", "max_args": 8, "actual_args": argument_count})
536
+ send_params_error(reader)
537
+ return
538
+ var timer: SceneTreeTimer = get_tree().create_timer(timeout)
539
+ _await_signal_received = false
540
+ _await_signal_args = []
541
+ var cb := Callable(self, "_capture_signal_%d" % argument_count)
542
+ @warning_ignore("return_value_discarded")
543
+ node.connect(signal_name, cb, CONNECT_ONE_SHOT)
544
+ while not _await_signal_received and timer.time_left > 0:
545
+ await get_tree().process_frame
546
+ if cancellation_requested():
547
+ break
548
+ if not is_instance_valid(node):
549
+ respond({"error": "Node was freed while awaiting signal", "error_data": {"reason": "node_freed", "node_path": node_path, "signal_name": signal_name}})
550
+ return
551
+ if is_instance_valid(node) and node.is_connected(signal_name, cb):
552
+ node.disconnect(signal_name, cb)
553
+ if cancellation_requested():
554
+ respond({})
555
+ return
556
+ if _await_signal_received:
557
+ respond({"success": true, "signal_name": signal_name, "received": true, "args": variant_to_json(_await_signal_args)})
558
+ else:
559
+ respond_timeout("Signal wait timed out", {"command": "await_signal", "timeout_seconds": timeout})
560
+
561
+
562
+ func _capture_signal_0() -> void:
563
+ _capture_signal_args([])
564
+
565
+ func _capture_signal_1(a: Variant) -> void:
566
+ _capture_signal_args([a])
567
+
568
+ func _capture_signal_2(a: Variant, b: Variant) -> void:
569
+ _capture_signal_args([a, b])
570
+
571
+ func _capture_signal_3(a: Variant, b: Variant, c: Variant) -> void:
572
+ _capture_signal_args([a, b, c])
573
+
574
+ func _capture_signal_4(a: Variant, b: Variant, c: Variant, d: Variant) -> void:
575
+ _capture_signal_args([a, b, c, d])
576
+
577
+ func _capture_signal_5(a: Variant, b: Variant, c: Variant, d: Variant, e: Variant) -> void:
578
+ _capture_signal_args([a, b, c, d, e])
579
+
580
+ func _capture_signal_6(a: Variant, b: Variant, c: Variant, d: Variant, e: Variant, f: Variant) -> void:
581
+ _capture_signal_args([a, b, c, d, e, f])
582
+
583
+ func _capture_signal_7(a: Variant, b: Variant, c: Variant, d: Variant, e: Variant, f: Variant, g: Variant) -> void:
584
+ _capture_signal_args([a, b, c, d, e, f, g])
585
+
586
+ func _capture_signal_8(a: Variant, b: Variant, c: Variant, d: Variant, e: Variant, f: Variant, g: Variant, h: Variant) -> void:
587
+ _capture_signal_args([a, b, c, d, e, f, g, h])
588
+
589
+ func _capture_signal_args(args: Array) -> void:
590
+ _await_signal_received = true
591
+ _await_signal_args = args