gopeak 2.3.7 → 2.3.8

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