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.
- package/README.md +224 -75
- package/build/addon/godot_mcp_editor/mcp_client.gd +178 -0
- package/build/addon/godot_mcp_editor/plugin.cfg +6 -0
- package/build/addon/godot_mcp_editor/plugin.gd +84 -0
- package/build/addon/godot_mcp_editor/tool_executor.gd +114 -0
- package/build/addon/godot_mcp_editor/tools/animation_tools.gd +502 -0
- package/build/addon/godot_mcp_editor/tools/resource_tools.gd +425 -0
- package/build/addon/godot_mcp_editor/tools/scene_tools.gd +710 -0
- package/build/cli/check.js +77 -0
- package/build/cli/notify.js +88 -0
- package/build/cli/setup.js +115 -0
- package/build/cli/star.js +51 -0
- package/build/cli/uninstall.js +26 -0
- package/build/cli/utils.js +149 -0
- package/build/cli.js +91 -0
- package/build/gdscript_parser.js +828 -0
- package/build/godot-bridge.js +556 -0
- package/build/index.js +2761 -2064
- package/build/prompts.js +163 -0
- package/build/visualizer/canvas.js +832 -0
- package/build/visualizer/events.js +814 -0
- package/build/visualizer/layout.js +304 -0
- package/build/visualizer/main.js +245 -0
- package/build/visualizer/modals.js +239 -0
- package/build/visualizer/panel.js +1091 -0
- package/build/visualizer/state.js +210 -0
- package/build/visualizer/syntax.js +106 -0
- package/build/visualizer/usages.js +352 -0
- package/build/visualizer/websocket.js +85 -0
- package/build/visualizer-server.js +375 -0
- package/build/visualizer.html +6395 -0
- package/package.json +15 -6
|
@@ -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}
|