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
@@ -31,25 +31,20 @@ const GdhHeadlessCheck = preload("headless_check.gd")
31
31
  const PUTTING_BACK_PASSES: int = 4
32
32
  const GdhRead = preload("reading.gd")
33
33
  const GdhSame = preload("same_values.gd")
34
- ## The names Object.ConnectFlags gives each bit. The number alone left a reader guessing that 2 is
35
- ## the persistence every connection in a saved scene carries.
36
- const CONNECT_FLAG_NAMES: Dictionary[int, String] = {
37
- Object.CONNECT_DEFERRED: "deferred",
38
- Object.CONNECT_PERSIST: "persist",
39
- Object.CONNECT_ONE_SHOT: "one_shot",
40
- Object.CONNECT_REFERENCE_COUNTED: "reference_counted",
41
- Object.CONNECT_APPEND_SOURCE_OBJECT: "append_source_object",
42
- }
34
+ const GdhSerialisation = preload("serialisation.gd")
35
+ const GdhSceneUids = preload("scene_uids.gd")
43
36
 
44
37
  ## Whether a scan asked for through [method scan_later] is still to start, which a caller waiting
45
38
  ## for the editor's files to settle has to wait through as well.
46
39
  static var scan_due: bool = false
47
- ## The files a headless save named by path alone because the uid cache holds no uid for them,
48
- ## gathered for the answer to the call that saved, the one call its engine runs.
49
- static var by_path: PackedStringArray = []
40
+ ## What the saves of a call wrote on nodes it did not ask to change, differently from their file,
41
+ ## gathered for that call's answer as [method _also_saved] finds them.
42
+ static var also_saved: Array[Dictionary] = []
50
43
 
51
44
  var root: Node = null
52
45
  var path: String = ""
46
+ ## What the last write saved, for its answer to read what the file now stores.
47
+ var written_pack: PackedScene = null
53
48
  ## The path of each node a scene this one inherits gives, with the file that makes it.
54
49
  var _base_paths: Dictionary = {}
55
50
  var _state: SceneState = null
@@ -67,6 +62,10 @@ var _copied_from: Dictionary = {}
67
62
  var _reached: Dictionary = {}
68
63
  ## The file as it was opened, to compare its embedded resources with what the write saves.
69
64
  var _text: String = ""
65
+ ## The engine properties a put back did not take on a node, as "id:name": the engine derives them
66
+ ## from where the node is, as it keeps layout_mode at Container under a container, and stores the
67
+ ## same from an editor.
68
+ var _engine_kept: Dictionary = {}
70
69
 
71
70
 
72
71
  ## Opens [param scene_path], answering a refusal, or {} with [member root] set.
@@ -91,26 +90,16 @@ func open(scene_path: String, for_writing: bool) -> Dictionary:
91
90
  if root == null:
92
91
  return {"ok": false, "error": "Failed to instantiate " + path}
93
92
  _state = packed.get_state()
94
- var base: SceneState = _state.get_base_scene_state()
95
- # Each node a base gives, with the file that makes it: the deepest base listing it as more than
96
- # values set on a node from further down.
97
- while base != null:
98
- for index: int in base.get_node_count():
99
- var given: String = _relative(str(base.get_node_path(index)))
100
- var made: bool = (
101
- base.get_node_instance(index) != null or not str(base.get_node_type(index)).is_empty()
102
- )
103
- if made or not _base_paths.has(given):
104
- _base_paths[given] = base.get_path()
105
- base = base.get_base_scene_state()
93
+ _base_paths = makers(_state)
106
94
  # On a read as well, which would otherwise answer what the scripts set as what the file holds.
107
- if not Engine.is_editor_hint():
108
- if for_writing:
109
- _text = FileAccess.get_file_as_string(path)
95
+ if for_writing or not Engine.is_editor_hint():
110
96
  for index: int in _state.get_node_count():
111
97
  var node: Node = _node_at(str(_state.get_node_path(index)))
112
98
  if node != null:
113
99
  _filed[node.get_instance_id()] = _stored_at(_state, index)
100
+ if not Engine.is_editor_hint():
101
+ if for_writing:
102
+ _text = FileAccess.get_file_as_string(path)
114
103
  var unsettled: String = settle()
115
104
  if not unsettled.is_empty():
116
105
  return refuse(("Nothing was written: " if for_writing else "Not read: ") + unsettled)
@@ -179,20 +168,6 @@ func refuse(error: String) -> Dictionary:
179
168
  return {"ok": false, "error": error}
180
169
 
181
170
 
182
- ## Each bit set in [param flags], a connection's, by name, and whatever bits are left unnamed as one
183
- ## number.
184
- static func flag_names(flags: int) -> Array[String]:
185
- var names: Array[String] = []
186
- var rest: int = flags
187
- for bit: int in CONNECT_FLAG_NAMES:
188
- if flags & bit:
189
- names.append(CONNECT_FLAG_NAMES[bit])
190
- rest &= ~bit
191
- if rest != 0:
192
- names.append(str(rest))
193
- return names
194
-
195
-
196
171
  ## [param said], the refusal for [param node_path] found nowhere in this scene, with how a path is
197
172
  ## read when it starts with the root's own name, and otherwise with the last node of the path the
198
173
  ## scene has and what is under it, as after a node was moved or renamed.
@@ -350,13 +325,20 @@ func _closed_around(node: Node) -> PackedStringArray:
350
325
  func fixed(node: Node, deleting: bool = false) -> String:
351
326
  if node == root:
352
327
  return "The root of %s cannot be moved, renamed, copied or deleted." % path
328
+ if node.owner == null:
329
+ return foreign(node)
353
330
  if node.owner != root:
354
- var why: String = foreign(node)
355
- if why.is_empty():
356
- why = "%s is inside an instanced scene, which creates it again on load." % path_of(node)
357
- if deleting:
358
- why += _deleted_elsewhere(node, node.owner.scene_file_path)
359
- return why
331
+ # Editable children keep values here and no more; the scene instanced may have the node from
332
+ # a scene it inherits, which is the one to edit.
333
+ var instanced: String = node.owner.scene_file_path
334
+ var packed: PackedScene = load(instanced) as PackedScene
335
+ var made_by: Dictionary = makers(packed.get_state()) if packed != null else {}
336
+ var maker: String = str(made_by.get(str(node.owner.get_path_to(node)), instanced))
337
+ var inside: String = (
338
+ "%s is inside the scene instanced at %s (%s), which creates it again on load."
339
+ % [path_of(node), path_of(node.owner), instanced]
340
+ )
341
+ return inside + (_deleted_elsewhere(node, maker) if deleting else " Edit %s instead." % maker)
360
342
  var at: String = path_of(node)
361
343
  if _base_paths.has(at):
362
344
  var maker: String = str(_base_paths[at])
@@ -367,6 +349,23 @@ func fixed(node: Node, deleting: bool = false) -> String:
367
349
  return ""
368
350
 
369
351
 
352
+ ## Each node the scenes [param state] inherits give it, by path, with the deepest file making it more
353
+ ## than values; a node [param state] makes itself is not in it.
354
+ static func makers(state: SceneState) -> Dictionary:
355
+ var made_by: Dictionary = {}
356
+ var at: SceneState = state.get_base_scene_state()
357
+ while at != null:
358
+ for index: int in at.get_node_count():
359
+ var given: String = _relative(str(at.get_node_path(index)))
360
+ var made: bool = (
361
+ at.get_node_instance(index) != null or not str(at.get_node_type(index)).is_empty()
362
+ )
363
+ if made or not made_by.has(given):
364
+ made_by[given] = at.get_path()
365
+ at = at.get_base_scene_state()
366
+ return made_by
367
+
368
+
370
369
  ## What a caller can do with [param node], made by [param maker], instead of deleting it here.
371
370
  static func _deleted_elsewhere(node: Node, maker: String) -> String:
372
371
  var hidden: String = (
@@ -504,7 +503,7 @@ func unusable_name(parent: Node, child_name: String, moving: Node = null) -> Str
504
503
  return ""
505
504
 
506
505
 
507
- ## The names of the properties this scene's own file sets on [param node], whatever their values.
506
+ ## The properties this scene's own file sets on [param node], each with the value the file gives it.
508
507
  func set_here(node: Node) -> Dictionary:
509
508
  return set_at(path_of(node))
510
509
 
@@ -531,17 +530,17 @@ func groups_at(at: String) -> PackedStringArray:
531
530
  return named
532
531
 
533
532
 
534
- ## The names of the properties this scene's own file sets on the node at [param at].
533
+ ## The properties this scene's own file sets on the node at [param at], with the values it gives.
535
534
  func set_at(at: String) -> Dictionary:
536
- var names: Dictionary = {}
535
+ var held: Dictionary = {}
537
536
  if _state == null:
538
- return names
537
+ return held
539
538
  for index: int in _state.get_node_count():
540
539
  if _relative(str(_state.get_node_path(index))) != at:
541
540
  continue
542
- for property: int in _state.get_node_property_count(index):
543
- names[str(_state.get_node_property_name(index, property))] = true
544
- return names
541
+ for p: int in _state.get_node_property_count(index):
542
+ held[str(_state.get_node_property_name(index, p))] = _state.get_node_property_value(index, p)
543
+ return held
545
544
 
546
545
 
547
546
  ## Whether [param loaded], read off [param saved] (a copy instantiated from the pack), is
@@ -583,19 +582,12 @@ func write(check: Callable = Callable()) -> Dictionary:
583
582
  return refuse("Nothing was written: " + str(settled[1]))
584
583
  packed = settled[0]
585
584
  if not path.ends_with(".tscn") and _holds_scripted_embedded():
586
- return refuse(
587
- (
588
- (
589
- (
590
- "Nothing was written: %s is a binary scene holding an embedded resource with a script "
591
- + "that is not a tool, and what that script changed when a headless engine loaded it "
592
- + "cannot be told apart from the file here. "
593
- )
594
- % path
595
- )
596
- + GdhHeadlessCheck.REMEDY
597
- )
585
+ var binary: String = (
586
+ "Nothing was written: %s is a binary scene holding an embedded resource with a script "
587
+ + "that is not a tool, and what that script changed when a headless engine loaded it "
588
+ + "cannot be told apart from the file here. "
598
589
  )
590
+ return refuse(binary % path + GdhHeadlessCheck.REMEDY)
599
591
  if check.is_valid():
600
592
  var saved: Node = packed.instantiate(PackedScene.GEN_EDIT_STATE_MAIN)
601
593
  if saved == null:
@@ -604,27 +596,73 @@ func write(check: Callable = Callable()) -> Dictionary:
604
596
  saved.free()
605
597
  if not missing.is_empty():
606
598
  return refuse("Nothing was written: " + missing)
599
+ var also: Array[Dictionary] = _also_saved(packed.get_state())
607
600
  close()
608
601
  var previous: Variant = null
609
602
  if FileAccess.file_exists(path):
610
603
  previous = FileAccess.get_file_as_bytes(path)
611
- var written: Dictionary = save_packed(packed, path)
612
- if not written.is_empty() or not headless or not path.ends_with(".tscn"):
613
- return written
604
+ written_pack = packed
614
605
  var state: SceneState = packed.get_state()
615
606
  var values: Array = []
616
607
  for index: int in state.get_node_count():
617
608
  values.append_array(_stored_at(state, index).values())
609
+ var carried: Dictionary = GdhHeadlessCheck.embedded_in(values, {})
610
+ var written: Dictionary = save_packed(packed, path)
611
+ if not written.is_empty() or not headless or not path.ends_with(".tscn"):
612
+ if written.is_empty():
613
+ also_saved.append_array(also)
614
+ return written
618
615
  var unkept: String = GdhHeadlessCheck.settle_saved(
619
- path, _text, values, null, {}, _reached, func() -> Error: return save_keeping_uid(packed, path)
616
+ path,
617
+ _text,
618
+ values,
619
+ null,
620
+ {},
621
+ _reached,
622
+ carried,
623
+ func() -> Error: return GdhSceneUids.save_keeping_uid(packed, path)
620
624
  )
621
625
  if unkept.is_empty():
626
+ also_saved.append_array(also)
622
627
  return {}
623
628
  return {
624
629
  "ok": false, "error": "Nothing was written: " + unkept + GdhHeadlessCheck.put_back(path, previous)
625
630
  }
626
631
 
627
632
 
633
+ ## Each property [param state] stores differently from the file, on a node the file held and the call
634
+ ## did not ask to change, as {node, property, was, saved}: was left out where the file did not set it,
635
+ ## saved where the write does not. The engine writes a node as it holds it once the file has loaded,
636
+ ## and one property can set others as it loads, as anchors_preset gives a centred Control both grow
637
+ ## directions, so a file not written by the editor gains lines on nodes nobody named.
638
+ func _also_saved(state: SceneState) -> Array[Dictionary]:
639
+ var noted: Array[Dictionary] = []
640
+ var values: GdhSerialisation = GdhSerialisation.new()
641
+ for index: int in state.get_node_count():
642
+ var node: Node = _node_at(str(state.get_node_path(index)))
643
+ if node == null:
644
+ continue
645
+ var id: int = node.get_instance_id()
646
+ if _asked.has(id) or not _filed.has(id):
647
+ continue
648
+ var filed: Dictionary = _filed[id]
649
+ var stored: Dictionary = _stored_at(state, index)
650
+ var names: Dictionary = filed.duplicate()
651
+ names.merge(stored)
652
+ for key: Variant in names:
653
+ var name: String = str(key)
654
+ var held: bool = filed.has(name)
655
+ if held == stored.has(name) and (not held or GdhSame.same(stored[name], filed[name])):
656
+ continue
657
+ var entry: Dictionary = {"node": path_of(node), "property": name}
658
+ if held:
659
+ entry["was"] = values.serialize_value(filed[name])
660
+ if stored.has(name):
661
+ entry["saved"] = values.serialize_value(stored[name])
662
+ noted.append(entry)
663
+ return noted
664
+
665
+
628
666
  func _holds_scripted_embedded() -> bool:
629
667
  var seen: Dictionary = {}
630
668
  var nodes: Array[Node] = [root]
@@ -644,112 +682,13 @@ static func save_packed(packed: PackedScene, scene_path: String) -> Dictionary:
644
682
  var made: Error = DirAccess.make_dir_recursive_absolute(directory)
645
683
  if made != OK:
646
684
  return {"ok": false, "error": "Could not create %s: %s" % [directory, error_string(made)]}
647
- var saving: Error = save_keeping_uid(packed, scene_path)
685
+ var saving: Error = GdhSceneUids.save_keeping_uid(packed, scene_path)
648
686
  if saving != OK:
649
687
  return {"ok": false, "error": "Failed to save %s: %s" % [scene_path, error_string(saving)]}
650
688
  refresh(scene_path)
651
689
  return {}
652
690
 
653
691
 
654
- ## Saves [param resource] at [param file_path], keeping the uid the file already has.
655
- ##
656
- ## The editor saves through its uid cache, which knows every file's. A headless engine that has not
657
- ## imported the project has none, and a resave dropped the uid from the header, so every reference
658
- ## made through it stopped resolving. Read from the file itself, since the loader does not answer it
659
- ## for a text scene there, and a new file is given one as the editor gives it.
660
- static func save_keeping_uid(resource: Resource, file_path: String) -> Error:
661
- if Engine.is_editor_hint():
662
- return ResourceSaver.save(resource, file_path)
663
- var uid: int = uid_in(file_path)
664
- var saving: Error = ResourceSaver.save(resource, file_path)
665
- if saving != OK:
666
- return saving
667
- if uid == ResourceUID.INVALID_ID:
668
- uid = ResourceUID.create_id()
669
- var marked: Error = ResourceSaver.set_uid(file_path, uid)
670
- if marked != OK:
671
- return marked
672
- return _with_external_uids(file_path)
673
-
674
-
675
- ## Puts back the uid on each external resource the text file at [param file_path] names. Only the
676
- ## editor's file system tells the saver a path's uid, so a headless save writes every reference by
677
- ## path alone, and each breaks when the file it names is moved.
678
- static func _with_external_uids(file_path: String) -> Error:
679
- if not (file_path.ends_with(".tscn") or file_path.ends_with(".tres")):
680
- return OK
681
- var lines: PackedStringArray = FileAccess.get_file_as_string(file_path).split("\n")
682
- var pattern: RegEx = RegEx.create_from_string(' path="([^"]+)"')
683
- var changed: bool = false
684
- for at: int in lines.size():
685
- var line: String = lines[at]
686
- if not line.begins_with("[ext_resource ") or line.contains(' uid="'):
687
- continue
688
- var named: RegExMatch = pattern.search(line)
689
- if named == null:
690
- continue
691
- var uid: String = reference_uid(named.get_string(1))
692
- if not uid.is_empty():
693
- lines[at] = line.replace(' path="', uid + ' path="')
694
- changed = true
695
- if not changed:
696
- return OK
697
- var file: FileAccess = FileAccess.open(file_path, FileAccess.WRITE)
698
- if file == null:
699
- return FileAccess.get_open_error()
700
- if not file.store_string("\n".join(lines)):
701
- return ERR_FILE_CANT_WRITE
702
- file.close()
703
- return OK
704
-
705
-
706
- ## [method uid_attribute] for a reference about to be written, noting [param named] under
707
- ## [member by_path] when it is left to be named by path.
708
- static func reference_uid(named: String) -> String:
709
- var uid: String = uid_attribute(named)
710
- if uid.is_empty():
711
- note_by_path(named)
712
- return uid
713
-
714
-
715
- ## Notes [param named], a reference just written by path alone, under [member by_path] when the uid
716
- ## cache holds nothing for it: a file made since the project was last scanned, whose uid the cache
717
- ## learns from the next scan. Not a file whose uid the cache gives to another, which a scan leaves.
718
- static func note_by_path(named: String) -> void:
719
- if Engine.is_editor_hint() or not FileAccess.file_exists(named) or by_path.has(named):
720
- return
721
- var own: int = uid_in(named)
722
- if own == ResourceUID.INVALID_ID and FileAccess.file_exists(named + ".uid"):
723
- own = ResourceUID.text_to_id(FileAccess.get_file_as_string(named + ".uid").strip_edges())
724
- if own == ResourceUID.INVALID_ID or not ResourceUID.has_id(own):
725
- var _noted: bool = by_path.append(named)
726
-
727
-
728
- ## The uid attribute a reference to [param named] is written with, as ' uid="uid://..."', or "".
729
- ##
730
- ## Only a uid the uid cache gives that path, which is what the loader resolves it by. A uid the cache
731
- ## does not know (a file made with no editor since the last scan) is answered with a warning on every
732
- ## load, and one it gives another file (a scene copied whole keeps its original's) loads that file.
733
- static func uid_attribute(named: String) -> String:
734
- var id: int = ResourceLoader.get_resource_uid(named)
735
- if id == ResourceUID.INVALID_ID or not ResourceUID.has_id(id) or ResourceUID.get_id_path(id) != named:
736
- return ""
737
- return ' uid="%s"' % ResourceUID.id_to_text(id)
738
-
739
-
740
- ## The uid the file at [param file_path] declares, or [constant ResourceUID.INVALID_ID].
741
- static func uid_in(file_path: String) -> int:
742
- if not FileAccess.file_exists(file_path):
743
- return ResourceUID.INVALID_ID
744
- var file: FileAccess = FileAccess.open(file_path, FileAccess.READ)
745
- if file != null:
746
- var header: RegExMatch = RegEx.create_from_string('uid="(uid://[a-z0-9]+)"').search(file.get_line())
747
- file.close()
748
- if header != null:
749
- return ResourceUID.text_to_id(header.get_string(1))
750
- return ResourceLoader.get_resource_uid(file_path)
751
-
752
-
753
692
  ## Brings the editor up to date with a scene file written under it: the cached copy anything holds,
754
693
  ## the editor's file list, and the tab showing it, whether current or not.
755
694
  static func refresh(scene_path: String) -> void:
@@ -824,7 +763,11 @@ func _settled(packed: PackedScene) -> Array:
824
763
  for found: Array in stray:
825
764
  var node: Node = found[0]
826
765
  var name: String = found[1]
827
- node.set(name, found[3] if found[2] else _unset_value(node, name))
766
+ var wanted: Variant = found[3] if found[2] else _unset_value(node, name)
767
+ var held: Variant = node.get(name)
768
+ node.set(name, wanted)
769
+ if GdhHeadlessCheck.kept_by_engine(node, name, wanted, held):
770
+ _engine_kept["%d:%s" % [node.get_instance_id(), name]] = true
828
771
  for found: Array in moved:
829
772
  var node: Node = found[0]
830
773
  var before: Dictionary = _before[node.get_instance_id()]
@@ -894,6 +837,8 @@ func _strays(state: SceneState) -> Array[Array]:
894
837
  continue
895
838
  if not held and GdhHeadlessCheck.at_default(node, name):
896
839
  continue
840
+ if _engine_kept.has("%d:%s" % [id, name]):
841
+ continue
897
842
  stray.append([node, name, held, filed.get(name)])
898
843
  return stray
899
844
 
@@ -988,7 +933,5 @@ static func _stored_at(state: SceneState, index: int) -> Dictionary:
988
933
  return stored
989
934
 
990
935
 
991
- func _relative(node_path: String) -> String:
992
- if node_path == ".":
993
- return node_path
936
+ static func _relative(node_path: String) -> String:
994
937
  return node_path.trim_prefix("./")
@@ -0,0 +1,107 @@
1
+ @tool
2
+ extends RefCounted
3
+
4
+ ## How a scene or resource a tool saves keeps naming files by uid, as the editor's saves do.
5
+
6
+ ## The files a headless save named by path alone because the uid cache holds no uid for them,
7
+ ## gathered for the answer to the call that saved, the one call its engine runs.
8
+ static var by_path: PackedStringArray = []
9
+
10
+
11
+ ## Saves [param resource] at [param file_path], keeping the uid the file already has.
12
+ ##
13
+ ## The editor saves through its uid cache, which knows every file's. A headless engine that has not
14
+ ## imported the project has none, and a resave dropped the uid from the header, so every reference
15
+ ## made through it stopped resolving. Read from the file itself, since the loader does not answer it
16
+ ## for a text scene there, and a new file is given one as the editor gives it.
17
+ static func save_keeping_uid(resource: Resource, file_path: String) -> Error:
18
+ if Engine.is_editor_hint():
19
+ return ResourceSaver.save(resource, file_path)
20
+ var uid: int = uid_in(file_path)
21
+ var saving: Error = ResourceSaver.save(resource, file_path)
22
+ if saving != OK:
23
+ return saving
24
+ if uid == ResourceUID.INVALID_ID:
25
+ uid = ResourceUID.create_id()
26
+ var marked: Error = ResourceSaver.set_uid(file_path, uid)
27
+ if marked != OK:
28
+ return marked
29
+ return _with_external_uids(file_path)
30
+
31
+
32
+ ## Puts back the uid on each external resource the text file at [param file_path] names. Only the
33
+ ## editor's file system tells the saver a path's uid, so a headless save writes every reference by
34
+ ## path alone, and each breaks when the file it names is moved.
35
+ static func _with_external_uids(file_path: String) -> Error:
36
+ if not (file_path.ends_with(".tscn") or file_path.ends_with(".tres")):
37
+ return OK
38
+ var lines: PackedStringArray = FileAccess.get_file_as_string(file_path).split("\n")
39
+ var pattern: RegEx = RegEx.create_from_string(' path="([^"]+)"')
40
+ var changed: bool = false
41
+ for at: int in lines.size():
42
+ var line: String = lines[at]
43
+ if not line.begins_with("[ext_resource ") or line.contains(' uid="'):
44
+ continue
45
+ var named: RegExMatch = pattern.search(line)
46
+ if named == null:
47
+ continue
48
+ var uid: String = reference_uid(named.get_string(1))
49
+ if not uid.is_empty():
50
+ lines[at] = line.replace(' path="', uid + ' path="')
51
+ changed = true
52
+ if not changed:
53
+ return OK
54
+ var file: FileAccess = FileAccess.open(file_path, FileAccess.WRITE)
55
+ if file == null:
56
+ return FileAccess.get_open_error()
57
+ if not file.store_string("\n".join(lines)):
58
+ return ERR_FILE_CANT_WRITE
59
+ file.close()
60
+ return OK
61
+
62
+
63
+ ## [method uid_attribute] for a reference about to be written, noting [param named] under
64
+ ## [member by_path] when it is left to be named by path.
65
+ static func reference_uid(named: String) -> String:
66
+ var uid: String = uid_attribute(named)
67
+ if uid.is_empty():
68
+ note_by_path(named)
69
+ return uid
70
+
71
+
72
+ ## Notes [param named], a reference just written by path alone, under [member by_path] when the uid
73
+ ## cache holds nothing for it: a file made since the project was last scanned, whose uid the cache
74
+ ## learns from the next scan. Not a file whose uid the cache gives to another, which a scan leaves.
75
+ static func note_by_path(named: String) -> void:
76
+ if Engine.is_editor_hint() or not FileAccess.file_exists(named) or by_path.has(named):
77
+ return
78
+ var own: int = uid_in(named)
79
+ if own == ResourceUID.INVALID_ID and FileAccess.file_exists(named + ".uid"):
80
+ own = ResourceUID.text_to_id(FileAccess.get_file_as_string(named + ".uid").strip_edges())
81
+ if own == ResourceUID.INVALID_ID or not ResourceUID.has_id(own):
82
+ var _noted: bool = by_path.append(named)
83
+
84
+
85
+ ## The uid attribute a reference to [param named] is written with, as ' uid="uid://..."', or "".
86
+ ##
87
+ ## Only a uid the uid cache gives that path, which is what the loader resolves it by. A uid the cache
88
+ ## does not know (a file made with no editor since the last scan) is answered with a warning on every
89
+ ## load, and one it gives another file (a scene copied whole keeps its original's) loads that file.
90
+ static func uid_attribute(named: String) -> String:
91
+ var id: int = ResourceLoader.get_resource_uid(named)
92
+ if id == ResourceUID.INVALID_ID or not ResourceUID.has_id(id) or ResourceUID.get_id_path(id) != named:
93
+ return ""
94
+ return ' uid="%s"' % ResourceUID.id_to_text(id)
95
+
96
+
97
+ ## The uid the file at [param file_path] declares, or [constant ResourceUID.INVALID_ID].
98
+ static func uid_in(file_path: String) -> int:
99
+ if not FileAccess.file_exists(file_path):
100
+ return ResourceUID.INVALID_ID
101
+ var file: FileAccess = FileAccess.open(file_path, FileAccess.READ)
102
+ if file != null:
103
+ var header: RegExMatch = RegEx.create_from_string('uid="(uid://[a-z0-9]+)"').search(file.get_line())
104
+ file.close()
105
+ if header != null:
106
+ return ResourceUID.text_to_id(header.get_string(1))
107
+ return ResourceLoader.get_resource_uid(file_path)
@@ -0,0 +1 @@
1
+ uid://bptdylr6kemgx
@@ -142,28 +142,29 @@ func serialize_value(value: Variant) -> Variant:
142
142
  return value
143
143
  var carried: Variant = call(serialiser, value)
144
144
  # The fixed shapes carry their components as bare floats, so an infinite one in a Vector2 is
145
- # tagged here; containers reach their members through this function already.
146
- return carried if typeof(value) in [TYPE_ARRAY, TYPE_DICTIONARY] else _finite(carried)
145
+ # tagged here, and a 32-bit one shortened; containers reach their members through this already.
146
+ var type: int = typeof(value)
147
+ return carried if type in [TYPE_ARRAY, TYPE_DICTIONARY] else _finite(carried, GdhRead.holds_singles(type))
147
148
 
148
149
 
149
150
  # [param carried] with every non-finite float in it tagged. JSON has no spelling for infinity or
150
151
  # NaN: the engine writes 1e99999 and null, and the server, reading 1e99999 as Infinity, writes it
151
152
  # back out as null, so a property holding INF was answered as holding nothing.
152
- func _finite(carried: Variant) -> Variant:
153
+ func _finite(carried: Variant, single: bool = false) -> Variant:
153
154
  if carried is float:
154
155
  var number: float = carried
155
- return _serialize_float(number)
156
+ return _serialize_float(GdhRead.single(number) if single else number)
156
157
  if carried is Dictionary:
157
158
  var fields: Dictionary = carried
158
159
  var tagged: Dictionary = {}
159
160
  for key: Variant in fields:
160
- tagged[key] = _finite(fields[key])
161
+ tagged[key] = _finite(fields[key], single)
161
162
  return tagged
162
163
  if carried is Array:
163
164
  var items: Array = carried
164
165
  var tagged_items: Array = []
165
166
  for item: Variant in items:
166
- tagged_items.append(_finite(item))
167
+ tagged_items.append(_finite(item, single))
167
168
  return tagged_items
168
169
  return carried
169
170
 
@@ -189,7 +190,7 @@ func _serialize_floats(value: Variant) -> Variant:
189
190
  for each: float in numbers:
190
191
  items.append(_serialize_float(each))
191
192
  return items
192
- return value
193
+ return numbers if value is PackedFloat32Array else value
193
194
 
194
195
 
195
196
  # Rebuilds a Godot value from the shape serialize_value gave it.
@@ -222,8 +223,7 @@ func deserialize_value(value: Variant) -> Variant:
222
223
  # under the target project's warning levels, so one such project lost every headless call.
223
224
  var tag: String = str(fields["_type"])
224
225
  var builder: String = BUILDERS.get(tag, "")
225
- var spare: Array = GdhShapes.spare(fields, tag)
226
- if builder.is_empty() or not spare.any(func(left: Array) -> bool: return left.is_empty()):
226
+ if builder.is_empty() or not GdhShapes.buildable(fields, tag):
227
227
  return fields
228
228
  if tag == "float" and str(fields["value"]) not in ["inf", "-inf", "nan"]:
229
229
  return fields
@@ -276,24 +276,7 @@ static func components_of(value: Variant) -> PackedStringArray:
276
276
  TYPE_VECTOR4, TYPE_VECTOR4I, TYPE_QUATERNION:
277
277
  return PackedStringArray(["x", "y", "z", "w"])
278
278
  TYPE_COLOR:
279
- return PackedStringArray(
280
- [
281
- "r",
282
- "g",
283
- "b",
284
- "a",
285
- "r8",
286
- "g8",
287
- "b8",
288
- "a8",
289
- "h",
290
- "s",
291
- "v",
292
- "ok_hsl_h",
293
- "ok_hsl_s",
294
- "ok_hsl_l"
295
- ]
296
- )
279
+ return "r g b a r8 g8 b8 a8 h s v ok_hsl_h ok_hsl_s ok_hsl_l".split(" ")
297
280
  TYPE_RECT2, TYPE_RECT2I, TYPE_AABB:
298
281
  return PackedStringArray(["position", "size", "end"])
299
282
  TYPE_PLANE:
@@ -404,7 +387,8 @@ func fitted(value: Variant, type: int) -> Variant:
404
387
  return built
405
388
 
406
389
  if rebuilt is Array and LIST_TYPES.has(type):
407
- return type_convert(rebuilt, type)
390
+ var items: Array = value if value is Array else rebuilt
391
+ return GdhShapes.packed_from(items, type, fitted)
408
392
 
409
393
  # The inspector and every colour picker give a colour as its html code. Anything else stays the
410
394
  # text it is, for the refusal to name.
@@ -704,7 +688,13 @@ func _describe_built_in(resource: Resource) -> Dictionary:
704
688
  var script: Script = null
705
689
  if attached is Script:
706
690
  script = attached
707
- described["script"] = serialize_value(script)
691
+ # By the class its script declares, which a write takes as the _type and which says what it
692
+ # is: as Resource with the script beside it, one answer named a Shop's built-in item that way
693
+ # and the file item beside it as an Item.
694
+ if script.get_global_name().is_empty():
695
+ described["script"] = serialize_value(script)
696
+ else:
697
+ described["_type"] = str(script.get_global_name())
708
698
  var fresh: Object = _fresh(script)
709
699
  for property: Dictionary in resource.get_property_list():
710
700
  var usage: int = property.get("usage", 0)