gdharness 1.3.8 → 1.3.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/cli.js +4668 -3229
- package/build/godot/addons/gdharness_editor/animation_reads.gd +115 -4
- package/build/godot/addons/gdharness_editor/declared_values.gd +272 -0
- package/build/godot/addons/gdharness_editor/file_reads.gd +115 -18
- package/build/godot/addons/gdharness_editor/headless_check.gd +49 -2
- package/build/godot/addons/gdharness_editor/property_paths.gd +89 -16
- package/build/godot/addons/gdharness_editor/property_values.gd +180 -121
- package/build/godot/addons/gdharness_editor/reading.gd +112 -6
- package/build/godot/addons/gdharness_editor/scene_file.gd +117 -174
- package/build/godot/addons/gdharness_editor/scene_uids.gd +107 -0
- package/build/godot/addons/gdharness_editor/scene_uids.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/serialisation.gd +19 -29
- package/build/godot/addons/gdharness_editor/setting_reads.gd +121 -0
- package/build/godot/addons/gdharness_editor/setting_reads.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tool_executor.gd +39 -6
- package/build/godot/addons/gdharness_editor/tools/animation_states.gd +263 -0
- package/build/godot/addons/gdharness_editor/tools/animation_states.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +49 -414
- package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +464 -0
- package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/class_tools.gd +37 -0
- package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd +221 -0
- package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +162 -483
- package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +16 -0
- package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +8 -4
- package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +2 -1
- package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +10 -0
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +151 -8
- package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +7 -30
- package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +190 -38
- package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd +485 -0
- package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/value_shapes.gd +128 -1
- package/build/godot/addons/gdharness_runtime/call_errors.gd +8 -2
- package/build/godot/addons/gdharness_runtime/declared_values.gd +272 -0
- package/build/godot/addons/gdharness_runtime/reading.gd +112 -6
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +35 -3
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +5 -0
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +188 -30
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +107 -104
- package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +17 -0
- package/build/godot/addons/gdharness_runtime/runtime_pad.gd +13 -4
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +320 -27
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +165 -20
- package/build/godot/addons/gdharness_runtime/runtime_screen.gd +49 -3
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +31 -2
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +250 -10
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +279 -11
- package/build/godot/addons/gdharness_runtime/runtime_words.gd +27 -0
- package/build/godot/addons/gdharness_runtime/serialisation.gd +19 -29
- package/build/godot/addons/gdharness_runtime/value_shapes.gd +128 -1
- package/build/godot/operations/animation_reads.gd +115 -4
- package/build/godot/operations/audio_buses.gd +93 -7
- package/build/godot/operations/classdb_queries.gd +119 -7
- package/build/godot/operations/declared_values.gd +272 -0
- package/build/godot/operations/dependencies.gd +154 -41
- package/build/godot/operations/editor_tools.gd +29 -13
- package/build/godot/operations/export_presets.gd +602 -4
- package/build/godot/operations/file_reads.gd +115 -18
- package/build/godot/operations/gdscript_analysis.gd +14 -0
- package/build/godot/operations/gdscript_authoring.gd +12 -2
- package/build/godot/operations/godot_operations.gd +18 -2
- package/build/godot/operations/import_pipeline.gd +20 -221
- package/build/godot/operations/input_actions.gd +127 -51
- package/build/godot/operations/project_config.gd +113 -68
- package/build/godot/operations/project_diagnostics.gd +4 -0
- package/build/godot/operations/project_validation.gd +266 -0
- package/build/godot/operations/property_paths.gd +89 -16
- package/build/godot/operations/property_values.gd +180 -121
- package/build/godot/operations/reading.gd +112 -6
- package/build/godot/operations/scene_connections.gd +222 -0
- package/build/godot/operations/serialisation.gd +19 -29
- package/build/godot/operations/setting_reads.gd +121 -0
- package/build/godot/operations/value_shapes.gd +128 -1
- package/build/godot/tests/listed.gd +20 -13
- package/build/keeper.js +1402 -707
- package/package.json +1 -1
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
@tool
|
|
2
|
+
extends "animation_tools.gd"
|
|
3
|
+
|
|
4
|
+
## The tracks and keys of an animation, beside the players and libraries the script it extends edits,
|
|
5
|
+
## whose helpers it shares.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# =============================================================================
|
|
9
|
+
# add_animation_track, remove_animation_track
|
|
10
|
+
# =============================================================================
|
|
11
|
+
func add_animation_track(args: Dictionary) -> Dictionary:
|
|
12
|
+
var scene_path: String = GdhSceneFile.res_path("", str(args.get("scenePath", "")))
|
|
13
|
+
var player_node_path: String = str(args.get("playerNodePath", "."))
|
|
14
|
+
var animation_name: String = str(args.get("animationName", ""))
|
|
15
|
+
var given_track: Variant = args.get("track", {})
|
|
16
|
+
|
|
17
|
+
if scene_path.strip_edges() == "res://":
|
|
18
|
+
return {"ok": false, "error": "Missing scenePath"}
|
|
19
|
+
if animation_name.strip_edges().is_empty():
|
|
20
|
+
return {"ok": false, "error": "Missing animationName"}
|
|
21
|
+
var track: Dictionary = given_track if given_track is Dictionary else {}
|
|
22
|
+
if track.is_empty():
|
|
23
|
+
return {"ok": false, "error": "Missing track"}
|
|
24
|
+
# The schema leaves keyframes out of what track requires, since remove_track finds a track without
|
|
25
|
+
# them.
|
|
26
|
+
if not track.has("keyframes"):
|
|
27
|
+
return {"ok": false, "error": "Missing track.keyframes"}
|
|
28
|
+
var keyed: Array = _keyframes(track)
|
|
29
|
+
if not keyed[0]:
|
|
30
|
+
return {"ok": false, "error": keyed[1]}
|
|
31
|
+
|
|
32
|
+
# Asked of the player, which knows every library: looking only in the default one refused
|
|
33
|
+
# "moves/walk" as missing.
|
|
34
|
+
var opened: Array = _animation_at(scene_path, player_node_path, animation_name)
|
|
35
|
+
var scene: GdhSceneFile = opened[0]
|
|
36
|
+
if opened[1] == null:
|
|
37
|
+
return opened[2]
|
|
38
|
+
var player: AnimationPlayer = opened[1]
|
|
39
|
+
var anim: Animation = player.get_animation(StringName(animation_name))
|
|
40
|
+
var homes: Array = _homes(
|
|
41
|
+
scene, [[_library_of(player, animation_name), "animation library"], [anim, "animation"]]
|
|
42
|
+
)
|
|
43
|
+
if not homes[0]:
|
|
44
|
+
return scene.refuse(str(homes[1]))
|
|
45
|
+
|
|
46
|
+
var target_path: String = str(track.get("nodePath", ""))
|
|
47
|
+
if target_path.is_empty():
|
|
48
|
+
target_path = "."
|
|
49
|
+
var target: Node = _animated(player, target_path)
|
|
50
|
+
if target == null:
|
|
51
|
+
return scene.refuse(
|
|
52
|
+
(
|
|
53
|
+
(
|
|
54
|
+
"No node at %s from the player's root node, %s, which is what the track path is read "
|
|
55
|
+
+ "against."
|
|
56
|
+
)
|
|
57
|
+
% [target_path, str(player.root_node)]
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
var keyframes: Array = keyed[1]
|
|
61
|
+
var built: Array = _add_track(anim, track, target, target_path, keyframes)
|
|
62
|
+
if not str(built[1]).is_empty():
|
|
63
|
+
return scene.refuse(str(built[1]))
|
|
64
|
+
var track_idx: int = built[0]
|
|
65
|
+
|
|
66
|
+
var at: String = scene.path_of(player)
|
|
67
|
+
var keys: int = anim.track_get_key_count(track_idx)
|
|
68
|
+
var check: Callable = func(saved: Node) -> String:
|
|
69
|
+
var copy: AnimationPlayer = saved.get_node_or_null(at) as AnimationPlayer
|
|
70
|
+
if copy == null or not copy.has_animation(StringName(animation_name)):
|
|
71
|
+
return "%s would not be in the scene when it loads." % animation_name
|
|
72
|
+
var loaded: Animation = copy.get_animation(StringName(animation_name))
|
|
73
|
+
if loaded.get_track_count() <= track_idx or loaded.track_get_key_count(track_idx) != keys:
|
|
74
|
+
return "the track would not be in %s when the scene loads." % animation_name
|
|
75
|
+
return ""
|
|
76
|
+
var answer: Dictionary = {
|
|
77
|
+
"ok": true,
|
|
78
|
+
"trackType": str(track.get("type", "")),
|
|
79
|
+
"trackIndex": track_idx,
|
|
80
|
+
"keys": keys,
|
|
81
|
+
"keysAdded": keyframes.size(),
|
|
82
|
+
}
|
|
83
|
+
if built[3]:
|
|
84
|
+
answer["existingTrack"] = true
|
|
85
|
+
if built[2] != null:
|
|
86
|
+
answer["methodFound"] = built[2]
|
|
87
|
+
# Written all the same, as a missing method is: the method may be changed next.
|
|
88
|
+
if built[2]:
|
|
89
|
+
answer["argumentsFit"] = str(built[4]).is_empty()
|
|
90
|
+
if not str(built[4]).is_empty():
|
|
91
|
+
answer["misfit"] = built[4]
|
|
92
|
+
# Only the keys this call wrote: a key left past the end by an earlier call was named again on
|
|
93
|
+
# every later one, as though the key just written were past it.
|
|
94
|
+
var written: PackedStringArray = []
|
|
95
|
+
for keyframe: Dictionary in keyframes:
|
|
96
|
+
var time: float = GdhRead.as_float(keyframe["time"])
|
|
97
|
+
if time > anim.length + 0.0001:
|
|
98
|
+
var _added: bool = written.append(
|
|
99
|
+
"%s at %s s" % [str(anim.track_get_path(track_idx)), _seconds(time)]
|
|
100
|
+
)
|
|
101
|
+
var past: String = _past_sentence(anim, animation_name, written)
|
|
102
|
+
if not past.is_empty():
|
|
103
|
+
answer["pastLength"] = past
|
|
104
|
+
var files: Dictionary = homes[1]
|
|
105
|
+
return _finish(scene, check, files, answer)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
## Adds [param track] to [param anim] with [param keyframes], as [index, why not, methodFound,
|
|
109
|
+
## existing, misfit]: methodFound is null for a property track, existing is whether the keys went
|
|
110
|
+
## into a track the animation already had for that path, and misfit why a method key's arguments
|
|
111
|
+
## cannot reach the method, or "".
|
|
112
|
+
##
|
|
113
|
+
## Into that track rather than a second one beside it, as the editor keys a property it already
|
|
114
|
+
## animates: two tracks for one path both play, the later overriding the earlier, and a key meant
|
|
115
|
+
## to extend the motion was split off where reading the animation showed two halves.
|
|
116
|
+
func _add_track(
|
|
117
|
+
anim: Animation, track: Dictionary, target: Node, target_path: String, keyframes: Array
|
|
118
|
+
) -> Array:
|
|
119
|
+
var track_type: String = str(track.get("type", ""))
|
|
120
|
+
var keys: Array = []
|
|
121
|
+
var method_found: Variant = null
|
|
122
|
+
var misfit: String = ""
|
|
123
|
+
var kind: Animation.TrackType = Animation.TYPE_VALUE
|
|
124
|
+
var path: NodePath = NodePath(target_path)
|
|
125
|
+
if track_type == "property":
|
|
126
|
+
var prop_name: String = str(track.get("property", ""))
|
|
127
|
+
if prop_name.is_empty():
|
|
128
|
+
return [-1, "track.property is required for property track", null, false, ""]
|
|
129
|
+
var values: Array = _key_values(target, prop_name, keyframes)
|
|
130
|
+
if not values[0]:
|
|
131
|
+
return [-1, values[1], null, false, ""]
|
|
132
|
+
keys = values[1]
|
|
133
|
+
path = NodePath(target_path + ":" + prop_name)
|
|
134
|
+
elif track_type == "method":
|
|
135
|
+
var method_name: String = str(track.get("method", ""))
|
|
136
|
+
if method_name.is_empty():
|
|
137
|
+
return [-1, "track.method is required for method track", null, false, ""]
|
|
138
|
+
for index: int in keyframes.size():
|
|
139
|
+
var keyframe: Dictionary = keyframes[index]
|
|
140
|
+
var given: Variant = keyframe.get("args", [])
|
|
141
|
+
if not given is Array:
|
|
142
|
+
return [-1, "keyframes[%d].args must be a list." % index, null, false, ""]
|
|
143
|
+
var raw: Array = given
|
|
144
|
+
var arguments: Array = _fitted(target, method_name, _parse_method_args(raw))
|
|
145
|
+
keys.append({"method": method_name, "args": arguments})
|
|
146
|
+
if misfit.is_empty():
|
|
147
|
+
misfit = _key_misfit(target, method_name, arguments, index)
|
|
148
|
+
method_found = target.has_method(method_name)
|
|
149
|
+
kind = Animation.TYPE_METHOD
|
|
150
|
+
else:
|
|
151
|
+
return [
|
|
152
|
+
-1,
|
|
153
|
+
(
|
|
154
|
+
"add_track makes property and method tracks, and track.type is %s; remove_track takes any."
|
|
155
|
+
% track_type
|
|
156
|
+
),
|
|
157
|
+
null,
|
|
158
|
+
false,
|
|
159
|
+
""
|
|
160
|
+
]
|
|
161
|
+
|
|
162
|
+
var track_idx: int = anim.find_track(path, kind)
|
|
163
|
+
var existing: bool = track_idx >= 0
|
|
164
|
+
if existing:
|
|
165
|
+
# Checked before any key goes in, since track_insert_key replaces a key at the same time
|
|
166
|
+
# without a word, and a refusal partway would leave the editor's scene half changed.
|
|
167
|
+
for index: int in keyframes.size():
|
|
168
|
+
var keyframe: Dictionary = keyframes[index]
|
|
169
|
+
var time: float = GdhRead.as_float(keyframe["time"])
|
|
170
|
+
if anim.track_find_key(track_idx, time, Animation.FIND_MODE_APPROX) >= 0:
|
|
171
|
+
return [
|
|
172
|
+
-1,
|
|
173
|
+
(
|
|
174
|
+
(
|
|
175
|
+
"Track %d already animates %s and has a key at %s s, which keyframes[%d] would "
|
|
176
|
+
+ "replace. Nothing was written."
|
|
177
|
+
)
|
|
178
|
+
% [track_idx, str(path), _seconds(time), index]
|
|
179
|
+
),
|
|
180
|
+
null,
|
|
181
|
+
false,
|
|
182
|
+
""
|
|
183
|
+
]
|
|
184
|
+
else:
|
|
185
|
+
track_idx = anim.add_track(kind)
|
|
186
|
+
anim.track_set_path(track_idx, path)
|
|
187
|
+
for index: int in keyframes.size():
|
|
188
|
+
var keyframe: Dictionary = keyframes[index]
|
|
189
|
+
if anim.track_insert_key(track_idx, GdhRead.as_float(keyframe["time"]), keys[index]) < 0:
|
|
190
|
+
return [-1, "Keyframe %d could not be inserted." % index, null, false, ""]
|
|
191
|
+
return [track_idx, "", method_found, existing, misfit]
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
## [param arguments] as [param method] on [param target] takes them: a whole number read as a
|
|
195
|
+
## float, as JSON gives every number, is an int unless the parameter is a float, so a key calling
|
|
196
|
+
## step(by: int) with 3 is saved as 3 rather than 3.0.
|
|
197
|
+
static func _fitted(target: Node, method: String, arguments: Array) -> Array:
|
|
198
|
+
var parameters: Array = []
|
|
199
|
+
for info: Dictionary in target.get_method_list():
|
|
200
|
+
if str(info.get("name", "")) == method:
|
|
201
|
+
parameters = info.get("args", [])
|
|
202
|
+
break
|
|
203
|
+
var fitted: Array = []
|
|
204
|
+
for at: int in arguments.size():
|
|
205
|
+
var value: Variant = arguments[at]
|
|
206
|
+
var declared: int = TYPE_NIL
|
|
207
|
+
if at < parameters.size():
|
|
208
|
+
var parameter: Dictionary = parameters[at]
|
|
209
|
+
declared = GdhRead.as_int(parameter.get("type", TYPE_NIL))
|
|
210
|
+
if value is float and declared != TYPE_FLOAT:
|
|
211
|
+
var number: float = value
|
|
212
|
+
if is_finite(number) and number == floorf(number) and absf(number) < 9007199254740992.0:
|
|
213
|
+
value = int(number)
|
|
214
|
+
fitted.append(value)
|
|
215
|
+
return fitted
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
## Why the key [param index], calling [param method] on [param target] with [param arguments],
|
|
219
|
+
## fails each time it plays, or "" when it can be called or the method is not there to judge. Only
|
|
220
|
+
## what fails every time is named, as scene_signal judges a connection: too few or too many
|
|
221
|
+
## arguments, and an object where the method takes a value or the reverse.
|
|
222
|
+
static func _key_misfit(target: Node, method: String, arguments: Array, index: int) -> String:
|
|
223
|
+
var taken: Dictionary = {}
|
|
224
|
+
for info: Dictionary in target.get_method_list():
|
|
225
|
+
if str(info.get("name", "")) == method:
|
|
226
|
+
taken = info
|
|
227
|
+
break
|
|
228
|
+
if taken.is_empty() or GdhRead.as_int(taken.get("flags", 0)) & METHOD_FLAG_VARARG:
|
|
229
|
+
return ""
|
|
230
|
+
var parameters: Array = taken.get("args", [])
|
|
231
|
+
var defaults: Array = taken.get("default_args", [])
|
|
232
|
+
var needed: int = parameters.size() - defaults.size()
|
|
233
|
+
var fails: String = ", so the call fails each time the key plays."
|
|
234
|
+
var passed: String = (
|
|
235
|
+
"no arguments"
|
|
236
|
+
if arguments.is_empty()
|
|
237
|
+
else ("1 argument" if arguments.size() == 1 else "%d arguments" % arguments.size())
|
|
238
|
+
)
|
|
239
|
+
if arguments.size() < needed:
|
|
240
|
+
return "keyframes[%d] calls %s with %s, and it needs %d%s" % [index, method, passed, needed, fails]
|
|
241
|
+
if arguments.size() > parameters.size():
|
|
242
|
+
var most: String = "none" if parameters.is_empty() else "at most %d" % parameters.size()
|
|
243
|
+
return "keyframes[%d] calls %s with %s, and it takes %s%s" % [index, method, passed, most, fails]
|
|
244
|
+
for at: int in arguments.size():
|
|
245
|
+
var parameter: Dictionary = parameters[at]
|
|
246
|
+
var taken_type: int = GdhRead.as_int(parameter.get("type", TYPE_NIL))
|
|
247
|
+
var given_type: int = typeof(arguments[at])
|
|
248
|
+
if taken_type == TYPE_NIL or given_type == TYPE_NIL:
|
|
249
|
+
continue
|
|
250
|
+
if (given_type == TYPE_OBJECT) != (taken_type == TYPE_OBJECT):
|
|
251
|
+
return (
|
|
252
|
+
"keyframes[%d] passes argument %d as %s, and %s takes %s as %s%s"
|
|
253
|
+
% [
|
|
254
|
+
index,
|
|
255
|
+
at + 1,
|
|
256
|
+
type_string(given_type),
|
|
257
|
+
method,
|
|
258
|
+
str(parameter.get("name", "")),
|
|
259
|
+
type_string(taken_type),
|
|
260
|
+
fails
|
|
261
|
+
]
|
|
262
|
+
)
|
|
263
|
+
return ""
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
## Takes out the track [code]track[/code] names by its type, nodePath and property as read answers
|
|
267
|
+
## them, so a track read back can be passed as it stands; its keyframes are not looked at. Every
|
|
268
|
+
## track of that shape goes, since the editor can make two for one path and both play. A method
|
|
269
|
+
## track given a method loses only the keys calling it, since add_track puts every method called on
|
|
270
|
+
## one node in one track; the track goes once no key is left.
|
|
271
|
+
func remove_animation_track(args: Dictionary) -> Dictionary:
|
|
272
|
+
var scene_path: String = GdhSceneFile.res_path("", str(args.get("scenePath", "")))
|
|
273
|
+
var player_node_path: String = str(args.get("playerNodePath", "."))
|
|
274
|
+
var animation_name: String = str(args.get("animationName", ""))
|
|
275
|
+
var given_track: Variant = args.get("track", {})
|
|
276
|
+
if scene_path.strip_edges() == "res://":
|
|
277
|
+
return {"ok": false, "error": "Missing scenePath"}
|
|
278
|
+
if animation_name.strip_edges().is_empty():
|
|
279
|
+
return {"ok": false, "error": "Missing animationName"}
|
|
280
|
+
var track: Dictionary = given_track if given_track is Dictionary else {}
|
|
281
|
+
if track.is_empty():
|
|
282
|
+
return {"ok": false, "error": "Missing track"}
|
|
283
|
+
var track_type: String = str(track.get("type", ""))
|
|
284
|
+
if not track_type in GdhAnimationReads.TRACK_TYPE_NAMES.values():
|
|
285
|
+
return {"ok": false, "error": "Unsupported track.type: " + track_type}
|
|
286
|
+
var node_path: String = str(track.get("nodePath", ""))
|
|
287
|
+
var property: String = str(track.get("property", ""))
|
|
288
|
+
var shape: Array = [track_type, node_path, property]
|
|
289
|
+
var method: String = str(track.get("method", "")) if track_type == "method" else ""
|
|
290
|
+
|
|
291
|
+
var found: Array = _animation_at(scene_path, player_node_path, animation_name)
|
|
292
|
+
if found[1] == null:
|
|
293
|
+
return found[2]
|
|
294
|
+
var scene: GdhSceneFile = found[0]
|
|
295
|
+
var player: AnimationPlayer = found[1]
|
|
296
|
+
var anim: Animation = player.get_animation(StringName(animation_name))
|
|
297
|
+
var homes: Array = _homes(
|
|
298
|
+
scene, [[_library_of(player, animation_name), "animation library"], [anim, "animation"]]
|
|
299
|
+
)
|
|
300
|
+
if not homes[0]:
|
|
301
|
+
return scene.refuse(str(homes[1]))
|
|
302
|
+
var matching: Array[int] = []
|
|
303
|
+
var held: PackedStringArray = []
|
|
304
|
+
for index: int in anim.get_track_count():
|
|
305
|
+
var of: Array = _track_shape(anim, index)
|
|
306
|
+
if of == shape:
|
|
307
|
+
matching.append(index)
|
|
308
|
+
var _listed: bool = held.append("%s (%s)" % [str(anim.track_get_path(index)), of[0]])
|
|
309
|
+
if matching.is_empty():
|
|
310
|
+
var said: String = (
|
|
311
|
+
"%s has no %s track on %s"
|
|
312
|
+
% [
|
|
313
|
+
animation_name,
|
|
314
|
+
track_type,
|
|
315
|
+
node_path if property.is_empty() else "%s:%s" % [node_path, property]
|
|
316
|
+
]
|
|
317
|
+
)
|
|
318
|
+
if held.is_empty():
|
|
319
|
+
return scene.refuse("%s; it has no tracks." % said)
|
|
320
|
+
return scene.refuse("%s; its tracks are %s." % [said, ", ".join(held)])
|
|
321
|
+
var calling: Array = _keys_calling(anim, matching, method)
|
|
322
|
+
if calling.is_empty():
|
|
323
|
+
return scene.refuse(
|
|
324
|
+
(
|
|
325
|
+
"%s's method track on %s has no key calling %s; its keys call %s."
|
|
326
|
+
% [animation_name, node_path, method, ", ".join(_methods_called(anim, matching))]
|
|
327
|
+
)
|
|
328
|
+
)
|
|
329
|
+
var keys: int = 0
|
|
330
|
+
var tracks_removed: int = 0
|
|
331
|
+
calling.reverse()
|
|
332
|
+
for pair: Array in calling:
|
|
333
|
+
var index: int = pair[0]
|
|
334
|
+
if pair[1] < 0:
|
|
335
|
+
keys += anim.track_get_key_count(index)
|
|
336
|
+
anim.remove_track(index)
|
|
337
|
+
tracks_removed += 1
|
|
338
|
+
continue
|
|
339
|
+
var key: int = pair[1]
|
|
340
|
+
anim.track_remove_key(index, key)
|
|
341
|
+
keys += 1
|
|
342
|
+
if anim.track_get_key_count(index) == 0:
|
|
343
|
+
anim.remove_track(index)
|
|
344
|
+
tracks_removed += 1
|
|
345
|
+
|
|
346
|
+
var at: String = scene.path_of(player)
|
|
347
|
+
var left: int = anim.get_track_count()
|
|
348
|
+
var keys_left: int = _key_total(anim)
|
|
349
|
+
var check: Callable = func(saved: Node) -> String:
|
|
350
|
+
var copy: AnimationPlayer = saved.get_node_or_null(at) as AnimationPlayer
|
|
351
|
+
if copy == null or not copy.has_animation(StringName(animation_name)):
|
|
352
|
+
return "%s would not be in the scene when it loads." % animation_name
|
|
353
|
+
var loaded: Animation = copy.get_animation(StringName(animation_name))
|
|
354
|
+
if loaded.get_track_count() != left or _key_total(loaded) != keys_left:
|
|
355
|
+
return "the track would still be in %s when the scene loads." % animation_name
|
|
356
|
+
return ""
|
|
357
|
+
var removed: Dictionary = {"type": track_type, "nodePath": node_path}
|
|
358
|
+
if not property.is_empty():
|
|
359
|
+
removed["property"] = property
|
|
360
|
+
if not method.is_empty():
|
|
361
|
+
removed["method"] = method
|
|
362
|
+
var answer: Dictionary = {
|
|
363
|
+
"ok": true,
|
|
364
|
+
"animationName": animation_name,
|
|
365
|
+
"removed": removed,
|
|
366
|
+
"tracksRemoved": tracks_removed,
|
|
367
|
+
"keysRemoved": keys,
|
|
368
|
+
"tracks": left,
|
|
369
|
+
}
|
|
370
|
+
var files: Dictionary = homes[1]
|
|
371
|
+
return _finish(scene, check, files, answer)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
## What of the tracks [param matching] of [param anim] goes, in track order, as [[track, key]]: key
|
|
375
|
+
## -1 for a whole track, or with [param method] given each key that calls it, by its index.
|
|
376
|
+
static func _keys_calling(anim: Animation, matching: Array[int], method: String) -> Array:
|
|
377
|
+
var calling: Array = []
|
|
378
|
+
for index: int in matching:
|
|
379
|
+
if method.is_empty():
|
|
380
|
+
calling.append([index, -1])
|
|
381
|
+
continue
|
|
382
|
+
for key: int in anim.track_get_key_count(index):
|
|
383
|
+
if str(anim.method_track_get_name(index, key)) == method:
|
|
384
|
+
calling.append([index, key])
|
|
385
|
+
return calling
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
## The methods the keys of the tracks [param matching] call, each once.
|
|
389
|
+
static func _methods_called(anim: Animation, matching: Array[int]) -> PackedStringArray:
|
|
390
|
+
var called: PackedStringArray = []
|
|
391
|
+
for index: int in matching:
|
|
392
|
+
for key: int in anim.track_get_key_count(index):
|
|
393
|
+
var method: String = str(anim.method_track_get_name(index, key))
|
|
394
|
+
if not called.has(method):
|
|
395
|
+
var _added: bool = called.append(method)
|
|
396
|
+
return called
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
static func _key_total(anim: Animation) -> int:
|
|
400
|
+
var total: int = 0
|
|
401
|
+
for track: int in anim.get_track_count():
|
|
402
|
+
total += anim.track_get_key_count(track)
|
|
403
|
+
return total
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
## Track [param index] of [param anim] as [type, nodePath, property], named as read names them.
|
|
407
|
+
static func _track_shape(anim: Animation, index: int) -> Array:
|
|
408
|
+
var path: NodePath = anim.track_get_path(index)
|
|
409
|
+
return [
|
|
410
|
+
GdhAnimationReads.TRACK_TYPE_NAMES.get(anim.track_get_type(index), str(anim.track_get_type(index))),
|
|
411
|
+
str(path.get_concatenated_names()),
|
|
412
|
+
str(path.get_concatenated_subnames()),
|
|
413
|
+
]
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
## The keyframes of [param track], as [true, list] or [false, why]. Each needs its own time: one
|
|
417
|
+
## that was not a dictionary was skipped, and one without a time went in at 0, where the next such
|
|
418
|
+
## key replaced it, and the answer said nothing of either.
|
|
419
|
+
func _keyframes(track: Dictionary) -> Array:
|
|
420
|
+
var given: Variant = track.get("keyframes", [])
|
|
421
|
+
if not given is Array:
|
|
422
|
+
return [false, "track.keyframes must be a list."]
|
|
423
|
+
var keyframes: Array = given
|
|
424
|
+
var times: Dictionary = {}
|
|
425
|
+
for index: int in keyframes.size():
|
|
426
|
+
if not keyframes[index] is Dictionary:
|
|
427
|
+
return [false, "keyframes[%d] is not an object with a time." % index]
|
|
428
|
+
var keyframe: Dictionary = keyframes[index]
|
|
429
|
+
var time: Variant = keyframe.get("time")
|
|
430
|
+
if not (time is float or time is int):
|
|
431
|
+
return [false, "keyframes[%d] has no time." % index]
|
|
432
|
+
var seconds: float = GdhRead.as_float(time)
|
|
433
|
+
if times.has(seconds):
|
|
434
|
+
return [
|
|
435
|
+
false, "keyframes[%d] and [%d] are both at %s seconds." % [times[seconds], index, seconds]
|
|
436
|
+
]
|
|
437
|
+
times[seconds] = index
|
|
438
|
+
return [true, keyframes]
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
## Each keyframe's value read as the property [param prop_name] on [param target] takes it, as
|
|
442
|
+
## [true, values] or [false, why]. Read without the property, a string "10" keyed on Label.text
|
|
443
|
+
## was stored as the number 10, and a Vector2 given as [1, 2] as a list.
|
|
444
|
+
func _key_values(target: Node, prop_name: String, keyframes: Array) -> Array:
|
|
445
|
+
var base: String = prop_name.get_slice(":", 0)
|
|
446
|
+
var declared: Dictionary = _properties.declaration(target, base)
|
|
447
|
+
if declared.is_empty():
|
|
448
|
+
return [false, GdhPropertyValues.no_property(target, base)]
|
|
449
|
+
var values: Array = []
|
|
450
|
+
for index: int in keyframes.size():
|
|
451
|
+
var keyframe: Dictionary = keyframes[index]
|
|
452
|
+
var raw: Variant = keyframe.get("value")
|
|
453
|
+
if base != prop_name:
|
|
454
|
+
# A subname such as position:x keys one component, which is a number.
|
|
455
|
+
var component: Variant = _properties.parse(raw, TYPE_FLOAT)
|
|
456
|
+
if not component is float:
|
|
457
|
+
return [false, "keyframes[%d].value keys %s, which is a number." % [index, prop_name]]
|
|
458
|
+
values.append(component)
|
|
459
|
+
continue
|
|
460
|
+
var value: Array = _properties.value_for(target, declared, raw)
|
|
461
|
+
if not value[0]:
|
|
462
|
+
return [false, "keyframes[%d]: %s" % [index, value[1]]]
|
|
463
|
+
values.append(value[1])
|
|
464
|
+
return [true, values]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://bt80qfqvsullr
|
|
@@ -121,6 +121,43 @@ func reload_script(args: Dictionary) -> Dictionary:
|
|
|
121
121
|
return answer
|
|
122
122
|
|
|
123
123
|
|
|
124
|
+
## Which of [code]scriptPaths[/code] the editor holds a compiled copy of whose text is not the file's,
|
|
125
|
+
## without loading or reloading anything. A script compiled against such a copy fails on the
|
|
126
|
+
## members the file has gained, and a reload of it fails the same way until the copy is reloaded
|
|
127
|
+
## first; the rescan that met this blamed the new classes the script also named, which were fine.
|
|
128
|
+
##
|
|
129
|
+
## Only a copy the editor already holds can be behind: one it has not loaded is compiled from the
|
|
130
|
+
## file when something asks for it. One open in a script tab with unsaved edits is answered apart,
|
|
131
|
+
## since reloading it from the file would put the file's text under the edits.
|
|
132
|
+
func scripts_behind(args: Dictionary) -> Dictionary:
|
|
133
|
+
var given: Variant = args.get("scriptPaths", [])
|
|
134
|
+
if not given is Array:
|
|
135
|
+
return {"ok": false, "error": "scriptPaths must be a list of res:// paths"}
|
|
136
|
+
var asked: Array = given
|
|
137
|
+
var unsaved: PackedStringArray = []
|
|
138
|
+
if Engine.is_editor_hint():
|
|
139
|
+
unsaved = EditorInterface.get_script_editor().get_unsaved_files()
|
|
140
|
+
var behind: Array[String] = []
|
|
141
|
+
var edited: Array[String] = []
|
|
142
|
+
for entry: Variant in asked:
|
|
143
|
+
var path: String = _spelled_as_on_disk(str(entry))
|
|
144
|
+
var script: GDScript = ResourceLoader.get_cached_ref(path) as GDScript
|
|
145
|
+
if script == null:
|
|
146
|
+
continue
|
|
147
|
+
var reader: FileAccess = FileAccess.open(path, FileAccess.READ)
|
|
148
|
+
if reader == null:
|
|
149
|
+
continue
|
|
150
|
+
var text: String = reader.get_as_text()
|
|
151
|
+
reader.close()
|
|
152
|
+
if script.source_code == text:
|
|
153
|
+
continue
|
|
154
|
+
if path in unsaved:
|
|
155
|
+
edited.append(path)
|
|
156
|
+
else:
|
|
157
|
+
behind.append(path)
|
|
158
|
+
return {"ok": true, "behind": behind, "unsavedInEditor": edited}
|
|
159
|
+
|
|
160
|
+
|
|
124
161
|
## Has the script editor's tabs reread what changed on disk, as the editor does when its window takes
|
|
125
162
|
## the focus, which a hidden editor's never does. A tab with unsaved edits is left alone and named:
|
|
126
163
|
## saving it writes its text over the file, and rereading would ask a question nobody can answer in a
|