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.
- package/build/cli.js +4668 -3229
- 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 +115 -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 +112 -6
- package/build/godot/addons/gdharness_editor/scene_file.gd +117 -174
- 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 +19 -29
- 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 +39 -6
- package/build/godot/addons/gdharness_editor/tools/animation_states.gd +263 -0
- package/build/godot/addons/gdharness_editor/tools/animation_states.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +49 -414
- 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 +162 -483
- package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +16 -0
- package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +8 -4
- package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +2 -1
- package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +10 -0
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +151 -8
- package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +7 -30
- package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +190 -38
- 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/call_errors.gd +8 -2
- package/build/godot/addons/gdharness_runtime/declared_values.gd +272 -0
- package/build/godot/addons/gdharness_runtime/reading.gd +112 -6
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +35 -3
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +5 -0
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +188 -30
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +107 -104
- 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 +320 -27
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +165 -20
- package/build/godot/addons/gdharness_runtime/runtime_screen.gd +49 -3
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +31 -2
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +250 -10
- 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 +19 -29
- 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 +29 -13
- package/build/godot/operations/export_presets.gd +602 -4
- package/build/godot/operations/file_reads.gd +115 -18
- package/build/godot/operations/gdscript_analysis.gd +14 -0
- package/build/godot/operations/gdscript_authoring.gd +12 -2
- package/build/godot/operations/godot_operations.gd +18 -2
- package/build/godot/operations/import_pipeline.gd +20 -221
- package/build/godot/operations/input_actions.gd +127 -51
- package/build/godot/operations/project_config.gd +113 -68
- 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 +112 -6
- package/build/godot/operations/scene_connections.gd +222 -0
- package/build/godot/operations/serialisation.gd +19 -29
- 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 +1402 -707
- 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
|
-
|
|
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 = ""
|
|
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
|
-
|
|
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
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
|
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
|
|
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
|
|
535
|
+
var held: Dictionary = {}
|
|
537
536
|
if _state == null:
|
|
538
|
-
return
|
|
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
|
|
543
|
-
|
|
544
|
-
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
146
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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)
|