breakpoint-mcp 1.82.1 → 1.84.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 (119) hide show
  1. package/addon/breakpoint_mcp/operations.gd +298 -275
  2. package/addon/breakpoint_mcp/plugin.cfg +1 -1
  3. package/addon/breakpoint_mcp/runtime_bridge.gd +20 -3
  4. package/dist/addon-version.js.map +1 -0
  5. package/dist/annotations.js.map +1 -0
  6. package/dist/bridge.js +86 -2
  7. package/dist/bridge.js.map +1 -0
  8. package/dist/capabilities.js +101 -0
  9. package/dist/capabilities.js.map +1 -0
  10. package/dist/cli/args.js.map +1 -0
  11. package/dist/cli/clients.js.map +1 -0
  12. package/dist/cli/doctor.js +147 -6
  13. package/dist/cli/doctor.js.map +1 -0
  14. package/dist/cli/github.js.map +1 -0
  15. package/dist/cli/init.js +26 -4
  16. package/dist/cli/init.js.map +1 -0
  17. package/dist/cli/tools.js.map +1 -0
  18. package/dist/cli/usage.js +40 -1
  19. package/dist/cli/usage.js.map +1 -0
  20. package/dist/client-caps.js +75 -0
  21. package/dist/client-caps.js.map +1 -0
  22. package/dist/close-cause.js.map +1 -0
  23. package/dist/config.js.map +1 -0
  24. package/dist/confirm.js +13 -21
  25. package/dist/confirm.js.map +1 -0
  26. package/dist/connect-cause.js.map +1 -0
  27. package/dist/csdap.js.map +1 -0
  28. package/dist/cslsp.js.map +1 -0
  29. package/dist/dap.js.map +1 -0
  30. package/dist/exit-cause.js.map +1 -0
  31. package/dist/finiteness.js.map +1 -0
  32. package/dist/framing.js.map +1 -0
  33. package/dist/index.js +33 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/late-reply.js.map +1 -0
  36. package/dist/logger.js.map +1 -0
  37. package/dist/lsp.js.map +1 -0
  38. package/dist/mutation-guard.js +235 -0
  39. package/dist/mutation-guard.js.map +1 -0
  40. package/dist/path-cohort.js.map +1 -0
  41. package/dist/paths.js.map +1 -0
  42. package/dist/pause.js.map +1 -0
  43. package/dist/peers.js +20 -4
  44. package/dist/peers.js.map +1 -0
  45. package/dist/ports.js.map +1 -0
  46. package/dist/readiness.js.map +1 -0
  47. package/dist/recipes.js.map +1 -0
  48. package/dist/remedies.js.map +1 -0
  49. package/dist/result-cost.js.map +1 -0
  50. package/dist/schemas.js +155 -46
  51. package/dist/schemas.js.map +1 -0
  52. package/dist/secret.js.map +1 -0
  53. package/dist/spawn-guard.js +76 -0
  54. package/dist/spawn-guard.js.map +1 -0
  55. package/dist/stdio.js.map +1 -0
  56. package/dist/subscriptions.js.map +1 -0
  57. package/dist/tasks.js.map +1 -0
  58. package/dist/timeout-cause.js.map +1 -0
  59. package/dist/timeout-caveat.js.map +1 -0
  60. package/dist/tool-census.js +47 -0
  61. package/dist/tool-census.js.map +1 -0
  62. package/dist/tools/assetgen.js +13 -7
  63. package/dist/tools/assetgen.js.map +1 -0
  64. package/dist/tools/backend.js +4 -0
  65. package/dist/tools/backend.js.map +1 -0
  66. package/dist/tools/cli.js +73 -20
  67. package/dist/tools/cli.js.map +1 -0
  68. package/dist/tools/csdap.js.map +1 -0
  69. package/dist/tools/cslsp.js.map +1 -0
  70. package/dist/tools/dap.js +2 -3
  71. package/dist/tools/dap.js.map +1 -0
  72. package/dist/tools/editor/animation.js +3 -3
  73. package/dist/tools/editor/animation.js.map +1 -0
  74. package/dist/tools/editor/audio.js +5 -3
  75. package/dist/tools/editor/audio.js.map +1 -0
  76. package/dist/tools/editor/common.js.map +1 -0
  77. package/dist/tools/editor/core.js.map +1 -0
  78. package/dist/tools/editor/filesystem.js.map +1 -0
  79. package/dist/tools/editor/introspection.js.map +1 -0
  80. package/dist/tools/editor/node.js +4 -4
  81. package/dist/tools/editor/node.js.map +1 -0
  82. package/dist/tools/editor/particles.js +2 -1
  83. package/dist/tools/editor/particles.js.map +1 -0
  84. package/dist/tools/editor/physics.js +5 -4
  85. package/dist/tools/editor/physics.js.map +1 -0
  86. package/dist/tools/editor/project_input_test.js.map +1 -0
  87. package/dist/tools/editor/resource.js +11 -6
  88. package/dist/tools/editor/resource.js.map +1 -0
  89. package/dist/tools/editor/scene.js +7 -4
  90. package/dist/tools/editor/scene.js.map +1 -0
  91. package/dist/tools/editor/shader.js +4 -3
  92. package/dist/tools/editor/shader.js.map +1 -0
  93. package/dist/tools/editor/signal.js.map +1 -0
  94. package/dist/tools/editor/spatial.js +15 -8
  95. package/dist/tools/editor/spatial.js.map +1 -0
  96. package/dist/tools/editor/tiles.js +5 -3
  97. package/dist/tools/editor/tiles.js.map +1 -0
  98. package/dist/tools/editor/ui.js +6 -4
  99. package/dist/tools/editor/ui.js.map +1 -0
  100. package/dist/tools/editor.js.map +1 -0
  101. package/dist/tools/knowledge.js.map +1 -0
  102. package/dist/tools/lsp-common.js.map +1 -0
  103. package/dist/tools/lsp.js +1 -1
  104. package/dist/tools/lsp.js.map +1 -0
  105. package/dist/tools/netcode.js +6 -5
  106. package/dist/tools/netcode.js.map +1 -0
  107. package/dist/tools/processes.js +51 -12
  108. package/dist/tools/processes.js.map +1 -0
  109. package/dist/tools/resources.js.map +1 -0
  110. package/dist/tools/runtime.js +17 -5
  111. package/dist/tools/runtime.js.map +1 -0
  112. package/dist/tools/tabletop.js +165 -29
  113. package/dist/tools/tabletop.js.map +1 -0
  114. package/dist/tools/vcs.js +189 -27
  115. package/dist/tools/vcs.js.map +1 -0
  116. package/dist/toolsets.js.map +1 -0
  117. package/dist/version.js.map +1 -0
  118. package/dist/wire-defaults.js.map +1 -0
  119. package/package.json +2 -1
@@ -8,7 +8,7 @@ extends RefCounted
8
8
 
9
9
  const Codec := preload("res://addons/breakpoint_mcp/variant_json.gd")
10
10
  const Remedies := preload("res://addons/breakpoint_mcp/error_remedies.gd")
11
- const ADDON_VERSION := "1.12.0"
11
+ const ADDON_VERSION := "1.15.0"
12
12
 
13
13
  var _plugin: EditorPlugin
14
14
 
@@ -373,6 +373,101 @@ func _path_of(root: Node, node: Node) -> String:
373
373
  return String(root.get_path_to(node))
374
374
 
375
375
 
376
+ ## THE ONE PLACE A NODE JOINS THE EDITED SCENE.
377
+ ##
378
+ ## 🔴 `force_readable_name = true` IS LOAD-BEARING, NOT COSMETIC. Left at Godot's
379
+ ## default of false — which is what all twenty-two authoring tools did before 283 —
380
+ ## the engine answers ANY name collision with the machine form `@Type@N`, a name no
381
+ ## caller asked for and no human reads. Measured on 4.7: `node_add {name: "SFX"}`
382
+ ## twice returned "SFX" and then "@AudioStreamPlayer3D@19825", with no error and no
383
+ ## flag, so the caller's next call addressing "SFX" got `bad_path`. `node_duplicate`
384
+ ## had NO non-colliding case at all — a duplicate always collides with its source,
385
+ ## so "@Label@19826" was that tool's ONLY behaviour, where the editor's own Duplicate
386
+ ## gives "Label2". With `true` the engine takes the readable path the editor's Add
387
+ ## Node and Duplicate take, and the host's shipped promise that a name "defaults to
388
+ ## the class name" is true for the first time.
389
+ ##
390
+ ## 🔵 AND THE RENAME IS REPORTED, BECAUSE "SFX2" IS STILL NOT WHAT WAS ASKED FOR.
391
+ ## The returned Dictionary carries `coerced: true` and `requested` whenever the
392
+ ## engine's name differs — the shape `node_set_property` has answered with since
393
+ ## 1.82.0, so a caller parses one convention rather than two. `requested` is read
394
+ ## off the node itself rather than passed in, so a site cannot report a name it did
395
+ ## not actually ask the engine for.
396
+ ##
397
+ ## 🔴 A BARE `add_child` IN AN AUTHORING HANDLER IS THE DEFECT AGAIN, so this is the
398
+ ## only registration site in this file and `contract_check.py` check 31 refuses a
399
+ ## second one. A twenty-third authoring tool is covered by USING the seam, not by
400
+ ## somebody remembering the flag.
401
+ func _commit_add(node: Node, parent: Node, root: Node, action: String, also_own: Array = []) -> Dictionary:
402
+ var requested := String(node.name)
403
+ var ur := _plugin.get_undo_redo()
404
+ ur.create_action(action)
405
+ ur.add_do_method(parent, "add_child", node, true)
406
+ ur.add_do_method(node, "set_owner", root)
407
+ for d in also_own:
408
+ ur.add_do_method(d, "set_owner", root)
409
+ ur.add_do_reference(node)
410
+ ur.add_undo_method(parent, "remove_child", node)
411
+ ur.commit_action()
412
+ var actual := String(node.name)
413
+ var out := {"path": _path_of(root, node), "name": actual}
414
+ if actual != requested:
415
+ out["coerced"] = true
416
+ out["requested"] = requested
417
+ return out
418
+
419
+
420
+ ## 🆕 284 — THE ONE PLACE THIS FILE WRITES A RESOURCE TO DISK.
421
+ ##
422
+ ## 🔴 MEASURED FROM A USER'S SIDE AGAINST A LIVE GODOT 4.7, AND IT IS 283 §1 ONE AXIS
423
+ ## OVER. 283 found that every tool taking a `name` handed it to `add_child` with
424
+ ## `force_readable_name` defaulted false, so a collision came back as `@Type@N` with no
425
+ ## flag. The collision here is a FILE and the answer was DELETION. Nine resources were
426
+ ## created, a sentinel line appended to each on disk, and each tool called a SECOND time
427
+ ## with identical arguments: every sentinel was gone, and every reply read `created` /
428
+ ## `saved` / `packed` — the same words, in the same shape, as the first call.
429
+ ##
430
+ ## `resource_create` overwrote an Environment a property had been set on, resetting it;
431
+ ## asked for a StandardMaterial3D at the same path it silently turned that resource into
432
+ ## a material, so anything referencing it as an Environment now holds the wrong type.
433
+ ## `scene_new` replaced a saved scene on disk while the editor's open copy still held the
434
+ ## node that had been added, so file and editor disagree and nothing says so.
435
+ ##
436
+ ## 🔴 AND THE PRODUCT GAVE FOUR ANSWERS TO ONE QUESTION. `filesystem_move` refused
437
+ ## `exists` with no way to force; the seven scaffold tools under `_mp_write_script`
438
+ ## refused `exists` unless `overwrite`; `filesystem_create_dir` succeeded and reported
439
+ ## `existed`; and the rest destroyed silently. `_mp_write_script` was already right, and
440
+ ## it was right because it is a SEAM — one function, one answer, seven tools. This is
441
+ ## that function for the twenty-four `ResourceSaver.save` sites, which were a roster:
442
+ ## 282 §2.3's rule, paid on a second uncounted population one session after the first.
443
+ ##
444
+ ## 🔵 THE TWO KINDS ARE A PROPERTY OF THE CALL SITE, NOT OF THE CALLER'S ARGUMENTS, and
445
+ ## that distinction is the whole design. `SAVE_NEW` means `to_path` is a DESTINATION the
446
+ ## caller named — a collision there is somebody else's file. `SAVE_BACK` means the path
447
+ ## is the resource's own home, loaded a few lines above and written back: overwriting is
448
+ ## not a hazard there, it IS the operation, and refusing it would break every
449
+ ## `*_set_property` tool on the surface. A flag the CALLER could set would let a caller
450
+ ## turn `theme_set_color` into a refusal; a constant at the SITE cannot.
451
+ ##
452
+ ## 🔵 `replaced` IS REPORTED ONLY WHEN IT HAPPENED — the shape `node_set_property` has
453
+ ## carried since 1.82.0 and `_commit_add` since 283: a field that appears when the engine
454
+ ## did something other than the plain thing, and is absent otherwise. *I created a file*
455
+ ## and *I overwrote your file because you asked me to* are not the same sentence.
456
+ const SAVE_NEW := 0
457
+ const SAVE_BACK := 1
458
+
459
+
460
+ func _commit_save(res: Resource, to_path: String, params: Dictionary, kind: int,
461
+ flags: int = 0) -> Dictionary:
462
+ var existed := FileAccess.file_exists(to_path)
463
+ if kind == SAVE_NEW and existed and not bool(params.get("overwrite", false)):
464
+ return {"error": _err("exists", "File already exists (pass overwrite): %s" % to_path)}
465
+ var e := ResourceSaver.save(res, to_path, flags)
466
+ if e != OK:
467
+ return {"error": _err("save_failed", "ResourceSaver.save() returned %d" % e)}
468
+ return {"fields": {"replaced": true} if (existed and kind == SAVE_NEW) else {}}
469
+
470
+
376
471
  # ------------------------------------------------------------- handlers ------
377
472
 
378
473
  func _ping() -> Dictionary:
@@ -542,11 +637,11 @@ func _scene_new(params: Dictionary) -> Dictionary:
542
637
  var e := packed.pack(root)
543
638
  if e != OK:
544
639
  return _err("pack_failed", "PackedScene.pack() returned %d" % e)
545
- e = ResourceSaver.save(packed, path)
546
- if e != OK:
547
- return _err("save_failed", "ResourceSaver.save() returned %d" % e)
640
+ var saved := _commit_save(packed, path, params, SAVE_NEW)
641
+ if saved.has("error"):
642
+ return saved["error"]
548
643
  EditorInterface.open_scene_from_path(path)
549
- return _ok({"created": path, "root_type": root_type})
644
+ return _ok({"created": path, "root_type": root_type}.merged(saved["fields"]))
550
645
 
551
646
 
552
647
  func _node_add(params: Dictionary) -> Dictionary:
@@ -561,14 +656,9 @@ func _node_add(params: Dictionary) -> Dictionary:
561
656
  return _err("bad_type", "Cannot instantiate class: %s" % type)
562
657
  var node: Node = ClassDB.instantiate(type)
563
658
  node.name = String(params.get("name", type))
564
- var ur := _plugin.get_undo_redo()
565
- ur.create_action("Breakpoint: add %s" % node.name)
566
- ur.add_do_method(parent, "add_child", node)
567
- ur.add_do_method(node, "set_owner", root)
568
- ur.add_do_reference(node)
569
- ur.add_undo_method(parent, "remove_child", node)
570
- ur.commit_action()
571
- return _ok({"path": _path_of(root, node), "name": String(node.name), "type": type})
659
+ var added := _commit_add(node, parent, root, "Breakpoint: add %s" % node.name)
660
+ added["type"] = type
661
+ return _ok(added)
572
662
 
573
663
 
574
664
  func _node_delete(params: Dictionary) -> Dictionary:
@@ -1101,16 +1191,9 @@ func _node_duplicate(params: Dictionary) -> Dictionary:
1101
1191
  var dup: Node = node.duplicate()
1102
1192
  if params.has("name"):
1103
1193
  dup.name = String(params.get("name"))
1104
- var ur := _plugin.get_undo_redo()
1105
- ur.create_action("Breakpoint: duplicate %s" % node.name)
1106
- ur.add_do_method(parent, "add_child", dup)
1107
- ur.add_do_method(dup, "set_owner", root)
1108
- for d in _descendants(dup):
1109
- ur.add_do_method(d, "set_owner", root)
1110
- ur.add_do_reference(dup)
1111
- ur.add_undo_method(parent, "remove_child", dup)
1112
- ur.commit_action()
1113
- return _ok({"path": _path_of(root, dup), "name": String(dup.name), "type": dup.get_class()})
1194
+ var added := _commit_add(dup, parent, root, "Breakpoint: duplicate %s" % node.name, _descendants(dup))
1195
+ added["type"] = dup.get_class()
1196
+ return _ok(added)
1114
1197
 
1115
1198
 
1116
1199
  func _node_get_children(params: Dictionary) -> Dictionary:
@@ -1221,14 +1304,10 @@ func _node_instantiate_scene(params: Dictionary) -> Dictionary:
1221
1304
  return _err("instantiate_failed", "Could not instantiate %s" % scene_path)
1222
1305
  if params.has("name"):
1223
1306
  inst.name = String(params.get("name"))
1224
- var ur := _plugin.get_undo_redo()
1225
- ur.create_action("Breakpoint: instance scene %s" % scene_path)
1226
- ur.add_do_method(parent, "add_child", inst)
1227
- ur.add_do_method(inst, "set_owner", root)
1228
- ur.add_do_reference(inst)
1229
- ur.add_undo_method(parent, "remove_child", inst)
1230
- ur.commit_action()
1231
- return _ok({"path": _path_of(root, inst), "name": String(inst.name), "type": inst.get_class(), "scene": scene_path})
1307
+ var added := _commit_add(inst, parent, root, "Breakpoint: instance scene %s" % scene_path)
1308
+ added["type"] = inst.get_class()
1309
+ added["scene"] = scene_path
1310
+ return _ok(added)
1232
1311
 
1233
1312
 
1234
1313
  func _node_move_child(params: Dictionary) -> Dictionary:
@@ -1569,11 +1648,11 @@ func _scene_pack(params: Dictionary) -> Dictionary:
1569
1648
  if e != OK:
1570
1649
  dup.free()
1571
1650
  return _err("pack_failed", "PackedScene.pack() returned %d" % e)
1572
- e = ResourceSaver.save(packed, to_path)
1651
+ var saved := _commit_save(packed, to_path, params, SAVE_NEW)
1573
1652
  dup.free()
1574
- if e != OK:
1575
- return _err("save_failed", "ResourceSaver.save() returned %d" % e)
1576
- return _ok({"packed": to_path, "branch": _path_of(root, branch)})
1653
+ if saved.has("error"):
1654
+ return saved["error"]
1655
+ return _ok({"packed": to_path, "branch": _path_of(root, branch)}.merged(saved["fields"]))
1577
1656
 
1578
1657
 
1579
1658
  func _scene_save_as(params: Dictionary) -> Dictionary:
@@ -1779,10 +1858,10 @@ func _resource_create(params: Dictionary) -> Dictionary:
1779
1858
  return _err("create_failed", "Could not instantiate %s" % cls)
1780
1859
  for key in params.get("properties", {}):
1781
1860
  res.set(String(key), Codec.decode(params["properties"][key]))
1782
- var e := ResourceSaver.save(res, to_path)
1783
- if e != OK:
1784
- return _err("save_failed", "ResourceSaver.save() returned %d" % e)
1785
- return _ok({"created": to_path, "type": cls})
1861
+ var saved := _commit_save(res, to_path, params, SAVE_NEW)
1862
+ if saved.has("error"):
1863
+ return saved["error"]
1864
+ return _ok({"created": to_path, "type": cls}.merged(saved["fields"]))
1786
1865
 
1787
1866
 
1788
1867
  func _resource_load(params: Dictionary) -> Dictionary:
@@ -1813,10 +1892,15 @@ func _resource_save(params: Dictionary) -> Dictionary:
1813
1892
  if res == null:
1814
1893
  return _err("load_failed", "Could not load resource: %s" % from_path)
1815
1894
  var flags := int(params.get("flags", 0))
1816
- var e := ResourceSaver.save(res, to_path, flags)
1817
- if e != OK:
1818
- return _err("save_failed", "ResourceSaver.save() returned %d" % e)
1819
- return _ok({"saved": to_path, "from": from_path})
1895
+ # 🔵 THE KIND IS DERIVED HERE AND NOWHERE ELSE: `to_path` DEFAULTS to `from_path`,
1896
+ # so this one tool is a save-back when the caller named no destination and a
1897
+ # create when they did. Reading it off the arguments at the site is the honest
1898
+ # version; a constant would be wrong half the time.
1899
+ var kind := SAVE_BACK if to_path == from_path else SAVE_NEW
1900
+ var saved := _commit_save(res, to_path, params, kind, flags)
1901
+ if saved.has("error"):
1902
+ return saved["error"]
1903
+ return _ok({"saved": to_path, "from": from_path}.merged(saved["fields"]))
1820
1904
 
1821
1905
 
1822
1906
  func _resource_duplicate(params: Dictionary) -> Dictionary:
@@ -1833,10 +1917,10 @@ func _resource_duplicate(params: Dictionary) -> Dictionary:
1833
1917
  var dup := res.duplicate(deep)
1834
1918
  if dup == null:
1835
1919
  return _err("duplicate_failed", "Could not duplicate resource")
1836
- var e := ResourceSaver.save(dup, to_path)
1837
- if e != OK:
1838
- return _err("save_failed", "ResourceSaver.save() returned %d" % e)
1839
- return _ok({"duplicated": to_path, "from": from_path, "deep": deep})
1920
+ var saved := _commit_save(dup, to_path, params, SAVE_NEW)
1921
+ if saved.has("error"):
1922
+ return saved["error"]
1923
+ return _ok({"duplicated": to_path, "from": from_path, "deep": deep}.merged(saved["fields"]))
1840
1924
 
1841
1925
 
1842
1926
  func _resource_get_property(params: Dictionary) -> Dictionary:
@@ -1863,9 +1947,9 @@ func _resource_set_property(params: Dictionary) -> Dictionary:
1863
1947
  if prop == "":
1864
1948
  return _err("bad_params", "Missing 'property'")
1865
1949
  res.set(prop, Codec.decode(params.get("value")))
1866
- var e := ResourceSaver.save(res, path)
1867
- if e != OK:
1868
- return _err("save_failed", "ResourceSaver.save() returned %d" % e)
1950
+ var saved := _commit_save(res, path, params, SAVE_BACK)
1951
+ if saved.has("error"):
1952
+ return saved["error"]
1869
1953
  return _ok({"path": path, "property": prop, "value": Codec.encode(res.get(prop))})
1870
1954
 
1871
1955
 
@@ -2102,10 +2186,11 @@ func _asset_gen_placeholder(params: Dictionary) -> Dictionary:
2102
2186
  # pipeline) — the placeholder must be usable synchronously. External
2103
2187
  # formats (a real backend's .png) go through asset.import instead.
2104
2188
  var tex := ImageTexture.create_from_image(img)
2105
- var e := ResourceSaver.save(tex, to_path)
2106
- if e != OK:
2107
- return _err("save_failed", "ResourceSaver.save() returned %d" % e)
2189
+ var saved_e := _commit_save(tex, to_path, params, SAVE_NEW)
2190
+ if saved_e.has("error"):
2191
+ return saved_e["error"]
2108
2192
  var desc := _import_and_describe(to_path)
2193
+ desc.merge(saved_e["fields"])
2109
2194
  desc["kind"] = kind
2110
2195
  desc["width"] = w
2111
2196
  desc["height"] = h
@@ -2114,10 +2199,11 @@ func _asset_gen_placeholder(params: Dictionary) -> Dictionary:
2114
2199
  "audio_sfx":
2115
2200
  var dur := clampi(int(params.get("duration_ms", 300)), 20, 5000)
2116
2201
  var stream := _gen_audio(seed_hash, dur)
2117
- var e2 := ResourceSaver.save(stream, to_path)
2118
- if e2 != OK:
2119
- return _err("save_failed", "ResourceSaver.save() returned %d" % e2)
2202
+ var saved_e2 := _commit_save(stream, to_path, params, SAVE_NEW)
2203
+ if saved_e2.has("error"):
2204
+ return saved_e2["error"]
2120
2205
  var desc2 := _import_and_describe(to_path)
2206
+ desc2.merge(saved_e2["fields"])
2121
2207
  desc2["kind"] = kind
2122
2208
  desc2["duration_ms"] = dur
2123
2209
  desc2["format"] = "AudioStreamWAV"
@@ -2125,10 +2211,11 @@ func _asset_gen_placeholder(params: Dictionary) -> Dictionary:
2125
2211
  "model":
2126
2212
  var shape := String(params.get("shape", "box"))
2127
2213
  var mesh := _gen_mesh(shape, seed_hash)
2128
- var e3 := ResourceSaver.save(mesh, to_path)
2129
- if e3 != OK:
2130
- return _err("save_failed", "ResourceSaver.save() returned %d" % e3)
2214
+ var saved_e3 := _commit_save(mesh, to_path, params, SAVE_NEW)
2215
+ if saved_e3.has("error"):
2216
+ return saved_e3["error"]
2131
2217
  var desc3 := _import_and_describe(to_path)
2218
+ desc3.merge(saved_e3["fields"])
2132
2219
  desc3["kind"] = kind
2133
2220
  desc3["shape"] = shape
2134
2221
  desc3["format"] = mesh.get_class()
@@ -2282,14 +2369,9 @@ func _anim_player_create(params: Dictionary) -> Dictionary:
2282
2369
  var node := AnimationPlayer.new()
2283
2370
  node.name = String(params.get("name", "AnimationPlayer"))
2284
2371
  node.add_animation_library("", AnimationLibrary.new())
2285
- var ur := _plugin.get_undo_redo()
2286
- ur.create_action("Breakpoint: add AnimationPlayer %s" % node.name)
2287
- ur.add_do_method(parent, "add_child", node)
2288
- ur.add_do_method(node, "set_owner", root)
2289
- ur.add_do_reference(node)
2290
- ur.add_undo_method(parent, "remove_child", node)
2291
- ur.commit_action()
2292
- return _ok({"path": _path_of(root, node), "name": String(node.name), "type": "AnimationPlayer"})
2372
+ var added := _commit_add(node, parent, root, "Breakpoint: add AnimationPlayer %s" % node.name)
2373
+ added["type"] = "AnimationPlayer"
2374
+ return _ok(added)
2293
2375
 
2294
2376
 
2295
2377
  func _anim_create(params: Dictionary) -> Dictionary:
@@ -2607,14 +2689,12 @@ func _anim_tree_create(params: Dictionary) -> Dictionary:
2607
2689
  if anim_player_path != "":
2608
2690
  node.set("anim_player", NodePath(anim_player_path))
2609
2691
  node.set("active", bool(params.get("active", false)))
2610
- var ur := _plugin.get_undo_redo()
2611
- ur.create_action("Breakpoint: add AnimationTree %s" % node.name)
2612
- ur.add_do_method(parent, "add_child", node)
2613
- ur.add_do_method(node, "set_owner", root)
2614
- ur.add_do_reference(node)
2615
- ur.add_undo_method(parent, "remove_child", node)
2616
- ur.commit_action()
2617
- return _ok({"path": _path_of(root, node), "name": String(node.name), "type": "AnimationTree", "root_type": rt, "anim_player": anim_player_path, "active": bool(node.get("active"))})
2692
+ var added := _commit_add(node, parent, root, "Breakpoint: add AnimationTree %s" % node.name)
2693
+ added["type"] = "AnimationTree"
2694
+ added["root_type"] = rt
2695
+ added["anim_player"] = anim_player_path
2696
+ added["active"] = bool(node.get("active"))
2697
+ return _ok(added)
2618
2698
 
2619
2699
 
2620
2700
  func _anim_tree_add_node(params: Dictionary) -> Dictionary:
@@ -2760,11 +2840,11 @@ func _tileset_create(params: Dictionary) -> Dictionary:
2760
2840
  var ts := TileSet.new()
2761
2841
  if params.has("tile_size"):
2762
2842
  ts.set("tile_size", _to_vec2i(params.get("tile_size")))
2763
- var e := ResourceSaver.save(ts, to_path)
2764
- if e != OK:
2765
- return _err("save_failed", "ResourceSaver.save() returned %d" % e)
2843
+ var saved := _commit_save(ts, to_path, params, SAVE_NEW)
2844
+ if saved.has("error"):
2845
+ return saved["error"]
2766
2846
  var tsz: Vector2i = ts.get("tile_size")
2767
- return _ok({"created": to_path, "tile_size": [tsz.x, tsz.y]})
2847
+ return _ok({"created": to_path, "tile_size": [tsz.x, tsz.y]}.merged(saved["fields"]))
2768
2848
 
2769
2849
 
2770
2850
  func _tileset_add_source(params: Dictionary) -> Dictionary:
@@ -2789,9 +2869,9 @@ func _tileset_add_source(params: Dictionary) -> Dictionary:
2789
2869
  if params.has("separation"):
2790
2870
  atlas.set("separation", _to_vec2i(params.get("separation")))
2791
2871
  var assigned: int = ts.add_source(atlas, int(params.get("source_id", -1)))
2792
- var e := ResourceSaver.save(ts, tileset_path)
2793
- if e != OK:
2794
- return _err("save_failed", "ResourceSaver.save() returned %d" % e)
2872
+ var saved := _commit_save(ts, tileset_path, params, SAVE_BACK)
2873
+ if saved.has("error"):
2874
+ return saved["error"]
2795
2875
  return _ok({"tileset": tileset_path, "source_id": assigned, "texture": texture_path, "texture_region_size": [region.x, region.y], "source_count": ts.get_source_count()})
2796
2876
 
2797
2877
 
@@ -2813,9 +2893,9 @@ func _tileset_add_tile(params: Dictionary) -> Dictionary:
2813
2893
  if params.has("size"):
2814
2894
  size = _to_vec2i(params.get("size"))
2815
2895
  src.create_tile(coords, size)
2816
- var e := ResourceSaver.save(ts, tileset_path)
2817
- if e != OK:
2818
- return _err("save_failed", "ResourceSaver.save() returned %d" % e)
2896
+ var saved := _commit_save(ts, tileset_path, params, SAVE_BACK)
2897
+ if saved.has("error"):
2898
+ return saved["error"]
2819
2899
  return _ok({"tileset": tileset_path, "source_id": sid, "atlas_coords": [coords.x, coords.y], "size": [size.x, size.y], "tiles_count": src.get_tiles_count()})
2820
2900
 
2821
2901
 
@@ -2850,9 +2930,9 @@ func _tileset_set_tile_collision(params: Dictionary) -> Dictionary:
2850
2930
  var one_way := bool(params.get("one_way", false))
2851
2931
  if params.has("one_way"):
2852
2932
  td.set_collision_polygon_one_way(layer, poly_index, one_way)
2853
- var e := ResourceSaver.save(ts, tileset_path)
2854
- if e != OK:
2855
- return _err("save_failed", "ResourceSaver.save() returned %d" % e)
2933
+ var saved := _commit_save(ts, tileset_path, params, SAVE_BACK)
2934
+ if saved.has("error"):
2935
+ return saved["error"]
2856
2936
  return _ok({"tileset": tileset_path, "source_id": sid, "atlas_coords": [coords.x, coords.y], "physics_layer": layer, "polygon_index": poly_index, "points": poly.size(), "one_way": one_way})
2857
2937
 
2858
2938
 
@@ -2886,14 +2966,10 @@ func _tilemaplayer_create(params: Dictionary) -> Dictionary:
2886
2966
  if ts == null:
2887
2967
  return _err("not_found", "TileSet not found: %s" % tileset_path)
2888
2968
  layer.tile_set = ts
2889
- var ur := _plugin.get_undo_redo()
2890
- ur.create_action("Breakpoint: add TileMapLayer %s" % layer.name)
2891
- ur.add_do_method(parent, "add_child", layer)
2892
- ur.add_do_method(layer, "set_owner", root)
2893
- ur.add_do_reference(layer)
2894
- ur.add_undo_method(parent, "remove_child", layer)
2895
- ur.commit_action()
2896
- return _ok({"path": _path_of(root, layer), "name": String(layer.name), "type": "TileMapLayer", "tile_set": tileset_path})
2969
+ var added := _commit_add(layer, parent, root, "Breakpoint: add TileMapLayer %s" % layer.name)
2970
+ added["type"] = "TileMapLayer"
2971
+ added["tile_set"] = tileset_path
2972
+ return _ok(added)
2897
2973
 
2898
2974
 
2899
2975
  func _tilemap_set_cell(params: Dictionary) -> Dictionary:
@@ -3041,14 +3117,11 @@ func _body_create(params: Dictionary) -> Dictionary:
3041
3117
  return _err("bad_type", "Cannot instantiate class: %s" % cls)
3042
3118
  var node: Node = ClassDB.instantiate(cls)
3043
3119
  node.name = String(params.get("name", cls))
3044
- var ur := _plugin.get_undo_redo()
3045
- ur.create_action("Breakpoint: add %s" % node.name)
3046
- ur.add_do_method(parent, "add_child", node)
3047
- ur.add_do_method(node, "set_owner", root)
3048
- ur.add_do_reference(node)
3049
- ur.add_undo_method(parent, "remove_child", node)
3050
- ur.commit_action()
3051
- return _ok({"path": _path_of(root, node), "name": String(node.name), "type": cls, "body": kind, "dim": ("3d" if dim3 else "2d")})
3120
+ var added := _commit_add(node, parent, root, "Breakpoint: add %s" % node.name)
3121
+ added["type"] = cls
3122
+ added["body"] = kind
3123
+ added["dim"] = ("3d" if dim3 else "2d")
3124
+ return _ok(added)
3052
3125
 
3053
3126
 
3054
3127
  func _make_collision_shape(kind: String, dim3: bool, params: Dictionary):
@@ -3118,14 +3191,12 @@ func _collisionshape_add(params: Dictionary) -> Dictionary:
3118
3191
  cs2.set("shape", shape_res)
3119
3192
  node = cs2
3120
3193
  node.name = String(params.get("name", node.get_class()))
3121
- var ur := _plugin.get_undo_redo()
3122
- ur.create_action("Breakpoint: add %s" % node.name)
3123
- ur.add_do_method(parent, "add_child", node)
3124
- ur.add_do_method(node, "set_owner", root)
3125
- ur.add_do_reference(node)
3126
- ur.add_undo_method(parent, "remove_child", node)
3127
- ur.commit_action()
3128
- return _ok({"path": _path_of(root, node), "name": String(node.name), "type": node.get_class(), "shape": kind, "shape_class": shape_res.get_class(), "dim": ("3d" if dim3 else "2d")})
3194
+ var added := _commit_add(node, parent, root, "Breakpoint: add %s" % node.name)
3195
+ added["type"] = node.get_class()
3196
+ added["shape"] = kind
3197
+ added["shape_class"] = shape_res.get_class()
3198
+ added["dim"] = ("3d" if dim3 else "2d")
3199
+ return _ok(added)
3129
3200
 
3130
3201
 
3131
3202
  func _body_set_collision_layer(params: Dictionary) -> Dictionary:
@@ -3282,14 +3353,13 @@ func _joint_create(params: Dictionary) -> Dictionary:
3282
3353
  node.set("node_a", NodePath(String(params.get("node_a"))))
3283
3354
  if params.has("node_b"):
3284
3355
  node.set("node_b", NodePath(String(params.get("node_b"))))
3285
- var ur := _plugin.get_undo_redo()
3286
- ur.create_action("Breakpoint: add %s" % node.name)
3287
- ur.add_do_method(parent, "add_child", node)
3288
- ur.add_do_method(node, "set_owner", root)
3289
- ur.add_do_reference(node)
3290
- ur.add_undo_method(parent, "remove_child", node)
3291
- ur.commit_action()
3292
- return _ok({"path": _path_of(root, node), "name": String(node.name), "type": cls, "joint": kind, "dim": ("3d" if dim3 else "2d"), "node_a": String(node.get("node_a")), "node_b": String(node.get("node_b"))})
3356
+ var added := _commit_add(node, parent, root, "Breakpoint: add %s" % node.name)
3357
+ added["type"] = cls
3358
+ added["joint"] = kind
3359
+ added["dim"] = ("3d" if dim3 else "2d")
3360
+ added["node_a"] = String(node.get("node_a"))
3361
+ added["node_b"] = String(node.get("node_b"))
3362
+ return _ok(added)
3293
3363
 
3294
3364
 
3295
3365
  func _joint_set_bodies(params: Dictionary) -> Dictionary:
@@ -3341,14 +3411,11 @@ func _collisionpolygon_add(params: Dictionary) -> Dictionary:
3341
3411
  c2.set("build_mode", bm)
3342
3412
  node = c2
3343
3413
  node.name = String(params.get("name", node.get_class()))
3344
- var ur := _plugin.get_undo_redo()
3345
- ur.create_action("Breakpoint: add %s" % node.name)
3346
- ur.add_do_method(parent, "add_child", node)
3347
- ur.add_do_method(node, "set_owner", root)
3348
- ur.add_do_reference(node)
3349
- ur.add_undo_method(parent, "remove_child", node)
3350
- ur.commit_action()
3351
- return _ok({"path": _path_of(root, node), "name": String(node.name), "type": node.get_class(), "dim": ("3d" if dim3 else "2d"), "points": pts.size()})
3414
+ var added := _commit_add(node, parent, root, "Breakpoint: add %s" % node.name)
3415
+ added["type"] = node.get_class()
3416
+ added["dim"] = ("3d" if dim3 else "2d")
3417
+ added["points"] = pts.size()
3418
+ return _ok(added)
3352
3419
 
3353
3420
 
3354
3421
  func _rigidbody_set_properties(params: Dictionary) -> Dictionary:
@@ -3461,14 +3528,13 @@ func _particles_create(params: Dictionary) -> Dictionary:
3461
3528
  node.set("lifetime", float(params.get("lifetime")))
3462
3529
  if params.has("emitting"):
3463
3530
  node.set("emitting", bool(params.get("emitting")))
3464
- var ur := _plugin.get_undo_redo()
3465
- ur.create_action("Breakpoint: add %s" % node.name)
3466
- ur.add_do_method(parent, "add_child", node)
3467
- ur.add_do_method(node, "set_owner", root)
3468
- ur.add_do_reference(node)
3469
- ur.add_undo_method(parent, "remove_child", node)
3470
- ur.commit_action()
3471
- return _ok({"path": _path_of(root, node), "name": String(node.name), "type": node.get_class(), "dim": ("3d" if dim3 else "2d"), "amount": int(node.get("amount")), "lifetime": float(node.get("lifetime")), "emitting": bool(node.get("emitting"))})
3531
+ var added := _commit_add(node, parent, root, "Breakpoint: add %s" % node.name)
3532
+ added["type"] = node.get_class()
3533
+ added["dim"] = ("3d" if dim3 else "2d")
3534
+ added["amount"] = int(node.get("amount"))
3535
+ added["lifetime"] = float(node.get("lifetime"))
3536
+ added["emitting"] = bool(node.get("emitting"))
3537
+ return _ok(added)
3472
3538
 
3473
3539
 
3474
3540
  func _particles_set_process_material(params: Dictionary) -> Dictionary:
@@ -3621,10 +3687,10 @@ func _shader_create(params: Dictionary) -> Dictionary:
3621
3687
  var sh := Shader.new()
3622
3688
  if params.has("code"):
3623
3689
  sh.code = String(params.get("code"))
3624
- var e := ResourceSaver.save(sh, to_path)
3625
- if e != OK:
3626
- return _err("save_failed", "ResourceSaver.save() returned %d" % e)
3627
- return _ok({"created": to_path, "type": "Shader", "code_length": sh.code.length()})
3690
+ var saved := _commit_save(sh, to_path, params, SAVE_NEW)
3691
+ if saved.has("error"):
3692
+ return saved["error"]
3693
+ return _ok({"created": to_path, "type": "Shader", "code_length": sh.code.length()}.merged(saved["fields"]))
3628
3694
 
3629
3695
 
3630
3696
  func _shader_set_code(params: Dictionary) -> Dictionary:
@@ -3638,9 +3704,9 @@ func _shader_set_code(params: Dictionary) -> Dictionary:
3638
3704
  return _err("bad_type", "%s is not a Shader" % path)
3639
3705
  var sh := res as Shader
3640
3706
  sh.code = String(params.get("code"))
3641
- var e := ResourceSaver.save(sh, path)
3642
- if e != OK:
3643
- return _err("save_failed", "ResourceSaver.save() returned %d" % e)
3707
+ var saved := _commit_save(sh, path, params, SAVE_BACK)
3708
+ if saved.has("error"):
3709
+ return saved["error"]
3644
3710
  return _ok({"path": path, "code_length": sh.code.length()})
3645
3711
 
3646
3712
 
@@ -3769,14 +3835,14 @@ func _audio_player_create(params: Dictionary) -> Dictionary:
3769
3835
  if not (sres is AudioStream):
3770
3836
  return _err("bad_type", "%s is not an AudioStream" % stream_path)
3771
3837
  node.set("stream", sres as AudioStream)
3772
- var ur := _plugin.get_undo_redo()
3773
- ur.create_action("Breakpoint: add %s" % node.name)
3774
- ur.add_do_method(parent, "add_child", node)
3775
- ur.add_do_method(node, "set_owner", root)
3776
- ur.add_do_reference(node)
3777
- ur.add_undo_method(parent, "remove_child", node)
3778
- ur.commit_action()
3779
- return _ok({"path": _path_of(root, node), "name": String(node.name), "type": node.get_class(), "dim": dim, "autoplay": bool(node.get("autoplay")), "volume_db": float(node.get("volume_db")), "bus": String(node.get("bus")), "stream_path": stream_path})
3838
+ var added := _commit_add(node, parent, root, "Breakpoint: add %s" % node.name)
3839
+ added["type"] = node.get_class()
3840
+ added["dim"] = dim
3841
+ added["autoplay"] = bool(node.get("autoplay"))
3842
+ added["volume_db"] = float(node.get("volume_db"))
3843
+ added["bus"] = String(node.get("bus"))
3844
+ added["stream_path"] = stream_path
3845
+ return _ok(added)
3780
3846
 
3781
3847
 
3782
3848
  func _audio_set_stream(params: Dictionary) -> Dictionary:
@@ -3847,10 +3913,10 @@ func _audio_set_bus_layout(params: Dictionary) -> Dictionary:
3847
3913
  if not to_path.begins_with("res://"):
3848
3914
  return _err("bad_params", "'to_path' must be a res:// path")
3849
3915
  var layout = AudioServer.generate_bus_layout()
3850
- var e := ResourceSaver.save(layout, to_path)
3851
- if e != OK:
3852
- return _err("save_failed", "ResourceSaver.save() returned %d" % e)
3853
- return _ok({"saved": to_path, "bus_count": AudioServer.get_bus_count()})
3916
+ var saved := _commit_save(layout, to_path, params, SAVE_NEW)
3917
+ if saved.has("error"):
3918
+ return saved["error"]
3919
+ return _ok({"saved": to_path, "bus_count": AudioServer.get_bus_count()}.merged(saved["fields"]))
3854
3920
 
3855
3921
 
3856
3922
  # ---------------------------------------------------------------- Group G: UI / control / theming ----
@@ -3876,14 +3942,9 @@ func _control_create(params: Dictionary) -> Dictionary:
3876
3942
  node.name = String(params.get("name", type))
3877
3943
  if params.has("text") and _has_property(node, "text"):
3878
3944
  node.set("text", String(params.get("text")))
3879
- var ur := _plugin.get_undo_redo()
3880
- ur.create_action("Breakpoint: add %s" % node.name)
3881
- ur.add_do_method(parent, "add_child", node)
3882
- ur.add_do_method(node, "set_owner", root)
3883
- ur.add_do_reference(node)
3884
- ur.add_undo_method(parent, "remove_child", node)
3885
- ur.commit_action()
3886
- return _ok({"path": _path_of(root, node), "name": String(node.name), "type": type})
3945
+ var added := _commit_add(node, parent, root, "Breakpoint: add %s" % node.name)
3946
+ added["type"] = type
3947
+ return _ok(added)
3887
3948
 
3888
3949
 
3889
3950
  func _container_add_child(params: Dictionary) -> Dictionary:
@@ -3902,14 +3963,10 @@ func _container_add_child(params: Dictionary) -> Dictionary:
3902
3963
  return _err("bad_type", "%s is not a Control subclass" % type)
3903
3964
  var node: Node = ClassDB.instantiate(type)
3904
3965
  node.name = String(params.get("name", type))
3905
- var ur := _plugin.get_undo_redo()
3906
- ur.create_action("Breakpoint: add %s to container" % node.name)
3907
- ur.add_do_method(parent, "add_child", node)
3908
- ur.add_do_method(node, "set_owner", root)
3909
- ur.add_do_reference(node)
3910
- ur.add_undo_method(parent, "remove_child", node)
3911
- ur.commit_action()
3912
- return _ok({"path": _path_of(root, node), "name": String(node.name), "type": type, "container": _path_of(root, parent)})
3966
+ var added := _commit_add(node, parent, root, "Breakpoint: add %s to container" % node.name)
3967
+ added["type"] = type
3968
+ added["container"] = _path_of(root, parent)
3969
+ return _ok(added)
3913
3970
 
3914
3971
 
3915
3972
  func _control_set_anchors(params: Dictionary) -> Dictionary:
@@ -4043,10 +4100,10 @@ func _theme_create(params: Dictionary) -> Dictionary:
4043
4100
  if not to_path.begins_with("res://"):
4044
4101
  return _err("bad_params", "'to_path' must be a res:// path")
4045
4102
  var theme := Theme.new()
4046
- var e := ResourceSaver.save(theme, to_path)
4047
- if e != OK:
4048
- return _err("save_failed", "ResourceSaver.save() returned %d" % e)
4049
- return _ok({"created": to_path, "type": "Theme"})
4103
+ var saved := _commit_save(theme, to_path, params, SAVE_NEW)
4104
+ if saved.has("error"):
4105
+ return saved["error"]
4106
+ return _ok({"created": to_path, "type": "Theme"}.merged(saved["fields"]))
4050
4107
 
4051
4108
 
4052
4109
  func _theme_set_color(params: Dictionary) -> Dictionary:
@@ -4060,9 +4117,9 @@ func _theme_set_color(params: Dictionary) -> Dictionary:
4060
4117
  return _err("bad_params", "Missing 'name' or 'theme_type'")
4061
4118
  var col := _to_color(params.get("color"))
4062
4119
  theme.set_color(iname, ttype, col)
4063
- var e := ResourceSaver.save(theme, path)
4064
- if e != OK:
4065
- return _err("save_failed", "ResourceSaver.save() returned %d" % e)
4120
+ var saved := _commit_save(theme, path, params, SAVE_BACK)
4121
+ if saved.has("error"):
4122
+ return saved["error"]
4066
4123
  return _ok({"path": path, "name": iname, "theme_type": ttype, "color": [col.r, col.g, col.b, col.a]})
4067
4124
 
4068
4125
 
@@ -4082,9 +4139,9 @@ func _theme_set_font(params: Dictionary) -> Dictionary:
4082
4139
  if not (fres is Font):
4083
4140
  return _err("bad_type", "%s is not a Font" % font_path)
4084
4141
  theme.set_font(iname, ttype, fres as Font)
4085
- var e := ResourceSaver.save(theme, path)
4086
- if e != OK:
4087
- return _err("save_failed", "ResourceSaver.save() returned %d" % e)
4142
+ var saved := _commit_save(theme, path, params, SAVE_BACK)
4143
+ if saved.has("error"):
4144
+ return saved["error"]
4088
4145
  return _ok({"path": path, "name": iname, "theme_type": ttype, "font_path": font_path})
4089
4146
 
4090
4147
 
@@ -4104,9 +4161,9 @@ func _theme_set_stylebox(params: Dictionary) -> Dictionary:
4104
4161
  if not (sres is StyleBox):
4105
4162
  return _err("bad_type", "%s is not a StyleBox" % sb_path)
4106
4163
  theme.set_stylebox(iname, ttype, sres as StyleBox)
4107
- var e := ResourceSaver.save(theme, path)
4108
- if e != OK:
4109
- return _err("save_failed", "ResourceSaver.save() returned %d" % e)
4164
+ var saved := _commit_save(theme, path, params, SAVE_BACK)
4165
+ if saved.has("error"):
4166
+ return saved["error"]
4110
4167
  return _ok({"path": path, "name": iname, "theme_type": ttype, "stylebox_path": sb_path})
4111
4168
 
4112
4169
 
@@ -4123,9 +4180,9 @@ func _theme_set_constant(params: Dictionary) -> Dictionary:
4123
4180
  return _err("bad_params", "Missing 'value'")
4124
4181
  var val := int(params.get("value"))
4125
4182
  theme.set_constant(iname, ttype, val)
4126
- var e := ResourceSaver.save(theme, path)
4127
- if e != OK:
4128
- return _err("save_failed", "ResourceSaver.save() returned %d" % e)
4183
+ var saved := _commit_save(theme, path, params, SAVE_BACK)
4184
+ if saved.has("error"):
4185
+ return saved["error"]
4129
4186
  return _ok({"path": path, "name": iname, "theme_type": ttype, "value": val})
4130
4187
 
4131
4188
 
@@ -4216,14 +4273,10 @@ func _meshinstance_create(params: Dictionary) -> Dictionary:
4216
4273
  node.name = String(params.get("name", "MeshInstance3D"))
4217
4274
  if mesh_res != null:
4218
4275
  node.mesh = mesh_res
4219
- var ur := _plugin.get_undo_redo()
4220
- ur.create_action("Breakpoint: add %s" % node.name)
4221
- ur.add_do_method(parent, "add_child", node)
4222
- ur.add_do_method(node, "set_owner", root)
4223
- ur.add_do_reference(node)
4224
- ur.add_undo_method(parent, "remove_child", node)
4225
- ur.commit_action()
4226
- return _ok({"path": _path_of(root, node), "name": String(node.name), "type": "MeshInstance3D", "mesh_path": mesh_path})
4276
+ var added := _commit_add(node, parent, root, "Breakpoint: add %s" % node.name)
4277
+ added["type"] = "MeshInstance3D"
4278
+ added["mesh_path"] = mesh_path
4279
+ return _ok(added)
4227
4280
 
4228
4281
 
4229
4282
  func _mesh_set_surface_material(params: Dictionary) -> Dictionary:
@@ -4277,10 +4330,10 @@ func _primitive_mesh_create(params: Dictionary) -> Dictionary:
4277
4330
  var mesh: Mesh = ClassDB.instantiate(cls)
4278
4331
  if mesh == null:
4279
4332
  return _err("create_failed", "Could not instantiate %s" % cls)
4280
- var e := ResourceSaver.save(mesh, to_path)
4281
- if e != OK:
4282
- return _err("save_failed", "ResourceSaver.save() returned %d" % e)
4283
- return _ok({"created": to_path, "type": cls, "shape": shape})
4333
+ var saved := _commit_save(mesh, to_path, params, SAVE_NEW)
4334
+ if saved.has("error"):
4335
+ return saved["error"]
4336
+ return _ok({"created": to_path, "type": cls, "shape": shape}.merged(saved["fields"]))
4284
4337
 
4285
4338
 
4286
4339
  func _light_create(params: Dictionary) -> Dictionary:
@@ -4297,14 +4350,10 @@ func _light_create(params: Dictionary) -> Dictionary:
4297
4350
  var cls := String(classes[kind])
4298
4351
  var node: Node = ClassDB.instantiate(cls)
4299
4352
  node.name = String(params.get("name", cls))
4300
- var ur := _plugin.get_undo_redo()
4301
- ur.create_action("Breakpoint: add %s" % node.name)
4302
- ur.add_do_method(parent, "add_child", node)
4303
- ur.add_do_method(node, "set_owner", root)
4304
- ur.add_do_reference(node)
4305
- ur.add_undo_method(parent, "remove_child", node)
4306
- ur.commit_action()
4307
- return _ok({"path": _path_of(root, node), "name": String(node.name), "type": cls, "kind": kind})
4353
+ var added := _commit_add(node, parent, root, "Breakpoint: add %s" % node.name)
4354
+ added["type"] = cls
4355
+ added["kind"] = kind
4356
+ return _ok(added)
4308
4357
 
4309
4358
 
4310
4359
  func _camera_create(params: Dictionary) -> Dictionary:
@@ -4318,14 +4367,10 @@ func _camera_create(params: Dictionary) -> Dictionary:
4318
4367
  node.name = String(params.get("name", "Camera3D"))
4319
4368
  if bool(params.get("current", false)):
4320
4369
  node.current = true
4321
- var ur := _plugin.get_undo_redo()
4322
- ur.create_action("Breakpoint: add %s" % node.name)
4323
- ur.add_do_method(parent, "add_child", node)
4324
- ur.add_do_method(node, "set_owner", root)
4325
- ur.add_do_reference(node)
4326
- ur.add_undo_method(parent, "remove_child", node)
4327
- ur.commit_action()
4328
- return _ok({"path": _path_of(root, node), "name": String(node.name), "type": "Camera3D", "current": node.current})
4370
+ var added := _commit_add(node, parent, root, "Breakpoint: add %s" % node.name)
4371
+ added["type"] = "Camera3D"
4372
+ added["current"] = node.current
4373
+ return _ok(added)
4329
4374
 
4330
4375
 
4331
4376
  func _environment_create(params: Dictionary) -> Dictionary:
@@ -4340,10 +4385,10 @@ func _environment_create(params: Dictionary) -> Dictionary:
4340
4385
  env.background_mode = int(modes[bg])
4341
4386
  if params.has("ambient_color"):
4342
4387
  env.ambient_light_color = _to_color(params.get("ambient_color"))
4343
- var e := ResourceSaver.save(env, to_path)
4344
- if e != OK:
4345
- return _err("save_failed", "ResourceSaver.save() returned %d" % e)
4346
- return _ok({"created": to_path, "type": "Environment", "background_mode": bg})
4388
+ var saved := _commit_save(env, to_path, params, SAVE_NEW)
4389
+ if saved.has("error"):
4390
+ return saved["error"]
4391
+ return _ok({"created": to_path, "type": "Environment", "background_mode": bg}.merged(saved["fields"]))
4347
4392
 
4348
4393
 
4349
4394
  func _environment_set_sky(params: Dictionary) -> Dictionary:
@@ -4367,9 +4412,9 @@ func _environment_set_sky(params: Dictionary) -> Dictionary:
4367
4412
  return _err("bad_params", "Unknown sky_material: %s (procedural/physical/panorama)" % mat_kind)
4368
4413
  env.sky = sky
4369
4414
  env.background_mode = Environment.BG_SKY
4370
- var e := ResourceSaver.save(env, path)
4371
- if e != OK:
4372
- return _err("save_failed", "ResourceSaver.save() returned %d" % e)
4415
+ var saved := _commit_save(env, path, params, SAVE_BACK)
4416
+ if saved.has("error"):
4417
+ return saved["error"]
4373
4418
  return _ok({"path": path, "background_mode": "sky", "sky_material": mat_kind})
4374
4419
 
4375
4420
 
@@ -4389,14 +4434,10 @@ func _csg_create(params: Dictionary) -> Dictionary:
4389
4434
  return _err("bad_type", "Cannot instantiate class: %s" % cls)
4390
4435
  var node: Node = ClassDB.instantiate(cls)
4391
4436
  node.name = String(params.get("name", cls))
4392
- var ur := _plugin.get_undo_redo()
4393
- ur.create_action("Breakpoint: add %s" % node.name)
4394
- ur.add_do_method(parent, "add_child", node)
4395
- ur.add_do_method(node, "set_owner", root)
4396
- ur.add_do_reference(node)
4397
- ur.add_undo_method(parent, "remove_child", node)
4398
- ur.commit_action()
4399
- return _ok({"path": _path_of(root, node), "name": String(node.name), "type": cls, "shape": shape})
4437
+ var added := _commit_add(node, parent, root, "Breakpoint: add %s" % node.name)
4438
+ added["type"] = cls
4439
+ added["shape"] = shape
4440
+ return _ok(added)
4400
4441
 
4401
4442
 
4402
4443
  func _navregion_create(params: Dictionary) -> Dictionary:
@@ -4410,14 +4451,10 @@ func _navregion_create(params: Dictionary) -> Dictionary:
4410
4451
  node.name = String(params.get("name", "NavigationRegion3D"))
4411
4452
  if bool(params.get("with_navmesh", true)):
4412
4453
  node.navigation_mesh = NavigationMesh.new()
4413
- var ur := _plugin.get_undo_redo()
4414
- ur.create_action("Breakpoint: add %s" % node.name)
4415
- ur.add_do_method(parent, "add_child", node)
4416
- ur.add_do_method(node, "set_owner", root)
4417
- ur.add_do_reference(node)
4418
- ur.add_undo_method(parent, "remove_child", node)
4419
- ur.commit_action()
4420
- return _ok({"path": _path_of(root, node), "name": String(node.name), "type": "NavigationRegion3D", "has_navmesh": node.navigation_mesh != null})
4454
+ var added := _commit_add(node, parent, root, "Breakpoint: add %s" % node.name)
4455
+ added["type"] = "NavigationRegion3D"
4456
+ added["has_navmesh"] = node.navigation_mesh != null
4457
+ return _ok(added)
4421
4458
 
4422
4459
 
4423
4460
  func _navagent_configure(params: Dictionary) -> Dictionary:
@@ -4441,14 +4478,10 @@ func _navagent_configure(params: Dictionary) -> Dictionary:
4441
4478
  node.target_desired_distance = float(params.get("target_desired_distance"))
4442
4479
  if params.has("avoidance_enabled"):
4443
4480
  node.avoidance_enabled = bool(params.get("avoidance_enabled"))
4444
- var ur := _plugin.get_undo_redo()
4445
- ur.create_action("Breakpoint: add %s" % node.name)
4446
- ur.add_do_method(parent, "add_child", node)
4447
- ur.add_do_method(node, "set_owner", root)
4448
- ur.add_do_reference(node)
4449
- ur.add_undo_method(parent, "remove_child", node)
4450
- ur.commit_action()
4451
- return _ok({"path": _path_of(root, node), "name": String(node.name), "type": "NavigationAgent3D", "config": {"radius": node.radius, "height": node.height, "max_speed": node.max_speed, "path_desired_distance": node.path_desired_distance, "target_desired_distance": node.target_desired_distance, "avoidance_enabled": node.avoidance_enabled}})
4481
+ var added := _commit_add(node, parent, root, "Breakpoint: add %s" % node.name)
4482
+ added["type"] = "NavigationAgent3D"
4483
+ added["config"] = {"radius": node.radius, "height": node.height, "max_speed": node.max_speed, "path_desired_distance": node.path_desired_distance, "target_desired_distance": node.target_desired_distance, "avoidance_enabled": node.avoidance_enabled}
4484
+ return _ok(added)
4452
4485
 
4453
4486
 
4454
4487
  # ------------------------------------------------ Group I: input/config/testing ----
@@ -4757,20 +4790,11 @@ func _mp_add_spawner(params: Dictionary) -> Dictionary:
4757
4790
  if sp.begins_with("res://"):
4758
4791
  spawner.add_spawnable_scene(sp)
4759
4792
  added_scenes.append(sp)
4760
- var ur := _plugin.get_undo_redo()
4761
- ur.create_action("Breakpoint: add %s" % spawner.name)
4762
- ur.add_do_method(parent, "add_child", spawner)
4763
- ur.add_do_method(spawner, "set_owner", root)
4764
- ur.add_do_reference(spawner)
4765
- ur.add_undo_method(parent, "remove_child", spawner)
4766
- ur.commit_action()
4767
- return _ok({
4768
- "path": _path_of(root, spawner),
4769
- "name": String(spawner.name),
4770
- "type": "MultiplayerSpawner",
4771
- "spawn_path": String(spawner.spawn_path),
4772
- "spawnable_scenes": added_scenes,
4773
- })
4793
+ var added := _commit_add(spawner, parent, root, "Breakpoint: add %s" % spawner.name)
4794
+ added["type"] = "MultiplayerSpawner"
4795
+ added["spawn_path"] = String(spawner.spawn_path)
4796
+ added["spawnable_scenes"] = added_scenes
4797
+ return _ok(added)
4774
4798
 
4775
4799
 
4776
4800
  func _mp_add_synchronizer(params: Dictionary) -> Dictionary:
@@ -4801,20 +4825,11 @@ func _mp_add_synchronizer(params: Dictionary) -> Dictionary:
4801
4825
  cfg.property_set_replication_mode(np, mode)
4802
4826
  added_props.append(String(p))
4803
4827
  sync.replication_config = cfg
4804
- var ur := _plugin.get_undo_redo()
4805
- ur.create_action("Breakpoint: add %s" % sync.name)
4806
- ur.add_do_method(parent, "add_child", sync)
4807
- ur.add_do_method(sync, "set_owner", root)
4808
- ur.add_do_reference(sync)
4809
- ur.add_undo_method(parent, "remove_child", sync)
4810
- ur.commit_action()
4811
- return _ok({
4812
- "path": _path_of(root, sync),
4813
- "name": String(sync.name),
4814
- "type": "MultiplayerSynchronizer",
4815
- "root_path": String(sync.root_path),
4816
- "properties": added_props,
4817
- })
4828
+ var added := _commit_add(sync, parent, root, "Breakpoint: add %s" % sync.name)
4829
+ added["type"] = "MultiplayerSynchronizer"
4830
+ added["root_path"] = String(sync.root_path)
4831
+ added["properties"] = added_props
4832
+ return _ok(added)
4818
4833
 
4819
4834
 
4820
4835
  func _mp_set_authority(params: Dictionary) -> Dictionary:
@@ -4862,7 +4877,15 @@ func _mp_write_script(params: Dictionary) -> Dictionary:
4862
4877
  var efs := EditorInterface.get_resource_filesystem()
4863
4878
  efs.update_file(to_path)
4864
4879
  efs.scan()
4865
- return _ok({"status": "written", "path": to_path, "bytes": content.to_utf8_buffer().size(), "created": not existed})
4880
+ # 🆕 284 — `replaced` JOINS `created`, IT DOES NOT REPLACE IT. This reply has carried
4881
+ # `created: false` since the scaffold family was built, which is a fifth spelling of
4882
+ # the one question 284 unified — and callers branch on it, so it stays. What is added
4883
+ # is the field every other destination-writing tool now answers with, present only
4884
+ # when it happened, so one convention covers the whole surface.
4885
+ var out := {"status": "written", "path": to_path, "bytes": content.to_utf8_buffer().size(), "created": not existed}
4886
+ if existed:
4887
+ out["replaced"] = true
4888
+ return _ok(out)
4866
4889
 
4867
4890
 
4868
4891
  ## Detect which known game-backend SDKs are installed in this project. Each is