gdharness 1.3.67 → 1.3.68
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 +33537 -32369
- package/build/godot/addons/gdharness_editor/bridge_client.gd +5 -3
- package/build/godot/addons/gdharness_editor/headless_check.gd +52 -0
- package/build/godot/addons/gdharness_editor/plugin.gd +80 -2
- package/build/godot/addons/gdharness_editor/scene_file.gd +128 -12
- package/build/godot/addons/gdharness_editor/tool_executor.gd +1 -0
- package/build/godot/addons/gdharness_editor/tools/class_tools.gd +3 -1
- package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +9 -13
- package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +87 -13
- package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +20 -4
- package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +43 -11
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +469 -7
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +2 -0
- package/build/godot/operations/editor_tools.gd +1 -0
- package/build/keeper.js +116 -114
- package/package.json +1 -1
|
@@ -44,6 +44,9 @@ var _homes: Dictionary = {}
|
|
|
44
44
|
## rewrite that is not saved is put back.
|
|
45
45
|
var _outside: Array[Array] = []
|
|
46
46
|
var _scene: GdhSceneFile = null
|
|
47
|
+
## Each unique name a node of this scene had that is unique only inside an instance now, to the
|
|
48
|
+
## node's path from the root, for a %unique path through it to be written as the plain path.
|
|
49
|
+
var _enclosed: Dictionary = {}
|
|
47
50
|
var _moves: Dictionary = {}
|
|
48
51
|
var _back: Dictionary = {}
|
|
49
52
|
var _seen: Dictionary = {}
|
|
@@ -64,7 +67,7 @@ static func moved(given: String, moves: Dictionary) -> String:
|
|
|
64
67
|
## Carries [param moves], from the root of [param scene], into the paths it holds. Called after the
|
|
65
68
|
## move and before the write. Answers how many paths it rewrote.
|
|
66
69
|
func carry(scene: GdhSceneFile, moves: Dictionary) -> int:
|
|
67
|
-
if moves.is_empty():
|
|
70
|
+
if moves.is_empty() and _enclosed.is_empty():
|
|
68
71
|
return 0
|
|
69
72
|
_scene = scene
|
|
70
73
|
_moves = moves
|
|
@@ -110,6 +113,14 @@ func carry(scene: GdhSceneFile, moves: Dictionary) -> int:
|
|
|
110
113
|
return retargeted.size() - before
|
|
111
114
|
|
|
112
115
|
|
|
116
|
+
## Carries into [param scene] a branch saved as a scene of its own and instanced in its place, as
|
|
117
|
+
## [method carry] does: each %unique path through one of [param enclosed], a unique name to its
|
|
118
|
+
## node's path from the root, is written as the plain path. Answers how many paths it rewrote.
|
|
119
|
+
func carry_enclosed(scene: GdhSceneFile, enclosed: Dictionary) -> int:
|
|
120
|
+
_enclosed = enclosed
|
|
121
|
+
return carry(scene, {})
|
|
122
|
+
|
|
123
|
+
|
|
113
124
|
## Opens [param scene_path], carries [param moves] into it, and writes it when that rewrote a path.
|
|
114
125
|
## Answers "" or why it was left as it was.
|
|
115
126
|
func carry_into(scene_path: String, moves: Dictionary) -> String:
|
|
@@ -389,8 +400,12 @@ func _after(path: NodePath, was: String, now: String) -> Array:
|
|
|
389
400
|
var _named: bool = names.append(str(path.get_name(index)))
|
|
390
401
|
var start: String = was
|
|
391
402
|
var unique: bool = names[0].begins_with("%")
|
|
403
|
+
var enclosed: bool = false
|
|
392
404
|
if unique:
|
|
393
405
|
start = _unique_was(names[0].substr(1), now)
|
|
406
|
+
if start.is_empty() and _enclosed.has(names[0].substr(1)):
|
|
407
|
+
start = _enclosed[names[0].substr(1)]
|
|
408
|
+
enclosed = true
|
|
394
409
|
if start.is_empty():
|
|
395
410
|
return [UNCHANGED, ""]
|
|
396
411
|
names = names.slice(1)
|
|
@@ -398,7 +413,7 @@ func _after(path: NodePath, was: String, now: String) -> Array:
|
|
|
398
413
|
if target_was.is_empty():
|
|
399
414
|
return [UNCHANGED, ""]
|
|
400
415
|
var target_now: String = moved(target_was, _moves)
|
|
401
|
-
if target_now == target_was and now == was:
|
|
416
|
+
if target_now == target_was and now == was and not enclosed:
|
|
402
417
|
return [UNCHANGED, ""]
|
|
403
418
|
if target_now.is_empty():
|
|
404
419
|
return [DELETED, ""]
|
|
@@ -408,8 +423,9 @@ func _after(path: NodePath, was: String, now: String) -> Array:
|
|
|
408
423
|
return [UNCHANGED, ""]
|
|
409
424
|
if holder.get_node_or_null(NodePath(path.get_concatenated_names())) == target:
|
|
410
425
|
return [UNCHANGED, ""]
|
|
411
|
-
var
|
|
412
|
-
|
|
426
|
+
var through_unique: bool = unique and not enclosed
|
|
427
|
+
var to: String = str(holder.get_path_to(target, through_unique))
|
|
428
|
+
if through_unique:
|
|
413
429
|
to = _unique_to(start, target, to)
|
|
414
430
|
if path.get_subname_count() > 0:
|
|
415
431
|
to += ":" + path.get_concatenated_subnames()
|
|
@@ -6,6 +6,9 @@ extends RefCounted
|
|
|
6
6
|
const GdhFileWalk = preload("../file_walk.gd")
|
|
7
7
|
const GdhRead = preload("../reading.gd")
|
|
8
8
|
const GdhSame = preload("../same_values.gd")
|
|
9
|
+
## Control's layout modes, which the engine does not expose to scripts.
|
|
10
|
+
const POSITION_MODE: int = 0
|
|
11
|
+
const ANCHORS_MODE: int = 1
|
|
9
12
|
|
|
10
13
|
|
|
11
14
|
## The scene to instance for an add, or null for a node of a class, beside why neither can be made
|
|
@@ -142,18 +145,10 @@ static func reconnect(copies: Dictionary, top: Node) -> void:
|
|
|
142
145
|
and (target == top or top.is_ancestor_of(target as Node))
|
|
143
146
|
):
|
|
144
147
|
continue
|
|
145
|
-
|
|
146
|
-
# from the method alone would save the copy without the arguments the
|
|
147
|
-
# original passes.
|
|
148
|
-
var aimed: Callable = Callable(to, callable.get_method())
|
|
149
|
-
var unbound: int = callable.get_unbound_arguments_count()
|
|
150
|
-
if unbound > 0:
|
|
151
|
-
aimed = aimed.unbind(unbound)
|
|
152
|
-
if callable.get_bound_arguments_count() > 0:
|
|
153
|
-
aimed = aimed.bindv(callable.get_bound_arguments())
|
|
148
|
+
var joined: Callable = aimed(callable, to)
|
|
154
149
|
if (
|
|
155
|
-
not copy.is_connected(signal_name,
|
|
156
|
-
and copy.connect(signal_name,
|
|
150
|
+
not copy.is_connected(signal_name, joined)
|
|
151
|
+
and copy.connect(signal_name, joined, flags) != OK
|
|
157
152
|
):
|
|
158
153
|
push_error(
|
|
159
154
|
(
|
|
@@ -163,6 +158,19 @@ static func reconnect(copies: Dictionary, top: Node) -> void:
|
|
|
163
158
|
)
|
|
164
159
|
|
|
165
160
|
|
|
161
|
+
## [param callable] aimed at [param to], unbound and then bound again as Node.duplicate does: a
|
|
162
|
+
## callable made from the method alone would save the connection without the arguments the original
|
|
163
|
+
## passes.
|
|
164
|
+
static func aimed(callable: Callable, to: Object) -> Callable:
|
|
165
|
+
var aimed_at: Callable = Callable(to, callable.get_method())
|
|
166
|
+
var unbound: int = callable.get_unbound_arguments_count()
|
|
167
|
+
if unbound > 0:
|
|
168
|
+
aimed_at = aimed_at.unbind(unbound)
|
|
169
|
+
if callable.get_bound_arguments_count() > 0:
|
|
170
|
+
aimed_at = aimed_at.bindv(callable.get_bound_arguments())
|
|
171
|
+
return aimed_at
|
|
172
|
+
|
|
173
|
+
|
|
166
174
|
## Owns [param node] to [param root], and under it the nodes the root owns already and those of
|
|
167
175
|
## [param placed], the copies a duplicate placed. A node nothing owns is not the scene's: a script's
|
|
168
176
|
## _init made it, which a headless engine runs and the editor does not, and owning it saved it into
|
|
@@ -197,6 +205,30 @@ static func unplaceable(parent: Node, node: Node, asked: Variant) -> String:
|
|
|
197
205
|
return ""
|
|
198
206
|
|
|
199
207
|
|
|
208
|
+
## Whether [param node], an instance of [param packed] just added, would load at its parent's
|
|
209
|
+
## top-left corner rather than where its own scene places it. A Control its scene stores as
|
|
210
|
+
## uncontrolled takes position mode under a Control that is not a Container, so the file stores
|
|
211
|
+
## layout_mode 0 and leaves out the offsets, which are the scene's own. Setting position mode moves
|
|
212
|
+
## a Control to the corner, so the node loads there. Anchors mode with the same anchors places it
|
|
213
|
+
## the same and leaves its offsets as they load.
|
|
214
|
+
static func cornered(node: Node, packed: PackedScene) -> bool:
|
|
215
|
+
var control: Control = node as Control
|
|
216
|
+
if control == null or packed == null:
|
|
217
|
+
return false
|
|
218
|
+
if GdhRead.as_int(control.get("layout_mode")) != POSITION_MODE:
|
|
219
|
+
return false
|
|
220
|
+
if control.offset_left == 0.0 and control.offset_top == 0.0:
|
|
221
|
+
return false
|
|
222
|
+
var state: SceneState = packed.get_state()
|
|
223
|
+
while state != null:
|
|
224
|
+
for property: int in state.get_node_property_count(0):
|
|
225
|
+
if str(state.get_node_property_name(0, property)) == "layout_mode":
|
|
226
|
+
var stored: int = GdhRead.as_int(state.get_node_property_value(0, property))
|
|
227
|
+
return stored != POSITION_MODE and stored != ANCHORS_MODE
|
|
228
|
+
state = state.get_base_scene_state()
|
|
229
|
+
return false
|
|
230
|
+
|
|
231
|
+
|
|
200
232
|
## Moves [param node] to [param asked] among its siblings, when a place was asked for.
|
|
201
233
|
static func place(node: Node, asked: Variant) -> void:
|
|
202
234
|
if asked != null:
|
|
@@ -15,6 +15,7 @@ const GdhPropertyValues = preload("../property_values.gd")
|
|
|
15
15
|
const GdhSame = preload("../same_values.gd")
|
|
16
16
|
const GdhSceneFile = preload("../scene_file.gd")
|
|
17
17
|
const GdhSceneSharing = preload("scene_sharing.gd")
|
|
18
|
+
const GdhScriptPaths = preload("script_paths.gd")
|
|
18
19
|
const GdhSerialisation = preload("../serialisation.gd")
|
|
19
20
|
const GdhTilesetTools = preload("tileset_tools.gd")
|
|
20
21
|
|
|
@@ -126,10 +127,11 @@ func _inherited(args: Dictionary) -> Dictionary:
|
|
|
126
127
|
)
|
|
127
128
|
|
|
128
129
|
|
|
129
|
-
## The res:// path
|
|
130
|
-
|
|
130
|
+
## The res:// path [param args] give under [param argument] for a new scene, given .tscn when it
|
|
131
|
+
## has no extension, or "".
|
|
132
|
+
func _new_scene_path(args: Dictionary, argument: String = "scenePath") -> String:
|
|
131
133
|
var scene_path: String = GdhSceneFile.res_path(
|
|
132
|
-
str(args.get("projectPath", "")), str(args.get(
|
|
134
|
+
str(args.get("projectPath", "")), str(args.get(argument, ""))
|
|
133
135
|
)
|
|
134
136
|
if scene_path == "res://":
|
|
135
137
|
return ""
|
|
@@ -301,18 +303,26 @@ func add_node(args: Dictionary) -> Dictionary:
|
|
|
301
303
|
if not unplaceable.is_empty():
|
|
302
304
|
return scene.refuse(unplaceable)
|
|
303
305
|
|
|
304
|
-
# In the tree before its properties, so a property holding a node can find it by path.
|
|
306
|
+
# In the tree before its properties, so a property holding a node can find it by path. The
|
|
307
|
+
# script goes on once the node is in place, as an add and then a set of the script would put
|
|
308
|
+
# it. What the parent gives the node, such as a Container's layout mode, is then what the node
|
|
309
|
+
# held before its script, and only what the script's _init changed is put back.
|
|
305
310
|
var new_node: Node = GdhPlacing.made(packed, node_type)
|
|
306
|
-
if class_script != null:
|
|
307
|
-
scene.asked(new_node, ["script"])
|
|
308
|
-
new_node.set_script(class_script)
|
|
309
311
|
new_node.name = node_name
|
|
310
312
|
parent.add_child(new_node)
|
|
311
313
|
GdhPlacing.place(new_node, place)
|
|
312
314
|
GdhPlacing.own(new_node, root)
|
|
315
|
+
if class_script != null:
|
|
316
|
+
scene.asked(new_node, ["script"])
|
|
317
|
+
new_node.set_script(class_script)
|
|
318
|
+
# The script's _init can name the node, and the call's name is the one kept.
|
|
319
|
+
new_node.name = node_name
|
|
313
320
|
var unsettled: String = scene.settle()
|
|
314
321
|
if not unsettled.is_empty():
|
|
315
322
|
return scene.refuse("Nothing was written: " + unsettled)
|
|
323
|
+
if GdhPlacing.cornered(new_node, packed):
|
|
324
|
+
scene.asked(new_node, ["layout_mode"])
|
|
325
|
+
new_node.set("layout_mode", GdhPlacing.ANCHORS_MODE)
|
|
316
326
|
scene.asked(new_node, properties.keys())
|
|
317
327
|
var unwritten: String = _properties.write_all(new_node, properties)
|
|
318
328
|
scene.asked(new_node, _properties.moved_by_engine.keys())
|
|
@@ -658,6 +668,449 @@ func reparent_node(args: Dictionary) -> Dictionary:
|
|
|
658
668
|
return answer
|
|
659
669
|
|
|
660
670
|
|
|
671
|
+
## Saves the branch at nodePath as a scene of its own at newScenePath and puts an instance of that
|
|
672
|
+
## scene where the branch was, as the editor's Save Branch as Scene does. The new file holds the
|
|
673
|
+
## branch's nodes, values, resources, groups and the connections among them, with its root's
|
|
674
|
+
## position reset as the editor resets it. The instance keeps the branch's name, place, position,
|
|
675
|
+
## rotation and scale. The rest of the scene keeps what it named in the branch: the connections to
|
|
676
|
+
## its nodes, the properties holding them and the %unique paths, which no longer reach a node
|
|
677
|
+
## unique only in the new scene.
|
|
678
|
+
func save_branch(args: Dictionary) -> Dictionary:
|
|
679
|
+
var project_path: String = str(args.get("projectPath", ""))
|
|
680
|
+
var scene_path: String = GdhSceneFile.res_path(project_path, str(args.get("scenePath", "")))
|
|
681
|
+
var node_path: String = str(args.get("nodePath", ""))
|
|
682
|
+
var branch_path: String = _new_scene_path(args, "newScenePath")
|
|
683
|
+
if node_path.is_empty() or node_path == ".":
|
|
684
|
+
return {
|
|
685
|
+
"ok": false,
|
|
686
|
+
"error":
|
|
687
|
+
"The root is the whole scene, which scene_create save_as copies to a new file.",
|
|
688
|
+
}
|
|
689
|
+
if branch_path.is_empty():
|
|
690
|
+
return { "ok": false, "error": "Missing newScenePath" }
|
|
691
|
+
if branch_path == scene_path:
|
|
692
|
+
return {
|
|
693
|
+
"ok": false, "error": "newScenePath is %s, the scene the branch is in." % scene_path,
|
|
694
|
+
}
|
|
695
|
+
if FileAccess.file_exists(branch_path):
|
|
696
|
+
return {
|
|
697
|
+
"ok": false,
|
|
698
|
+
"error": "%s already exists, and save_branch writes a new scene only." % branch_path,
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
var opened: Array = _open(scene_path, true)
|
|
702
|
+
var scene: GdhSceneFile = opened[0]
|
|
703
|
+
var refused: Dictionary = opened[1]
|
|
704
|
+
if not refused.is_empty():
|
|
705
|
+
return refused
|
|
706
|
+
var root: Node = scene.root
|
|
707
|
+
var top: Node = GdhSceneFile.node_at(root, node_path)
|
|
708
|
+
if top == null:
|
|
709
|
+
return scene.refuse(scene.not_found("Node not found: " + node_path, node_path))
|
|
710
|
+
var fixed: String = scene.fixed(top)
|
|
711
|
+
if not fixed.is_empty():
|
|
712
|
+
return scene.refuse(fixed)
|
|
713
|
+
if not top.scene_file_path.is_empty():
|
|
714
|
+
return scene.refuse(
|
|
715
|
+
"%s is an instance of %s, its scene already." % [node_path, top.scene_file_path]
|
|
716
|
+
)
|
|
717
|
+
|
|
718
|
+
var at: String = scene.path_of(top)
|
|
719
|
+
var top_name: StringName = top.name
|
|
720
|
+
var parent: Node = top.get_parent()
|
|
721
|
+
var index: int = top.get_index()
|
|
722
|
+
var unique: bool = top.unique_name_in_owner
|
|
723
|
+
var placed_at: Dictionary = {}
|
|
724
|
+
if top is Node2D or top is Node3D:
|
|
725
|
+
for property: String in ["position", "rotation", "scale"]:
|
|
726
|
+
placed_at[property] = top.get(property)
|
|
727
|
+
var outside: Array[Node] = [root]
|
|
728
|
+
var inside: Array[Node] = [top]
|
|
729
|
+
for node: Node in root.find_children("*", "", true, false):
|
|
730
|
+
if node.owner == root and node != top:
|
|
731
|
+
if top.is_ancestor_of(node):
|
|
732
|
+
inside.append(node)
|
|
733
|
+
else:
|
|
734
|
+
outside.append(node)
|
|
735
|
+
|
|
736
|
+
var answer: Dictionary = { "ok": true, "nodePath": at, "newScenePath": branch_path }
|
|
737
|
+
var enclosed: Dictionary = {}
|
|
738
|
+
for node: Node in inside:
|
|
739
|
+
if node != top and node.unique_name_in_owner:
|
|
740
|
+
enclosed[str(node.name)] = scene.path_of(node)
|
|
741
|
+
var held: Array[Array] = _holding_the_branch(outside, top)
|
|
742
|
+
var crossing: Array[Array] = _crossing(outside, inside, top)
|
|
743
|
+
_named_outside(scene, inside, top, answer)
|
|
744
|
+
var scripts_out: Array[Dictionary] = _scripts_naming_outside(inside, top, branch_path)
|
|
745
|
+
|
|
746
|
+
var split: Array = scene.split_off(top, branch_path)
|
|
747
|
+
var branch: GdhSceneFile = split[0]
|
|
748
|
+
var crossing_unmade: Array[Dictionary] = split[1]
|
|
749
|
+
var editable: Array[Node] = []
|
|
750
|
+
for node: Node in inside:
|
|
751
|
+
if node != top and not node.scene_file_path.is_empty() and root.is_editable_instance(node):
|
|
752
|
+
editable.append(node)
|
|
753
|
+
var below: Array[String] = []
|
|
754
|
+
for node: Node in inside:
|
|
755
|
+
below.append(str(top.get_path_to(node)))
|
|
756
|
+
branch.asked(top, ["unique_name_in_owner"] + placed_at.keys())
|
|
757
|
+
parent.remove_child(top)
|
|
758
|
+
top.unique_name_in_owner = false
|
|
759
|
+
top.owner = null
|
|
760
|
+
for node: Node in inside:
|
|
761
|
+
if node != top:
|
|
762
|
+
node.owner = top
|
|
763
|
+
for node: Node in editable:
|
|
764
|
+
top.set_editable_instance(node, true)
|
|
765
|
+
if top is Node2D:
|
|
766
|
+
(top as Node2D).position = Vector2.ZERO
|
|
767
|
+
elif top is Node3D:
|
|
768
|
+
(top as Node3D).position = Vector3.ZERO
|
|
769
|
+
var written: Dictionary = branch.write(
|
|
770
|
+
func(saved: Node) -> String:
|
|
771
|
+
for one: String in below:
|
|
772
|
+
if saved.get_node_or_null(one) == null:
|
|
773
|
+
return "%s would not be in %s when it loads." % [one, branch_path]
|
|
774
|
+
return ""
|
|
775
|
+
)
|
|
776
|
+
if not written.is_empty():
|
|
777
|
+
scene.close()
|
|
778
|
+
return written
|
|
779
|
+
|
|
780
|
+
var packed: PackedScene = ResourceLoader.load(branch_path, "PackedScene") as PackedScene
|
|
781
|
+
var instance: Node = (
|
|
782
|
+
packed.instantiate(PackedScene.GEN_EDIT_STATE_INSTANCE) if packed != null else null
|
|
783
|
+
)
|
|
784
|
+
if instance == null:
|
|
785
|
+
return _unwritten(scene, branch_path, "%s did not instance once written." % branch_path)
|
|
786
|
+
instance.name = top_name
|
|
787
|
+
parent.add_child(instance)
|
|
788
|
+
parent.move_child(instance, index)
|
|
789
|
+
instance.owner = root
|
|
790
|
+
# Before the scene is next packed, which reads the path to every node a property holds.
|
|
791
|
+
for one: Array in held:
|
|
792
|
+
var holder: Node = one[0]
|
|
793
|
+
var holding: String = one[1]
|
|
794
|
+
scene.asked(holder, [holding])
|
|
795
|
+
holder.set(holding, _unmarked(one[2], instance))
|
|
796
|
+
var unsettled: String = scene.settle()
|
|
797
|
+
if not unsettled.is_empty():
|
|
798
|
+
return _unwritten(scene, branch_path, unsettled)
|
|
799
|
+
scene.asked(instance, ["unique_name_in_owner"] + placed_at.keys())
|
|
800
|
+
instance.unique_name_in_owner = unique
|
|
801
|
+
for property: String in placed_at:
|
|
802
|
+
instance.set(property, placed_at[property])
|
|
803
|
+
scene.rejoin_unmade(crossing_unmade, instance)
|
|
804
|
+
var lost: Array[Dictionary] = _rejoined(scene, crossing, instance)
|
|
805
|
+
if not lost.is_empty():
|
|
806
|
+
answer["connectionsLost"] = lost
|
|
807
|
+
var carried: GdhPaths = GdhPaths.new()
|
|
808
|
+
var _rewritten: int = carried.carry_enclosed(scene, enclosed)
|
|
809
|
+
|
|
810
|
+
written = scene.write(
|
|
811
|
+
func(saved: Node) -> String:
|
|
812
|
+
var copy: Node = saved.get_node_or_null(at)
|
|
813
|
+
if copy == null or copy.scene_file_path != branch_path:
|
|
814
|
+
return "%s would not load as an instance of %s." % [at, branch_path]
|
|
815
|
+
if copy.get_index() != index:
|
|
816
|
+
return "%s would load at index %d, not %d." % [at, copy.get_index(), index]
|
|
817
|
+
return ""
|
|
818
|
+
)
|
|
819
|
+
if not written.is_empty():
|
|
820
|
+
carried.put_back(scene_path)
|
|
821
|
+
return _unwritten(null, branch_path, str(written.get("error", "")))
|
|
822
|
+
answer["index"] = index
|
|
823
|
+
answer.merge(carried.answer())
|
|
824
|
+
answer.merge(GdhKin.enclose(scene_path, at))
|
|
825
|
+
if not scripts_out.is_empty():
|
|
826
|
+
var naming: Array = answer.get_or_add("scriptsNaming", [])
|
|
827
|
+
naming.append_array(scripts_out)
|
|
828
|
+
return answer
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
## Removes the scene [method save_branch] wrote at [param branch_path] and answers [param why] as
|
|
832
|
+
## its refusal. [param scene] is closed when it is still open.
|
|
833
|
+
static func _unwritten(scene: GdhSceneFile, branch_path: String, why: String) -> Dictionary:
|
|
834
|
+
if scene != null:
|
|
835
|
+
scene.close()
|
|
836
|
+
var _removed: Error = DirAccess.remove_absolute(branch_path)
|
|
837
|
+
if Engine.is_editor_hint():
|
|
838
|
+
GdhSceneFile.scan_later()
|
|
839
|
+
return { "ok": false, "error": why }
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
## Each property of the nodes [param outside] the branch at [param top] that holds a node of it, as
|
|
843
|
+
## [holder, property, the value with each such node marked by its path from [param top]], for
|
|
844
|
+
## [method _unmarked] to give the instance's node at that path.
|
|
845
|
+
static func _holding_the_branch(outside: Array[Node], top: Node) -> Array[Array]:
|
|
846
|
+
var held: Array[Array] = []
|
|
847
|
+
for node: Node in outside:
|
|
848
|
+
for property: Dictionary in node.get_property_list():
|
|
849
|
+
if not GdhRead.as_int(property.get("usage", 0)) & PROPERTY_USAGE_STORAGE:
|
|
850
|
+
continue
|
|
851
|
+
var called: String = str(property["name"])
|
|
852
|
+
var marked: Array = _marked(node.get(called), top)
|
|
853
|
+
if marked[0]:
|
|
854
|
+
held.append([node, called, marked[1]])
|
|
855
|
+
return held
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
## [param value] with each node of the branch at [param top] in it as a [GdhBranchMark], as
|
|
859
|
+
## [whether it held one, the value].
|
|
860
|
+
static func _marked(value: Variant, top: Node) -> Array:
|
|
861
|
+
if typeof(value) == TYPE_OBJECT and is_instance_valid(value) and value is Node:
|
|
862
|
+
var node: Node = value
|
|
863
|
+
if node == top or top.is_ancestor_of(node):
|
|
864
|
+
return [true, GdhBranchMark.new(top.get_path_to(node))]
|
|
865
|
+
elif value is Array:
|
|
866
|
+
var items: Array = value
|
|
867
|
+
var changed: bool = false
|
|
868
|
+
var copy: Array = items.duplicate()
|
|
869
|
+
for at: int in items.size():
|
|
870
|
+
var one: Array = _marked(items[at], top)
|
|
871
|
+
if one[0]:
|
|
872
|
+
copy[at] = one[1]
|
|
873
|
+
changed = true
|
|
874
|
+
return [changed, copy if changed else value]
|
|
875
|
+
elif value is Dictionary:
|
|
876
|
+
var pairs: Dictionary = value
|
|
877
|
+
var changed: bool = false
|
|
878
|
+
var copy: Dictionary = pairs.duplicate()
|
|
879
|
+
for key: Variant in pairs:
|
|
880
|
+
var one: Array = _marked(pairs[key], top)
|
|
881
|
+
if one[0]:
|
|
882
|
+
copy[key] = one[1]
|
|
883
|
+
changed = true
|
|
884
|
+
return [changed, copy if changed else value]
|
|
885
|
+
return [false, value]
|
|
886
|
+
|
|
887
|
+
|
|
888
|
+
## [param value] with each [GdhBranchMark] in it as the node at its path below [param instance].
|
|
889
|
+
static func _unmarked(value: Variant, instance: Node) -> Variant:
|
|
890
|
+
if value is GdhBranchMark:
|
|
891
|
+
var mark: GdhBranchMark = value
|
|
892
|
+
return instance.get_node_or_null(mark.below)
|
|
893
|
+
if value is Array:
|
|
894
|
+
var items: Array = value
|
|
895
|
+
return items.map(func(item: Variant) -> Variant: return _unmarked(item, instance))
|
|
896
|
+
if value is Dictionary:
|
|
897
|
+
var pairs: Dictionary = value
|
|
898
|
+
var copy: Dictionary = pairs.duplicate()
|
|
899
|
+
for key: Variant in pairs:
|
|
900
|
+
copy[key] = _unmarked(pairs[key], instance)
|
|
901
|
+
return copy
|
|
902
|
+
return value
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
## Each node path and node the nodes [param inside] the branch at [param top] hold that reaches a
|
|
906
|
+
## node outside it, answered under namesOutside as {nodePath, property, path, names} with nodePath
|
|
907
|
+
## the holder's path in the new scene and names the node's path here. A path is kept, and names
|
|
908
|
+
## that node only where the new scene is instanced as here. A node is cleared, with cleared: true,
|
|
909
|
+
## since the new scene's file can only save it as a path within itself.
|
|
910
|
+
func _named_outside(
|
|
911
|
+
scene: GdhSceneFile, inside: Array[Node], top: Node, answer: Dictionary
|
|
912
|
+
) -> void:
|
|
913
|
+
var named: Array[Dictionary] = []
|
|
914
|
+
for node: Node in inside:
|
|
915
|
+
for property: Dictionary in node.get_property_list():
|
|
916
|
+
if not GdhRead.as_int(property.get("usage", 0)) & PROPERTY_USAGE_STORAGE:
|
|
917
|
+
continue
|
|
918
|
+
var called: String = str(property["name"])
|
|
919
|
+
if GdhHeadlessCheck.at_default(node, called):
|
|
920
|
+
continue
|
|
921
|
+
var record: Dictionary = {
|
|
922
|
+
"nodePath": str(top.get_path_to(node)), "property": called,
|
|
923
|
+
}
|
|
924
|
+
var kept: Array = _outside_of(node.get(called), node, top, scene, record, named)
|
|
925
|
+
if kept[0]:
|
|
926
|
+
scene.asked(node, [called])
|
|
927
|
+
node.set(called, kept[1])
|
|
928
|
+
if not named.is_empty():
|
|
929
|
+
answer["namesOutside"] = named
|
|
930
|
+
|
|
931
|
+
|
|
932
|
+
## [param value], held by [param holder] in the branch at [param top], with each node outside the
|
|
933
|
+
## branch in it cleared, each such node and each path reaching one recorded from [param record] in
|
|
934
|
+
## [param named], as [whether a node was cleared, the value].
|
|
935
|
+
static func _outside_of(
|
|
936
|
+
value: Variant,
|
|
937
|
+
holder: Node,
|
|
938
|
+
top: Node,
|
|
939
|
+
scene: GdhSceneFile,
|
|
940
|
+
record: Dictionary,
|
|
941
|
+
named: Array[Dictionary]
|
|
942
|
+
) -> Array:
|
|
943
|
+
if value is NodePath:
|
|
944
|
+
var path: NodePath = value
|
|
945
|
+
var target: Node = (
|
|
946
|
+
holder.get_node_or_null(NodePath(path.get_concatenated_names()))
|
|
947
|
+
if path.get_name_count() > 0
|
|
948
|
+
else null
|
|
949
|
+
)
|
|
950
|
+
if target != null and target != top and not top.is_ancestor_of(target):
|
|
951
|
+
var entry: Dictionary = record.duplicate()
|
|
952
|
+
entry["path"] = str(path)
|
|
953
|
+
entry["names"] = scene.path_of(target)
|
|
954
|
+
named.append(entry)
|
|
955
|
+
return [false, value]
|
|
956
|
+
if typeof(value) == TYPE_OBJECT and is_instance_valid(value) and value is Node:
|
|
957
|
+
var node: Node = value
|
|
958
|
+
if node == top or top.is_ancestor_of(node):
|
|
959
|
+
return [false, value]
|
|
960
|
+
var entry: Dictionary = record.duplicate()
|
|
961
|
+
entry["names"] = scene.path_of(node)
|
|
962
|
+
entry["cleared"] = true
|
|
963
|
+
named.append(entry)
|
|
964
|
+
return [true, null]
|
|
965
|
+
if value is Array:
|
|
966
|
+
var items: Array = value
|
|
967
|
+
var changed: bool = false
|
|
968
|
+
var copy: Array = items.duplicate()
|
|
969
|
+
for at: int in items.size():
|
|
970
|
+
var one: Array = _outside_of(items[at], holder, top, scene, record, named)
|
|
971
|
+
if one[0]:
|
|
972
|
+
copy[at] = one[1]
|
|
973
|
+
changed = true
|
|
974
|
+
return [changed, copy if changed else value]
|
|
975
|
+
if value is Dictionary:
|
|
976
|
+
var pairs: Dictionary = value
|
|
977
|
+
var changed: bool = false
|
|
978
|
+
var copy: Dictionary = pairs.duplicate()
|
|
979
|
+
for key: Variant in pairs:
|
|
980
|
+
var one: Array = _outside_of(pairs[key], holder, top, scene, record, named)
|
|
981
|
+
if one[0]:
|
|
982
|
+
copy[key] = one[1]
|
|
983
|
+
changed = true
|
|
984
|
+
return [changed, copy if changed else value]
|
|
985
|
+
return [false, value]
|
|
986
|
+
|
|
987
|
+
|
|
988
|
+
## The persistent connections between a node [param outside] the branch at [param top] and one
|
|
989
|
+
## [param inside] it, each taken off its source and answered as [source, signal, callable, flags,
|
|
990
|
+
## the source's path from [param top] or "", the target's path from [param top] or ""]. The source
|
|
991
|
+
## is given only when it is outside, since a node of the branch is freed with it.
|
|
992
|
+
static func _crossing(outside: Array[Node], inside: Array[Node], top: Node) -> Array[Array]:
|
|
993
|
+
var crossing: Array[Array] = []
|
|
994
|
+
for from_inside: bool in [false, true]:
|
|
995
|
+
var sources: Array[Node] = inside if from_inside else outside
|
|
996
|
+
for source: Node in sources:
|
|
997
|
+
for info: Dictionary in source.get_signal_list():
|
|
998
|
+
var signal_name: String = str(info.get("name", ""))
|
|
999
|
+
for conn: Dictionary in source.get_signal_connection_list(signal_name):
|
|
1000
|
+
var flags: int = GdhRead.as_int(conn.get("flags", 0))
|
|
1001
|
+
var callable: Callable = conn.get("callable", Callable())
|
|
1002
|
+
var target: Node = callable.get_object() as Node
|
|
1003
|
+
if not flags & Object.CONNECT_PERSIST or target == null:
|
|
1004
|
+
continue
|
|
1005
|
+
if (target == top or top.is_ancestor_of(target)) == from_inside:
|
|
1006
|
+
continue
|
|
1007
|
+
crossing.append(
|
|
1008
|
+
[
|
|
1009
|
+
null if from_inside else source,
|
|
1010
|
+
signal_name,
|
|
1011
|
+
callable,
|
|
1012
|
+
flags,
|
|
1013
|
+
str(top.get_path_to(source)) if from_inside else "",
|
|
1014
|
+
"" if from_inside else str(top.get_path_to(target)),
|
|
1015
|
+
]
|
|
1016
|
+
)
|
|
1017
|
+
source.disconnect(signal_name, callable)
|
|
1018
|
+
return crossing
|
|
1019
|
+
|
|
1020
|
+
|
|
1021
|
+
## Makes each of [param crossing], from [method _crossing], again between the node outside and the
|
|
1022
|
+
## node at its path below [param instance]. Answers each that the scene's file would not hold, as
|
|
1023
|
+
## {sourceNodePath, signalName, targetNodePath, methodName}: one from a node inside an instance is
|
|
1024
|
+
## saved only once the instance's children are editable.
|
|
1025
|
+
func _rejoined(scene: GdhSceneFile, crossing: Array[Array], instance: Node) -> Array[Dictionary]:
|
|
1026
|
+
var made: Array[Array] = []
|
|
1027
|
+
for one: Array in crossing:
|
|
1028
|
+
var callable: Callable = one[2]
|
|
1029
|
+
var source: Node = one[0]
|
|
1030
|
+
if not str(one[4]).is_empty():
|
|
1031
|
+
source = instance.get_node_or_null(NodePath(str(one[4])))
|
|
1032
|
+
# The node of the branch a callable aims at has been freed with the branch.
|
|
1033
|
+
var target: Object = (
|
|
1034
|
+
instance.get_node_or_null(NodePath(str(one[5])))
|
|
1035
|
+
if not str(one[5]).is_empty()
|
|
1036
|
+
else callable.get_object()
|
|
1037
|
+
)
|
|
1038
|
+
if source == null or target == null:
|
|
1039
|
+
continue
|
|
1040
|
+
var aimed: Callable = GdhPlacing.aimed(callable, target)
|
|
1041
|
+
if source.connect(str(one[1]), aimed, GdhRead.as_int(one[3])) == OK:
|
|
1042
|
+
made.append([source, str(one[1]), aimed, target])
|
|
1043
|
+
var lost: Array[Dictionary] = []
|
|
1044
|
+
if made.is_empty():
|
|
1045
|
+
return lost
|
|
1046
|
+
var probe: PackedScene = PackedScene.new()
|
|
1047
|
+
if probe.pack(scene.root) != OK:
|
|
1048
|
+
return lost
|
|
1049
|
+
var state: SceneState = probe.get_state()
|
|
1050
|
+
var saved: Dictionary = {}
|
|
1051
|
+
for at: int in state.get_connection_count():
|
|
1052
|
+
var key: String = "%s|%s|%s|%s" % [
|
|
1053
|
+
str(state.get_connection_source(at)).trim_prefix("./"),
|
|
1054
|
+
state.get_connection_signal(at),
|
|
1055
|
+
str(state.get_connection_target(at)).trim_prefix("./"),
|
|
1056
|
+
state.get_connection_method(at),
|
|
1057
|
+
]
|
|
1058
|
+
saved[key] = true
|
|
1059
|
+
for one: Array in made:
|
|
1060
|
+
var source: Node = one[0]
|
|
1061
|
+
var aimed: Callable = one[2]
|
|
1062
|
+
var target: Node = one[3]
|
|
1063
|
+
var held: Dictionary = {
|
|
1064
|
+
"sourceNodePath": scene.path_of(source),
|
|
1065
|
+
"signalName": one[1],
|
|
1066
|
+
"targetNodePath": scene.path_of(target),
|
|
1067
|
+
"methodName": str(aimed.get_method()),
|
|
1068
|
+
}
|
|
1069
|
+
var key: String = "%s|%s|%s|%s" % held.values()
|
|
1070
|
+
if not saved.has(key):
|
|
1071
|
+
source.disconnect(str(one[1]), aimed)
|
|
1072
|
+
lost.append(held)
|
|
1073
|
+
return lost
|
|
1074
|
+
|
|
1075
|
+
|
|
1076
|
+
## Each line of a script on a node [param inside] the branch at [param top] that names a node
|
|
1077
|
+
## outside it through a %unique node, which is not unique in the new scene at [param branch_path],
|
|
1078
|
+
## as scriptsNaming gives it with no to: {scenePath, nodePath, script, line, from}, nodePath the
|
|
1079
|
+
## node's path in the new scene.
|
|
1080
|
+
static func _scripts_naming_outside(
|
|
1081
|
+
inside: Array[Node], top: Node, branch_path: String
|
|
1082
|
+
) -> Array[Dictionary]:
|
|
1083
|
+
var found: Array[Dictionary] = []
|
|
1084
|
+
var seen: Dictionary = {}
|
|
1085
|
+
for node: Node in inside:
|
|
1086
|
+
var script: Script = node.get_script()
|
|
1087
|
+
while script is GDScript:
|
|
1088
|
+
var file: String = script.resource_path
|
|
1089
|
+
var source: String = (script as GDScript).source_code
|
|
1090
|
+
if source.is_empty() and FileAccess.file_exists(file):
|
|
1091
|
+
source = FileAccess.get_file_as_string(file)
|
|
1092
|
+
for named: Dictionary in GdhScriptPaths.named(source):
|
|
1093
|
+
var path: String = named["path"]
|
|
1094
|
+
if not path.begins_with("%"):
|
|
1095
|
+
continue
|
|
1096
|
+
var through: Node = node.get_node_or_null(NodePath(path.get_slice("/", 0)))
|
|
1097
|
+
if through == null or through == top or top.is_ancestor_of(through):
|
|
1098
|
+
continue
|
|
1099
|
+
var record: Dictionary = {
|
|
1100
|
+
"scenePath": branch_path,
|
|
1101
|
+
"nodePath": str(top.get_path_to(node)),
|
|
1102
|
+
"script": file,
|
|
1103
|
+
"line": named["line"],
|
|
1104
|
+
"from": path,
|
|
1105
|
+
}
|
|
1106
|
+
var key: String = "%s:%d:%s:%s" % [file, named["line"], path, record["nodePath"]]
|
|
1107
|
+
if not seen.has(key):
|
|
1108
|
+
seen[key] = true
|
|
1109
|
+
found.append(record)
|
|
1110
|
+
script = script.get_base_script()
|
|
1111
|
+
return found
|
|
1112
|
+
|
|
1113
|
+
|
|
661
1114
|
func set_node_properties(args: Dictionary) -> Dictionary:
|
|
662
1115
|
var project_path: String = str(args.get("projectPath", ""))
|
|
663
1116
|
var scene_path: String = GdhSceneFile.res_path(project_path, str(args.get("scenePath", "")))
|
|
@@ -1062,3 +1515,12 @@ func _save_open_scene(scene_path: String) -> Dictionary:
|
|
|
1062
1515
|
"from": "editor",
|
|
1063
1516
|
"unsavedInEditor": GdhSceneFile.unsaved(scene_path),
|
|
1064
1517
|
}
|
|
1518
|
+
|
|
1519
|
+
|
|
1520
|
+
## A node of a branch being saved as its own scene, held by a property outside it, by its path from
|
|
1521
|
+
## the branch's top, until the instance replacing the branch gives the node at that path.
|
|
1522
|
+
class GdhBranchMark:
|
|
1523
|
+
var below: NodePath
|
|
1524
|
+
|
|
1525
|
+
func _init(at: NodePath) -> void:
|
|
1526
|
+
below = at
|
|
@@ -76,6 +76,8 @@ func capture_viewport(params: Dictionary) -> Dictionary:
|
|
|
76
76
|
# refuses that state itself.
|
|
77
77
|
if mode == SubViewport.UPDATE_ALWAYS or not _host.get_tree().root.can_draw():
|
|
78
78
|
return await _capture(viewport, params)
|
|
79
|
+
# Asked for before the wait: in low-processor mode a still game draws no frame of its own, and
|
|
80
|
+
# this request is what makes it draw the one waited for.
|
|
79
81
|
sub.render_target_update_mode = SubViewport.UPDATE_ONCE
|
|
80
82
|
await RenderingServer.frame_post_draw
|
|
81
83
|
var answer: Dictionary = await _capture(viewport, params)
|