gopeak 2.1.0 → 2.2.1

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.
@@ -0,0 +1,710 @@
1
+ @tool
2
+ extends Node
3
+ class_name MCPSceneTools
4
+
5
+ var _editor_plugin: EditorPlugin = null
6
+
7
+ func set_editor_plugin(plugin: EditorPlugin) -> void:
8
+ _editor_plugin = plugin
9
+
10
+
11
+ func _refresh_and_reload(scene_path: String) -> void:
12
+ _refresh_filesystem()
13
+ _reload_scene_in_editor(scene_path)
14
+
15
+
16
+ func _refresh_filesystem() -> void:
17
+ if _editor_plugin:
18
+ _editor_plugin.get_editor_interface().get_resource_filesystem().scan()
19
+
20
+
21
+ func _reload_scene_in_editor(scene_path: String) -> void:
22
+ if not _editor_plugin:
23
+ return
24
+ var ei := _editor_plugin.get_editor_interface()
25
+ var edited := ei.get_edited_scene_root()
26
+ if edited and edited.scene_file_path == scene_path:
27
+ ei.reload_scene_from_path(scene_path)
28
+
29
+
30
+ func _ensure_res_path(path: String) -> String:
31
+ if not path.begins_with("res://"):
32
+ return "res://" + path
33
+ return path
34
+
35
+
36
+ func _to_scene_res_path(project_path: String, scene_path: String) -> String:
37
+ var p := scene_path.strip_edges()
38
+ if p.begins_with("res://"):
39
+ return p
40
+
41
+ if project_path.strip_edges() != "":
42
+ var normalized_project := project_path.replace("\\", "/")
43
+ var normalized_scene := p.replace("\\", "/")
44
+ if normalized_scene.begins_with(normalized_project):
45
+ var rel := normalized_scene.substr(normalized_project.length())
46
+ if rel.begins_with("/"):
47
+ rel = rel.substr(1)
48
+ return _ensure_res_path(rel)
49
+
50
+ return _ensure_res_path(p)
51
+
52
+
53
+ func _load_scene(scene_path: String) -> Array:
54
+ if not FileAccess.file_exists(scene_path):
55
+ return [null, {"ok": false, "error": "Scene not found: " + scene_path}]
56
+ var packed := load(scene_path) as PackedScene
57
+ if not packed:
58
+ return [null, {"ok": false, "error": "Failed to load: " + scene_path}]
59
+ var root := packed.instantiate()
60
+ if not root:
61
+ return [null, {"ok": false, "error": "Failed to instantiate: " + scene_path}]
62
+ return [root, {}]
63
+
64
+
65
+ func _save_scene(scene_root: Node, scene_path: String) -> Dictionary:
66
+ var packed := PackedScene.new()
67
+ if packed.pack(scene_root) != OK:
68
+ scene_root.queue_free()
69
+ return {"ok": false, "error": "Failed to pack scene"}
70
+ if ResourceSaver.save(packed, scene_path) != OK:
71
+ scene_root.queue_free()
72
+ return {"ok": false, "error": "Failed to save scene"}
73
+ scene_root.queue_free()
74
+ _refresh_and_reload(scene_path)
75
+ return {}
76
+
77
+
78
+ func _find_node(root: Node, path: String) -> Node:
79
+ if path == "." or path.is_empty():
80
+ return root
81
+ return root.get_node_or_null(path)
82
+
83
+
84
+ func _parse_value(value):
85
+ if typeof(value) == TYPE_DICTIONARY and value.has("type"):
86
+ match value["type"]:
87
+ "Vector2":
88
+ return Vector2(value.get("x", 0), value.get("y", 0))
89
+ "Vector3":
90
+ return Vector3(value.get("x", 0), value.get("y", 0), value.get("z", 0))
91
+ "Color":
92
+ return Color(value.get("r", 1), value.get("g", 1), value.get("b", 1), value.get("a", 1))
93
+ "Vector2i":
94
+ return Vector2i(value.get("x", 0), value.get("y", 0))
95
+ "Vector3i":
96
+ return Vector3i(value.get("x", 0), value.get("y", 0), value.get("z", 0))
97
+ "Rect2":
98
+ return Rect2(value.get("x", 0), value.get("y", 0), value.get("width", 0), value.get("height", 0))
99
+ "Transform2D":
100
+ if value.has("x") and value.has("y") and value.has("origin"):
101
+ var xx: Dictionary = value["x"]
102
+ var yy: Dictionary = value["y"]
103
+ var oo: Dictionary = value["origin"]
104
+ return Transform2D(
105
+ Vector2(xx.get("x", 1), xx.get("y", 0)),
106
+ Vector2(yy.get("x", 0), yy.get("y", 1)),
107
+ Vector2(oo.get("x", 0), oo.get("y", 0))
108
+ )
109
+ "Transform3D":
110
+ if value.has("basis") and value.has("origin"):
111
+ var b: Dictionary = value["basis"]
112
+ var o: Dictionary = value["origin"]
113
+ var basis := Basis(
114
+ Vector3(b.get("x", {}).get("x", 1), b.get("x", {}).get("y", 0), b.get("x", {}).get("z", 0)),
115
+ Vector3(b.get("y", {}).get("x", 0), b.get("y", {}).get("y", 1), b.get("y", {}).get("z", 0)),
116
+ Vector3(b.get("z", {}).get("x", 0), b.get("z", {}).get("y", 0), b.get("z", {}).get("z", 1))
117
+ )
118
+ return Transform3D(basis, Vector3(o.get("x", 0), o.get("y", 0), o.get("z", 0)))
119
+ "NodePath":
120
+ return NodePath(value.get("path", ""))
121
+ "Resource":
122
+ var resource_path: String = str(value.get("path", ""))
123
+ if resource_path.is_empty():
124
+ return null
125
+ return load(resource_path)
126
+ if typeof(value) == TYPE_ARRAY:
127
+ var result: Array = []
128
+ for item in value:
129
+ result.append(_parse_value(item))
130
+ return result
131
+ return value
132
+
133
+
134
+ func _serialize_value(value) -> Variant:
135
+ match typeof(value):
136
+ TYPE_VECTOR2:
137
+ return {"type": "Vector2", "x": value.x, "y": value.y}
138
+ TYPE_VECTOR3:
139
+ return {"type": "Vector3", "x": value.x, "y": value.y, "z": value.z}
140
+ TYPE_COLOR:
141
+ return {"type": "Color", "r": value.r, "g": value.g, "b": value.b, "a": value.a}
142
+ TYPE_VECTOR2I:
143
+ return {"type": "Vector2i", "x": value.x, "y": value.y}
144
+ TYPE_VECTOR3I:
145
+ return {"type": "Vector3i", "x": value.x, "y": value.y, "z": value.z}
146
+ TYPE_RECT2:
147
+ return {"type": "Rect2", "x": value.position.x, "y": value.position.y, "width": value.size.x, "height": value.size.y}
148
+ TYPE_NODE_PATH:
149
+ return {"type": "NodePath", "path": str(value)}
150
+ TYPE_TRANSFORM2D:
151
+ return {
152
+ "type": "Transform2D",
153
+ "x": {"x": value.x.x, "y": value.x.y},
154
+ "y": {"x": value.y.x, "y": value.y.y},
155
+ "origin": {"x": value.origin.x, "y": value.origin.y}
156
+ }
157
+ TYPE_TRANSFORM3D:
158
+ return {
159
+ "type": "Transform3D",
160
+ "basis": {
161
+ "x": {"x": value.basis.x.x, "y": value.basis.x.y, "z": value.basis.x.z},
162
+ "y": {"x": value.basis.y.x, "y": value.basis.y.y, "z": value.basis.y.z},
163
+ "z": {"x": value.basis.z.x, "y": value.basis.z.y, "z": value.basis.z.z}
164
+ },
165
+ "origin": {"x": value.origin.x, "y": value.origin.y, "z": value.origin.z}
166
+ }
167
+ TYPE_OBJECT:
168
+ if value and value is Resource and value.resource_path:
169
+ return {"type": "Resource", "path": value.resource_path}
170
+ return null
171
+ _:
172
+ return value
173
+
174
+
175
+ func _set_node_properties(node: Node, properties: Dictionary) -> void:
176
+ for prop_name in properties:
177
+ var val = _parse_value(properties[prop_name])
178
+ node.set(prop_name, val)
179
+
180
+
181
+ func _parse_properties_arg(raw_properties) -> Dictionary:
182
+ if typeof(raw_properties) == TYPE_DICTIONARY:
183
+ return raw_properties
184
+ if typeof(raw_properties) == TYPE_STRING:
185
+ var text := String(raw_properties)
186
+ if text.strip_edges().is_empty():
187
+ return {}
188
+ var parsed = JSON.parse_string(text)
189
+ if typeof(parsed) == TYPE_DICTIONARY:
190
+ return parsed
191
+ return {}
192
+
193
+
194
+ func _ensure_parent_dir_for_scene(scene_path: String) -> void:
195
+ var base_dir := scene_path.get_base_dir()
196
+ if not DirAccess.dir_exists_absolute(base_dir):
197
+ DirAccess.make_dir_recursive_absolute(base_dir)
198
+
199
+
200
+ func _set_owner_recursive(node: Node, scene_owner: Node) -> void:
201
+ node.owner = scene_owner
202
+ for child in node.get_children():
203
+ if child is Node:
204
+ _set_owner_recursive(child as Node, scene_owner)
205
+
206
+
207
+ func _build_node_tree(node: Node, include_properties: bool, depth: int, current_depth: int, node_path: String) -> Dictionary:
208
+ var data := {
209
+ "name": str(node.name),
210
+ "type": node.get_class(),
211
+ "path": node_path,
212
+ "children": []
213
+ }
214
+
215
+ if include_properties:
216
+ var props := {}
217
+ for p in node.get_property_list():
218
+ if not (p.get("usage", 0) & PROPERTY_USAGE_STORAGE):
219
+ continue
220
+ var pn := str(p.get("name", ""))
221
+ if pn.is_empty():
222
+ continue
223
+ props[pn] = _serialize_value(node.get(pn))
224
+ data["properties"] = props
225
+
226
+ if depth >= 0 and current_depth >= depth:
227
+ return data
228
+
229
+ for child in node.get_children():
230
+ if child is Node:
231
+ var child_node := child as Node
232
+ var child_path := str(child_node.name) if node_path == "." else node_path + "/" + str(child_node.name)
233
+ data["children"].append(_build_node_tree(child_node, include_properties, depth, current_depth + 1, child_path))
234
+
235
+ return data
236
+
237
+
238
+ func _collect_nodes_recursive(node: Node, path: String, out_nodes: Array) -> void:
239
+ out_nodes.append({"path": path, "node": node})
240
+ for child in node.get_children():
241
+ if child is Node:
242
+ var child_node := child as Node
243
+ var child_path := str(child_node.name) if path == "." else path + "/" + str(child_node.name)
244
+ _collect_nodes_recursive(child_node, child_path, out_nodes)
245
+
246
+
247
+ func create_scene(args: Dictionary) -> Dictionary:
248
+ var project_path := str(args.get("projectPath", ""))
249
+ var scene_path := _to_scene_res_path(project_path, str(args.get("scenePath", "")))
250
+ var root_node_type := str(args.get("rootNodeType", "Node"))
251
+ var script_path := str(args.get("scriptPath", ""))
252
+
253
+ if scene_path == "res://":
254
+ return {"ok": false, "error": "Missing scenePath"}
255
+ if not scene_path.ends_with(".tscn"):
256
+ scene_path += ".tscn"
257
+ if not ClassDB.class_exists(root_node_type):
258
+ return {"ok": false, "error": "Invalid rootNodeType: " + root_node_type}
259
+
260
+ _ensure_parent_dir_for_scene(scene_path)
261
+
262
+ var root := ClassDB.instantiate(root_node_type) as Node
263
+ if not root:
264
+ return {"ok": false, "error": "Failed to instantiate root node: " + root_node_type}
265
+ root.name = root_node_type
266
+
267
+ if not script_path.is_empty():
268
+ var full_script_path := _to_scene_res_path(project_path, script_path)
269
+ var script = load(full_script_path)
270
+ if not script:
271
+ root.queue_free()
272
+ return {"ok": false, "error": "Failed to load script: " + full_script_path}
273
+ root.set_script(script)
274
+
275
+ var err := _save_scene(root, scene_path)
276
+ if not err.is_empty():
277
+ return err
278
+
279
+ return {"ok": true, "scenePath": scene_path, "rootNodeType": root_node_type}
280
+
281
+
282
+ func list_scene_nodes(args: Dictionary) -> Dictionary:
283
+ var project_path := str(args.get("projectPath", ""))
284
+ var scene_path := _to_scene_res_path(project_path, str(args.get("scenePath", "")))
285
+ var depth := int(args.get("depth", -1))
286
+ var include_properties := bool(args.get("includeProperties", false))
287
+
288
+ var result := _load_scene(scene_path)
289
+ if not result[1].is_empty():
290
+ return result[1]
291
+
292
+ var root := result[0] as Node
293
+ var tree := _build_node_tree(root, include_properties, depth, 0, ".")
294
+ root.queue_free()
295
+ return {"ok": true, "tree": tree}
296
+
297
+
298
+ func add_node(args: Dictionary) -> Dictionary:
299
+ var project_path := str(args.get("projectPath", ""))
300
+ var scene_path := _to_scene_res_path(project_path, str(args.get("scenePath", "")))
301
+ var node_type := str(args.get("nodeType", ""))
302
+ var node_name := str(args.get("nodeName", ""))
303
+ var parent_node_path := str(args.get("parentNodePath", "."))
304
+ var properties := _parse_properties_arg(args.get("properties", {}))
305
+
306
+ if node_type.is_empty() or node_name.is_empty():
307
+ return {"ok": false, "error": "Missing nodeType or nodeName"}
308
+ if not ClassDB.class_exists(node_type):
309
+ return {"ok": false, "error": "Invalid nodeType: " + node_type}
310
+
311
+ var result := _load_scene(scene_path)
312
+ if not result[1].is_empty():
313
+ return result[1]
314
+
315
+ var root := result[0] as Node
316
+ var parent := _find_node(root, parent_node_path)
317
+ if not parent:
318
+ root.queue_free()
319
+ return {"ok": false, "error": "Parent node not found: " + parent_node_path}
320
+
321
+ var new_node := ClassDB.instantiate(node_type) as Node
322
+ if not new_node:
323
+ root.queue_free()
324
+ return {"ok": false, "error": "Failed to instantiate nodeType: " + node_type}
325
+
326
+ new_node.name = node_name
327
+ _set_node_properties(new_node, properties)
328
+ parent.add_child(new_node)
329
+ _set_owner_recursive(new_node, root)
330
+
331
+ var err := _save_scene(root, scene_path)
332
+ if not err.is_empty():
333
+ return err
334
+
335
+ return {"ok": true, "nodeName": node_name, "nodeType": node_type}
336
+
337
+
338
+ func delete_node(args: Dictionary) -> Dictionary:
339
+ var project_path := str(args.get("projectPath", ""))
340
+ var scene_path := _to_scene_res_path(project_path, str(args.get("scenePath", "")))
341
+ var node_path := str(args.get("nodePath", ""))
342
+
343
+ if node_path.is_empty() or node_path == ".":
344
+ return {"ok": false, "error": "Cannot delete root node"}
345
+
346
+ var result := _load_scene(scene_path)
347
+ if not result[1].is_empty():
348
+ return result[1]
349
+
350
+ var root := result[0] as Node
351
+ var node := _find_node(root, node_path)
352
+ if not node:
353
+ root.queue_free()
354
+ return {"ok": false, "error": "Node not found: " + node_path}
355
+
356
+ var parent := node.get_parent()
357
+ if not parent:
358
+ root.queue_free()
359
+ return {"ok": false, "error": "Cannot delete root node"}
360
+
361
+ parent.remove_child(node)
362
+ node.queue_free()
363
+
364
+ var err := _save_scene(root, scene_path)
365
+ if not err.is_empty():
366
+ return err
367
+
368
+ return {"ok": true, "deletedNodePath": node_path}
369
+
370
+
371
+ func duplicate_node(args: Dictionary) -> Dictionary:
372
+ var project_path := str(args.get("projectPath", ""))
373
+ var scene_path := _to_scene_res_path(project_path, str(args.get("scenePath", "")))
374
+ var node_path := str(args.get("nodePath", ""))
375
+ var new_name := str(args.get("newName", ""))
376
+ var parent_path := str(args.get("parentPath", ""))
377
+
378
+ if node_path.is_empty() or new_name.is_empty():
379
+ return {"ok": false, "error": "Missing nodePath or newName"}
380
+
381
+ var result := _load_scene(scene_path)
382
+ if not result[1].is_empty():
383
+ return result[1]
384
+
385
+ var root := result[0] as Node
386
+ var source := _find_node(root, node_path)
387
+ if not source:
388
+ root.queue_free()
389
+ return {"ok": false, "error": "Node not found: " + node_path}
390
+
391
+ var target_parent: Node = source.get_parent()
392
+ if not parent_path.is_empty():
393
+ target_parent = _find_node(root, parent_path)
394
+ if not target_parent:
395
+ root.queue_free()
396
+ return {"ok": false, "error": "Parent not found: " + parent_path}
397
+
398
+ var duplicated_node := source.duplicate() as Node
399
+ if not duplicated_node:
400
+ root.queue_free()
401
+ return {"ok": false, "error": "Failed to duplicate node: " + node_path}
402
+
403
+ duplicated_node.name = new_name
404
+ target_parent.add_child(duplicated_node)
405
+ _set_owner_recursive(duplicated_node, root)
406
+
407
+ var err := _save_scene(root, scene_path)
408
+ if not err.is_empty():
409
+ return err
410
+
411
+ return {"ok": true, "nodePath": node_path, "newName": new_name}
412
+
413
+
414
+ func reparent_node(args: Dictionary) -> Dictionary:
415
+ var project_path := str(args.get("projectPath", ""))
416
+ var scene_path := _to_scene_res_path(project_path, str(args.get("scenePath", "")))
417
+ var node_path := str(args.get("nodePath", ""))
418
+ var new_parent_path := str(args.get("newParentPath", ""))
419
+
420
+ if node_path.is_empty() or node_path == ".":
421
+ return {"ok": false, "error": "Cannot reparent root node"}
422
+ if new_parent_path.is_empty():
423
+ return {"ok": false, "error": "Missing newParentPath"}
424
+
425
+ var result := _load_scene(scene_path)
426
+ if not result[1].is_empty():
427
+ return result[1]
428
+
429
+ var root := result[0] as Node
430
+ var node := _find_node(root, node_path)
431
+ var new_parent := _find_node(root, new_parent_path)
432
+ if not node:
433
+ root.queue_free()
434
+ return {"ok": false, "error": "Node not found: " + node_path}
435
+ if not new_parent:
436
+ root.queue_free()
437
+ return {"ok": false, "error": "New parent not found: " + new_parent_path}
438
+
439
+ var old_parent := node.get_parent()
440
+ if not old_parent:
441
+ root.queue_free()
442
+ return {"ok": false, "error": "Cannot reparent root node"}
443
+
444
+ old_parent.remove_child(node)
445
+ new_parent.add_child(node)
446
+ _set_owner_recursive(node, root)
447
+
448
+ var err := _save_scene(root, scene_path)
449
+ if not err.is_empty():
450
+ return err
451
+
452
+ return {"ok": true, "nodePath": node_path, "newParentPath": new_parent_path}
453
+
454
+
455
+ func set_node_properties(args: Dictionary) -> Dictionary:
456
+ var project_path := str(args.get("projectPath", ""))
457
+ var scene_path := _to_scene_res_path(project_path, str(args.get("scenePath", "")))
458
+ var node_path := str(args.get("nodePath", "."))
459
+ var properties := _parse_properties_arg(args.get("properties", {}))
460
+
461
+ var result := _load_scene(scene_path)
462
+ if not result[1].is_empty():
463
+ return result[1]
464
+
465
+ var root := result[0] as Node
466
+ var node := _find_node(root, node_path)
467
+ if not node:
468
+ root.queue_free()
469
+ return {"ok": false, "error": "Node not found: " + node_path}
470
+
471
+ _set_node_properties(node, properties)
472
+
473
+ var err := _save_scene(root, scene_path)
474
+ if not err.is_empty():
475
+ return err
476
+
477
+ return {"ok": true, "nodePath": node_path}
478
+
479
+
480
+ func get_node_properties(args: Dictionary) -> Dictionary:
481
+ var project_path := str(args.get("projectPath", ""))
482
+ var scene_path := _to_scene_res_path(project_path, str(args.get("scenePath", "")))
483
+ var node_path := str(args.get("nodePath", "."))
484
+ var include_defaults := bool(args.get("includeDefaults", false))
485
+
486
+ var result := _load_scene(scene_path)
487
+ if not result[1].is_empty():
488
+ return result[1]
489
+
490
+ var root := result[0] as Node
491
+ var node := _find_node(root, node_path)
492
+ if not node:
493
+ root.queue_free()
494
+ return {"ok": false, "error": "Node not found: " + node_path}
495
+
496
+ var defaults: Node = null
497
+ if not include_defaults and ClassDB.class_exists(node.get_class()):
498
+ defaults = ClassDB.instantiate(node.get_class()) as Node
499
+
500
+ var props := {}
501
+ for p in node.get_property_list():
502
+ var usage := int(p.get("usage", 0))
503
+ if not (usage & PROPERTY_USAGE_STORAGE):
504
+ continue
505
+ var prop_name := str(p.get("name", ""))
506
+ if prop_name.is_empty():
507
+ continue
508
+ var current_val = node.get(prop_name)
509
+ if not include_defaults and defaults:
510
+ var default_val = defaults.get(prop_name)
511
+ if current_val == default_val:
512
+ continue
513
+ props[prop_name] = _serialize_value(current_val)
514
+
515
+ if defaults:
516
+ defaults.queue_free()
517
+ root.queue_free()
518
+ return {"ok": true, "nodePath": node_path, "properties": props}
519
+
520
+
521
+ func load_sprite(args: Dictionary) -> Dictionary:
522
+ var project_path := str(args.get("projectPath", ""))
523
+ var scene_path := _to_scene_res_path(project_path, str(args.get("scenePath", "")))
524
+ var node_path := str(args.get("nodePath", "."))
525
+ var texture_path := _to_scene_res_path(project_path, str(args.get("texturePath", "")))
526
+
527
+ if texture_path == "res://":
528
+ return {"ok": false, "error": "Missing texturePath"}
529
+
530
+ var texture = load(texture_path)
531
+ if not texture or not (texture is Texture2D):
532
+ return {"ok": false, "error": "Failed to load texture: " + texture_path}
533
+
534
+ var result := _load_scene(scene_path)
535
+ if not result[1].is_empty():
536
+ return result[1]
537
+
538
+ var root := result[0] as Node
539
+ var node := _find_node(root, node_path)
540
+ if not node:
541
+ root.queue_free()
542
+ return {"ok": false, "error": "Node not found: " + node_path}
543
+
544
+ if node is Sprite2D:
545
+ (node as Sprite2D).texture = texture as Texture2D
546
+ elif node is Sprite3D:
547
+ (node as Sprite3D).texture = texture as Texture2D
548
+ else:
549
+ root.queue_free()
550
+ return {"ok": false, "error": "Node is not Sprite2D or Sprite3D: " + node_path}
551
+
552
+ var err := _save_scene(root, scene_path)
553
+ if not err.is_empty():
554
+ return err
555
+
556
+ return {"ok": true, "nodePath": node_path, "texturePath": texture_path}
557
+
558
+
559
+ func save_scene(args: Dictionary) -> Dictionary:
560
+ var project_path := str(args.get("projectPath", ""))
561
+ var scene_path := _to_scene_res_path(project_path, str(args.get("scenePath", "")))
562
+ var new_path_raw := str(args.get("newPath", ""))
563
+ var target_path := scene_path
564
+ if not new_path_raw.is_empty():
565
+ target_path = _to_scene_res_path(project_path, new_path_raw)
566
+
567
+ var result := _load_scene(scene_path)
568
+ if not result[1].is_empty():
569
+ return result[1]
570
+
571
+ _ensure_parent_dir_for_scene(target_path)
572
+ var root := result[0] as Node
573
+ var err := _save_scene(root, target_path)
574
+ if not err.is_empty():
575
+ return err
576
+
577
+ return {"ok": true, "scenePath": scene_path, "savedPath": target_path}
578
+
579
+
580
+ func connect_signal(args: Dictionary) -> Dictionary:
581
+ var project_path := str(args.get("projectPath", ""))
582
+ var scene_path := _to_scene_res_path(project_path, str(args.get("scenePath", "")))
583
+ var source_node_path := str(args.get("sourceNodePath", ""))
584
+ var signal_name := str(args.get("signalName", ""))
585
+ var target_node_path := str(args.get("targetNodePath", ""))
586
+ var method_name := str(args.get("methodName", ""))
587
+ var flags := int(args.get("flags", 0))
588
+
589
+ if source_node_path.is_empty() or signal_name.is_empty() or target_node_path.is_empty() or method_name.is_empty():
590
+ return {"ok": false, "error": "Missing required signal connection arguments"}
591
+
592
+ var result := _load_scene(scene_path)
593
+ if not result[1].is_empty():
594
+ return result[1]
595
+
596
+ var root := result[0] as Node
597
+ var source := _find_node(root, source_node_path)
598
+ var target := _find_node(root, target_node_path)
599
+ if not source:
600
+ root.queue_free()
601
+ return {"ok": false, "error": "Source node not found: " + source_node_path}
602
+ if not target:
603
+ root.queue_free()
604
+ return {"ok": false, "error": "Target node not found: " + target_node_path}
605
+ if not source.has_signal(signal_name):
606
+ root.queue_free()
607
+ return {"ok": false, "error": "Signal not found on source: " + signal_name}
608
+
609
+ var callable := Callable(target, method_name)
610
+ if not source.is_connected(signal_name, callable):
611
+ var connect_result := source.connect(signal_name, callable, flags)
612
+ if connect_result != OK:
613
+ root.queue_free()
614
+ return {"ok": false, "error": "Failed to connect signal: " + str(connect_result)}
615
+
616
+ var err := _save_scene(root, scene_path)
617
+ if not err.is_empty():
618
+ return err
619
+
620
+ return {
621
+ "ok": true,
622
+ "sourceNodePath": source_node_path,
623
+ "signalName": signal_name,
624
+ "targetNodePath": target_node_path,
625
+ "methodName": method_name,
626
+ "flags": flags
627
+ }
628
+
629
+
630
+ func disconnect_signal(args: Dictionary) -> Dictionary:
631
+ var project_path := str(args.get("projectPath", ""))
632
+ var scene_path := _to_scene_res_path(project_path, str(args.get("scenePath", "")))
633
+ var source_node_path := str(args.get("sourceNodePath", ""))
634
+ var signal_name := str(args.get("signalName", ""))
635
+ var target_node_path := str(args.get("targetNodePath", ""))
636
+ var method_name := str(args.get("methodName", ""))
637
+
638
+ if source_node_path.is_empty() or signal_name.is_empty() or target_node_path.is_empty() or method_name.is_empty():
639
+ return {"ok": false, "error": "Missing required signal disconnection arguments"}
640
+
641
+ var result := _load_scene(scene_path)
642
+ if not result[1].is_empty():
643
+ return result[1]
644
+
645
+ var root := result[0] as Node
646
+ var source := _find_node(root, source_node_path)
647
+ var target := _find_node(root, target_node_path)
648
+ if not source:
649
+ root.queue_free()
650
+ return {"ok": false, "error": "Source node not found: " + source_node_path}
651
+ if not target:
652
+ root.queue_free()
653
+ return {"ok": false, "error": "Target node not found: " + target_node_path}
654
+
655
+ var callable := Callable(target, method_name)
656
+ if source.is_connected(signal_name, callable):
657
+ source.disconnect(signal_name, callable)
658
+
659
+ var err := _save_scene(root, scene_path)
660
+ if not err.is_empty():
661
+ return err
662
+
663
+ return {
664
+ "ok": true,
665
+ "sourceNodePath": source_node_path,
666
+ "signalName": signal_name,
667
+ "targetNodePath": target_node_path,
668
+ "methodName": method_name
669
+ }
670
+
671
+
672
+ func list_connections(args: Dictionary) -> Dictionary:
673
+ var project_path := str(args.get("projectPath", ""))
674
+ var scene_path := _to_scene_res_path(project_path, str(args.get("scenePath", "")))
675
+ var filter_path := str(args.get("nodePath", ""))
676
+
677
+ var result := _load_scene(scene_path)
678
+ if not result[1].is_empty():
679
+ return result[1]
680
+
681
+ var root := result[0] as Node
682
+ var nodes: Array = []
683
+ _collect_nodes_recursive(root, ".", nodes)
684
+
685
+ var connections: Array = []
686
+ for entry in nodes:
687
+ var path := str(entry["path"])
688
+ if not filter_path.is_empty() and filter_path != path:
689
+ continue
690
+ var node := entry["node"] as Node
691
+ for signal_info in node.get_signal_list():
692
+ var signal_name := str(signal_info.get("name", ""))
693
+ if signal_name.is_empty():
694
+ continue
695
+ for conn in node.get_signal_connection_list(signal_name):
696
+ var callable: Callable = conn.get("callable", Callable())
697
+ var target_obj: Object = callable.get_object()
698
+ var target_path := ""
699
+ if target_obj and target_obj is Node:
700
+ target_path = str(root.get_path_to(target_obj as Node))
701
+ connections.append({
702
+ "sourceNodePath": path,
703
+ "signalName": signal_name,
704
+ "targetNodePath": target_path,
705
+ "methodName": str(callable.get_method()),
706
+ "flags": int(conn.get("flags", 0))
707
+ })
708
+
709
+ root.queue_free()
710
+ return {"ok": true, "connections": connections}