godot-mcp-runtime 2.3.0 → 3.0.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.
Files changed (59) hide show
  1. package/README.md +30 -93
  2. package/dist/dispatch.d.ts +1 -11
  3. package/dist/dispatch.d.ts.map +1 -1
  4. package/dist/dispatch.js +7 -8
  5. package/dist/dispatch.js.map +1 -1
  6. package/dist/index.d.ts +1 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +12 -10
  9. package/dist/index.js.map +1 -1
  10. package/dist/scripts/godot_operations.gd +134 -283
  11. package/dist/scripts/mcp_bridge.gd +210 -43
  12. package/dist/tools/autoload-tools.d.ts +51 -0
  13. package/dist/tools/autoload-tools.d.ts.map +1 -0
  14. package/dist/tools/autoload-tools.js +187 -0
  15. package/dist/tools/autoload-tools.js.map +1 -0
  16. package/dist/tools/node-tools.d.ts +9 -78
  17. package/dist/tools/node-tools.d.ts.map +1 -1
  18. package/dist/tools/node-tools.js +105 -309
  19. package/dist/tools/node-tools.js.map +1 -1
  20. package/dist/tools/project-tools.d.ts +0 -168
  21. package/dist/tools/project-tools.d.ts.map +1 -1
  22. package/dist/tools/project-tools.js +106 -1192
  23. package/dist/tools/project-tools.js.map +1 -1
  24. package/dist/tools/runtime-tools.d.ts +108 -0
  25. package/dist/tools/runtime-tools.d.ts.map +1 -0
  26. package/dist/tools/runtime-tools.js +808 -0
  27. package/dist/tools/runtime-tools.js.map +1 -0
  28. package/dist/tools/scene-tools.d.ts +6 -48
  29. package/dist/tools/scene-tools.d.ts.map +1 -1
  30. package/dist/tools/scene-tools.js +55 -209
  31. package/dist/tools/scene-tools.js.map +1 -1
  32. package/dist/tools/validate-tools.d.ts.map +1 -1
  33. package/dist/tools/validate-tools.js +33 -28
  34. package/dist/tools/validate-tools.js.map +1 -1
  35. package/dist/utils/autoload-ini.d.ts +32 -0
  36. package/dist/utils/autoload-ini.d.ts.map +1 -0
  37. package/dist/utils/autoload-ini.js +111 -0
  38. package/dist/utils/autoload-ini.js.map +1 -0
  39. package/dist/utils/bridge-manager.d.ts +29 -0
  40. package/dist/utils/bridge-manager.d.ts.map +1 -0
  41. package/dist/utils/bridge-manager.js +136 -0
  42. package/dist/utils/bridge-manager.js.map +1 -0
  43. package/dist/utils/bridge-protocol.d.ts +34 -0
  44. package/dist/utils/bridge-protocol.d.ts.map +1 -0
  45. package/dist/utils/bridge-protocol.js +65 -0
  46. package/dist/utils/bridge-protocol.js.map +1 -0
  47. package/dist/utils/godot-runner.d.ts +57 -15
  48. package/dist/utils/godot-runner.d.ts.map +1 -1
  49. package/dist/utils/godot-runner.js +309 -277
  50. package/dist/utils/godot-runner.js.map +1 -1
  51. package/dist/utils/handler-helpers.d.ts +34 -0
  52. package/dist/utils/handler-helpers.d.ts.map +1 -0
  53. package/dist/utils/handler-helpers.js +55 -0
  54. package/dist/utils/handler-helpers.js.map +1 -0
  55. package/dist/utils/logger.d.ts +3 -0
  56. package/dist/utils/logger.d.ts.map +1 -0
  57. package/dist/utils/logger.js +11 -0
  58. package/dist/utils/logger.js.map +1 -0
  59. package/package.json +7 -4
@@ -61,19 +61,13 @@ func _init():
61
61
  export_mesh_library(params)
62
62
  "save_scene":
63
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)
64
+ # Node operations (always-array)
65
+ "delete_nodes":
66
+ delete_nodes(params)
67
+ "set_node_properties":
68
+ set_node_properties(params)
73
69
  "get_node_properties":
74
70
  get_node_properties(params)
75
- "list_nodes":
76
- list_nodes(params)
77
71
  "get_scene_tree":
78
72
  get_scene_tree(params)
79
73
  "attach_script":
@@ -93,10 +87,6 @@ func _init():
93
87
  validate_batch(params)
94
88
  "batch_scene_operations":
95
89
  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
90
  _:
101
91
  log_error("Unknown operation: " + operation)
102
92
  quit(1)
@@ -220,14 +210,22 @@ func load_scene_instance(scene_path: String):
220
210
 
221
211
  return instance
222
212
 
223
- # Helper to find a node by path
213
+ # Helper to find a node by path. Accepts "root", ".", "" (all → scene_root),
214
+ # the actual scene root's name (e.g. "Main"), or a path with either as the first
215
+ # segment (e.g. "root/Button" or "Main/Button"). Bare paths ("Button") resolve
216
+ # normally via get_node_or_null.
224
217
  func find_node_by_path(scene_root: Node, node_path: String) -> Node:
225
- if node_path == "root" or node_path.is_empty():
218
+ if node_path == "" or node_path == "." or node_path == "root":
219
+ return scene_root
220
+ if node_path == String(scene_root.name):
226
221
  return scene_root
227
222
 
228
223
  var path = node_path
229
- if path.begins_with("root/"):
230
- path = path.substr(5)
224
+ var first_slash = path.find("/")
225
+ if first_slash != -1:
226
+ var first_segment = path.substr(0, first_slash)
227
+ if first_segment == "root" or first_segment == String(scene_root.name):
228
+ path = path.substr(first_slash + 1)
231
229
 
232
230
  if path.is_empty():
233
231
  return scene_root
@@ -252,6 +250,20 @@ func save_scene_to_path(scene_root: Node, save_path: String) -> bool:
252
250
 
253
251
  return true
254
252
 
253
+ # Ensure the parent directory of a res:// path exists, creating it recursively
254
+ # if needed. Returns true on success or when the directory already exists.
255
+ func _ensure_res_dir(full_res_path: String) -> bool:
256
+ var dir_path = full_res_path.get_base_dir()
257
+ if dir_path == "res://" or dir_path.is_empty():
258
+ return true
259
+ var dir = DirAccess.open("res://")
260
+ if not dir:
261
+ return false
262
+ var relative_dir = dir_path.substr(6) if dir_path.begins_with("res://") else dir_path
263
+ if relative_dir.is_empty() or dir.dir_exists(relative_dir):
264
+ return true
265
+ return dir.make_dir_recursive(relative_dir) == OK
266
+
255
267
  # Create a new scene with a specified root node type
256
268
  func create_scene(params):
257
269
  printerr("Creating scene: " + params.scene_path)
@@ -272,17 +284,9 @@ func create_scene(params):
272
284
  scene_root.name = "root"
273
285
  scene_root.owner = scene_root
274
286
 
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)
287
+ if not _ensure_res_dir(full_scene_path):
288
+ log_error("Failed to create directory for scene: " + full_scene_path)
289
+ quit(1)
286
290
 
287
291
  if save_scene_to_path(scene_root, full_scene_path):
288
292
  print("Scene created successfully at: " + params.scene_path)
@@ -318,7 +322,7 @@ func add_node(params):
318
322
  var properties = params.properties
319
323
  for property in properties:
320
324
  log_debug("Setting property: " + property + " = " + str(properties[property]))
321
- new_node.set(property, properties[property])
325
+ new_node.set(property, _coerce_property_value(properties[property]))
322
326
 
323
327
  parent.add_child(new_node)
324
328
  new_node.owner = scene_root
@@ -351,6 +355,14 @@ func load_sprite(params):
351
355
  if not texture:
352
356
  log_error("Failed to load texture: " + full_texture_path)
353
357
  quit(1)
358
+ if not (texture is Texture2D):
359
+ log_error("Loaded resource is not a Texture2D: " + full_texture_path)
360
+ quit(1)
361
+ # A texture without a resource_path is a runtime-only object — PackedScene.pack()
362
+ # cannot serialize it, so the assignment would silently vanish on save.
363
+ if texture.resource_path == "":
364
+ log_error("Texture has no resource_path — likely not imported. Open project in Godot editor once, or run 'godot --headless --editor --quit' to import assets.")
365
+ quit(1)
354
366
 
355
367
  sprite_node.texture = texture
356
368
 
@@ -398,22 +410,16 @@ func export_mesh_library(params):
398
410
  mesh_library.set_item_shapes(item_id, [collision_child.shape])
399
411
  break
400
412
 
401
- if mesh_instance.mesh:
402
- mesh_library.set_item_preview(item_id, mesh_instance.mesh)
413
+ mesh_library.set_item_preview(item_id, mesh_instance.mesh)
403
414
 
404
415
  item_id += 1
405
416
 
406
417
  if item_id > 0:
407
418
  var full_output_path = normalize_scene_path(params.output_path)
408
419
 
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)
420
+ if not _ensure_res_dir(full_output_path):
421
+ log_error("Failed to create directory for MeshLibrary: " + full_output_path)
422
+ quit(1)
417
423
 
418
424
  var error = ResourceSaver.save(mesh_library, full_output_path)
419
425
  if error == OK:
@@ -441,216 +447,97 @@ func save_scene(params):
441
447
  log_error("Failed to save scene")
442
448
  quit(1)
443
449
 
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
450
  # ============================================
542
- # NEW NODE OPERATIONS
451
+ # NODE OPERATIONS
543
452
  # ============================================
544
453
 
545
- # Delete a node from a scene
546
- func delete_node(params):
547
- printerr("Deleting node from scene: " + params.scene_path)
454
+ # Delete one or more nodes from a scene (saves once)
455
+ func delete_nodes(params):
456
+ printerr("Deleting nodes from scene: " + params.scene_path)
548
457
 
549
458
  var scene_root = load_scene_instance(params.scene_path)
550
459
  if not scene_root:
551
460
  quit(1)
552
461
 
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)
462
+ var node_paths: Array = params.node_paths
463
+ var results: Array = []
464
+ var any_deleted := false
591
465
 
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)
466
+ for node_path in node_paths:
467
+ var entry = {"nodePath": node_path}
468
+ var node = find_node_by_path(scene_root, node_path)
469
+ if not node:
470
+ entry["error"] = "Node not found: " + node_path
471
+ elif node == scene_root:
472
+ entry["error"] = "Cannot delete the root node"
473
+ else:
474
+ var parent = node.get_parent()
475
+ parent.remove_child(node)
476
+ node.queue_free()
477
+ entry["success"] = true
478
+ any_deleted = true
479
+ results.append(entry)
480
+
481
+ if any_deleted:
482
+ if not save_scene_to_path(scene_root, params.scene_path):
483
+ print(JSON.stringify({"error": "Failed to save scene after deleting nodes", "results": results}))
484
+ return
597
485
 
598
- # Get all properties of a specific node
599
- func get_node_properties(params):
600
- printerr("Getting node properties from scene: " + params.scene_path)
486
+ print(JSON.stringify({"results": results}))
601
487
 
488
+ # Update one or more node properties in a single headless process (saves once)
489
+ func set_node_properties(params: Dictionary) -> void:
602
490
  var scene_root = load_scene_instance(params.scene_path)
603
491
  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)
492
+ print(JSON.stringify({"error": "Failed to load scene: " + params.scene_path, "results": []}))
493
+ return
610
494
 
611
- var changed_only = params.has("changed_only") and params.changed_only
612
- var properties = _collect_node_properties(node, changed_only)
495
+ var abort_on_error = params.get("abort_on_error", false)
496
+ var results: Array = []
497
+ var any_set := false
613
498
 
614
- var result = {
615
- "nodePath": params.node_path,
616
- "nodeType": node.get_class(),
617
- "properties": properties
618
- }
499
+ for update in params.updates:
500
+ var result = {"nodePath": update.node_path, "property": update.property}
501
+ var node = find_node_by_path(scene_root, update.node_path)
502
+ if node == null:
503
+ result["error"] = "Node not found: " + update.node_path
504
+ elif not (update.property in node):
505
+ result["error"] = "Property '%s' does not exist on node of type '%s'" % [update.property, node.get_class()]
506
+ else:
507
+ node.set(update.property, _coerce_property_value(update.value))
508
+ result["success"] = true
509
+ any_set = true
510
+ results.append(result)
511
+ if abort_on_error and result.has("error"):
512
+ break
619
513
 
620
- print(JSON.stringify(result))
514
+ if any_set:
515
+ if not save_scene_to_path(scene_root, params.scene_path):
516
+ print(JSON.stringify({"error": "Failed to save scene after updates", "results": results}))
517
+ return
621
518
 
622
- # List all child nodes under a parent
623
- func list_nodes(params):
624
- printerr("Listing nodes in scene: " + params.scene_path)
519
+ print(JSON.stringify({"results": results}))
625
520
 
521
+ # Get properties from one or more nodes in a single headless process (loads scene once)
522
+ func get_node_properties(params: Dictionary) -> void:
626
523
  var scene_root = load_scene_instance(params.scene_path)
627
524
  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)
525
+ print(JSON.stringify({"error": "Failed to load scene: " + params.scene_path, "results": []}))
526
+ return
638
527
 
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
- })
528
+ var results: Array = []
646
529
 
647
- var result = {
648
- "parentPath": parent_path,
649
- "parentType": parent.get_class(),
650
- "children": children
651
- }
530
+ for node_spec in params.nodes:
531
+ var node_path = node_spec.get("node_path", "")
532
+ var changed_only = node_spec.get("changed_only", false)
533
+ var node = find_node_by_path(scene_root, node_path)
534
+ if node == null:
535
+ results.append({"nodePath": node_path, "error": "Node not found"})
536
+ else:
537
+ var props = _collect_node_properties(node, changed_only)
538
+ results.append({"nodePath": node_path, "nodeType": node.get_class(), "properties": props})
652
539
 
653
- print(JSON.stringify(result))
540
+ print(JSON.stringify({"results": results}))
654
541
 
655
542
  # Get full hierarchical tree structure of a scene
656
543
  func get_scene_tree(params):
@@ -660,11 +547,18 @@ func get_scene_tree(params):
660
547
  if not scene_root:
661
548
  quit(1)
662
549
 
550
+ var tree_root = scene_root
551
+ if params.has("parent_path") and params.parent_path:
552
+ tree_root = find_node_by_path(scene_root, params.parent_path)
553
+ if not tree_root:
554
+ log_error("Parent node not found: " + str(params.parent_path))
555
+ quit(1)
556
+
663
557
  var max_depth = -1
664
558
  if params.has("max_depth"):
665
559
  max_depth = int(params.max_depth)
666
560
 
667
- var tree = build_tree_recursive(scene_root, "", 0, max_depth)
561
+ var tree = build_tree_recursive(tree_root, "", 0, max_depth)
668
562
  print(JSON.stringify(tree))
669
563
 
670
564
  func build_tree_recursive(node: Node, path: String, depth: int = 0, max_depth: int = -1) -> Dictionary:
@@ -676,10 +570,9 @@ func build_tree_recursive(node: Node, path: String, depth: int = 0, max_depth: i
676
570
  children.append(build_tree_recursive(child, node_path, depth + 1, max_depth))
677
571
 
678
572
  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
573
+ var script = node.get_script()
574
+ if script and script.resource_path:
575
+ script_path = script.resource_path
683
576
 
684
577
  return {
685
578
  "name": node.name,
@@ -822,7 +715,9 @@ func connect_signal(params):
822
715
  log_error("Method does not exist: " + params.method + " on " + target.get_class())
823
716
  quit(1)
824
717
 
825
- var err = source.connect(params.signal, Callable(target, params.method))
718
+ # CONNECT_PERSIST is required for the connection to be serialized into the
719
+ # packed scene; without it the connection is runtime-only and disappears on save.
720
+ var err = source.connect(params.signal, Callable(target, params.method), CONNECT_PERSIST)
826
721
  if err != OK:
827
722
  log_error("Failed to connect signal: " + str(err))
828
723
  quit(1)
@@ -976,7 +871,7 @@ func _batch_add_node(scene_root: Node, op: Dictionary) -> String:
976
871
  new_node.name = op.node_name
977
872
  if op.has("properties"):
978
873
  for property in op.properties:
979
- new_node.set(property, op.properties[property])
874
+ new_node.set(property, _coerce_property_value(op.properties[property]))
980
875
  parent.add_child(new_node)
981
876
  new_node.owner = scene_root
982
877
  return ""
@@ -995,6 +890,10 @@ func _batch_load_sprite(scene_root: Node, op: Dictionary) -> String:
995
890
  var texture = load(normalize_scene_path(op.texture_path))
996
891
  if not texture:
997
892
  return "Failed to load texture: " + op.texture_path
893
+ if not (texture is Texture2D):
894
+ return "Loaded resource is not a Texture2D: " + op.texture_path
895
+ if texture.resource_path == "":
896
+ return "Texture has no resource_path — likely not imported. Open project in Godot editor once, or run 'godot --headless --editor --quit' to import assets."
998
897
  sprite_node.texture = texture
999
898
  return ""
1000
899
 
@@ -1067,51 +966,3 @@ func batch_scene_operations(params: Dictionary) -> void:
1067
966
  save_scene_to_path(scene_cache[scene_path], scene_path)
1068
967
 
1069
968
  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}))