gdharness 1.2.32 → 1.2.34
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 +49 -20
- package/build/godot/addons/gdharness_editor/animation_reads.gd +224 -0
- package/build/godot/addons/gdharness_editor/animation_reads.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/file_reads.gd +76 -5
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +30 -143
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +4 -0
- package/build/godot/operations/animation_reads.gd +224 -0
- package/build/godot/operations/file_reads.gd +76 -5
- package/build/godot/operations/godot_operations.gd +17 -0
- package/build/godot/operations/input_actions.gd +51 -7
- package/build/index.js +48 -19
- package/package.json +1 -1
|
@@ -3,36 +3,18 @@ extends Node
|
|
|
3
3
|
|
|
4
4
|
## Animations and animation trees, edited in the open editor.
|
|
5
5
|
|
|
6
|
+
const AnimationReads = preload("../animation_reads.gd")
|
|
6
7
|
const PropertyValues = preload("../property_values.gd")
|
|
7
8
|
const Read = preload("../reading.gd")
|
|
8
9
|
const SceneFile = preload("../scene_file.gd")
|
|
9
10
|
const Serialisation = preload("../serialisation.gd")
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
Animation.LOOP_PINGPONG: "pingpong",
|
|
15
|
-
}
|
|
16
|
-
const TRACK_TYPE_NAMES: Dictionary[int, String] = {
|
|
17
|
-
Animation.TYPE_VALUE: "property",
|
|
18
|
-
Animation.TYPE_POSITION_3D: "position_3d",
|
|
19
|
-
Animation.TYPE_ROTATION_3D: "rotation_3d",
|
|
20
|
-
Animation.TYPE_SCALE_3D: "scale_3d",
|
|
21
|
-
Animation.TYPE_BLEND_SHAPE: "blend_shape",
|
|
22
|
-
Animation.TYPE_METHOD: "method",
|
|
23
|
-
Animation.TYPE_BEZIER: "bezier",
|
|
24
|
-
Animation.TYPE_AUDIO: "audio",
|
|
25
|
-
Animation.TYPE_ANIMATION: "animation",
|
|
26
|
-
}
|
|
27
|
-
const SWITCH_MODE_NAMES: Dictionary[int, String] = {
|
|
28
|
-
AnimationNodeStateMachineTransition.SWITCH_MODE_IMMEDIATE: "immediate",
|
|
29
|
-
AnimationNodeStateMachineTransition.SWITCH_MODE_SYNC: "sync",
|
|
30
|
-
AnimationNodeStateMachineTransition.SWITCH_MODE_AT_END: "at_end",
|
|
31
|
-
}
|
|
12
|
+
## How far right of the rightmost state a new one goes in the editor's graph, wider than a state's
|
|
13
|
+
## box there.
|
|
14
|
+
const STATE_SPACING: float = 200.0
|
|
32
15
|
|
|
33
16
|
var _editor_plugin: EditorPlugin = null
|
|
34
17
|
var _properties: PropertyValues = PropertyValues.new()
|
|
35
|
-
var _values: Serialisation = Serialisation.new()
|
|
36
18
|
|
|
37
19
|
|
|
38
20
|
func set_editor_plugin(plugin: EditorPlugin) -> void:
|
|
@@ -177,33 +159,12 @@ func _library_of(player: AnimationPlayer, animation_name: String) -> AnimationLi
|
|
|
177
159
|
return player.get_animation_library(StringName(library))
|
|
178
160
|
|
|
179
161
|
|
|
180
|
-
## The state machine at a slash-separated path of nested state machine names, or null when
|
|
181
|
-
## any step of the path is not one. Every node in a tree of state machines is a state machine
|
|
182
|
-
## itself, so the walk asks each step for the child by name and refuses anything else.
|
|
183
|
-
func _get_state_machine(
|
|
184
|
-
anim_tree: AnimationTree, state_machine_path: String = ""
|
|
185
|
-
) -> AnimationNodeStateMachine:
|
|
186
|
-
if not anim_tree:
|
|
187
|
-
return null
|
|
188
|
-
if state_machine_path.is_empty() or state_machine_path == "root":
|
|
189
|
-
return anim_tree.tree_root as AnimationNodeStateMachine
|
|
190
|
-
|
|
191
|
-
var current: AnimationNodeStateMachine = anim_tree.tree_root as AnimationNodeStateMachine
|
|
192
|
-
for segment: String in state_machine_path.split("/", false):
|
|
193
|
-
if segment.is_empty():
|
|
194
|
-
continue
|
|
195
|
-
if current == null or not current.has_node(StringName(segment)):
|
|
196
|
-
return null
|
|
197
|
-
current = current.get_node(StringName(segment)) as AnimationNodeStateMachine
|
|
198
|
-
return current
|
|
199
|
-
|
|
200
|
-
|
|
201
162
|
## The state machine at [param state_machine_path] in [param anim_tree], with the files it is kept
|
|
202
163
|
## in, as [machine, homes, why not].
|
|
203
164
|
func _machine(scene: SceneFile, anim_tree: AnimationTree, state_machine_path: String) -> Array:
|
|
204
|
-
var sm: AnimationNodeStateMachine =
|
|
165
|
+
var sm: AnimationNodeStateMachine = AnimationReads.state_machine(anim_tree, state_machine_path)
|
|
205
166
|
if not sm:
|
|
206
|
-
return [null, {},
|
|
167
|
+
return [null, {}, AnimationReads.no_machine(anim_tree, state_machine_path)]
|
|
207
168
|
var homes: Array = _homes(scene, [[sm, "state machine"]])
|
|
208
169
|
if not homes[0]:
|
|
209
170
|
return [null, {}, homes[1]]
|
|
@@ -235,77 +196,18 @@ func read_animations(args: Dictionary) -> Dictionary:
|
|
|
235
196
|
if opened[1] == null:
|
|
236
197
|
return opened[2]
|
|
237
198
|
var player: AnimationPlayer = opened[1]
|
|
238
|
-
|
|
239
|
-
if animation_name.is_empty():
|
|
240
|
-
var listed: Array[Dictionary] = []
|
|
241
|
-
for called: String in player.get_animation_list():
|
|
242
|
-
var listed_one: Animation = player.get_animation(StringName(called))
|
|
243
|
-
var summary: Dictionary = _summary(listed_one)
|
|
244
|
-
summary["animationName"] = called
|
|
245
|
-
summary["tracks"] = listed_one.get_track_count()
|
|
246
|
-
listed.append(summary)
|
|
247
|
-
answer["animations"] = listed
|
|
248
|
-
scene.close()
|
|
249
|
-
return answer
|
|
250
|
-
if not player.has_animation(StringName(animation_name)):
|
|
251
|
-
var names: PackedStringArray = player.get_animation_list()
|
|
252
|
-
return scene.refuse(
|
|
253
|
-
(
|
|
254
|
-
"%s has no animation %s. It has: %s."
|
|
255
|
-
% [player.name, animation_name, ", ".join(names) if not names.is_empty() else "none"]
|
|
256
|
-
)
|
|
257
|
-
)
|
|
199
|
+
return _read_in(scene, AnimationReads.animations(player, animation_name))
|
|
258
200
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
answer["tracks"] = tracks
|
|
201
|
+
|
|
202
|
+
## [param answer] read from [param scene], with whether the editor holds the scene unsaved.
|
|
203
|
+
func _read_in(scene: SceneFile, answer: Dictionary) -> Dictionary:
|
|
204
|
+
if not answer.get("ok", false):
|
|
205
|
+
return scene.refuse(str(answer.get("error", "")))
|
|
206
|
+
answer["unsavedInEditor"] = SceneFile.unsaved(scene.path)
|
|
266
207
|
scene.close()
|
|
267
208
|
return answer
|
|
268
209
|
|
|
269
210
|
|
|
270
|
-
func _summary(anim: Animation) -> Dictionary:
|
|
271
|
-
return {
|
|
272
|
-
"length": anim.length,
|
|
273
|
-
"loopMode": LOOP_MODE_NAMES.get(anim.loop_mode, str(anim.loop_mode)),
|
|
274
|
-
"step": anim.step,
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
func _track(anim: Animation, track: int) -> Dictionary:
|
|
279
|
-
var kind: int = anim.track_get_type(track)
|
|
280
|
-
var path: NodePath = anim.track_get_path(track)
|
|
281
|
-
var shaped: Dictionary = {
|
|
282
|
-
"type": TRACK_TYPE_NAMES.get(kind, str(kind)), "nodePath": str(path.get_concatenated_names())
|
|
283
|
-
}
|
|
284
|
-
var property: String = str(path.get_concatenated_subnames())
|
|
285
|
-
if not property.is_empty():
|
|
286
|
-
shaped["property"] = property
|
|
287
|
-
var keyframes: Array[Dictionary] = []
|
|
288
|
-
var methods: Dictionary = {}
|
|
289
|
-
for key: int in anim.track_get_key_count(track):
|
|
290
|
-
var keyframe: Dictionary = {"time": anim.track_get_key_time(track, key)}
|
|
291
|
-
if kind == Animation.TYPE_METHOD:
|
|
292
|
-
var method: String = str(anim.method_track_get_name(track, key))
|
|
293
|
-
methods[method] = true
|
|
294
|
-
keyframe["method"] = method
|
|
295
|
-
keyframe["args"] = _values.serialize_value(anim.method_track_get_params(track, key))
|
|
296
|
-
else:
|
|
297
|
-
keyframe["value"] = _values.serialize_value(anim.track_get_key_value(track, key))
|
|
298
|
-
keyframes.append(keyframe)
|
|
299
|
-
# One method across every key is the track's, as add_track takes it. Keys calling different
|
|
300
|
-
# methods, which the editor can make and add_track cannot, keep their own.
|
|
301
|
-
if methods.size() == 1:
|
|
302
|
-
shaped["method"] = methods.keys()[0]
|
|
303
|
-
for keyframe: Dictionary in keyframes:
|
|
304
|
-
var _shared: bool = keyframe.erase("method")
|
|
305
|
-
shaped["keyframes"] = keyframes
|
|
306
|
-
return shaped
|
|
307
|
-
|
|
308
|
-
|
|
309
211
|
## The states and transitions of a state machine in an AnimationTree.
|
|
310
212
|
func read_animation_states(args: Dictionary) -> Dictionary:
|
|
311
213
|
var scene_path: String = _ensure_res_path(str(args.get("scenePath", "")))
|
|
@@ -321,36 +223,7 @@ func read_animation_states(args: Dictionary) -> Dictionary:
|
|
|
321
223
|
if opened[1] == null:
|
|
322
224
|
return opened[2]
|
|
323
225
|
var anim_tree: AnimationTree = opened[1]
|
|
324
|
-
|
|
325
|
-
if sm == null:
|
|
326
|
-
return scene.refuse("AnimationNodeStateMachine not found")
|
|
327
|
-
|
|
328
|
-
var states: Array[Dictionary] = []
|
|
329
|
-
for state: StringName in sm.get_node_list():
|
|
330
|
-
var node: AnimationNode = sm.get_node(state)
|
|
331
|
-
var entry: Dictionary = {"stateName": str(state), "type": node.get_class()}
|
|
332
|
-
if node is AnimationNodeAnimation:
|
|
333
|
-
var plays: AnimationNodeAnimation = node
|
|
334
|
-
entry["animationName"] = str(plays.animation)
|
|
335
|
-
states.append(entry)
|
|
336
|
-
var transitions: Array[Dictionary] = []
|
|
337
|
-
for index: int in sm.get_transition_count():
|
|
338
|
-
var transition: AnimationNodeStateMachineTransition = sm.get_transition(index)
|
|
339
|
-
var shaped: Dictionary = {
|
|
340
|
-
"from": str(sm.get_transition_from(index)),
|
|
341
|
-
"to": str(sm.get_transition_to(index)),
|
|
342
|
-
"transitionType": SWITCH_MODE_NAMES.get(transition.switch_mode, str(transition.switch_mode)),
|
|
343
|
-
}
|
|
344
|
-
if not str(transition.advance_condition).is_empty():
|
|
345
|
-
shaped["advanceCondition"] = str(transition.advance_condition)
|
|
346
|
-
transitions.append(shaped)
|
|
347
|
-
scene.close()
|
|
348
|
-
return {
|
|
349
|
-
"ok": true,
|
|
350
|
-
"states": states,
|
|
351
|
-
"transitions": transitions,
|
|
352
|
-
"unsavedInEditor": SceneFile.unsaved(scene_path),
|
|
353
|
-
}
|
|
226
|
+
return _read_in(scene, AnimationReads.states(anim_tree, state_machine_path))
|
|
354
227
|
|
|
355
228
|
|
|
356
229
|
# =============================================================================
|
|
@@ -611,13 +484,26 @@ func add_animation_state(args: Dictionary) -> Dictionary:
|
|
|
611
484
|
return scene.refuse("The state machine already has a state named %s." % state_name)
|
|
612
485
|
var player: AnimationPlayer = anim_tree.get_node_or_null(anim_tree.anim_player) as AnimationPlayer
|
|
613
486
|
if player != null and not player.has_animation(StringName(animation_name)):
|
|
487
|
+
# By its path from the scene root, as playerNodePath takes it; anim_player is relative to the
|
|
488
|
+
# tree ("../Anim"), which no argument takes.
|
|
614
489
|
return scene.refuse(
|
|
615
|
-
|
|
490
|
+
(
|
|
491
|
+
"The AnimationPlayer at %s has no animation %s for the state to play."
|
|
492
|
+
% [str(scene.root.get_path_to(player)), animation_name]
|
|
493
|
+
)
|
|
616
494
|
)
|
|
617
495
|
|
|
618
496
|
var anim_node: AnimationNodeAnimation = AnimationNodeAnimation.new()
|
|
619
497
|
anim_node.animation = StringName(animation_name)
|
|
620
|
-
|
|
498
|
+
# Beside the rightmost state: add_node puts a state at the origin, where the editor's graph
|
|
499
|
+
# draws it over whatever is already there, Start included.
|
|
500
|
+
var rightmost: Vector2 = Vector2.ZERO
|
|
501
|
+
for state: StringName in sm.get_node_list():
|
|
502
|
+
var held: Vector2 = sm.get_node_position(state)
|
|
503
|
+
if held.x > rightmost.x:
|
|
504
|
+
rightmost = held
|
|
505
|
+
var position: Vector2 = rightmost + Vector2(STATE_SPACING, 0)
|
|
506
|
+
sm.add_node(StringName(state_name), anim_node, position)
|
|
621
507
|
if not sm.has_node(StringName(state_name)):
|
|
622
508
|
return scene.refuse("The state machine did not take the state %s." % state_name)
|
|
623
509
|
|
|
@@ -627,6 +513,7 @@ func add_animation_state(args: Dictionary) -> Dictionary:
|
|
|
627
513
|
"animationName": animation_name,
|
|
628
514
|
# False when the tree has no player to look the animation up in.
|
|
629
515
|
"animationFound": player != null,
|
|
516
|
+
"position": Serialisation.new().serialize_value(position),
|
|
630
517
|
}
|
|
631
518
|
var files: Dictionary = machine[1]
|
|
632
519
|
return _finish(scene, Callable(), files, answer)
|
|
@@ -243,6 +243,10 @@ func read_resource(args: Dictionary) -> Dictionary:
|
|
|
243
243
|
var res_path: String = _ensure_res_path(str(args.get("resourcePath", "")))
|
|
244
244
|
if res_path == "res://":
|
|
245
245
|
return {"ok": false, "error": "resourcePath is required"}
|
|
246
|
+
# The server takes a resource kept inside a file apart, so the boundary judges the file alone.
|
|
247
|
+
var inner: String = str(args.get("subResource", ""))
|
|
248
|
+
if not inner.is_empty():
|
|
249
|
+
res_path += "::" + inner
|
|
246
250
|
return FileReads.resource(res_path, Read.as_bool(args.get("includeDefaults", false)))
|
|
247
251
|
|
|
248
252
|
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
extends RefCounted
|
|
2
|
+
|
|
3
|
+
## Animations and state machines read off their nodes: by the editor's tools from the scene it holds,
|
|
4
|
+
## and by a headless engine with none connected from a bare node of the class given what the file
|
|
5
|
+
## stores, which runs no script of the scene's, so a read answers the same either way.
|
|
6
|
+
|
|
7
|
+
const FileReads = preload("file_reads.gd")
|
|
8
|
+
const Serialisation = preload("serialisation.gd")
|
|
9
|
+
|
|
10
|
+
const LOOP_MODE_NAMES: Dictionary[int, String] = {
|
|
11
|
+
Animation.LOOP_NONE: "none",
|
|
12
|
+
Animation.LOOP_LINEAR: "linear",
|
|
13
|
+
Animation.LOOP_PINGPONG: "pingpong",
|
|
14
|
+
}
|
|
15
|
+
const TRACK_TYPE_NAMES: Dictionary[int, String] = {
|
|
16
|
+
Animation.TYPE_VALUE: "property",
|
|
17
|
+
Animation.TYPE_POSITION_3D: "position_3d",
|
|
18
|
+
Animation.TYPE_ROTATION_3D: "rotation_3d",
|
|
19
|
+
Animation.TYPE_SCALE_3D: "scale_3d",
|
|
20
|
+
Animation.TYPE_BLEND_SHAPE: "blend_shape",
|
|
21
|
+
Animation.TYPE_METHOD: "method",
|
|
22
|
+
Animation.TYPE_BEZIER: "bezier",
|
|
23
|
+
Animation.TYPE_AUDIO: "audio",
|
|
24
|
+
Animation.TYPE_ANIMATION: "animation",
|
|
25
|
+
}
|
|
26
|
+
const SWITCH_MODE_NAMES: Dictionary[int, String] = {
|
|
27
|
+
AnimationNodeStateMachineTransition.SWITCH_MODE_IMMEDIATE: "immediate",
|
|
28
|
+
AnimationNodeStateMachineTransition.SWITCH_MODE_SYNC: "sync",
|
|
29
|
+
AnimationNodeStateMachineTransition.SWITCH_MODE_AT_END: "at_end",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
## [method animations] of the AnimationPlayer at [param player_path] in the file [param scene_path].
|
|
34
|
+
static func scene_animations(scene_path: String, player_path: String, animation_name: String) -> Dictionary:
|
|
35
|
+
var made: Array = _bare(scene_path, player_path, "AnimationPlayer")
|
|
36
|
+
if made[0] == null:
|
|
37
|
+
return made[1]
|
|
38
|
+
var player: AnimationPlayer = made[0]
|
|
39
|
+
var answer: Dictionary = animations(player, animation_name)
|
|
40
|
+
player.free()
|
|
41
|
+
return answer
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
## [method states] of the AnimationTree at [param tree_path] in the file [param scene_path].
|
|
45
|
+
static func scene_states(scene_path: String, tree_path: String, state_machine_path: String) -> Dictionary:
|
|
46
|
+
var made: Array = _bare(scene_path, tree_path, "AnimationTree")
|
|
47
|
+
if made[0] == null:
|
|
48
|
+
return made[1]
|
|
49
|
+
var tree: AnimationTree = made[0]
|
|
50
|
+
var answer: Dictionary = states(tree, state_machine_path)
|
|
51
|
+
tree.free()
|
|
52
|
+
return answer
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
## A node of the class the file gives the node at [param node_path], holding every property the
|
|
56
|
+
## files set but its script, as [node, null] or [null, refusal]. The caller frees it.
|
|
57
|
+
static func _bare(scene_path: String, node_path: String, kind: String) -> Array:
|
|
58
|
+
var read: Dictionary = FileReads.scene_node(scene_path, node_path)
|
|
59
|
+
if not read.is_empty() and not read.get("ok", false):
|
|
60
|
+
return [null, read]
|
|
61
|
+
var type: String = str(read.get("type", ""))
|
|
62
|
+
if not ClassDB.class_exists(type) or not ClassDB.is_parent_class(type, kind):
|
|
63
|
+
return [null, {"ok": false, "error": "%s not found at: %s" % [kind, node_path]}]
|
|
64
|
+
var bare: Node = ClassDB.instantiate(type)
|
|
65
|
+
bare.name = str(read["name"])
|
|
66
|
+
var given: Dictionary = read["properties"]
|
|
67
|
+
for property: Variant in given:
|
|
68
|
+
if str(property) != "script":
|
|
69
|
+
bare.set(str(property), given[property])
|
|
70
|
+
return [bare, null]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
## The animations of [param player] with their lengths, or [param animation_name] whole when it is
|
|
74
|
+
## given. A track comes back in the shape add_track takes, so one read here can be written to
|
|
75
|
+
## another animation as it stands.
|
|
76
|
+
static func animations(player: AnimationPlayer, animation_name: String) -> Dictionary:
|
|
77
|
+
if animation_name.is_empty():
|
|
78
|
+
var listed: Array[Dictionary] = []
|
|
79
|
+
for called: String in player.get_animation_list():
|
|
80
|
+
var listed_one: Animation = player.get_animation(StringName(called))
|
|
81
|
+
var summary: Dictionary = _summary(listed_one)
|
|
82
|
+
summary["animationName"] = called
|
|
83
|
+
summary["tracks"] = listed_one.get_track_count()
|
|
84
|
+
listed.append(summary)
|
|
85
|
+
return {"ok": true, "animations": listed}
|
|
86
|
+
if not player.has_animation(StringName(animation_name)):
|
|
87
|
+
var names: PackedStringArray = player.get_animation_list()
|
|
88
|
+
return {
|
|
89
|
+
"ok": false,
|
|
90
|
+
"error":
|
|
91
|
+
(
|
|
92
|
+
"%s has no animation %s. It has: %s."
|
|
93
|
+
% [player.name, animation_name, ", ".join(names) if not names.is_empty() else "none"]
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
var anim: Animation = player.get_animation(StringName(animation_name))
|
|
97
|
+
var answer: Dictionary = _summary(anim)
|
|
98
|
+
answer["ok"] = true
|
|
99
|
+
answer["animationName"] = animation_name
|
|
100
|
+
var tracks: Array[Dictionary] = []
|
|
101
|
+
var values: Serialisation = Serialisation.new()
|
|
102
|
+
for track: int in anim.get_track_count():
|
|
103
|
+
tracks.append(_track(anim, track, values))
|
|
104
|
+
answer["tracks"] = tracks
|
|
105
|
+
return answer
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
static func _summary(anim: Animation) -> Dictionary:
|
|
109
|
+
return {
|
|
110
|
+
"length": anim.length,
|
|
111
|
+
"loopMode": LOOP_MODE_NAMES.get(anim.loop_mode, str(anim.loop_mode)),
|
|
112
|
+
"step": anim.step,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
static func _track(anim: Animation, track: int, values: Serialisation) -> Dictionary:
|
|
117
|
+
var kind: int = anim.track_get_type(track)
|
|
118
|
+
var path: NodePath = anim.track_get_path(track)
|
|
119
|
+
var shaped: Dictionary = {
|
|
120
|
+
"type": TRACK_TYPE_NAMES.get(kind, str(kind)), "nodePath": str(path.get_concatenated_names())
|
|
121
|
+
}
|
|
122
|
+
var property: String = str(path.get_concatenated_subnames())
|
|
123
|
+
if not property.is_empty():
|
|
124
|
+
shaped["property"] = property
|
|
125
|
+
var keyframes: Array[Dictionary] = []
|
|
126
|
+
var methods: Dictionary = {}
|
|
127
|
+
for key: int in anim.track_get_key_count(track):
|
|
128
|
+
var keyframe: Dictionary = {"time": anim.track_get_key_time(track, key)}
|
|
129
|
+
if kind == Animation.TYPE_METHOD:
|
|
130
|
+
var method: String = str(anim.method_track_get_name(track, key))
|
|
131
|
+
methods[method] = true
|
|
132
|
+
keyframe["method"] = method
|
|
133
|
+
keyframe["args"] = values.serialize_value(anim.method_track_get_params(track, key))
|
|
134
|
+
else:
|
|
135
|
+
keyframe["value"] = values.serialize_value(anim.track_get_key_value(track, key))
|
|
136
|
+
keyframes.append(keyframe)
|
|
137
|
+
# One method across every key is the track's, as add_track takes it. Keys calling different
|
|
138
|
+
# methods, which the editor can make and add_track cannot, keep their own.
|
|
139
|
+
if methods.size() == 1:
|
|
140
|
+
shaped["method"] = methods.keys()[0]
|
|
141
|
+
for keyframe: Dictionary in keyframes:
|
|
142
|
+
var _shared: bool = keyframe.erase("method")
|
|
143
|
+
shaped["keyframes"] = keyframes
|
|
144
|
+
return shaped
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
## The state machine at a slash-separated path of nested state machine names in [param tree], or
|
|
148
|
+
## null when any step of the path is not one. Every node in a tree of state machines is a state
|
|
149
|
+
## machine itself, so the walk asks each step for the child by name and refuses anything else.
|
|
150
|
+
static func state_machine(tree: AnimationTree, state_machine_path: String) -> AnimationNodeStateMachine:
|
|
151
|
+
if tree == null:
|
|
152
|
+
return null
|
|
153
|
+
var current: AnimationNodeStateMachine = tree.tree_root as AnimationNodeStateMachine
|
|
154
|
+
if state_machine_path == "root":
|
|
155
|
+
return current
|
|
156
|
+
for segment: String in state_machine_path.split("/", false):
|
|
157
|
+
if current == null or not current.has_node(StringName(segment)):
|
|
158
|
+
return null
|
|
159
|
+
current = current.get_node(StringName(segment)) as AnimationNodeStateMachine
|
|
160
|
+
return current
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
## Why [method state_machine] found none at [param state_machine_path] in [param tree]: the step of
|
|
164
|
+
## the path that failed, and what is there instead.
|
|
165
|
+
static func no_machine(tree: AnimationTree, state_machine_path: String) -> String:
|
|
166
|
+
if tree == null or tree.tree_root == null:
|
|
167
|
+
return "The AnimationTree has no tree_root."
|
|
168
|
+
var current: AnimationNodeStateMachine = tree.tree_root as AnimationNodeStateMachine
|
|
169
|
+
if current == null:
|
|
170
|
+
return (
|
|
171
|
+
"The AnimationTree's tree_root is not a state machine: its type is %s."
|
|
172
|
+
% tree.tree_root.get_class()
|
|
173
|
+
)
|
|
174
|
+
var walked: PackedStringArray = []
|
|
175
|
+
for segment: String in state_machine_path.split("/", false):
|
|
176
|
+
var where: String = "the root state machine"
|
|
177
|
+
if not walked.is_empty():
|
|
178
|
+
where = "the state machine at " + "/".join(walked)
|
|
179
|
+
if not current.has_node(StringName(segment)):
|
|
180
|
+
var names: PackedStringArray = []
|
|
181
|
+
for state: StringName in current.get_node_list():
|
|
182
|
+
if state != &"Start" and state != &"End":
|
|
183
|
+
var _listed: bool = names.push_back(str(state))
|
|
184
|
+
var held: String = ", ".join(names) if not names.is_empty() else "none"
|
|
185
|
+
return (
|
|
186
|
+
"%s has no state %s. Its states are: %s."
|
|
187
|
+
% [where.to_upper()[0] + where.substr(1), segment, held]
|
|
188
|
+
)
|
|
189
|
+
var next: AnimationNode = current.get_node(StringName(segment))
|
|
190
|
+
if not next is AnimationNodeStateMachine:
|
|
191
|
+
return (
|
|
192
|
+
"The state %s in %s is not a state machine: its type is %s."
|
|
193
|
+
% [segment, where, next.get_class()]
|
|
194
|
+
)
|
|
195
|
+
current = next
|
|
196
|
+
var _walked: bool = walked.push_back(segment)
|
|
197
|
+
return "No state machine at " + state_machine_path
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
## The states and transitions of the state machine at [param state_machine_path] in [param tree].
|
|
201
|
+
static func states(tree: AnimationTree, state_machine_path: String) -> Dictionary:
|
|
202
|
+
var sm: AnimationNodeStateMachine = state_machine(tree, state_machine_path)
|
|
203
|
+
if sm == null:
|
|
204
|
+
return {"ok": false, "error": no_machine(tree, state_machine_path)}
|
|
205
|
+
var listed: Array[Dictionary] = []
|
|
206
|
+
for state: StringName in sm.get_node_list():
|
|
207
|
+
var node: AnimationNode = sm.get_node(state)
|
|
208
|
+
var entry: Dictionary = {"stateName": str(state), "type": node.get_class()}
|
|
209
|
+
if node is AnimationNodeAnimation:
|
|
210
|
+
var plays: AnimationNodeAnimation = node
|
|
211
|
+
entry["animationName"] = str(plays.animation)
|
|
212
|
+
listed.append(entry)
|
|
213
|
+
var transitions: Array[Dictionary] = []
|
|
214
|
+
for index: int in sm.get_transition_count():
|
|
215
|
+
var transition: AnimationNodeStateMachineTransition = sm.get_transition(index)
|
|
216
|
+
var shaped: Dictionary = {
|
|
217
|
+
"from": str(sm.get_transition_from(index)),
|
|
218
|
+
"to": str(sm.get_transition_to(index)),
|
|
219
|
+
"transitionType": SWITCH_MODE_NAMES.get(transition.switch_mode, str(transition.switch_mode)),
|
|
220
|
+
}
|
|
221
|
+
if not str(transition.advance_condition).is_empty():
|
|
222
|
+
shaped["advanceCondition"] = str(transition.advance_condition)
|
|
223
|
+
transitions.append(shaped)
|
|
224
|
+
return {"ok": true, "states": listed, "transitions": transitions}
|
|
@@ -19,6 +19,30 @@ const Serialisation = preload("serialisation.gd")
|
|
|
19
19
|
## instances another scene and every stored property when [param include_properties] is set, the
|
|
20
20
|
## file's value or the default where it sets none. [param depth] -1 reads every level.
|
|
21
21
|
static func scene_tree(scene_path: String, depth: int, include_properties: bool) -> Dictionary:
|
|
22
|
+
var laid: Dictionary = _laid(scene_path)
|
|
23
|
+
if not laid.get("ok", false):
|
|
24
|
+
return laid
|
|
25
|
+
var nodes: Dictionary = laid["nodes"]
|
|
26
|
+
return {"ok": true, "tree": _entry(nodes, ".", include_properties, depth, 0)}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
## The node at [param node_path] in [param scene_path] as {ok, name, type, properties}, laid as
|
|
30
|
+
## [method scene_tree] lays it, with only the properties the files set; {} when the scene has no
|
|
31
|
+
## node there, and a refusal when the file does not read as a scene.
|
|
32
|
+
static func scene_node(scene_path: String, node_path: String) -> Dictionary:
|
|
33
|
+
var laid: Dictionary = _laid(scene_path)
|
|
34
|
+
if not laid.get("ok", false):
|
|
35
|
+
return laid
|
|
36
|
+
var nodes: Dictionary = laid["nodes"]
|
|
37
|
+
var path: String = _joined(".", node_path)
|
|
38
|
+
if not nodes.has(path):
|
|
39
|
+
return {}
|
|
40
|
+
var found: Dictionary = nodes[path]
|
|
41
|
+
return {"ok": true, "name": found["name"], "type": found["type"], "properties": found["properties"]}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
## Every node of [param scene_path] as {ok, nodes}, keyed by path, or a refusal.
|
|
45
|
+
static func _laid(scene_path: String) -> Dictionary:
|
|
22
46
|
if not FileAccess.file_exists(scene_path):
|
|
23
47
|
return {"ok": false, "error": "Scene not found: " + scene_path}
|
|
24
48
|
# Loaded without naming the type, so a resource handed in by mistake loads as what it is and the
|
|
@@ -42,7 +66,7 @@ static func scene_tree(scene_path: String, depth: int, include_properties: bool)
|
|
|
42
66
|
return {"ok": false, "error": refused}
|
|
43
67
|
if not nodes.has("."):
|
|
44
68
|
return {"ok": false, "error": "%s holds no nodes" % scene_path}
|
|
45
|
-
return {"ok": true, "
|
|
69
|
+
return {"ok": true, "nodes": nodes}
|
|
46
70
|
|
|
47
71
|
|
|
48
72
|
## Lays the nodes of [param state] into [param nodes] under [param prefix], keyed by their path
|
|
@@ -192,12 +216,22 @@ static func _stored(type: String, given: Dictionary) -> Dictionary:
|
|
|
192
216
|
## What a resource file holds, in the form modify takes, so a value read here can be written back.
|
|
193
217
|
## Read from the file rather than a cached copy, since the file is what the writes change and what
|
|
194
218
|
## the project loads.
|
|
219
|
+
##
|
|
220
|
+
## [param res_path] may name a resource kept inside a file, as "res://hero.tscn::Animation_jump",
|
|
221
|
+
## the path a property holding one is read back with.
|
|
195
222
|
static func resource(res_path: String, include_defaults: bool) -> Dictionary:
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
223
|
+
var file: String = res_path.get_slice("::", 0)
|
|
224
|
+
if not FileAccess.file_exists(file):
|
|
225
|
+
return {"ok": false, "error": "No resource at " + file}
|
|
226
|
+
var loaded: Resource = ResourceLoader.load(file, "", ResourceLoader.CACHE_MODE_IGNORE)
|
|
199
227
|
if loaded == null:
|
|
200
|
-
return {"ok": false, "error": "%s did not load as a resource." %
|
|
228
|
+
return {"ok": false, "error": "%s did not load as a resource." % file}
|
|
229
|
+
if file != res_path:
|
|
230
|
+
var id: String = res_path.trim_prefix(file + "::")
|
|
231
|
+
var inner: Resource = _kept_in(loaded, file, id, {})
|
|
232
|
+
if inner == null:
|
|
233
|
+
return {"ok": false, "error": "%s keeps no resource with the id %s." % [file, id]}
|
|
234
|
+
loaded = inner
|
|
201
235
|
# A scene's own properties are its packed bundle, which says nothing a caller can use.
|
|
202
236
|
if loaded is PackedScene:
|
|
203
237
|
return {
|
|
@@ -235,3 +269,40 @@ static func resource(res_path: String, include_defaults: bool) -> Dictionary:
|
|
|
235
269
|
if script != null:
|
|
236
270
|
answer["script"] = script.resource_path
|
|
237
271
|
return answer
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
## The resource [param file] keeps under [param id], found in [param value] or anything it holds.
|
|
275
|
+
## The engine loads such a path only while the file is cached, and a read loads the file uncached,
|
|
276
|
+
## so it is looked for among what the file holds. A resource of another file is neither taken nor
|
|
277
|
+
## walked into: ids are unique only within one file, and a scene instancing another holds that
|
|
278
|
+
## file's resources under ids of the other file's.
|
|
279
|
+
static func _kept_in(value: Variant, file: String, id: String, seen: Dictionary) -> Resource:
|
|
280
|
+
if value is Resource:
|
|
281
|
+
var held: Resource = value
|
|
282
|
+
if seen.has(held.get_instance_id()):
|
|
283
|
+
return null
|
|
284
|
+
seen[held.get_instance_id()] = true
|
|
285
|
+
var at: String = held.resource_path
|
|
286
|
+
if not at.is_empty() and at.get_slice("::", 0) != file:
|
|
287
|
+
return null
|
|
288
|
+
if held.resource_scene_unique_id == id:
|
|
289
|
+
return held
|
|
290
|
+
for p: Dictionary in held.get_property_list():
|
|
291
|
+
if Read.as_int(p.get("usage", 0)) & PROPERTY_USAGE_STORAGE:
|
|
292
|
+
var found: Resource = _kept_in(held.get(str(p.get("name", ""))), file, id, seen)
|
|
293
|
+
if found != null:
|
|
294
|
+
return found
|
|
295
|
+
elif value is Array:
|
|
296
|
+
var items: Array = value
|
|
297
|
+
for item: Variant in items:
|
|
298
|
+
var found: Resource = _kept_in(item, file, id, seen)
|
|
299
|
+
if found != null:
|
|
300
|
+
return found
|
|
301
|
+
elif value is Dictionary:
|
|
302
|
+
var entries: Dictionary = value
|
|
303
|
+
for key: Variant in entries:
|
|
304
|
+
for side: Variant in [key, entries[key]]:
|
|
305
|
+
var found: Resource = _kept_in(side, file, id, seen)
|
|
306
|
+
if found != null:
|
|
307
|
+
return found
|
|
308
|
+
return null
|
|
@@ -14,6 +14,7 @@ extends SceneTree
|
|
|
14
14
|
# the operations directory ships inside the server package and is handed to the engine as an
|
|
15
15
|
# absolute path outside the project. A res:// path would only resolve in the test fixture.
|
|
16
16
|
|
|
17
|
+
const AnimationReads = preload("animation_reads.gd")
|
|
17
18
|
const AudioBuses = preload("audio_buses.gd")
|
|
18
19
|
const ClassCache = preload("class_cache.gd")
|
|
19
20
|
const ClassDbQueries = preload("classdb_queries.gd")
|
|
@@ -153,6 +154,22 @@ func _run(operation: String, params: Dictionary) -> Dictionary:
|
|
|
153
154
|
str(params.get("resource_path", "")), Read.as_bool(params.get("include_defaults", false))
|
|
154
155
|
)
|
|
155
156
|
)
|
|
157
|
+
"read_animations":
|
|
158
|
+
payload = _read(
|
|
159
|
+
AnimationReads.scene_animations(
|
|
160
|
+
str(params.get("scene_path", "")),
|
|
161
|
+
str(params.get("player_node_path", ".")),
|
|
162
|
+
str(params.get("animation_name", ""))
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
"read_animation_states":
|
|
166
|
+
payload = _read(
|
|
167
|
+
AnimationReads.scene_states(
|
|
168
|
+
str(params.get("scene_path", "")),
|
|
169
|
+
str(params.get("anim_tree_path", "")),
|
|
170
|
+
str(params.get("state_machine_path", ""))
|
|
171
|
+
)
|
|
172
|
+
)
|
|
156
173
|
|
|
157
174
|
# Import and export pipeline
|
|
158
175
|
"get_import_status":
|