gdharness 1.3.8 → 1.3.10

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 (78) hide show
  1. package/build/cli.js +4668 -3229
  2. package/build/godot/addons/gdharness_editor/animation_reads.gd +115 -4
  3. package/build/godot/addons/gdharness_editor/declared_values.gd +272 -0
  4. package/build/godot/addons/gdharness_editor/file_reads.gd +115 -18
  5. package/build/godot/addons/gdharness_editor/headless_check.gd +49 -2
  6. package/build/godot/addons/gdharness_editor/property_paths.gd +89 -16
  7. package/build/godot/addons/gdharness_editor/property_values.gd +180 -121
  8. package/build/godot/addons/gdharness_editor/reading.gd +112 -6
  9. package/build/godot/addons/gdharness_editor/scene_file.gd +117 -174
  10. package/build/godot/addons/gdharness_editor/scene_uids.gd +107 -0
  11. package/build/godot/addons/gdharness_editor/scene_uids.gd.uid +1 -0
  12. package/build/godot/addons/gdharness_editor/serialisation.gd +19 -29
  13. package/build/godot/addons/gdharness_editor/setting_reads.gd +121 -0
  14. package/build/godot/addons/gdharness_editor/setting_reads.gd.uid +1 -0
  15. package/build/godot/addons/gdharness_editor/tool_executor.gd +39 -6
  16. package/build/godot/addons/gdharness_editor/tools/animation_states.gd +263 -0
  17. package/build/godot/addons/gdharness_editor/tools/animation_states.gd.uid +1 -0
  18. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +49 -414
  19. package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +464 -0
  20. package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd.uid +1 -0
  21. package/build/godot/addons/gdharness_editor/tools/class_tools.gd +37 -0
  22. package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd +221 -0
  23. package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd.uid +1 -0
  24. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +162 -483
  25. package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +16 -0
  26. package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +8 -4
  27. package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +2 -1
  28. package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +10 -0
  29. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +151 -8
  30. package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +7 -30
  31. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +190 -38
  32. package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd +485 -0
  33. package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd.uid +1 -0
  34. package/build/godot/addons/gdharness_editor/value_shapes.gd +128 -1
  35. package/build/godot/addons/gdharness_runtime/call_errors.gd +8 -2
  36. package/build/godot/addons/gdharness_runtime/declared_values.gd +272 -0
  37. package/build/godot/addons/gdharness_runtime/reading.gd +112 -6
  38. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +35 -3
  39. package/build/godot/addons/gdharness_runtime/runtime_capture.gd +5 -0
  40. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +188 -30
  41. package/build/godot/addons/gdharness_runtime/runtime_input.gd +107 -104
  42. package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +17 -0
  43. package/build/godot/addons/gdharness_runtime/runtime_pad.gd +13 -4
  44. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +320 -27
  45. package/build/godot/addons/gdharness_runtime/runtime_queries.gd +165 -20
  46. package/build/godot/addons/gdharness_runtime/runtime_screen.gd +49 -3
  47. package/build/godot/addons/gdharness_runtime/runtime_targets.gd +31 -2
  48. package/build/godot/addons/gdharness_runtime/runtime_values.gd +250 -10
  49. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +279 -11
  50. package/build/godot/addons/gdharness_runtime/runtime_words.gd +27 -0
  51. package/build/godot/addons/gdharness_runtime/serialisation.gd +19 -29
  52. package/build/godot/addons/gdharness_runtime/value_shapes.gd +128 -1
  53. package/build/godot/operations/animation_reads.gd +115 -4
  54. package/build/godot/operations/audio_buses.gd +93 -7
  55. package/build/godot/operations/classdb_queries.gd +119 -7
  56. package/build/godot/operations/declared_values.gd +272 -0
  57. package/build/godot/operations/dependencies.gd +154 -41
  58. package/build/godot/operations/editor_tools.gd +29 -13
  59. package/build/godot/operations/export_presets.gd +602 -4
  60. package/build/godot/operations/file_reads.gd +115 -18
  61. package/build/godot/operations/gdscript_analysis.gd +14 -0
  62. package/build/godot/operations/gdscript_authoring.gd +12 -2
  63. package/build/godot/operations/godot_operations.gd +18 -2
  64. package/build/godot/operations/import_pipeline.gd +20 -221
  65. package/build/godot/operations/input_actions.gd +127 -51
  66. package/build/godot/operations/project_config.gd +113 -68
  67. package/build/godot/operations/project_diagnostics.gd +4 -0
  68. package/build/godot/operations/project_validation.gd +266 -0
  69. package/build/godot/operations/property_paths.gd +89 -16
  70. package/build/godot/operations/property_values.gd +180 -121
  71. package/build/godot/operations/reading.gd +112 -6
  72. package/build/godot/operations/scene_connections.gd +222 -0
  73. package/build/godot/operations/serialisation.gd +19 -29
  74. package/build/godot/operations/setting_reads.gd +121 -0
  75. package/build/godot/operations/value_shapes.gd +128 -1
  76. package/build/godot/tests/listed.gd +20 -13
  77. package/build/keeper.js +1402 -707
  78. package/package.json +1 -1
@@ -22,6 +22,22 @@ static func read(scene: GdhSceneFile, node: Node) -> Dictionary:
22
22
  return answer
23
23
 
24
24
 
25
+ ## The groups of [param node], just added to this scene and put in [param adding] there, in the shape
26
+ ## [method read] answers: this scene's file does not hold the node until it is saved.
27
+ static func read_added(node: Node, adding: Array) -> Dictionary:
28
+ var all: Array = _sorted(node.get_groups())
29
+ if all.is_empty():
30
+ return {}
31
+ var answer: Dictionary = {"groups": all}
32
+ var here: Array = []
33
+ for group: String in _sorted(adding):
34
+ if not here.has(group):
35
+ here.append(group)
36
+ if here.size() != all.size():
37
+ answer["groupsHere"] = here
38
+ return answer
39
+
40
+
25
41
  ## The group names under [param key] of [param args], which the server has checked are strings, or
26
42
  ## none when not given.
27
43
  static func asked(args: Dictionary, key: String) -> Array:
@@ -7,6 +7,7 @@ extends RefCounted
7
7
  const GdhFileReads = preload("../file_reads.gd")
8
8
  const GdhPaths = preload("scene_paths.gd")
9
9
  const GdhSceneFile = preload("../scene_file.gd")
10
+ const GdhSceneUids = preload("../scene_uids.gd")
10
11
 
11
12
 
12
13
  ## Why a create at [param scene_path] is refused when a file is there already, which saving over
@@ -87,14 +88,14 @@ static func inherit(scene_path: String, base_path: String, root_name: String, sc
87
88
  "",
88
89
  (
89
90
  '[ext_resource type="PackedScene"%s path="%s" id="1_base"]'
90
- % [GdhSceneFile.reference_uid(base_path), base_path]
91
+ % [GdhSceneUids.reference_uid(base_path), base_path]
91
92
  ),
92
93
  ]
93
94
  if script != null:
94
95
  var _resourced: bool = lines.append(
95
96
  (
96
97
  '[ext_resource type="Script"%s path="%s" id="2_script"]'
97
- % [GdhSceneFile.reference_uid(script.resource_path), script.resource_path]
98
+ % [GdhSceneUids.reference_uid(script.resource_path), script.resource_path]
98
99
  )
99
100
  )
100
101
  lines.append_array(["", '[node name="%s" instance=ExtResource("1_base")]' % name])
@@ -118,13 +119,16 @@ static func inherit(scene_path: String, base_path: String, root_name: String, sc
118
119
  "%s did not load back as a scene inheriting %s, so it was removed." % [scene_path, base_path]
119
120
  }
120
121
  GdhSceneFile.refresh(scene_path)
121
- return {
122
+ var answer: Dictionary = {
122
123
  "ok": true,
123
124
  "scenePath": scene_path,
124
125
  "inherits": base_path,
125
126
  "rootNodeType": root_type,
126
127
  "rootNodeName": name
127
128
  }
129
+ if script != null:
130
+ answer["script"] = script.resource_path
131
+ return answer
128
132
 
129
133
 
130
134
  ## Makes the children of [param node], an instance in [param scene], editable or not, answering why
@@ -176,7 +180,7 @@ static func follow(changed: String, old_path: String, new_path: String, carried:
176
180
  var moves: Dictionary = {}
177
181
  if _inherits(scene_path, changed, texts):
178
182
  moves[old_path] = new_path
179
- var read: Dictionary = GdhFileReads.scene_tree(scene_path, -1, false)
183
+ var read: Dictionary = GdhFileReads.scene_tree(scene_path, -1, false, false)
180
184
  if not read.get("ok", false):
181
185
  not_followed.append({"scenePath": scene_path, "reason": str(read.get("error", ""))})
182
186
  continue
@@ -14,6 +14,7 @@ enum { UNCHANGED, REWRITTEN, DELETED }
14
14
  const GdhHeadlessCheck = preload("../headless_check.gd")
15
15
  const GdhRead = preload("../reading.gd")
16
16
  const GdhSceneFile = preload("../scene_file.gd")
17
+ const GdhSceneUids = preload("../scene_uids.gd")
17
18
  ## PROPERTY_HINT_NO_NODEPATH, which marks a property the editor's rename skips. It is not bound for
18
19
  ## scripts, and follows PROPERTY_HINT_ONESHOT in the engine's list.
19
20
  const HINT_NO_NODEPATH: int = PROPERTY_HINT_ONESHOT + 1
@@ -127,7 +128,7 @@ func put_back(scene_path: String, why: String = "") -> void:
127
128
  func answer() -> Dictionary:
128
129
  for file: String in _homes:
129
130
  var resource: Resource = _homes[file]
130
- var saving: Error = GdhSceneFile.save_keeping_uid(resource, file)
131
+ var saving: Error = GdhSceneUids.save_keeping_uid(resource, file)
131
132
  if saving == OK:
132
133
  if Engine.is_editor_hint():
133
134
  EditorInterface.get_resource_filesystem().update_file(file)
@@ -171,6 +171,16 @@ static func place(node: Node, asked: Variant) -> void:
171
171
  node.get_parent().move_child(node, GdhRead.as_int(asked))
172
172
 
173
173
 
174
+ ## Whether [param basis] has axes that are not at right angles, which a node kept in place under a
175
+ ## parent scaled unevenly across its turn ends up with. A Node2D says so through skew; a Node3D has
176
+ ## no property for it, and its position, rotation and scale read as if the axes were square.
177
+ static func sheared(basis: Basis) -> bool:
178
+ var x: Vector3 = basis.x.normalized()
179
+ var y: Vector3 = basis.y.normalized()
180
+ var z: Vector3 = basis.z.normalized()
181
+ return absf(x.dot(y)) > 0.0001 or absf(x.dot(z)) > 0.0001 or absf(y.dot(z)) > 0.0001
182
+
183
+
174
184
  ## The properties that hold where [param node] is drawn, which a reparent keeping it in place
175
185
  ## changes: a Control keeps only its position, as Node.reparent keeps it.
176
186
  static func placing(node: Node) -> Array[String]:
@@ -3,17 +3,20 @@ extends Node
3
3
 
4
4
  ## Scenes and their nodes, edited in the open editor and read back from what it holds.
5
5
 
6
+ const GdhDeclared = preload("../declared_values.gd")
6
7
  const GdhFileReads = preload("../file_reads.gd")
7
8
  const GdhRead = preload("../reading.gd")
8
9
  const GdhGroups = preload("scene_groups.gd")
10
+ const GdhHeadlessCheck = preload("../headless_check.gd")
9
11
  const GdhKin = preload("scene_kin.gd")
12
+ const GdhNavigationBake = preload("navigation_bake.gd")
10
13
  const GdhPaths = preload("scene_paths.gd")
11
14
  const GdhPlacing = preload("scene_placing.gd")
12
15
  const GdhPropertyValues = preload("../property_values.gd")
13
- const GdhResourceTools = preload("resource_tools.gd")
14
16
  const GdhSame = preload("../same_values.gd")
15
17
  const GdhSceneFile = preload("../scene_file.gd")
16
18
  const GdhSerialisation = preload("../serialisation.gd")
19
+ const GdhTilesetTools = preload("tileset_tools.gd")
17
20
 
18
21
  var _editor_plugin: EditorPlugin = null
19
22
  var _values: GdhSerialisation = GdhSerialisation.new()
@@ -204,6 +207,8 @@ func _create_of_a_class(args: Dictionary) -> Dictionary:
204
207
  var answer: Dictionary = {
205
208
  "ok": true, "scenePath": scene_path, "rootNodeType": asked_type, "rootNodeName": root_node_name
206
209
  }
210
+ if not script_path.is_empty():
211
+ answer["script"] = script_path
207
212
  if not had_folder:
208
213
  answer["madeFolder"] = scene_path.get_base_dir()
209
214
  return answer
@@ -214,10 +219,11 @@ func list_scene_nodes(args: Dictionary) -> Dictionary:
214
219
  var scene_path: String = GdhSceneFile.res_path(project_path, str(args.get("scenePath", "")))
215
220
  var depth: int = GdhRead.as_int(args.get("depth", -1), -1)
216
221
  var include_properties: bool = GdhRead.as_bool(args.get("includeProperties", false))
222
+ var include_defaults: bool = GdhRead.as_bool(args.get("includeDefaults", false))
217
223
 
218
224
  # Read as a headless engine reads it with no editor connected, so the answer does not depend on
219
225
  # whether one is.
220
- var read: Dictionary = GdhFileReads.scene_tree(scene_path, depth, include_properties)
226
+ var read: Dictionary = GdhFileReads.scene_tree(scene_path, depth, include_properties, include_defaults)
221
227
  if read.get("ok", false):
222
228
  read["unsavedInEditor"] = GdhSceneFile.unsaved(scene_path)
223
229
  return read
@@ -285,6 +291,9 @@ func add_node(args: Dictionary) -> Dictionary:
285
291
  return scene.refuse("Nothing was written: " + unsettled)
286
292
  scene.asked(new_node, properties.keys())
287
293
  var unwritten: String = _properties.write_all(new_node, properties)
294
+ scene.asked(new_node, _properties.moved_by_engine.keys())
295
+ if not unwritten.is_empty():
296
+ unwritten = GdhHeadlessCheck.setter_ran(new_node, _properties.unheld, unwritten)
288
297
  if unwritten.is_empty():
289
298
  unwritten = GdhGroups.change(scene, new_node, adding, [])
290
299
  if not unwritten.is_empty():
@@ -299,9 +308,14 @@ func add_node(args: Dictionary) -> Dictionary:
299
308
  "nodePath": new_path,
300
309
  "index": new_node.get_index(),
301
310
  }
311
+ if not properties.is_empty():
312
+ var held: Dictionary = {}
313
+ for key: Variant in properties:
314
+ held[str(key)] = _readable(new_node, _properties.read_at(new_node, str(key)))
315
+ answer["properties"] = held
302
316
  _properties.note_write(answer, new_node)
303
317
  if not adding.is_empty():
304
- answer.merge(GdhGroups.read(scene, new_node))
318
+ answer.merge(GdhGroups.read_added(new_node, adding))
305
319
  var written: Dictionary = scene.write(
306
320
  func(saved: Node) -> String:
307
321
  var unkept: String = _kept(
@@ -503,6 +517,7 @@ func reparent_node(args: Dictionary) -> Dictionary:
503
517
  var drawn_at: Dictionary = {}
504
518
  for property: String in GdhPlacing.placing(node):
505
519
  drawn_at[property] = _properties.read_at(node, property)
520
+ var held_at: Transform3D = (node as Node3D).transform if node is Node3D else Transform3D.IDENTITY
506
521
  # Unowned while it moves: a node taken out of the tree keeps its owner, and adding it under the new
507
522
  # parent warned that the owner was not above it. Node.reparent warns the same way.
508
523
  node.owner = null
@@ -517,6 +532,20 @@ func reparent_node(args: Dictionary) -> Dictionary:
517
532
  var now: Variant = _properties.read_at(node, property)
518
533
  if not GdhSame.same(now, drawn_at[property]):
519
534
  also_changed[property] = {"was": _readable(node, drawn_at[property]), "now": _readable(node, now)}
535
+ var sheared: String = ""
536
+ if node is Node3D and GdhPlacing.sheared((node as Node3D).basis):
537
+ var spatial: Node3D = node
538
+ also_changed["transform"] = {
539
+ "was": _readable(node, held_at), "now": _readable(node, spatial.transform)
540
+ }
541
+ sheared = (
542
+ (
543
+ "%s keeps where it is drawn under %s, which is scaled unevenly across the way it is turned,"
544
+ + " so its basis is sheared: no position, rotation and scale describe it, and transform is"
545
+ + " what it holds. keepGlobalTransform false keeps its own transform instead."
546
+ )
547
+ % [str(node.name), new_parent_path]
548
+ )
520
549
 
521
550
  var new_path: String = scene.path_of(node)
522
551
  var index: int = node.get_index()
@@ -546,6 +575,8 @@ func reparent_node(args: Dictionary) -> Dictionary:
546
575
  }
547
576
  if not also_changed.is_empty():
548
577
  answer["alsoChanged"] = also_changed
578
+ if not sheared.is_empty():
579
+ answer["sheared"] = sheared
549
580
  if new_path != old_path:
550
581
  answer.merge(GdhKin.follow(scene_path, old_path, new_path, carried))
551
582
  return answer
@@ -571,9 +602,8 @@ func set_node_properties(args: Dictionary) -> Dictionary:
571
602
  var node: Node = GdhSceneFile.node_at(scene.root, node_path)
572
603
  if not node:
573
604
  return scene.refuse(scene.not_found("Node not found: " + node_path, node_path))
574
- var foreign: String = scene.foreign(node)
575
- if not foreign.is_empty():
576
- return scene.refuse(foreign)
605
+ # A rename is refused for the scene that makes the node before anything else, since editable
606
+ # children would let this scene keep values on it and still never rename it.
577
607
  if properties.has("name") and node != scene.root:
578
608
  var fixed: String = scene.fixed(node)
579
609
  if not fixed.is_empty():
@@ -581,6 +611,9 @@ func set_node_properties(args: Dictionary) -> Dictionary:
581
611
  var unusable: String = scene.unusable_name(node.get_parent(), str(properties["name"]), node)
582
612
  if not unusable.is_empty():
583
613
  return scene.refuse(unusable)
614
+ var foreign: String = scene.foreign(node)
615
+ if not foreign.is_empty():
616
+ return scene.refuse(foreign)
584
617
 
585
618
  var dropped: Dictionary = {}
586
619
  if editable != null:
@@ -593,8 +626,13 @@ func set_node_properties(args: Dictionary) -> Dictionary:
593
626
  var old_path: String = scene.path_of(node)
594
627
  scene.asked(node, properties.keys())
595
628
  var refused_property: String = _properties.write_all(node, properties)
629
+ scene.asked(node, _properties.moved_by_engine.keys())
596
630
  if not refused_property.is_empty():
597
- return scene.refuse(GdhSceneFile.missing_on(node, refused_property))
631
+ return scene.refuse(
632
+ GdhHeadlessCheck.setter_ran(
633
+ node, _properties.unheld, GdhSceneFile.missing_on(node, refused_property)
634
+ )
635
+ )
598
636
  var ungrouped: String = GdhGroups.change(scene, node, adding, removing)
599
637
  if not ungrouped.is_empty():
600
638
  return scene.refuse(ungrouped)
@@ -607,6 +645,12 @@ func set_node_properties(args: Dictionary) -> Dictionary:
607
645
  _properties.note_write(answer, node)
608
646
  if not adding.is_empty() or not removing.is_empty():
609
647
  answer["groups"] = GdhGroups.read(scene, node).get("groups", [])
648
+ var named: Array[String] = []
649
+ for key: Variant in properties:
650
+ var property: String = str(key).get_slice(":", 0)
651
+ if property != "name" and property in node and not named.has(property):
652
+ named.append(property)
653
+ var source: String = scene.values_from(node, old_path)
610
654
  var carried: GdhPaths = GdhPaths.new()
611
655
  var _rewritten: int = carried.carry(scene, {old_path: new_path} if new_path != old_path else {})
612
656
  var written: Dictionary = scene.write(
@@ -630,11 +674,48 @@ func set_node_properties(args: Dictionary) -> Dictionary:
630
674
  if not dropped.is_empty():
631
675
  answer["droppedValues"] = dropped["values"]
632
676
  answer["droppedNodes"] = dropped["nodes"]
677
+ _note_left_out(answer, scene.written_pack, new_path, named, source)
633
678
  if new_path != old_path:
634
679
  answer.merge(GdhKin.follow(scene_path, old_path, new_path, carried))
635
680
  return answer
636
681
 
637
682
 
683
+ ## Notes under notStored each of [param named] the file saved from [param pack] does not set on the
684
+ ## node at [param at]: a set gave it the value the node has without it, so the packer leaves it out,
685
+ ## as the editor does, and it follows [param source], or its default, from then on.
686
+ static func _note_left_out(
687
+ answer: Dictionary, pack: PackedScene, at: String, named: Array[String], source: String
688
+ ) -> void:
689
+ var stored: Dictionary = {}
690
+ var state: SceneState = pack.get_state()
691
+ for index: int in state.get_node_count():
692
+ if str(state.get_node_path(index)).trim_prefix("./") == at:
693
+ for property: int in state.get_node_property_count(index):
694
+ stored[str(state.get_node_property_name(index, property))] = true
695
+ var left: Array[String] = []
696
+ for asked: String in named:
697
+ if not stored.has(asked):
698
+ left.append(asked)
699
+ if left.is_empty():
700
+ return
701
+ answer["notStored"] = left
702
+ var names: String = ", ".join(left)
703
+ var verb: String = "holds" if left.size() == 1 else "hold"
704
+ var pronoun: String = "it" if left.size() == 1 else "them"
705
+ if source.is_empty():
706
+ answer["notStoredNote"] = (
707
+ "%s now %s the default, so the file does not store %s." % [names, verb, pronoun]
708
+ )
709
+ else:
710
+ answer["notStoredNote"] = (
711
+ (
712
+ "%s now %s what %s gives this node, so the file does not store %s, and a change there reaches "
713
+ + "this node."
714
+ )
715
+ % [names, verb, source, pronoun]
716
+ )
717
+
718
+
638
719
  func get_node_properties(args: Dictionary) -> Dictionary:
639
720
  var project_path: String = str(args.get("projectPath", ""))
640
721
  var scene_path: String = GdhSceneFile.res_path(project_path, str(args.get("scenePath", "")))
@@ -657,11 +738,55 @@ func get_node_properties(args: Dictionary) -> Dictionary:
657
738
  # names this scene's file sets are always answered.
658
739
  var set_here: Dictionary = scene.set_here(node)
659
740
  var props: Dictionary = _properties_of(node, include_defaults, set_here)
660
- var cells: Dictionary = GdhResourceTools.cells_of(node)
741
+ var cells: Dictionary = GdhTilesetTools.cells_of(node)
661
742
  var at: String = scene.path_of(node)
662
743
  var here: Array = set_here.keys()
663
744
  here.sort()
664
745
  var answer: Dictionary = {"ok": true, "nodePath": at, "properties": props, "setHere": here}
746
+ # A Node3D stores only its transform, a matrix, while a set takes the position, rotation and
747
+ # scale the inspector shows, so a node turned 45 degrees down read back as nine numbers. A
748
+ # sheared one has no such three, and is left to its transform.
749
+ var placed: Node3D = node as Node3D
750
+ var rebuilt: Transform3D = Transform3D()
751
+ if placed != null:
752
+ var turned: Basis = Basis.from_euler(placed.rotation, placed.rotation_order)
753
+ rebuilt = Transform3D(turned * Basis.from_scale(placed.scale), placed.position)
754
+ if placed != null and props.has("transform") and rebuilt.is_equal_approx(placed.transform):
755
+ # Rounded, since a node never scaled came back with a scale of 0.99999994 out of the matrix;
756
+ # the transform beside it is exact.
757
+ var step: Vector3 = Vector3.ONE * 0.00001
758
+ answer["placement"] = {
759
+ "position": _readable(node, placed.position.snapped(step)),
760
+ "rotation_degrees": _readable(node, placed.rotation_degrees.snapped(step)),
761
+ "scale": _readable(node, placed.scale.snapped(step)),
762
+ }
763
+ # A player's getter answers Master for a bus the layout does not have, while the file keeps the
764
+ # name, so a player left on a renamed or removed bus read as one put on Master.
765
+ var bus: String = str(set_here.get("bus", ""))
766
+ var plays: bool = node is AudioStreamPlayer or node is AudioStreamPlayer2D or node is AudioStreamPlayer3D
767
+ if plays and not bus.is_empty() and AudioServer.get_bus_index(bus) == -1:
768
+ props["bus"] = bus
769
+ var buses: PackedStringArray = []
770
+ for index: int in AudioServer.bus_count:
771
+ var _added: bool = buses.append(AudioServer.get_bus_name(index))
772
+ answer["busNote"] = (
773
+ (
774
+ "%s names the bus %s, which the project's bus layout does not have, so it plays on %s; "
775
+ + "the buses are %s."
776
+ )
777
+ % [str(node.name) if at == "." else at, bus, AudioServer.get_bus_name(0), ", ".join(buses)]
778
+ )
779
+ # A mask reads as a number, and which layers it holds, under the names the project gives them,
780
+ # is what a caller reads it for.
781
+ var layers: Dictionary = {}
782
+ for declared: Dictionary in node.get_property_list():
783
+ var hint: int = GdhRead.as_int(declared.get("hint", PROPERTY_HINT_NONE))
784
+ var named: String = str(declared.get("name", ""))
785
+ var mask: Variant = node.get(named) if props.has(named) else null
786
+ if hint in GdhDeclared.LAYER_NAMES and mask is int:
787
+ layers[named] = GdhDeclared.layers_in(hint, GdhRead.as_int(mask))
788
+ if not layers.is_empty():
789
+ answer["layers"] = layers
665
790
  answer.merge(GdhSceneFile.class_of(node))
666
791
  answer.merge(GdhGroups.read(scene, node))
667
792
  var from: String = scene.values_from(node, at)
@@ -684,6 +809,24 @@ func get_node_properties(args: Dictionary) -> Dictionary:
684
809
  return answer
685
810
 
686
811
 
812
+ func bake_navigation(args: Dictionary) -> Dictionary:
813
+ var project_path: String = str(args.get("projectPath", ""))
814
+ var scene_path: String = GdhSceneFile.res_path(project_path, str(args.get("scenePath", "")))
815
+ var node_path: String = str(args.get("nodePath", "."))
816
+ var opened: Array = _open(scene_path, true)
817
+ var scene: GdhSceneFile = opened[0]
818
+ var refused: Dictionary = opened[1]
819
+ if not refused.is_empty():
820
+ return refused
821
+ var node: Node = GdhSceneFile.node_at(scene.root, node_path)
822
+ if not node:
823
+ return scene.refuse(scene.not_found("Node not found: " + node_path, node_path))
824
+ var foreign: String = scene.foreign(node)
825
+ if not foreign.is_empty():
826
+ return scene.refuse(foreign)
827
+ return GdhNavigationBake.bake(scene, node)
828
+
829
+
687
830
  func save_scene(args: Dictionary) -> Dictionary:
688
831
  var project_path: String = str(args.get("projectPath", ""))
689
832
  var scene_path: String = GdhSceneFile.res_path(project_path, str(args.get("scenePath", "")))
@@ -10,12 +10,12 @@ extends Node
10
10
  ## of the two a caller wants is theirs to say, so the server only comes here when asked, and the
11
11
  ## difference is the reason the argument exists rather than an inconvenience to paper over.
12
12
  ##
13
- ## Every value goes through the same serialiser the headless answer uses, because the two are one
13
+ ## Every answer is built by the same reads the headless answer uses, because the two are one
14
14
  ## question asked of two places: a value spelled differently here would read as the editor and the
15
15
  ## file disagreeing about something they agree about.
16
16
 
17
- const GdhRead = preload("../reading.gd")
18
17
  const GdhSerialisation = preload("../serialisation.gd")
18
+ const GdhSettingReads = preload("../setting_reads.gd")
19
19
 
20
20
  var _values: GdhSerialisation = GdhSerialisation.new()
21
21
 
@@ -23,42 +23,19 @@ var _values: GdhSerialisation = GdhSerialisation.new()
23
23
  func get_project_setting(args: Dictionary) -> Dictionary:
24
24
  var prefix: String = str(args.get("prefix", ""))
25
25
  if not prefix.is_empty():
26
- return _settings_under(prefix)
26
+ var family: Dictionary = GdhSettingReads.under(prefix, _values)
27
+ family["ok"] = true
28
+ return family
27
29
 
28
30
  var setting: String = str(args.get("setting", ""))
29
31
  if setting.is_empty():
30
32
  return {"ok": false, "error": "setting is required, or prefix to read every setting under one"}
31
33
 
32
- var exists: bool = ProjectSettings.has_setting(setting)
33
- var result: Dictionary = {"ok": true, "setting_path": setting, "exists": exists, "value": null}
34
- if exists:
35
- result["value"] = _values.serialize_value(ProjectSettings.get_setting(setting))
36
- else:
37
- result["message"] = "Setting does not exist"
34
+ var result: Dictionary = GdhSettingReads.one(setting, _values)
35
+ result["ok"] = true
38
36
  return result
39
37
 
40
38
 
41
- ## Every setting under [prefix] with the type the engine registers for each, which is the half a
42
- ## name hides: a family of levels can hold a setting that is a bool, and writing a level over it
43
- ## looks like it worked.
44
- func _settings_under(prefix: String) -> Dictionary:
45
- var found: Array[Dictionary] = []
46
- for property: Dictionary in ProjectSettings.get_property_list():
47
- var setting: String = str(property.get("name", ""))
48
- if not setting.begins_with(prefix):
49
- continue
50
- var kind: int = GdhRead.as_int(property.get("type", TYPE_NIL), TYPE_NIL)
51
- var described: Dictionary = {
52
- "setting": setting,
53
- "type": type_string(kind),
54
- "value": _values.serialize_value(ProjectSettings.get_setting(setting)),
55
- }
56
- found.append(described)
57
-
58
- found.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return a["setting"] < b["setting"])
59
- return {"ok": true, "prefix": prefix, "count": found.size(), "settings": found}
60
-
61
-
62
39
  ## Takes the named settings from project.godot as it is on disk, after something else wrote it.
63
40
  ##
64
41
  ## The editor loads the file once, at startup. Written from outside, it goes on holding the old