gdharness 1.3.45 → 1.3.47

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.
@@ -11,6 +11,11 @@ const LOOP_MODE_NAMES: Dictionary[int, String] = {
11
11
  Animation.LOOP_LINEAR: "linear",
12
12
  Animation.LOOP_PINGPONG: "pingpong",
13
13
  }
14
+ const UPDATE_MODE_NAMES: Dictionary[int, String] = {
15
+ Animation.UPDATE_CONTINUOUS: "continuous",
16
+ Animation.UPDATE_DISCRETE: "discrete",
17
+ Animation.UPDATE_CAPTURE: "capture",
18
+ }
14
19
  const TRACK_TYPE_NAMES: Dictionary[int, String] = {
15
20
  Animation.TYPE_VALUE: "property",
16
21
  Animation.TYPE_POSITION_3D: "position_3d",
@@ -166,6 +171,9 @@ static func _track(anim: Animation, track: int, values: GdhSerialisation) -> Dic
166
171
  var property: String = str(path.get_concatenated_subnames())
167
172
  if not property.is_empty():
168
173
  shaped["property"] = property
174
+ if kind == Animation.TYPE_VALUE:
175
+ var mode: int = anim.value_track_get_update_mode(track)
176
+ shaped["update"] = UPDATE_MODE_NAMES.get(mode, str(mode))
169
177
  var keyframes: Array[Dictionary] = []
170
178
  var methods: Dictionary = {}
171
179
  for key: int in anim.track_get_key_count(track):
@@ -38,6 +38,9 @@ static var scan_due: bool = false
38
38
  ## What the saves of a call wrote on nodes it did not ask to change, differently from their file,
39
39
  ## gathered for that call's answer as [method _also_saved] finds them.
40
40
  static var also_saved: Array[Dictionary] = []
41
+ ## What a write said of the connections it kept in the file though the engine cannot make them,
42
+ ## gathered for that call's answer as [member also_saved] is.
43
+ static var unmade_note: String = ""
41
44
 
42
45
  var root: Node = null
43
46
  var path: String = ""
@@ -75,6 +78,11 @@ var _text: String = ""
75
78
  ## from where the node is, as it keeps layout_mode at Container under a container, and stores the
76
79
  ## same from an editor.
77
80
  var _engine_kept: Dictionary = {}
81
+ ## Each connection the file holds that the engine refused to make as it loaded the scene, as
82
+ ## {source, target, from, signal, to, method, flags, binds, unbinds}: one passing binds or unbinds
83
+ ## to a method its target lacks. A pack saves only the connections made, so [method write] puts
84
+ ## these back.
85
+ var _unmade: Array[Dictionary] = []
78
86
 
79
87
 
80
88
  ## Opens [param scene_path], answering a refusal, or {} with [member root] set.
@@ -100,6 +108,8 @@ func open(scene_path: String, for_writing: bool) -> Dictionary:
100
108
  return { "ok": false, "error": "Failed to instantiate " + path }
101
109
  _state = packed.get_state()
102
110
  _base_paths = makers(_state)
111
+ if for_writing:
112
+ _unmade = _unmade_connections(_state)
103
113
  # On a read as well, which would otherwise answer what the scripts set as what the file holds.
104
114
  if for_writing or not Engine.is_editor_hint():
105
115
  for index: int in _state.get_node_count():
@@ -115,6 +125,163 @@ func open(scene_path: String, for_writing: bool) -> Dictionary:
115
125
  return {}
116
126
 
117
127
 
128
+ ## The connections in [param state] the engine did not make on [member root]: Object.connect
129
+ ## refuses a callable that binds or unbinds arguments unless its target has the method, so the scene
130
+ ## loads without them, in the editor as well, and every save drops them.
131
+ func _unmade_connections(state: SceneState) -> Array[Dictionary]:
132
+ var unmade: Array[Dictionary] = []
133
+ for index: int in state.get_connection_count():
134
+ var from: NodePath = state.get_connection_source(index)
135
+ var to: NodePath = state.get_connection_target(index)
136
+ var signal_name: StringName = state.get_connection_signal(index)
137
+ var method: StringName = state.get_connection_method(index)
138
+ var source: Node = root.get_node_or_null(from)
139
+ var target: Node = root.get_node_or_null(to)
140
+ if source == null or target == null or not source.has_signal(signal_name):
141
+ continue
142
+ var made: bool = false
143
+ for conn: Dictionary in source.get_signal_connection_list(signal_name):
144
+ var callable: Callable = conn.get("callable", Callable())
145
+ if callable.get_object() == target and callable.get_method() == method:
146
+ made = true
147
+ break
148
+ if not made:
149
+ unmade.append(
150
+ {
151
+ "source": source,
152
+ "target": target,
153
+ "from": str(from),
154
+ "signal": str(signal_name),
155
+ "to": str(to),
156
+ "method": str(method),
157
+ "flags": state.get_connection_flags(index),
158
+ "binds": state.get_connection_binds(index),
159
+ "unbinds": state.get_connection_unbinds(index),
160
+ }
161
+ )
162
+ return unmade
163
+
164
+
165
+ ## The connection the file holds that the engine could not make, from [param from].[param signal]
166
+ ## to [param to].[param method], taken out of those a write refuses to drop, since the tool removes
167
+ ## it; {} when there is none.
168
+ func drop_unmade(from: String, signal_name: String, to: String, method: String) -> Dictionary:
169
+ for index: int in _unmade.size():
170
+ var held: Dictionary = _unmade[index]
171
+ var ends: Array = [held["from"], held["signal"], held["to"], held["method"]]
172
+ if ends == [from, signal_name, to, method]:
173
+ _unmade.remove_at(index)
174
+ return held
175
+ return {}
176
+
177
+
178
+ ## The [code][connection][/code] lines that put back each connection the engine could not make, at
179
+ ## its nodes' paths now. One whose source or target the call took out of the scene goes with it.
180
+ ## Read before the write frees [member root].
181
+ func _unmade_lines() -> PackedStringArray:
182
+ var lines: PackedStringArray = []
183
+ var still: Array[Dictionary] = []
184
+ for held: Dictionary in _unmade:
185
+ var ends: Array[Node] = []
186
+ for end: String in ["source", "target"]:
187
+ var held_end: Variant = held[end]
188
+ if not is_instance_valid(held_end) or not held_end is Node:
189
+ continue
190
+ var node: Node = held_end
191
+ if node == root or root.is_ancestor_of(node):
192
+ ends.append(node)
193
+ if ends.size() < 2:
194
+ continue
195
+ var from: String = path_of(ends[0])
196
+ var to: String = path_of(ends[1])
197
+ var line: String = (
198
+ '[connection signal="%s" from="%s" to="%s" method="%s"'
199
+ % [held["signal"], from, to, held["method"]]
200
+ )
201
+ var flags: int = held["flags"]
202
+ if flags != Object.CONNECT_PERSIST:
203
+ line += " flags=%d" % flags
204
+ var bound: PackedStringArray = []
205
+ var unbinds: int = held["unbinds"]
206
+ if unbinds > 0:
207
+ line += " unbinds=%d" % unbinds
208
+ var _unbinding: bool = bound.append("unbinds %d" % unbinds)
209
+ var binds: Array = held["binds"]
210
+ if not binds.is_empty():
211
+ line += " binds= " + var_to_str(binds)
212
+ var _binding: bool = bound.append("binds " + var_to_str(binds))
213
+ var _added: bool = lines.append(line + "]")
214
+ var named_to: String = str(ends[1].name) if ends[1] == root else to
215
+ held["said"] = (
216
+ "%s.%s to %s.%s with %s"
217
+ % [
218
+ str(ends[0].name) if ends[0] == root else from,
219
+ held["signal"],
220
+ named_to,
221
+ held["method"],
222
+ " and ".join(bound),
223
+ ]
224
+ )
225
+ held["lacks"] = "%s has no method %s" % [named_to, held["method"]]
226
+ still.append(held)
227
+ _unmade = still
228
+ return lines
229
+
230
+
231
+ ## The connections [method _unmade_lines] kept, named for a sentence.
232
+ func _unmade_named() -> String:
233
+ var said: PackedStringArray = []
234
+ var lacking: PackedStringArray = []
235
+ for held: Dictionary in _unmade:
236
+ var _said: bool = said.append(str(held["said"]))
237
+ var _lacks: bool = lacking.append(str(held["lacks"]))
238
+ return (
239
+ "%s the engine cannot make as it loads the scene, %s, since %s"
240
+ % [
241
+ "a connection" if _unmade.size() == 1 else "connections",
242
+ "; ".join(said),
243
+ ", ".join(lacking),
244
+ ]
245
+ )
246
+
247
+
248
+ func _unmade_remedy() -> String:
249
+ return (
250
+ "The engine connects a signal that binds or unbinds arguments only to a method its target "
251
+ + "has: add the method to the target's script, or scene_signal disconnect takes the "
252
+ + "connection out."
253
+ )
254
+
255
+
256
+ ## Adds [param lines] to the file just saved, answering "" or why they could not be added.
257
+ func _keep_unmade(lines: PackedStringArray) -> String:
258
+ var saved: String = FileAccess.get_file_as_string(path)
259
+ var file: FileAccess = FileAccess.open(path, FileAccess.WRITE)
260
+ if file == null:
261
+ return (
262
+ "%s was saved without %s, and could not be opened again to put it back: %s"
263
+ % [path, _unmade_named(), error_string(FileAccess.get_open_error())]
264
+ )
265
+ var joined: String = "\n".join(lines) + "\n"
266
+ var _stored: bool = file.store_string(
267
+ saved + joined if saved.contains("\n[connection ") else saved + "\n" + joined
268
+ )
269
+ file.close()
270
+ refresh(path)
271
+ unmade_note = (
272
+ "%s holds %s. This write kept %s as the file held %s, and %s nothing until the method "
273
+ + "exists. %s"
274
+ ) % [
275
+ path,
276
+ _unmade_named(),
277
+ "it" if _unmade.size() == 1 else "them",
278
+ "it" if _unmade.size() == 1 else "them",
279
+ "it does" if _unmade.size() == 1 else "they do",
280
+ _unmade_remedy(),
281
+ ]
282
+ return ""
283
+
284
+
118
285
  ## Puts back what the scene's scripts changed on every node a tool has not asked to change, as
119
286
  ## [method write] does, answering "" or why that did not hold. Called once a node has come into the
120
287
  ## scene, so what its scripts did on the way in is not taken for the tool's own change.
@@ -629,7 +796,30 @@ func stores(at: String, property: String, given: Variant) -> bool:
629
796
  ## The check reads a scene instantiated from the pack, which is what loading the file gives, so
630
797
  ## anything the pack leaves out (a property with no storage, a node the scene does not own) is seen
631
798
  ## as missing before the file is touched.
799
+ ##
800
+ ## A connection the file holds that the engine could not make is written back after the save as the
801
+ ## file held it, at its nodes' paths now, and goes only with a node of it the call deleted, since a
802
+ ## pack saves only the connections made. A binary scene cannot be added to, so there the write is
803
+ ## refused.
632
804
  func write(check: Callable = Callable()) -> Dictionary:
805
+ var unmade_lines: PackedStringArray = _unmade_lines()
806
+ if not unmade_lines.is_empty() and not path.ends_with(".tscn"):
807
+ return refuse(
808
+ (
809
+ "Nothing was written: %s holds %s, and a binary scene saves only the connections "
810
+ + "the engine made. %s"
811
+ )
812
+ % [path, _unmade_named(), _unmade_remedy()]
813
+ )
814
+ var written: Dictionary = _write(check)
815
+ if written.is_empty() and not unmade_lines.is_empty():
816
+ var unkept: String = _keep_unmade(unmade_lines)
817
+ if not unkept.is_empty():
818
+ return refuse(unkept)
819
+ return written
820
+
821
+
822
+ func _write(check: Callable) -> Dictionary:
633
823
  var packed: PackedScene = PackedScene.new()
634
824
  var packing: Error = packed.pack(root)
635
825
  if packing != OK:
@@ -25,29 +25,41 @@ static func save_keeping_uid(resource: Resource, file_path: String) -> Error:
25
25
  var marked: Error = ResourceSaver.set_uid(file_path, uid)
26
26
  if marked != OK:
27
27
  return marked
28
- return _with_external_uids(file_path)
28
+ return _as_the_editor_writes(file_path)
29
29
 
30
30
 
31
- ## Puts back the uid on each external resource the text file at [param file_path] names. Only the
32
- ## editor's file system tells the saver a path's uid, so a headless save writes every reference by
33
- ## path alone, and each breaks when the file it names is moved.
34
- static func _with_external_uids(file_path: String) -> Error:
31
+ ## Writes the text file at [param file_path] as the editor's save would have.
32
+ ##
33
+ ## Only the editor's file system tells the saver a path's uid, so a headless save writes every
34
+ ## reference by path alone, and each breaks when the file it names is moved: the uid is put back on
35
+ ## each external resource.
36
+ ##
37
+ ## The saver leaves out a shader parameter holding the default the renderer gives it, and a headless
38
+ ## engine's renderer gives none, so every parameter a ShaderMaterial never set was written as null.
39
+ ## Loading null clears the parameter, so the line changes nothing, and the editor drops it again.
40
+ static func _as_the_editor_writes(file_path: String) -> Error:
35
41
  if not (file_path.ends_with(".tscn") or file_path.ends_with(".tres")):
36
42
  return OK
37
43
  var lines: PackedStringArray = FileAccess.get_file_as_string(file_path).split("\n")
38
44
  var pattern: RegEx = RegEx.create_from_string(' path="([^"]+)"')
45
+ var unset: RegEx = RegEx.create_from_string("^shader_parameter/\\S+ = null$")
46
+ var kept: PackedStringArray = []
39
47
  var changed: bool = false
40
- for at: int in lines.size():
41
- var line: String = lines[at]
42
- if not line.begins_with("[ext_resource ") or line.contains(' uid="'):
43
- continue
44
- var named: RegExMatch = pattern.search(line)
45
- if named == null:
48
+ for line: String in lines:
49
+ if unset.search(line) != null:
50
+ changed = true
46
51
  continue
47
- var uid: String = reference_uid(named.get_string(1))
52
+ var named: RegExMatch = (
53
+ pattern.search(line)
54
+ if line.begins_with("[ext_resource ") and not line.contains(' uid="')
55
+ else null
56
+ )
57
+ var uid: String = "" if named == null else reference_uid(named.get_string(1))
48
58
  if not uid.is_empty():
49
- lines[at] = line.replace(' path="', uid + ' path="')
59
+ line = line.replace(' path="', uid + ' path="')
50
60
  changed = true
61
+ var _added: bool = kept.append(line)
62
+ lines = kept
51
63
  if not changed:
52
64
  return OK
53
65
  var file: FileAccess = FileAccess.open(file_path, FileAccess.WRITE)
@@ -176,12 +176,16 @@ func execute_tool(tool_name: String, args: Dictionary) -> Dictionary:
176
176
  return { "ok": false, "error": "Tool method not found: %s.%s" % [node.name, method] }
177
177
 
178
178
  GdhSceneFile.also_saved.clear()
179
+ GdhSceneFile.unmade_note = ""
179
180
  var result: Variant = node.call(method, args)
180
181
  if result is Dictionary:
181
182
  var answered: Dictionary = result
182
183
  if not GdhSceneFile.also_saved.is_empty():
183
184
  answered["alsoSaved"] = GdhSceneFile.also_saved.duplicate(true)
184
185
  GdhSceneFile.also_saved.clear()
186
+ if not GdhSceneFile.unmade_note.is_empty():
187
+ answered["unmadeConnections"] = GdhSceneFile.unmade_note
188
+ GdhSceneFile.unmade_note = ""
185
189
  return answered
186
190
 
187
191
  return { "ok": false, "error": "Invalid tool result from: " + tool_name }
@@ -137,13 +137,26 @@ func connect_animation_states(args: Dictionary) -> Dictionary:
137
137
  sm.add_transition(StringName(from_state), StringName(to_state), transition)
138
138
  if not sm.has_transition(StringName(from_state), StringName(to_state)):
139
139
  return scene.refuse("The state machine did not take the transition.")
140
+ _tell_the_tree(sm)
140
141
  var files: Dictionary = found[2]
141
- return _finish(
142
- scene,
143
- Callable(),
144
- files,
145
- { "ok": true, "from": from_state, "to": to_state, "advanceMode": advance_mode }
146
- )
142
+ var made: Dictionary = {
143
+ "ok": true,
144
+ "from": from_state,
145
+ "to": to_state,
146
+ "transitionType": transition_type,
147
+ "advanceMode": advance_mode,
148
+ }
149
+ if not advance_condition.is_empty():
150
+ made["advanceCondition"] = advance_condition
151
+ return _finish(scene, Callable(), files, made)
152
+
153
+
154
+ ## Has the AnimationTree playing [param sm] list its parameters again. Adding or removing a
155
+ ## transition does not say the tree changed, so the save kept the parameters from before it: a
156
+ ## condition's parameter was written by the next call to change the machine, and a removed one's
157
+ ## stayed until then.
158
+ static func _tell_the_tree(sm: AnimationNodeStateMachine) -> void:
159
+ var _told: Error = sm.emit_signal(&"tree_changed")
147
160
 
148
161
 
149
162
  ## Why [param sm] cannot take a transition from [param from_state] to [param to_state], or "".
@@ -270,6 +283,7 @@ func disconnect_animation_states(args: Dictionary) -> Dictionary:
270
283
  sm.remove_transition(StringName(from_state), StringName(to_state))
271
284
  if sm.has_transition(StringName(from_state), StringName(to_state)):
272
285
  return scene.refuse("The state machine did not let go of the transition.")
286
+ _tell_the_tree(sm)
273
287
  var files: Dictionary = found[2]
274
288
  return _finish(
275
289
  scene, Callable(), files, { "ok": true, "removed": { "from": from_state, "to": to_state } }
@@ -3,6 +3,29 @@ extends "animation_tools.gd"
3
3
  ## The tracks and keys of an animation, beside the players and libraries the script it extends
4
4
  ## edits, whose helpers it shares.
5
5
 
6
+ ## The types whose values the editor blends between keys, so it makes a track on one continuous.
7
+ const BLENDED_TYPES: Array[int] = [
8
+ TYPE_FLOAT,
9
+ TYPE_VECTOR2,
10
+ TYPE_RECT2,
11
+ TYPE_VECTOR3,
12
+ TYPE_TRANSFORM2D,
13
+ TYPE_VECTOR4,
14
+ TYPE_PLANE,
15
+ TYPE_QUATERNION,
16
+ TYPE_AABB,
17
+ TYPE_BASIS,
18
+ TYPE_TRANSFORM3D,
19
+ TYPE_PROJECTION,
20
+ TYPE_COLOR,
21
+ TYPE_PACKED_FLOAT32_ARRAY,
22
+ TYPE_PACKED_FLOAT64_ARRAY,
23
+ TYPE_PACKED_VECTOR2_ARRAY,
24
+ TYPE_PACKED_VECTOR3_ARRAY,
25
+ TYPE_PACKED_COLOR_ARRAY,
26
+ TYPE_PACKED_VECTOR4_ARRAY,
27
+ ]
28
+
6
29
 
7
30
  # =============================================================================
8
31
  # add_animation_track, remove_animation_track
@@ -61,7 +84,7 @@ func add_animation_track(args: Dictionary) -> Dictionary:
61
84
  )
62
85
  var keyframes: Array = keyed[1]
63
86
  var replacing: bool = GdhRead.as_bool(args.get("replaceKeys", false))
64
- var built: Array = _add_track(anim, track, target, target_path, keyframes, replacing)
87
+ var built: Array = _add_track(player, anim, track, target_path, keyframes, replacing)
65
88
  if not str(built[1]).is_empty():
66
89
  return scene.refuse(str(built[1]))
67
90
  var track_idx: int = built[0]
@@ -83,6 +106,9 @@ func add_animation_track(args: Dictionary) -> Dictionary:
83
106
  "keys": keys,
84
107
  "keysAdded": keyframes.size(),
85
108
  }
109
+ if anim.track_get_type(track_idx) == Animation.TYPE_VALUE:
110
+ var mode: int = anim.value_track_get_update_mode(track_idx)
111
+ answer["update"] = GdhAnimationReads.UPDATE_MODE_NAMES.get(mode, str(mode))
86
112
  if built[3]:
87
113
  answer["existingTrack"] = true
88
114
  var replaced: Array = built[5]
@@ -112,7 +138,9 @@ func add_animation_track(args: Dictionary) -> Dictionary:
112
138
  return _finish(scene, check, files, answer)
113
139
 
114
140
 
115
- ## Adds [param track] to [param anim] with [param keyframes], as [index, why not, methodFound,
141
+ ## Adds [param track] to [param anim] of [param player], on the node at [param target_path] from the
142
+ ## player's root, which the caller has found, with [param keyframes], as [index, why not,
143
+ ## methodFound,
116
144
  ## existing, misfit, replaced]: methodFound is null for a property track, existing is whether the
117
145
  ## keys went into a track the animation already had for that path, misfit why a method key's
118
146
  ## arguments cannot reach the method, or "", and replaced each key [param replacing] let a keyframe
@@ -122,23 +150,31 @@ func add_animation_track(args: Dictionary) -> Dictionary:
122
150
  ## animates: two tracks for one path both play, the later overriding the earlier, and a key meant
123
151
  ## to extend the motion was split off where reading the animation showed two halves.
124
152
  func _add_track(
153
+ player: AnimationPlayer,
125
154
  anim: Animation,
126
155
  track: Dictionary,
127
- target: Node,
128
156
  target_path: String,
129
157
  keyframes: Array,
130
158
  replacing: bool
131
159
  ) -> Array:
160
+ var target: Node = _animated(player, target_path)
132
161
  var track_type: String = str(track.get("type", ""))
133
162
  var keys: Array = []
134
163
  var method_found: Variant = null
135
164
  var misfit: String = ""
136
165
  var kind: Animation.TrackType = Animation.TYPE_VALUE
137
166
  var path: NodePath = NodePath(target_path)
167
+ var update: int = -1
138
168
  if track_type == "property":
139
169
  var prop_name: String = str(track.get("property", ""))
140
170
  if prop_name.is_empty():
141
171
  return _not_added("track.property is required for property track")
172
+ # The server has refused any name but these.
173
+ var named: Variant = GdhAnimationReads.UPDATE_MODE_NAMES.find_key(
174
+ str(track.get("update", ""))
175
+ )
176
+ if named != null:
177
+ update = GdhRead.as_int(named)
142
178
  var values: Array = _key_values(target, prop_name, keyframes)
143
179
  if not values[0]:
144
180
  return _not_added(str(values[1]))
@@ -210,6 +246,10 @@ func _add_track(
210
246
  else:
211
247
  track_idx = anim.add_track(kind)
212
248
  anim.track_set_path(track_idx, path)
249
+ if kind == Animation.TYPE_VALUE:
250
+ _as_the_editor_makes(anim, track_idx, player, target)
251
+ if update >= 0:
252
+ anim.value_track_set_update_mode(track_idx, update as Animation.UpdateMode)
213
253
  for index: int in keyframes.size():
214
254
  var keyframe: Dictionary = keyframes[index]
215
255
  if anim.track_insert_key(track_idx, GdhRead.as_float(keyframe["time"]), keys[index]) < 0:
@@ -217,6 +257,52 @@ func _add_track(
217
257
  return [track_idx, "", method_found, existing, misfit, replaced]
218
258
 
219
259
 
260
+ ## Sets the new property track [param index] up as the editor sets up a track it makes: as the
261
+ ## RESET animation's track for the same path when there is one, and otherwise discrete unless the
262
+ ## property's declared type is one the editor blends, so a bool or an int changes at its key rather
263
+ ## than halfway to it.
264
+ static func _as_the_editor_makes(
265
+ anim: Animation, index: int, player: AnimationPlayer, target: Node
266
+ ) -> void:
267
+ var path: NodePath = anim.track_get_path(index)
268
+ var mode: Animation.UpdateMode = Animation.UPDATE_DISCRETE
269
+ if player.has_animation(&"RESET") and player.get_animation(&"RESET") != anim:
270
+ var reset: Animation = player.get_animation(&"RESET")
271
+ for kind: Animation.TrackType in [Animation.TYPE_VALUE, Animation.TYPE_BEZIER]:
272
+ var at: int = reset.find_track(path, kind)
273
+ if at < 0:
274
+ continue
275
+ if kind == Animation.TYPE_VALUE:
276
+ mode = reset.value_track_get_update_mode(at)
277
+ anim.value_track_set_update_mode(index, mode)
278
+ anim.track_set_interpolation_type(index, reset.track_get_interpolation_type(at))
279
+ anim.track_set_interpolation_loop_wrap(
280
+ index, reset.track_get_interpolation_loop_wrap(at)
281
+ )
282
+ return
283
+ if BLENDED_TYPES.has(_declared_type(target, path)):
284
+ mode = Animation.UPDATE_CONTINUOUS
285
+ anim.value_track_set_update_mode(index, mode)
286
+
287
+
288
+ ## The type the node or resource holding the last name of [param path] declares for it, read from
289
+ ## [param target] as the editor reads it: a property of a property, as position:x, is no name the
290
+ ## node declares, so it has none.
291
+ static func _declared_type(target: Node, path: NodePath) -> int:
292
+ var reached: Array = target.get_node_and_resource(
293
+ NodePath(".:" + str(path.get_concatenated_subnames()))
294
+ )
295
+ var leftover: NodePath = reached[2]
296
+ var holder: Object = reached[1] if reached[1] != null else reached[0]
297
+ if holder == null or leftover.get_subname_count() == 0:
298
+ return TYPE_NIL
299
+ var leaf: String = str(leftover.get_subname(leftover.get_subname_count() - 1))
300
+ for declared: Dictionary in holder.get_property_list():
301
+ if str(declared.get("name", "")) == leaf:
302
+ return GdhRead.as_int(declared.get("type", TYPE_NIL))
303
+ return TYPE_NIL
304
+
305
+
220
306
  ## What [method _add_track] answers when it added nothing, because of [param why].
221
307
  static func _not_added(why: String) -> Array:
222
308
  return [-1, why, null, false, "", []]
@@ -86,11 +86,18 @@ func carry(scene: GdhSceneFile, moves: Dictionary) -> int:
86
86
  # A value another file gives is in that file's terms, which its own pass carried already.
87
87
  var from: String = scene.values_from(node, was)
88
88
  var set_here: Dictionary = scene.set_at(was) if not from.is_empty() else {}
89
+ var read_here: bool = not from.is_empty()
89
90
  for property: Dictionary in node.get_property_list():
90
91
  var name: String = str(property["name"])
91
92
  if not _may_hold_paths(property):
92
93
  continue
93
94
  var entry: Dictionary = { "scenePath": scene.path, "nodePath": now, "property": name }
95
+ if _names_nodes(property):
96
+ if not read_here:
97
+ set_here = scene.set_at(was)
98
+ read_here = true
99
+ _carry_nodes(node, name, set_here.get(name), was, now, entry)
100
+ continue
94
101
  var elsewhere: String = "" if set_here.has(name) else from
95
102
  var carried: Array = _carried(node.get(name), was, now, entry, elsewhere)
96
103
  if carried[0]:
@@ -240,6 +247,60 @@ static func _may_hold_paths(property: Dictionary) -> bool:
240
247
  )
241
248
 
242
249
 
250
+ ## Whether [param property] is typed as a node, or as an array or dictionary of nodes. It holds the
251
+ ## nodes themselves, and the pack writes the path to each as it saves.
252
+ static func _names_nodes(property: Dictionary) -> bool:
253
+ var hint: int = GdhRead.as_int(property.get("hint"))
254
+ match GdhRead.as_int(property.get("type")):
255
+ TYPE_OBJECT:
256
+ return hint == PROPERTY_HINT_NODE_TYPE
257
+ TYPE_ARRAY, TYPE_DICTIONARY:
258
+ return (
259
+ hint == PROPERTY_HINT_TYPE_STRING
260
+ and str(property.get("hint_string", "")).contains(
261
+ "%d/%d:" % [TYPE_OBJECT, PROPERTY_HINT_NODE_TYPE]
262
+ )
263
+ )
264
+ return false
265
+
266
+
267
+ ## Carries the property [param name] of [param node], typed as nodes, from [param held], the paths
268
+ ## the file gives it. The nodes it holds moved with the move, so only what the file held is named;
269
+ ## marking it asked lets the write save the new paths the pack gives. A node the move deleted is
270
+ ## cleared, as the editor's save clears it, since no path reaches it.
271
+ func _carry_nodes(
272
+ node: Node, name: String, held: Variant, was: String, now: String, entry: Dictionary
273
+ ) -> void:
274
+ var naming: int = still_naming.size()
275
+ var carried: Array = _carried(held, was, now, entry, "")
276
+ if still_naming.size() > naming:
277
+ for index: int in range(naming, still_naming.size()):
278
+ still_naming[index]["cleared"] = true
279
+ _scene.asked(node, [name])
280
+ node.set(name, _unfreed(node.get(name)))
281
+ elif carried[0]:
282
+ _scene.asked(node, [name])
283
+
284
+
285
+ ## [param value] with each freed node in it as null.
286
+ static func _unfreed(value: Variant) -> Variant:
287
+ if typeof(value) == TYPE_OBJECT:
288
+ return value if is_instance_valid(value) else null
289
+ if typeof(value) == TYPE_ARRAY:
290
+ var items: Array = value
291
+ items = items.duplicate()
292
+ for index: int in items.size():
293
+ items[index] = _unfreed(items[index])
294
+ return items
295
+ if typeof(value) == TYPE_DICTIONARY:
296
+ var pairs: Dictionary = value
297
+ pairs = pairs.duplicate()
298
+ for key: Variant in pairs.keys():
299
+ pairs[key] = _unfreed(pairs[key])
300
+ return pairs
301
+ return value
302
+
303
+
243
304
  ## [param value], held by the node that was at [param was] and is at [param now], with each path in
244
305
  ## it carried, as [whether any was, the value]. A resource built into the scene is changed in place.
245
306
  ## [param entry] says where the value is held, for the record of each path. A path in a resource may
@@ -112,8 +112,9 @@ static func copied(source: Node, root: Node, copies: Dictionary) -> Node:
112
112
  return copy
113
113
 
114
114
 
115
- ## Gives each copy in [param copies] the persistent connections its source has, to the copy of the
116
- ## target when the target was copied too. A connection an instance makes inside itself is skipped:
115
+ ## Gives each copy in [param copies] the persistent connections its source has, with their flags,
116
+ ## bound arguments and unbinds, to the copy of the target when the target was copied too. A
117
+ ## connection an instance makes inside itself is skipped:
117
118
  ## the instance made again from its file already has it.
118
119
  static func reconnect(copies: Dictionary, top: Node) -> void:
119
120
  for key: Variant in copies:
@@ -134,7 +135,15 @@ static func reconnect(copies: Dictionary, top: Node) -> void:
134
135
  and (target == top or top.is_ancestor_of(target as Node))
135
136
  ):
136
137
  continue
138
+ # Unbound and then bound again as Node.duplicate does: a callable made
139
+ # from the method alone would save the copy without the arguments the
140
+ # original passes.
137
141
  var aimed: Callable = Callable(to, callable.get_method())
142
+ var unbound: int = callable.get_unbound_arguments_count()
143
+ if unbound > 0:
144
+ aimed = aimed.unbind(unbound)
145
+ if callable.get_bound_arguments_count() > 0:
146
+ aimed = aimed.bindv(callable.get_bound_arguments())
138
147
  if (
139
148
  not copy.is_connected(signal_name, aimed)
140
149
  and copy.connect(signal_name, aimed, flags) != OK
@@ -207,6 +216,16 @@ static func placing(node: Node) -> Array[String]:
207
216
  return []
208
217
 
209
218
 
219
+ ## The stored properties [param node] keeps what [method placing] names in: a Control's position
220
+ ## is its offsets, and a Node3D's position, rotation and scale its transform.
221
+ static func placed_in(node: Node) -> Array[String]:
222
+ if node is Node3D:
223
+ return ["transform"]
224
+ if node is Control:
225
+ return ["offset_left", "offset_top", "offset_right", "offset_bottom"]
226
+ return placing(node)
227
+
228
+
210
229
  ## Gives [param node], just moved from under [param was], the transform that keeps it where it was
211
230
  ## drawn, as the editor's reparent and Node.reparent do. Worked out from the local transforms
212
231
  ## rather than read off the global ones, which the engine answers only for a node in the tree, and