@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,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
|