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.
@@ -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
- const LOOP_MODE_NAMES: Dictionary[int, String] = {
12
- Animation.LOOP_NONE: "none",
13
- Animation.LOOP_LINEAR: "linear",
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 = _get_state_machine(anim_tree, state_machine_path)
165
+ var sm: AnimationNodeStateMachine = AnimationReads.state_machine(anim_tree, state_machine_path)
205
166
  if not sm:
206
- return [null, {}, "AnimationNodeStateMachine not found"]
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
- var answer: Dictionary = {"ok": true, "unsavedInEditor": SceneFile.unsaved(scene_path)}
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
- var anim: Animation = player.get_animation(StringName(animation_name))
260
- answer.merge(_summary(anim))
261
- answer["animationName"] = animation_name
262
- var tracks: Array[Dictionary] = []
263
- for track: int in anim.get_track_count():
264
- tracks.append(_track(anim, track))
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
- var sm: AnimationNodeStateMachine = _get_state_machine(anim_tree, state_machine_path)
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
- "%s has no animation %s for the state to play." % [str(anim_tree.anim_player), animation_name]
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
- sm.add_node(StringName(state_name), anim_node)
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, "tree": _entry(nodes, ".", include_properties, depth, 0)}
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
- if not FileAccess.file_exists(res_path):
197
- return {"ok": false, "error": "No resource at " + res_path}
198
- var loaded: Resource = ResourceLoader.load(res_path, "", ResourceLoader.CACHE_MODE_IGNORE)
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." % res_path}
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":