godot-mcp-runtime 2.3.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -132
- package/dist/dispatch.d.ts +1 -11
- package/dist/dispatch.d.ts.map +1 -1
- package/dist/dispatch.js +32 -33
- package/dist/dispatch.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -10
- package/dist/index.js.map +1 -1
- package/dist/scripts/godot_operations.gd +268 -382
- package/dist/scripts/mcp_bridge.gd +206 -44
- package/dist/tools/autoload-tools.d.ts +51 -0
- package/dist/tools/autoload-tools.d.ts.map +1 -0
- package/dist/tools/autoload-tools.js +191 -0
- package/dist/tools/autoload-tools.js.map +1 -0
- package/dist/tools/node-tools.d.ts +9 -78
- package/dist/tools/node-tools.d.ts.map +1 -1
- package/dist/tools/node-tools.js +188 -312
- package/dist/tools/node-tools.js.map +1 -1
- package/dist/tools/project-tools.d.ts +0 -168
- package/dist/tools/project-tools.d.ts.map +1 -1
- package/dist/tools/project-tools.js +191 -1240
- package/dist/tools/project-tools.js.map +1 -1
- package/dist/tools/runtime-tools.d.ts +108 -0
- package/dist/tools/runtime-tools.d.ts.map +1 -0
- package/dist/tools/runtime-tools.js +994 -0
- package/dist/tools/runtime-tools.js.map +1 -0
- package/dist/tools/scene-tools.d.ts +6 -48
- package/dist/tools/scene-tools.d.ts.map +1 -1
- package/dist/tools/scene-tools.js +76 -212
- package/dist/tools/scene-tools.js.map +1 -1
- package/dist/tools/validate-tools.d.ts.map +1 -1
- package/dist/tools/validate-tools.js +115 -51
- package/dist/tools/validate-tools.js.map +1 -1
- package/dist/utils/autoload-ini.d.ts +38 -0
- package/dist/utils/autoload-ini.d.ts.map +1 -0
- package/dist/utils/autoload-ini.js +124 -0
- package/dist/utils/autoload-ini.js.map +1 -0
- package/dist/utils/bridge-manager.d.ts +46 -0
- package/dist/utils/bridge-manager.d.ts.map +1 -0
- package/dist/utils/bridge-manager.js +186 -0
- package/dist/utils/bridge-manager.js.map +1 -0
- package/dist/utils/bridge-protocol.d.ts +37 -0
- package/dist/utils/bridge-protocol.d.ts.map +1 -0
- package/dist/utils/bridge-protocol.js +78 -0
- package/dist/utils/bridge-protocol.js.map +1 -0
- package/dist/utils/godot-runner.d.ts +102 -16
- package/dist/utils/godot-runner.d.ts.map +1 -1
- package/dist/utils/godot-runner.js +497 -284
- package/dist/utils/godot-runner.js.map +1 -1
- package/dist/utils/handler-helpers.d.ts +34 -0
- package/dist/utils/handler-helpers.d.ts.map +1 -0
- package/dist/utils/handler-helpers.js +55 -0
- package/dist/utils/handler-helpers.js.map +1 -0
- package/dist/utils/logger.d.ts +4 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +11 -0
- package/dist/utils/logger.js.map +1 -0
- package/package.json +8 -4
|
@@ -10,11 +10,15 @@ func _init():
|
|
|
10
10
|
# Check for debug flag
|
|
11
11
|
debug_mode = "--debug-godot" in args
|
|
12
12
|
|
|
13
|
+
# SceneTree.quit(n) only schedules a quit for end-of-frame in Godot 4.
|
|
14
|
+
# Every quit(1) must be followed by `return` to halt the failing path,
|
|
15
|
+
# otherwise control falls through into success-print + scene save.
|
|
13
16
|
# Find the script argument and determine the positions of operation and params
|
|
14
17
|
var script_index = args.find("--script")
|
|
15
18
|
if script_index == -1:
|
|
16
19
|
log_error("Could not find --script argument")
|
|
17
20
|
quit(1)
|
|
21
|
+
return
|
|
18
22
|
|
|
19
23
|
var operation_index = script_index + 2
|
|
20
24
|
var params_index = script_index + 3
|
|
@@ -23,6 +27,7 @@ func _init():
|
|
|
23
27
|
log_error("Usage: godot --headless --script godot_operations.gd <operation> <json_params>")
|
|
24
28
|
log_error("Not enough command-line arguments provided.")
|
|
25
29
|
quit(1)
|
|
30
|
+
return
|
|
26
31
|
|
|
27
32
|
log_debug("All arguments: " + str(args))
|
|
28
33
|
|
|
@@ -42,10 +47,12 @@ func _init():
|
|
|
42
47
|
log_error("Failed to parse JSON parameters: " + params_json)
|
|
43
48
|
log_error("JSON Error: " + json.get_error_message() + " at line " + str(json.get_error_line()))
|
|
44
49
|
quit(1)
|
|
50
|
+
return
|
|
45
51
|
|
|
46
52
|
if not params:
|
|
47
53
|
log_error("Failed to parse JSON parameters: " + params_json)
|
|
48
54
|
quit(1)
|
|
55
|
+
return
|
|
49
56
|
|
|
50
57
|
log_info("Executing operation: " + operation)
|
|
51
58
|
|
|
@@ -61,19 +68,13 @@ func _init():
|
|
|
61
68
|
export_mesh_library(params)
|
|
62
69
|
"save_scene":
|
|
63
70
|
save_scene(params)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
"delete_node":
|
|
70
|
-
delete_node(params)
|
|
71
|
-
"set_node_property":
|
|
72
|
-
set_node_property(params)
|
|
71
|
+
# Node operations (always-array)
|
|
72
|
+
"delete_nodes":
|
|
73
|
+
delete_nodes(params)
|
|
74
|
+
"set_node_properties":
|
|
75
|
+
set_node_properties(params)
|
|
73
76
|
"get_node_properties":
|
|
74
77
|
get_node_properties(params)
|
|
75
|
-
"list_nodes":
|
|
76
|
-
list_nodes(params)
|
|
77
78
|
"get_scene_tree":
|
|
78
79
|
get_scene_tree(params)
|
|
79
80
|
"attach_script":
|
|
@@ -93,15 +94,13 @@ func _init():
|
|
|
93
94
|
validate_batch(params)
|
|
94
95
|
"batch_scene_operations":
|
|
95
96
|
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
97
|
_:
|
|
101
98
|
log_error("Unknown operation: " + operation)
|
|
102
99
|
quit(1)
|
|
100
|
+
return
|
|
103
101
|
|
|
104
102
|
quit()
|
|
103
|
+
return
|
|
105
104
|
|
|
106
105
|
# Logging functions
|
|
107
106
|
func log_debug(message):
|
|
@@ -220,14 +219,22 @@ func load_scene_instance(scene_path: String):
|
|
|
220
219
|
|
|
221
220
|
return instance
|
|
222
221
|
|
|
223
|
-
# Helper to find a node by path
|
|
222
|
+
# Helper to find a node by path. Accepts "root", ".", "" (all → scene_root),
|
|
223
|
+
# the actual scene root's name (e.g. "Main"), or a path with either as the first
|
|
224
|
+
# segment (e.g. "root/Button" or "Main/Button"). Bare paths ("Button") resolve
|
|
225
|
+
# normally via get_node_or_null.
|
|
224
226
|
func find_node_by_path(scene_root: Node, node_path: String) -> Node:
|
|
225
|
-
if node_path == "
|
|
227
|
+
if node_path == "" or node_path == "." or node_path == "root":
|
|
228
|
+
return scene_root
|
|
229
|
+
if node_path == String(scene_root.name):
|
|
226
230
|
return scene_root
|
|
227
231
|
|
|
228
232
|
var path = node_path
|
|
229
|
-
|
|
230
|
-
|
|
233
|
+
var first_slash = path.find("/")
|
|
234
|
+
if first_slash != -1:
|
|
235
|
+
var first_segment = path.substr(0, first_slash)
|
|
236
|
+
if first_segment == "root" or first_segment == String(scene_root.name):
|
|
237
|
+
path = path.substr(first_slash + 1)
|
|
231
238
|
|
|
232
239
|
if path.is_empty():
|
|
233
240
|
return scene_root
|
|
@@ -252,6 +259,20 @@ func save_scene_to_path(scene_root: Node, save_path: String) -> bool:
|
|
|
252
259
|
|
|
253
260
|
return true
|
|
254
261
|
|
|
262
|
+
# Ensure the parent directory of a res:// path exists, creating it recursively
|
|
263
|
+
# if needed. Returns true on success or when the directory already exists.
|
|
264
|
+
func _ensure_res_dir(full_res_path: String) -> bool:
|
|
265
|
+
var dir_path = full_res_path.get_base_dir()
|
|
266
|
+
if dir_path == "res://" or dir_path.is_empty():
|
|
267
|
+
return true
|
|
268
|
+
var dir = DirAccess.open("res://")
|
|
269
|
+
if not dir:
|
|
270
|
+
return false
|
|
271
|
+
var relative_dir = dir_path.substr(6) if dir_path.begins_with("res://") else dir_path
|
|
272
|
+
if relative_dir.is_empty() or dir.dir_exists(relative_dir):
|
|
273
|
+
return true
|
|
274
|
+
return dir.make_dir_recursive(relative_dir) == OK
|
|
275
|
+
|
|
255
276
|
# Create a new scene with a specified root node type
|
|
256
277
|
func create_scene(params):
|
|
257
278
|
printerr("Creating scene: " + params.scene_path)
|
|
@@ -268,66 +289,94 @@ func create_scene(params):
|
|
|
268
289
|
if not scene_root:
|
|
269
290
|
log_error("Failed to instantiate node of type: " + root_node_type)
|
|
270
291
|
quit(1)
|
|
292
|
+
return
|
|
271
293
|
|
|
272
294
|
scene_root.name = "root"
|
|
273
295
|
scene_root.owner = scene_root
|
|
274
296
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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)
|
|
297
|
+
if not _ensure_res_dir(full_scene_path):
|
|
298
|
+
log_error("Failed to create directory for scene: " + full_scene_path)
|
|
299
|
+
quit(1)
|
|
300
|
+
return
|
|
286
301
|
|
|
287
302
|
if save_scene_to_path(scene_root, full_scene_path):
|
|
288
303
|
print("Scene created successfully at: " + params.scene_path)
|
|
289
304
|
else:
|
|
290
305
|
log_error("Failed to create scene: " + params.scene_path)
|
|
291
306
|
quit(1)
|
|
307
|
+
return
|
|
292
308
|
|
|
293
309
|
# Add a node to an existing scene
|
|
310
|
+
# Apply an add_node mutation without saving. Shared by standalone add_node
|
|
311
|
+
# and batch_scene_operations so both paths validate identically.
|
|
312
|
+
# Returns {"ok": bool, "error": String}; error is empty on success.
|
|
313
|
+
func _apply_add_node(scene_root: Node, op: Dictionary) -> Dictionary:
|
|
314
|
+
var parent_path = "root"
|
|
315
|
+
if op.has("parent_node_path"):
|
|
316
|
+
parent_path = op.parent_node_path
|
|
317
|
+
var parent = find_node_by_path(scene_root, parent_path)
|
|
318
|
+
if not parent:
|
|
319
|
+
return {"ok": false, "error": "Parent node not found: " + parent_path}
|
|
320
|
+
if not op.has("node_type") or op.node_type == "":
|
|
321
|
+
return {"ok": false, "error": "node_type is required for add_node"}
|
|
322
|
+
if not op.has("node_name") or op.node_name == "":
|
|
323
|
+
return {"ok": false, "error": "node_name is required for add_node"}
|
|
324
|
+
var new_node = instantiate_class(op.node_type)
|
|
325
|
+
if not new_node:
|
|
326
|
+
return {"ok": false, "error": "Failed to instantiate node of type: " + op.node_type}
|
|
327
|
+
new_node.name = op.node_name
|
|
328
|
+
if op.has("properties"):
|
|
329
|
+
for property in op.properties:
|
|
330
|
+
new_node.set(property, _coerce_property_value(op.properties[property]))
|
|
331
|
+
parent.add_child(new_node)
|
|
332
|
+
new_node.owner = scene_root
|
|
333
|
+
return {"ok": true, "error": ""}
|
|
334
|
+
|
|
335
|
+
# Apply a load_sprite mutation without saving. Shared by standalone load_sprite
|
|
336
|
+
# and batch_scene_operations.
|
|
337
|
+
func _apply_load_sprite(scene_root: Node, op: Dictionary) -> Dictionary:
|
|
338
|
+
if not op.has("node_path") or op.node_path == "":
|
|
339
|
+
return {"ok": false, "error": "node_path is required for load_sprite"}
|
|
340
|
+
if not op.has("texture_path") or op.texture_path == "":
|
|
341
|
+
return {"ok": false, "error": "texture_path is required for load_sprite"}
|
|
342
|
+
var sprite_node = find_node_by_path(scene_root, op.node_path)
|
|
343
|
+
if not sprite_node:
|
|
344
|
+
return {"ok": false, "error": "Node not found: " + op.node_path}
|
|
345
|
+
if not (sprite_node is Sprite2D or sprite_node is Sprite3D or sprite_node is TextureRect):
|
|
346
|
+
return {"ok": false, "error": "Node is not a sprite-compatible type: " + sprite_node.get_class()}
|
|
347
|
+
var full_texture_path = normalize_scene_path(op.texture_path)
|
|
348
|
+
var texture = load(full_texture_path)
|
|
349
|
+
if not texture:
|
|
350
|
+
return {"ok": false, "error": "Failed to load texture: " + full_texture_path}
|
|
351
|
+
if not (texture is Texture2D):
|
|
352
|
+
return {"ok": false, "error": "Loaded resource is not a Texture2D: " + full_texture_path}
|
|
353
|
+
# A texture without a resource_path is a runtime-only object — PackedScene.pack()
|
|
354
|
+
# cannot serialize it, so the assignment would silently vanish on save.
|
|
355
|
+
if texture.resource_path == "":
|
|
356
|
+
return {"ok": false, "error": "Texture has no resource_path — likely not imported. Open project in Godot editor once, or run 'godot --headless --editor --quit' to import assets."}
|
|
357
|
+
sprite_node.texture = texture
|
|
358
|
+
return {"ok": true, "error": ""}
|
|
359
|
+
|
|
294
360
|
func add_node(params):
|
|
295
361
|
printerr("Adding node to scene: " + params.scene_path)
|
|
296
362
|
|
|
297
363
|
var scene_root = load_scene_instance(params.scene_path)
|
|
298
364
|
if not scene_root:
|
|
299
365
|
quit(1)
|
|
366
|
+
return
|
|
300
367
|
|
|
301
|
-
var
|
|
302
|
-
if
|
|
303
|
-
|
|
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)
|
|
368
|
+
var result = _apply_add_node(scene_root, params)
|
|
369
|
+
if not result.ok:
|
|
370
|
+
log_error(result.error)
|
|
313
371
|
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
|
|
372
|
+
return
|
|
325
373
|
|
|
326
374
|
if save_scene_to_path(scene_root, params.scene_path):
|
|
327
375
|
print("Node '" + params.node_name + "' of type '" + params.node_type + "' added successfully")
|
|
328
376
|
else:
|
|
329
377
|
log_error("Failed to save scene after adding node")
|
|
330
378
|
quit(1)
|
|
379
|
+
return
|
|
331
380
|
|
|
332
381
|
# Load a sprite into a Sprite2D node
|
|
333
382
|
func load_sprite(params):
|
|
@@ -336,29 +385,20 @@ func load_sprite(params):
|
|
|
336
385
|
var scene_root = load_scene_instance(params.scene_path)
|
|
337
386
|
if not scene_root:
|
|
338
387
|
quit(1)
|
|
388
|
+
return
|
|
339
389
|
|
|
340
|
-
var
|
|
341
|
-
if not
|
|
342
|
-
log_error(
|
|
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)
|
|
390
|
+
var result = _apply_load_sprite(scene_root, params)
|
|
391
|
+
if not result.ok:
|
|
392
|
+
log_error(result.error)
|
|
353
393
|
quit(1)
|
|
354
|
-
|
|
355
|
-
sprite_node.texture = texture
|
|
394
|
+
return
|
|
356
395
|
|
|
357
396
|
if save_scene_to_path(scene_root, params.scene_path):
|
|
358
397
|
print("Sprite loaded successfully with texture: " + params.texture_path)
|
|
359
398
|
else:
|
|
360
399
|
log_error("Failed to save scene after loading sprite")
|
|
361
400
|
quit(1)
|
|
401
|
+
return
|
|
362
402
|
|
|
363
403
|
# Export a scene as a MeshLibrary resource
|
|
364
404
|
func export_mesh_library(params):
|
|
@@ -367,6 +407,7 @@ func export_mesh_library(params):
|
|
|
367
407
|
var scene_root = load_scene_instance(params.scene_path)
|
|
368
408
|
if not scene_root:
|
|
369
409
|
quit(1)
|
|
410
|
+
return
|
|
370
411
|
|
|
371
412
|
var mesh_library = MeshLibrary.new()
|
|
372
413
|
|
|
@@ -398,22 +439,17 @@ func export_mesh_library(params):
|
|
|
398
439
|
mesh_library.set_item_shapes(item_id, [collision_child.shape])
|
|
399
440
|
break
|
|
400
441
|
|
|
401
|
-
|
|
402
|
-
mesh_library.set_item_preview(item_id, mesh_instance.mesh)
|
|
442
|
+
mesh_library.set_item_preview(item_id, mesh_instance.mesh)
|
|
403
443
|
|
|
404
444
|
item_id += 1
|
|
405
445
|
|
|
406
446
|
if item_id > 0:
|
|
407
447
|
var full_output_path = normalize_scene_path(params.output_path)
|
|
408
448
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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)
|
|
449
|
+
if not _ensure_res_dir(full_output_path):
|
|
450
|
+
log_error("Failed to create directory for MeshLibrary: " + full_output_path)
|
|
451
|
+
quit(1)
|
|
452
|
+
return
|
|
417
453
|
|
|
418
454
|
var error = ResourceSaver.save(mesh_library, full_output_path)
|
|
419
455
|
if error == OK:
|
|
@@ -421,9 +457,11 @@ func export_mesh_library(params):
|
|
|
421
457
|
else:
|
|
422
458
|
log_error("Failed to save MeshLibrary: " + str(error))
|
|
423
459
|
quit(1)
|
|
460
|
+
return
|
|
424
461
|
else:
|
|
425
462
|
log_error("No valid meshes found in the scene")
|
|
426
463
|
quit(1)
|
|
464
|
+
return
|
|
427
465
|
|
|
428
466
|
# Save changes to a scene file
|
|
429
467
|
func save_scene(params):
|
|
@@ -432,6 +470,7 @@ func save_scene(params):
|
|
|
432
470
|
var scene_root = load_scene_instance(params.scene_path)
|
|
433
471
|
if not scene_root:
|
|
434
472
|
quit(1)
|
|
473
|
+
return
|
|
435
474
|
|
|
436
475
|
var save_path = params.new_path if params.has("new_path") else params.scene_path
|
|
437
476
|
|
|
@@ -440,217 +479,109 @@ func save_scene(params):
|
|
|
440
479
|
else:
|
|
441
480
|
log_error("Failed to save scene")
|
|
442
481
|
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))
|
|
482
|
+
return
|
|
540
483
|
|
|
541
484
|
# ============================================
|
|
542
|
-
#
|
|
485
|
+
# NODE OPERATIONS
|
|
543
486
|
# ============================================
|
|
544
487
|
|
|
545
|
-
# Delete
|
|
546
|
-
func
|
|
547
|
-
printerr("Deleting
|
|
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)
|
|
488
|
+
# Delete one or more nodes from a scene (saves once)
|
|
489
|
+
func delete_nodes(params):
|
|
490
|
+
printerr("Deleting nodes from scene: " + params.scene_path)
|
|
575
491
|
|
|
576
492
|
var scene_root = load_scene_instance(params.scene_path)
|
|
577
493
|
if not scene_root:
|
|
578
494
|
quit(1)
|
|
495
|
+
return
|
|
579
496
|
|
|
580
|
-
var
|
|
581
|
-
|
|
582
|
-
|
|
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)
|
|
497
|
+
var node_paths: Array = params.node_paths
|
|
498
|
+
var results: Array = []
|
|
499
|
+
var any_deleted := false
|
|
591
500
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
501
|
+
for node_path in node_paths:
|
|
502
|
+
var entry = {"nodePath": node_path}
|
|
503
|
+
var node = find_node_by_path(scene_root, node_path)
|
|
504
|
+
if not node:
|
|
505
|
+
entry["error"] = "Node not found: " + node_path
|
|
506
|
+
elif node == scene_root:
|
|
507
|
+
entry["error"] = "Cannot delete the root node"
|
|
508
|
+
else:
|
|
509
|
+
var parent = node.get_parent()
|
|
510
|
+
parent.remove_child(node)
|
|
511
|
+
node.queue_free()
|
|
512
|
+
entry["success"] = true
|
|
513
|
+
any_deleted = true
|
|
514
|
+
results.append(entry)
|
|
515
|
+
|
|
516
|
+
if any_deleted:
|
|
517
|
+
if not save_scene_to_path(scene_root, params.scene_path):
|
|
518
|
+
print(JSON.stringify({"error": "Failed to save scene after deleting nodes", "results": results}))
|
|
519
|
+
return
|
|
597
520
|
|
|
598
|
-
|
|
599
|
-
func get_node_properties(params):
|
|
600
|
-
printerr("Getting node properties from scene: " + params.scene_path)
|
|
521
|
+
print(JSON.stringify({"results": results}))
|
|
601
522
|
|
|
523
|
+
# Update one or more node properties in a single headless process (saves once)
|
|
524
|
+
func set_node_properties(params: Dictionary) -> void:
|
|
602
525
|
var scene_root = load_scene_instance(params.scene_path)
|
|
603
526
|
if not scene_root:
|
|
604
|
-
|
|
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)
|
|
527
|
+
print(JSON.stringify({"error": "Failed to load scene: " + params.scene_path, "results": []}))
|
|
528
|
+
return
|
|
610
529
|
|
|
611
|
-
var
|
|
612
|
-
var
|
|
530
|
+
var abort_on_error = params.get("abort_on_error", false)
|
|
531
|
+
var results: Array = []
|
|
532
|
+
var any_set := false
|
|
613
533
|
|
|
614
|
-
|
|
615
|
-
"nodePath":
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
534
|
+
for update in params.updates:
|
|
535
|
+
var result = {"nodePath": update.node_path, "property": update.property}
|
|
536
|
+
var node = find_node_by_path(scene_root, update.node_path)
|
|
537
|
+
if node == null:
|
|
538
|
+
result["error"] = "Node not found: " + update.node_path
|
|
539
|
+
elif not (update.property in node):
|
|
540
|
+
result["error"] = "Property '%s' does not exist on node of type '%s'" % [update.property, node.get_class()]
|
|
541
|
+
else:
|
|
542
|
+
node.set(update.property, _coerce_property_value(update.value))
|
|
543
|
+
result["success"] = true
|
|
544
|
+
any_set = true
|
|
545
|
+
results.append(result)
|
|
546
|
+
if abort_on_error and result.has("error"):
|
|
547
|
+
break
|
|
619
548
|
|
|
620
|
-
|
|
549
|
+
if any_set:
|
|
550
|
+
if not save_scene_to_path(scene_root, params.scene_path):
|
|
551
|
+
print(JSON.stringify({"error": "Failed to save scene after updates", "results": results}))
|
|
552
|
+
return
|
|
621
553
|
|
|
622
|
-
|
|
623
|
-
func list_nodes(params):
|
|
624
|
-
printerr("Listing nodes in scene: " + params.scene_path)
|
|
554
|
+
print(JSON.stringify({"results": results}))
|
|
625
555
|
|
|
556
|
+
# Get properties from one or more nodes in a single headless process (loads scene once)
|
|
557
|
+
func get_node_properties(params: Dictionary) -> void:
|
|
626
558
|
var scene_root = load_scene_instance(params.scene_path)
|
|
627
559
|
if not scene_root:
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
var parent_path = "root"
|
|
631
|
-
if params.has("parent_path"):
|
|
632
|
-
parent_path = params.parent_path
|
|
560
|
+
print(JSON.stringify({"error": "Failed to load scene: " + params.scene_path, "results": []}))
|
|
561
|
+
return
|
|
633
562
|
|
|
634
|
-
var
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
563
|
+
var results: Array = []
|
|
564
|
+
# Class-name → default-instance cache. Reused across all nodes in this call
|
|
565
|
+
# so we don't instantiate a fresh default per node when changed_only is true.
|
|
566
|
+
var defaults_cache: Dictionary = {}
|
|
638
567
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
"
|
|
645
|
-
|
|
568
|
+
for node_spec in params.nodes:
|
|
569
|
+
var node_path = node_spec.get("node_path", "")
|
|
570
|
+
var changed_only = node_spec.get("changed_only", false)
|
|
571
|
+
var node = find_node_by_path(scene_root, node_path)
|
|
572
|
+
if node == null:
|
|
573
|
+
results.append({"nodePath": node_path, "error": "Node not found"})
|
|
574
|
+
else:
|
|
575
|
+
var props = _collect_node_properties(node, changed_only, defaults_cache)
|
|
576
|
+
results.append({"nodePath": node_path, "nodeType": node.get_class(), "properties": props})
|
|
646
577
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
578
|
+
# Free cached default instances; they were created via instantiate_class.
|
|
579
|
+
for klass in defaults_cache:
|
|
580
|
+
var inst = defaults_cache[klass]
|
|
581
|
+
if inst:
|
|
582
|
+
inst.free()
|
|
652
583
|
|
|
653
|
-
print(JSON.stringify(
|
|
584
|
+
print(JSON.stringify({"results": results}))
|
|
654
585
|
|
|
655
586
|
# Get full hierarchical tree structure of a scene
|
|
656
587
|
func get_scene_tree(params):
|
|
@@ -659,12 +590,21 @@ func get_scene_tree(params):
|
|
|
659
590
|
var scene_root = load_scene_instance(params.scene_path)
|
|
660
591
|
if not scene_root:
|
|
661
592
|
quit(1)
|
|
593
|
+
return
|
|
594
|
+
|
|
595
|
+
var tree_root = scene_root
|
|
596
|
+
if params.has("parent_path") and params.parent_path:
|
|
597
|
+
tree_root = find_node_by_path(scene_root, params.parent_path)
|
|
598
|
+
if not tree_root:
|
|
599
|
+
log_error("Parent node not found: " + str(params.parent_path))
|
|
600
|
+
quit(1)
|
|
601
|
+
return
|
|
662
602
|
|
|
663
603
|
var max_depth = -1
|
|
664
604
|
if params.has("max_depth"):
|
|
665
605
|
max_depth = int(params.max_depth)
|
|
666
606
|
|
|
667
|
-
var tree = build_tree_recursive(
|
|
607
|
+
var tree = build_tree_recursive(tree_root, "", 0, max_depth)
|
|
668
608
|
print(JSON.stringify(tree))
|
|
669
609
|
|
|
670
610
|
func build_tree_recursive(node: Node, path: String, depth: int = 0, max_depth: int = -1) -> Dictionary:
|
|
@@ -676,10 +616,9 @@ func build_tree_recursive(node: Node, path: String, depth: int = 0, max_depth: i
|
|
|
676
616
|
children.append(build_tree_recursive(child, node_path, depth + 1, max_depth))
|
|
677
617
|
|
|
678
618
|
var script_path = ""
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
script_path = script.resource_path
|
|
619
|
+
var script = node.get_script()
|
|
620
|
+
if script and script.resource_path:
|
|
621
|
+
script_path = script.resource_path
|
|
683
622
|
|
|
684
623
|
return {
|
|
685
624
|
"name": node.name,
|
|
@@ -696,22 +635,26 @@ func attach_script(params):
|
|
|
696
635
|
var scene_root = load_scene_instance(params.scene_path)
|
|
697
636
|
if not scene_root:
|
|
698
637
|
quit(1)
|
|
638
|
+
return
|
|
699
639
|
|
|
700
640
|
var node = find_node_by_path(scene_root, params.node_path)
|
|
701
641
|
if not node:
|
|
702
642
|
log_error("Node not found: " + params.node_path)
|
|
703
643
|
quit(1)
|
|
644
|
+
return
|
|
704
645
|
|
|
705
646
|
var full_script_path = normalize_scene_path(params.script_path)
|
|
706
647
|
|
|
707
648
|
if not FileAccess.file_exists(full_script_path):
|
|
708
649
|
log_error("Script file does not exist: " + full_script_path)
|
|
709
650
|
quit(1)
|
|
651
|
+
return
|
|
710
652
|
|
|
711
653
|
var script = load(full_script_path)
|
|
712
654
|
if not script:
|
|
713
655
|
log_error("Failed to load script: " + full_script_path)
|
|
714
656
|
quit(1)
|
|
657
|
+
return
|
|
715
658
|
|
|
716
659
|
node.set_script(script)
|
|
717
660
|
|
|
@@ -720,6 +663,7 @@ func attach_script(params):
|
|
|
720
663
|
else:
|
|
721
664
|
log_error("Failed to save scene after attaching script")
|
|
722
665
|
quit(1)
|
|
666
|
+
return
|
|
723
667
|
|
|
724
668
|
# ============================================
|
|
725
669
|
# SIGNAL AND DUPLICATE OPERATIONS
|
|
@@ -728,15 +672,19 @@ func attach_script(params):
|
|
|
728
672
|
# Duplicate a node and its children within a scene
|
|
729
673
|
func duplicate_node(params):
|
|
730
674
|
var scene_root = load_scene_instance(params.scene_path)
|
|
731
|
-
if not scene_root:
|
|
675
|
+
if not scene_root:
|
|
676
|
+
quit(1)
|
|
677
|
+
return
|
|
732
678
|
|
|
733
679
|
var node = find_node_by_path(scene_root, params.node_path)
|
|
734
680
|
if not node:
|
|
735
681
|
log_error("Node not found: " + params.node_path)
|
|
736
682
|
quit(1)
|
|
683
|
+
return
|
|
737
684
|
if node == scene_root:
|
|
738
685
|
log_error("Cannot duplicate the root node")
|
|
739
686
|
quit(1)
|
|
687
|
+
return
|
|
740
688
|
|
|
741
689
|
var duplicate = node.duplicate()
|
|
742
690
|
if params.has("new_name"):
|
|
@@ -750,33 +698,36 @@ func duplicate_node(params):
|
|
|
750
698
|
if not parent:
|
|
751
699
|
log_error("Target parent not found: " + params.target_parent_path)
|
|
752
700
|
quit(1)
|
|
701
|
+
return
|
|
753
702
|
|
|
754
703
|
parent.add_child(duplicate)
|
|
755
704
|
duplicate.owner = scene_root
|
|
756
|
-
#
|
|
757
|
-
|
|
758
|
-
|
|
705
|
+
# Iterative BFS to set owner on all descendants — avoids recursion depth.
|
|
706
|
+
var queue: Array = duplicate.get_children()
|
|
707
|
+
while not queue.is_empty():
|
|
708
|
+
var current = queue.pop_front()
|
|
709
|
+
current.owner = scene_root
|
|
710
|
+
queue.append_array(current.get_children())
|
|
759
711
|
|
|
760
712
|
if save_scene_to_path(scene_root, params.scene_path):
|
|
761
713
|
print("Node duplicated successfully as '" + duplicate.name + "'")
|
|
762
714
|
else:
|
|
763
715
|
log_error("Failed to save scene after duplicating node")
|
|
764
716
|
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)
|
|
717
|
+
return
|
|
770
718
|
|
|
771
719
|
# List signals defined on a node and their current connections
|
|
772
720
|
func get_node_signals(params):
|
|
773
721
|
var scene_root = load_scene_instance(params.scene_path)
|
|
774
|
-
if not scene_root:
|
|
722
|
+
if not scene_root:
|
|
723
|
+
quit(1)
|
|
724
|
+
return
|
|
775
725
|
|
|
776
726
|
var node = find_node_by_path(scene_root, params.node_path)
|
|
777
727
|
if not node:
|
|
778
728
|
log_error("Node not found: " + params.node_path)
|
|
779
729
|
quit(1)
|
|
730
|
+
return
|
|
780
731
|
|
|
781
732
|
var signals = []
|
|
782
733
|
for sig in node.get_signal_list():
|
|
@@ -802,55 +753,70 @@ func get_node_signals(params):
|
|
|
802
753
|
# Connect a signal from one node to a method on another node
|
|
803
754
|
func connect_signal(params):
|
|
804
755
|
var scene_root = load_scene_instance(params.scene_path)
|
|
805
|
-
if not scene_root:
|
|
756
|
+
if not scene_root:
|
|
757
|
+
quit(1)
|
|
758
|
+
return
|
|
806
759
|
|
|
807
760
|
var source = find_node_by_path(scene_root, params.node_path)
|
|
808
761
|
if not source:
|
|
809
762
|
log_error("Source node not found: " + params.node_path)
|
|
810
763
|
quit(1)
|
|
764
|
+
return
|
|
811
765
|
|
|
812
766
|
var target = find_node_by_path(scene_root, params.target_node_path)
|
|
813
767
|
if not target:
|
|
814
768
|
log_error("Target node not found: " + params.target_node_path)
|
|
815
769
|
quit(1)
|
|
770
|
+
return
|
|
816
771
|
|
|
817
772
|
if not source.has_signal(params.signal):
|
|
818
773
|
log_error("Signal does not exist: " + params.signal + " on " + source.get_class())
|
|
819
774
|
quit(1)
|
|
775
|
+
return
|
|
820
776
|
|
|
821
777
|
if not target.has_method(params.method):
|
|
822
778
|
log_error("Method does not exist: " + params.method + " on " + target.get_class())
|
|
823
779
|
quit(1)
|
|
780
|
+
return
|
|
824
781
|
|
|
825
|
-
|
|
782
|
+
# CONNECT_PERSIST is required for the connection to be serialized into the
|
|
783
|
+
# packed scene; without it the connection is runtime-only and disappears on save.
|
|
784
|
+
var err = source.connect(params.signal, Callable(target, params.method), CONNECT_PERSIST)
|
|
826
785
|
if err != OK:
|
|
827
786
|
log_error("Failed to connect signal: " + str(err))
|
|
828
787
|
quit(1)
|
|
788
|
+
return
|
|
829
789
|
|
|
830
790
|
if save_scene_to_path(scene_root, params.scene_path):
|
|
831
791
|
print("Signal '" + params.signal + "' connected from '" + params.node_path + "' to '" + params.target_node_path + "." + params.method + "'")
|
|
832
792
|
else:
|
|
833
793
|
log_error("Failed to save scene after connecting signal")
|
|
834
794
|
quit(1)
|
|
795
|
+
return
|
|
835
796
|
|
|
836
797
|
# Disconnect a signal connection between two nodes
|
|
837
798
|
func disconnect_signal(params):
|
|
838
799
|
var scene_root = load_scene_instance(params.scene_path)
|
|
839
|
-
if not scene_root:
|
|
800
|
+
if not scene_root:
|
|
801
|
+
quit(1)
|
|
802
|
+
return
|
|
840
803
|
|
|
841
804
|
var source = find_node_by_path(scene_root, params.node_path)
|
|
842
805
|
if not source:
|
|
843
806
|
log_error("Source node not found: " + params.node_path)
|
|
844
807
|
quit(1)
|
|
808
|
+
return
|
|
845
809
|
|
|
846
810
|
var target = find_node_by_path(scene_root, params.target_node_path)
|
|
847
811
|
if not target:
|
|
848
812
|
log_error("Target node not found: " + params.target_node_path)
|
|
849
813
|
quit(1)
|
|
814
|
+
return
|
|
850
815
|
|
|
851
816
|
if not source.is_connected(params.signal, Callable(target, params.method)):
|
|
852
817
|
log_error("Signal connection does not exist")
|
|
853
818
|
quit(1)
|
|
819
|
+
return
|
|
854
820
|
|
|
855
821
|
source.disconnect(params.signal, Callable(target, params.method))
|
|
856
822
|
|
|
@@ -859,6 +825,7 @@ func disconnect_signal(params):
|
|
|
859
825
|
else:
|
|
860
826
|
log_error("Failed to save scene after disconnecting signal")
|
|
861
827
|
quit(1)
|
|
828
|
+
return
|
|
862
829
|
|
|
863
830
|
# ============================================
|
|
864
831
|
# VALIDATE OPERATION
|
|
@@ -869,6 +836,7 @@ func validate_resource(params):
|
|
|
869
836
|
if not (params.has("script_path") or params.has("scene_path")):
|
|
870
837
|
log_error("validate_resource requires script_path or scene_path")
|
|
871
838
|
quit(1)
|
|
839
|
+
return
|
|
872
840
|
var result = _validate_single(params)
|
|
873
841
|
print(JSON.stringify({"valid": result.valid, "errors": result.errors}))
|
|
874
842
|
|
|
@@ -889,11 +857,20 @@ func _coerce_property_value(value):
|
|
|
889
857
|
return Color(value.r, value.g, value.b, a)
|
|
890
858
|
return value
|
|
891
859
|
|
|
892
|
-
# Helper: collect node properties into a serializable Dictionary
|
|
893
|
-
|
|
860
|
+
# Helper: collect node properties into a serializable Dictionary. When
|
|
861
|
+
# changed_only is true, compares each property against a default instance of
|
|
862
|
+
# the node's class. The defaults_cache dict is keyed by class name so the
|
|
863
|
+
# caller can reuse default instances across many nodes (caller is responsible
|
|
864
|
+
# for freeing the cache when done).
|
|
865
|
+
func _collect_node_properties(node: Node, changed_only: bool, defaults_cache: Dictionary) -> Dictionary:
|
|
894
866
|
var default_node = null
|
|
895
867
|
if changed_only:
|
|
896
|
-
|
|
868
|
+
var klass = node.get_class()
|
|
869
|
+
if defaults_cache.has(klass):
|
|
870
|
+
default_node = defaults_cache[klass]
|
|
871
|
+
else:
|
|
872
|
+
default_node = instantiate_class(klass)
|
|
873
|
+
defaults_cache[klass] = default_node
|
|
897
874
|
|
|
898
875
|
var properties = {}
|
|
899
876
|
var property_list = node.get_property_list()
|
|
@@ -928,9 +905,6 @@ func _collect_node_properties(node: Node, changed_only: bool) -> Dictionary:
|
|
|
928
905
|
else:
|
|
929
906
|
properties[prop_name] = str(value)
|
|
930
907
|
|
|
931
|
-
if default_node:
|
|
932
|
-
default_node.free()
|
|
933
|
-
|
|
934
908
|
return properties
|
|
935
909
|
|
|
936
910
|
# Helper: validate a single target dict (script_path or scene_path)
|
|
@@ -958,46 +932,6 @@ func validate_batch(params: Dictionary) -> void:
|
|
|
958
932
|
results.append(_validate_single(target))
|
|
959
933
|
print(JSON.stringify({"results": results}))
|
|
960
934
|
|
|
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
935
|
# Execute multiple scene operations in a single headless process
|
|
1002
936
|
# Scenes are loaded once and cached in memory; mutations accumulate until a save op
|
|
1003
937
|
func batch_scene_operations(params: Dictionary) -> void:
|
|
@@ -1028,18 +962,18 @@ func batch_scene_operations(params: Dictionary) -> void:
|
|
|
1028
962
|
if scene_root == null:
|
|
1029
963
|
result["error"] = "scene_path required for add_node"
|
|
1030
964
|
else:
|
|
1031
|
-
var
|
|
1032
|
-
if
|
|
1033
|
-
result["error"] =
|
|
965
|
+
var apply_result = _apply_add_node(scene_root, op)
|
|
966
|
+
if not apply_result.ok:
|
|
967
|
+
result["error"] = apply_result.error
|
|
1034
968
|
else:
|
|
1035
969
|
result["success"] = true
|
|
1036
970
|
"load_sprite":
|
|
1037
971
|
if scene_root == null:
|
|
1038
972
|
result["error"] = "scene_path required for load_sprite"
|
|
1039
973
|
else:
|
|
1040
|
-
var
|
|
1041
|
-
if
|
|
1042
|
-
result["error"] =
|
|
974
|
+
var apply_result = _apply_load_sprite(scene_root, op)
|
|
975
|
+
if not apply_result.ok:
|
|
976
|
+
result["error"] = apply_result.error
|
|
1043
977
|
else:
|
|
1044
978
|
result["success"] = true
|
|
1045
979
|
"save":
|
|
@@ -1067,51 +1001,3 @@ func batch_scene_operations(params: Dictionary) -> void:
|
|
|
1067
1001
|
save_scene_to_path(scene_cache[scene_path], scene_path)
|
|
1068
1002
|
|
|
1069
1003
|
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}))
|