godot-mcp-runtime 2.2.0 → 2.2.2
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 +61 -64
- package/dist/dispatch.d.ts +26 -0
- package/dist/dispatch.d.ts.map +1 -0
- package/dist/dispatch.js +70 -0
- package/dist/dispatch.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +31 -118
- package/dist/index.js.map +1 -1
- package/dist/scripts/godot_operations.gd +1117 -1117
- package/dist/scripts/mcp_bridge.gd +9 -1
- package/dist/tools/node-tools.d.ts +1 -1
- package/dist/tools/node-tools.d.ts.map +1 -1
- package/dist/tools/node-tools.js +168 -56
- package/dist/tools/node-tools.js.map +1 -1
- package/dist/tools/project-tools.d.ts +4 -11
- package/dist/tools/project-tools.d.ts.map +1 -1
- package/dist/tools/project-tools.js +489 -153
- package/dist/tools/project-tools.js.map +1 -1
- package/dist/tools/scene-tools.d.ts +1 -1
- package/dist/tools/scene-tools.d.ts.map +1 -1
- package/dist/tools/scene-tools.js +168 -44
- package/dist/tools/scene-tools.js.map +1 -1
- package/dist/tools/validate-tools.d.ts +1 -1
- package/dist/tools/validate-tools.d.ts.map +1 -1
- package/dist/tools/validate-tools.js +44 -15
- package/dist/tools/validate-tools.js.map +1 -1
- package/dist/utils/godot-runner.d.ts +39 -1
- package/dist/utils/godot-runner.d.ts.map +1 -1
- package/dist/utils/godot-runner.js +200 -36
- package/dist/utils/godot-runner.js.map +1 -1
- package/package.json +22 -4
|
@@ -1,1117 +1,1117 @@
|
|
|
1
|
-
#!/usr/bin/env -S godot --headless --script
|
|
2
|
-
extends SceneTree
|
|
3
|
-
|
|
4
|
-
# Debug mode flag
|
|
5
|
-
var debug_mode = false
|
|
6
|
-
|
|
7
|
-
func _init():
|
|
8
|
-
var args = OS.get_cmdline_args()
|
|
9
|
-
|
|
10
|
-
# Check for debug flag
|
|
11
|
-
debug_mode = "--debug-godot" in args
|
|
12
|
-
|
|
13
|
-
# Find the script argument and determine the positions of operation and params
|
|
14
|
-
var script_index = args.find("--script")
|
|
15
|
-
if script_index == -1:
|
|
16
|
-
log_error("Could not find --script argument")
|
|
17
|
-
quit(1)
|
|
18
|
-
|
|
19
|
-
var operation_index = script_index + 2
|
|
20
|
-
var params_index = script_index + 3
|
|
21
|
-
|
|
22
|
-
if args.size() <= params_index:
|
|
23
|
-
log_error("Usage: godot --headless --script godot_operations.gd <operation> <json_params>")
|
|
24
|
-
log_error("Not enough command-line arguments provided.")
|
|
25
|
-
quit(1)
|
|
26
|
-
|
|
27
|
-
log_debug("All arguments: " + str(args))
|
|
28
|
-
|
|
29
|
-
var operation = args[operation_index]
|
|
30
|
-
var params_json = args[params_index]
|
|
31
|
-
|
|
32
|
-
log_info("Operation: " + operation)
|
|
33
|
-
log_debug("Params JSON: " + params_json)
|
|
34
|
-
|
|
35
|
-
var json = JSON.new()
|
|
36
|
-
var error = json.parse(params_json)
|
|
37
|
-
var params = null
|
|
38
|
-
|
|
39
|
-
if error == OK:
|
|
40
|
-
params = json.get_data()
|
|
41
|
-
else:
|
|
42
|
-
log_error("Failed to parse JSON parameters: " + params_json)
|
|
43
|
-
log_error("JSON Error: " + json.get_error_message() + " at line " + str(json.get_error_line()))
|
|
44
|
-
quit(1)
|
|
45
|
-
|
|
46
|
-
if not params:
|
|
47
|
-
log_error("Failed to parse JSON parameters: " + params_json)
|
|
48
|
-
quit(1)
|
|
49
|
-
|
|
50
|
-
log_info("Executing operation: " + operation)
|
|
51
|
-
|
|
52
|
-
match operation:
|
|
53
|
-
# Original operations
|
|
54
|
-
"create_scene":
|
|
55
|
-
create_scene(params)
|
|
56
|
-
"add_node":
|
|
57
|
-
add_node(params)
|
|
58
|
-
"load_sprite":
|
|
59
|
-
load_sprite(params)
|
|
60
|
-
"export_mesh_library":
|
|
61
|
-
export_mesh_library(params)
|
|
62
|
-
"save_scene":
|
|
63
|
-
save_scene(params)
|
|
64
|
-
"get_uid":
|
|
65
|
-
get_uid(params)
|
|
66
|
-
"resave_resources":
|
|
67
|
-
resave_resources(params)
|
|
68
|
-
# New node operations
|
|
69
|
-
"delete_node":
|
|
70
|
-
delete_node(params)
|
|
71
|
-
"
|
|
72
|
-
|
|
73
|
-
"get_node_properties":
|
|
74
|
-
get_node_properties(params)
|
|
75
|
-
"list_nodes":
|
|
76
|
-
list_nodes(params)
|
|
77
|
-
"get_scene_tree":
|
|
78
|
-
get_scene_tree(params)
|
|
79
|
-
"attach_script":
|
|
80
|
-
attach_script(params)
|
|
81
|
-
"duplicate_node":
|
|
82
|
-
duplicate_node(params)
|
|
83
|
-
"get_node_signals":
|
|
84
|
-
get_node_signals(params)
|
|
85
|
-
"
|
|
86
|
-
|
|
87
|
-
"
|
|
88
|
-
|
|
89
|
-
"validate_resource":
|
|
90
|
-
validate_resource(params)
|
|
91
|
-
# Batch operations
|
|
92
|
-
"validate_batch":
|
|
93
|
-
validate_batch(params)
|
|
94
|
-
"batch_scene_operations":
|
|
95
|
-
batch_scene_operations(params)
|
|
96
|
-
"
|
|
97
|
-
|
|
98
|
-
"batch_get_node_properties":
|
|
99
|
-
batch_get_node_properties(params)
|
|
100
|
-
_:
|
|
101
|
-
log_error("Unknown operation: " + operation)
|
|
102
|
-
quit(1)
|
|
103
|
-
|
|
104
|
-
quit()
|
|
105
|
-
|
|
106
|
-
# Logging functions
|
|
107
|
-
func log_debug(message):
|
|
108
|
-
if debug_mode:
|
|
109
|
-
print("[DEBUG] " + message)
|
|
110
|
-
|
|
111
|
-
func log_info(message):
|
|
112
|
-
printerr("[INFO] " + message)
|
|
113
|
-
|
|
114
|
-
func log_error(message):
|
|
115
|
-
printerr("[ERROR] " + message)
|
|
116
|
-
|
|
117
|
-
# Get a script by name or path
|
|
118
|
-
func get_script_by_name(name_of_class):
|
|
119
|
-
if debug_mode:
|
|
120
|
-
printerr("Attempting to get script for class: " + name_of_class)
|
|
121
|
-
|
|
122
|
-
if ResourceLoader.exists(name_of_class, "Script"):
|
|
123
|
-
if debug_mode:
|
|
124
|
-
printerr("Resource exists, loading directly: " + name_of_class)
|
|
125
|
-
var script = load(name_of_class) as Script
|
|
126
|
-
if script:
|
|
127
|
-
if debug_mode:
|
|
128
|
-
printerr("Successfully loaded script from path")
|
|
129
|
-
return script
|
|
130
|
-
else:
|
|
131
|
-
printerr("Failed to load script from path: " + name_of_class)
|
|
132
|
-
elif debug_mode:
|
|
133
|
-
printerr("Resource not found, checking global class registry")
|
|
134
|
-
|
|
135
|
-
var global_classes = ProjectSettings.get_global_class_list()
|
|
136
|
-
if debug_mode:
|
|
137
|
-
printerr("Searching through " + str(global_classes.size()) + " global classes")
|
|
138
|
-
|
|
139
|
-
for global_class in global_classes:
|
|
140
|
-
var found_name_of_class = global_class["class"]
|
|
141
|
-
var found_path = global_class["path"]
|
|
142
|
-
|
|
143
|
-
if found_name_of_class == name_of_class:
|
|
144
|
-
if debug_mode:
|
|
145
|
-
printerr("Found matching class in registry: " + found_name_of_class + " at path: " + found_path)
|
|
146
|
-
var script = load(found_path) as Script
|
|
147
|
-
if script:
|
|
148
|
-
if debug_mode:
|
|
149
|
-
printerr("Successfully loaded script from registry")
|
|
150
|
-
return script
|
|
151
|
-
else:
|
|
152
|
-
printerr("Failed to load script from registry path: " + found_path)
|
|
153
|
-
break
|
|
154
|
-
|
|
155
|
-
printerr("Could not find script for class: " + name_of_class)
|
|
156
|
-
return null
|
|
157
|
-
|
|
158
|
-
# Instantiate a class by name
|
|
159
|
-
func instantiate_class(name_of_class):
|
|
160
|
-
if name_of_class.is_empty():
|
|
161
|
-
printerr("Cannot instantiate class: name is empty")
|
|
162
|
-
return null
|
|
163
|
-
|
|
164
|
-
var result = null
|
|
165
|
-
if debug_mode:
|
|
166
|
-
printerr("Attempting to instantiate class: " + name_of_class)
|
|
167
|
-
|
|
168
|
-
if ClassDB.class_exists(name_of_class):
|
|
169
|
-
if debug_mode:
|
|
170
|
-
printerr("Class exists in ClassDB, using ClassDB.instantiate()")
|
|
171
|
-
if ClassDB.can_instantiate(name_of_class):
|
|
172
|
-
result = ClassDB.instantiate(name_of_class)
|
|
173
|
-
if result == null:
|
|
174
|
-
printerr("ClassDB.instantiate() returned null for class: " + name_of_class)
|
|
175
|
-
else:
|
|
176
|
-
printerr("Class exists but cannot be instantiated: " + name_of_class)
|
|
177
|
-
else:
|
|
178
|
-
if debug_mode:
|
|
179
|
-
printerr("Class not found in ClassDB, trying to get script")
|
|
180
|
-
var script = get_script_by_name(name_of_class)
|
|
181
|
-
if script is GDScript:
|
|
182
|
-
if debug_mode:
|
|
183
|
-
printerr("Found GDScript, creating instance")
|
|
184
|
-
result = script.new()
|
|
185
|
-
else:
|
|
186
|
-
printerr("Failed to get script for class: " + name_of_class)
|
|
187
|
-
return null
|
|
188
|
-
|
|
189
|
-
if result == null:
|
|
190
|
-
printerr("Failed to instantiate class: " + name_of_class)
|
|
191
|
-
elif debug_mode:
|
|
192
|
-
printerr("Successfully instantiated class: " + name_of_class + " of type: " + result.get_class())
|
|
193
|
-
|
|
194
|
-
return result
|
|
195
|
-
|
|
196
|
-
# Helper to normalize scene path
|
|
197
|
-
func normalize_scene_path(scene_path: String) -> String:
|
|
198
|
-
if not scene_path.begins_with("res://"):
|
|
199
|
-
return "res://" + scene_path
|
|
200
|
-
return scene_path
|
|
201
|
-
|
|
202
|
-
# Helper to load and instantiate a scene
|
|
203
|
-
func load_scene_instance(scene_path: String):
|
|
204
|
-
var full_path = normalize_scene_path(scene_path)
|
|
205
|
-
log_debug("Loading scene from: " + full_path)
|
|
206
|
-
|
|
207
|
-
if not FileAccess.file_exists(full_path):
|
|
208
|
-
log_error("Scene file does not exist: " + full_path)
|
|
209
|
-
return null
|
|
210
|
-
|
|
211
|
-
var scene = load(full_path)
|
|
212
|
-
if not scene:
|
|
213
|
-
log_error("Failed to load scene: " + full_path)
|
|
214
|
-
return null
|
|
215
|
-
|
|
216
|
-
var instance = scene.instantiate()
|
|
217
|
-
if not instance:
|
|
218
|
-
log_error("Failed to instantiate scene: " + full_path)
|
|
219
|
-
return null
|
|
220
|
-
|
|
221
|
-
return instance
|
|
222
|
-
|
|
223
|
-
# Helper to find a node by path
|
|
224
|
-
func find_node_by_path(scene_root: Node, node_path: String) -> Node:
|
|
225
|
-
if node_path == "root" or node_path.is_empty():
|
|
226
|
-
return scene_root
|
|
227
|
-
|
|
228
|
-
var path = node_path
|
|
229
|
-
if path.begins_with("root/"):
|
|
230
|
-
path = path.substr(5)
|
|
231
|
-
|
|
232
|
-
if path.is_empty():
|
|
233
|
-
return scene_root
|
|
234
|
-
|
|
235
|
-
return scene_root.get_node_or_null(path)
|
|
236
|
-
|
|
237
|
-
# Helper to save a scene
|
|
238
|
-
func save_scene_to_path(scene_root: Node, save_path: String) -> bool:
|
|
239
|
-
var full_path = normalize_scene_path(save_path)
|
|
240
|
-
|
|
241
|
-
var packed_scene = PackedScene.new()
|
|
242
|
-
var result = packed_scene.pack(scene_root)
|
|
243
|
-
|
|
244
|
-
if result != OK:
|
|
245
|
-
log_error("Failed to pack scene: " + str(result))
|
|
246
|
-
return false
|
|
247
|
-
|
|
248
|
-
var save_error = ResourceSaver.save(packed_scene, full_path)
|
|
249
|
-
if save_error != OK:
|
|
250
|
-
log_error("Failed to save scene: " + str(save_error))
|
|
251
|
-
return false
|
|
252
|
-
|
|
253
|
-
return true
|
|
254
|
-
|
|
255
|
-
# Create a new scene with a specified root node type
|
|
256
|
-
func create_scene(params):
|
|
257
|
-
printerr("Creating scene: " + params.scene_path)
|
|
258
|
-
|
|
259
|
-
var full_scene_path = normalize_scene_path(params.scene_path)
|
|
260
|
-
log_debug("Scene path: " + full_scene_path)
|
|
261
|
-
|
|
262
|
-
var root_node_type = "Node2D"
|
|
263
|
-
if params.has("root_node_type"):
|
|
264
|
-
root_node_type = params.root_node_type
|
|
265
|
-
log_debug("Root node type: " + root_node_type)
|
|
266
|
-
|
|
267
|
-
var scene_root = instantiate_class(root_node_type)
|
|
268
|
-
if not scene_root:
|
|
269
|
-
log_error("Failed to instantiate node of type: " + root_node_type)
|
|
270
|
-
quit(1)
|
|
271
|
-
|
|
272
|
-
scene_root.name = "root"
|
|
273
|
-
scene_root.owner = scene_root
|
|
274
|
-
|
|
275
|
-
# Ensure directory exists
|
|
276
|
-
var scene_dir = full_scene_path.get_base_dir()
|
|
277
|
-
if scene_dir != "res://" and not scene_dir.is_empty():
|
|
278
|
-
var dir = DirAccess.open("res://")
|
|
279
|
-
if dir:
|
|
280
|
-
var relative_dir = scene_dir.substr(6) if scene_dir.begins_with("res://") else scene_dir
|
|
281
|
-
if not relative_dir.is_empty() and not dir.dir_exists(relative_dir):
|
|
282
|
-
var make_error = dir.make_dir_recursive(relative_dir)
|
|
283
|
-
if make_error != OK:
|
|
284
|
-
log_error("Failed to create directory: " + relative_dir)
|
|
285
|
-
quit(1)
|
|
286
|
-
|
|
287
|
-
if save_scene_to_path(scene_root, full_scene_path):
|
|
288
|
-
print("Scene created successfully at: " + params.scene_path)
|
|
289
|
-
else:
|
|
290
|
-
log_error("Failed to create scene: " + params.scene_path)
|
|
291
|
-
quit(1)
|
|
292
|
-
|
|
293
|
-
# Add a node to an existing scene
|
|
294
|
-
func add_node(params):
|
|
295
|
-
printerr("Adding node to scene: " + params.scene_path)
|
|
296
|
-
|
|
297
|
-
var scene_root = load_scene_instance(params.scene_path)
|
|
298
|
-
if not scene_root:
|
|
299
|
-
quit(1)
|
|
300
|
-
|
|
301
|
-
var parent_path = "root"
|
|
302
|
-
if params.has("parent_node_path"):
|
|
303
|
-
parent_path = params.parent_node_path
|
|
304
|
-
|
|
305
|
-
var parent = find_node_by_path(scene_root, parent_path)
|
|
306
|
-
if not parent:
|
|
307
|
-
log_error("Parent node not found: " + parent_path)
|
|
308
|
-
quit(1)
|
|
309
|
-
|
|
310
|
-
var new_node = instantiate_class(params.node_type)
|
|
311
|
-
if not new_node:
|
|
312
|
-
log_error("Failed to instantiate node of type: " + params.node_type)
|
|
313
|
-
quit(1)
|
|
314
|
-
|
|
315
|
-
new_node.name = params.node_name
|
|
316
|
-
|
|
317
|
-
if params.has("properties"):
|
|
318
|
-
var properties = params.properties
|
|
319
|
-
for property in properties:
|
|
320
|
-
log_debug("Setting property: " + property + " = " + str(properties[property]))
|
|
321
|
-
new_node.set(property, properties[property])
|
|
322
|
-
|
|
323
|
-
parent.add_child(new_node)
|
|
324
|
-
new_node.owner = scene_root
|
|
325
|
-
|
|
326
|
-
if save_scene_to_path(scene_root, params.scene_path):
|
|
327
|
-
print("Node '" + params.node_name + "' of type '" + params.node_type + "' added successfully")
|
|
328
|
-
else:
|
|
329
|
-
log_error("Failed to save scene after adding node")
|
|
330
|
-
quit(1)
|
|
331
|
-
|
|
332
|
-
# Load a sprite into a Sprite2D node
|
|
333
|
-
func load_sprite(params):
|
|
334
|
-
printerr("Loading sprite into scene: " + params.scene_path)
|
|
335
|
-
|
|
336
|
-
var scene_root = load_scene_instance(params.scene_path)
|
|
337
|
-
if not scene_root:
|
|
338
|
-
quit(1)
|
|
339
|
-
|
|
340
|
-
var sprite_node = find_node_by_path(scene_root, params.node_path)
|
|
341
|
-
if not sprite_node:
|
|
342
|
-
log_error("Node not found: " + params.node_path)
|
|
343
|
-
quit(1)
|
|
344
|
-
|
|
345
|
-
if not (sprite_node is Sprite2D or sprite_node is Sprite3D or sprite_node is TextureRect):
|
|
346
|
-
log_error("Node is not a sprite-compatible type: " + sprite_node.get_class())
|
|
347
|
-
quit(1)
|
|
348
|
-
|
|
349
|
-
var full_texture_path = normalize_scene_path(params.texture_path)
|
|
350
|
-
var texture = load(full_texture_path)
|
|
351
|
-
if not texture:
|
|
352
|
-
log_error("Failed to load texture: " + full_texture_path)
|
|
353
|
-
quit(1)
|
|
354
|
-
|
|
355
|
-
sprite_node.texture = texture
|
|
356
|
-
|
|
357
|
-
if save_scene_to_path(scene_root, params.scene_path):
|
|
358
|
-
print("Sprite loaded successfully with texture: " + params.texture_path)
|
|
359
|
-
else:
|
|
360
|
-
log_error("Failed to save scene after loading sprite")
|
|
361
|
-
quit(1)
|
|
362
|
-
|
|
363
|
-
# Export a scene as a MeshLibrary resource
|
|
364
|
-
func export_mesh_library(params):
|
|
365
|
-
printerr("Exporting MeshLibrary from scene: " + params.scene_path)
|
|
366
|
-
|
|
367
|
-
var scene_root = load_scene_instance(params.scene_path)
|
|
368
|
-
if not scene_root:
|
|
369
|
-
quit(1)
|
|
370
|
-
|
|
371
|
-
var mesh_library = MeshLibrary.new()
|
|
372
|
-
|
|
373
|
-
var mesh_item_names = params.mesh_item_names if params.has("mesh_item_names") else []
|
|
374
|
-
var use_specific_items = mesh_item_names.size() > 0
|
|
375
|
-
|
|
376
|
-
var item_id = 0
|
|
377
|
-
|
|
378
|
-
for child in scene_root.get_children():
|
|
379
|
-
if use_specific_items and not (child.name in mesh_item_names):
|
|
380
|
-
continue
|
|
381
|
-
|
|
382
|
-
var mesh_instance = null
|
|
383
|
-
if child is MeshInstance3D:
|
|
384
|
-
mesh_instance = child
|
|
385
|
-
else:
|
|
386
|
-
for descendant in child.get_children():
|
|
387
|
-
if descendant is MeshInstance3D:
|
|
388
|
-
mesh_instance = descendant
|
|
389
|
-
break
|
|
390
|
-
|
|
391
|
-
if mesh_instance and mesh_instance.mesh:
|
|
392
|
-
mesh_library.create_item(item_id)
|
|
393
|
-
mesh_library.set_item_name(item_id, child.name)
|
|
394
|
-
mesh_library.set_item_mesh(item_id, mesh_instance.mesh)
|
|
395
|
-
|
|
396
|
-
for collision_child in child.get_children():
|
|
397
|
-
if collision_child is CollisionShape3D and collision_child.shape:
|
|
398
|
-
mesh_library.set_item_shapes(item_id, [collision_child.shape])
|
|
399
|
-
break
|
|
400
|
-
|
|
401
|
-
if mesh_instance.mesh:
|
|
402
|
-
mesh_library.set_item_preview(item_id, mesh_instance.mesh)
|
|
403
|
-
|
|
404
|
-
item_id += 1
|
|
405
|
-
|
|
406
|
-
if item_id > 0:
|
|
407
|
-
var full_output_path = normalize_scene_path(params.output_path)
|
|
408
|
-
|
|
409
|
-
# Ensure output directory exists
|
|
410
|
-
var output_dir = full_output_path.get_base_dir()
|
|
411
|
-
if output_dir != "res://":
|
|
412
|
-
var dir = DirAccess.open("res://")
|
|
413
|
-
if dir:
|
|
414
|
-
var relative_dir = output_dir.substr(6) if output_dir.begins_with("res://") else output_dir
|
|
415
|
-
if not relative_dir.is_empty() and not dir.dir_exists(relative_dir):
|
|
416
|
-
dir.make_dir_recursive(relative_dir)
|
|
417
|
-
|
|
418
|
-
var error = ResourceSaver.save(mesh_library, full_output_path)
|
|
419
|
-
if error == OK:
|
|
420
|
-
print("MeshLibrary exported successfully with " + str(item_id) + " items to: " + params.output_path)
|
|
421
|
-
else:
|
|
422
|
-
log_error("Failed to save MeshLibrary: " + str(error))
|
|
423
|
-
quit(1)
|
|
424
|
-
else:
|
|
425
|
-
log_error("No valid meshes found in the scene")
|
|
426
|
-
quit(1)
|
|
427
|
-
|
|
428
|
-
# Save changes to a scene file
|
|
429
|
-
func save_scene(params):
|
|
430
|
-
printerr("Saving scene: " + params.scene_path)
|
|
431
|
-
|
|
432
|
-
var scene_root = load_scene_instance(params.scene_path)
|
|
433
|
-
if not scene_root:
|
|
434
|
-
quit(1)
|
|
435
|
-
|
|
436
|
-
var save_path = params.new_path if params.has("new_path") else params.scene_path
|
|
437
|
-
|
|
438
|
-
if save_scene_to_path(scene_root, save_path):
|
|
439
|
-
print("Scene saved successfully to: " + save_path)
|
|
440
|
-
else:
|
|
441
|
-
log_error("Failed to save scene")
|
|
442
|
-
quit(1)
|
|
443
|
-
|
|
444
|
-
# Find files with a specific extension recursively
|
|
445
|
-
func find_files(path, extension):
|
|
446
|
-
var files = []
|
|
447
|
-
var dir = DirAccess.open(path)
|
|
448
|
-
|
|
449
|
-
if dir:
|
|
450
|
-
dir.list_dir_begin()
|
|
451
|
-
var file_name = dir.get_next()
|
|
452
|
-
|
|
453
|
-
while file_name != "":
|
|
454
|
-
if dir.current_is_dir() and not file_name.begins_with("."):
|
|
455
|
-
files.append_array(find_files(path + file_name + "/", extension))
|
|
456
|
-
elif file_name.ends_with(extension):
|
|
457
|
-
files.append(path + file_name)
|
|
458
|
-
|
|
459
|
-
file_name = dir.get_next()
|
|
460
|
-
|
|
461
|
-
return files
|
|
462
|
-
|
|
463
|
-
# Get UID for a specific file
|
|
464
|
-
func get_uid(params):
|
|
465
|
-
if not params.has("file_path"):
|
|
466
|
-
log_error("File path is required")
|
|
467
|
-
quit(1)
|
|
468
|
-
|
|
469
|
-
var file_path = normalize_scene_path(params.file_path)
|
|
470
|
-
printerr("Getting UID for file: " + file_path)
|
|
471
|
-
|
|
472
|
-
if not FileAccess.file_exists(file_path):
|
|
473
|
-
log_error("File does not exist: " + file_path)
|
|
474
|
-
quit(1)
|
|
475
|
-
|
|
476
|
-
var uid_path = file_path + ".uid"
|
|
477
|
-
var f = FileAccess.open(uid_path, FileAccess.READ)
|
|
478
|
-
|
|
479
|
-
if f:
|
|
480
|
-
var uid_content = f.get_as_text()
|
|
481
|
-
f.close()
|
|
482
|
-
|
|
483
|
-
var result = {
|
|
484
|
-
"file": file_path,
|
|
485
|
-
"uid": uid_content.strip_edges(),
|
|
486
|
-
"exists": true
|
|
487
|
-
}
|
|
488
|
-
print(JSON.stringify(result))
|
|
489
|
-
else:
|
|
490
|
-
var result = {
|
|
491
|
-
"file": file_path,
|
|
492
|
-
"exists": false,
|
|
493
|
-
"message": "UID file does not exist for this file. Use resave_resources to generate UIDs."
|
|
494
|
-
}
|
|
495
|
-
print(JSON.stringify(result))
|
|
496
|
-
|
|
497
|
-
# Resave all resources to update UID references
|
|
498
|
-
func resave_resources(params):
|
|
499
|
-
printerr("Resaving all resources to update UID references...")
|
|
500
|
-
|
|
501
|
-
var project_path = "res://"
|
|
502
|
-
if params.has("project_path"):
|
|
503
|
-
project_path = params.project_path
|
|
504
|
-
if not project_path.begins_with("res://"):
|
|
505
|
-
project_path = "res://" + project_path
|
|
506
|
-
if not project_path.ends_with("/"):
|
|
507
|
-
project_path += "/"
|
|
508
|
-
|
|
509
|
-
var scenes = find_files(project_path, ".tscn")
|
|
510
|
-
var success_count = 0
|
|
511
|
-
var error_count = 0
|
|
512
|
-
|
|
513
|
-
for scene_path in scenes:
|
|
514
|
-
var scene = load(scene_path)
|
|
515
|
-
if scene:
|
|
516
|
-
var error = ResourceSaver.save(scene, scene_path)
|
|
517
|
-
if error == OK:
|
|
518
|
-
success_count += 1
|
|
519
|
-
else:
|
|
520
|
-
error_count += 1
|
|
521
|
-
log_error("Failed to save: " + scene_path)
|
|
522
|
-
else:
|
|
523
|
-
error_count += 1
|
|
524
|
-
log_error("Failed to load: " + scene_path)
|
|
525
|
-
|
|
526
|
-
var scripts = find_files(project_path, ".gd") + find_files(project_path, ".shader") + find_files(project_path, ".gdshader")
|
|
527
|
-
var generated_uids = 0
|
|
528
|
-
|
|
529
|
-
for script_path in scripts:
|
|
530
|
-
var uid_path = script_path + ".uid"
|
|
531
|
-
var f = FileAccess.open(uid_path, FileAccess.READ)
|
|
532
|
-
if not f:
|
|
533
|
-
var res = load(script_path)
|
|
534
|
-
if res:
|
|
535
|
-
var error = ResourceSaver.save(res, script_path)
|
|
536
|
-
if error == OK:
|
|
537
|
-
generated_uids += 1
|
|
538
|
-
|
|
539
|
-
print("Resave operation complete. Scenes: " + str(success_count) + " saved, " + str(error_count) + " errors. UIDs generated: " + str(generated_uids))
|
|
540
|
-
|
|
541
|
-
# ============================================
|
|
542
|
-
# NEW NODE OPERATIONS
|
|
543
|
-
# ============================================
|
|
544
|
-
|
|
545
|
-
# Delete a node from a scene
|
|
546
|
-
func delete_node(params):
|
|
547
|
-
printerr("Deleting node from scene: " + params.scene_path)
|
|
548
|
-
|
|
549
|
-
var scene_root = load_scene_instance(params.scene_path)
|
|
550
|
-
if not scene_root:
|
|
551
|
-
quit(1)
|
|
552
|
-
|
|
553
|
-
var node = find_node_by_path(scene_root, params.node_path)
|
|
554
|
-
if not node:
|
|
555
|
-
log_error("Node not found: " + params.node_path)
|
|
556
|
-
quit(1)
|
|
557
|
-
|
|
558
|
-
if node == scene_root:
|
|
559
|
-
log_error("Cannot delete the root node")
|
|
560
|
-
quit(1)
|
|
561
|
-
|
|
562
|
-
var parent = node.get_parent()
|
|
563
|
-
parent.remove_child(node)
|
|
564
|
-
node.queue_free()
|
|
565
|
-
|
|
566
|
-
if save_scene_to_path(scene_root, params.scene_path):
|
|
567
|
-
print("Node '" + params.node_path + "' deleted successfully")
|
|
568
|
-
else:
|
|
569
|
-
log_error("Failed to save scene after deleting node")
|
|
570
|
-
quit(1)
|
|
571
|
-
|
|
572
|
-
# Update a single property on a node
|
|
573
|
-
func
|
|
574
|
-
printerr("Updating node property in scene: " + params.scene_path)
|
|
575
|
-
|
|
576
|
-
var scene_root = load_scene_instance(params.scene_path)
|
|
577
|
-
if not scene_root:
|
|
578
|
-
quit(1)
|
|
579
|
-
|
|
580
|
-
var node = find_node_by_path(scene_root, params.node_path)
|
|
581
|
-
if not node:
|
|
582
|
-
log_error("Node not found: " + params.node_path)
|
|
583
|
-
quit(1)
|
|
584
|
-
|
|
585
|
-
var property_name = params.property
|
|
586
|
-
var property_value = _coerce_property_value(params.value)
|
|
587
|
-
|
|
588
|
-
log_debug("Setting property '" + property_name + "' to: " + str(property_value))
|
|
589
|
-
|
|
590
|
-
node.set(property_name, property_value)
|
|
591
|
-
|
|
592
|
-
if save_scene_to_path(scene_root, params.scene_path):
|
|
593
|
-
print("Property '" + property_name + "' updated successfully on node '" + params.node_path + "'")
|
|
594
|
-
else:
|
|
595
|
-
log_error("Failed to save scene after updating property")
|
|
596
|
-
quit(1)
|
|
597
|
-
|
|
598
|
-
# Get all properties of a specific node
|
|
599
|
-
func get_node_properties(params):
|
|
600
|
-
printerr("Getting node properties from scene: " + params.scene_path)
|
|
601
|
-
|
|
602
|
-
var scene_root = load_scene_instance(params.scene_path)
|
|
603
|
-
if not scene_root:
|
|
604
|
-
quit(1)
|
|
605
|
-
|
|
606
|
-
var node = find_node_by_path(scene_root, params.node_path)
|
|
607
|
-
if not node:
|
|
608
|
-
log_error("Node not found: " + params.node_path)
|
|
609
|
-
quit(1)
|
|
610
|
-
|
|
611
|
-
var changed_only = params.has("changed_only") and params.changed_only
|
|
612
|
-
var properties = _collect_node_properties(node, changed_only)
|
|
613
|
-
|
|
614
|
-
var result = {
|
|
615
|
-
"nodePath": params.node_path,
|
|
616
|
-
"nodeType": node.get_class(),
|
|
617
|
-
"properties": properties
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
print(JSON.stringify(result))
|
|
621
|
-
|
|
622
|
-
# List all child nodes under a parent
|
|
623
|
-
func list_nodes(params):
|
|
624
|
-
printerr("Listing nodes in scene: " + params.scene_path)
|
|
625
|
-
|
|
626
|
-
var scene_root = load_scene_instance(params.scene_path)
|
|
627
|
-
if not scene_root:
|
|
628
|
-
quit(1)
|
|
629
|
-
|
|
630
|
-
var parent_path = "root"
|
|
631
|
-
if params.has("parent_path"):
|
|
632
|
-
parent_path = params.parent_path
|
|
633
|
-
|
|
634
|
-
var parent = find_node_by_path(scene_root, parent_path)
|
|
635
|
-
if not parent:
|
|
636
|
-
log_error("Parent node not found: " + parent_path)
|
|
637
|
-
quit(1)
|
|
638
|
-
|
|
639
|
-
var children = []
|
|
640
|
-
for child in parent.get_children():
|
|
641
|
-
children.append({
|
|
642
|
-
"name": child.name,
|
|
643
|
-
"type": child.get_class(),
|
|
644
|
-
"childCount": child.get_child_count()
|
|
645
|
-
})
|
|
646
|
-
|
|
647
|
-
var result = {
|
|
648
|
-
"parentPath": parent_path,
|
|
649
|
-
"parentType": parent.get_class(),
|
|
650
|
-
"children": children
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
print(JSON.stringify(result))
|
|
654
|
-
|
|
655
|
-
# Get full hierarchical tree structure of a scene
|
|
656
|
-
func get_scene_tree(params):
|
|
657
|
-
printerr("Getting scene tree for: " + params.scene_path)
|
|
658
|
-
|
|
659
|
-
var scene_root = load_scene_instance(params.scene_path)
|
|
660
|
-
if not scene_root:
|
|
661
|
-
quit(1)
|
|
662
|
-
|
|
663
|
-
var max_depth = -1
|
|
664
|
-
if params.has("max_depth"):
|
|
665
|
-
max_depth = int(params.max_depth)
|
|
666
|
-
|
|
667
|
-
var tree = build_tree_recursive(scene_root, "", 0, max_depth)
|
|
668
|
-
print(JSON.stringify(tree))
|
|
669
|
-
|
|
670
|
-
func build_tree_recursive(node: Node, path: String, depth: int = 0, max_depth: int = -1) -> Dictionary:
|
|
671
|
-
var node_path = path + "/" + node.name if not path.is_empty() else node.name
|
|
672
|
-
|
|
673
|
-
var children = []
|
|
674
|
-
if max_depth < 0 or depth < max_depth:
|
|
675
|
-
for child in node.get_children():
|
|
676
|
-
children.append(build_tree_recursive(child, node_path, depth + 1, max_depth))
|
|
677
|
-
|
|
678
|
-
var script_path = ""
|
|
679
|
-
if node.get_script():
|
|
680
|
-
var script = node.get_script()
|
|
681
|
-
if script.resource_path:
|
|
682
|
-
script_path = script.resource_path
|
|
683
|
-
|
|
684
|
-
return {
|
|
685
|
-
"name": node.name,
|
|
686
|
-
"type": node.get_class(),
|
|
687
|
-
"path": node_path,
|
|
688
|
-
"script": script_path,
|
|
689
|
-
"children": children
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
# Attach or change a script on a node
|
|
693
|
-
func attach_script(params):
|
|
694
|
-
printerr("Attaching script to node in scene: " + params.scene_path)
|
|
695
|
-
|
|
696
|
-
var scene_root = load_scene_instance(params.scene_path)
|
|
697
|
-
if not scene_root:
|
|
698
|
-
quit(1)
|
|
699
|
-
|
|
700
|
-
var node = find_node_by_path(scene_root, params.node_path)
|
|
701
|
-
if not node:
|
|
702
|
-
log_error("Node not found: " + params.node_path)
|
|
703
|
-
quit(1)
|
|
704
|
-
|
|
705
|
-
var full_script_path = normalize_scene_path(params.script_path)
|
|
706
|
-
|
|
707
|
-
if not FileAccess.file_exists(full_script_path):
|
|
708
|
-
log_error("Script file does not exist: " + full_script_path)
|
|
709
|
-
quit(1)
|
|
710
|
-
|
|
711
|
-
var script = load(full_script_path)
|
|
712
|
-
if not script:
|
|
713
|
-
log_error("Failed to load script: " + full_script_path)
|
|
714
|
-
quit(1)
|
|
715
|
-
|
|
716
|
-
node.set_script(script)
|
|
717
|
-
|
|
718
|
-
if save_scene_to_path(scene_root, params.scene_path):
|
|
719
|
-
print("Script '" + params.script_path + "' attached successfully to node '" + params.node_path + "'")
|
|
720
|
-
else:
|
|
721
|
-
log_error("Failed to save scene after attaching script")
|
|
722
|
-
quit(1)
|
|
723
|
-
|
|
724
|
-
# ============================================
|
|
725
|
-
# SIGNAL AND DUPLICATE OPERATIONS
|
|
726
|
-
# ============================================
|
|
727
|
-
|
|
728
|
-
# Duplicate a node and its children within a scene
|
|
729
|
-
func duplicate_node(params):
|
|
730
|
-
var scene_root = load_scene_instance(params.scene_path)
|
|
731
|
-
if not scene_root: quit(1)
|
|
732
|
-
|
|
733
|
-
var node = find_node_by_path(scene_root, params.node_path)
|
|
734
|
-
if not node:
|
|
735
|
-
log_error("Node not found: " + params.node_path)
|
|
736
|
-
quit(1)
|
|
737
|
-
if node == scene_root:
|
|
738
|
-
log_error("Cannot duplicate the root node")
|
|
739
|
-
quit(1)
|
|
740
|
-
|
|
741
|
-
var duplicate = node.duplicate()
|
|
742
|
-
if params.has("new_name"):
|
|
743
|
-
duplicate.name = params.new_name
|
|
744
|
-
else:
|
|
745
|
-
duplicate.name = node.name + "2"
|
|
746
|
-
|
|
747
|
-
var parent = node.get_parent()
|
|
748
|
-
if params.has("target_parent_path"):
|
|
749
|
-
parent = find_node_by_path(scene_root, params.target_parent_path)
|
|
750
|
-
if not parent:
|
|
751
|
-
log_error("Target parent not found: " + params.target_parent_path)
|
|
752
|
-
quit(1)
|
|
753
|
-
|
|
754
|
-
parent.add_child(duplicate)
|
|
755
|
-
duplicate.owner = scene_root
|
|
756
|
-
# Recursively set owner on all descendants
|
|
757
|
-
for child in duplicate.get_children():
|
|
758
|
-
set_owner_recursive(child, scene_root)
|
|
759
|
-
|
|
760
|
-
if save_scene_to_path(scene_root, params.scene_path):
|
|
761
|
-
print("Node duplicated successfully as '" + duplicate.name + "'")
|
|
762
|
-
else:
|
|
763
|
-
log_error("Failed to save scene after duplicating node")
|
|
764
|
-
quit(1)
|
|
765
|
-
|
|
766
|
-
func set_owner_recursive(node: Node, owner: Node):
|
|
767
|
-
node.owner = owner
|
|
768
|
-
for child in node.get_children():
|
|
769
|
-
set_owner_recursive(child, owner)
|
|
770
|
-
|
|
771
|
-
# List signals defined on a node and their current connections
|
|
772
|
-
func get_node_signals(params):
|
|
773
|
-
var scene_root = load_scene_instance(params.scene_path)
|
|
774
|
-
if not scene_root: quit(1)
|
|
775
|
-
|
|
776
|
-
var node = find_node_by_path(scene_root, params.node_path)
|
|
777
|
-
if not node:
|
|
778
|
-
log_error("Node not found: " + params.node_path)
|
|
779
|
-
quit(1)
|
|
780
|
-
|
|
781
|
-
var signals = []
|
|
782
|
-
for sig in node.get_signal_list():
|
|
783
|
-
var sig_name = sig["name"]
|
|
784
|
-
var connections = []
|
|
785
|
-
for conn in node.get_signal_connection_list(sig_name):
|
|
786
|
-
connections.append({
|
|
787
|
-
"signal": sig_name,
|
|
788
|
-
"target": str(conn["callable"].get_object().get_path()) if conn["callable"].get_object() else "unknown",
|
|
789
|
-
"method": conn["callable"].get_method()
|
|
790
|
-
})
|
|
791
|
-
signals.append({
|
|
792
|
-
"name": sig_name,
|
|
793
|
-
"connections": connections
|
|
794
|
-
})
|
|
795
|
-
|
|
796
|
-
print(JSON.stringify({
|
|
797
|
-
"nodePath": params.node_path,
|
|
798
|
-
"nodeType": node.get_class(),
|
|
799
|
-
"signals": signals
|
|
800
|
-
}))
|
|
801
|
-
|
|
802
|
-
# Connect a signal from one node to a method on another node
|
|
803
|
-
func
|
|
804
|
-
var scene_root = load_scene_instance(params.scene_path)
|
|
805
|
-
if not scene_root: quit(1)
|
|
806
|
-
|
|
807
|
-
var source = find_node_by_path(scene_root, params.node_path)
|
|
808
|
-
if not source:
|
|
809
|
-
log_error("Source node not found: " + params.node_path)
|
|
810
|
-
quit(1)
|
|
811
|
-
|
|
812
|
-
var target = find_node_by_path(scene_root, params.target_node_path)
|
|
813
|
-
if not target:
|
|
814
|
-
log_error("Target node not found: " + params.target_node_path)
|
|
815
|
-
quit(1)
|
|
816
|
-
|
|
817
|
-
if not source.has_signal(params.signal):
|
|
818
|
-
log_error("Signal does not exist: " + params.signal + " on " + source.get_class())
|
|
819
|
-
quit(1)
|
|
820
|
-
|
|
821
|
-
if not target.has_method(params.method):
|
|
822
|
-
log_error("Method does not exist: " + params.method + " on " + target.get_class())
|
|
823
|
-
quit(1)
|
|
824
|
-
|
|
825
|
-
var err = source.connect(params.signal, Callable(target, params.method))
|
|
826
|
-
if err != OK:
|
|
827
|
-
log_error("Failed to connect signal: " + str(err))
|
|
828
|
-
quit(1)
|
|
829
|
-
|
|
830
|
-
if save_scene_to_path(scene_root, params.scene_path):
|
|
831
|
-
print("Signal '" + params.signal + "' connected from '" + params.node_path + "' to '" + params.target_node_path + "." + params.method + "'")
|
|
832
|
-
else:
|
|
833
|
-
log_error("Failed to save scene after connecting signal")
|
|
834
|
-
quit(1)
|
|
835
|
-
|
|
836
|
-
# Disconnect a signal connection between two nodes
|
|
837
|
-
func
|
|
838
|
-
var scene_root = load_scene_instance(params.scene_path)
|
|
839
|
-
if not scene_root: quit(1)
|
|
840
|
-
|
|
841
|
-
var source = find_node_by_path(scene_root, params.node_path)
|
|
842
|
-
if not source:
|
|
843
|
-
log_error("Source node not found: " + params.node_path)
|
|
844
|
-
quit(1)
|
|
845
|
-
|
|
846
|
-
var target = find_node_by_path(scene_root, params.target_node_path)
|
|
847
|
-
if not target:
|
|
848
|
-
log_error("Target node not found: " + params.target_node_path)
|
|
849
|
-
quit(1)
|
|
850
|
-
|
|
851
|
-
if not source.is_connected(params.signal, Callable(target, params.method)):
|
|
852
|
-
log_error("Signal connection does not exist")
|
|
853
|
-
quit(1)
|
|
854
|
-
|
|
855
|
-
source.disconnect(params.signal, Callable(target, params.method))
|
|
856
|
-
|
|
857
|
-
if save_scene_to_path(scene_root, params.scene_path):
|
|
858
|
-
print("Signal '" + params.signal + "' disconnected from '" + params.target_node_path + "." + params.method + "'")
|
|
859
|
-
else:
|
|
860
|
-
log_error("Failed to save scene after disconnecting signal")
|
|
861
|
-
quit(1)
|
|
862
|
-
|
|
863
|
-
# ============================================
|
|
864
|
-
# VALIDATE OPERATION
|
|
865
|
-
# ============================================
|
|
866
|
-
|
|
867
|
-
# Validate a GDScript or scene file by loading it headlessly
|
|
868
|
-
func validate_resource(params):
|
|
869
|
-
if not (params.has("script_path") or params.has("scene_path")):
|
|
870
|
-
log_error("validate_resource requires script_path or scene_path")
|
|
871
|
-
quit(1)
|
|
872
|
-
var result = _validate_single(params)
|
|
873
|
-
print(JSON.stringify({"valid": result.valid, "errors": result.errors}))
|
|
874
|
-
|
|
875
|
-
# ============================================
|
|
876
|
-
# BATCH OPERATIONS
|
|
877
|
-
# ============================================
|
|
878
|
-
|
|
879
|
-
# Helper: coerce a JSON-parsed value to a GDScript type (Vector2, Vector3, Color)
|
|
880
|
-
func _coerce_property_value(value):
|
|
881
|
-
if typeof(value) == TYPE_DICTIONARY:
|
|
882
|
-
if value.has("x") and value.has("y"):
|
|
883
|
-
if value.has("z"):
|
|
884
|
-
return Vector3(value.x, value.y, value.z)
|
|
885
|
-
else:
|
|
886
|
-
return Vector2(value.x, value.y)
|
|
887
|
-
elif value.has("r") and value.has("g") and value.has("b"):
|
|
888
|
-
var a = value.a if value.has("a") else 1.0
|
|
889
|
-
return Color(value.r, value.g, value.b, a)
|
|
890
|
-
return value
|
|
891
|
-
|
|
892
|
-
# Helper: collect node properties into a serializable Dictionary
|
|
893
|
-
func _collect_node_properties(node: Node, changed_only: bool) -> Dictionary:
|
|
894
|
-
var default_node = null
|
|
895
|
-
if changed_only:
|
|
896
|
-
default_node = instantiate_class(node.get_class())
|
|
897
|
-
|
|
898
|
-
var properties = {}
|
|
899
|
-
var property_list = node.get_property_list()
|
|
900
|
-
|
|
901
|
-
for prop in property_list:
|
|
902
|
-
var prop_name = prop["name"]
|
|
903
|
-
var prop_usage = prop["usage"]
|
|
904
|
-
|
|
905
|
-
if prop_usage & PROPERTY_USAGE_STORAGE or prop_usage & PROPERTY_USAGE_EDITOR:
|
|
906
|
-
var value = node.get(prop_name)
|
|
907
|
-
|
|
908
|
-
if default_node and default_node.get(prop_name) == value:
|
|
909
|
-
continue
|
|
910
|
-
|
|
911
|
-
if value is Vector2:
|
|
912
|
-
properties[prop_name] = {"x": value.x, "y": value.y}
|
|
913
|
-
elif value is Vector3:
|
|
914
|
-
properties[prop_name] = {"x": value.x, "y": value.y, "z": value.z}
|
|
915
|
-
elif value is Color:
|
|
916
|
-
properties[prop_name] = {"r": value.r, "g": value.g, "b": value.b, "a": value.a}
|
|
917
|
-
elif value is Transform2D:
|
|
918
|
-
properties[prop_name] = str(value)
|
|
919
|
-
elif value is Transform3D:
|
|
920
|
-
properties[prop_name] = str(value)
|
|
921
|
-
elif value is Object:
|
|
922
|
-
if value:
|
|
923
|
-
properties[prop_name] = value.get_class()
|
|
924
|
-
else:
|
|
925
|
-
properties[prop_name] = null
|
|
926
|
-
elif typeof(value) in [TYPE_NIL, TYPE_BOOL, TYPE_INT, TYPE_FLOAT, TYPE_STRING, TYPE_ARRAY, TYPE_DICTIONARY]:
|
|
927
|
-
properties[prop_name] = value
|
|
928
|
-
else:
|
|
929
|
-
properties[prop_name] = str(value)
|
|
930
|
-
|
|
931
|
-
if default_node:
|
|
932
|
-
default_node.free()
|
|
933
|
-
|
|
934
|
-
return properties
|
|
935
|
-
|
|
936
|
-
# Helper: validate a single target dict (script_path or scene_path)
|
|
937
|
-
func _validate_single(target: Dictionary) -> Dictionary:
|
|
938
|
-
if target.has("script_path") and target.script_path != "":
|
|
939
|
-
var path = normalize_scene_path(target.script_path)
|
|
940
|
-
if not FileAccess.file_exists(path):
|
|
941
|
-
return {"valid": false, "errors": [{"message": "File not found: " + path}], "target": target.script_path}
|
|
942
|
-
var resource = load(path)
|
|
943
|
-
# Actual parse errors go to stderr and are parsed by TypeScript
|
|
944
|
-
return {"valid": resource != null, "errors": [], "target": target.script_path}
|
|
945
|
-
elif target.has("scene_path") and target.scene_path != "":
|
|
946
|
-
var path = normalize_scene_path(target.scene_path)
|
|
947
|
-
if not FileAccess.file_exists(path):
|
|
948
|
-
return {"valid": false, "errors": [{"message": "File not found: " + path}], "target": target.scene_path}
|
|
949
|
-
var scene = load(path)
|
|
950
|
-
return {"valid": scene != null, "errors": [], "target": target.scene_path}
|
|
951
|
-
else:
|
|
952
|
-
return {"valid": false, "errors": [{"message": "No valid target: provide script_path or scene_path"}], "target": ""}
|
|
953
|
-
|
|
954
|
-
# Validate multiple scripts/scenes in a single headless process
|
|
955
|
-
func validate_batch(params: Dictionary) -> void:
|
|
956
|
-
var results: Array = []
|
|
957
|
-
for target in params.targets:
|
|
958
|
-
results.append(_validate_single(target))
|
|
959
|
-
print(JSON.stringify({"results": results}))
|
|
960
|
-
|
|
961
|
-
# Helper: add a node to a scene root without saving (returns error string or "")
|
|
962
|
-
func _batch_add_node(scene_root: Node, op: Dictionary) -> String:
|
|
963
|
-
var parent_path = "root"
|
|
964
|
-
if op.has("parent_node_path"):
|
|
965
|
-
parent_path = op.parent_node_path
|
|
966
|
-
var parent = find_node_by_path(scene_root, parent_path)
|
|
967
|
-
if not parent:
|
|
968
|
-
return "Parent node not found: " + parent_path
|
|
969
|
-
if not op.has("node_type") or op.node_type == "":
|
|
970
|
-
return "node_type is required for add_node"
|
|
971
|
-
if not op.has("node_name") or op.node_name == "":
|
|
972
|
-
return "node_name is required for add_node"
|
|
973
|
-
var new_node = instantiate_class(op.node_type)
|
|
974
|
-
if not new_node:
|
|
975
|
-
return "Failed to instantiate node of type: " + op.node_type
|
|
976
|
-
new_node.name = op.node_name
|
|
977
|
-
if op.has("properties"):
|
|
978
|
-
for property in op.properties:
|
|
979
|
-
new_node.set(property, op.properties[property])
|
|
980
|
-
parent.add_child(new_node)
|
|
981
|
-
new_node.owner = scene_root
|
|
982
|
-
return ""
|
|
983
|
-
|
|
984
|
-
# Helper: set a sprite texture without saving (returns error string or "")
|
|
985
|
-
func _batch_load_sprite(scene_root: Node, op: Dictionary) -> String:
|
|
986
|
-
if not op.has("node_path") or op.node_path == "":
|
|
987
|
-
return "node_path is required for load_sprite"
|
|
988
|
-
if not op.has("texture_path") or op.texture_path == "":
|
|
989
|
-
return "texture_path is required for load_sprite"
|
|
990
|
-
var sprite_node = find_node_by_path(scene_root, op.node_path)
|
|
991
|
-
if not sprite_node:
|
|
992
|
-
return "Node not found: " + op.node_path
|
|
993
|
-
if not (sprite_node is Sprite2D or sprite_node is Sprite3D or sprite_node is TextureRect):
|
|
994
|
-
return "Node is not sprite-compatible: " + sprite_node.get_class()
|
|
995
|
-
var texture = load(normalize_scene_path(op.texture_path))
|
|
996
|
-
if not texture:
|
|
997
|
-
return "Failed to load texture: " + op.texture_path
|
|
998
|
-
sprite_node.texture = texture
|
|
999
|
-
return ""
|
|
1000
|
-
|
|
1001
|
-
# Execute multiple scene operations in a single headless process
|
|
1002
|
-
# Scenes are loaded once and cached in memory; mutations accumulate until a save op
|
|
1003
|
-
func batch_scene_operations(params: Dictionary) -> void:
|
|
1004
|
-
var abort_on_error = params.get("abort_on_error", false)
|
|
1005
|
-
var results: Array = []
|
|
1006
|
-
var scene_cache: Dictionary = {}
|
|
1007
|
-
|
|
1008
|
-
for op in params.operations:
|
|
1009
|
-
var op_name = op.get("operation", "")
|
|
1010
|
-
var scene_path = op.get("scene_path", "")
|
|
1011
|
-
var result = {"operation": op_name, "scenePath": scene_path}
|
|
1012
|
-
|
|
1013
|
-
if scene_path != "" and scene_path not in scene_cache:
|
|
1014
|
-
var scene_root = load_scene_instance(scene_path)
|
|
1015
|
-
if scene_root:
|
|
1016
|
-
scene_cache[scene_path] = scene_root
|
|
1017
|
-
else:
|
|
1018
|
-
result["error"] = "Failed to load scene: " + scene_path
|
|
1019
|
-
results.append(result)
|
|
1020
|
-
if abort_on_error:
|
|
1021
|
-
break
|
|
1022
|
-
continue
|
|
1023
|
-
|
|
1024
|
-
var scene_root = scene_cache.get(scene_path, null) if scene_path != "" else null
|
|
1025
|
-
|
|
1026
|
-
match op_name:
|
|
1027
|
-
"add_node":
|
|
1028
|
-
if scene_root == null:
|
|
1029
|
-
result["error"] = "scene_path required for add_node"
|
|
1030
|
-
else:
|
|
1031
|
-
var err = _batch_add_node(scene_root, op)
|
|
1032
|
-
if err != "":
|
|
1033
|
-
result["error"] = err
|
|
1034
|
-
else:
|
|
1035
|
-
result["success"] = true
|
|
1036
|
-
"load_sprite":
|
|
1037
|
-
if scene_root == null:
|
|
1038
|
-
result["error"] = "scene_path required for load_sprite"
|
|
1039
|
-
else:
|
|
1040
|
-
var err = _batch_load_sprite(scene_root, op)
|
|
1041
|
-
if err != "":
|
|
1042
|
-
result["error"] = err
|
|
1043
|
-
else:
|
|
1044
|
-
result["success"] = true
|
|
1045
|
-
"save":
|
|
1046
|
-
if scene_root == null:
|
|
1047
|
-
result["error"] = "scene_path required for save"
|
|
1048
|
-
else:
|
|
1049
|
-
var new_path = op.get("new_path", scene_path)
|
|
1050
|
-
if save_scene_to_path(scene_root, new_path):
|
|
1051
|
-
result["success"] = true
|
|
1052
|
-
# Only evict on normal save; save-as leaves the mutated scene in
|
|
1053
|
-
# cache so subsequent ops on scene_path still see accumulated mutations.
|
|
1054
|
-
if new_path == scene_path:
|
|
1055
|
-
scene_cache.erase(scene_path)
|
|
1056
|
-
else:
|
|
1057
|
-
result["error"] = "Failed to save scene: " + scene_path
|
|
1058
|
-
_:
|
|
1059
|
-
result["error"] = "Unknown batch operation: " + op_name
|
|
1060
|
-
|
|
1061
|
-
results.append(result)
|
|
1062
|
-
if abort_on_error and result.has("error"):
|
|
1063
|
-
break
|
|
1064
|
-
|
|
1065
|
-
# Auto-save any scenes that were mutated but not explicitly saved
|
|
1066
|
-
for scene_path in scene_cache:
|
|
1067
|
-
save_scene_to_path(scene_cache[scene_path], scene_path)
|
|
1068
|
-
|
|
1069
|
-
print(JSON.stringify({"results": results}))
|
|
1070
|
-
|
|
1071
|
-
# Update multiple node properties in a single headless process (loads and saves scene once)
|
|
1072
|
-
func
|
|
1073
|
-
var scene_root = load_scene_instance(params.scene_path)
|
|
1074
|
-
if not scene_root:
|
|
1075
|
-
print(JSON.stringify({"error": "Failed to load scene: " + params.scene_path, "results": []}))
|
|
1076
|
-
return
|
|
1077
|
-
|
|
1078
|
-
var abort_on_error = params.get("abort_on_error", false)
|
|
1079
|
-
var results: Array = []
|
|
1080
|
-
|
|
1081
|
-
for update in params.updates:
|
|
1082
|
-
var result = {"nodePath": update.node_path, "property": update.property}
|
|
1083
|
-
var node = find_node_by_path(scene_root, update.node_path)
|
|
1084
|
-
if node == null:
|
|
1085
|
-
result["error"] = "Node not found: " + update.node_path
|
|
1086
|
-
else:
|
|
1087
|
-
node.set(update.property, _coerce_property_value(update.value))
|
|
1088
|
-
result["success"] = true
|
|
1089
|
-
results.append(result)
|
|
1090
|
-
if abort_on_error and result.has("error"):
|
|
1091
|
-
break
|
|
1092
|
-
|
|
1093
|
-
if save_scene_to_path(scene_root, params.scene_path):
|
|
1094
|
-
print(JSON.stringify({"results": results}))
|
|
1095
|
-
else:
|
|
1096
|
-
print(JSON.stringify({"error": "Failed to save scene after batch updates", "partial_results": results}))
|
|
1097
|
-
|
|
1098
|
-
# Get properties from multiple nodes in a single headless process (loads scene once)
|
|
1099
|
-
func batch_get_node_properties(params: Dictionary) -> void:
|
|
1100
|
-
var scene_root = load_scene_instance(params.scene_path)
|
|
1101
|
-
if not scene_root:
|
|
1102
|
-
print(JSON.stringify({"error": "Failed to load scene: " + params.scene_path, "results": []}))
|
|
1103
|
-
return
|
|
1104
|
-
|
|
1105
|
-
var results: Array = []
|
|
1106
|
-
|
|
1107
|
-
for node_spec in params.nodes:
|
|
1108
|
-
var node_path = node_spec.get("node_path", "")
|
|
1109
|
-
var changed_only = node_spec.get("changed_only", false)
|
|
1110
|
-
var node = find_node_by_path(scene_root, node_path)
|
|
1111
|
-
if node == null:
|
|
1112
|
-
results.append({"nodePath": node_path, "error": "Node not found"})
|
|
1113
|
-
else:
|
|
1114
|
-
var props = _collect_node_properties(node, changed_only)
|
|
1115
|
-
results.append({"nodePath": node_path, "nodeType": node.get_class(), "properties": props})
|
|
1116
|
-
|
|
1117
|
-
print(JSON.stringify({"results": results}))
|
|
1
|
+
#!/usr/bin/env -S godot --headless --script
|
|
2
|
+
extends SceneTree
|
|
3
|
+
|
|
4
|
+
# Debug mode flag
|
|
5
|
+
var debug_mode = false
|
|
6
|
+
|
|
7
|
+
func _init():
|
|
8
|
+
var args = OS.get_cmdline_args()
|
|
9
|
+
|
|
10
|
+
# Check for debug flag
|
|
11
|
+
debug_mode = "--debug-godot" in args
|
|
12
|
+
|
|
13
|
+
# Find the script argument and determine the positions of operation and params
|
|
14
|
+
var script_index = args.find("--script")
|
|
15
|
+
if script_index == -1:
|
|
16
|
+
log_error("Could not find --script argument")
|
|
17
|
+
quit(1)
|
|
18
|
+
|
|
19
|
+
var operation_index = script_index + 2
|
|
20
|
+
var params_index = script_index + 3
|
|
21
|
+
|
|
22
|
+
if args.size() <= params_index:
|
|
23
|
+
log_error("Usage: godot --headless --script godot_operations.gd <operation> <json_params>")
|
|
24
|
+
log_error("Not enough command-line arguments provided.")
|
|
25
|
+
quit(1)
|
|
26
|
+
|
|
27
|
+
log_debug("All arguments: " + str(args))
|
|
28
|
+
|
|
29
|
+
var operation = args[operation_index]
|
|
30
|
+
var params_json = args[params_index]
|
|
31
|
+
|
|
32
|
+
log_info("Operation: " + operation)
|
|
33
|
+
log_debug("Params JSON: " + params_json)
|
|
34
|
+
|
|
35
|
+
var json = JSON.new()
|
|
36
|
+
var error = json.parse(params_json)
|
|
37
|
+
var params = null
|
|
38
|
+
|
|
39
|
+
if error == OK:
|
|
40
|
+
params = json.get_data()
|
|
41
|
+
else:
|
|
42
|
+
log_error("Failed to parse JSON parameters: " + params_json)
|
|
43
|
+
log_error("JSON Error: " + json.get_error_message() + " at line " + str(json.get_error_line()))
|
|
44
|
+
quit(1)
|
|
45
|
+
|
|
46
|
+
if not params:
|
|
47
|
+
log_error("Failed to parse JSON parameters: " + params_json)
|
|
48
|
+
quit(1)
|
|
49
|
+
|
|
50
|
+
log_info("Executing operation: " + operation)
|
|
51
|
+
|
|
52
|
+
match operation:
|
|
53
|
+
# Original operations
|
|
54
|
+
"create_scene":
|
|
55
|
+
create_scene(params)
|
|
56
|
+
"add_node":
|
|
57
|
+
add_node(params)
|
|
58
|
+
"load_sprite":
|
|
59
|
+
load_sprite(params)
|
|
60
|
+
"export_mesh_library":
|
|
61
|
+
export_mesh_library(params)
|
|
62
|
+
"save_scene":
|
|
63
|
+
save_scene(params)
|
|
64
|
+
"get_uid":
|
|
65
|
+
get_uid(params)
|
|
66
|
+
"resave_resources":
|
|
67
|
+
resave_resources(params)
|
|
68
|
+
# New node operations
|
|
69
|
+
"delete_node":
|
|
70
|
+
delete_node(params)
|
|
71
|
+
"set_node_property":
|
|
72
|
+
set_node_property(params)
|
|
73
|
+
"get_node_properties":
|
|
74
|
+
get_node_properties(params)
|
|
75
|
+
"list_nodes":
|
|
76
|
+
list_nodes(params)
|
|
77
|
+
"get_scene_tree":
|
|
78
|
+
get_scene_tree(params)
|
|
79
|
+
"attach_script":
|
|
80
|
+
attach_script(params)
|
|
81
|
+
"duplicate_node":
|
|
82
|
+
duplicate_node(params)
|
|
83
|
+
"get_node_signals":
|
|
84
|
+
get_node_signals(params)
|
|
85
|
+
"connect_signal":
|
|
86
|
+
connect_signal(params)
|
|
87
|
+
"disconnect_signal":
|
|
88
|
+
disconnect_signal(params)
|
|
89
|
+
"validate_resource":
|
|
90
|
+
validate_resource(params)
|
|
91
|
+
# Batch operations
|
|
92
|
+
"validate_batch":
|
|
93
|
+
validate_batch(params)
|
|
94
|
+
"batch_scene_operations":
|
|
95
|
+
batch_scene_operations(params)
|
|
96
|
+
"batch_set_node_properties":
|
|
97
|
+
batch_set_node_properties(params)
|
|
98
|
+
"batch_get_node_properties":
|
|
99
|
+
batch_get_node_properties(params)
|
|
100
|
+
_:
|
|
101
|
+
log_error("Unknown operation: " + operation)
|
|
102
|
+
quit(1)
|
|
103
|
+
|
|
104
|
+
quit()
|
|
105
|
+
|
|
106
|
+
# Logging functions
|
|
107
|
+
func log_debug(message):
|
|
108
|
+
if debug_mode:
|
|
109
|
+
print("[DEBUG] " + message)
|
|
110
|
+
|
|
111
|
+
func log_info(message):
|
|
112
|
+
printerr("[INFO] " + message)
|
|
113
|
+
|
|
114
|
+
func log_error(message):
|
|
115
|
+
printerr("[ERROR] " + message)
|
|
116
|
+
|
|
117
|
+
# Get a script by name or path
|
|
118
|
+
func get_script_by_name(name_of_class):
|
|
119
|
+
if debug_mode:
|
|
120
|
+
printerr("Attempting to get script for class: " + name_of_class)
|
|
121
|
+
|
|
122
|
+
if ResourceLoader.exists(name_of_class, "Script"):
|
|
123
|
+
if debug_mode:
|
|
124
|
+
printerr("Resource exists, loading directly: " + name_of_class)
|
|
125
|
+
var script = load(name_of_class) as Script
|
|
126
|
+
if script:
|
|
127
|
+
if debug_mode:
|
|
128
|
+
printerr("Successfully loaded script from path")
|
|
129
|
+
return script
|
|
130
|
+
else:
|
|
131
|
+
printerr("Failed to load script from path: " + name_of_class)
|
|
132
|
+
elif debug_mode:
|
|
133
|
+
printerr("Resource not found, checking global class registry")
|
|
134
|
+
|
|
135
|
+
var global_classes = ProjectSettings.get_global_class_list()
|
|
136
|
+
if debug_mode:
|
|
137
|
+
printerr("Searching through " + str(global_classes.size()) + " global classes")
|
|
138
|
+
|
|
139
|
+
for global_class in global_classes:
|
|
140
|
+
var found_name_of_class = global_class["class"]
|
|
141
|
+
var found_path = global_class["path"]
|
|
142
|
+
|
|
143
|
+
if found_name_of_class == name_of_class:
|
|
144
|
+
if debug_mode:
|
|
145
|
+
printerr("Found matching class in registry: " + found_name_of_class + " at path: " + found_path)
|
|
146
|
+
var script = load(found_path) as Script
|
|
147
|
+
if script:
|
|
148
|
+
if debug_mode:
|
|
149
|
+
printerr("Successfully loaded script from registry")
|
|
150
|
+
return script
|
|
151
|
+
else:
|
|
152
|
+
printerr("Failed to load script from registry path: " + found_path)
|
|
153
|
+
break
|
|
154
|
+
|
|
155
|
+
printerr("Could not find script for class: " + name_of_class)
|
|
156
|
+
return null
|
|
157
|
+
|
|
158
|
+
# Instantiate a class by name
|
|
159
|
+
func instantiate_class(name_of_class):
|
|
160
|
+
if name_of_class.is_empty():
|
|
161
|
+
printerr("Cannot instantiate class: name is empty")
|
|
162
|
+
return null
|
|
163
|
+
|
|
164
|
+
var result = null
|
|
165
|
+
if debug_mode:
|
|
166
|
+
printerr("Attempting to instantiate class: " + name_of_class)
|
|
167
|
+
|
|
168
|
+
if ClassDB.class_exists(name_of_class):
|
|
169
|
+
if debug_mode:
|
|
170
|
+
printerr("Class exists in ClassDB, using ClassDB.instantiate()")
|
|
171
|
+
if ClassDB.can_instantiate(name_of_class):
|
|
172
|
+
result = ClassDB.instantiate(name_of_class)
|
|
173
|
+
if result == null:
|
|
174
|
+
printerr("ClassDB.instantiate() returned null for class: " + name_of_class)
|
|
175
|
+
else:
|
|
176
|
+
printerr("Class exists but cannot be instantiated: " + name_of_class)
|
|
177
|
+
else:
|
|
178
|
+
if debug_mode:
|
|
179
|
+
printerr("Class not found in ClassDB, trying to get script")
|
|
180
|
+
var script = get_script_by_name(name_of_class)
|
|
181
|
+
if script is GDScript:
|
|
182
|
+
if debug_mode:
|
|
183
|
+
printerr("Found GDScript, creating instance")
|
|
184
|
+
result = script.new()
|
|
185
|
+
else:
|
|
186
|
+
printerr("Failed to get script for class: " + name_of_class)
|
|
187
|
+
return null
|
|
188
|
+
|
|
189
|
+
if result == null:
|
|
190
|
+
printerr("Failed to instantiate class: " + name_of_class)
|
|
191
|
+
elif debug_mode:
|
|
192
|
+
printerr("Successfully instantiated class: " + name_of_class + " of type: " + result.get_class())
|
|
193
|
+
|
|
194
|
+
return result
|
|
195
|
+
|
|
196
|
+
# Helper to normalize scene path
|
|
197
|
+
func normalize_scene_path(scene_path: String) -> String:
|
|
198
|
+
if not scene_path.begins_with("res://"):
|
|
199
|
+
return "res://" + scene_path
|
|
200
|
+
return scene_path
|
|
201
|
+
|
|
202
|
+
# Helper to load and instantiate a scene
|
|
203
|
+
func load_scene_instance(scene_path: String):
|
|
204
|
+
var full_path = normalize_scene_path(scene_path)
|
|
205
|
+
log_debug("Loading scene from: " + full_path)
|
|
206
|
+
|
|
207
|
+
if not FileAccess.file_exists(full_path):
|
|
208
|
+
log_error("Scene file does not exist: " + full_path)
|
|
209
|
+
return null
|
|
210
|
+
|
|
211
|
+
var scene = load(full_path)
|
|
212
|
+
if not scene:
|
|
213
|
+
log_error("Failed to load scene: " + full_path)
|
|
214
|
+
return null
|
|
215
|
+
|
|
216
|
+
var instance = scene.instantiate()
|
|
217
|
+
if not instance:
|
|
218
|
+
log_error("Failed to instantiate scene: " + full_path)
|
|
219
|
+
return null
|
|
220
|
+
|
|
221
|
+
return instance
|
|
222
|
+
|
|
223
|
+
# Helper to find a node by path
|
|
224
|
+
func find_node_by_path(scene_root: Node, node_path: String) -> Node:
|
|
225
|
+
if node_path == "root" or node_path.is_empty():
|
|
226
|
+
return scene_root
|
|
227
|
+
|
|
228
|
+
var path = node_path
|
|
229
|
+
if path.begins_with("root/"):
|
|
230
|
+
path = path.substr(5)
|
|
231
|
+
|
|
232
|
+
if path.is_empty():
|
|
233
|
+
return scene_root
|
|
234
|
+
|
|
235
|
+
return scene_root.get_node_or_null(path)
|
|
236
|
+
|
|
237
|
+
# Helper to save a scene
|
|
238
|
+
func save_scene_to_path(scene_root: Node, save_path: String) -> bool:
|
|
239
|
+
var full_path = normalize_scene_path(save_path)
|
|
240
|
+
|
|
241
|
+
var packed_scene = PackedScene.new()
|
|
242
|
+
var result = packed_scene.pack(scene_root)
|
|
243
|
+
|
|
244
|
+
if result != OK:
|
|
245
|
+
log_error("Failed to pack scene: " + str(result))
|
|
246
|
+
return false
|
|
247
|
+
|
|
248
|
+
var save_error = ResourceSaver.save(packed_scene, full_path)
|
|
249
|
+
if save_error != OK:
|
|
250
|
+
log_error("Failed to save scene: " + str(save_error))
|
|
251
|
+
return false
|
|
252
|
+
|
|
253
|
+
return true
|
|
254
|
+
|
|
255
|
+
# Create a new scene with a specified root node type
|
|
256
|
+
func create_scene(params):
|
|
257
|
+
printerr("Creating scene: " + params.scene_path)
|
|
258
|
+
|
|
259
|
+
var full_scene_path = normalize_scene_path(params.scene_path)
|
|
260
|
+
log_debug("Scene path: " + full_scene_path)
|
|
261
|
+
|
|
262
|
+
var root_node_type = "Node2D"
|
|
263
|
+
if params.has("root_node_type"):
|
|
264
|
+
root_node_type = params.root_node_type
|
|
265
|
+
log_debug("Root node type: " + root_node_type)
|
|
266
|
+
|
|
267
|
+
var scene_root = instantiate_class(root_node_type)
|
|
268
|
+
if not scene_root:
|
|
269
|
+
log_error("Failed to instantiate node of type: " + root_node_type)
|
|
270
|
+
quit(1)
|
|
271
|
+
|
|
272
|
+
scene_root.name = "root"
|
|
273
|
+
scene_root.owner = scene_root
|
|
274
|
+
|
|
275
|
+
# Ensure directory exists
|
|
276
|
+
var scene_dir = full_scene_path.get_base_dir()
|
|
277
|
+
if scene_dir != "res://" and not scene_dir.is_empty():
|
|
278
|
+
var dir = DirAccess.open("res://")
|
|
279
|
+
if dir:
|
|
280
|
+
var relative_dir = scene_dir.substr(6) if scene_dir.begins_with("res://") else scene_dir
|
|
281
|
+
if not relative_dir.is_empty() and not dir.dir_exists(relative_dir):
|
|
282
|
+
var make_error = dir.make_dir_recursive(relative_dir)
|
|
283
|
+
if make_error != OK:
|
|
284
|
+
log_error("Failed to create directory: " + relative_dir)
|
|
285
|
+
quit(1)
|
|
286
|
+
|
|
287
|
+
if save_scene_to_path(scene_root, full_scene_path):
|
|
288
|
+
print("Scene created successfully at: " + params.scene_path)
|
|
289
|
+
else:
|
|
290
|
+
log_error("Failed to create scene: " + params.scene_path)
|
|
291
|
+
quit(1)
|
|
292
|
+
|
|
293
|
+
# Add a node to an existing scene
|
|
294
|
+
func add_node(params):
|
|
295
|
+
printerr("Adding node to scene: " + params.scene_path)
|
|
296
|
+
|
|
297
|
+
var scene_root = load_scene_instance(params.scene_path)
|
|
298
|
+
if not scene_root:
|
|
299
|
+
quit(1)
|
|
300
|
+
|
|
301
|
+
var parent_path = "root"
|
|
302
|
+
if params.has("parent_node_path"):
|
|
303
|
+
parent_path = params.parent_node_path
|
|
304
|
+
|
|
305
|
+
var parent = find_node_by_path(scene_root, parent_path)
|
|
306
|
+
if not parent:
|
|
307
|
+
log_error("Parent node not found: " + parent_path)
|
|
308
|
+
quit(1)
|
|
309
|
+
|
|
310
|
+
var new_node = instantiate_class(params.node_type)
|
|
311
|
+
if not new_node:
|
|
312
|
+
log_error("Failed to instantiate node of type: " + params.node_type)
|
|
313
|
+
quit(1)
|
|
314
|
+
|
|
315
|
+
new_node.name = params.node_name
|
|
316
|
+
|
|
317
|
+
if params.has("properties"):
|
|
318
|
+
var properties = params.properties
|
|
319
|
+
for property in properties:
|
|
320
|
+
log_debug("Setting property: " + property + " = " + str(properties[property]))
|
|
321
|
+
new_node.set(property, properties[property])
|
|
322
|
+
|
|
323
|
+
parent.add_child(new_node)
|
|
324
|
+
new_node.owner = scene_root
|
|
325
|
+
|
|
326
|
+
if save_scene_to_path(scene_root, params.scene_path):
|
|
327
|
+
print("Node '" + params.node_name + "' of type '" + params.node_type + "' added successfully")
|
|
328
|
+
else:
|
|
329
|
+
log_error("Failed to save scene after adding node")
|
|
330
|
+
quit(1)
|
|
331
|
+
|
|
332
|
+
# Load a sprite into a Sprite2D node
|
|
333
|
+
func load_sprite(params):
|
|
334
|
+
printerr("Loading sprite into scene: " + params.scene_path)
|
|
335
|
+
|
|
336
|
+
var scene_root = load_scene_instance(params.scene_path)
|
|
337
|
+
if not scene_root:
|
|
338
|
+
quit(1)
|
|
339
|
+
|
|
340
|
+
var sprite_node = find_node_by_path(scene_root, params.node_path)
|
|
341
|
+
if not sprite_node:
|
|
342
|
+
log_error("Node not found: " + params.node_path)
|
|
343
|
+
quit(1)
|
|
344
|
+
|
|
345
|
+
if not (sprite_node is Sprite2D or sprite_node is Sprite3D or sprite_node is TextureRect):
|
|
346
|
+
log_error("Node is not a sprite-compatible type: " + sprite_node.get_class())
|
|
347
|
+
quit(1)
|
|
348
|
+
|
|
349
|
+
var full_texture_path = normalize_scene_path(params.texture_path)
|
|
350
|
+
var texture = load(full_texture_path)
|
|
351
|
+
if not texture:
|
|
352
|
+
log_error("Failed to load texture: " + full_texture_path)
|
|
353
|
+
quit(1)
|
|
354
|
+
|
|
355
|
+
sprite_node.texture = texture
|
|
356
|
+
|
|
357
|
+
if save_scene_to_path(scene_root, params.scene_path):
|
|
358
|
+
print("Sprite loaded successfully with texture: " + params.texture_path)
|
|
359
|
+
else:
|
|
360
|
+
log_error("Failed to save scene after loading sprite")
|
|
361
|
+
quit(1)
|
|
362
|
+
|
|
363
|
+
# Export a scene as a MeshLibrary resource
|
|
364
|
+
func export_mesh_library(params):
|
|
365
|
+
printerr("Exporting MeshLibrary from scene: " + params.scene_path)
|
|
366
|
+
|
|
367
|
+
var scene_root = load_scene_instance(params.scene_path)
|
|
368
|
+
if not scene_root:
|
|
369
|
+
quit(1)
|
|
370
|
+
|
|
371
|
+
var mesh_library = MeshLibrary.new()
|
|
372
|
+
|
|
373
|
+
var mesh_item_names = params.mesh_item_names if params.has("mesh_item_names") else []
|
|
374
|
+
var use_specific_items = mesh_item_names.size() > 0
|
|
375
|
+
|
|
376
|
+
var item_id = 0
|
|
377
|
+
|
|
378
|
+
for child in scene_root.get_children():
|
|
379
|
+
if use_specific_items and not (child.name in mesh_item_names):
|
|
380
|
+
continue
|
|
381
|
+
|
|
382
|
+
var mesh_instance = null
|
|
383
|
+
if child is MeshInstance3D:
|
|
384
|
+
mesh_instance = child
|
|
385
|
+
else:
|
|
386
|
+
for descendant in child.get_children():
|
|
387
|
+
if descendant is MeshInstance3D:
|
|
388
|
+
mesh_instance = descendant
|
|
389
|
+
break
|
|
390
|
+
|
|
391
|
+
if mesh_instance and mesh_instance.mesh:
|
|
392
|
+
mesh_library.create_item(item_id)
|
|
393
|
+
mesh_library.set_item_name(item_id, child.name)
|
|
394
|
+
mesh_library.set_item_mesh(item_id, mesh_instance.mesh)
|
|
395
|
+
|
|
396
|
+
for collision_child in child.get_children():
|
|
397
|
+
if collision_child is CollisionShape3D and collision_child.shape:
|
|
398
|
+
mesh_library.set_item_shapes(item_id, [collision_child.shape])
|
|
399
|
+
break
|
|
400
|
+
|
|
401
|
+
if mesh_instance.mesh:
|
|
402
|
+
mesh_library.set_item_preview(item_id, mesh_instance.mesh)
|
|
403
|
+
|
|
404
|
+
item_id += 1
|
|
405
|
+
|
|
406
|
+
if item_id > 0:
|
|
407
|
+
var full_output_path = normalize_scene_path(params.output_path)
|
|
408
|
+
|
|
409
|
+
# Ensure output directory exists
|
|
410
|
+
var output_dir = full_output_path.get_base_dir()
|
|
411
|
+
if output_dir != "res://":
|
|
412
|
+
var dir = DirAccess.open("res://")
|
|
413
|
+
if dir:
|
|
414
|
+
var relative_dir = output_dir.substr(6) if output_dir.begins_with("res://") else output_dir
|
|
415
|
+
if not relative_dir.is_empty() and not dir.dir_exists(relative_dir):
|
|
416
|
+
dir.make_dir_recursive(relative_dir)
|
|
417
|
+
|
|
418
|
+
var error = ResourceSaver.save(mesh_library, full_output_path)
|
|
419
|
+
if error == OK:
|
|
420
|
+
print("MeshLibrary exported successfully with " + str(item_id) + " items to: " + params.output_path)
|
|
421
|
+
else:
|
|
422
|
+
log_error("Failed to save MeshLibrary: " + str(error))
|
|
423
|
+
quit(1)
|
|
424
|
+
else:
|
|
425
|
+
log_error("No valid meshes found in the scene")
|
|
426
|
+
quit(1)
|
|
427
|
+
|
|
428
|
+
# Save changes to a scene file
|
|
429
|
+
func save_scene(params):
|
|
430
|
+
printerr("Saving scene: " + params.scene_path)
|
|
431
|
+
|
|
432
|
+
var scene_root = load_scene_instance(params.scene_path)
|
|
433
|
+
if not scene_root:
|
|
434
|
+
quit(1)
|
|
435
|
+
|
|
436
|
+
var save_path = params.new_path if params.has("new_path") else params.scene_path
|
|
437
|
+
|
|
438
|
+
if save_scene_to_path(scene_root, save_path):
|
|
439
|
+
print("Scene saved successfully to: " + save_path)
|
|
440
|
+
else:
|
|
441
|
+
log_error("Failed to save scene")
|
|
442
|
+
quit(1)
|
|
443
|
+
|
|
444
|
+
# Find files with a specific extension recursively
|
|
445
|
+
func find_files(path, extension):
|
|
446
|
+
var files = []
|
|
447
|
+
var dir = DirAccess.open(path)
|
|
448
|
+
|
|
449
|
+
if dir:
|
|
450
|
+
dir.list_dir_begin()
|
|
451
|
+
var file_name = dir.get_next()
|
|
452
|
+
|
|
453
|
+
while file_name != "":
|
|
454
|
+
if dir.current_is_dir() and not file_name.begins_with("."):
|
|
455
|
+
files.append_array(find_files(path + file_name + "/", extension))
|
|
456
|
+
elif file_name.ends_with(extension):
|
|
457
|
+
files.append(path + file_name)
|
|
458
|
+
|
|
459
|
+
file_name = dir.get_next()
|
|
460
|
+
|
|
461
|
+
return files
|
|
462
|
+
|
|
463
|
+
# Get UID for a specific file
|
|
464
|
+
func get_uid(params):
|
|
465
|
+
if not params.has("file_path"):
|
|
466
|
+
log_error("File path is required")
|
|
467
|
+
quit(1)
|
|
468
|
+
|
|
469
|
+
var file_path = normalize_scene_path(params.file_path)
|
|
470
|
+
printerr("Getting UID for file: " + file_path)
|
|
471
|
+
|
|
472
|
+
if not FileAccess.file_exists(file_path):
|
|
473
|
+
log_error("File does not exist: " + file_path)
|
|
474
|
+
quit(1)
|
|
475
|
+
|
|
476
|
+
var uid_path = file_path + ".uid"
|
|
477
|
+
var f = FileAccess.open(uid_path, FileAccess.READ)
|
|
478
|
+
|
|
479
|
+
if f:
|
|
480
|
+
var uid_content = f.get_as_text()
|
|
481
|
+
f.close()
|
|
482
|
+
|
|
483
|
+
var result = {
|
|
484
|
+
"file": file_path,
|
|
485
|
+
"uid": uid_content.strip_edges(),
|
|
486
|
+
"exists": true
|
|
487
|
+
}
|
|
488
|
+
print(JSON.stringify(result))
|
|
489
|
+
else:
|
|
490
|
+
var result = {
|
|
491
|
+
"file": file_path,
|
|
492
|
+
"exists": false,
|
|
493
|
+
"message": "UID file does not exist for this file. Use resave_resources to generate UIDs."
|
|
494
|
+
}
|
|
495
|
+
print(JSON.stringify(result))
|
|
496
|
+
|
|
497
|
+
# Resave all resources to update UID references
|
|
498
|
+
func resave_resources(params):
|
|
499
|
+
printerr("Resaving all resources to update UID references...")
|
|
500
|
+
|
|
501
|
+
var project_path = "res://"
|
|
502
|
+
if params.has("project_path"):
|
|
503
|
+
project_path = params.project_path
|
|
504
|
+
if not project_path.begins_with("res://"):
|
|
505
|
+
project_path = "res://" + project_path
|
|
506
|
+
if not project_path.ends_with("/"):
|
|
507
|
+
project_path += "/"
|
|
508
|
+
|
|
509
|
+
var scenes = find_files(project_path, ".tscn")
|
|
510
|
+
var success_count = 0
|
|
511
|
+
var error_count = 0
|
|
512
|
+
|
|
513
|
+
for scene_path in scenes:
|
|
514
|
+
var scene = load(scene_path)
|
|
515
|
+
if scene:
|
|
516
|
+
var error = ResourceSaver.save(scene, scene_path)
|
|
517
|
+
if error == OK:
|
|
518
|
+
success_count += 1
|
|
519
|
+
else:
|
|
520
|
+
error_count += 1
|
|
521
|
+
log_error("Failed to save: " + scene_path)
|
|
522
|
+
else:
|
|
523
|
+
error_count += 1
|
|
524
|
+
log_error("Failed to load: " + scene_path)
|
|
525
|
+
|
|
526
|
+
var scripts = find_files(project_path, ".gd") + find_files(project_path, ".shader") + find_files(project_path, ".gdshader")
|
|
527
|
+
var generated_uids = 0
|
|
528
|
+
|
|
529
|
+
for script_path in scripts:
|
|
530
|
+
var uid_path = script_path + ".uid"
|
|
531
|
+
var f = FileAccess.open(uid_path, FileAccess.READ)
|
|
532
|
+
if not f:
|
|
533
|
+
var res = load(script_path)
|
|
534
|
+
if res:
|
|
535
|
+
var error = ResourceSaver.save(res, script_path)
|
|
536
|
+
if error == OK:
|
|
537
|
+
generated_uids += 1
|
|
538
|
+
|
|
539
|
+
print("Resave operation complete. Scenes: " + str(success_count) + " saved, " + str(error_count) + " errors. UIDs generated: " + str(generated_uids))
|
|
540
|
+
|
|
541
|
+
# ============================================
|
|
542
|
+
# NEW NODE OPERATIONS
|
|
543
|
+
# ============================================
|
|
544
|
+
|
|
545
|
+
# Delete a node from a scene
|
|
546
|
+
func delete_node(params):
|
|
547
|
+
printerr("Deleting node from scene: " + params.scene_path)
|
|
548
|
+
|
|
549
|
+
var scene_root = load_scene_instance(params.scene_path)
|
|
550
|
+
if not scene_root:
|
|
551
|
+
quit(1)
|
|
552
|
+
|
|
553
|
+
var node = find_node_by_path(scene_root, params.node_path)
|
|
554
|
+
if not node:
|
|
555
|
+
log_error("Node not found: " + params.node_path)
|
|
556
|
+
quit(1)
|
|
557
|
+
|
|
558
|
+
if node == scene_root:
|
|
559
|
+
log_error("Cannot delete the root node")
|
|
560
|
+
quit(1)
|
|
561
|
+
|
|
562
|
+
var parent = node.get_parent()
|
|
563
|
+
parent.remove_child(node)
|
|
564
|
+
node.queue_free()
|
|
565
|
+
|
|
566
|
+
if save_scene_to_path(scene_root, params.scene_path):
|
|
567
|
+
print("Node '" + params.node_path + "' deleted successfully")
|
|
568
|
+
else:
|
|
569
|
+
log_error("Failed to save scene after deleting node")
|
|
570
|
+
quit(1)
|
|
571
|
+
|
|
572
|
+
# Update a single property on a node
|
|
573
|
+
func set_node_property(params):
|
|
574
|
+
printerr("Updating node property in scene: " + params.scene_path)
|
|
575
|
+
|
|
576
|
+
var scene_root = load_scene_instance(params.scene_path)
|
|
577
|
+
if not scene_root:
|
|
578
|
+
quit(1)
|
|
579
|
+
|
|
580
|
+
var node = find_node_by_path(scene_root, params.node_path)
|
|
581
|
+
if not node:
|
|
582
|
+
log_error("Node not found: " + params.node_path)
|
|
583
|
+
quit(1)
|
|
584
|
+
|
|
585
|
+
var property_name = params.property
|
|
586
|
+
var property_value = _coerce_property_value(params.value)
|
|
587
|
+
|
|
588
|
+
log_debug("Setting property '" + property_name + "' to: " + str(property_value))
|
|
589
|
+
|
|
590
|
+
node.set(property_name, property_value)
|
|
591
|
+
|
|
592
|
+
if save_scene_to_path(scene_root, params.scene_path):
|
|
593
|
+
print("Property '" + property_name + "' updated successfully on node '" + params.node_path + "'")
|
|
594
|
+
else:
|
|
595
|
+
log_error("Failed to save scene after updating property")
|
|
596
|
+
quit(1)
|
|
597
|
+
|
|
598
|
+
# Get all properties of a specific node
|
|
599
|
+
func get_node_properties(params):
|
|
600
|
+
printerr("Getting node properties from scene: " + params.scene_path)
|
|
601
|
+
|
|
602
|
+
var scene_root = load_scene_instance(params.scene_path)
|
|
603
|
+
if not scene_root:
|
|
604
|
+
quit(1)
|
|
605
|
+
|
|
606
|
+
var node = find_node_by_path(scene_root, params.node_path)
|
|
607
|
+
if not node:
|
|
608
|
+
log_error("Node not found: " + params.node_path)
|
|
609
|
+
quit(1)
|
|
610
|
+
|
|
611
|
+
var changed_only = params.has("changed_only") and params.changed_only
|
|
612
|
+
var properties = _collect_node_properties(node, changed_only)
|
|
613
|
+
|
|
614
|
+
var result = {
|
|
615
|
+
"nodePath": params.node_path,
|
|
616
|
+
"nodeType": node.get_class(),
|
|
617
|
+
"properties": properties
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
print(JSON.stringify(result))
|
|
621
|
+
|
|
622
|
+
# List all child nodes under a parent
|
|
623
|
+
func list_nodes(params):
|
|
624
|
+
printerr("Listing nodes in scene: " + params.scene_path)
|
|
625
|
+
|
|
626
|
+
var scene_root = load_scene_instance(params.scene_path)
|
|
627
|
+
if not scene_root:
|
|
628
|
+
quit(1)
|
|
629
|
+
|
|
630
|
+
var parent_path = "root"
|
|
631
|
+
if params.has("parent_path"):
|
|
632
|
+
parent_path = params.parent_path
|
|
633
|
+
|
|
634
|
+
var parent = find_node_by_path(scene_root, parent_path)
|
|
635
|
+
if not parent:
|
|
636
|
+
log_error("Parent node not found: " + parent_path)
|
|
637
|
+
quit(1)
|
|
638
|
+
|
|
639
|
+
var children = []
|
|
640
|
+
for child in parent.get_children():
|
|
641
|
+
children.append({
|
|
642
|
+
"name": child.name,
|
|
643
|
+
"type": child.get_class(),
|
|
644
|
+
"childCount": child.get_child_count()
|
|
645
|
+
})
|
|
646
|
+
|
|
647
|
+
var result = {
|
|
648
|
+
"parentPath": parent_path,
|
|
649
|
+
"parentType": parent.get_class(),
|
|
650
|
+
"children": children
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
print(JSON.stringify(result))
|
|
654
|
+
|
|
655
|
+
# Get full hierarchical tree structure of a scene
|
|
656
|
+
func get_scene_tree(params):
|
|
657
|
+
printerr("Getting scene tree for: " + params.scene_path)
|
|
658
|
+
|
|
659
|
+
var scene_root = load_scene_instance(params.scene_path)
|
|
660
|
+
if not scene_root:
|
|
661
|
+
quit(1)
|
|
662
|
+
|
|
663
|
+
var max_depth = -1
|
|
664
|
+
if params.has("max_depth"):
|
|
665
|
+
max_depth = int(params.max_depth)
|
|
666
|
+
|
|
667
|
+
var tree = build_tree_recursive(scene_root, "", 0, max_depth)
|
|
668
|
+
print(JSON.stringify(tree))
|
|
669
|
+
|
|
670
|
+
func build_tree_recursive(node: Node, path: String, depth: int = 0, max_depth: int = -1) -> Dictionary:
|
|
671
|
+
var node_path = path + "/" + node.name if not path.is_empty() else node.name
|
|
672
|
+
|
|
673
|
+
var children = []
|
|
674
|
+
if max_depth < 0 or depth < max_depth:
|
|
675
|
+
for child in node.get_children():
|
|
676
|
+
children.append(build_tree_recursive(child, node_path, depth + 1, max_depth))
|
|
677
|
+
|
|
678
|
+
var script_path = ""
|
|
679
|
+
if node.get_script():
|
|
680
|
+
var script = node.get_script()
|
|
681
|
+
if script.resource_path:
|
|
682
|
+
script_path = script.resource_path
|
|
683
|
+
|
|
684
|
+
return {
|
|
685
|
+
"name": node.name,
|
|
686
|
+
"type": node.get_class(),
|
|
687
|
+
"path": node_path,
|
|
688
|
+
"script": script_path,
|
|
689
|
+
"children": children
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
# Attach or change a script on a node
|
|
693
|
+
func attach_script(params):
|
|
694
|
+
printerr("Attaching script to node in scene: " + params.scene_path)
|
|
695
|
+
|
|
696
|
+
var scene_root = load_scene_instance(params.scene_path)
|
|
697
|
+
if not scene_root:
|
|
698
|
+
quit(1)
|
|
699
|
+
|
|
700
|
+
var node = find_node_by_path(scene_root, params.node_path)
|
|
701
|
+
if not node:
|
|
702
|
+
log_error("Node not found: " + params.node_path)
|
|
703
|
+
quit(1)
|
|
704
|
+
|
|
705
|
+
var full_script_path = normalize_scene_path(params.script_path)
|
|
706
|
+
|
|
707
|
+
if not FileAccess.file_exists(full_script_path):
|
|
708
|
+
log_error("Script file does not exist: " + full_script_path)
|
|
709
|
+
quit(1)
|
|
710
|
+
|
|
711
|
+
var script = load(full_script_path)
|
|
712
|
+
if not script:
|
|
713
|
+
log_error("Failed to load script: " + full_script_path)
|
|
714
|
+
quit(1)
|
|
715
|
+
|
|
716
|
+
node.set_script(script)
|
|
717
|
+
|
|
718
|
+
if save_scene_to_path(scene_root, params.scene_path):
|
|
719
|
+
print("Script '" + params.script_path + "' attached successfully to node '" + params.node_path + "'")
|
|
720
|
+
else:
|
|
721
|
+
log_error("Failed to save scene after attaching script")
|
|
722
|
+
quit(1)
|
|
723
|
+
|
|
724
|
+
# ============================================
|
|
725
|
+
# SIGNAL AND DUPLICATE OPERATIONS
|
|
726
|
+
# ============================================
|
|
727
|
+
|
|
728
|
+
# Duplicate a node and its children within a scene
|
|
729
|
+
func duplicate_node(params):
|
|
730
|
+
var scene_root = load_scene_instance(params.scene_path)
|
|
731
|
+
if not scene_root: quit(1)
|
|
732
|
+
|
|
733
|
+
var node = find_node_by_path(scene_root, params.node_path)
|
|
734
|
+
if not node:
|
|
735
|
+
log_error("Node not found: " + params.node_path)
|
|
736
|
+
quit(1)
|
|
737
|
+
if node == scene_root:
|
|
738
|
+
log_error("Cannot duplicate the root node")
|
|
739
|
+
quit(1)
|
|
740
|
+
|
|
741
|
+
var duplicate = node.duplicate()
|
|
742
|
+
if params.has("new_name"):
|
|
743
|
+
duplicate.name = params.new_name
|
|
744
|
+
else:
|
|
745
|
+
duplicate.name = node.name + "2"
|
|
746
|
+
|
|
747
|
+
var parent = node.get_parent()
|
|
748
|
+
if params.has("target_parent_path"):
|
|
749
|
+
parent = find_node_by_path(scene_root, params.target_parent_path)
|
|
750
|
+
if not parent:
|
|
751
|
+
log_error("Target parent not found: " + params.target_parent_path)
|
|
752
|
+
quit(1)
|
|
753
|
+
|
|
754
|
+
parent.add_child(duplicate)
|
|
755
|
+
duplicate.owner = scene_root
|
|
756
|
+
# Recursively set owner on all descendants
|
|
757
|
+
for child in duplicate.get_children():
|
|
758
|
+
set_owner_recursive(child, scene_root)
|
|
759
|
+
|
|
760
|
+
if save_scene_to_path(scene_root, params.scene_path):
|
|
761
|
+
print("Node duplicated successfully as '" + duplicate.name + "'")
|
|
762
|
+
else:
|
|
763
|
+
log_error("Failed to save scene after duplicating node")
|
|
764
|
+
quit(1)
|
|
765
|
+
|
|
766
|
+
func set_owner_recursive(node: Node, owner: Node):
|
|
767
|
+
node.owner = owner
|
|
768
|
+
for child in node.get_children():
|
|
769
|
+
set_owner_recursive(child, owner)
|
|
770
|
+
|
|
771
|
+
# List signals defined on a node and their current connections
|
|
772
|
+
func get_node_signals(params):
|
|
773
|
+
var scene_root = load_scene_instance(params.scene_path)
|
|
774
|
+
if not scene_root: quit(1)
|
|
775
|
+
|
|
776
|
+
var node = find_node_by_path(scene_root, params.node_path)
|
|
777
|
+
if not node:
|
|
778
|
+
log_error("Node not found: " + params.node_path)
|
|
779
|
+
quit(1)
|
|
780
|
+
|
|
781
|
+
var signals = []
|
|
782
|
+
for sig in node.get_signal_list():
|
|
783
|
+
var sig_name = sig["name"]
|
|
784
|
+
var connections = []
|
|
785
|
+
for conn in node.get_signal_connection_list(sig_name):
|
|
786
|
+
connections.append({
|
|
787
|
+
"signal": sig_name,
|
|
788
|
+
"target": str(conn["callable"].get_object().get_path()) if conn["callable"].get_object() else "unknown",
|
|
789
|
+
"method": conn["callable"].get_method()
|
|
790
|
+
})
|
|
791
|
+
signals.append({
|
|
792
|
+
"name": sig_name,
|
|
793
|
+
"connections": connections
|
|
794
|
+
})
|
|
795
|
+
|
|
796
|
+
print(JSON.stringify({
|
|
797
|
+
"nodePath": params.node_path,
|
|
798
|
+
"nodeType": node.get_class(),
|
|
799
|
+
"signals": signals
|
|
800
|
+
}))
|
|
801
|
+
|
|
802
|
+
# Connect a signal from one node to a method on another node
|
|
803
|
+
func connect_signal(params):
|
|
804
|
+
var scene_root = load_scene_instance(params.scene_path)
|
|
805
|
+
if not scene_root: quit(1)
|
|
806
|
+
|
|
807
|
+
var source = find_node_by_path(scene_root, params.node_path)
|
|
808
|
+
if not source:
|
|
809
|
+
log_error("Source node not found: " + params.node_path)
|
|
810
|
+
quit(1)
|
|
811
|
+
|
|
812
|
+
var target = find_node_by_path(scene_root, params.target_node_path)
|
|
813
|
+
if not target:
|
|
814
|
+
log_error("Target node not found: " + params.target_node_path)
|
|
815
|
+
quit(1)
|
|
816
|
+
|
|
817
|
+
if not source.has_signal(params.signal):
|
|
818
|
+
log_error("Signal does not exist: " + params.signal + " on " + source.get_class())
|
|
819
|
+
quit(1)
|
|
820
|
+
|
|
821
|
+
if not target.has_method(params.method):
|
|
822
|
+
log_error("Method does not exist: " + params.method + " on " + target.get_class())
|
|
823
|
+
quit(1)
|
|
824
|
+
|
|
825
|
+
var err = source.connect(params.signal, Callable(target, params.method))
|
|
826
|
+
if err != OK:
|
|
827
|
+
log_error("Failed to connect signal: " + str(err))
|
|
828
|
+
quit(1)
|
|
829
|
+
|
|
830
|
+
if save_scene_to_path(scene_root, params.scene_path):
|
|
831
|
+
print("Signal '" + params.signal + "' connected from '" + params.node_path + "' to '" + params.target_node_path + "." + params.method + "'")
|
|
832
|
+
else:
|
|
833
|
+
log_error("Failed to save scene after connecting signal")
|
|
834
|
+
quit(1)
|
|
835
|
+
|
|
836
|
+
# Disconnect a signal connection between two nodes
|
|
837
|
+
func disconnect_signal(params):
|
|
838
|
+
var scene_root = load_scene_instance(params.scene_path)
|
|
839
|
+
if not scene_root: quit(1)
|
|
840
|
+
|
|
841
|
+
var source = find_node_by_path(scene_root, params.node_path)
|
|
842
|
+
if not source:
|
|
843
|
+
log_error("Source node not found: " + params.node_path)
|
|
844
|
+
quit(1)
|
|
845
|
+
|
|
846
|
+
var target = find_node_by_path(scene_root, params.target_node_path)
|
|
847
|
+
if not target:
|
|
848
|
+
log_error("Target node not found: " + params.target_node_path)
|
|
849
|
+
quit(1)
|
|
850
|
+
|
|
851
|
+
if not source.is_connected(params.signal, Callable(target, params.method)):
|
|
852
|
+
log_error("Signal connection does not exist")
|
|
853
|
+
quit(1)
|
|
854
|
+
|
|
855
|
+
source.disconnect(params.signal, Callable(target, params.method))
|
|
856
|
+
|
|
857
|
+
if save_scene_to_path(scene_root, params.scene_path):
|
|
858
|
+
print("Signal '" + params.signal + "' disconnected from '" + params.target_node_path + "." + params.method + "'")
|
|
859
|
+
else:
|
|
860
|
+
log_error("Failed to save scene after disconnecting signal")
|
|
861
|
+
quit(1)
|
|
862
|
+
|
|
863
|
+
# ============================================
|
|
864
|
+
# VALIDATE OPERATION
|
|
865
|
+
# ============================================
|
|
866
|
+
|
|
867
|
+
# Validate a GDScript or scene file by loading it headlessly
|
|
868
|
+
func validate_resource(params):
|
|
869
|
+
if not (params.has("script_path") or params.has("scene_path")):
|
|
870
|
+
log_error("validate_resource requires script_path or scene_path")
|
|
871
|
+
quit(1)
|
|
872
|
+
var result = _validate_single(params)
|
|
873
|
+
print(JSON.stringify({"valid": result.valid, "errors": result.errors}))
|
|
874
|
+
|
|
875
|
+
# ============================================
|
|
876
|
+
# BATCH OPERATIONS
|
|
877
|
+
# ============================================
|
|
878
|
+
|
|
879
|
+
# Helper: coerce a JSON-parsed value to a GDScript type (Vector2, Vector3, Color)
|
|
880
|
+
func _coerce_property_value(value):
|
|
881
|
+
if typeof(value) == TYPE_DICTIONARY:
|
|
882
|
+
if value.has("x") and value.has("y"):
|
|
883
|
+
if value.has("z"):
|
|
884
|
+
return Vector3(value.x, value.y, value.z)
|
|
885
|
+
else:
|
|
886
|
+
return Vector2(value.x, value.y)
|
|
887
|
+
elif value.has("r") and value.has("g") and value.has("b"):
|
|
888
|
+
var a = value.a if value.has("a") else 1.0
|
|
889
|
+
return Color(value.r, value.g, value.b, a)
|
|
890
|
+
return value
|
|
891
|
+
|
|
892
|
+
# Helper: collect node properties into a serializable Dictionary
|
|
893
|
+
func _collect_node_properties(node: Node, changed_only: bool) -> Dictionary:
|
|
894
|
+
var default_node = null
|
|
895
|
+
if changed_only:
|
|
896
|
+
default_node = instantiate_class(node.get_class())
|
|
897
|
+
|
|
898
|
+
var properties = {}
|
|
899
|
+
var property_list = node.get_property_list()
|
|
900
|
+
|
|
901
|
+
for prop in property_list:
|
|
902
|
+
var prop_name = prop["name"]
|
|
903
|
+
var prop_usage = prop["usage"]
|
|
904
|
+
|
|
905
|
+
if prop_usage & PROPERTY_USAGE_STORAGE or prop_usage & PROPERTY_USAGE_EDITOR:
|
|
906
|
+
var value = node.get(prop_name)
|
|
907
|
+
|
|
908
|
+
if default_node and default_node.get(prop_name) == value:
|
|
909
|
+
continue
|
|
910
|
+
|
|
911
|
+
if value is Vector2:
|
|
912
|
+
properties[prop_name] = {"x": value.x, "y": value.y}
|
|
913
|
+
elif value is Vector3:
|
|
914
|
+
properties[prop_name] = {"x": value.x, "y": value.y, "z": value.z}
|
|
915
|
+
elif value is Color:
|
|
916
|
+
properties[prop_name] = {"r": value.r, "g": value.g, "b": value.b, "a": value.a}
|
|
917
|
+
elif value is Transform2D:
|
|
918
|
+
properties[prop_name] = str(value)
|
|
919
|
+
elif value is Transform3D:
|
|
920
|
+
properties[prop_name] = str(value)
|
|
921
|
+
elif value is Object:
|
|
922
|
+
if value:
|
|
923
|
+
properties[prop_name] = value.get_class()
|
|
924
|
+
else:
|
|
925
|
+
properties[prop_name] = null
|
|
926
|
+
elif typeof(value) in [TYPE_NIL, TYPE_BOOL, TYPE_INT, TYPE_FLOAT, TYPE_STRING, TYPE_ARRAY, TYPE_DICTIONARY]:
|
|
927
|
+
properties[prop_name] = value
|
|
928
|
+
else:
|
|
929
|
+
properties[prop_name] = str(value)
|
|
930
|
+
|
|
931
|
+
if default_node:
|
|
932
|
+
default_node.free()
|
|
933
|
+
|
|
934
|
+
return properties
|
|
935
|
+
|
|
936
|
+
# Helper: validate a single target dict (script_path or scene_path)
|
|
937
|
+
func _validate_single(target: Dictionary) -> Dictionary:
|
|
938
|
+
if target.has("script_path") and target.script_path != "":
|
|
939
|
+
var path = normalize_scene_path(target.script_path)
|
|
940
|
+
if not FileAccess.file_exists(path):
|
|
941
|
+
return {"valid": false, "errors": [{"message": "File not found: " + path}], "target": target.script_path}
|
|
942
|
+
var resource = load(path)
|
|
943
|
+
# Actual parse errors go to stderr and are parsed by TypeScript
|
|
944
|
+
return {"valid": resource != null, "errors": [], "target": target.script_path}
|
|
945
|
+
elif target.has("scene_path") and target.scene_path != "":
|
|
946
|
+
var path = normalize_scene_path(target.scene_path)
|
|
947
|
+
if not FileAccess.file_exists(path):
|
|
948
|
+
return {"valid": false, "errors": [{"message": "File not found: " + path}], "target": target.scene_path}
|
|
949
|
+
var scene = load(path)
|
|
950
|
+
return {"valid": scene != null, "errors": [], "target": target.scene_path}
|
|
951
|
+
else:
|
|
952
|
+
return {"valid": false, "errors": [{"message": "No valid target: provide script_path or scene_path"}], "target": ""}
|
|
953
|
+
|
|
954
|
+
# Validate multiple scripts/scenes in a single headless process
|
|
955
|
+
func validate_batch(params: Dictionary) -> void:
|
|
956
|
+
var results: Array = []
|
|
957
|
+
for target in params.targets:
|
|
958
|
+
results.append(_validate_single(target))
|
|
959
|
+
print(JSON.stringify({"results": results}))
|
|
960
|
+
|
|
961
|
+
# Helper: add a node to a scene root without saving (returns error string or "")
|
|
962
|
+
func _batch_add_node(scene_root: Node, op: Dictionary) -> String:
|
|
963
|
+
var parent_path = "root"
|
|
964
|
+
if op.has("parent_node_path"):
|
|
965
|
+
parent_path = op.parent_node_path
|
|
966
|
+
var parent = find_node_by_path(scene_root, parent_path)
|
|
967
|
+
if not parent:
|
|
968
|
+
return "Parent node not found: " + parent_path
|
|
969
|
+
if not op.has("node_type") or op.node_type == "":
|
|
970
|
+
return "node_type is required for add_node"
|
|
971
|
+
if not op.has("node_name") or op.node_name == "":
|
|
972
|
+
return "node_name is required for add_node"
|
|
973
|
+
var new_node = instantiate_class(op.node_type)
|
|
974
|
+
if not new_node:
|
|
975
|
+
return "Failed to instantiate node of type: " + op.node_type
|
|
976
|
+
new_node.name = op.node_name
|
|
977
|
+
if op.has("properties"):
|
|
978
|
+
for property in op.properties:
|
|
979
|
+
new_node.set(property, op.properties[property])
|
|
980
|
+
parent.add_child(new_node)
|
|
981
|
+
new_node.owner = scene_root
|
|
982
|
+
return ""
|
|
983
|
+
|
|
984
|
+
# Helper: set a sprite texture without saving (returns error string or "")
|
|
985
|
+
func _batch_load_sprite(scene_root: Node, op: Dictionary) -> String:
|
|
986
|
+
if not op.has("node_path") or op.node_path == "":
|
|
987
|
+
return "node_path is required for load_sprite"
|
|
988
|
+
if not op.has("texture_path") or op.texture_path == "":
|
|
989
|
+
return "texture_path is required for load_sprite"
|
|
990
|
+
var sprite_node = find_node_by_path(scene_root, op.node_path)
|
|
991
|
+
if not sprite_node:
|
|
992
|
+
return "Node not found: " + op.node_path
|
|
993
|
+
if not (sprite_node is Sprite2D or sprite_node is Sprite3D or sprite_node is TextureRect):
|
|
994
|
+
return "Node is not sprite-compatible: " + sprite_node.get_class()
|
|
995
|
+
var texture = load(normalize_scene_path(op.texture_path))
|
|
996
|
+
if not texture:
|
|
997
|
+
return "Failed to load texture: " + op.texture_path
|
|
998
|
+
sprite_node.texture = texture
|
|
999
|
+
return ""
|
|
1000
|
+
|
|
1001
|
+
# Execute multiple scene operations in a single headless process
|
|
1002
|
+
# Scenes are loaded once and cached in memory; mutations accumulate until a save op
|
|
1003
|
+
func batch_scene_operations(params: Dictionary) -> void:
|
|
1004
|
+
var abort_on_error = params.get("abort_on_error", false)
|
|
1005
|
+
var results: Array = []
|
|
1006
|
+
var scene_cache: Dictionary = {}
|
|
1007
|
+
|
|
1008
|
+
for op in params.operations:
|
|
1009
|
+
var op_name = op.get("operation", "")
|
|
1010
|
+
var scene_path = op.get("scene_path", "")
|
|
1011
|
+
var result = {"operation": op_name, "scenePath": scene_path}
|
|
1012
|
+
|
|
1013
|
+
if scene_path != "" and scene_path not in scene_cache:
|
|
1014
|
+
var scene_root = load_scene_instance(scene_path)
|
|
1015
|
+
if scene_root:
|
|
1016
|
+
scene_cache[scene_path] = scene_root
|
|
1017
|
+
else:
|
|
1018
|
+
result["error"] = "Failed to load scene: " + scene_path
|
|
1019
|
+
results.append(result)
|
|
1020
|
+
if abort_on_error:
|
|
1021
|
+
break
|
|
1022
|
+
continue
|
|
1023
|
+
|
|
1024
|
+
var scene_root = scene_cache.get(scene_path, null) if scene_path != "" else null
|
|
1025
|
+
|
|
1026
|
+
match op_name:
|
|
1027
|
+
"add_node":
|
|
1028
|
+
if scene_root == null:
|
|
1029
|
+
result["error"] = "scene_path required for add_node"
|
|
1030
|
+
else:
|
|
1031
|
+
var err = _batch_add_node(scene_root, op)
|
|
1032
|
+
if err != "":
|
|
1033
|
+
result["error"] = err
|
|
1034
|
+
else:
|
|
1035
|
+
result["success"] = true
|
|
1036
|
+
"load_sprite":
|
|
1037
|
+
if scene_root == null:
|
|
1038
|
+
result["error"] = "scene_path required for load_sprite"
|
|
1039
|
+
else:
|
|
1040
|
+
var err = _batch_load_sprite(scene_root, op)
|
|
1041
|
+
if err != "":
|
|
1042
|
+
result["error"] = err
|
|
1043
|
+
else:
|
|
1044
|
+
result["success"] = true
|
|
1045
|
+
"save":
|
|
1046
|
+
if scene_root == null:
|
|
1047
|
+
result["error"] = "scene_path required for save"
|
|
1048
|
+
else:
|
|
1049
|
+
var new_path = op.get("new_path", scene_path)
|
|
1050
|
+
if save_scene_to_path(scene_root, new_path):
|
|
1051
|
+
result["success"] = true
|
|
1052
|
+
# Only evict on normal save; save-as leaves the mutated scene in
|
|
1053
|
+
# cache so subsequent ops on scene_path still see accumulated mutations.
|
|
1054
|
+
if new_path == scene_path:
|
|
1055
|
+
scene_cache.erase(scene_path)
|
|
1056
|
+
else:
|
|
1057
|
+
result["error"] = "Failed to save scene: " + scene_path
|
|
1058
|
+
_:
|
|
1059
|
+
result["error"] = "Unknown batch operation: " + op_name
|
|
1060
|
+
|
|
1061
|
+
results.append(result)
|
|
1062
|
+
if abort_on_error and result.has("error"):
|
|
1063
|
+
break
|
|
1064
|
+
|
|
1065
|
+
# Auto-save any scenes that were mutated but not explicitly saved
|
|
1066
|
+
for scene_path in scene_cache:
|
|
1067
|
+
save_scene_to_path(scene_cache[scene_path], scene_path)
|
|
1068
|
+
|
|
1069
|
+
print(JSON.stringify({"results": results}))
|
|
1070
|
+
|
|
1071
|
+
# Update multiple node properties in a single headless process (loads and saves scene once)
|
|
1072
|
+
func batch_set_node_properties(params: Dictionary) -> void:
|
|
1073
|
+
var scene_root = load_scene_instance(params.scene_path)
|
|
1074
|
+
if not scene_root:
|
|
1075
|
+
print(JSON.stringify({"error": "Failed to load scene: " + params.scene_path, "results": []}))
|
|
1076
|
+
return
|
|
1077
|
+
|
|
1078
|
+
var abort_on_error = params.get("abort_on_error", false)
|
|
1079
|
+
var results: Array = []
|
|
1080
|
+
|
|
1081
|
+
for update in params.updates:
|
|
1082
|
+
var result = {"nodePath": update.node_path, "property": update.property}
|
|
1083
|
+
var node = find_node_by_path(scene_root, update.node_path)
|
|
1084
|
+
if node == null:
|
|
1085
|
+
result["error"] = "Node not found: " + update.node_path
|
|
1086
|
+
else:
|
|
1087
|
+
node.set(update.property, _coerce_property_value(update.value))
|
|
1088
|
+
result["success"] = true
|
|
1089
|
+
results.append(result)
|
|
1090
|
+
if abort_on_error and result.has("error"):
|
|
1091
|
+
break
|
|
1092
|
+
|
|
1093
|
+
if save_scene_to_path(scene_root, params.scene_path):
|
|
1094
|
+
print(JSON.stringify({"results": results}))
|
|
1095
|
+
else:
|
|
1096
|
+
print(JSON.stringify({"error": "Failed to save scene after batch updates", "partial_results": results}))
|
|
1097
|
+
|
|
1098
|
+
# Get properties from multiple nodes in a single headless process (loads scene once)
|
|
1099
|
+
func batch_get_node_properties(params: Dictionary) -> void:
|
|
1100
|
+
var scene_root = load_scene_instance(params.scene_path)
|
|
1101
|
+
if not scene_root:
|
|
1102
|
+
print(JSON.stringify({"error": "Failed to load scene: " + params.scene_path, "results": []}))
|
|
1103
|
+
return
|
|
1104
|
+
|
|
1105
|
+
var results: Array = []
|
|
1106
|
+
|
|
1107
|
+
for node_spec in params.nodes:
|
|
1108
|
+
var node_path = node_spec.get("node_path", "")
|
|
1109
|
+
var changed_only = node_spec.get("changed_only", false)
|
|
1110
|
+
var node = find_node_by_path(scene_root, node_path)
|
|
1111
|
+
if node == null:
|
|
1112
|
+
results.append({"nodePath": node_path, "error": "Node not found"})
|
|
1113
|
+
else:
|
|
1114
|
+
var props = _collect_node_properties(node, changed_only)
|
|
1115
|
+
results.append({"nodePath": node_path, "nodeType": node.get_class(), "properties": props})
|
|
1116
|
+
|
|
1117
|
+
print(JSON.stringify({"results": results}))
|