gdharness 1.2.41 → 1.2.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/cli.js +84 -26
- package/build/godot/addons/gdharness_editor/animation_reads.gd +7 -4
- package/build/godot/addons/gdharness_editor/file_reads.gd +134 -10
- package/build/godot/addons/gdharness_editor/headless_check.gd +34 -4
- package/build/godot/addons/gdharness_editor/reading.gd +25 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd +209 -15
- package/build/godot/addons/gdharness_editor/serialisation.gd +5 -1
- package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +346 -0
- package/build/godot/addons/gdharness_editor/tools/scene_kin.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +438 -0
- package/build/godot/addons/gdharness_editor/tools/scene_paths.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +90 -48
- package/build/godot/addons/gdharness_runtime/reading.gd +25 -0
- package/build/godot/addons/gdharness_runtime/serialisation.gd +5 -1
- package/build/godot/operations/animation_reads.gd +7 -4
- package/build/godot/operations/editor_tools.gd +19 -1
- package/build/godot/operations/file_reads.gd +134 -10
- package/build/godot/operations/reading.gd +25 -0
- package/build/godot/operations/serialisation.gd +5 -1
- package/build/index.js +84 -26
- package/package.json +1 -1
|
@@ -5,22 +5,14 @@ extends Node
|
|
|
5
5
|
|
|
6
6
|
const FileReads = preload("../file_reads.gd")
|
|
7
7
|
const Read = preload("../reading.gd")
|
|
8
|
+
const Kin = preload("scene_kin.gd")
|
|
9
|
+
const Paths = preload("scene_paths.gd")
|
|
8
10
|
const Placing = preload("scene_placing.gd")
|
|
9
11
|
const PropertyValues = preload("../property_values.gd")
|
|
10
12
|
const ResourceTools = preload("resource_tools.gd")
|
|
11
13
|
const SceneFile = preload("../scene_file.gd")
|
|
12
14
|
const Serialisation = preload("../serialisation.gd")
|
|
13
15
|
|
|
14
|
-
## The names Object.ConnectFlags gives each bit. The number alone left a reader guessing that 2 is
|
|
15
|
-
## the persistence every connection in a saved scene carries.
|
|
16
|
-
const CONNECT_FLAG_NAMES: Dictionary[int, String] = {
|
|
17
|
-
Object.CONNECT_DEFERRED: "deferred",
|
|
18
|
-
Object.CONNECT_PERSIST: "persist",
|
|
19
|
-
Object.CONNECT_ONE_SHOT: "one_shot",
|
|
20
|
-
Object.CONNECT_REFERENCE_COUNTED: "reference_counted",
|
|
21
|
-
Object.CONNECT_APPEND_SOURCE_OBJECT: "append_source_object",
|
|
22
|
-
}
|
|
23
|
-
|
|
24
16
|
var _editor_plugin: EditorPlugin = null
|
|
25
17
|
var _values: Serialisation = Serialisation.new()
|
|
26
18
|
var _properties: PropertyValues = PropertyValues.new()
|
|
@@ -30,19 +22,6 @@ func set_editor_plugin(plugin: EditorPlugin) -> void:
|
|
|
30
22
|
_editor_plugin = plugin
|
|
31
23
|
|
|
32
24
|
|
|
33
|
-
## Each bit set in [param flags] by name, and whatever bits are left unnamed as one number.
|
|
34
|
-
static func _flag_names(flags: int) -> Array[String]:
|
|
35
|
-
var names: Array[String] = []
|
|
36
|
-
var rest: int = flags
|
|
37
|
-
for bit: int in CONNECT_FLAG_NAMES:
|
|
38
|
-
if flags & bit:
|
|
39
|
-
names.append(CONNECT_FLAG_NAMES[bit])
|
|
40
|
-
rest &= ~bit
|
|
41
|
-
if rest != 0:
|
|
42
|
-
names.append(str(rest))
|
|
43
|
-
return names
|
|
44
|
-
|
|
45
|
-
|
|
46
25
|
func _ensure_res_path(path: String) -> String:
|
|
47
26
|
if not path.begins_with("res://"):
|
|
48
27
|
return "res://" + path
|
|
@@ -175,17 +154,42 @@ func _collect_nodes_recursive(node: Node, path: String, out_nodes: Array) -> voi
|
|
|
175
154
|
_collect_nodes_recursive(child, child_path, out_nodes)
|
|
176
155
|
|
|
177
156
|
|
|
157
|
+
func _inherited(args: Dictionary) -> Dictionary:
|
|
158
|
+
var project_path: String = str(args.get("projectPath", ""))
|
|
159
|
+
var script_path: String = str(args.get("script", ""))
|
|
160
|
+
return Kin.inherited(
|
|
161
|
+
args,
|
|
162
|
+
_new_scene_path(args),
|
|
163
|
+
_to_scene_res_path(project_path, str(args.get("inherits", ""))),
|
|
164
|
+
"" if script_path.is_empty() else _to_scene_res_path(project_path, script_path)
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
## The res:// path a create in [param args] writes, given .tscn when it has no extension, or "".
|
|
169
|
+
func _new_scene_path(args: Dictionary) -> String:
|
|
170
|
+
var scene_path: String = _to_scene_res_path(
|
|
171
|
+
str(args.get("projectPath", "")), str(args.get("scenePath", ""))
|
|
172
|
+
)
|
|
173
|
+
if scene_path == "res://":
|
|
174
|
+
return ""
|
|
175
|
+
return scene_path if scene_path.ends_with(".tscn") else scene_path + ".tscn"
|
|
176
|
+
|
|
177
|
+
|
|
178
178
|
func create_scene(args: Dictionary) -> Dictionary:
|
|
179
|
+
if str(args.get("inherits", "")).is_empty():
|
|
180
|
+
return _create_of_a_class(args)
|
|
181
|
+
return _inherited(args)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
func _create_of_a_class(args: Dictionary) -> Dictionary:
|
|
179
185
|
var project_path: String = str(args.get("projectPath", ""))
|
|
180
|
-
var scene_path: String =
|
|
186
|
+
var scene_path: String = _new_scene_path(args)
|
|
181
187
|
var root_node_type: String = str(args.get("rootNodeType", "Node"))
|
|
182
188
|
var root_node_name: String = str(args.get("rootNodeName", root_node_type))
|
|
183
189
|
var script_path: String = str(args.get("script", ""))
|
|
184
190
|
|
|
185
|
-
if scene_path
|
|
191
|
+
if scene_path.is_empty():
|
|
186
192
|
return {"ok": false, "error": "Missing scenePath"}
|
|
187
|
-
if not scene_path.ends_with(".tscn"):
|
|
188
|
-
scene_path += ".tscn"
|
|
189
193
|
var asked_type: String = root_node_type
|
|
190
194
|
var made: Dictionary = SceneFile.made_as(
|
|
191
195
|
root_node_type,
|
|
@@ -208,19 +212,8 @@ func create_scene(args: Dictionary) -> Dictionary:
|
|
|
208
212
|
var unfit: String = SceneFile.unfit_name(root_node_name)
|
|
209
213
|
if not unfit.is_empty():
|
|
210
214
|
return {"ok": false, "error": unfit}
|
|
211
|
-
# Saving over a scene that is there replaced it, nodes and all, and answered as a new scene.
|
|
212
215
|
if FileAccess.file_exists(scene_path):
|
|
213
|
-
return
|
|
214
|
-
"ok": false,
|
|
215
|
-
"error":
|
|
216
|
-
(
|
|
217
|
-
(
|
|
218
|
-
"%s already exists, and scene_create op=create would replace everything in it. Edit it "
|
|
219
|
-
+ "with scene_node, or create the scene under another path."
|
|
220
|
-
)
|
|
221
|
-
% scene_path
|
|
222
|
-
)
|
|
223
|
-
}
|
|
216
|
+
return Kin.taken(scene_path)
|
|
224
217
|
|
|
225
218
|
var root: Node = ClassDB.instantiate(root_node_type)
|
|
226
219
|
root.name = root_node_name
|
|
@@ -395,6 +388,8 @@ func delete_node(args: Dictionary) -> Dictionary:
|
|
|
395
388
|
var at: String = scene.path_of(node)
|
|
396
389
|
node.get_parent().remove_child(node)
|
|
397
390
|
node.free()
|
|
391
|
+
var carried: Paths = Paths.new()
|
|
392
|
+
var _rewritten: int = carried.carry(scene, {at: ""})
|
|
398
393
|
|
|
399
394
|
var written: Dictionary = scene.write(
|
|
400
395
|
func(saved: Node) -> String:
|
|
@@ -405,7 +400,9 @@ func delete_node(args: Dictionary) -> Dictionary:
|
|
|
405
400
|
if not written.is_empty():
|
|
406
401
|
return written
|
|
407
402
|
|
|
408
|
-
|
|
403
|
+
var answer: Dictionary = {"ok": true, "deletedNodePath": at}
|
|
404
|
+
answer.merge(Kin.follow(scene_path, at, "", carried))
|
|
405
|
+
return answer
|
|
409
406
|
|
|
410
407
|
|
|
411
408
|
func duplicate_node(args: Dictionary) -> Dictionary:
|
|
@@ -458,6 +455,7 @@ func duplicate_node(args: Dictionary) -> Dictionary:
|
|
|
458
455
|
scene.copied(copies)
|
|
459
456
|
|
|
460
457
|
var new_path: String = scene.path_of(duplicated_node)
|
|
458
|
+
var placed: Array = [scene.path_of(target_parent), duplicated_node.get_index()]
|
|
461
459
|
var written: Dictionary = scene.write(
|
|
462
460
|
func(saved: Node) -> String:
|
|
463
461
|
if saved.get_node_or_null(new_path) == null:
|
|
@@ -467,7 +465,14 @@ func duplicate_node(args: Dictionary) -> Dictionary:
|
|
|
467
465
|
if not written.is_empty():
|
|
468
466
|
return written
|
|
469
467
|
|
|
470
|
-
return {
|
|
468
|
+
return {
|
|
469
|
+
"ok": true,
|
|
470
|
+
"nodePath": node_path,
|
|
471
|
+
"newName": new_name,
|
|
472
|
+
"newNodePath": new_path,
|
|
473
|
+
"parentNodePath": placed[0],
|
|
474
|
+
"index": placed[1],
|
|
475
|
+
}
|
|
471
476
|
|
|
472
477
|
|
|
473
478
|
func reparent_node(args: Dictionary) -> Dictionary:
|
|
@@ -525,6 +530,8 @@ func reparent_node(args: Dictionary) -> Dictionary:
|
|
|
525
530
|
|
|
526
531
|
var new_path: String = scene.path_of(node)
|
|
527
532
|
var index: int = node.get_index()
|
|
533
|
+
var carried: Paths = Paths.new()
|
|
534
|
+
var _rewritten: int = carried.carry(scene, {old_path: new_path} if new_path != old_path else {})
|
|
528
535
|
var written: Dictionary = scene.write(
|
|
529
536
|
func(saved: Node) -> String:
|
|
530
537
|
var copy: Node = saved.get_node_or_null(new_path)
|
|
@@ -537,15 +544,19 @@ func reparent_node(args: Dictionary) -> Dictionary:
|
|
|
537
544
|
return ""
|
|
538
545
|
)
|
|
539
546
|
if not written.is_empty():
|
|
547
|
+
carried.put_back(scene_path)
|
|
540
548
|
return written
|
|
541
549
|
|
|
542
|
-
|
|
550
|
+
var answer: Dictionary = {
|
|
543
551
|
"ok": true,
|
|
544
552
|
"nodePath": old_path,
|
|
545
553
|
"newParentPath": new_parent_path,
|
|
546
554
|
"newNodePath": new_path,
|
|
547
555
|
"index": index,
|
|
548
556
|
}
|
|
557
|
+
if new_path != old_path:
|
|
558
|
+
answer.merge(Kin.follow(scene_path, old_path, new_path, carried))
|
|
559
|
+
return answer
|
|
549
560
|
|
|
550
561
|
|
|
551
562
|
func set_node_properties(args: Dictionary) -> Dictionary:
|
|
@@ -553,6 +564,9 @@ func set_node_properties(args: Dictionary) -> Dictionary:
|
|
|
553
564
|
var scene_path: String = _to_scene_res_path(project_path, str(args.get("scenePath", "")))
|
|
554
565
|
var node_path: String = str(args.get("nodePath", "."))
|
|
555
566
|
var properties: Dictionary = _parse_properties_arg(args.get("properties", {}))
|
|
567
|
+
var editable: Variant = args.get("editableChildren")
|
|
568
|
+
if properties.is_empty() and editable == null:
|
|
569
|
+
return {"ok": false, "error": "set takes properties, editableChildren, or both."}
|
|
556
570
|
|
|
557
571
|
var opened: Array = _open(scene_path, true)
|
|
558
572
|
var scene: SceneFile = opened[0]
|
|
@@ -574,6 +588,15 @@ func set_node_properties(args: Dictionary) -> Dictionary:
|
|
|
574
588
|
if not unusable.is_empty():
|
|
575
589
|
return scene.refuse(unusable)
|
|
576
590
|
|
|
591
|
+
var dropped: Dictionary = {}
|
|
592
|
+
if editable != null:
|
|
593
|
+
var not_an_instance: String = Kin.set_editable(scene, node, Read.as_bool(editable))
|
|
594
|
+
if not not_an_instance.is_empty():
|
|
595
|
+
return scene.refuse(not_an_instance)
|
|
596
|
+
if not Read.as_bool(editable):
|
|
597
|
+
dropped = scene.held_under(scene.path_of(node))
|
|
598
|
+
|
|
599
|
+
var old_path: String = scene.path_of(node)
|
|
577
600
|
scene.asked(node, properties.keys())
|
|
578
601
|
var refused_property: String = _properties.write_all(node, properties)
|
|
579
602
|
if not refused_property.is_empty():
|
|
@@ -583,13 +606,29 @@ func set_node_properties(args: Dictionary) -> Dictionary:
|
|
|
583
606
|
for key: Variant in properties:
|
|
584
607
|
held[str(key)] = _readable(node, _properties.read_at(node, str(key)))
|
|
585
608
|
var new_path: String = scene.path_of(node)
|
|
609
|
+
var carried: Paths = Paths.new()
|
|
610
|
+
var _rewritten: int = carried.carry(scene, {old_path: new_path} if new_path != old_path else {})
|
|
586
611
|
var written: Dictionary = scene.write(
|
|
587
|
-
func(saved: Node) -> String:
|
|
612
|
+
func(saved: Node) -> String:
|
|
613
|
+
var unkept: String = _kept(scene, saved, node, properties.keys())
|
|
614
|
+
if unkept.is_empty() and editable != null:
|
|
615
|
+
if saved.is_editable_instance(saved.get_node(new_path)) != Read.as_bool(editable):
|
|
616
|
+
return "%s would load with editableChildren %s." % [new_path, not Read.as_bool(editable)]
|
|
617
|
+
return unkept
|
|
588
618
|
)
|
|
589
619
|
if not written.is_empty():
|
|
620
|
+
carried.put_back(scene_path)
|
|
590
621
|
return written
|
|
591
622
|
|
|
592
|
-
|
|
623
|
+
var answer: Dictionary = {"ok": true, "nodePath": new_path, "properties": held}
|
|
624
|
+
if editable != null:
|
|
625
|
+
answer["editableChildren"] = Read.as_bool(editable)
|
|
626
|
+
if not dropped.is_empty():
|
|
627
|
+
answer["droppedValues"] = dropped["values"]
|
|
628
|
+
answer["droppedNodes"] = dropped["nodes"]
|
|
629
|
+
if new_path != old_path:
|
|
630
|
+
answer.merge(Kin.follow(scene_path, old_path, new_path, carried))
|
|
631
|
+
return answer
|
|
593
632
|
|
|
594
633
|
|
|
595
634
|
func get_node_properties(args: Dictionary) -> Dictionary:
|
|
@@ -614,8 +653,11 @@ func get_node_properties(args: Dictionary) -> Dictionary:
|
|
|
614
653
|
# names this scene's file sets are always answered.
|
|
615
654
|
var props: Dictionary = _properties_of(node, include_defaults, scene.set_here(node))
|
|
616
655
|
var cells: Dictionary = ResourceTools.cells_of(node)
|
|
617
|
-
scene.close()
|
|
618
656
|
var answer: Dictionary = {"ok": true, "nodePath": node_path, "properties": props}
|
|
657
|
+
if node != scene.root and node.owner != null and not node.scene_file_path.is_empty():
|
|
658
|
+
answer["instanceOf"] = node.scene_file_path
|
|
659
|
+
answer["editableChildren"] = scene.root.is_editable_instance(node)
|
|
660
|
+
scene.close()
|
|
619
661
|
# Only an editor holds anything unsaved; a headless engine answering false would say it looked.
|
|
620
662
|
if Engine.is_editor_hint():
|
|
621
663
|
answer["unsavedInEditor"] = SceneFile.unsaved(scene_path)
|
|
@@ -770,7 +812,7 @@ func connect_signal(args: Dictionary) -> Dictionary:
|
|
|
770
812
|
"targetNodePath": to,
|
|
771
813
|
"methodName": method_name,
|
|
772
814
|
"flags": saved_flags[0],
|
|
773
|
-
"flagNames":
|
|
815
|
+
"flagNames": SceneFile.flag_names(Read.as_int(saved_flags[0])),
|
|
774
816
|
# Connected either way, as the editor's own dialog allows, since the method may be written
|
|
775
817
|
# next; a caller told nothing learnt of a misspelt one only when the signal fired.
|
|
776
818
|
"methodFound": method_found
|
|
@@ -781,7 +823,7 @@ func connect_signal(args: Dictionary) -> Dictionary:
|
|
|
781
823
|
if flags_before >= 0:
|
|
782
824
|
answer["alreadyConnected"] = true
|
|
783
825
|
if flags_before != Read.as_int(saved_flags[0]):
|
|
784
|
-
answer["flagsBefore"] =
|
|
826
|
+
answer["flagsBefore"] = SceneFile.flag_names(flags_before)
|
|
785
827
|
return answer
|
|
786
828
|
|
|
787
829
|
|
|
@@ -934,7 +976,7 @@ func list_connections(args: Dictionary) -> Dictionary:
|
|
|
934
976
|
"targetNodePath": target_path,
|
|
935
977
|
"methodName": str(callable.get_method()),
|
|
936
978
|
"flags": flags,
|
|
937
|
-
"flagNames":
|
|
979
|
+
"flagNames": SceneFile.flag_names(flags),
|
|
938
980
|
# False for a connection an instanced scene makes inside itself, which only that
|
|
939
981
|
# scene can change.
|
|
940
982
|
"editable": editable
|
|
@@ -22,6 +22,9 @@ extends RefCounted
|
|
|
22
22
|
# number was expected. Absent keys never reach here: the callers pass their own default to
|
|
23
23
|
# `Dictionary.get`, so what arrives is that default.
|
|
24
24
|
|
|
25
|
+
## How many of a node's children a refusal for a path missing below it names.
|
|
26
|
+
const NAMED_CHILDREN: int = 12
|
|
27
|
+
|
|
25
28
|
|
|
26
29
|
static func as_int(value: Variant, fallback: int = 0) -> int:
|
|
27
30
|
if value is int:
|
|
@@ -114,3 +117,25 @@ static func from_the_root(said: String, node_path: String, root_name: String) ->
|
|
|
114
117
|
if not rest.is_empty():
|
|
115
118
|
how += ', so this one is "%s"' % rest
|
|
116
119
|
return "%s%s %s." % [said, "" if said.ends_with(".") else ".", how]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
## [param said], the refusal for a path a scene does not have, with where the path leaves it: the
|
|
123
|
+
## last node of the path it has, [param reached] from the root, called [param root_name], when that
|
|
124
|
+
## is empty; the name below it that is missing; and [param children], the names that node has, as
|
|
125
|
+
## after a node was moved or renamed. Names up to [constant NAMED_CHILDREN] of them.
|
|
126
|
+
static func missing_below(
|
|
127
|
+
said: String, root_name: String, reached: PackedStringArray, missing: String, children: PackedStringArray
|
|
128
|
+
) -> String:
|
|
129
|
+
var shown: String = ", ".join(children.slice(0, NAMED_CHILDREN))
|
|
130
|
+
if children.size() > NAMED_CHILDREN:
|
|
131
|
+
shown += " and %d more" % (children.size() - NAMED_CHILDREN)
|
|
132
|
+
return (
|
|
133
|
+
"%s%s %s has no %s; %s."
|
|
134
|
+
% [
|
|
135
|
+
said,
|
|
136
|
+
"" if said.ends_with(".") else ".",
|
|
137
|
+
"The root, %s," % root_name if reached.is_empty() else "/".join(reached),
|
|
138
|
+
missing,
|
|
139
|
+
"its children are " + shown if not children.is_empty() else "it has no children",
|
|
140
|
+
]
|
|
141
|
+
)
|
|
@@ -664,8 +664,12 @@ func _serialize_array(value: Array) -> Array:
|
|
|
664
664
|
|
|
665
665
|
# Base64 rather than a list of numbers. A byte array is a texture, a sound or a save file often
|
|
666
666
|
# enough that the list would be the whole answer, megabytes of it, and unreadable either way.
|
|
667
|
+
#
|
|
668
|
+
# The engine's encoder raises an error on an empty array, and an empty tile_map_data is every
|
|
669
|
+
# TileMapLayer with nothing painted yet, so a read of one carried an engine error in its answer.
|
|
667
670
|
func _serialize_bytes(value: PackedByteArray) -> Dictionary:
|
|
668
|
-
|
|
671
|
+
var text: String = "" if value.is_empty() else Marshalls.raw_to_base64(value)
|
|
672
|
+
return {"base64": text, "size": value.size(), "_type": "PackedByteArray"}
|
|
669
673
|
|
|
670
674
|
|
|
671
675
|
# The packed vector and colour arrays, each element serialised the way one of them on its own is.
|
|
@@ -62,10 +62,13 @@ static func _bare(scene_path: String, node_path: String, kind: String) -> Array:
|
|
|
62
62
|
var type: String = str(read.get("type", ""))
|
|
63
63
|
if not ClassDB.class_exists(type) or not ClassDB.is_parent_class(type, kind):
|
|
64
64
|
var said: String = "%s not found at: %s" % [kind, node_path]
|
|
65
|
-
if read.is_empty()
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
if read.is_empty():
|
|
66
|
+
var hinted: String = said
|
|
67
|
+
if FileReads.scene_node(scene_path, node_path.get_slice("/", 0)).is_empty():
|
|
68
|
+
hinted = Read.from_the_root(
|
|
69
|
+
said, node_path, str(FileReads.scene_node(scene_path, ".").get("name", ""))
|
|
70
|
+
)
|
|
71
|
+
said = hinted if hinted != said else FileReads.missing_from(said, scene_path, node_path)
|
|
69
72
|
return [null, {"ok": false, "error": said}]
|
|
70
73
|
var bare: Node = ClassDB.instantiate(type)
|
|
71
74
|
bare.name = str(read["name"])
|
|
@@ -10,6 +10,7 @@ extends RefCounted
|
|
|
10
10
|
const AnimationTools = preload("../addons/gdharness_editor/tools/animation_tools.gd")
|
|
11
11
|
const ResourceTools = preload("../addons/gdharness_editor/tools/resource_tools.gd")
|
|
12
12
|
const SceneTools = preload("../addons/gdharness_editor/tools/scene_tools.gd")
|
|
13
|
+
const SceneFile = preload("../addons/gdharness_editor/scene_file.gd")
|
|
13
14
|
|
|
14
15
|
const SCENE_COMMANDS: Array[String] = [
|
|
15
16
|
"create_scene",
|
|
@@ -60,7 +61,24 @@ static func run(params: Dictionary) -> Dictionary:
|
|
|
60
61
|
var answer: Variant = tools.call(command, arguments)
|
|
61
62
|
tools.free()
|
|
62
63
|
if answer is Dictionary:
|
|
63
|
-
|
|
64
|
+
var answered: Dictionary = answer
|
|
65
|
+
if not SceneFile.by_path.is_empty():
|
|
66
|
+
answered["namedByPath"] = (
|
|
67
|
+
(
|
|
68
|
+
"%s %s named by path rather than by uid. A uid in a file's own header is resolved "
|
|
69
|
+
+ "through the project's uid cache, which holds none for %s yet, as with a file "
|
|
70
|
+
+ "made since the project was last scanned, and a uid it does not hold would warn "
|
|
71
|
+
+ "on every load. project_import refresh_uids scans the project, and writes after "
|
|
72
|
+
+ "it name %s by uid."
|
|
73
|
+
)
|
|
74
|
+
% [
|
|
75
|
+
", ".join(SceneFile.by_path),
|
|
76
|
+
"is" if SceneFile.by_path.size() == 1 else "are",
|
|
77
|
+
"it" if SceneFile.by_path.size() == 1 else "them",
|
|
78
|
+
"it" if SceneFile.by_path.size() == 1 else "them",
|
|
79
|
+
]
|
|
80
|
+
)
|
|
81
|
+
return answered
|
|
64
82
|
return {
|
|
65
83
|
"ok": false, "error": "%s answered %s rather than an object." % [command, type_string(typeof(answer))]
|
|
66
84
|
}
|
|
@@ -15,9 +15,10 @@ const Read = preload("reading.gd")
|
|
|
15
15
|
const Serialisation = preload("serialisation.gd")
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
## The nodes of [param scene_path] as {name, type, path, children}, with instanceOf
|
|
19
|
-
## instances another scene and every stored property when
|
|
20
|
-
## file's value or the default where it sets none.
|
|
18
|
+
## The nodes of [param scene_path] as {name, type, path, children}, with instanceOf and
|
|
19
|
+
## editableChildren on a node that instances another scene and every stored property when
|
|
20
|
+
## [param include_properties] is set, the file's value or the default where it sets none.
|
|
21
|
+
## [param depth] -1 reads every level.
|
|
21
22
|
static func scene_tree(scene_path: String, depth: int, include_properties: bool) -> Dictionary:
|
|
22
23
|
var laid: Dictionary = _laid(scene_path)
|
|
23
24
|
if not laid.get("ok", false):
|
|
@@ -41,6 +42,31 @@ static func scene_node(scene_path: String, node_path: String) -> Dictionary:
|
|
|
41
42
|
return {"ok": true, "name": found["name"], "type": found["type"], "properties": found["properties"]}
|
|
42
43
|
|
|
43
44
|
|
|
45
|
+
## [param said], the refusal for [param node_path], which [param scene_path] does not have, with
|
|
46
|
+
## where the path leaves the scene as [method Read.missing_below] says it.
|
|
47
|
+
static func missing_from(said: String, scene_path: String, node_path: String) -> String:
|
|
48
|
+
var laid: Dictionary = _laid(scene_path)
|
|
49
|
+
if not laid.get("ok", false):
|
|
50
|
+
return said
|
|
51
|
+
var nodes: Dictionary = laid["nodes"]
|
|
52
|
+
var parts: PackedStringArray = node_path.trim_prefix("./").split("/", false)
|
|
53
|
+
var at: String = "."
|
|
54
|
+
var reached: PackedStringArray = []
|
|
55
|
+
for part: String in parts:
|
|
56
|
+
var below: String = _joined(at, part)
|
|
57
|
+
if not nodes.has(below):
|
|
58
|
+
break
|
|
59
|
+
at = below
|
|
60
|
+
var _walked: bool = reached.append(part)
|
|
61
|
+
if reached.size() >= parts.size():
|
|
62
|
+
return said
|
|
63
|
+
var names: PackedStringArray = []
|
|
64
|
+
var children: Array[String] = nodes[at]["children"]
|
|
65
|
+
for child: String in children:
|
|
66
|
+
var _named: bool = names.append(str(nodes[child]["name"]))
|
|
67
|
+
return Read.missing_below(said, str(nodes["."]["name"]), reached, parts[reached.size()], names)
|
|
68
|
+
|
|
69
|
+
|
|
44
70
|
## Every node of [param scene_path] as {ok, nodes}, keyed by path, or a refusal.
|
|
45
71
|
static func _laid(scene_path: String) -> Dictionary:
|
|
46
72
|
if not FileAccess.file_exists(scene_path):
|
|
@@ -51,13 +77,11 @@ static func _laid(scene_path: String) -> Dictionary:
|
|
|
51
77
|
if loaded == null:
|
|
52
78
|
return {"ok": false, "error": "Failed to load %s as a scene" % scene_path}
|
|
53
79
|
if not loaded is PackedScene:
|
|
80
|
+
var kind: String = loaded.get_class()
|
|
81
|
+
var sort: String = ("an " if kind.left(1).to_upper() in ["A", "E", "I", "O", "U"] else "a ") + kind
|
|
54
82
|
return {
|
|
55
83
|
"ok": false,
|
|
56
|
-
"error":
|
|
57
|
-
(
|
|
58
|
-
"%s is a %s, not a scene: resource_edit read reads its properties."
|
|
59
|
-
% [scene_path, loaded.get_class()]
|
|
60
|
-
)
|
|
84
|
+
"error": "%s is %s, not a scene: resource_edit read reads its properties." % [scene_path, sort]
|
|
61
85
|
}
|
|
62
86
|
var packed: PackedScene = loaded
|
|
63
87
|
var nodes: Dictionary = {}
|
|
@@ -82,8 +106,18 @@ static func _lay(state: SceneState, prefix: String, nodes: Dictionary, within: A
|
|
|
82
106
|
var refused: String = _lay(base, prefix, nodes, within)
|
|
83
107
|
if not refused.is_empty():
|
|
84
108
|
return refused
|
|
109
|
+
var ids: Array[int] = _unique_ids(state.get_path(), state.get_node_count())
|
|
85
110
|
for index: int in state.get_node_count():
|
|
86
111
|
var path: String = _joined(prefix, str(state.get_node_path(index)))
|
|
112
|
+
var recovered: bool = false
|
|
113
|
+
if not nodes.has(path) and index > 0 and _overrides(state, index) and ids[index] >= 0:
|
|
114
|
+
# The engine finds an entry whose node was renamed or moved in the scene it comes from by
|
|
115
|
+
# the node's unique_id, and keeps the node's new name.
|
|
116
|
+
var parent: String = _joined(prefix, str(state.get_node_path(index, true)))
|
|
117
|
+
var found: String = _by_id(nodes, _owner_of(nodes, parent, prefix), ids[index])
|
|
118
|
+
recovered = not found.is_empty()
|
|
119
|
+
if recovered:
|
|
120
|
+
path = found
|
|
87
121
|
var fresh: bool = not nodes.has(path)
|
|
88
122
|
if fresh:
|
|
89
123
|
var instance: PackedScene = state.get_node_instance(index)
|
|
@@ -98,6 +132,7 @@ static func _lay(state: SceneState, prefix: String, nodes: Dictionary, within: A
|
|
|
98
132
|
if not nodes.has(path):
|
|
99
133
|
return "%s holds no nodes" % instance.resource_path
|
|
100
134
|
nodes[path]["instanceOf"] = instance.resource_path
|
|
135
|
+
nodes[path]["editableChildren"] = false
|
|
101
136
|
elif state.is_node_instance_placeholder(index):
|
|
102
137
|
nodes[path] = _made("InstancePlaceholder")
|
|
103
138
|
nodes[path]["instanceOf"] = state.get_node_instance_placeholder(index)
|
|
@@ -106,9 +141,12 @@ static func _lay(state: SceneState, prefix: String, nodes: Dictionary, within: A
|
|
|
106
141
|
continue
|
|
107
142
|
else:
|
|
108
143
|
nodes[path] = _made(str(state.get_node_type(index)))
|
|
144
|
+
if ids[index] >= 0:
|
|
145
|
+
nodes[path]["uniqueId"] = ids[index]
|
|
109
146
|
# The name is the state's even for an entry laid already, since an inherited scene names its
|
|
110
147
|
# root itself; a root instanced inside another scene is renamed by that scene once laid.
|
|
111
|
-
|
|
148
|
+
if not recovered:
|
|
149
|
+
nodes[path]["name"] = str(state.get_node_name(index))
|
|
112
150
|
if fresh:
|
|
113
151
|
# The root of what is laid here is placed by whoever instanced it.
|
|
114
152
|
if path != prefix:
|
|
@@ -123,6 +161,86 @@ static func _lay(state: SceneState, prefix: String, nodes: Dictionary, within: A
|
|
|
123
161
|
held[str(state.get_node_property_name(index, property))] = state.get_node_property_value(
|
|
124
162
|
index, property
|
|
125
163
|
)
|
|
164
|
+
# Only the scene being read and the scenes it inherits share its root, the owner whose editable
|
|
165
|
+
# instances a write here sets; a file instanced below makes its own owner's editable.
|
|
166
|
+
if prefix == ".":
|
|
167
|
+
for editable: String in _editable_in(state.get_path()):
|
|
168
|
+
var at: String = _joined(prefix, editable)
|
|
169
|
+
var marked: Dictionary = nodes.get(at, {})
|
|
170
|
+
if marked.has("editableChildren"):
|
|
171
|
+
marked["editableChildren"] = true
|
|
172
|
+
return ""
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
## The paths of the instances [param scene_path] gives editable children, from its root. SceneState
|
|
176
|
+
## does not give them to a script, so they are read off the scene's bundle.
|
|
177
|
+
static func _editable_in(scene_path: String) -> PackedStringArray:
|
|
178
|
+
var found: PackedStringArray = []
|
|
179
|
+
var loaded: Resource = ResourceLoader.load(scene_path, "", ResourceLoader.CACHE_MODE_IGNORE)
|
|
180
|
+
if not loaded is PackedScene:
|
|
181
|
+
return found
|
|
182
|
+
var given: Variant = loaded.get("_bundled")
|
|
183
|
+
if not given is Dictionary:
|
|
184
|
+
return found
|
|
185
|
+
var bundled: Dictionary = given
|
|
186
|
+
var listed: Variant = bundled.get("editable_instances", [])
|
|
187
|
+
if listed is Array:
|
|
188
|
+
for one: Variant in listed:
|
|
189
|
+
var _kept: bool = found.append(str(one))
|
|
190
|
+
return found
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
## The unique_id of each node entry in the text scene at [param scene_path], in the order its state
|
|
194
|
+
## holds them, -1 where an entry has none. All -1 for a binary scene, and for a file whose entries do
|
|
195
|
+
## not number [param count], since SceneState does not give the ids to a script.
|
|
196
|
+
static func _unique_ids(scene_path: String, count: int) -> Array[int]:
|
|
197
|
+
var ids: Array[int] = []
|
|
198
|
+
var _sized: int = ids.resize(count)
|
|
199
|
+
ids.fill(-1)
|
|
200
|
+
if not scene_path.ends_with(".tscn") or not FileAccess.file_exists(scene_path):
|
|
201
|
+
return ids
|
|
202
|
+
var read: Array[int] = []
|
|
203
|
+
var pattern: RegEx = RegEx.create_from_string(" unique_id=(\\d+)")
|
|
204
|
+
for line: String in FileAccess.get_file_as_string(scene_path).split("\n"):
|
|
205
|
+
if line.begins_with("[node "):
|
|
206
|
+
var found: RegExMatch = pattern.search(line)
|
|
207
|
+
read.append(int(found.get_string(1)) if found != null else -1)
|
|
208
|
+
return read if read.size() == count else ids
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
## Whether entry [param index] of [param state] only sets values on a node another file makes.
|
|
212
|
+
static func _overrides(state: SceneState, index: int) -> bool:
|
|
213
|
+
return (
|
|
214
|
+
state.get_node_instance(index) == null
|
|
215
|
+
and not state.is_node_instance_placeholder(index)
|
|
216
|
+
and str(state.get_node_type(index)).is_empty()
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
## The node whose file owns what sits under [param parent]: the nearest instance at or above it, or
|
|
221
|
+
## [param prefix], the root of what is being laid, as the engine searches.
|
|
222
|
+
static func _owner_of(nodes: Dictionary, parent: String, prefix: String) -> String:
|
|
223
|
+
var at: String = parent
|
|
224
|
+
while at != prefix and at != "." and nodes.has(at):
|
|
225
|
+
var entry: Dictionary = nodes[at]
|
|
226
|
+
if entry.has("instanceOf"):
|
|
227
|
+
break
|
|
228
|
+
at = "." if not at.contains("/") else at.get_base_dir()
|
|
229
|
+
return at if nodes.has(at) else prefix
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
## The path of the node [param owner] owns whose unique_id is [param id], or "": one under an instance
|
|
233
|
+
## nested below [param owner] belongs to that instance's file, whose ids are numbered apart.
|
|
234
|
+
static func _by_id(nodes: Dictionary, owner: String, id: int) -> String:
|
|
235
|
+
for path: String in nodes:
|
|
236
|
+
if path == owner or not (owner == "." or path.begins_with(owner + "/")):
|
|
237
|
+
continue
|
|
238
|
+
var entry: Dictionary = nodes[path]
|
|
239
|
+
if Read.as_int(entry.get("uniqueId", -1), -1) != id:
|
|
240
|
+
continue
|
|
241
|
+
var above: String = "." if not path.contains("/") else path.get_base_dir()
|
|
242
|
+
if _owner_of(nodes, above, owner) == owner:
|
|
243
|
+
return path
|
|
126
244
|
return ""
|
|
127
245
|
|
|
128
246
|
|
|
@@ -162,12 +280,18 @@ static func _entry(
|
|
|
162
280
|
# The root's file is the scene being read, so only another node's names a second scene.
|
|
163
281
|
if path != "." and node.has("instanceOf"):
|
|
164
282
|
data["instanceOf"] = node["instanceOf"]
|
|
283
|
+
if node.has("editableChildren"):
|
|
284
|
+
data["editableChildren"] = node["editableChildren"]
|
|
165
285
|
if include_properties:
|
|
166
286
|
var given: Dictionary = node["properties"]
|
|
167
287
|
data["properties"] = _stored(str(node["type"]), given)
|
|
288
|
+
var below: Array[String] = node["children"]
|
|
289
|
+
# Counted rather than left as an empty list, which reads as a node with no children.
|
|
168
290
|
if depth >= 0 and current_depth >= depth:
|
|
291
|
+
if not below.is_empty():
|
|
292
|
+
data["childrenNotListed"] = below.size()
|
|
169
293
|
return data
|
|
170
|
-
for child: String in
|
|
294
|
+
for child: String in below:
|
|
171
295
|
children.append(_entry(nodes, child, include_properties, depth, current_depth + 1))
|
|
172
296
|
return data
|
|
173
297
|
|
|
@@ -22,6 +22,9 @@ extends RefCounted
|
|
|
22
22
|
# number was expected. Absent keys never reach here: the callers pass their own default to
|
|
23
23
|
# `Dictionary.get`, so what arrives is that default.
|
|
24
24
|
|
|
25
|
+
## How many of a node's children a refusal for a path missing below it names.
|
|
26
|
+
const NAMED_CHILDREN: int = 12
|
|
27
|
+
|
|
25
28
|
|
|
26
29
|
static func as_int(value: Variant, fallback: int = 0) -> int:
|
|
27
30
|
if value is int:
|
|
@@ -114,3 +117,25 @@ static func from_the_root(said: String, node_path: String, root_name: String) ->
|
|
|
114
117
|
if not rest.is_empty():
|
|
115
118
|
how += ', so this one is "%s"' % rest
|
|
116
119
|
return "%s%s %s." % [said, "" if said.ends_with(".") else ".", how]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
## [param said], the refusal for a path a scene does not have, with where the path leaves it: the
|
|
123
|
+
## last node of the path it has, [param reached] from the root, called [param root_name], when that
|
|
124
|
+
## is empty; the name below it that is missing; and [param children], the names that node has, as
|
|
125
|
+
## after a node was moved or renamed. Names up to [constant NAMED_CHILDREN] of them.
|
|
126
|
+
static func missing_below(
|
|
127
|
+
said: String, root_name: String, reached: PackedStringArray, missing: String, children: PackedStringArray
|
|
128
|
+
) -> String:
|
|
129
|
+
var shown: String = ", ".join(children.slice(0, NAMED_CHILDREN))
|
|
130
|
+
if children.size() > NAMED_CHILDREN:
|
|
131
|
+
shown += " and %d more" % (children.size() - NAMED_CHILDREN)
|
|
132
|
+
return (
|
|
133
|
+
"%s%s %s has no %s; %s."
|
|
134
|
+
% [
|
|
135
|
+
said,
|
|
136
|
+
"" if said.ends_with(".") else ".",
|
|
137
|
+
"The root, %s," % root_name if reached.is_empty() else "/".join(reached),
|
|
138
|
+
missing,
|
|
139
|
+
"its children are " + shown if not children.is_empty() else "it has no children",
|
|
140
|
+
]
|
|
141
|
+
)
|
|
@@ -664,8 +664,12 @@ func _serialize_array(value: Array) -> Array:
|
|
|
664
664
|
|
|
665
665
|
# Base64 rather than a list of numbers. A byte array is a texture, a sound or a save file often
|
|
666
666
|
# enough that the list would be the whole answer, megabytes of it, and unreadable either way.
|
|
667
|
+
#
|
|
668
|
+
# The engine's encoder raises an error on an empty array, and an empty tile_map_data is every
|
|
669
|
+
# TileMapLayer with nothing painted yet, so a read of one carried an engine error in its answer.
|
|
667
670
|
func _serialize_bytes(value: PackedByteArray) -> Dictionary:
|
|
668
|
-
|
|
671
|
+
var text: String = "" if value.is_empty() else Marshalls.raw_to_base64(value)
|
|
672
|
+
return {"base64": text, "size": value.size(), "_type": "PackedByteArray"}
|
|
669
673
|
|
|
670
674
|
|
|
671
675
|
# The packed vector and colour arrays, each element serialised the way one of them on its own is.
|