gdharness 1.2.40 → 1.2.42

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 (31) hide show
  1. package/build/cli.js +103 -38
  2. package/build/godot/addons/gdharness_editor/animation_reads.gd +14 -2
  3. package/build/godot/addons/gdharness_editor/bridge_client.gd +2 -1
  4. package/build/godot/addons/gdharness_editor/file_reads.gd +134 -10
  5. package/build/godot/addons/gdharness_editor/headless_check.gd +56 -15
  6. package/build/godot/addons/gdharness_editor/property_values.gd +1 -1
  7. package/build/godot/addons/gdharness_editor/reading.gd +40 -0
  8. package/build/godot/addons/gdharness_editor/scene_file.gd +215 -12
  9. package/build/godot/addons/gdharness_editor/serialisation.gd +5 -1
  10. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +39 -5
  11. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +68 -15
  12. package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +346 -0
  13. package/build/godot/addons/gdharness_editor/tools/scene_kin.gd.uid +1 -0
  14. package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +438 -0
  15. package/build/godot/addons/gdharness_editor/tools/scene_paths.gd.uid +1 -0
  16. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +102 -60
  17. package/build/godot/addons/gdharness_runtime/reading.gd +40 -0
  18. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +3 -1
  19. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +2 -2
  20. package/build/godot/addons/gdharness_runtime/runtime_targets.gd +11 -6
  21. package/build/godot/addons/gdharness_runtime/runtime_values.gd +3 -1
  22. package/build/godot/addons/gdharness_runtime/serialisation.gd +5 -1
  23. package/build/godot/operations/animation_reads.gd +14 -2
  24. package/build/godot/operations/editor_tools.gd +28 -1
  25. package/build/godot/operations/file_reads.gd +134 -10
  26. package/build/godot/operations/godot_operations.gd +2 -1
  27. package/build/godot/operations/property_values.gd +1 -1
  28. package/build/godot/operations/reading.gd +40 -0
  29. package/build/godot/operations/serialisation.gd +5 -1
  30. package/build/index.js +103 -38
  31. package/package.json +1 -1
@@ -5,22 +5,14 @@ extends Node
5
5
 
6
6
  const FileReads = preload("../file_reads.gd")
7
7
  const Read = preload("../reading.gd")
8
+ const Kin = preload("scene_kin.gd")
9
+ const Paths = preload("scene_paths.gd")
8
10
  const Placing = preload("scene_placing.gd")
9
11
  const PropertyValues = preload("../property_values.gd")
10
12
  const ResourceTools = preload("resource_tools.gd")
11
13
  const SceneFile = preload("../scene_file.gd")
12
14
  const Serialisation = preload("../serialisation.gd")
13
15
 
14
- ## The names Object.ConnectFlags gives each bit. The number alone left a reader guessing that 2 is
15
- ## the persistence every connection in a saved scene carries.
16
- const CONNECT_FLAG_NAMES: Dictionary[int, String] = {
17
- Object.CONNECT_DEFERRED: "deferred",
18
- Object.CONNECT_PERSIST: "persist",
19
- Object.CONNECT_ONE_SHOT: "one_shot",
20
- Object.CONNECT_REFERENCE_COUNTED: "reference_counted",
21
- Object.CONNECT_APPEND_SOURCE_OBJECT: "append_source_object",
22
- }
23
-
24
16
  var _editor_plugin: EditorPlugin = null
25
17
  var _values: Serialisation = Serialisation.new()
26
18
  var _properties: PropertyValues = PropertyValues.new()
@@ -30,19 +22,6 @@ func set_editor_plugin(plugin: EditorPlugin) -> void:
30
22
  _editor_plugin = plugin
31
23
 
32
24
 
33
- ## Each bit set in [param flags] by name, and whatever bits are left unnamed as one number.
34
- static func _flag_names(flags: int) -> Array[String]:
35
- var names: Array[String] = []
36
- var rest: int = flags
37
- for bit: int in CONNECT_FLAG_NAMES:
38
- if flags & bit:
39
- names.append(CONNECT_FLAG_NAMES[bit])
40
- rest &= ~bit
41
- if rest != 0:
42
- names.append(str(rest))
43
- return names
44
-
45
-
46
25
  func _ensure_res_path(path: String) -> String:
47
26
  if not path.begins_with("res://"):
48
27
  return "res://" + path
@@ -111,7 +90,7 @@ func _kept(scene: SceneFile, saved: Node, node: Node, names: Array) -> String:
111
90
  "%s.%s would load from the file as %s: the scene does not keep it, because the property "
112
91
  + "is not one a scene stores or the node belongs to another scene."
113
92
  )
114
- % [at, property, JSON.stringify(_values.serialize_value(loaded))]
93
+ % [at, property, JSON.stringify(_values.serialize_value(loaded), "", false)]
115
94
  )
116
95
  return ""
117
96
 
@@ -175,17 +154,42 @@ func _collect_nodes_recursive(node: Node, path: String, out_nodes: Array) -> voi
175
154
  _collect_nodes_recursive(child, child_path, out_nodes)
176
155
 
177
156
 
157
+ func _inherited(args: Dictionary) -> Dictionary:
158
+ var project_path: String = str(args.get("projectPath", ""))
159
+ var script_path: String = str(args.get("script", ""))
160
+ return Kin.inherited(
161
+ args,
162
+ _new_scene_path(args),
163
+ _to_scene_res_path(project_path, str(args.get("inherits", ""))),
164
+ "" if script_path.is_empty() else _to_scene_res_path(project_path, script_path)
165
+ )
166
+
167
+
168
+ ## The res:// path a create in [param args] writes, given .tscn when it has no extension, or "".
169
+ func _new_scene_path(args: Dictionary) -> String:
170
+ var scene_path: String = _to_scene_res_path(
171
+ str(args.get("projectPath", "")), str(args.get("scenePath", ""))
172
+ )
173
+ if scene_path == "res://":
174
+ return ""
175
+ return scene_path if scene_path.ends_with(".tscn") else scene_path + ".tscn"
176
+
177
+
178
178
  func create_scene(args: Dictionary) -> Dictionary:
179
+ if str(args.get("inherits", "")).is_empty():
180
+ return _create_of_a_class(args)
181
+ return _inherited(args)
182
+
183
+
184
+ func _create_of_a_class(args: Dictionary) -> Dictionary:
179
185
  var project_path: String = str(args.get("projectPath", ""))
180
- var scene_path: String = _to_scene_res_path(project_path, str(args.get("scenePath", "")))
186
+ var scene_path: String = _new_scene_path(args)
181
187
  var root_node_type: String = str(args.get("rootNodeType", "Node"))
182
188
  var root_node_name: String = str(args.get("rootNodeName", root_node_type))
183
189
  var script_path: String = str(args.get("script", ""))
184
190
 
185
- if scene_path == "res://":
191
+ if scene_path.is_empty():
186
192
  return {"ok": false, "error": "Missing scenePath"}
187
- if not scene_path.ends_with(".tscn"):
188
- scene_path += ".tscn"
189
193
  var asked_type: String = root_node_type
190
194
  var made: Dictionary = SceneFile.made_as(
191
195
  root_node_type,
@@ -208,19 +212,8 @@ func create_scene(args: Dictionary) -> Dictionary:
208
212
  var unfit: String = SceneFile.unfit_name(root_node_name)
209
213
  if not unfit.is_empty():
210
214
  return {"ok": false, "error": unfit}
211
- # Saving over a scene that is there replaced it, nodes and all, and answered as a new scene.
212
215
  if FileAccess.file_exists(scene_path):
213
- return {
214
- "ok": false,
215
- "error":
216
- (
217
- (
218
- "%s already exists, and scene_create op=create would replace everything in it. Edit it "
219
- + "with scene_node, or create the scene under another path."
220
- )
221
- % scene_path
222
- )
223
- }
216
+ return Kin.taken(scene_path)
224
217
 
225
218
  var root: Node = ClassDB.instantiate(root_node_type)
226
219
  root.name = root_node_name
@@ -312,7 +305,7 @@ func add_node(args: Dictionary) -> Dictionary:
312
305
  var root: Node = scene.root
313
306
  var parent: Node = _find_node(root, parent_node_path)
314
307
  if not parent:
315
- return scene.refuse("Parent node not found: " + parent_node_path)
308
+ return scene.refuse(scene.not_found("Parent node not found: " + parent_node_path, parent_node_path))
316
309
  var foreign: String = scene.foreign(parent)
317
310
  if not foreign.is_empty():
318
311
  return scene.refuse(foreign)
@@ -387,7 +380,7 @@ func delete_node(args: Dictionary) -> Dictionary:
387
380
 
388
381
  var node: Node = _find_node(scene.root, node_path)
389
382
  if not node:
390
- return scene.refuse("Node not found: " + node_path)
383
+ return scene.refuse(scene.not_found("Node not found: " + node_path, node_path))
391
384
  var fixed: String = scene.fixed(node)
392
385
  if not fixed.is_empty():
393
386
  return scene.refuse(fixed)
@@ -395,6 +388,8 @@ func delete_node(args: Dictionary) -> Dictionary:
395
388
  var at: String = scene.path_of(node)
396
389
  node.get_parent().remove_child(node)
397
390
  node.free()
391
+ var carried: Paths = Paths.new()
392
+ var _rewritten: int = carried.carry(scene, {at: ""})
398
393
 
399
394
  var written: Dictionary = scene.write(
400
395
  func(saved: Node) -> String:
@@ -405,7 +400,9 @@ func delete_node(args: Dictionary) -> Dictionary:
405
400
  if not written.is_empty():
406
401
  return written
407
402
 
408
- return {"ok": true, "deletedNodePath": at}
403
+ var answer: Dictionary = {"ok": true, "deletedNodePath": at}
404
+ answer.merge(Kin.follow(scene_path, at, "", carried))
405
+ return answer
409
406
 
410
407
 
411
408
  func duplicate_node(args: Dictionary) -> Dictionary:
@@ -427,7 +424,7 @@ func duplicate_node(args: Dictionary) -> Dictionary:
427
424
  var root: Node = scene.root
428
425
  var source: Node = _find_node(root, node_path)
429
426
  if not source:
430
- return scene.refuse("Node not found: " + node_path)
427
+ return scene.refuse(scene.not_found("Node not found: " + node_path, node_path))
431
428
  if source == root:
432
429
  return scene.refuse("The root of %s cannot be copied into its own scene." % scene_path)
433
430
  var foreign: String = scene.foreign(source)
@@ -438,7 +435,7 @@ func duplicate_node(args: Dictionary) -> Dictionary:
438
435
  if not parent_path.is_empty():
439
436
  target_parent = _find_node(root, parent_path)
440
437
  if not target_parent:
441
- return scene.refuse("Parent not found: " + parent_path)
438
+ return scene.refuse(scene.not_found("Parent not found: " + parent_path, parent_path))
442
439
  foreign = scene.foreign(target_parent)
443
440
  if not foreign.is_empty():
444
441
  return scene.refuse(foreign)
@@ -458,6 +455,7 @@ func duplicate_node(args: Dictionary) -> Dictionary:
458
455
  scene.copied(copies)
459
456
 
460
457
  var new_path: String = scene.path_of(duplicated_node)
458
+ var placed: Array = [scene.path_of(target_parent), duplicated_node.get_index()]
461
459
  var written: Dictionary = scene.write(
462
460
  func(saved: Node) -> String:
463
461
  if saved.get_node_or_null(new_path) == null:
@@ -467,7 +465,14 @@ func duplicate_node(args: Dictionary) -> Dictionary:
467
465
  if not written.is_empty():
468
466
  return written
469
467
 
470
- return {"ok": true, "nodePath": node_path, "newName": new_name, "newNodePath": new_path}
468
+ return {
469
+ "ok": true,
470
+ "nodePath": node_path,
471
+ "newName": new_name,
472
+ "newNodePath": new_path,
473
+ "parentNodePath": placed[0],
474
+ "index": placed[1],
475
+ }
471
476
 
472
477
 
473
478
  func reparent_node(args: Dictionary) -> Dictionary:
@@ -491,9 +496,9 @@ func reparent_node(args: Dictionary) -> Dictionary:
491
496
  var node: Node = _find_node(root, node_path)
492
497
  var new_parent: Node = _find_node(root, new_parent_path)
493
498
  if not node:
494
- return scene.refuse("Node not found: " + node_path)
499
+ return scene.refuse(scene.not_found("Node not found: " + node_path, node_path))
495
500
  if not new_parent:
496
- return scene.refuse("New parent not found: " + new_parent_path)
501
+ return scene.refuse(scene.not_found("New parent not found: " + new_parent_path, new_parent_path))
497
502
  var fixed: String = scene.fixed(node)
498
503
  if not fixed.is_empty():
499
504
  return scene.refuse(fixed)
@@ -525,6 +530,8 @@ func reparent_node(args: Dictionary) -> Dictionary:
525
530
 
526
531
  var new_path: String = scene.path_of(node)
527
532
  var index: int = node.get_index()
533
+ var carried: Paths = Paths.new()
534
+ var _rewritten: int = carried.carry(scene, {old_path: new_path} if new_path != old_path else {})
528
535
  var written: Dictionary = scene.write(
529
536
  func(saved: Node) -> String:
530
537
  var copy: Node = saved.get_node_or_null(new_path)
@@ -537,15 +544,19 @@ func reparent_node(args: Dictionary) -> Dictionary:
537
544
  return ""
538
545
  )
539
546
  if not written.is_empty():
547
+ carried.put_back(scene_path)
540
548
  return written
541
549
 
542
- return {
550
+ var answer: Dictionary = {
543
551
  "ok": true,
544
552
  "nodePath": old_path,
545
553
  "newParentPath": new_parent_path,
546
554
  "newNodePath": new_path,
547
555
  "index": index,
548
556
  }
557
+ if new_path != old_path:
558
+ answer.merge(Kin.follow(scene_path, old_path, new_path, carried))
559
+ return answer
549
560
 
550
561
 
551
562
  func set_node_properties(args: Dictionary) -> Dictionary:
@@ -553,6 +564,9 @@ func set_node_properties(args: Dictionary) -> Dictionary:
553
564
  var scene_path: String = _to_scene_res_path(project_path, str(args.get("scenePath", "")))
554
565
  var node_path: String = str(args.get("nodePath", "."))
555
566
  var properties: Dictionary = _parse_properties_arg(args.get("properties", {}))
567
+ var editable: Variant = args.get("editableChildren")
568
+ if properties.is_empty() and editable == null:
569
+ return {"ok": false, "error": "set takes properties, editableChildren, or both."}
556
570
 
557
571
  var opened: Array = _open(scene_path, true)
558
572
  var scene: SceneFile = opened[0]
@@ -562,7 +576,7 @@ func set_node_properties(args: Dictionary) -> Dictionary:
562
576
 
563
577
  var node: Node = _find_node(scene.root, node_path)
564
578
  if not node:
565
- return scene.refuse("Node not found: " + node_path)
579
+ return scene.refuse(scene.not_found("Node not found: " + node_path, node_path))
566
580
  var foreign: String = scene.foreign(node)
567
581
  if not foreign.is_empty():
568
582
  return scene.refuse(foreign)
@@ -574,6 +588,15 @@ func set_node_properties(args: Dictionary) -> Dictionary:
574
588
  if not unusable.is_empty():
575
589
  return scene.refuse(unusable)
576
590
 
591
+ var dropped: Dictionary = {}
592
+ if editable != null:
593
+ var not_an_instance: String = Kin.set_editable(scene, node, Read.as_bool(editable))
594
+ if not not_an_instance.is_empty():
595
+ return scene.refuse(not_an_instance)
596
+ if not Read.as_bool(editable):
597
+ dropped = scene.held_under(scene.path_of(node))
598
+
599
+ var old_path: String = scene.path_of(node)
577
600
  scene.asked(node, properties.keys())
578
601
  var refused_property: String = _properties.write_all(node, properties)
579
602
  if not refused_property.is_empty():
@@ -583,13 +606,29 @@ func set_node_properties(args: Dictionary) -> Dictionary:
583
606
  for key: Variant in properties:
584
607
  held[str(key)] = _readable(node, _properties.read_at(node, str(key)))
585
608
  var new_path: String = scene.path_of(node)
609
+ var carried: Paths = Paths.new()
610
+ var _rewritten: int = carried.carry(scene, {old_path: new_path} if new_path != old_path else {})
586
611
  var written: Dictionary = scene.write(
587
- func(saved: Node) -> String: return _kept(scene, saved, node, properties.keys())
612
+ func(saved: Node) -> String:
613
+ var unkept: String = _kept(scene, saved, node, properties.keys())
614
+ if unkept.is_empty() and editable != null:
615
+ if saved.is_editable_instance(saved.get_node(new_path)) != Read.as_bool(editable):
616
+ return "%s would load with editableChildren %s." % [new_path, not Read.as_bool(editable)]
617
+ return unkept
588
618
  )
589
619
  if not written.is_empty():
620
+ carried.put_back(scene_path)
590
621
  return written
591
622
 
592
- return {"ok": true, "nodePath": new_path, "properties": held}
623
+ var answer: Dictionary = {"ok": true, "nodePath": new_path, "properties": held}
624
+ if editable != null:
625
+ answer["editableChildren"] = Read.as_bool(editable)
626
+ if not dropped.is_empty():
627
+ answer["droppedValues"] = dropped["values"]
628
+ answer["droppedNodes"] = dropped["nodes"]
629
+ if new_path != old_path:
630
+ answer.merge(Kin.follow(scene_path, old_path, new_path, carried))
631
+ return answer
593
632
 
594
633
 
595
634
  func get_node_properties(args: Dictionary) -> Dictionary:
@@ -606,7 +645,7 @@ func get_node_properties(args: Dictionary) -> Dictionary:
606
645
 
607
646
  var node: Node = _find_node(scene.root, node_path)
608
647
  if not node:
609
- return scene.refuse("Node not found: " + node_path)
648
+ return scene.refuse(scene.not_found("Node not found: " + node_path, node_path))
610
649
 
611
650
  # Against the default the node would have in no scene: its script's for what the script
612
651
  # declares, the class's for the rest. Compared with a bare instance of the class, every exported
@@ -614,8 +653,11 @@ func get_node_properties(args: Dictionary) -> Dictionary:
614
653
  # names this scene's file sets are always answered.
615
654
  var props: Dictionary = _properties_of(node, include_defaults, scene.set_here(node))
616
655
  var cells: Dictionary = ResourceTools.cells_of(node)
617
- scene.close()
618
656
  var answer: Dictionary = {"ok": true, "nodePath": node_path, "properties": props}
657
+ if node != scene.root and node.owner != null and not node.scene_file_path.is_empty():
658
+ answer["instanceOf"] = node.scene_file_path
659
+ answer["editableChildren"] = scene.root.is_editable_instance(node)
660
+ scene.close()
619
661
  # Only an editor holds anything unsaved; a headless engine answering false would say it looked.
620
662
  if Engine.is_editor_hint():
621
663
  answer["unsavedInEditor"] = SceneFile.unsaved(scene_path)
@@ -770,7 +812,7 @@ func connect_signal(args: Dictionary) -> Dictionary:
770
812
  "targetNodePath": to,
771
813
  "methodName": method_name,
772
814
  "flags": saved_flags[0],
773
- "flagNames": _flag_names(Read.as_int(saved_flags[0])),
815
+ "flagNames": SceneFile.flag_names(Read.as_int(saved_flags[0])),
774
816
  # Connected either way, as the editor's own dialog allows, since the method may be written
775
817
  # next; a caller told nothing learnt of a misspelt one only when the signal fired.
776
818
  "methodFound": method_found
@@ -781,7 +823,7 @@ func connect_signal(args: Dictionary) -> Dictionary:
781
823
  if flags_before >= 0:
782
824
  answer["alreadyConnected"] = true
783
825
  if flags_before != Read.as_int(saved_flags[0]):
784
- answer["flagsBefore"] = _flag_names(flags_before)
826
+ answer["flagsBefore"] = SceneFile.flag_names(flags_before)
785
827
  return answer
786
828
 
787
829
 
@@ -792,9 +834,9 @@ func _connection_ends(
792
834
  var source: Node = _find_node(scene.root, source_node_path)
793
835
  var target: Node = _find_node(scene.root, target_node_path)
794
836
  if not source:
795
- return [null, "Source node not found: " + source_node_path]
837
+ return [null, scene.not_found("Source node not found: " + source_node_path, source_node_path)]
796
838
  if not target:
797
- return [null, "Target node not found: " + target_node_path]
839
+ return [null, scene.not_found("Target node not found: " + target_node_path, target_node_path)]
798
840
  # pack drops a connection with either end inside an instanced scene, and the answer said it was
799
841
  # made or removed.
800
842
  for end: Node in [source, target]:
@@ -897,7 +939,7 @@ func list_connections(args: Dictionary) -> Dictionary:
897
939
  if not filter_path.is_empty():
898
940
  filtered = _find_node(root, filter_path)
899
941
  if filtered == null:
900
- return scene.refuse("Node not found: " + filter_path)
942
+ return scene.refuse(scene.not_found("Node not found: " + filter_path, filter_path))
901
943
  var nodes: Array = []
902
944
  _collect_nodes_recursive(root, ".", nodes)
903
945
 
@@ -934,7 +976,7 @@ func list_connections(args: Dictionary) -> Dictionary:
934
976
  "targetNodePath": target_path,
935
977
  "methodName": str(callable.get_method()),
936
978
  "flags": flags,
937
- "flagNames": _flag_names(flags),
979
+ "flagNames": SceneFile.flag_names(flags),
938
980
  # False for a connection an instanced scene makes inside itself, which only that
939
981
  # scene can change.
940
982
  "editable": editable
@@ -22,6 +22,9 @@ extends RefCounted
22
22
  # number was expected. Absent keys never reach here: the callers pass their own default to
23
23
  # `Dictionary.get`, so what arrives is that default.
24
24
 
25
+ ## How many of a node's children a refusal for a path missing below it names.
26
+ const NAMED_CHILDREN: int = 12
27
+
25
28
 
26
29
  static func as_int(value: Variant, fallback: int = 0) -> int:
27
30
  if value is int:
@@ -99,3 +102,40 @@ static func json_or_null(text: String) -> Variant:
99
102
  if json.parse(text) != OK:
100
103
  return null
101
104
  return json.data
105
+
106
+
107
+ ## [param said], the refusal for [param node_path] found nowhere in a scene whose root is called
108
+ ## [param root_name], with how paths are read when the path starts with that name and the root has
109
+ ## no child called that. Paths go from the root, so its own name there names a child of it, and a
110
+ ## caller who read the root as Main in a tree wrote Main/Body and was told only that nothing is there.
111
+ static func from_the_root(said: String, node_path: String, root_name: String) -> String:
112
+ var first: String = node_path.get_slice("/", 0)
113
+ if root_name.is_empty() or first != root_name:
114
+ return said
115
+ var rest: String = node_path.substr(first.length() + 1)
116
+ var how: String = 'Paths go from the root, %s, whose own path is "."' % root_name
117
+ if not rest.is_empty():
118
+ how += ', so this one is "%s"' % rest
119
+ return "%s%s %s." % [said, "" if said.ends_with(".") else ".", how]
120
+
121
+
122
+ ## [param said], the refusal for a path a scene does not have, with where the path leaves it: the
123
+ ## last node of the path it has, [param reached] from the root, called [param root_name], when that
124
+ ## is empty; the name below it that is missing; and [param children], the names that node has, as
125
+ ## after a node was moved or renamed. Names up to [constant NAMED_CHILDREN] of them.
126
+ static func missing_below(
127
+ said: String, root_name: String, reached: PackedStringArray, missing: String, children: PackedStringArray
128
+ ) -> String:
129
+ var shown: String = ", ".join(children.slice(0, NAMED_CHILDREN))
130
+ if children.size() > NAMED_CHILDREN:
131
+ shown += " and %d more" % (children.size() - NAMED_CHILDREN)
132
+ return (
133
+ "%s%s %s has no %s; %s."
134
+ % [
135
+ said,
136
+ "" if said.ends_with(".") else ".",
137
+ "The root, %s," % root_name if reached.is_empty() else "/".join(reached),
138
+ missing,
139
+ "its children are " + shown if not children.is_empty() else "it has no children",
140
+ ]
141
+ )
@@ -660,7 +660,9 @@ static func _is_empty(owed: Variant) -> bool:
660
660
  ## taken the lot: see [member _outgoing].
661
661
  func _send_response(client: StreamPeerTCP, data: Dictionary) -> void:
662
662
  var owed: PackedByteArray = _outgoing.get(client, PackedByteArray())
663
- owed.append_array((JSON.stringify(data) + "\n").to_utf8_buffer())
663
+ # Keys left in the order they were put in: sorted, a map read came back in another order than
664
+ # its keys() and the game iterate it in.
665
+ owed.append_array((JSON.stringify(data, "", false) + "\n").to_utf8_buffer())
664
666
  _outgoing[client] = owed
665
667
 
666
668
 
@@ -129,8 +129,8 @@ func set_property(params: Dictionary) -> Dictionary:
129
129
  % [
130
130
  reached["called"],
131
131
  named,
132
- JSON.stringify(_values.serialize(given)),
133
- JSON.stringify(_values.serialize(now)),
132
+ JSON.stringify(_values.serialize(given), "", false),
133
+ JSON.stringify(_values.serialize(now), "", false),
134
134
  why,
135
135
  ]
136
136
  )
@@ -195,14 +195,19 @@ static func control_saying(root: Node, root_path: String, wanted: String, which:
195
195
  }
196
196
  var index: int = 0 if which == null else Read.as_int(which, -1)
197
197
  if index < 0 or index >= targets.size():
198
- var plural: String = "" if targets.size() == 1 else "s"
198
+ # A number arrives from JSON as a float, and 5 was quoted back as 5.0.
199
+ var given: String = str(index) if index >= 0 else str(which)
200
+ var among: String = (
201
+ 'the only match on screen%s saying "%s" is index 0' % [under, wanted]
202
+ if targets.size() == 1
203
+ else (
204
+ 'the %d matches on screen%s saying "%s" are 0 to %d'
205
+ % [targets.size(), under, wanted, targets.size() - 1]
206
+ )
207
+ )
199
208
  return {
200
209
  "type": "error",
201
- "message":
202
- (
203
- 'index %s is not one of the %d control%s on screen%s saying "%s": %s'
204
- % [str(which), targets.size(), plural, under, wanted, _candidates(targets, lines)]
205
- )
210
+ "message": "index %s picks nothing: %s: %s" % [given, among, _candidates(targets, lines)]
206
211
  }
207
212
  # A match saying the words as part of more, while another control says exactly them, is a stand-in
208
213
  # for that one, not the control meant: a tab reading "Out" sat in a collapsed drawer and a dispatch
@@ -477,5 +477,7 @@ func _element(item: Variant, builtin: int, declared: String, script: Script, res
477
477
  var wanted: String = type_string(builtin)
478
478
  if builtin == TYPE_OBJECT:
479
479
  wanted = declared if script == null else script_named(script)
480
- return {"message": "is %s, which cannot become %s" % [JSON.stringify(serialize(item)), wanted]}
480
+ return {
481
+ "message": "is %s, which cannot become %s" % [JSON.stringify(serialize(item), "", false), wanted]
482
+ }
481
483
  return {"value": fitted_item}
@@ -664,8 +664,12 @@ func _serialize_array(value: Array) -> Array:
664
664
 
665
665
  # Base64 rather than a list of numbers. A byte array is a texture, a sound or a save file often
666
666
  # enough that the list would be the whole answer, megabytes of it, and unreadable either way.
667
+ #
668
+ # The engine's encoder raises an error on an empty array, and an empty tile_map_data is every
669
+ # TileMapLayer with nothing painted yet, so a read of one carried an engine error in its answer.
667
670
  func _serialize_bytes(value: PackedByteArray) -> Dictionary:
668
- return {"base64": Marshalls.raw_to_base64(value), "size": value.size(), "_type": "PackedByteArray"}
671
+ var text: String = "" if value.is_empty() else Marshalls.raw_to_base64(value)
672
+ return {"base64": text, "size": value.size(), "_type": "PackedByteArray"}
669
673
 
670
674
 
671
675
  # The packed vector and colour arrays, each element serialised the way one of them on its own is.
@@ -5,6 +5,7 @@ extends RefCounted
5
5
  ## stores, which runs no script of the scene's, so a read answers the same either way.
6
6
 
7
7
  const FileReads = preload("file_reads.gd")
8
+ const Read = preload("reading.gd")
8
9
  const Serialisation = preload("serialisation.gd")
9
10
 
10
11
  const LOOP_MODE_NAMES: Dictionary[int, String] = {
@@ -60,7 +61,15 @@ static func _bare(scene_path: String, node_path: String, kind: String) -> Array:
60
61
  return [null, read]
61
62
  var type: String = str(read.get("type", ""))
62
63
  if not ClassDB.class_exists(type) or not ClassDB.is_parent_class(type, kind):
63
- return [null, {"ok": false, "error": "%s not found at: %s" % [kind, node_path]}]
64
+ var said: String = "%s not found at: %s" % [kind, node_path]
65
+ if read.is_empty():
66
+ var hinted: String = said
67
+ if FileReads.scene_node(scene_path, node_path.get_slice("/", 0)).is_empty():
68
+ hinted = Read.from_the_root(
69
+ said, node_path, str(FileReads.scene_node(scene_path, ".").get("name", ""))
70
+ )
71
+ said = hinted if hinted != said else FileReads.missing_from(said, scene_path, node_path)
72
+ return [null, {"ok": false, "error": said}]
64
73
  var bare: Node = ClassDB.instantiate(type)
65
74
  bare.name = str(read["name"])
66
75
  var given: Dictionary = read["properties"]
@@ -164,7 +173,10 @@ static func state_machine(tree: AnimationTree, state_machine_path: String) -> An
164
173
  ## the path that failed, and what is there instead.
165
174
  static func no_machine(tree: AnimationTree, state_machine_path: String) -> String:
166
175
  if tree == null or tree.tree_root == null:
167
- return "The AnimationTree has no tree_root."
176
+ return (
177
+ "The AnimationTree has no tree_root. Make one with resource_edit create of an "
178
+ + "AnimationNodeStateMachine, then give its res:// path to tree_root with scene_node set."
179
+ )
168
180
  var current: AnimationNodeStateMachine = tree.tree_root as AnimationNodeStateMachine
169
181
  if current == null:
170
182
  return (
@@ -7,8 +7,10 @@ extends RefCounted
7
7
  ## made here passes the same checks and answers in the same shape. What the engine does that an
8
8
  ## editor does not, running every script, is put back in scene_file.gd and headless_check.gd.
9
9
 
10
+ const AnimationTools = preload("../addons/gdharness_editor/tools/animation_tools.gd")
10
11
  const ResourceTools = preload("../addons/gdharness_editor/tools/resource_tools.gd")
11
12
  const SceneTools = preload("../addons/gdharness_editor/tools/scene_tools.gd")
13
+ const SceneFile = preload("../addons/gdharness_editor/scene_file.gd")
12
14
 
13
15
  const SCENE_COMMANDS: Array[String] = [
14
16
  "create_scene",
@@ -32,6 +34,12 @@ const RESOURCE_COMMANDS: Array[String] = [
32
34
  "set_theme_color",
33
35
  "set_theme_font_size",
34
36
  ]
37
+ const ANIMATION_COMMANDS: Array[String] = [
38
+ "create_animation",
39
+ "add_animation_track",
40
+ "add_animation_state",
41
+ "connect_animation_states",
42
+ ]
35
43
 
36
44
 
37
45
  ## Runs the tool [code]command[/code] names with [code]arguments[/code], answering what it answers.
@@ -46,12 +54,31 @@ static func run(params: Dictionary) -> Dictionary:
46
54
  tools = SceneTools.new()
47
55
  elif command in RESOURCE_COMMANDS:
48
56
  tools = ResourceTools.new()
57
+ elif command in ANIMATION_COMMANDS:
58
+ tools = AnimationTools.new()
49
59
  else:
50
60
  return {"ok": false, "error": "No editor tool runs headless as " + command}
51
61
  var answer: Variant = tools.call(command, arguments)
52
62
  tools.free()
53
63
  if answer is Dictionary:
54
- return answer
64
+ var answered: Dictionary = answer
65
+ if not SceneFile.by_path.is_empty():
66
+ answered["namedByPath"] = (
67
+ (
68
+ "%s %s named by path rather than by uid. A uid in a file's own header is resolved "
69
+ + "through the project's uid cache, which holds none for %s yet, as with a file "
70
+ + "made since the project was last scanned, and a uid it does not hold would warn "
71
+ + "on every load. project_import refresh_uids scans the project, and writes after "
72
+ + "it name %s by uid."
73
+ )
74
+ % [
75
+ ", ".join(SceneFile.by_path),
76
+ "is" if SceneFile.by_path.size() == 1 else "are",
77
+ "it" if SceneFile.by_path.size() == 1 else "them",
78
+ "it" if SceneFile.by_path.size() == 1 else "them",
79
+ ]
80
+ )
81
+ return answered
55
82
  return {
56
83
  "ok": false, "error": "%s answered %s rather than an object." % [command, type_string(typeof(answer))]
57
84
  }