gdharness 1.1.21 → 1.1.22

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 (47) hide show
  1. package/build/cli.js +1036 -525
  2. package/build/godot/addons/gdharness_editor/property_values.gd +484 -65
  3. package/build/godot/addons/gdharness_editor/reading.gd +17 -0
  4. package/build/godot/addons/gdharness_editor/scene_file.gd +229 -0
  5. package/build/godot/addons/gdharness_editor/scene_file.gd.uid +1 -0
  6. package/build/godot/addons/gdharness_editor/serialisation.gd +116 -7
  7. package/build/godot/addons/gdharness_editor/tool_executor.gd +10 -3
  8. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +374 -242
  9. package/build/godot/addons/gdharness_editor/tools/class_tools.gd +47 -9
  10. package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +206 -0
  11. package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd.uid +1 -0
  12. package/build/godot/addons/gdharness_editor/tools/play_tools.gd +90 -15
  13. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +373 -140
  14. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +566 -472
  15. package/build/godot/addons/gdharness_runtime/call_errors.gd +42 -0
  16. package/build/godot/addons/gdharness_runtime/call_errors.gd.uid +1 -0
  17. package/build/godot/addons/gdharness_runtime/reading.gd +17 -0
  18. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +8 -4
  19. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +440 -0
  20. package/build/godot/addons/gdharness_runtime/runtime_changes.gd.uid +1 -0
  21. package/build/godot/addons/gdharness_runtime/runtime_input.gd +184 -338
  22. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +28 -8
  23. package/build/godot/addons/gdharness_runtime/runtime_queries.gd +7 -299
  24. package/build/godot/addons/gdharness_runtime/runtime_screen.gd +13 -1
  25. package/build/godot/addons/gdharness_runtime/runtime_targets.gd +90 -24
  26. package/build/godot/addons/gdharness_runtime/runtime_typing.gd +351 -0
  27. package/build/godot/addons/gdharness_runtime/runtime_typing.gd.uid +1 -0
  28. package/build/godot/addons/gdharness_runtime/runtime_values.gd +92 -33
  29. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +20 -9
  30. package/build/godot/addons/gdharness_runtime/serialisation.gd +116 -7
  31. package/build/godot/operations/audio_buses.gd +47 -11
  32. package/build/godot/operations/class_cache.gd +18 -2
  33. package/build/godot/operations/dependencies.gd +128 -17
  34. package/build/godot/operations/file_walk.gd +20 -0
  35. package/build/godot/operations/gdscript_analysis.gd +59 -20
  36. package/build/godot/operations/gdscript_authoring.gd +127 -60
  37. package/build/godot/operations/godot_operations.gd +24 -5
  38. package/build/godot/operations/import_pipeline.gd +179 -19
  39. package/build/godot/operations/logger.gd +2 -2
  40. package/build/godot/operations/plugins.gd +88 -70
  41. package/build/godot/operations/project_config.gd +41 -6
  42. package/build/godot/operations/project_diagnostics.gd +25 -10
  43. package/build/godot/operations/reading.gd +17 -0
  44. package/build/godot/operations/resource_files.gd +60 -17
  45. package/build/godot/operations/serialisation.gd +116 -7
  46. package/build/index.js +949 -435
  47. package/package.json +1 -1
@@ -5,6 +5,7 @@ extends Node
5
5
 
6
6
  const PropertyValues = preload("../property_values.gd")
7
7
  const Read = preload("../reading.gd")
8
+ const SceneFile = preload("../scene_file.gd")
8
9
 
9
10
  var _editor_plugin: EditorPlugin = null
10
11
  var _properties: PropertyValues = PropertyValues.new()
@@ -23,63 +24,42 @@ func _ensure_res_path(path: String) -> String:
23
24
  return path
24
25
 
25
26
 
26
- func _refresh_and_reload(scene_path: String) -> void:
27
- _refresh_filesystem()
28
- _reload_scene_in_editor(scene_path)
29
-
30
-
31
- func _refresh_filesystem() -> void:
32
- if _editor_plugin:
33
- EditorInterface.get_resource_filesystem().scan()
34
-
35
-
36
- func _reload_scene_in_editor(scene_path: String) -> void:
37
- if not _editor_plugin:
38
- return
39
- var edited: Node = EditorInterface.get_edited_scene_root()
40
- if edited and edited.scene_file_path == scene_path:
41
- EditorInterface.reload_scene_from_path(scene_path)
42
-
43
-
44
- func _load_scene(scene_path: String) -> Array:
45
- if not FileAccess.file_exists(scene_path):
46
- return [null, {"ok": false, "error": "Scene not found: " + scene_path}]
47
- var packed: PackedScene = load(scene_path)
48
- if not packed:
49
- return [null, {"ok": false, "error": "Failed to load: " + scene_path}]
50
- var root: Node = packed.instantiate()
51
- if not root:
52
- return [null, {"ok": false, "error": "Failed to instantiate: " + scene_path}]
53
- return [root, {}]
54
-
55
-
56
- func _save_scene(scene_root: Node, scene_path: String) -> Dictionary:
57
- var packed: PackedScene = PackedScene.new()
58
- if packed.pack(scene_root) != OK:
59
- scene_root.queue_free()
60
- return {"ok": false, "error": "Failed to pack scene"}
61
- if ResourceSaver.save(packed, scene_path) != OK:
62
- scene_root.queue_free()
63
- return {"ok": false, "error": "Failed to save scene"}
64
- scene_root.queue_free()
65
- _refresh_and_reload(scene_path)
66
- return {}
67
-
68
-
69
27
  func _find_node(root: Node, path: String) -> Node:
70
28
  if path == "." or path.is_empty():
71
29
  return root
72
30
  return root.get_node_or_null(path)
73
31
 
74
32
 
75
- ## A keyframe value or a method argument, read as whatever it says it is.
76
- ##
77
- ## No type to read it against, unlike a property write: what a track animates is named by a path
78
- ## that has still to be resolved against a scene this tool does not hold. So the tags are what
79
- ## decide, which is why they have to be all of them. Three were handled here, and a Quaternion,
80
- ## a Rect2 or a Transform2D keyframe was stored as the dictionary it arrived as.
81
- func _parse_value(value: Variant) -> Variant:
82
- return _properties.parse(value)
33
+ ## [param scene_path] opened for writing, with the node at [param node_path] in it, which must be a
34
+ ## [param kind] the scene owns. Answers [scene, node, refusal]; the refusal is {} when both were
35
+ ## found.
36
+ func _open_at(scene_path: String, node_path: String, kind: String) -> Array:
37
+ var scene: SceneFile = SceneFile.new()
38
+ var refused: Dictionary = scene.open(scene_path, true)
39
+ if not refused.is_empty():
40
+ return [scene, null, refused]
41
+ var node: Node = _find_node(scene.root, node_path)
42
+ if node == null or not node.is_class(kind):
43
+ return [scene, null, scene.refuse("%s not found at: %s" % [kind, node_path])]
44
+ var foreign: String = scene.foreign(node)
45
+ if not foreign.is_empty():
46
+ return [scene, null, scene.refuse(foreign)]
47
+ return [scene, node, {}]
48
+
49
+
50
+ ## Writes [param scene] once [param check] passes, then the files of [param homes], answering
51
+ ## [param answer] or a refusal.
52
+ func _finish(scene: SceneFile, check: Callable, homes: Dictionary, answer: Dictionary) -> Dictionary:
53
+ var written: Dictionary = scene.write(check)
54
+ if not written.is_empty():
55
+ return written
56
+ for file: Variant in homes:
57
+ var resource: Resource = homes[file]
58
+ var saving: Error = ResourceSaver.save(resource, str(file))
59
+ if saving != OK:
60
+ return {"ok": false, "error": "Failed to save %s: %s" % [file, error_string(saving)]}
61
+ EditorInterface.get_resource_filesystem().update_file(str(file))
62
+ return answer
83
63
 
84
64
 
85
65
  func _parse_json_maybe(value: Variant) -> Variant:
@@ -96,19 +76,68 @@ func _parse_method_args(raw_args: Array) -> Array:
96
76
  var parsed_args: Array = []
97
77
  for raw_arg: Variant in raw_args:
98
78
  var parsed: Variant = _parse_json_maybe(raw_arg)
99
- parsed_args.append(_parse_value(parsed))
79
+ parsed_args.append(_properties.parse(parsed))
100
80
  return parsed_args
101
81
 
102
82
 
103
- func _get_default_animation_library(player: AnimationPlayer) -> AnimationLibrary:
104
- var anim_lib: AnimationLibrary = player.get_animation_library("")
105
- if anim_lib:
106
- return anim_lib
107
- anim_lib = AnimationLibrary.new()
108
- var add_lib_err: Error = player.add_animation_library("", anim_lib)
109
- if add_lib_err != OK:
110
- return null
111
- return anim_lib
83
+ ## The files an edit to each of [param held] has to be saved in beside the scene, as
84
+ ## [true, {file: resource}] or [false, why]. Each entry is [resource, what it is called].
85
+ ##
86
+ ## A resource that is part of the scene file is saved with it. One kept in a .tres of its own was
87
+ ## edited in memory while only the scene was saved, which refers to the file, so the edit was
88
+ ## answered as made and was gone; it is saved too. One inside some other file (an imported model,
89
+ ## the scene this one inherits from) is refused, since nothing written here would keep the edit.
90
+ func _homes(scene: SceneFile, held: Array) -> Array:
91
+ var homes: Dictionary = {}
92
+ for entry: Array in held:
93
+ var resource: Resource = entry[0]
94
+ var at: String = resource.resource_path
95
+ # Built into the scene, or into a file saved beside it here: an animation inside a library
96
+ # kept in its own .tres goes with the library.
97
+ if at.is_empty() or at.get_slice("::", 0) == scene.path or homes.has(at.get_slice("::", 0)):
98
+ continue
99
+ if (
100
+ not at.contains("::")
101
+ and FileAccess.file_exists(at)
102
+ and not FileAccess.file_exists(at + ".import")
103
+ ):
104
+ homes[at] = resource
105
+ continue
106
+ return [
107
+ false,
108
+ (
109
+ (
110
+ "The %s is kept in %s, not in %s, and an edit here would not be saved there. Edit it "
111
+ + "where it is kept instead."
112
+ )
113
+ % [entry[1], at.get_slice("::", 0), scene.path]
114
+ )
115
+ ]
116
+ return [true, homes]
117
+
118
+
119
+ ## The library a new animation called [param animation_name] goes in, as [library, why not]: the
120
+ ## default one, made when the player has none, or the one named before a slash, as the player
121
+ ## names them ("moves/walk").
122
+ func _library_for(player: AnimationPlayer, animation_name: String) -> Array:
123
+ var library: String = ""
124
+ if animation_name.contains("/"):
125
+ library = animation_name.get_slice("/", 0)
126
+ if player.has_animation_library(StringName(library)):
127
+ return [player.get_animation_library(StringName(library)), ""]
128
+ if not library.is_empty():
129
+ return [null, "%s has no animation library named %s." % [player.name, library]]
130
+ var made: AnimationLibrary = AnimationLibrary.new()
131
+ var adding: Error = player.add_animation_library("", made)
132
+ if adding != OK:
133
+ return [null, "Failed to create the default AnimationLibrary: " + error_string(adding)]
134
+ return [made, ""]
135
+
136
+
137
+ ## The library [param animation_name] is in on [param player].
138
+ func _library_of(player: AnimationPlayer, animation_name: String) -> AnimationLibrary:
139
+ var library: String = animation_name.get_slice("/", 0) if animation_name.contains("/") else ""
140
+ return player.get_animation_library(StringName(library))
112
141
 
113
142
 
114
143
  ## The state machine at a slash-separated path of nested state machine names, or null when
@@ -132,6 +161,26 @@ func _get_state_machine(
132
161
  return current
133
162
 
134
163
 
164
+ ## The state machine at [param state_machine_path] in [param anim_tree], with the files it is kept
165
+ ## in, as [machine, homes, why not].
166
+ func _machine(scene: SceneFile, anim_tree: AnimationTree, state_machine_path: String) -> Array:
167
+ var sm: AnimationNodeStateMachine = _get_state_machine(anim_tree, state_machine_path)
168
+ if not sm:
169
+ return [null, {}, "AnimationNodeStateMachine not found"]
170
+ var homes: Array = _homes(scene, [[sm, "state machine"]])
171
+ if not homes[0]:
172
+ return [null, {}, homes[1]]
173
+ return [sm, homes[1], ""]
174
+
175
+
176
+ ## The node [param track_path] animates, resolved as the player resolves it: from its root node.
177
+ func _animated(player: AnimationPlayer, track_path: String) -> Node:
178
+ var from: Node = player.get_node_or_null(player.root_node)
179
+ if from == null:
180
+ return null
181
+ return from.get_node_or_null(NodePath(track_path))
182
+
183
+
135
184
  # =============================================================================
136
185
  # create_animation
137
186
  # =============================================================================
@@ -145,56 +194,47 @@ func create_animation(args: Dictionary) -> Dictionary:
145
194
  return {"ok": false, "error": "Missing scenePath"}
146
195
  if animation_name.strip_edges().is_empty():
147
196
  return {"ok": false, "error": "Missing animationName"}
148
-
149
- var loaded: Array = _load_scene(scene_path)
150
- var refused: Dictionary = loaded[1]
151
- if not refused.is_empty():
152
- return refused
153
-
154
- var scene_root: Node = loaded[0]
155
- var player: AnimationPlayer = _find_node(scene_root, player_node_path) as AnimationPlayer
156
- if not player:
157
- scene_root.queue_free()
158
- return {"ok": false, "error": "AnimationPlayer not found at: " + player_node_path}
159
-
160
- var anim_lib: AnimationLibrary = _get_default_animation_library(player)
161
- if not anim_lib:
162
- scene_root.queue_free()
163
- return {"ok": false, "error": "Failed to create default AnimationLibrary"}
164
- if anim_lib.has_animation(StringName(animation_name)):
165
- scene_root.queue_free()
166
- return {"ok": false, "error": "Animation already exists: " + animation_name}
167
-
168
- var loop_mode: Animation.LoopMode = Animation.LOOP_NONE
169
- match loop_mode_name:
170
- "linear":
171
- loop_mode = Animation.LOOP_LINEAR
172
- "pingpong":
173
- loop_mode = Animation.LOOP_PINGPONG
174
- _:
175
- loop_mode_name = "none"
176
- loop_mode = Animation.LOOP_NONE
197
+ var loop_modes: Dictionary = {
198
+ "none": Animation.LOOP_NONE, "linear": Animation.LOOP_LINEAR, "pingpong": Animation.LOOP_PINGPONG
199
+ }
200
+ if not loop_modes.has(loop_mode_name):
201
+ return {"ok": false, "error": "Unsupported loopMode: " + loop_mode_name}
202
+
203
+ var opened: Array = _open_at(scene_path, player_node_path, "AnimationPlayer")
204
+ var scene: SceneFile = opened[0]
205
+ if opened[1] == null:
206
+ return opened[2]
207
+ var player: AnimationPlayer = opened[1]
208
+ if player.has_animation(StringName(animation_name)):
209
+ return scene.refuse("Animation already exists: " + animation_name)
210
+ var library: Array = _library_for(player, animation_name)
211
+ if library[0] == null:
212
+ return scene.refuse(str(library[1]))
213
+ var anim_lib: AnimationLibrary = library[0]
214
+ var homes: Array = _homes(scene, [[anim_lib, "animation library"]])
215
+ if not homes[0]:
216
+ return scene.refuse(str(homes[1]))
177
217
 
178
218
  var anim: Animation = Animation.new()
179
219
  anim.length = Read.as_float(args.get("length", 1.0), 1.0)
180
- anim.loop_mode = loop_mode
220
+ anim.loop_mode = Read.as_int(loop_modes[loop_mode_name]) as Animation.LoopMode
181
221
  anim.step = Read.as_float(args.get("step", 0.1), 0.1)
182
-
183
- var add_err: Error = anim_lib.add_animation(StringName(animation_name), anim)
222
+ var called: String = animation_name.get_slice("/", 1) if animation_name.contains("/") else animation_name
223
+ var add_err: Error = anim_lib.add_animation(StringName(called), anim)
184
224
  if add_err != OK:
185
- scene_root.queue_free()
186
- return {"ok": false, "error": "Failed to add animation: " + error_string(add_err)}
187
-
188
- var save_err: Dictionary = _save_scene(scene_root, scene_path)
189
- if not save_err.is_empty():
190
- return save_err
191
-
192
- return {
193
- "ok": true,
194
- "animationName": animation_name,
195
- "length": anim.length,
196
- "loopMode": loop_mode_name,
225
+ return scene.refuse("Failed to add animation: " + error_string(add_err))
226
+
227
+ var at: String = scene.path_of(player)
228
+ var check: Callable = func(saved: Node) -> String:
229
+ var copy: AnimationPlayer = saved.get_node_or_null(at) as AnimationPlayer
230
+ if copy == null or not copy.has_animation(StringName(animation_name)):
231
+ return "%s would not be in the scene when it loads." % animation_name
232
+ return ""
233
+ var answer: Dictionary = {
234
+ "ok": true, "animationName": animation_name, "length": anim.length, "loopMode": loop_mode_name
197
235
  }
236
+ var files: Dictionary = homes[1]
237
+ return _finish(scene, check, files, answer)
198
238
 
199
239
 
200
240
  # =============================================================================
@@ -204,93 +244,166 @@ func add_animation_track(args: Dictionary) -> Dictionary:
204
244
  var scene_path: String = _ensure_res_path(str(args.get("scenePath", "")))
205
245
  var player_node_path: String = str(args.get("playerNodePath", "."))
206
246
  var animation_name: String = str(args.get("animationName", ""))
207
- var track: Dictionary = args.get("track", {})
247
+ var given_track: Variant = args.get("track", {})
208
248
 
209
249
  if scene_path.strip_edges() == "res://":
210
250
  return {"ok": false, "error": "Missing scenePath"}
211
251
  if animation_name.strip_edges().is_empty():
212
252
  return {"ok": false, "error": "Missing animationName"}
253
+ var track: Dictionary = given_track if given_track is Dictionary else {}
213
254
  if track.is_empty():
214
255
  return {"ok": false, "error": "Missing track"}
256
+ var keyed: Array = _keyframes(track)
257
+ if not keyed[0]:
258
+ return {"ok": false, "error": keyed[1]}
259
+
260
+ var opened: Array = _open_at(scene_path, player_node_path, "AnimationPlayer")
261
+ var scene: SceneFile = opened[0]
262
+ if opened[1] == null:
263
+ return opened[2]
264
+ var player: AnimationPlayer = opened[1]
265
+ # Asked of the player, which knows every library: looking only in the default one refused
266
+ # "moves/walk" as missing.
267
+ if not player.has_animation(StringName(animation_name)):
268
+ return scene.refuse("%s has no animation %s" % [player_node_path, animation_name])
269
+ var anim: Animation = player.get_animation(StringName(animation_name))
270
+ var homes: Array = _homes(
271
+ scene, [[_library_of(player, animation_name), "animation library"], [anim, "animation"]]
272
+ )
273
+ if not homes[0]:
274
+ return scene.refuse(str(homes[1]))
275
+
276
+ var target_path: String = str(track.get("nodePath", ""))
277
+ if target_path.is_empty():
278
+ target_path = "."
279
+ var target: Node = _animated(player, target_path)
280
+ if target == null:
281
+ return scene.refuse(
282
+ (
283
+ (
284
+ "No node at %s from the player's root node, %s, which is what the track path is read "
285
+ + "against."
286
+ )
287
+ % [target_path, str(player.root_node)]
288
+ )
289
+ )
290
+ var keyframes: Array = keyed[1]
291
+ var built: Array = _add_track(anim, track, target, target_path, keyframes)
292
+ if not str(built[1]).is_empty():
293
+ return scene.refuse(str(built[1]))
294
+ var track_idx: int = built[0]
295
+
296
+ var at: String = scene.path_of(player)
297
+ var keys: int = anim.track_get_key_count(track_idx)
298
+ var check: Callable = func(saved: Node) -> String:
299
+ var copy: AnimationPlayer = saved.get_node_or_null(at) as AnimationPlayer
300
+ if copy == null or not copy.has_animation(StringName(animation_name)):
301
+ return "%s would not be in the scene when it loads." % animation_name
302
+ var loaded: Animation = copy.get_animation(StringName(animation_name))
303
+ if loaded.get_track_count() <= track_idx or loaded.track_get_key_count(track_idx) != keys:
304
+ return "the track would not be in %s when the scene loads." % animation_name
305
+ return ""
306
+ var answer: Dictionary = {
307
+ "ok": true, "trackType": str(track.get("type", "")), "trackIndex": track_idx, "keys": keys
308
+ }
309
+ if built[2] != null:
310
+ answer["methodFound"] = built[2]
311
+ var files: Dictionary = homes[1]
312
+ return _finish(scene, check, files, answer)
215
313
 
216
- var loaded: Array = _load_scene(scene_path)
217
- var refused: Dictionary = loaded[1]
218
- if not refused.is_empty():
219
- return refused
220
-
221
- var scene_root: Node = loaded[0]
222
- var player: AnimationPlayer = _find_node(scene_root, player_node_path) as AnimationPlayer
223
- if not player:
224
- scene_root.queue_free()
225
- return {"ok": false, "error": "AnimationPlayer not found at: " + player_node_path}
226
-
227
- var anim_lib: AnimationLibrary = player.get_animation_library("")
228
- if not anim_lib:
229
- scene_root.queue_free()
230
- return {"ok": false, "error": "Default AnimationLibrary not found"}
231
-
232
- var anim: Animation = anim_lib.get_animation(StringName(animation_name))
233
- if not anim:
234
- scene_root.queue_free()
235
- return {"ok": false, "error": "Animation not found: " + animation_name}
236
314
 
315
+ ## Adds [param track] to [param anim] with [param keyframes], as [index, why not, methodFound]:
316
+ ## methodFound is null for a property track.
317
+ func _add_track(
318
+ anim: Animation, track: Dictionary, target: Node, target_path: String, keyframes: Array
319
+ ) -> Array:
237
320
  var track_type: String = str(track.get("type", ""))
321
+ var keys: Array = []
322
+ var method_found: Variant = null
238
323
  var track_idx: int = -1
239
- var keyframes: Array = track.get("keyframes", [])
240
-
241
- match track_type:
242
- "property":
243
- var node_path_str: String = str(track.get("nodePath", ""))
244
- var prop_name: String = str(track.get("property", ""))
245
- if prop_name.is_empty():
246
- scene_root.queue_free()
247
- return {"ok": false, "error": "track.property is required for property track"}
248
- track_idx = anim.add_track(Animation.TYPE_VALUE)
249
- anim.track_set_path(track_idx, NodePath(node_path_str + ":" + prop_name))
250
- for entry: Variant in keyframes:
251
- if typeof(entry) != TYPE_DICTIONARY:
252
- continue
253
- var keyframe: Dictionary = entry
254
- var raw_value: Variant = keyframe.get("value")
255
- var parsed_value: Variant = (
256
- _parse_json_maybe(raw_value) if typeof(raw_value) == TYPE_STRING else raw_value
257
- )
258
- var placed: int = anim.track_insert_key(
259
- track_idx, Read.as_float(keyframe.get("time", 0.0)), _parse_value(parsed_value)
260
- )
261
- if placed < 0:
262
- push_error("gdharness: could not key " + prop_name + " on " + node_path_str)
263
-
264
- "method":
265
- var method_node_path: String = str(track.get("nodePath", ""))
266
- var method_name: String = str(track.get("method", ""))
267
- if method_name.is_empty():
268
- scene_root.queue_free()
269
- return {"ok": false, "error": "track.method is required for method track"}
270
- track_idx = anim.add_track(Animation.TYPE_METHOD)
271
- anim.track_set_path(track_idx, NodePath(method_node_path))
272
- for entry: Variant in keyframes:
273
- if typeof(entry) != TYPE_DICTIONARY:
274
- continue
275
- var keyframe: Dictionary = entry
276
- var given: Variant = keyframe.get("args", [])
277
- var arguments: Array = given if given is Array else []
278
- var invocation: Dictionary = {"method": method_name, "args": _parse_method_args(arguments)}
279
- var placed: int = anim.track_insert_key(
280
- track_idx, Read.as_float(keyframe.get("time", 0.0)), invocation
281
- )
282
- if placed < 0:
283
- push_error("gdharness: could not key " + method_name + " on " + method_node_path)
284
-
285
- _:
286
- scene_root.queue_free()
287
- return {"ok": false, "error": "Unsupported track.type: " + track_type}
288
-
289
- var save_err: Dictionary = _save_scene(scene_root, scene_path)
290
- if not save_err.is_empty():
291
- return save_err
292
-
293
- return {"ok": true, "trackType": track_type, "trackIndex": track_idx}
324
+ if track_type == "property":
325
+ var prop_name: String = str(track.get("property", ""))
326
+ if prop_name.is_empty():
327
+ return [-1, "track.property is required for property track", null]
328
+ var values: Array = _key_values(target, prop_name, keyframes)
329
+ if not values[0]:
330
+ return [-1, values[1], null]
331
+ keys = values[1]
332
+ track_idx = anim.add_track(Animation.TYPE_VALUE)
333
+ anim.track_set_path(track_idx, NodePath(target_path + ":" + prop_name))
334
+ elif track_type == "method":
335
+ var method_name: String = str(track.get("method", ""))
336
+ if method_name.is_empty():
337
+ return [-1, "track.method is required for method track", null]
338
+ for index: int in keyframes.size():
339
+ var keyframe: Dictionary = keyframes[index]
340
+ var given: Variant = keyframe.get("args", [])
341
+ if not given is Array:
342
+ return [-1, "keyframes[%d].args must be a list." % index, null]
343
+ var arguments: Array = given
344
+ keys.append({"method": method_name, "args": _parse_method_args(arguments)})
345
+ method_found = target.has_method(method_name)
346
+ track_idx = anim.add_track(Animation.TYPE_METHOD)
347
+ anim.track_set_path(track_idx, NodePath(target_path))
348
+ else:
349
+ return [-1, "Unsupported track.type: " + track_type, null]
350
+
351
+ for index: int in keyframes.size():
352
+ var keyframe: Dictionary = keyframes[index]
353
+ if anim.track_insert_key(track_idx, Read.as_float(keyframe["time"]), keys[index]) < 0:
354
+ return [-1, "Keyframe %d could not be inserted." % index, null]
355
+ return [track_idx, "", method_found]
356
+
357
+
358
+ ## The keyframes of [param track], as [true, list] or [false, why]. Each needs its own time: one
359
+ ## that was not a dictionary was skipped, and one without a time went in at 0, where the next such
360
+ ## key replaced it, and the answer said nothing of either.
361
+ func _keyframes(track: Dictionary) -> Array:
362
+ var given: Variant = track.get("keyframes", [])
363
+ if not given is Array:
364
+ return [false, "track.keyframes must be a list."]
365
+ var keyframes: Array = given
366
+ var times: Dictionary = {}
367
+ for index: int in keyframes.size():
368
+ if not keyframes[index] is Dictionary:
369
+ return [false, "keyframes[%d] is not an object with a time." % index]
370
+ var keyframe: Dictionary = keyframes[index]
371
+ var time: Variant = keyframe.get("time")
372
+ if not (time is float or time is int):
373
+ return [false, "keyframes[%d] has no time." % index]
374
+ var seconds: float = Read.as_float(time)
375
+ if times.has(seconds):
376
+ return [
377
+ false, "keyframes[%d] and [%d] are both at %s seconds." % [times[seconds], index, seconds]
378
+ ]
379
+ times[seconds] = index
380
+ return [true, keyframes]
381
+
382
+
383
+ ## Each keyframe's value read as the property [param prop_name] on [param target] takes it, as
384
+ ## [true, values] or [false, why]. Read without the property, a string "10" keyed on Label.text
385
+ ## was stored as the number 10, and a Vector2 given as [1, 2] as a list.
386
+ func _key_values(target: Node, prop_name: String, keyframes: Array) -> Array:
387
+ var base: String = prop_name.get_slice(":", 0)
388
+ var declared: Dictionary = _properties.declaration(target, base)
389
+ if declared.is_empty():
390
+ return [false, "%s has no property %s" % [target.get_class(), base]]
391
+ var values: Array = []
392
+ for index: int in keyframes.size():
393
+ var keyframe: Dictionary = keyframes[index]
394
+ var raw: Variant = keyframe.get("value")
395
+ if base != prop_name:
396
+ # A subname such as position:x keys one component, which is a number.
397
+ var component: Variant = _properties.parse(raw, TYPE_FLOAT)
398
+ if not component is float:
399
+ return [false, "keyframes[%d].value keys %s, which is a number." % [index, prop_name]]
400
+ values.append(component)
401
+ continue
402
+ var value: Array = _properties.value_for(target, declared, raw)
403
+ if not value[0]:
404
+ return [false, "keyframes[%d]: %s" % [index, value[1]]]
405
+ values.append(value[1])
406
+ return [true, values]
294
407
 
295
408
 
296
409
  # =============================================================================
@@ -305,38 +418,46 @@ func add_animation_state(args: Dictionary) -> Dictionary:
305
418
 
306
419
  if scene_path.strip_edges() == "res://":
307
420
  return {"ok": false, "error": "Missing scenePath"}
308
- if anim_tree_path.is_empty():
309
- return {"ok": false, "error": "Missing animTreePath"}
310
- if state_name.is_empty():
311
- return {"ok": false, "error": "Missing stateName"}
312
- if animation_name.is_empty():
313
- return {"ok": false, "error": "Missing animationName"}
314
-
315
- var loaded: Array = _load_scene(scene_path)
316
- var refused: Dictionary = loaded[1]
317
- if not refused.is_empty():
318
- return refused
319
-
320
- var scene_root: Node = loaded[0]
321
- var anim_tree: AnimationTree = _find_node(scene_root, anim_tree_path) as AnimationTree
322
- if not anim_tree:
323
- scene_root.queue_free()
324
- return {"ok": false, "error": "AnimationTree not found at: " + anim_tree_path}
325
-
326
- var sm: AnimationNodeStateMachine = _get_state_machine(anim_tree, state_machine_path)
327
- if not sm:
328
- scene_root.queue_free()
329
- return {"ok": false, "error": "AnimationNodeStateMachine not found"}
421
+ for required: Array in [
422
+ [anim_tree_path, "animTreePath"], [state_name, "stateName"], [animation_name, "animationName"]
423
+ ]:
424
+ if str(required[0]).is_empty():
425
+ return {"ok": false, "error": "Missing " + str(required[1])}
426
+
427
+ var opened: Array = _open_at(scene_path, anim_tree_path, "AnimationTree")
428
+ var scene: SceneFile = opened[0]
429
+ if opened[1] == null:
430
+ return opened[2]
431
+ var anim_tree: AnimationTree = opened[1]
432
+ var machine: Array = _machine(scene, anim_tree, state_machine_path)
433
+ if machine[0] == null:
434
+ return scene.refuse(str(machine[2]))
435
+ var sm: AnimationNodeStateMachine = machine[0]
436
+ # add_node fails with nothing but a line in the editor's log on a name in use, and Start and End
437
+ # are in use in every state machine.
438
+ if sm.has_node(StringName(state_name)):
439
+ return scene.refuse("The state machine already has a state named %s." % state_name)
440
+ var player: AnimationPlayer = anim_tree.get_node_or_null(anim_tree.anim_player) as AnimationPlayer
441
+ if player != null and not player.has_animation(StringName(animation_name)):
442
+ return scene.refuse(
443
+ "%s has no animation %s for the state to play." % [str(anim_tree.anim_player), animation_name]
444
+ )
330
445
 
331
446
  var anim_node: AnimationNodeAnimation = AnimationNodeAnimation.new()
332
447
  anim_node.animation = StringName(animation_name)
333
448
  sm.add_node(StringName(state_name), anim_node)
449
+ if not sm.has_node(StringName(state_name)):
450
+ return scene.refuse("The state machine did not take the state %s." % state_name)
334
451
 
335
- var save_err: Dictionary = _save_scene(scene_root, scene_path)
336
- if not save_err.is_empty():
337
- return save_err
338
-
339
- return {"ok": true, "stateName": state_name, "animationName": animation_name}
452
+ var answer: Dictionary = {
453
+ "ok": true,
454
+ "stateName": state_name,
455
+ "animationName": animation_name,
456
+ # False when the tree has no player to look the animation up in.
457
+ "animationFound": player != null,
458
+ }
459
+ var files: Dictionary = machine[1]
460
+ return _finish(scene, Callable(), files, answer)
340
461
 
341
462
 
342
463
  # =============================================================================
@@ -357,42 +478,53 @@ func connect_animation_states(args: Dictionary) -> Dictionary:
357
478
  return {"ok": false, "error": "Missing animTreePath"}
358
479
  if from_state.is_empty() or to_state.is_empty():
359
480
  return {"ok": false, "error": "Missing fromState or toState"}
360
-
361
- var loaded: Array = _load_scene(scene_path)
362
- var refused: Dictionary = loaded[1]
363
- if not refused.is_empty():
364
- return refused
365
-
366
- var scene_root: Node = loaded[0]
367
- var anim_tree: AnimationTree = _find_node(scene_root, anim_tree_path) as AnimationTree
368
- if not anim_tree:
369
- scene_root.queue_free()
370
- return {"ok": false, "error": "AnimationTree not found at: " + anim_tree_path}
371
-
372
- var sm: AnimationNodeStateMachine = _get_state_machine(anim_tree, state_machine_path)
373
- if not sm:
374
- scene_root.queue_free()
375
- return {"ok": false, "error": "AnimationNodeStateMachine not found"}
376
-
481
+ var modes: Dictionary = {
482
+ "sync": AnimationNodeStateMachineTransition.SWITCH_MODE_SYNC,
483
+ "at_end": AnimationNodeStateMachineTransition.SWITCH_MODE_AT_END,
484
+ "immediate": AnimationNodeStateMachineTransition.SWITCH_MODE_IMMEDIATE,
485
+ }
486
+ if not modes.has(transition_type):
487
+ return {"ok": false, "error": "Unsupported transitionType: " + transition_type}
377
488
  var transition: AnimationNodeStateMachineTransition = AnimationNodeStateMachineTransition.new()
378
- match transition_type:
379
- "sync":
380
- transition.switch_mode = AnimationNodeStateMachineTransition.SWITCH_MODE_SYNC
381
- "at_end":
382
- transition.switch_mode = AnimationNodeStateMachineTransition.SWITCH_MODE_AT_END
383
- "immediate":
384
- transition.switch_mode = AnimationNodeStateMachineTransition.SWITCH_MODE_IMMEDIATE
385
- _:
386
- scene_root.queue_free()
387
- return {"ok": false, "error": "Unsupported transitionType: " + transition_type}
388
-
489
+ transition.switch_mode = (
490
+ Read.as_int(modes[transition_type]) as AnimationNodeStateMachineTransition.SwitchMode
491
+ )
389
492
  if not advance_condition.is_empty():
390
493
  transition.advance_condition = StringName(advance_condition)
391
494
 
392
- sm.add_transition(StringName(from_state), StringName(to_state), transition)
393
-
394
- var save_err: Dictionary = _save_scene(scene_root, scene_path)
395
- if not save_err.is_empty():
396
- return save_err
495
+ var opened: Array = _open_at(scene_path, anim_tree_path, "AnimationTree")
496
+ var scene: SceneFile = opened[0]
497
+ if opened[1] == null:
498
+ return opened[2]
499
+ var anim_tree: AnimationTree = opened[1]
500
+ var machine: Array = _machine(scene, anim_tree, state_machine_path)
501
+ if machine[0] == null:
502
+ return scene.refuse(str(machine[2]))
503
+ var sm: AnimationNodeStateMachine = machine[0]
504
+ var problem: String = _transition_problem(sm, from_state, to_state)
505
+ if not problem.is_empty():
506
+ return scene.refuse(problem)
397
507
 
398
- return {"ok": true, "from": from_state, "to": to_state}
508
+ sm.add_transition(StringName(from_state), StringName(to_state), transition)
509
+ if not sm.has_transition(StringName(from_state), StringName(to_state)):
510
+ return scene.refuse("The state machine did not take the transition.")
511
+ var files: Dictionary = machine[1]
512
+ return _finish(scene, Callable(), files, {"ok": true, "from": from_state, "to": to_state})
513
+
514
+
515
+ ## Why [param sm] cannot take a transition from [param from_state] to [param to_state], or "".
516
+ ## Each is a condition add_transition fails on with only a line in the editor's log, and the
517
+ ## transition was answered as made.
518
+ func _transition_problem(sm: AnimationNodeStateMachine, from_state: String, to_state: String) -> String:
519
+ for state: String in [from_state, to_state]:
520
+ if not sm.has_node(StringName(state)):
521
+ return "The state machine has no state named %s." % state
522
+ if from_state == to_state:
523
+ return "A transition needs two different states."
524
+ if from_state == "End":
525
+ return "No transition can leave End."
526
+ if to_state == "Start":
527
+ return "No transition can arrive at Start."
528
+ if sm.has_transition(StringName(from_state), StringName(to_state)):
529
+ return "There is already a transition from %s to %s." % [from_state, to_state]
530
+ return ""