gdharness 1.3.45 → 1.3.46
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 +291 -191
- package/build/godot/addons/gdharness_editor/animation_reads.gd +8 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd +190 -0
- package/build/godot/addons/gdharness_editor/tool_executor.gd +4 -0
- package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +89 -3
- package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +61 -0
- package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +21 -2
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +18 -5
- package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +22 -3
- package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd +7 -0
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +9 -0
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +29 -2
- package/build/godot/operations/animation_reads.gd +8 -0
- package/build/godot/operations/editor_tools.gd +2 -0
- package/build/godot/operations/patterns.gd +27 -2
- package/build/godot/operations/project_diagnostics.gd +2 -1
- package/build/godot/operations/project_validation.gd +4 -2
- package/build/keeper.js +3 -2
- package/package.json +1 -1
|
@@ -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:
|
|
@@ -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 }
|
|
@@ -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,
|
|
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]
|
|
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,
|
|
116
|
-
## target when the target was copied too. A
|
|
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
|
|
@@ -560,11 +560,12 @@ func reparent_node(args: Dictionary) -> Dictionary:
|
|
|
560
560
|
GdhPlacing.own(node, root)
|
|
561
561
|
if GdhRead.as_bool(args.get("keepGlobalTransform", true)):
|
|
562
562
|
GdhPlacing.keep_in_place(node, old_parent)
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
563
|
+
# Unasked, a headless write takes the move on a node whose script is not a tool for the script's
|
|
564
|
+
# doing and puts the node back as the file held it. A new parent changes more than where the
|
|
565
|
+
# node is drawn: a container gives a Control its own layout mode.
|
|
566
|
+
var placed: Array[String] = GdhHeadlessCheck.differing(node, stored_before, {})
|
|
567
|
+
if not placed.is_empty():
|
|
568
|
+
scene.asked(node, placed)
|
|
568
569
|
var also_changed: Dictionary = {}
|
|
569
570
|
for property: String in drawn_at:
|
|
570
571
|
var now: Variant = _properties.read_at(node, property)
|
|
@@ -572,6 +573,18 @@ func reparent_node(args: Dictionary) -> Dictionary:
|
|
|
572
573
|
also_changed[property] = {
|
|
573
574
|
"was": _readable(node, drawn_at[property]), "now": _readable(node, now),
|
|
574
575
|
}
|
|
576
|
+
var drawn_in: Array[String] = GdhPlacing.placed_in(node)
|
|
577
|
+
for property: String in placed:
|
|
578
|
+
if property in drawn_in or also_changed.has(property):
|
|
579
|
+
continue
|
|
580
|
+
var was: Variant = (
|
|
581
|
+
stored_before[property]
|
|
582
|
+
if stored_before.has(property)
|
|
583
|
+
else GdhHeadlessCheck.default_of(node, property)
|
|
584
|
+
)
|
|
585
|
+
also_changed[property] = {
|
|
586
|
+
"was": _readable(node, was), "now": _readable(node, node.get(property)),
|
|
587
|
+
}
|
|
575
588
|
var sheared: String = ""
|
|
576
589
|
if node is Node3D and GdhPlacing.sheared((node as Node3D).basis):
|
|
577
590
|
var spatial: Node3D = node
|
|
@@ -92,6 +92,19 @@ func connect_signal(args: Dictionary) -> Dictionary:
|
|
|
92
92
|
if unbinds > 0:
|
|
93
93
|
callable = callable.unbind(unbinds)
|
|
94
94
|
var method_found: bool = target.has_method(method_name)
|
|
95
|
+
if not method_found and (not binds.is_empty() or unbinds > 0):
|
|
96
|
+
var named_to: String = str(target.name) if target == scene.root else target_node_path
|
|
97
|
+
return scene.refuse(
|
|
98
|
+
(
|
|
99
|
+
(
|
|
100
|
+
"%s has no method %s, and the engine connects a signal that binds or unbinds "
|
|
101
|
+
+ "arguments only to a method its target has, so the scene would load without "
|
|
102
|
+
+ "the connection. Add the method to the target's script first, or connect "
|
|
103
|
+
+ "without binds and unbinds."
|
|
104
|
+
)
|
|
105
|
+
% [named_to, method_name]
|
|
106
|
+
)
|
|
107
|
+
)
|
|
95
108
|
var misfit: String = _misfit(source, signal_name, callable, flags)
|
|
96
109
|
if not before.is_empty():
|
|
97
110
|
var held: Callable = before["callable"]
|
|
@@ -102,6 +115,7 @@ func connect_signal(args: Dictionary) -> Dictionary:
|
|
|
102
115
|
|
|
103
116
|
var from: String = scene.path_of(source)
|
|
104
117
|
var to: String = scene.path_of(target)
|
|
118
|
+
var _replaced: Dictionary = scene.drop_unmade(from, signal_name, to, method_name)
|
|
105
119
|
var saved: Dictionary = {}
|
|
106
120
|
var written: Dictionary = scene.write(
|
|
107
121
|
func(loaded: Node) -> String:
|
|
@@ -464,7 +478,11 @@ func disconnect_signal(args: Dictionary) -> Dictionary:
|
|
|
464
478
|
# A connection that was never there saved the scene unchanged and answered as removed, so a
|
|
465
479
|
# typo in either name read as success.
|
|
466
480
|
var held: Dictionary = _saved(scene.root, from, signal_name, to, method_name)
|
|
467
|
-
|
|
481
|
+
# One the engine could not make is in the file alone, and the write drops it.
|
|
482
|
+
var unmade: Dictionary = scene.drop_unmade(from, signal_name, to, method_name)
|
|
483
|
+
if held.is_empty() and not unmade.is_empty():
|
|
484
|
+
held = { "flags": unmade["flags"], "binds": unmade["binds"], "unbinds": unmade["unbinds"] }
|
|
485
|
+
elif held.is_empty():
|
|
468
486
|
# The root's path is ".", which before ".method" read as "..method".
|
|
469
487
|
var said_from: String = str(source.name) if source == scene.root else from
|
|
470
488
|
var said_to: String = str(target.name) if target == scene.root else to
|
|
@@ -492,8 +510,9 @@ func disconnect_signal(args: Dictionary) -> Dictionary:
|
|
|
492
510
|
)
|
|
493
511
|
)
|
|
494
512
|
)
|
|
495
|
-
|
|
496
|
-
|
|
513
|
+
if held.has("callable"):
|
|
514
|
+
var callable: Callable = held["callable"]
|
|
515
|
+
source.disconnect(signal_name, callable)
|
|
497
516
|
|
|
498
517
|
var written: Dictionary = scene.write(
|
|
499
518
|
func(loaded: Node) -> String:
|
|
@@ -303,6 +303,13 @@ func set_tilemap_cells(args: Dictionary) -> Dictionary:
|
|
|
303
303
|
wanted.append([at, source_id, atlas_coords, alternative])
|
|
304
304
|
if source_id == -1:
|
|
305
305
|
erased += 1
|
|
306
|
+
# An erased cell leaves the layer's data at its next update, which a layer outside the tree
|
|
307
|
+
# never has, so the pack wrote it as a cell naming no tile, where the editor's save leaves it
|
|
308
|
+
# out.
|
|
309
|
+
if layered != null:
|
|
310
|
+
layered.update_internals()
|
|
311
|
+
else:
|
|
312
|
+
single.update_internals()
|
|
306
313
|
|
|
307
314
|
var at_path: String = scene.path_of(node)
|
|
308
315
|
var written: Dictionary = scene.write(
|
|
@@ -62,6 +62,15 @@ func set_property(params: Dictionary) -> Dictionary:
|
|
|
62
62
|
"type": "error",
|
|
63
63
|
"message": GdhPaths.nothing_there(holder, named, str(reached["called"])),
|
|
64
64
|
}
|
|
65
|
+
if GdhPaths.is_constant(holder, named):
|
|
66
|
+
return {
|
|
67
|
+
"type": "error",
|
|
68
|
+
"message":
|
|
69
|
+
(
|
|
70
|
+
"%s:%s is a constant its script declares, and a constant cannot be set"
|
|
71
|
+
% [reached["called"], named]
|
|
72
|
+
),
|
|
73
|
+
}
|
|
65
74
|
var old_value: Variant = GdhPaths.read_under(holder, named)
|
|
66
75
|
var slot: Dictionary = GdhValues.slot_declared(holder, named)
|
|
67
76
|
var declared: int = slot["type"]
|