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.
Files changed (78) hide show
  1. package/build/cli.js +4668 -3229
  2. package/build/godot/addons/gdharness_editor/animation_reads.gd +115 -4
  3. package/build/godot/addons/gdharness_editor/declared_values.gd +272 -0
  4. package/build/godot/addons/gdharness_editor/file_reads.gd +115 -18
  5. package/build/godot/addons/gdharness_editor/headless_check.gd +49 -2
  6. package/build/godot/addons/gdharness_editor/property_paths.gd +89 -16
  7. package/build/godot/addons/gdharness_editor/property_values.gd +180 -121
  8. package/build/godot/addons/gdharness_editor/reading.gd +112 -6
  9. package/build/godot/addons/gdharness_editor/scene_file.gd +117 -174
  10. package/build/godot/addons/gdharness_editor/scene_uids.gd +107 -0
  11. package/build/godot/addons/gdharness_editor/scene_uids.gd.uid +1 -0
  12. package/build/godot/addons/gdharness_editor/serialisation.gd +19 -29
  13. package/build/godot/addons/gdharness_editor/setting_reads.gd +121 -0
  14. package/build/godot/addons/gdharness_editor/setting_reads.gd.uid +1 -0
  15. package/build/godot/addons/gdharness_editor/tool_executor.gd +39 -6
  16. package/build/godot/addons/gdharness_editor/tools/animation_states.gd +263 -0
  17. package/build/godot/addons/gdharness_editor/tools/animation_states.gd.uid +1 -0
  18. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +49 -414
  19. package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +464 -0
  20. package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd.uid +1 -0
  21. package/build/godot/addons/gdharness_editor/tools/class_tools.gd +37 -0
  22. package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd +221 -0
  23. package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd.uid +1 -0
  24. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +162 -483
  25. package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +16 -0
  26. package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +8 -4
  27. package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +2 -1
  28. package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +10 -0
  29. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +151 -8
  30. package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +7 -30
  31. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +190 -38
  32. package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd +485 -0
  33. package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd.uid +1 -0
  34. package/build/godot/addons/gdharness_editor/value_shapes.gd +128 -1
  35. package/build/godot/addons/gdharness_runtime/call_errors.gd +8 -2
  36. package/build/godot/addons/gdharness_runtime/declared_values.gd +272 -0
  37. package/build/godot/addons/gdharness_runtime/reading.gd +112 -6
  38. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +35 -3
  39. package/build/godot/addons/gdharness_runtime/runtime_capture.gd +5 -0
  40. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +188 -30
  41. package/build/godot/addons/gdharness_runtime/runtime_input.gd +107 -104
  42. package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +17 -0
  43. package/build/godot/addons/gdharness_runtime/runtime_pad.gd +13 -4
  44. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +320 -27
  45. package/build/godot/addons/gdharness_runtime/runtime_queries.gd +165 -20
  46. package/build/godot/addons/gdharness_runtime/runtime_screen.gd +49 -3
  47. package/build/godot/addons/gdharness_runtime/runtime_targets.gd +31 -2
  48. package/build/godot/addons/gdharness_runtime/runtime_values.gd +250 -10
  49. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +279 -11
  50. package/build/godot/addons/gdharness_runtime/runtime_words.gd +27 -0
  51. package/build/godot/addons/gdharness_runtime/serialisation.gd +19 -29
  52. package/build/godot/addons/gdharness_runtime/value_shapes.gd +128 -1
  53. package/build/godot/operations/animation_reads.gd +115 -4
  54. package/build/godot/operations/audio_buses.gd +93 -7
  55. package/build/godot/operations/classdb_queries.gd +119 -7
  56. package/build/godot/operations/declared_values.gd +272 -0
  57. package/build/godot/operations/dependencies.gd +154 -41
  58. package/build/godot/operations/editor_tools.gd +29 -13
  59. package/build/godot/operations/export_presets.gd +602 -4
  60. package/build/godot/operations/file_reads.gd +115 -18
  61. package/build/godot/operations/gdscript_analysis.gd +14 -0
  62. package/build/godot/operations/gdscript_authoring.gd +12 -2
  63. package/build/godot/operations/godot_operations.gd +18 -2
  64. package/build/godot/operations/import_pipeline.gd +20 -221
  65. package/build/godot/operations/input_actions.gd +127 -51
  66. package/build/godot/operations/project_config.gd +113 -68
  67. package/build/godot/operations/project_diagnostics.gd +4 -0
  68. package/build/godot/operations/project_validation.gd +266 -0
  69. package/build/godot/operations/property_paths.gd +89 -16
  70. package/build/godot/operations/property_values.gd +180 -121
  71. package/build/godot/operations/reading.gd +112 -6
  72. package/build/godot/operations/scene_connections.gd +222 -0
  73. package/build/godot/operations/serialisation.gd +19 -29
  74. package/build/godot/operations/setting_reads.gd +121 -0
  75. package/build/godot/operations/value_shapes.gd +128 -1
  76. package/build/godot/tests/listed.gd +20 -13
  77. package/build/keeper.js +1402 -707
  78. 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]
@@ -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