gdharness 1.3.9 → 1.3.11

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 (73) hide show
  1. package/build/cli.js +4740 -3212
  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 +102 -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 +51 -1
  9. package/build/godot/addons/gdharness_editor/scene_file.gd +77 -140
  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 +11 -22
  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 +28 -5
  16. package/build/godot/addons/gdharness_editor/tools/animation_states.gd +39 -20
  17. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +49 -362
  18. package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +464 -0
  19. package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd.uid +1 -0
  20. package/build/godot/addons/gdharness_editor/tools/class_tools.gd +37 -0
  21. package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd +221 -0
  22. package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd.uid +1 -0
  23. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +147 -492
  24. package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +16 -0
  25. package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +4 -3
  26. package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +2 -1
  27. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +79 -5
  28. package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +7 -30
  29. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +183 -37
  30. package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd +485 -0
  31. package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd.uid +1 -0
  32. package/build/godot/addons/gdharness_editor/value_shapes.gd +128 -1
  33. package/build/godot/addons/gdharness_runtime/declared_values.gd +272 -0
  34. package/build/godot/addons/gdharness_runtime/reading.gd +51 -1
  35. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +35 -3
  36. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +185 -30
  37. package/build/godot/addons/gdharness_runtime/runtime_input.gd +86 -79
  38. package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +17 -0
  39. package/build/godot/addons/gdharness_runtime/runtime_pad.gd +13 -4
  40. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +309 -22
  41. package/build/godot/addons/gdharness_runtime/runtime_queries.gd +163 -20
  42. package/build/godot/addons/gdharness_runtime/runtime_screen.gd +49 -3
  43. package/build/godot/addons/gdharness_runtime/runtime_targets.gd +5 -2
  44. package/build/godot/addons/gdharness_runtime/runtime_values.gd +146 -5
  45. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +279 -11
  46. package/build/godot/addons/gdharness_runtime/runtime_words.gd +27 -0
  47. package/build/godot/addons/gdharness_runtime/serialisation.gd +11 -22
  48. package/build/godot/addons/gdharness_runtime/value_shapes.gd +128 -1
  49. package/build/godot/operations/animation_reads.gd +115 -4
  50. package/build/godot/operations/audio_buses.gd +93 -7
  51. package/build/godot/operations/classdb_queries.gd +119 -7
  52. package/build/godot/operations/declared_values.gd +272 -0
  53. package/build/godot/operations/dependencies.gd +154 -41
  54. package/build/godot/operations/editor_tools.gd +22 -14
  55. package/build/godot/operations/export_presets.gd +602 -4
  56. package/build/godot/operations/file_reads.gd +102 -18
  57. package/build/godot/operations/gdscript_authoring.gd +11 -1
  58. package/build/godot/operations/godot_operations.gd +16 -2
  59. package/build/godot/operations/import_pipeline.gd +20 -221
  60. package/build/godot/operations/input_actions.gd +108 -47
  61. package/build/godot/operations/project_config.gd +83 -69
  62. package/build/godot/operations/project_diagnostics.gd +4 -0
  63. package/build/godot/operations/project_validation.gd +266 -0
  64. package/build/godot/operations/property_paths.gd +89 -16
  65. package/build/godot/operations/property_values.gd +180 -121
  66. package/build/godot/operations/reading.gd +51 -1
  67. package/build/godot/operations/scene_connections.gd +222 -0
  68. package/build/godot/operations/serialisation.gd +11 -22
  69. package/build/godot/operations/setting_reads.gd +121 -0
  70. package/build/godot/operations/value_shapes.gd +128 -1
  71. package/build/godot/tests/listed.gd +73 -13
  72. package/build/keeper.js +1371 -706
  73. package/package.json +1 -1
@@ -31,22 +31,15 @@ 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 = ""
@@ -69,6 +62,10 @@ var _copied_from: Dictionary = {}
69
62
  var _reached: Dictionary = {}
70
63
  ## The file as it was opened, to compare its embedded resources with what the write saves.
71
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 = {}
72
69
 
73
70
 
74
71
  ## Opens [param scene_path], answering a refusal, or {} with [member root] set.
@@ -95,13 +92,14 @@ func open(scene_path: String, for_writing: bool) -> Dictionary:
95
92
  _state = packed.get_state()
96
93
  _base_paths = makers(_state)
97
94
  # On a read as well, which would otherwise answer what the scripts set as what the file holds.
98
- if not Engine.is_editor_hint():
99
- if for_writing:
100
- _text = FileAccess.get_file_as_string(path)
95
+ if for_writing or not Engine.is_editor_hint():
101
96
  for index: int in _state.get_node_count():
102
97
  var node: Node = _node_at(str(_state.get_node_path(index)))
103
98
  if node != null:
104
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)
105
103
  var unsettled: String = settle()
106
104
  if not unsettled.is_empty():
107
105
  return refuse(("Nothing was written: " if for_writing else "Not read: ") + unsettled)
@@ -170,19 +168,6 @@ func refuse(error: String) -> Dictionary:
170
168
  return {"ok": false, "error": error}
171
169
 
172
170
 
173
- ## Each bit set in [param flags], a connection's, by name, and any bits left unnamed as one number.
174
- static func flag_names(flags: int) -> Array[String]:
175
- var names: Array[String] = []
176
- var rest: int = flags
177
- for bit: int in CONNECT_FLAG_NAMES:
178
- if flags & bit:
179
- names.append(CONNECT_FLAG_NAMES[bit])
180
- rest &= ~bit
181
- if rest != 0:
182
- names.append(str(rest))
183
- return names
184
-
185
-
186
171
  ## [param said], the refusal for [param node_path] found nowhere in this scene, with how a path is
187
172
  ## read when it starts with the root's own name, and otherwise with the last node of the path the
188
173
  ## scene has and what is under it, as after a node was moved or renamed.
@@ -518,7 +503,7 @@ func unusable_name(parent: Node, child_name: String, moving: Node = null) -> Str
518
503
  return ""
519
504
 
520
505
 
521
- ## 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.
522
507
  func set_here(node: Node) -> Dictionary:
523
508
  return set_at(path_of(node))
524
509
 
@@ -545,17 +530,17 @@ func groups_at(at: String) -> PackedStringArray:
545
530
  return named
546
531
 
547
532
 
548
- ## 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.
549
534
  func set_at(at: String) -> Dictionary:
550
- var names: Dictionary = {}
535
+ var held: Dictionary = {}
551
536
  if _state == null:
552
- return names
537
+ return held
553
538
  for index: int in _state.get_node_count():
554
539
  if _relative(str(_state.get_node_path(index))) != at:
555
540
  continue
556
- for property: int in _state.get_node_property_count(index):
557
- names[str(_state.get_node_property_name(index, property))] = true
558
- 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
559
544
 
560
545
 
561
546
  ## Whether [param loaded], read off [param saved] (a copy instantiated from the pack), is
@@ -611,28 +596,73 @@ func write(check: Callable = Callable()) -> Dictionary:
611
596
  saved.free()
612
597
  if not missing.is_empty():
613
598
  return refuse("Nothing was written: " + missing)
599
+ var also: Array[Dictionary] = _also_saved(packed.get_state())
614
600
  close()
615
601
  var previous: Variant = null
616
602
  if FileAccess.file_exists(path):
617
603
  previous = FileAccess.get_file_as_bytes(path)
618
604
  written_pack = packed
619
- var written: Dictionary = save_packed(packed, path)
620
- if not written.is_empty() or not headless or not path.ends_with(".tscn"):
621
- return written
622
605
  var state: SceneState = packed.get_state()
623
606
  var values: Array = []
624
607
  for index: int in state.get_node_count():
625
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
626
615
  var unkept: String = GdhHeadlessCheck.settle_saved(
627
- 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)
628
624
  )
629
625
  if unkept.is_empty():
626
+ also_saved.append_array(also)
630
627
  return {}
631
628
  return {
632
629
  "ok": false, "error": "Nothing was written: " + unkept + GdhHeadlessCheck.put_back(path, previous)
633
630
  }
634
631
 
635
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
+
636
666
  func _holds_scripted_embedded() -> bool:
637
667
  var seen: Dictionary = {}
638
668
  var nodes: Array[Node] = [root]
@@ -652,112 +682,13 @@ static func save_packed(packed: PackedScene, scene_path: String) -> Dictionary:
652
682
  var made: Error = DirAccess.make_dir_recursive_absolute(directory)
653
683
  if made != OK:
654
684
  return {"ok": false, "error": "Could not create %s: %s" % [directory, error_string(made)]}
655
- var saving: Error = save_keeping_uid(packed, scene_path)
685
+ var saving: Error = GdhSceneUids.save_keeping_uid(packed, scene_path)
656
686
  if saving != OK:
657
687
  return {"ok": false, "error": "Failed to save %s: %s" % [scene_path, error_string(saving)]}
658
688
  refresh(scene_path)
659
689
  return {}
660
690
 
661
691
 
662
- ## Saves [param resource] at [param file_path], keeping the uid the file already has.
663
- ##
664
- ## The editor saves through its uid cache, which knows every file's. A headless engine that has not
665
- ## imported the project has none, and a resave dropped the uid from the header, so every reference
666
- ## made through it stopped resolving. Read from the file itself, since the loader does not answer it
667
- ## for a text scene there, and a new file is given one as the editor gives it.
668
- static func save_keeping_uid(resource: Resource, file_path: String) -> Error:
669
- if Engine.is_editor_hint():
670
- return ResourceSaver.save(resource, file_path)
671
- var uid: int = uid_in(file_path)
672
- var saving: Error = ResourceSaver.save(resource, file_path)
673
- if saving != OK:
674
- return saving
675
- if uid == ResourceUID.INVALID_ID:
676
- uid = ResourceUID.create_id()
677
- var marked: Error = ResourceSaver.set_uid(file_path, uid)
678
- if marked != OK:
679
- return marked
680
- return _with_external_uids(file_path)
681
-
682
-
683
- ## Puts back the uid on each external resource the text file at [param file_path] names. Only the
684
- ## editor's file system tells the saver a path's uid, so a headless save writes every reference by
685
- ## path alone, and each breaks when the file it names is moved.
686
- static func _with_external_uids(file_path: String) -> Error:
687
- if not (file_path.ends_with(".tscn") or file_path.ends_with(".tres")):
688
- return OK
689
- var lines: PackedStringArray = FileAccess.get_file_as_string(file_path).split("\n")
690
- var pattern: RegEx = RegEx.create_from_string(' path="([^"]+)"')
691
- var changed: bool = false
692
- for at: int in lines.size():
693
- var line: String = lines[at]
694
- if not line.begins_with("[ext_resource ") or line.contains(' uid="'):
695
- continue
696
- var named: RegExMatch = pattern.search(line)
697
- if named == null:
698
- continue
699
- var uid: String = reference_uid(named.get_string(1))
700
- if not uid.is_empty():
701
- lines[at] = line.replace(' path="', uid + ' path="')
702
- changed = true
703
- if not changed:
704
- return OK
705
- var file: FileAccess = FileAccess.open(file_path, FileAccess.WRITE)
706
- if file == null:
707
- return FileAccess.get_open_error()
708
- if not file.store_string("\n".join(lines)):
709
- return ERR_FILE_CANT_WRITE
710
- file.close()
711
- return OK
712
-
713
-
714
- ## [method uid_attribute] for a reference about to be written, noting [param named] under
715
- ## [member by_path] when it is left to be named by path.
716
- static func reference_uid(named: String) -> String:
717
- var uid: String = uid_attribute(named)
718
- if uid.is_empty():
719
- note_by_path(named)
720
- return uid
721
-
722
-
723
- ## Notes [param named], a reference just written by path alone, under [member by_path] when the uid
724
- ## cache holds nothing for it: a file made since the project was last scanned, whose uid the cache
725
- ## learns from the next scan. Not a file whose uid the cache gives to another, which a scan leaves.
726
- static func note_by_path(named: String) -> void:
727
- if Engine.is_editor_hint() or not FileAccess.file_exists(named) or by_path.has(named):
728
- return
729
- var own: int = uid_in(named)
730
- if own == ResourceUID.INVALID_ID and FileAccess.file_exists(named + ".uid"):
731
- own = ResourceUID.text_to_id(FileAccess.get_file_as_string(named + ".uid").strip_edges())
732
- if own == ResourceUID.INVALID_ID or not ResourceUID.has_id(own):
733
- var _noted: bool = by_path.append(named)
734
-
735
-
736
- ## The uid attribute a reference to [param named] is written with, as ' uid="uid://..."', or "".
737
- ##
738
- ## Only a uid the uid cache gives that path, which is what the loader resolves it by. A uid the cache
739
- ## does not know (a file made with no editor since the last scan) is answered with a warning on every
740
- ## load, and one it gives another file (a scene copied whole keeps its original's) loads that file.
741
- static func uid_attribute(named: String) -> String:
742
- var id: int = ResourceLoader.get_resource_uid(named)
743
- if id == ResourceUID.INVALID_ID or not ResourceUID.has_id(id) or ResourceUID.get_id_path(id) != named:
744
- return ""
745
- return ' uid="%s"' % ResourceUID.id_to_text(id)
746
-
747
-
748
- ## The uid the file at [param file_path] declares, or [constant ResourceUID.INVALID_ID].
749
- static func uid_in(file_path: String) -> int:
750
- if not FileAccess.file_exists(file_path):
751
- return ResourceUID.INVALID_ID
752
- var file: FileAccess = FileAccess.open(file_path, FileAccess.READ)
753
- if file != null:
754
- var header: RegExMatch = RegEx.create_from_string('uid="(uid://[a-z0-9]+)"').search(file.get_line())
755
- file.close()
756
- if header != null:
757
- return ResourceUID.text_to_id(header.get_string(1))
758
- return ResourceLoader.get_resource_uid(file_path)
759
-
760
-
761
692
  ## Brings the editor up to date with a scene file written under it: the cached copy anything holds,
762
693
  ## the editor's file list, and the tab showing it, whether current or not.
763
694
  static func refresh(scene_path: String) -> void:
@@ -832,7 +763,11 @@ func _settled(packed: PackedScene) -> Array:
832
763
  for found: Array in stray:
833
764
  var node: Node = found[0]
834
765
  var name: String = found[1]
835
- 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
836
771
  for found: Array in moved:
837
772
  var node: Node = found[0]
838
773
  var before: Dictionary = _before[node.get_instance_id()]
@@ -902,6 +837,8 @@ func _strays(state: SceneState) -> Array[Array]:
902
837
  continue
903
838
  if not held and GdhHeadlessCheck.at_default(node, name):
904
839
  continue
840
+ if _engine_kept.has("%d:%s" % [id, name]):
841
+ continue
905
842
  stray.append([node, name, held, filed.get(name)])
906
843
  return stray
907
844
 
@@ -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
@@ -223,8 +223,7 @@ func deserialize_value(value: Variant) -> Variant:
223
223
  # under the target project's warning levels, so one such project lost every headless call.
224
224
  var tag: String = str(fields["_type"])
225
225
  var builder: String = BUILDERS.get(tag, "")
226
- var spare: Array = GdhShapes.spare(fields, tag)
227
- 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):
228
227
  return fields
229
228
  if tag == "float" and str(fields["value"]) not in ["inf", "-inf", "nan"]:
230
229
  return fields
@@ -277,24 +276,7 @@ static func components_of(value: Variant) -> PackedStringArray:
277
276
  TYPE_VECTOR4, TYPE_VECTOR4I, TYPE_QUATERNION:
278
277
  return PackedStringArray(["x", "y", "z", "w"])
279
278
  TYPE_COLOR:
280
- return PackedStringArray(
281
- [
282
- "r",
283
- "g",
284
- "b",
285
- "a",
286
- "r8",
287
- "g8",
288
- "b8",
289
- "a8",
290
- "h",
291
- "s",
292
- "v",
293
- "ok_hsl_h",
294
- "ok_hsl_s",
295
- "ok_hsl_l"
296
- ]
297
- )
279
+ return "r g b a r8 g8 b8 a8 h s v ok_hsl_h ok_hsl_s ok_hsl_l".split(" ")
298
280
  TYPE_RECT2, TYPE_RECT2I, TYPE_AABB:
299
281
  return PackedStringArray(["position", "size", "end"])
300
282
  TYPE_PLANE:
@@ -405,7 +387,8 @@ func fitted(value: Variant, type: int) -> Variant:
405
387
  return built
406
388
 
407
389
  if rebuilt is Array and LIST_TYPES.has(type):
408
- return type_convert(rebuilt, type)
390
+ var items: Array = value if value is Array else rebuilt
391
+ return GdhShapes.packed_from(items, type, fitted)
409
392
 
410
393
  # The inspector and every colour picker give a colour as its html code. Anything else stays the
411
394
  # text it is, for the refusal to name.
@@ -705,7 +688,13 @@ func _describe_built_in(resource: Resource) -> Dictionary:
705
688
  var script: Script = null
706
689
  if attached is Script:
707
690
  script = attached
708
- 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())
709
698
  var fresh: Object = _fresh(script)
710
699
  for property: Dictionary in resource.get_property_list():
711
700
  var usage: int = property.get("usage", 0)
@@ -0,0 +1,121 @@
1
+ extends RefCounted
2
+
3
+ ## Project settings read the same way headless and from the open editor, because the two are one
4
+ ## question asked of two places: a field one of them left out read as the editor and the file
5
+ ## disagreeing about something they agree about.
6
+
7
+ const GdhRead = preload("reading.gd")
8
+ const GdhSerialisation = preload("serialisation.gd")
9
+
10
+
11
+ ## One setting: whether the engine knows it, its value, whether that is the engine's default, and
12
+ ## the feature overrides that replace it in some builds.
13
+ static func one(setting_path: String, values: GdhSerialisation) -> Dictionary:
14
+ var result: Dictionary = {
15
+ "setting_path": setting_path, "exists": ProjectSettings.has_setting(setting_path)
16
+ }
17
+
18
+ if result["exists"]:
19
+ var value: Variant = ProjectSettings.get_setting(setting_path)
20
+ result["value"] = values.serialize_value(value)
21
+ # A setting the engine registers exists whether or not the project sets it, so exists did
22
+ # not say whether project.godot has a line for it. At its default it has none: the engine
23
+ # drops such a key when it saves.
24
+ var default_value: Variant = ProjectSettings.property_get_revert(setting_path)
25
+ if default_value != null:
26
+ result["at_default"] = value == default_value
27
+ else:
28
+ result["value"] = null
29
+ result["message"] = "Setting does not exist"
30
+
31
+ # An override exists only while project.godot sets it, so one the file does not set read as a
32
+ # setting nobody had heard of, when a build with that feature reads its base setting instead.
33
+ var base: String = override_base(setting_path)
34
+ if not base.is_empty():
35
+ result["override_of"] = base
36
+ if not result["exists"] and ProjectSettings.has_setting(base):
37
+ result["holds"] = values.serialize_value(ProjectSettings.get_setting(base))
38
+ result["message"] = overridden_by_none(setting_path, values)
39
+
40
+ # The value under this name is not the one every build reads: `name.windows` replaces it on
41
+ # Windows, `name.editor` in the editor. Read alone, a caller saw the plain value and not the one
42
+ # their game showed.
43
+ var overrides: Dictionary = overrides_of(setting_path, values)
44
+ if not overrides.is_empty():
45
+ result["overrides"] = overrides
46
+
47
+ return result
48
+
49
+
50
+ ## Every setting whose name starts with [param prefix], with the type the engine registers for
51
+ ## each.
52
+ ##
53
+ ## The type is here because it is the half a name hides. A setting sitting among a family of levels
54
+ ## can be a bool, and a caller that assumes otherwise writes a level over it and sees nothing go
55
+ ## wrong: 2 is as true as true is.
56
+ static func under(prefix: String, values: GdhSerialisation) -> Dictionary:
57
+ var found: Array[Dictionary] = []
58
+ for property: Dictionary in ProjectSettings.get_property_list():
59
+ var name: String = str(property.get("name", ""))
60
+ if not name.begins_with(prefix):
61
+ continue
62
+ var kind: int = GdhRead.as_int(property.get("type", TYPE_NIL), TYPE_NIL)
63
+ var value: Variant = ProjectSettings.get_setting(name)
64
+ var described: Dictionary = {
65
+ "setting": name,
66
+ "type": type_string(kind),
67
+ "value": values.serialize_value(value),
68
+ }
69
+ # As a single get says it, so a family read can be told apart into what the project sets
70
+ # and the engine's defaults: "input/" is ninety actions the engine ships and the project's few.
71
+ var default_value: Variant = ProjectSettings.property_get_revert(name)
72
+ if default_value != null:
73
+ described["at_default"] = value == default_value
74
+ var base: String = override_base(name)
75
+ if not base.is_empty():
76
+ described["override_of"] = base
77
+ found.append(described)
78
+
79
+ found.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return a["setting"] < b["setting"])
80
+ return {"prefix": prefix, "count": found.size(), "settings": found}
81
+
82
+
83
+ ## The setting [param setting_path] overrides for builds with one feature, as
84
+ ## `application/run/max_fps.mobile` does `application/run/max_fps`, or "" for a plain setting.
85
+ static func override_base(setting_path: String) -> String:
86
+ var dot: int = setting_path.find(".", setting_path.rfind("/") + 1)
87
+ return "" if dot == -1 else setting_path.substr(0, dot)
88
+
89
+
90
+ ## The overrides project.godot sets for [param setting_path], by feature.
91
+ static func overrides_of(setting_path: String, values: GdhSerialisation) -> Dictionary:
92
+ var overrides: Dictionary = {}
93
+ for info: Dictionary in ProjectSettings.get_property_list():
94
+ var name: String = str(info.get("name", ""))
95
+ if name.begins_with(setting_path + "."):
96
+ overrides[name.substr(setting_path.length() + 1)] = values.serialize_value(
97
+ ProjectSettings.get_setting(name)
98
+ )
99
+ return overrides
100
+
101
+
102
+ ## What a build with the feature of [param setting_path], an override project.godot does not set,
103
+ ## reads in its place.
104
+ static func overridden_by_none(setting_path: String, values: GdhSerialisation) -> String:
105
+ var base: String = override_base(setting_path)
106
+ var feature: String = setting_path.substr(base.length() + 1)
107
+ var said: String = (
108
+ (
109
+ "project.godot sets no %s override of %s, so a build with the %s feature reads %s, "
110
+ + "the value under holds"
111
+ )
112
+ % [feature, base, feature, base]
113
+ )
114
+ var others: Array[String] = []
115
+ for name: String in overrides_of(base, values).keys():
116
+ if name != feature:
117
+ others.append(name)
118
+ if others.is_empty():
119
+ return said + "."
120
+ others.sort()
121
+ return said + (", unless it also has a feature with an override: %s." % ", ".join(others))
@@ -0,0 +1 @@
1
+ uid://c1ncxrof35y6e