gdharness 1.3.9 → 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.
- package/build/cli.js +4563 -3195
- package/build/godot/addons/gdharness_editor/animation_reads.gd +115 -4
- package/build/godot/addons/gdharness_editor/declared_values.gd +272 -0
- package/build/godot/addons/gdharness_editor/file_reads.gd +102 -18
- package/build/godot/addons/gdharness_editor/headless_check.gd +49 -2
- package/build/godot/addons/gdharness_editor/property_paths.gd +89 -16
- package/build/godot/addons/gdharness_editor/property_values.gd +180 -121
- package/build/godot/addons/gdharness_editor/reading.gd +51 -1
- package/build/godot/addons/gdharness_editor/scene_file.gd +77 -140
- package/build/godot/addons/gdharness_editor/scene_uids.gd +107 -0
- package/build/godot/addons/gdharness_editor/scene_uids.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/serialisation.gd +11 -22
- package/build/godot/addons/gdharness_editor/setting_reads.gd +121 -0
- package/build/godot/addons/gdharness_editor/setting_reads.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tool_executor.gd +28 -5
- package/build/godot/addons/gdharness_editor/tools/animation_states.gd +39 -20
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +49 -362
- package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +464 -0
- package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/class_tools.gd +37 -0
- package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd +221 -0
- package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +147 -492
- package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +16 -0
- package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +4 -3
- package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +2 -1
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +79 -5
- package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +7 -30
- package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +183 -37
- package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd +485 -0
- package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/value_shapes.gd +128 -1
- package/build/godot/addons/gdharness_runtime/declared_values.gd +272 -0
- package/build/godot/addons/gdharness_runtime/reading.gd +51 -1
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +35 -3
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +185 -30
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +86 -79
- package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +17 -0
- package/build/godot/addons/gdharness_runtime/runtime_pad.gd +13 -4
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +309 -22
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +163 -20
- package/build/godot/addons/gdharness_runtime/runtime_screen.gd +49 -3
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +5 -2
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +146 -5
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +279 -11
- package/build/godot/addons/gdharness_runtime/runtime_words.gd +27 -0
- package/build/godot/addons/gdharness_runtime/serialisation.gd +11 -22
- package/build/godot/addons/gdharness_runtime/value_shapes.gd +128 -1
- package/build/godot/operations/animation_reads.gd +115 -4
- package/build/godot/operations/audio_buses.gd +93 -7
- package/build/godot/operations/classdb_queries.gd +119 -7
- package/build/godot/operations/declared_values.gd +272 -0
- package/build/godot/operations/dependencies.gd +154 -41
- package/build/godot/operations/editor_tools.gd +22 -14
- package/build/godot/operations/export_presets.gd +602 -4
- package/build/godot/operations/file_reads.gd +102 -18
- package/build/godot/operations/gdscript_authoring.gd +11 -1
- package/build/godot/operations/godot_operations.gd +16 -2
- package/build/godot/operations/import_pipeline.gd +20 -221
- package/build/godot/operations/input_actions.gd +108 -47
- package/build/godot/operations/project_config.gd +83 -69
- package/build/godot/operations/project_diagnostics.gd +4 -0
- package/build/godot/operations/project_validation.gd +266 -0
- package/build/godot/operations/property_paths.gd +89 -16
- package/build/godot/operations/property_values.gd +180 -121
- package/build/godot/operations/reading.gd +51 -1
- package/build/godot/operations/scene_connections.gd +222 -0
- package/build/godot/operations/serialisation.gd +11 -22
- package/build/godot/operations/setting_reads.gd +121 -0
- package/build/godot/operations/value_shapes.gd +128 -1
- package/build/godot/tests/listed.gd +20 -13
- package/build/keeper.js +1371 -706
- 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
|
-
|
|
35
|
-
|
|
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
|
-
##
|
|
48
|
-
## gathered for
|
|
49
|
-
static var
|
|
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
|
|
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
|
|
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
|
|
535
|
+
var held: Dictionary = {}
|
|
551
536
|
if _state == null:
|
|
552
|
-
return
|
|
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
|
|
557
|
-
|
|
558
|
-
return
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|