gdharness 1.3.41 → 1.3.43

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 (107) hide show
  1. package/build/cli.js +131 -47
  2. package/build/godot/addons/auto_reload/auto_reload.gd +18 -19
  3. package/build/godot/addons/gdharness_editor/animation_reads.gd +84 -35
  4. package/build/godot/addons/gdharness_editor/bridge_client.gd +53 -58
  5. package/build/godot/addons/gdharness_editor/declared_values.gd +68 -32
  6. package/build/godot/addons/gdharness_editor/file_reads.gd +114 -39
  7. package/build/godot/addons/gdharness_editor/file_walk.gd +12 -5
  8. package/build/godot/addons/gdharness_editor/headless_check.gd +94 -41
  9. package/build/godot/addons/gdharness_editor/plugin.gd +2 -3
  10. package/build/godot/addons/gdharness_editor/property_paths.gd +22 -12
  11. package/build/godot/addons/gdharness_editor/property_values.gd +143 -78
  12. package/build/godot/addons/gdharness_editor/reading.gd +37 -12
  13. package/build/godot/addons/gdharness_editor/same_values.gd +4 -4
  14. package/build/godot/addons/gdharness_editor/scene_file.gd +136 -79
  15. package/build/godot/addons/gdharness_editor/scene_uids.gd +12 -6
  16. package/build/godot/addons/gdharness_editor/serialisation.gd +75 -51
  17. package/build/godot/addons/gdharness_editor/setting_reads.gd +7 -6
  18. package/build/godot/addons/gdharness_editor/tool_executor.gd +3 -6
  19. package/build/godot/addons/gdharness_editor/tools/animation_states.gd +41 -28
  20. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +84 -50
  21. package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +70 -40
  22. package/build/godot/addons/gdharness_editor/tools/class_tools.gd +34 -30
  23. package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +24 -22
  24. package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd +49 -29
  25. package/build/godot/addons/gdharness_editor/tools/play_tools.gd +26 -21
  26. package/build/godot/addons/gdharness_editor/tools/profile_tools.gd +40 -40
  27. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +181 -107
  28. package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +16 -9
  29. package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +75 -43
  30. package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +35 -20
  31. package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +28 -13
  32. package/build/godot/addons/gdharness_editor/tools/scene_sharing.gd +66 -0
  33. package/build/godot/addons/gdharness_editor/tools/scene_sharing.gd.uid +1 -0
  34. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +171 -62
  35. package/build/godot/addons/gdharness_editor/tools/script_paths.gd +20 -8
  36. package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +11 -8
  37. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +47 -34
  38. package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd +77 -50
  39. package/build/godot/addons/gdharness_editor/value_shapes.gd +5 -6
  40. package/build/godot/addons/gdharness_runtime/call_errors.gd +1 -2
  41. package/build/godot/addons/gdharness_runtime/declared_values.gd +68 -32
  42. package/build/godot/addons/gdharness_runtime/error_report.gd +1 -2
  43. package/build/godot/addons/gdharness_runtime/reading.gd +37 -12
  44. package/build/godot/addons/gdharness_runtime/runtime_accessibility.gd +84 -63
  45. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +153 -148
  46. package/build/godot/addons/gdharness_runtime/runtime_capture.gd +64 -42
  47. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +184 -122
  48. package/build/godot/addons/gdharness_runtime/runtime_frames.gd +2 -2
  49. package/build/godot/addons/gdharness_runtime/runtime_hover.gd +24 -20
  50. package/build/godot/addons/gdharness_runtime/runtime_input.gd +134 -92
  51. package/build/godot/addons/gdharness_runtime/runtime_items.gd +29 -13
  52. package/build/godot/addons/gdharness_runtime/runtime_menus.gd +0 -1
  53. package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +38 -20
  54. package/build/godot/addons/gdharness_runtime/runtime_pad.gd +17 -13
  55. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +84 -46
  56. package/build/godot/addons/gdharness_runtime/runtime_queries.gd +117 -79
  57. package/build/godot/addons/gdharness_runtime/runtime_says.gd +14 -17
  58. package/build/godot/addons/gdharness_runtime/runtime_screen.gd +23 -16
  59. package/build/godot/addons/gdharness_runtime/runtime_targets.gd +117 -66
  60. package/build/godot/addons/gdharness_runtime/runtime_touch.gd +211 -0
  61. package/build/godot/addons/gdharness_runtime/runtime_touch.gd.uid +1 -0
  62. package/build/godot/addons/gdharness_runtime/runtime_typing.gd +44 -27
  63. package/build/godot/addons/gdharness_runtime/runtime_values.gd +108 -73
  64. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +111 -84
  65. package/build/godot/addons/gdharness_runtime/runtime_words.gd +10 -7
  66. package/build/godot/addons/gdharness_runtime/serialisation.gd +75 -51
  67. package/build/godot/addons/gdharness_runtime/value_shapes.gd +5 -6
  68. package/build/godot/addons/gdharness_tests/bench.gd +4 -5
  69. package/build/godot/addons/gdharness_tests/event.gd +10 -5
  70. package/build/godot/addons/gdharness_tests/share.gd +38 -18
  71. package/build/godot/addons/gdharness_tests/suites.gd +3 -3
  72. package/build/godot/addons/gdharness_tests/wait.gd +3 -2
  73. package/build/godot/loader/loader.gd +9 -9
  74. package/build/godot/operations/animation_reads.gd +84 -35
  75. package/build/godot/operations/audio_buses.gd +48 -19
  76. package/build/godot/operations/class_cache.gd +16 -11
  77. package/build/godot/operations/classdb_queries.gd +89 -44
  78. package/build/godot/operations/declared_values.gd +68 -32
  79. package/build/godot/operations/dependencies.gd +139 -93
  80. package/build/godot/operations/editor_tools.gd +9 -10
  81. package/build/godot/operations/export_presets.gd +100 -54
  82. package/build/godot/operations/file_reads.gd +114 -39
  83. package/build/godot/operations/file_walk.gd +12 -5
  84. package/build/godot/operations/gdscript_analysis.gd +105 -66
  85. package/build/godot/operations/gdscript_authoring.gd +138 -39
  86. package/build/godot/operations/godot_operations.gd +17 -11
  87. package/build/godot/operations/import_pipeline.gd +110 -59
  88. package/build/godot/operations/input_actions.gd +63 -39
  89. package/build/godot/operations/patterns.gd +3 -1
  90. package/build/godot/operations/plugins.gd +36 -16
  91. package/build/godot/operations/project_config.gd +144 -27
  92. package/build/godot/operations/project_diagnostics.gd +24 -11
  93. package/build/godot/operations/project_file.gd +16 -16
  94. package/build/godot/operations/project_validation.gd +71 -44
  95. package/build/godot/operations/property_paths.gd +22 -12
  96. package/build/godot/operations/property_values.gd +143 -78
  97. package/build/godot/operations/reading.gd +37 -12
  98. package/build/godot/operations/rename_checks.gd +2 -2
  99. package/build/godot/operations/resource_files.gd +11 -9
  100. package/build/godot/operations/same_values.gd +4 -4
  101. package/build/godot/operations/scene_connections.gd +27 -13
  102. package/build/godot/operations/script_check.gd +52 -49
  103. package/build/godot/operations/serialisation.gd +75 -51
  104. package/build/godot/operations/setting_reads.gd +7 -6
  105. package/build/godot/operations/value_shapes.gd +5 -6
  106. package/build/godot/tests/listed.gd +321 -268
  107. package/package.json +1 -1
@@ -1,6 +1,5 @@
1
1
  @tool
2
2
  extends Node
3
-
4
3
  ## Scenes and their nodes, edited in the open editor and read back from what it holds.
5
4
 
6
5
  const GdhDeclared = preload("../declared_values.gd")
@@ -15,6 +14,7 @@ const GdhPlacing = preload("scene_placing.gd")
15
14
  const GdhPropertyValues = preload("../property_values.gd")
16
15
  const GdhSame = preload("../same_values.gd")
17
16
  const GdhSceneFile = preload("../scene_file.gd")
17
+ const GdhSceneSharing = preload("scene_sharing.gd")
18
18
  const GdhSerialisation = preload("../serialisation.gd")
19
19
  const GdhTilesetTools = preload("tileset_tools.gd")
20
20
 
@@ -43,11 +43,20 @@ func _kept(scene: GdhSceneFile, saved: Node, node: Node, names: Array) -> String
43
43
  for key: Variant in names:
44
44
  var property: String = str(key)
45
45
  var loaded: Variant = _properties.read_at(copy, property)
46
- if not scene.kept(saved, loaded, _properties.read_at(node, property)):
46
+ var given: Variant = _properties.read_at(node, property)
47
+ if not scene.kept(saved, loaded, given):
48
+ # The file holds it, and the copy's own script set it again as the copy loaded, as it
49
+ # will wherever the scene loads; an editor saves the same.
50
+ if scene.stores(at, property, given):
51
+ var on: Dictionary = scene.loads_as.get_or_add(at, {})
52
+ if GdhHeadlessCheck.untooled(copy.get_script()):
53
+ scene.loads_untooled[at] = true
54
+ on[property] = _values.serialize_value(loaded)
55
+ continue
47
56
  return (
48
57
  (
49
- "%s.%s would load from the file as %s: the scene does not keep it, because the property "
50
- + "is not one a scene stores or the node belongs to another scene."
58
+ "%s.%s would load from the file as %s: the scene does not keep it, because the "
59
+ + "property is not one a scene stores or the node belongs to another scene."
51
60
  )
52
61
  % [at, property, JSON.stringify(_values.serialize_value(loaded), "", false)]
53
62
  )
@@ -141,7 +150,7 @@ func _create_of_a_class(args: Dictionary) -> Dictionary:
141
150
  var script_path: String = str(args.get("script", ""))
142
151
 
143
152
  if scene_path.is_empty():
144
- return {"ok": false, "error": "Missing scenePath"}
153
+ return { "ok": false, "error": "Missing scenePath" }
145
154
  var made: Dictionary = GdhSceneFile.made_as(
146
155
  root_node_type,
147
156
  "" if script_path.is_empty() else GdhSceneFile.res_path(project_path, script_path),
@@ -149,18 +158,21 @@ func _create_of_a_class(args: Dictionary) -> Dictionary:
149
158
  "rootNodeType"
150
159
  )
151
160
  if made.has("error"):
152
- return {"ok": false, "error": made["error"]}
161
+ return { "ok": false, "error": made["error"] }
153
162
  root_node_type = str(made["type"])
154
163
  script_path = str(made["script"])
155
164
  # A class that is not a node went into a Node variable, which is a runtime error the caller
156
165
  # heard about only as "Invalid tool result".
157
- if not ClassDB.class_exists(root_node_type) or not ClassDB.is_parent_class(root_node_type, "Node"):
158
- return {"ok": false, "error": GdhPlacing.not_a_node_class("rootNodeType", root_node_type)}
166
+ if (
167
+ not ClassDB.class_exists(root_node_type)
168
+ or not ClassDB.is_parent_class(root_node_type, "Node")
169
+ ):
170
+ return { "ok": false, "error": GdhPlacing.not_a_node_class("rootNodeType", root_node_type) }
159
171
  if not ClassDB.can_instantiate(root_node_type):
160
- return {"ok": false, "error": "%s cannot be instantiated." % root_node_type}
172
+ return { "ok": false, "error": "%s cannot be instantiated." % root_node_type }
161
173
  var unfit: String = GdhSceneFile.unfit_name(root_node_name)
162
174
  if not unfit.is_empty():
163
- return {"ok": false, "error": unfit}
175
+ return { "ok": false, "error": unfit }
164
176
  if FileAccess.file_exists(scene_path):
165
177
  return GdhKin.taken(scene_path)
166
178
 
@@ -176,29 +188,32 @@ func _create_of_a_class(args: Dictionary) -> Dictionary:
176
188
  var script: Script = load(full_script_path) as Script
177
189
  if script == null:
178
190
  root.free()
179
- return {"ok": false, "error": "No script loads from " + full_script_path}
191
+ return { "ok": false, "error": "No script loads from " + full_script_path }
180
192
  var uncompiled: String = GdhSceneFile.uncompiled(script, full_script_path)
181
193
  if not uncompiled.is_empty():
182
194
  root.free()
183
- return {"ok": false, "error": uncompiled}
195
+ return { "ok": false, "error": uncompiled }
184
196
  var base: String = str(script.get_instance_base_type())
185
197
  if not ClassDB.is_parent_class(root_node_type, base):
186
198
  root.free()
187
199
  return {
188
200
  "ok": false,
189
201
  "error":
190
- "%s extends %s, so it cannot be on a %s root." % [full_script_path, base, root_node_type]
202
+ (
203
+ "%s extends %s, so it cannot be on a %s root."
204
+ % [full_script_path, base, root_node_type]
205
+ ),
191
206
  }
192
207
  root.set_script(script)
193
- # Named again, since a headless engine runs the script's _init as it is attached, and a node's
194
- # name is no property the write compares.
208
+ # Named again, since a headless engine runs the script's _init as it is attached, and a
209
+ # node's name is no property the write compares.
195
210
  root.name = root_node_name
196
211
 
197
212
  var had_folder: bool = DirAccess.dir_exists_absolute(scene_path.get_base_dir())
198
213
  var folder: String = GdhSceneFile.make_folder_for(scene_path)
199
214
  if not folder.is_empty():
200
215
  root.free()
201
- return {"ok": false, "error": folder}
216
+ return { "ok": false, "error": folder }
202
217
  # Read before the write, which frees the root.
203
218
  var root_class: Dictionary = GdhSceneFile.class_of(root)
204
219
  var written: Dictionary = scene.write()
@@ -209,7 +224,7 @@ func _create_of_a_class(args: Dictionary) -> Dictionary:
209
224
  "ok": true,
210
225
  "scenePath": scene_path,
211
226
  "rootNodeType": root_class["nodeType"],
212
- "rootNodeName": root_node_name
227
+ "rootNodeName": root_node_name,
213
228
  }
214
229
  if root_class.has("scriptClass"):
215
230
  answer["scriptClass"] = root_class["scriptClass"]
@@ -229,7 +244,9 @@ func list_scene_nodes(args: Dictionary) -> Dictionary:
229
244
 
230
245
  # Read as a headless engine reads it with no editor connected, so the answer does not depend on
231
246
  # whether one is.
232
- var read: Dictionary = GdhFileReads.scene_tree(scene_path, depth, include_properties, include_defaults)
247
+ var read: Dictionary = GdhFileReads.scene_tree(
248
+ scene_path, depth, include_properties, include_defaults
249
+ )
233
250
  if read.get("ok", false):
234
251
  read["unsavedInEditor"] = GdhSceneFile.unsaved(scene_path)
235
252
  return read
@@ -246,12 +263,12 @@ func add_node(args: Dictionary) -> Dictionary:
246
263
  var adding: Array = GdhGroups.asked(args, "addGroups")
247
264
 
248
265
  if node_name.is_empty():
249
- return {"ok": false, "error": "Missing nodeName"}
266
+ return { "ok": false, "error": "Missing nodeName" }
250
267
  var class_script: Script = null
251
268
  if not node_type.is_empty() and not ClassDB.class_exists(node_type):
252
269
  var declared: Dictionary = GdhSceneFile.script_class(node_type, "Node")
253
270
  if declared.has("error"):
254
- return {"ok": false, "error": declared["error"]}
271
+ return { "ok": false, "error": declared["error"] }
255
272
  var held: Variant = declared.get("script")
256
273
  if held is Script:
257
274
  class_script = held
@@ -259,7 +276,7 @@ func add_node(args: Dictionary) -> Dictionary:
259
276
  var making: Array = GdhPlacing.what_to_add(node_type, instance_path, scene_path)
260
277
  var packed: PackedScene = making[0]
261
278
  if not str(making[1]).is_empty():
262
- return {"ok": false, "error": making[1]}
279
+ return { "ok": false, "error": making[1] }
263
280
 
264
281
  var opened: Array = _open(scene_path, true)
265
282
  var scene: GdhSceneFile = opened[0]
@@ -270,7 +287,9 @@ func add_node(args: Dictionary) -> Dictionary:
270
287
  var root: Node = scene.root
271
288
  var parent: Node = GdhSceneFile.node_at(root, parent_node_path)
272
289
  if not parent:
273
- return scene.refuse(scene.not_found("Parent node not found: " + parent_node_path, parent_node_path))
290
+ return scene.refuse(
291
+ scene.not_found("Parent node not found: " + parent_node_path, parent_node_path)
292
+ )
274
293
  var foreign: String = scene.foreign(parent)
275
294
  if not foreign.is_empty():
276
295
  return scene.refuse(foreign)
@@ -306,9 +325,9 @@ func add_node(args: Dictionary) -> Dictionary:
306
325
 
307
326
  var new_path: String = scene.path_of(new_node)
308
327
  # Read before the write, which frees the scene it was added to.
309
- var answer: Dictionary = {"ok": true, "nodeName": node_name}
328
+ var answer: Dictionary = { "ok": true, "nodeName": node_name }
310
329
  answer.merge(GdhSceneFile.class_of(new_node))
311
- answer.merge({"nodePath": new_path, "index": new_node.get_index()})
330
+ answer.merge({ "nodePath": new_path, "index": new_node.get_index() })
312
331
  if not properties.is_empty():
313
332
  var held: Dictionary = {}
314
333
  for key: Variant in properties:
@@ -317,10 +336,14 @@ func add_node(args: Dictionary) -> Dictionary:
317
336
  _properties.note_write(answer, new_node)
318
337
  if not adding.is_empty():
319
338
  answer.merge(GdhGroups.read_added(new_node, adding))
339
+ var id: int = new_node.get_instance_id()
320
340
  var written: Dictionary = scene.write(
321
341
  func(saved: Node) -> String:
322
342
  var unkept: String = _kept(
323
- scene, saved, new_node, properties.keys() + (["script"] if class_script != null else [])
343
+ scene,
344
+ saved,
345
+ new_node,
346
+ properties.keys() + (["script"] if class_script != null else [])
324
347
  )
325
348
  if unkept.is_empty() and saved.get_node_or_null(new_path) != null:
326
349
  unkept = GdhGroups.unkept(saved.get_node(new_path), adding, [])
@@ -329,6 +352,8 @@ func add_node(args: Dictionary) -> Dictionary:
329
352
  if not written.is_empty():
330
353
  return written
331
354
 
355
+ _note_put_back(answer, scene, id, new_path)
356
+ _note_loads_as(answer, scene, new_path)
332
357
  if packed != null:
333
358
  answer["instanceOf"] = instance_path
334
359
  return answer
@@ -344,9 +369,9 @@ func delete_node(args: Dictionary) -> Dictionary:
344
369
  "ok": false,
345
370
  "error":
346
371
  (
347
- "The root cannot be deleted, since a scene has exactly one. Delete the nodes under it, or "
348
- + "make a scene with another root with scene_create."
349
- )
372
+ "The root cannot be deleted, since a scene has exactly one. Delete the nodes under "
373
+ + "it, or make a scene with another root with scene_create."
374
+ ),
350
375
  }
351
376
 
352
377
  var opened: Array = _open(scene_path, true)
@@ -370,7 +395,7 @@ func delete_node(args: Dictionary) -> Dictionary:
370
395
  node.get_parent().remove_child(node)
371
396
  node.free()
372
397
  var carried: GdhPaths = GdhPaths.new()
373
- var _rewritten: int = carried.carry(scene, {at: ""})
398
+ var _rewritten: int = carried.carry(scene, { at: "" })
374
399
 
375
400
  var written: Dictionary = scene.write(
376
401
  func(saved: Node) -> String:
@@ -381,7 +406,7 @@ func delete_node(args: Dictionary) -> Dictionary:
381
406
  if not written.is_empty():
382
407
  return written
383
408
 
384
- var answer: Dictionary = {"ok": true, "deletedNodePath": at}
409
+ var answer: Dictionary = { "ok": true, "deletedNodePath": at }
385
410
  if not went.is_empty():
386
411
  answer["deletedWithIt"] = went
387
412
  answer.merge(GdhKin.follow(scene_path, at, "", carried))
@@ -396,7 +421,7 @@ func duplicate_node(args: Dictionary) -> Dictionary:
396
421
  var parent_path: String = str(args.get("parentNodePath", ""))
397
422
 
398
423
  if node_path.is_empty() or new_name.is_empty():
399
- return {"ok": false, "error": "Missing nodePath or newName"}
424
+ return { "ok": false, "error": "Missing nodePath or newName" }
400
425
 
401
426
  var opened: Array = _open(scene_path, true)
402
427
  var scene: GdhSceneFile = opened[0]
@@ -453,7 +478,9 @@ func duplicate_node(args: Dictionary) -> Dictionary:
453
478
  if copy == null:
454
479
  return "the copy would not be in the scene when it loads."
455
480
  if copy.get_index() != placed[1]:
456
- return "%s would load at index %d, not %d." % [new_path, copy.get_index(), placed[1]]
481
+ return (
482
+ "%s would load at index %d, not %d." % [new_path, copy.get_index(), placed[1]]
483
+ )
457
484
  return ""
458
485
  )
459
486
  if not written.is_empty():
@@ -476,9 +503,9 @@ func reparent_node(args: Dictionary) -> Dictionary:
476
503
  var new_parent_path: String = str(args.get("newParentPath", ""))
477
504
 
478
505
  if node_path.is_empty() or node_path == ".":
479
- return {"ok": false, "error": "Cannot reparent root node"}
506
+ return { "ok": false, "error": "Cannot reparent root node" }
480
507
  if new_parent_path.is_empty():
481
- return {"ok": false, "error": "Missing newParentPath"}
508
+ return { "ok": false, "error": "Missing newParentPath" }
482
509
 
483
510
  var opened: Array = _open(scene_path, true)
484
511
  var scene: GdhSceneFile = opened[0]
@@ -492,7 +519,9 @@ func reparent_node(args: Dictionary) -> Dictionary:
492
519
  if not node:
493
520
  return scene.refuse(scene.not_found("Node not found: " + node_path, node_path))
494
521
  if not new_parent:
495
- return scene.refuse(scene.not_found("New parent not found: " + new_parent_path, new_parent_path))
522
+ return scene.refuse(
523
+ scene.not_found("New parent not found: " + new_parent_path, new_parent_path)
524
+ )
496
525
  var fixed: String = scene.fixed(node)
497
526
  if not fixed.is_empty():
498
527
  return scene.refuse(fixed)
@@ -518,9 +547,12 @@ func reparent_node(args: Dictionary) -> Dictionary:
518
547
  var drawn_at: Dictionary = {}
519
548
  for property: String in GdhPlacing.placing(node):
520
549
  drawn_at[property] = _properties.read_at(node, property)
521
- var held_at: Transform3D = (node as Node3D).transform if node is Node3D else Transform3D.IDENTITY
522
- # Unowned while it moves: a node taken out of the tree keeps its owner, and adding it under the new
523
- # parent warned that the owner was not above it. Node.reparent warns the same way.
550
+ var held_at: Transform3D = (
551
+ (node as Node3D).transform if node is Node3D else Transform3D.IDENTITY
552
+ )
553
+ var stored_before: Dictionary = GdhHeadlessCheck.stored(node)
554
+ # Unowned while it moves: a node taken out of the tree keeps its owner, and adding it under the
555
+ # new parent warned that the owner was not above it. Node.reparent warns the same way.
524
556
  node.owner = null
525
557
  old_parent.remove_child(node)
526
558
  new_parent.add_child(node)
@@ -528,22 +560,30 @@ func reparent_node(args: Dictionary) -> Dictionary:
528
560
  GdhPlacing.own(node, root)
529
561
  if GdhRead.as_bool(args.get("keepGlobalTransform", true)):
530
562
  GdhPlacing.keep_in_place(node, old_parent)
563
+ # Unasked, a headless write takes the move on a node whose script is not a tool for the
564
+ # script's doing and puts the node back as the file held it.
565
+ var placed: Array[String] = GdhHeadlessCheck.differing(node, stored_before, {})
566
+ if not placed.is_empty():
567
+ scene.asked(node, placed)
531
568
  var also_changed: Dictionary = {}
532
569
  for property: String in drawn_at:
533
570
  var now: Variant = _properties.read_at(node, property)
534
571
  if not GdhSame.same(now, drawn_at[property]):
535
- also_changed[property] = {"was": _readable(node, drawn_at[property]), "now": _readable(node, now)}
572
+ also_changed[property] = {
573
+ "was": _readable(node, drawn_at[property]), "now": _readable(node, now),
574
+ }
536
575
  var sheared: String = ""
537
576
  if node is Node3D and GdhPlacing.sheared((node as Node3D).basis):
538
577
  var spatial: Node3D = node
539
578
  also_changed["transform"] = {
540
- "was": _readable(node, held_at), "now": _readable(node, spatial.transform)
579
+ "was": _readable(node, held_at), "now": _readable(node, spatial.transform),
541
580
  }
542
581
  sheared = (
543
582
  (
544
- "%s keeps where it is drawn under %s, which is scaled unevenly across the way it is turned,"
545
- + " so its basis is sheared: no position, rotation and scale describe it, and transform is"
546
- + " what it holds. keepGlobalTransform false keeps its own transform instead."
583
+ "%s keeps where it is drawn under %s, which is scaled unevenly across the way it "
584
+ + "is turned, so its basis is sheared: no position, rotation and scale describe "
585
+ + "it, and transform is what it holds. keepGlobalTransform false keeps its own "
586
+ + "transform instead."
547
587
  )
548
588
  % [str(node.name), new_parent_path]
549
589
  )
@@ -551,7 +591,10 @@ func reparent_node(args: Dictionary) -> Dictionary:
551
591
  var new_path: String = scene.path_of(node)
552
592
  var index: int = node.get_index()
553
593
  var carried: GdhPaths = GdhPaths.new()
554
- var _rewritten: int = carried.carry(scene, {old_path: new_path} if new_path != old_path else {})
594
+ var _rewritten: int = carried.carry(
595
+ scene,
596
+ { old_path: new_path } if new_path != old_path else {},
597
+ )
555
598
  var written: Dictionary = scene.write(
556
599
  func(saved: Node) -> String:
557
600
  var copy: Node = saved.get_node_or_null(new_path)
@@ -592,7 +635,10 @@ func set_node_properties(args: Dictionary) -> Dictionary:
592
635
  var adding: Array = GdhGroups.asked(args, "addGroups")
593
636
  var removing: Array = GdhGroups.asked(args, "removeGroups")
594
637
  if properties.is_empty() and editable == null and adding.is_empty() and removing.is_empty():
595
- return {"ok": false, "error": "set takes properties, editableChildren, addGroups or removeGroups."}
638
+ return {
639
+ "ok": false,
640
+ "error": "set takes properties, editableChildren, addGroups or removeGroups.",
641
+ }
596
642
 
597
643
  var opened: Array = _open(scene_path, true)
598
644
  var scene: GdhSceneFile = opened[0]
@@ -642,8 +688,15 @@ func set_node_properties(args: Dictionary) -> Dictionary:
642
688
  for key: Variant in properties:
643
689
  held[str(key)] = _readable(node, _properties.read_at(node, str(key)))
644
690
  var new_path: String = scene.path_of(node)
645
- var answer: Dictionary = {"ok": true, "nodePath": new_path, "properties": held}
691
+ var answer: Dictionary = { "ok": true, "nodePath": new_path, "properties": held }
646
692
  _properties.note_write(answer, node)
693
+ var shared: Dictionary = {}
694
+ for key: Variant in properties:
695
+ var holders: Array[Dictionary] = GdhSceneSharing.holders(scene, node, str(key))
696
+ if not holders.is_empty():
697
+ shared[str(key)] = holders
698
+ if not shared.is_empty():
699
+ answer["sharedWith"] = shared
647
700
  if not adding.is_empty() or not removing.is_empty():
648
701
  answer["groups"] = GdhGroups.read(scene, node).get("groups", [])
649
702
  var named: Array[String] = []
@@ -653,16 +706,24 @@ func set_node_properties(args: Dictionary) -> Dictionary:
653
706
  named.append(property)
654
707
  var source: String = scene.values_from(node, old_path)
655
708
  var carried: GdhPaths = GdhPaths.new()
656
- var _rewritten: int = carried.carry(scene, {old_path: new_path} if new_path != old_path else {})
709
+ var _rewritten: int = carried.carry(
710
+ scene,
711
+ { old_path: new_path } if new_path != old_path else {},
712
+ )
713
+ var id: int = node.get_instance_id()
657
714
  var written: Dictionary = scene.write(
658
715
  func(saved: Node) -> String:
659
716
  var unkept: String = _kept(scene, saved, node, properties.keys())
660
717
  if unkept.is_empty():
661
718
  unkept = GdhGroups.unkept(saved.get_node(new_path), adding, removing)
662
719
  if unkept.is_empty() and editable != null:
663
- if saved.is_editable_instance(saved.get_node(new_path)) != GdhRead.as_bool(editable):
720
+ if (
721
+ saved.is_editable_instance(saved.get_node(new_path))
722
+ != GdhRead.as_bool(editable)
723
+ ):
664
724
  return (
665
- "%s would load with editableChildren %s." % [new_path, not GdhRead.as_bool(editable)]
725
+ "%s would load with editableChildren %s."
726
+ % [new_path, not GdhRead.as_bool(editable)]
666
727
  )
667
728
  return unkept
668
729
  )
@@ -675,12 +736,48 @@ func set_node_properties(args: Dictionary) -> Dictionary:
675
736
  if not dropped.is_empty():
676
737
  answer["droppedValues"] = dropped["values"]
677
738
  answer["droppedNodes"] = dropped["nodes"]
739
+ _note_put_back(answer, scene, id, new_path)
740
+ _note_loads_as(answer, scene, new_path)
678
741
  _note_left_out(answer, scene.written_pack, new_path, named, source)
679
742
  if new_path != old_path:
680
743
  answer.merge(GdhKin.follow(scene_path, old_path, new_path, carried))
681
744
  return answer
682
745
 
683
746
 
747
+ ## Puts under loadsAs in [param answer] each value given that the file holds and the script of the
748
+ ## node at [param at] changes as the scene loads, as it loads.
749
+ static func _note_loads_as(answer: Dictionary, scene: GdhSceneFile, at: String) -> void:
750
+ var on: Dictionary = scene.loads_as.get(at, {})
751
+ if on.is_empty():
752
+ return
753
+ answer["loadsAs"] = on
754
+ var names: Array = on.keys()
755
+ var pronoun: String = "it" if names.size() == 1 else "them"
756
+ if scene.loads_untooled.has(at):
757
+ answer["loadsAsNote"] = (
758
+ (
759
+ "The file holds %s as given, which the editor shows. %s's script is not a tool, so "
760
+ + "in the game it runs as the scene loads and changes %s to what loadsAs gives."
761
+ )
762
+ % [", ".join(names), at, pronoun]
763
+ )
764
+ else:
765
+ answer["loadsAsNote"] = (
766
+ (
767
+ "The file holds %s as given. %s's script runs as the scene loads, in the editor "
768
+ + "and in the game, and changes %s to what loadsAs gives."
769
+ )
770
+ % [", ".join(names), at, pronoun]
771
+ )
772
+
773
+
774
+ ## Moves out of alsoChanged in [param answer] what the last write of [param scene] put back on the
775
+ ## node with instance id [param id], at [param at], to setByScript.
776
+ static func _note_put_back(answer: Dictionary, scene: GdhSceneFile, id: int, at: String) -> void:
777
+ var restored: Dictionary = scene.restored_by_write.get(id, {})
778
+ GdhHeadlessCheck.move_script_set(answer, restored.keys(), at)
779
+
780
+
684
781
  ## Notes under notStored each of [param named] the file saved from [param pack] does not set on the
685
782
  ## node at [param at]: a set gave it the value the node has without it, so the packer leaves it out,
686
783
  ## as the editor does, and it follows [param source], or its default, from then on.
@@ -710,8 +807,8 @@ static func _note_left_out(
710
807
  else:
711
808
  answer["notStoredNote"] = (
712
809
  (
713
- "%s now %s what %s gives this node, so the file does not store %s, and a change there reaches "
714
- + "this node."
810
+ "%s now %s what %s gives this node, so the file does not store %s, and a change "
811
+ + "there reaches this node."
715
812
  )
716
813
  % [names, verb, source, pronoun]
717
814
  )
@@ -743,7 +840,7 @@ func get_node_properties(args: Dictionary) -> Dictionary:
743
840
  var at: String = scene.path_of(node)
744
841
  var here: Array = set_here.keys()
745
842
  here.sort()
746
- var answer: Dictionary = {"ok": true, "nodePath": at, "properties": props, "setHere": here}
843
+ var answer: Dictionary = { "ok": true, "nodePath": at, "properties": props, "setHere": here }
747
844
  # A Node3D stores only its transform, a matrix, while a set takes the position, rotation and
748
845
  # scale the inspector shows, so a node turned 45 degrees down read back as nine numbers. A
749
846
  # sheared one has no such three, and is left to its transform.
@@ -764,7 +861,9 @@ func get_node_properties(args: Dictionary) -> Dictionary:
764
861
  # A player's getter answers Master for a bus the layout does not have, while the file keeps the
765
862
  # name, so a player left on a renamed or removed bus read as one put on Master.
766
863
  var bus: String = str(set_here.get("bus", ""))
767
- var plays: bool = node is AudioStreamPlayer or node is AudioStreamPlayer2D or node is AudioStreamPlayer3D
864
+ var plays: bool = (
865
+ node is AudioStreamPlayer or node is AudioStreamPlayer2D or node is AudioStreamPlayer3D
866
+ )
768
867
  if plays and not bus.is_empty() and AudioServer.get_bus_index(bus) == -1:
769
868
  props["bus"] = bus
770
869
  var buses: PackedStringArray = []
@@ -772,10 +871,15 @@ func get_node_properties(args: Dictionary) -> Dictionary:
772
871
  var _added: bool = buses.append(AudioServer.get_bus_name(index))
773
872
  answer["busNote"] = (
774
873
  (
775
- "%s names the bus %s, which the project's bus layout does not have, so it plays on %s; "
776
- + "the buses are %s."
874
+ "%s names the bus %s, which the project's bus layout does not have, so it plays on "
875
+ + "%s; the buses are %s."
777
876
  )
778
- % [str(node.name) if at == "." else at, bus, AudioServer.get_bus_name(0), ", ".join(buses)]
877
+ % [
878
+ str(node.name) if at == "." else at,
879
+ bus,
880
+ AudioServer.get_bus_name(0),
881
+ ", ".join(buses),
882
+ ]
779
883
  )
780
884
  # A mask reads as a number, and which layers it holds, under the names the project gives them,
781
885
  # is what a caller reads it for.
@@ -839,7 +943,7 @@ func save_scene(args: Dictionary) -> Dictionary:
839
943
  if target_path != scene_path:
840
944
  var unsaved: String = GdhSceneFile.unsaved_in_editor(target_path)
841
945
  if not unsaved.is_empty():
842
- return {"ok": false, "error": unsaved}
946
+ return { "ok": false, "error": unsaved }
843
947
 
844
948
  var open_root: Node = null
845
949
  var open_roots: Array[Node] = []
@@ -859,7 +963,9 @@ func save_scene(args: Dictionary) -> Dictionary:
859
963
  var packed: PackedScene = PackedScene.new()
860
964
  var packing: Error = packed.pack(open_root)
861
965
  if packing != OK:
862
- return {"ok": false, "error": "Failed to pack %s: %s" % [scene_path, error_string(packing)]}
966
+ return {
967
+ "ok": false, "error": "Failed to pack %s: %s" % [scene_path, error_string(packing)],
968
+ }
863
969
  written = GdhSceneFile.save_packed(packed, target_path)
864
970
  else:
865
971
  var scene: GdhSceneFile = GdhSceneFile.new()
@@ -875,7 +981,7 @@ func save_scene(args: Dictionary) -> Dictionary:
875
981
  "ok": true,
876
982
  "scenePath": scene_path,
877
983
  "savedPath": target_path,
878
- "from": "editor" if open_root != null else "file"
984
+ "from": "editor" if open_root != null else "file",
879
985
  }
880
986
 
881
987
 
@@ -889,16 +995,19 @@ func _save_open_scene(scene_path: String) -> Dictionary:
889
995
  EditorInterface.open_scene_from_path(scene_path)
890
996
  var now: Node = EditorInterface.get_edited_scene_root()
891
997
  if now == null or now.scene_file_path != scene_path:
892
- return {"ok": false, "error": "The editor did not switch to %s to save it." % scene_path}
998
+ return { "ok": false, "error": "The editor did not switch to %s to save it." % scene_path }
893
999
  var saving: Error = EditorInterface.save_scene()
894
1000
  if not previous.is_empty() and previous != scene_path:
895
1001
  EditorInterface.open_scene_from_path(previous)
896
1002
  if saving != OK:
897
- return {"ok": false, "error": "The editor failed to save %s: %s" % [scene_path, error_string(saving)]}
1003
+ return {
1004
+ "ok": false,
1005
+ "error": "The editor failed to save %s: %s" % [scene_path, error_string(saving)],
1006
+ }
898
1007
  return {
899
1008
  "ok": true,
900
1009
  "scenePath": scene_path,
901
1010
  "savedPath": scene_path,
902
1011
  "from": "editor",
903
- "unsavedInEditor": GdhSceneFile.unsaved(scene_path)
1012
+ "unsavedInEditor": GdhSceneFile.unsaved(scene_path),
904
1013
  }
@@ -1,18 +1,21 @@
1
1
  @tool
2
2
  extends RefCounted
3
-
4
3
  ## The node paths a GDScript names in its own source, from the node it is on: `$Path`, `%Unique`,
5
4
  ## and a string handed to get_node, get_node_or_null or has_node called on itself. Read from the
6
5
  ## tokens rather than the lines, so a `$` or a `%` inside a string or a comment names nothing, and
7
6
  ## a `%` after an operand is the remainder of a division rather than a unique name.
8
7
 
9
- enum { NAME, STRING, MARK }
8
+ enum {
9
+ NAME,
10
+ STRING,
11
+ MARK,
12
+ }
10
13
 
11
14
  ## The calls that take a path from the node they are called on.
12
15
  const CALLS: Array[String] = ["get_node", "get_node_or_null", "has_node"]
13
16
  ## The words after which a `%` starts an operand.
14
17
  const OPERAND_AFTER: Array[String] = [
15
- "return", "and", "or", "not", "in", "if", "elif", "while", "await", "match", "when", "else"
18
+ "return", "and", "or", "not", "in", "if", "elif", "while", "await", "match", "when", "else",
16
19
  ]
17
20
  ## The marks after which a `%` starts an operand: an opening bracket, a separator or an operator.
18
21
  const OPERAND_AFTER_MARKS: String = "([{,=:;+-*/<>!&|^~%"
@@ -33,10 +36,14 @@ static func named(source: String) -> Array[Dictionary]:
33
36
  elif _is_mark(token, "%") and _starts_operand(tokens, at):
34
37
  path = _path_from(tokens, at)
35
38
  elif token[0] == NAME and CALLS.has(str(token[1])) and _on_itself(tokens, at):
36
- if at + 2 < tokens.size() and _is_mark(tokens[at + 1], "(") and tokens[at + 2][0] == STRING:
39
+ if (
40
+ at + 2 < tokens.size()
41
+ and _is_mark(tokens[at + 1], "(")
42
+ and tokens[at + 2][0] == STRING
43
+ ):
37
44
  path = str(tokens[at + 2][1])
38
45
  if not path.is_empty():
39
- found.append({"line": token[2], "path": path})
46
+ found.append({ "line": token[2], "path": path })
40
47
  return found
41
48
 
42
49
 
@@ -58,7 +65,8 @@ static func _path_from(tokens: Array[Array], at: int) -> String:
58
65
  return "/".join(parts)
59
66
 
60
67
 
61
- ## The name of a path at [param at], as [its text, where the next token is], or [] when there is none.
68
+ ## The name of a path at [param at], as [its text, where the next token is], or [] when there is
69
+ ## none.
62
70
  static func _part_at(tokens: Array[Array], at: int) -> Array:
63
71
  var unique: String = ""
64
72
  if at < tokens.size() and _is_mark(tokens[at], "%"):
@@ -112,7 +120,10 @@ static func _tokens(source: String) -> Array[Array]:
112
120
  at += 1
113
121
  elif c in " \t\r":
114
122
  at += 1
115
- elif QUOTES.contains(c) or ("r^&".contains(c) and QUOTES.contains(next) and not next.is_empty()):
123
+ elif (
124
+ QUOTES.contains(c)
125
+ or ("r^&".contains(c) and QUOTES.contains(next) and not next.is_empty())
126
+ ):
116
127
  var quoted: Array = _string(source, at if QUOTES.contains(c) else at + 1)
117
128
  tokens.append([STRING, quoted[0], line])
118
129
  line += str(quoted[0]).count("\n")
@@ -133,7 +144,8 @@ static func _tokens(source: String) -> Array[Array]:
133
144
 
134
145
 
135
146
  ## The string whose opening quote is at [param at], as [what is between its quotes, where it ends].
136
- ## A backslash keeps the character after it inside the string, raw or not, as the tokenizer reads it.
147
+ ## A backslash keeps the character after it inside the string, raw or not, as the tokenizer reads
148
+ ## it.
137
149
  static func _string(source: String, at: int) -> Array:
138
150
  var quote: String = source[at]
139
151
  var closing: String = quote.repeat(3) if source.substr(at, 3) == quote.repeat(3) else quote
@@ -1,6 +1,5 @@
1
1
  @tool
2
2
  extends Node
3
-
4
3
  ## Project settings read from the open editor, for a caller that asked for the editor's answer, and
5
4
  ## taken up by it after a write to the file.
6
5
  ##
@@ -29,7 +28,9 @@ func get_project_setting(args: Dictionary) -> Dictionary:
29
28
 
30
29
  var setting: String = str(args.get("setting", ""))
31
30
  if setting.is_empty():
32
- return {"ok": false, "error": "setting is required, or prefix to read every setting under one"}
31
+ return {
32
+ "ok": false, "error": "setting is required, or prefix to read every setting under one",
33
+ }
33
34
 
34
35
  var result: Dictionary = GdhSettingReads.one(setting, _values)
35
36
  result["ok"] = true
@@ -46,11 +47,11 @@ func adopt_project_settings(args: Dictionary) -> Dictionary:
46
47
  var file: ConfigFile = ConfigFile.new()
47
48
  var loaded: Error = file.load("res://project.godot")
48
49
  if loaded != OK:
49
- return {"ok": false, "error": "Could not read project.godot: " + error_string(loaded)}
50
+ return { "ok": false, "error": "Could not read project.godot: " + error_string(loaded) }
50
51
  var adopted: Array[String] = []
51
52
  var named: Variant = args.get("settings", [])
52
53
  if not named is Array:
53
- return {"ok": false, "error": "settings must be a list of setting names"}
54
+ return { "ok": false, "error": "settings must be a list of setting names" }
54
55
  var settings: Array = named
55
56
  for entry: Variant in settings:
56
57
  var setting: String = str(entry)
@@ -66,19 +67,21 @@ func adopt_project_settings(args: Dictionary) -> Dictionary:
66
67
  else:
67
68
  ProjectSettings.set_setting(setting, null)
68
69
  adopted.append(setting)
69
- return {"ok": true, "adopted": adopted}
70
+ return { "ok": true, "adopted": adopted }
70
71
 
71
72
 
72
73
  ## Loads the bus layout the project names into the editor's audio server, after something else
73
74
  ## wrote it: the editor holds the layout it loaded and saves that one when its audio panel changes.
74
75
  func adopt_audio_bus_layout(_args: Dictionary) -> Dictionary:
75
76
  var path: String = str(
76
- ProjectSettings.get_setting("audio/buses/default_bus_layout", "res://default_bus_layout.tres")
77
+ ProjectSettings.get_setting(
78
+ "audio/buses/default_bus_layout", "res://default_bus_layout.tres"
79
+ )
77
80
  )
78
81
  var layout: AudioBusLayout = ResourceLoader.load(
79
82
  path, "AudioBusLayout", ResourceLoader.CACHE_MODE_REPLACE
80
83
  )
81
84
  if layout == null:
82
- return {"ok": false, "error": "Could not load the bus layout at " + path}
85
+ return { "ok": false, "error": "Could not load the bus layout at " + path }
83
86
  AudioServer.set_bus_layout(layout)
84
- return {"ok": true, "layout": path}
87
+ return { "ok": true, "layout": path }