gdharness 1.1.21 → 1.1.23
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 +1470 -726
- package/build/godot/addons/gdharness_editor/bridge_client.gd +12 -2
- package/build/godot/addons/gdharness_editor/property_values.gd +484 -65
- package/build/godot/addons/gdharness_editor/reading.gd +17 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd +229 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/serialisation.gd +116 -7
- package/build/godot/addons/gdharness_editor/tool_executor.gd +10 -3
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +374 -242
- package/build/godot/addons/gdharness_editor/tools/class_tools.gd +47 -9
- package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +206 -0
- package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/play_tools.gd +90 -15
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +373 -140
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +566 -472
- package/build/godot/addons/gdharness_runtime/call_errors.gd +42 -0
- package/build/godot/addons/gdharness_runtime/call_errors.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/reading.gd +17 -0
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +15 -5
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +51 -5
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +440 -0
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +184 -338
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +89 -16
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +107 -343
- package/build/godot/addons/gdharness_runtime/runtime_screen.gd +13 -1
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +90 -24
- package/build/godot/addons/gdharness_runtime/runtime_typing.gd +351 -0
- package/build/godot/addons/gdharness_runtime/runtime_typing.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +92 -33
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +61 -28
- package/build/godot/addons/gdharness_runtime/serialisation.gd +116 -7
- package/build/godot/operations/audio_buses.gd +47 -11
- package/build/godot/operations/class_cache.gd +18 -2
- package/build/godot/operations/classdb_queries.gd +28 -17
- package/build/godot/operations/dependencies.gd +128 -17
- package/build/godot/operations/file_walk.gd +20 -0
- package/build/godot/operations/gdscript_analysis.gd +59 -20
- package/build/godot/operations/gdscript_authoring.gd +127 -60
- package/build/godot/operations/godot_operations.gd +24 -5
- package/build/godot/operations/import_pipeline.gd +179 -19
- package/build/godot/operations/logger.gd +2 -2
- package/build/godot/operations/plugins.gd +88 -70
- package/build/godot/operations/project_config.gd +41 -6
- package/build/godot/operations/project_diagnostics.gd +27 -11
- package/build/godot/operations/reading.gd +17 -0
- package/build/godot/operations/resource_files.gd +60 -17
- package/build/godot/operations/serialisation.gd +116 -7
- package/build/index.js +1385 -553
- package/build/keeper.js +4 -0
- 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
|
-
##
|
|
76
|
-
##
|
|
77
|
-
##
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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(
|
|
79
|
+
parsed_args.append(_properties.parse(parsed))
|
|
100
80
|
return parsed_args
|
|
101
81
|
|
|
102
82
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
if not
|
|
152
|
-
return
|
|
153
|
-
|
|
154
|
-
var
|
|
155
|
-
var
|
|
156
|
-
if
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
return
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
var
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
"
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
var
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
return
|
|
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
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
-
|
|
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 ""
|