gdharness 1.3.2 → 1.3.4
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 +163 -85
- package/build/godot/addons/gdharness_editor/animation_reads.gd +14 -11
- package/build/godot/addons/gdharness_editor/declared_values.gd +157 -4
- package/build/godot/addons/gdharness_editor/file_reads.gd +15 -3
- package/build/godot/addons/gdharness_editor/file_walk.gd +134 -0
- package/build/godot/addons/gdharness_editor/file_walk.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/headless_check.gd +44 -23
- package/build/godot/addons/gdharness_editor/property_paths.gd +225 -0
- package/build/godot/addons/gdharness_editor/property_paths.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/property_values.gd +249 -227
- package/build/godot/addons/gdharness_editor/reading.gd +15 -0
- package/build/godot/addons/gdharness_editor/same_values.gd +177 -0
- package/build/godot/addons/gdharness_editor/same_values.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd +100 -15
- package/build/godot/addons/gdharness_editor/serialisation.gd +75 -3
- package/build/godot/addons/gdharness_editor/tool_executor.gd +12 -3
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +402 -55
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +3 -2
- package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +91 -0
- package/build/godot/addons/gdharness_editor/tools/scene_groups.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +88 -5
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +102 -337
- package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +363 -0
- package/build/godot/addons/gdharness_editor/tools/signal_tools.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/reading.gd +15 -0
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +10 -26
- package/build/godot/addons/gdharness_runtime/runtime_pad.gd +3 -2
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +2 -1
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +30 -0
- package/build/godot/addons/gdharness_runtime/serialisation.gd +75 -3
- package/build/godot/operations/animation_reads.gd +14 -11
- package/build/godot/operations/declared_values.gd +157 -4
- package/build/godot/operations/dependencies.gd +14 -39
- package/build/godot/operations/editor_tools.gd +7 -3
- package/build/godot/operations/file_reads.gd +15 -3
- package/build/godot/operations/file_walk.gd +1 -0
- package/build/godot/operations/gdscript_analysis.gd +229 -75
- package/build/godot/operations/gdscript_authoring.gd +142 -34
- package/build/godot/operations/import_pipeline.gd +17 -6
- package/build/godot/operations/logger.gd +7 -0
- package/build/godot/operations/property_paths.gd +225 -0
- package/build/godot/operations/property_values.gd +249 -227
- package/build/godot/operations/reading.gd +15 -0
- package/build/godot/operations/same_values.gd +177 -0
- package/build/godot/operations/serialisation.gd +75 -3
- package/package.json +1 -1
|
@@ -14,6 +14,9 @@ const GdhSerialisation = preload("../serialisation.gd")
|
|
|
14
14
|
## How far right of the rightmost state a new one goes in the editor's graph, wider than a state's
|
|
15
15
|
## box there.
|
|
16
16
|
const STATE_SPACING: float = 200.0
|
|
17
|
+
const LOOP_MODES: Dictionary = {
|
|
18
|
+
"none": Animation.LOOP_NONE, "linear": Animation.LOOP_LINEAR, "pingpong": Animation.LOOP_PINGPONG
|
|
19
|
+
}
|
|
17
20
|
|
|
18
21
|
var _editor_plugin: EditorPlugin = null
|
|
19
22
|
var _properties: GdhPropertyValues = GdhPropertyValues.new()
|
|
@@ -26,18 +29,6 @@ func set_editor_plugin(plugin: EditorPlugin) -> void:
|
|
|
26
29
|
# =============================================================================
|
|
27
30
|
# Shared helpers
|
|
28
31
|
# =============================================================================
|
|
29
|
-
func _ensure_res_path(path: String) -> String:
|
|
30
|
-
if not path.begins_with("res://"):
|
|
31
|
-
return "res://" + path
|
|
32
|
-
return path
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
func _find_node(root: Node, path: String) -> Node:
|
|
36
|
-
if path == "." or path.is_empty():
|
|
37
|
-
return root
|
|
38
|
-
return root.get_node_or_null(path)
|
|
39
|
-
|
|
40
|
-
|
|
41
32
|
## [param scene_path] opened for writing, with the node at [param node_path] in it, which must be a
|
|
42
33
|
## [param kind] the scene owns. Answers [scene, node, refusal]; the refusal is {} when both were
|
|
43
34
|
## found.
|
|
@@ -46,7 +37,7 @@ func _open_at(scene_path: String, node_path: String, kind: String) -> Array:
|
|
|
46
37
|
var refused: Dictionary = scene.open(scene_path, true)
|
|
47
38
|
if not refused.is_empty():
|
|
48
39
|
return [scene, null, refused]
|
|
49
|
-
var node: Node =
|
|
40
|
+
var node: Node = GdhSceneFile.node_at(scene.root, node_path)
|
|
50
41
|
if node == null or not node.is_class(kind):
|
|
51
42
|
return [scene, null, _not_of_kind(scene, node, node_path, kind)]
|
|
52
43
|
var foreign: String = scene.foreign(node)
|
|
@@ -62,7 +53,7 @@ func _read_at(scene_path: String, node_path: String, kind: String) -> Array:
|
|
|
62
53
|
var refused: Dictionary = scene.open(scene_path, false)
|
|
63
54
|
if not refused.is_empty():
|
|
64
55
|
return [scene, null, refused]
|
|
65
|
-
var node: Node =
|
|
56
|
+
var node: Node = GdhSceneFile.node_at(scene.root, node_path)
|
|
66
57
|
if node == null or not node.is_class(kind):
|
|
67
58
|
return [scene, null, _not_of_kind(scene, node, node_path, kind)]
|
|
68
59
|
return [scene, node, {}]
|
|
@@ -230,7 +221,7 @@ func _animated(player: AnimationPlayer, track_path: String) -> Node:
|
|
|
230
221
|
## The animations of an AnimationPlayer, or one of them whole. A track comes back in the shape
|
|
231
222
|
## add_track takes, so one read here can be written to another animation as it stands.
|
|
232
223
|
func read_animations(args: Dictionary) -> Dictionary:
|
|
233
|
-
var scene_path: String =
|
|
224
|
+
var scene_path: String = GdhSceneFile.res_path("", str(args.get("scenePath", "")))
|
|
234
225
|
var player_node_path: String = str(args.get("playerNodePath", "."))
|
|
235
226
|
var animation_name: String = str(args.get("animationName", ""))
|
|
236
227
|
if scene_path.strip_edges() == "res://":
|
|
@@ -255,7 +246,7 @@ func _read_in(scene: GdhSceneFile, answer: Dictionary) -> Dictionary:
|
|
|
255
246
|
|
|
256
247
|
## The states and transitions of a state machine in an AnimationTree.
|
|
257
248
|
func read_animation_states(args: Dictionary) -> Dictionary:
|
|
258
|
-
var scene_path: String =
|
|
249
|
+
var scene_path: String = GdhSceneFile.res_path("", str(args.get("scenePath", "")))
|
|
259
250
|
var anim_tree_path: String = str(args.get("animTreePath", ""))
|
|
260
251
|
var state_machine_path: String = str(args.get("stateMachinePath", ""))
|
|
261
252
|
if scene_path.strip_edges() == "res://":
|
|
@@ -275,7 +266,7 @@ func read_animation_states(args: Dictionary) -> Dictionary:
|
|
|
275
266
|
# create_animation
|
|
276
267
|
# =============================================================================
|
|
277
268
|
func create_animation(args: Dictionary) -> Dictionary:
|
|
278
|
-
var scene_path: String =
|
|
269
|
+
var scene_path: String = GdhSceneFile.res_path("", str(args.get("scenePath", "")))
|
|
279
270
|
var player_node_path: String = str(args.get("playerNodePath", "."))
|
|
280
271
|
var animation_name: String = str(args.get("animationName", ""))
|
|
281
272
|
var loop_mode_name: String = str(args.get("loopMode", "none"))
|
|
@@ -284,11 +275,12 @@ func create_animation(args: Dictionary) -> Dictionary:
|
|
|
284
275
|
return {"ok": false, "error": "Missing scenePath"}
|
|
285
276
|
if animation_name.strip_edges().is_empty():
|
|
286
277
|
return {"ok": false, "error": "Missing animationName"}
|
|
287
|
-
var loop_modes: Dictionary =
|
|
288
|
-
"none": Animation.LOOP_NONE, "linear": Animation.LOOP_LINEAR, "pingpong": Animation.LOOP_PINGPONG
|
|
289
|
-
}
|
|
278
|
+
var loop_modes: Dictionary = LOOP_MODES
|
|
290
279
|
if not loop_modes.has(loop_mode_name):
|
|
291
280
|
return {"ok": false, "error": "Unsupported loopMode: " + loop_mode_name}
|
|
281
|
+
var timing: String = _timing_refusal(args)
|
|
282
|
+
if not timing.is_empty():
|
|
283
|
+
return {"ok": false, "error": timing}
|
|
292
284
|
|
|
293
285
|
var opened: Array = _open_at(scene_path, player_node_path, "AnimationPlayer")
|
|
294
286
|
var scene: GdhSceneFile = opened[0]
|
|
@@ -331,7 +323,7 @@ func create_animation(args: Dictionary) -> Dictionary:
|
|
|
331
323
|
# add_animation_track
|
|
332
324
|
# =============================================================================
|
|
333
325
|
func add_animation_track(args: Dictionary) -> Dictionary:
|
|
334
|
-
var scene_path: String =
|
|
326
|
+
var scene_path: String = GdhSceneFile.res_path("", str(args.get("scenePath", "")))
|
|
335
327
|
var player_node_path: String = str(args.get("playerNodePath", "."))
|
|
336
328
|
var animation_name: String = str(args.get("animationName", ""))
|
|
337
329
|
var given_track: Variant = args.get("track", {})
|
|
@@ -347,15 +339,13 @@ func add_animation_track(args: Dictionary) -> Dictionary:
|
|
|
347
339
|
if not keyed[0]:
|
|
348
340
|
return {"ok": false, "error": keyed[1]}
|
|
349
341
|
|
|
350
|
-
|
|
342
|
+
# Asked of the player, which knows every library: looking only in the default one refused
|
|
343
|
+
# "moves/walk" as missing.
|
|
344
|
+
var opened: Array = _animation_at(scene_path, player_node_path, animation_name)
|
|
351
345
|
var scene: GdhSceneFile = opened[0]
|
|
352
346
|
if opened[1] == null:
|
|
353
347
|
return opened[2]
|
|
354
348
|
var player: AnimationPlayer = opened[1]
|
|
355
|
-
# Asked of the player, which knows every library: looking only in the default one refused
|
|
356
|
-
# "moves/walk" as missing.
|
|
357
|
-
if not player.has_animation(StringName(animation_name)):
|
|
358
|
-
return scene.refuse("%s has no animation %s" % [player_node_path, animation_name])
|
|
359
349
|
var anim: Animation = player.get_animation(StringName(animation_name))
|
|
360
350
|
var homes: Array = _homes(
|
|
361
351
|
scene, [[_library_of(player, animation_name), "animation library"], [anim, "animation"]]
|
|
@@ -394,55 +384,129 @@ func add_animation_track(args: Dictionary) -> Dictionary:
|
|
|
394
384
|
return "the track would not be in %s when the scene loads." % animation_name
|
|
395
385
|
return ""
|
|
396
386
|
var answer: Dictionary = {
|
|
397
|
-
"ok": true,
|
|
387
|
+
"ok": true,
|
|
388
|
+
"trackType": str(track.get("type", "")),
|
|
389
|
+
"trackIndex": track_idx,
|
|
390
|
+
"keys": keys,
|
|
391
|
+
"keysAdded": keyframes.size(),
|
|
398
392
|
}
|
|
393
|
+
if built[3]:
|
|
394
|
+
answer["existingTrack"] = true
|
|
399
395
|
if built[2] != null:
|
|
400
396
|
answer["methodFound"] = built[2]
|
|
397
|
+
var past: String = _past_length(anim, animation_name)
|
|
398
|
+
if not past.is_empty():
|
|
399
|
+
answer["pastLength"] = past
|
|
401
400
|
var files: Dictionary = homes[1]
|
|
402
401
|
return _finish(scene, check, files, answer)
|
|
403
402
|
|
|
404
403
|
|
|
405
|
-
## Adds [param track] to [param anim] with [param keyframes], as [index, why not, methodFound
|
|
406
|
-
## methodFound is null for a property track
|
|
404
|
+
## Adds [param track] to [param anim] with [param keyframes], as [index, why not, methodFound,
|
|
405
|
+
## existing]: methodFound is null for a property track, and existing is whether the keys went into
|
|
406
|
+
## a track the animation already had for that path.
|
|
407
|
+
##
|
|
408
|
+
## Into that track rather than a second one beside it, as the editor keys a property it already
|
|
409
|
+
## animates: two tracks for one path both play, the later overriding the earlier, and a key meant
|
|
410
|
+
## to extend the motion was split off where reading the animation showed two halves.
|
|
407
411
|
func _add_track(
|
|
408
412
|
anim: Animation, track: Dictionary, target: Node, target_path: String, keyframes: Array
|
|
409
413
|
) -> Array:
|
|
410
414
|
var track_type: String = str(track.get("type", ""))
|
|
411
415
|
var keys: Array = []
|
|
412
416
|
var method_found: Variant = null
|
|
413
|
-
var
|
|
417
|
+
var kind: Animation.TrackType = Animation.TYPE_VALUE
|
|
418
|
+
var path: NodePath = NodePath(target_path)
|
|
414
419
|
if track_type == "property":
|
|
415
420
|
var prop_name: String = str(track.get("property", ""))
|
|
416
421
|
if prop_name.is_empty():
|
|
417
|
-
return [-1, "track.property is required for property track", null]
|
|
422
|
+
return [-1, "track.property is required for property track", null, false]
|
|
418
423
|
var values: Array = _key_values(target, prop_name, keyframes)
|
|
419
424
|
if not values[0]:
|
|
420
|
-
return [-1, values[1], null]
|
|
425
|
+
return [-1, values[1], null, false]
|
|
421
426
|
keys = values[1]
|
|
422
|
-
|
|
423
|
-
anim.track_set_path(track_idx, NodePath(target_path + ":" + prop_name))
|
|
427
|
+
path = NodePath(target_path + ":" + prop_name)
|
|
424
428
|
elif track_type == "method":
|
|
425
429
|
var method_name: String = str(track.get("method", ""))
|
|
426
430
|
if method_name.is_empty():
|
|
427
|
-
return [-1, "track.method is required for method track", null]
|
|
431
|
+
return [-1, "track.method is required for method track", null, false]
|
|
428
432
|
for index: int in keyframes.size():
|
|
429
433
|
var keyframe: Dictionary = keyframes[index]
|
|
430
434
|
var given: Variant = keyframe.get("args", [])
|
|
431
435
|
if not given is Array:
|
|
432
|
-
return [-1, "keyframes[%d].args must be a list." % index, null]
|
|
436
|
+
return [-1, "keyframes[%d].args must be a list." % index, null, false]
|
|
433
437
|
var arguments: Array = given
|
|
434
438
|
keys.append({"method": method_name, "args": _parse_method_args(arguments)})
|
|
435
439
|
method_found = target.has_method(method_name)
|
|
436
|
-
|
|
437
|
-
anim.track_set_path(track_idx, NodePath(target_path))
|
|
440
|
+
kind = Animation.TYPE_METHOD
|
|
438
441
|
else:
|
|
439
|
-
return [-1, "Unsupported track.type: " + track_type, null]
|
|
442
|
+
return [-1, "Unsupported track.type: " + track_type, null, false]
|
|
440
443
|
|
|
444
|
+
var track_idx: int = anim.find_track(path, kind)
|
|
445
|
+
var existing: bool = track_idx >= 0
|
|
446
|
+
if existing:
|
|
447
|
+
# Checked before any key goes in, since track_insert_key replaces a key at the same time
|
|
448
|
+
# without a word, and a refusal partway would leave the editor's scene half changed.
|
|
449
|
+
for index: int in keyframes.size():
|
|
450
|
+
var keyframe: Dictionary = keyframes[index]
|
|
451
|
+
var time: float = GdhRead.as_float(keyframe["time"])
|
|
452
|
+
if anim.track_find_key(track_idx, time, Animation.FIND_MODE_APPROX) >= 0:
|
|
453
|
+
return [
|
|
454
|
+
-1,
|
|
455
|
+
(
|
|
456
|
+
(
|
|
457
|
+
"Track %d already animates %s and has a key at %s s, which keyframes[%d] would "
|
|
458
|
+
+ "replace. Nothing was written."
|
|
459
|
+
)
|
|
460
|
+
% [track_idx, str(path), _seconds(time), index]
|
|
461
|
+
),
|
|
462
|
+
null,
|
|
463
|
+
false
|
|
464
|
+
]
|
|
465
|
+
else:
|
|
466
|
+
track_idx = anim.add_track(kind)
|
|
467
|
+
anim.track_set_path(track_idx, path)
|
|
441
468
|
for index: int in keyframes.size():
|
|
442
469
|
var keyframe: Dictionary = keyframes[index]
|
|
443
470
|
if anim.track_insert_key(track_idx, GdhRead.as_float(keyframe["time"]), keys[index]) < 0:
|
|
444
|
-
return [-1, "Keyframe %d could not be inserted." % index, null]
|
|
445
|
-
return [track_idx, "", method_found]
|
|
471
|
+
return [-1, "Keyframe %d could not be inserted." % index, null, false]
|
|
472
|
+
return [track_idx, "", method_found, existing]
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
## The keys of [param anim], called [param animation_name], that lie past its end, where they never
|
|
476
|
+
## play, as a sentence naming each by its track's path and time, or "" when none does. The engine
|
|
477
|
+
## keeps such a key, as the editor does, so a longer length plays it again.
|
|
478
|
+
static func _past_length(anim: Animation, animation_name: String) -> String:
|
|
479
|
+
var past: PackedStringArray = []
|
|
480
|
+
for track: int in anim.get_track_count():
|
|
481
|
+
for key: int in anim.track_get_key_count(track):
|
|
482
|
+
var time: float = anim.track_get_key_time(track, key)
|
|
483
|
+
if time > anim.length + 0.0001:
|
|
484
|
+
var _added: bool = past.append(
|
|
485
|
+
"%s at %s s" % [str(anim.track_get_path(track)), _seconds(time)]
|
|
486
|
+
)
|
|
487
|
+
if past.is_empty():
|
|
488
|
+
return ""
|
|
489
|
+
return (
|
|
490
|
+
(
|
|
491
|
+
"%s is %s s long, and %s past its end, where %s: %s. %s; scene_animation set "
|
|
492
|
+
+ "with a longer length plays %s."
|
|
493
|
+
)
|
|
494
|
+
% [
|
|
495
|
+
animation_name,
|
|
496
|
+
_seconds(anim.length),
|
|
497
|
+
"this key is" if past.size() == 1 else "these keys are",
|
|
498
|
+
"it never plays" if past.size() == 1 else "they never play",
|
|
499
|
+
", ".join(past),
|
|
500
|
+
"It is kept" if past.size() == 1 else "They are kept",
|
|
501
|
+
"it" if past.size() == 1 else "them",
|
|
502
|
+
]
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
## [param seconds] as a caller wrote it: a key at 2 reads 2, not 2.0.
|
|
507
|
+
static func _seconds(seconds: float) -> String:
|
|
508
|
+
var rounded: float = snappedf(seconds, 0.0001)
|
|
509
|
+
return str(int(rounded)) if rounded == floorf(rounded) else str(rounded)
|
|
446
510
|
|
|
447
511
|
|
|
448
512
|
## The keyframes of [param track], as [true, list] or [false, why]. Each needs its own time: one
|
|
@@ -477,7 +541,7 @@ func _key_values(target: Node, prop_name: String, keyframes: Array) -> Array:
|
|
|
477
541
|
var base: String = prop_name.get_slice(":", 0)
|
|
478
542
|
var declared: Dictionary = _properties.declaration(target, base)
|
|
479
543
|
if declared.is_empty():
|
|
480
|
-
return [false,
|
|
544
|
+
return [false, GdhPropertyValues.no_property(target, base)]
|
|
481
545
|
var values: Array = []
|
|
482
546
|
for index: int in keyframes.size():
|
|
483
547
|
var keyframe: Dictionary = keyframes[index]
|
|
@@ -496,11 +560,279 @@ func _key_values(target: Node, prop_name: String, keyframes: Array) -> Array:
|
|
|
496
560
|
return [true, values]
|
|
497
561
|
|
|
498
562
|
|
|
563
|
+
# =============================================================================
|
|
564
|
+
# set_animation, rename_animation, remove_animation
|
|
565
|
+
# =============================================================================
|
|
566
|
+
## Why the length or step in [param args] cannot be taken, or "". The engine raises a length below
|
|
567
|
+
## a thousandth of a second to that without a word.
|
|
568
|
+
static func _timing_refusal(args: Dictionary) -> String:
|
|
569
|
+
if args.has("length"):
|
|
570
|
+
var length: Variant = args["length"]
|
|
571
|
+
if not (length is float or length is int) or GdhRead.as_float(length) <= 0.0:
|
|
572
|
+
return "length is the animation's length in seconds, more than 0, and %s is not." % _given(length)
|
|
573
|
+
if args.has("step"):
|
|
574
|
+
var step: Variant = args["step"]
|
|
575
|
+
if not (step is float or step is int) or GdhRead.as_float(step) < 0.0:
|
|
576
|
+
return "step is the keyframe snap in seconds, 0 or more, and %s is not." % _given(step)
|
|
577
|
+
return ""
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
static func _given(value: Variant) -> String:
|
|
581
|
+
if value is float or value is int:
|
|
582
|
+
return _seconds(GdhRead.as_float(value))
|
|
583
|
+
return JSON.stringify(value)
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
## [param scene_path] opened for writing with the player at [param player_node_path], which must
|
|
587
|
+
## have [param animation_name], as [method _open_at] answers.
|
|
588
|
+
func _animation_at(scene_path: String, player_node_path: String, animation_name: String) -> Array:
|
|
589
|
+
var opened: Array = _open_at(scene_path, player_node_path, "AnimationPlayer")
|
|
590
|
+
if opened[1] == null:
|
|
591
|
+
return opened
|
|
592
|
+
var scene: GdhSceneFile = opened[0]
|
|
593
|
+
var player: AnimationPlayer = opened[1]
|
|
594
|
+
if player.has_animation(StringName(animation_name)):
|
|
595
|
+
return opened
|
|
596
|
+
var said: String = GdhAnimationReads.no_animation(player, player_node_path, animation_name)
|
|
597
|
+
return [scene, null, scene.refuse(said)]
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
## Changes the length, loop mode or step of an animation, naming any key a shorter length leaves
|
|
601
|
+
## past the end.
|
|
602
|
+
func set_animation(args: Dictionary) -> Dictionary:
|
|
603
|
+
var scene_path: String = GdhSceneFile.res_path("", str(args.get("scenePath", "")))
|
|
604
|
+
var player_node_path: String = str(args.get("playerNodePath", "."))
|
|
605
|
+
var animation_name: String = str(args.get("animationName", ""))
|
|
606
|
+
var loop_mode_name: String = str(args.get("loopMode", ""))
|
|
607
|
+
if scene_path.strip_edges() == "res://":
|
|
608
|
+
return {"ok": false, "error": "Missing scenePath"}
|
|
609
|
+
if animation_name.strip_edges().is_empty():
|
|
610
|
+
return {"ok": false, "error": "Missing animationName"}
|
|
611
|
+
if not (args.has("length") or args.has("loopMode") or args.has("step")):
|
|
612
|
+
return {"ok": false, "error": "set changes length, loopMode or step, and was given none of them."}
|
|
613
|
+
if args.has("loopMode") and not LOOP_MODES.has(loop_mode_name):
|
|
614
|
+
return {"ok": false, "error": "Unsupported loopMode: " + loop_mode_name}
|
|
615
|
+
var timing: String = _timing_refusal(args)
|
|
616
|
+
if not timing.is_empty():
|
|
617
|
+
return {"ok": false, "error": timing}
|
|
618
|
+
|
|
619
|
+
var found: Array = _animation_at(scene_path, player_node_path, animation_name)
|
|
620
|
+
if found[1] == null:
|
|
621
|
+
return found[2]
|
|
622
|
+
var scene: GdhSceneFile = found[0]
|
|
623
|
+
var player: AnimationPlayer = found[1]
|
|
624
|
+
var anim: Animation = player.get_animation(StringName(animation_name))
|
|
625
|
+
var homes: Array = _homes(
|
|
626
|
+
scene, [[_library_of(player, animation_name), "animation library"], [anim, "animation"]]
|
|
627
|
+
)
|
|
628
|
+
if not homes[0]:
|
|
629
|
+
return scene.refuse(str(homes[1]))
|
|
630
|
+
if args.has("length"):
|
|
631
|
+
anim.length = GdhRead.as_float(args["length"])
|
|
632
|
+
if args.has("loopMode"):
|
|
633
|
+
anim.loop_mode = GdhRead.as_int(LOOP_MODES[loop_mode_name]) as Animation.LoopMode
|
|
634
|
+
if args.has("step"):
|
|
635
|
+
anim.step = GdhRead.as_float(args["step"])
|
|
636
|
+
|
|
637
|
+
var length: float = anim.length
|
|
638
|
+
var at: String = scene.path_of(player)
|
|
639
|
+
var check: Callable = func(saved: Node) -> String:
|
|
640
|
+
var copy: AnimationPlayer = saved.get_node_or_null(at) as AnimationPlayer
|
|
641
|
+
if copy == null or not copy.has_animation(StringName(animation_name)):
|
|
642
|
+
return "%s would not be in the scene when it loads." % animation_name
|
|
643
|
+
if not is_equal_approx(copy.get_animation(StringName(animation_name)).length, length):
|
|
644
|
+
return "%s would not be %s s long when the scene loads." % [animation_name, _seconds(length)]
|
|
645
|
+
return ""
|
|
646
|
+
var answer: Dictionary = {
|
|
647
|
+
"ok": true,
|
|
648
|
+
"animationName": animation_name,
|
|
649
|
+
"length": anim.length,
|
|
650
|
+
"loopMode": GdhAnimationReads.LOOP_MODE_NAMES.get(anim.loop_mode, str(anim.loop_mode)),
|
|
651
|
+
"step": anim.step,
|
|
652
|
+
}
|
|
653
|
+
var past: String = _past_length(anim, animation_name)
|
|
654
|
+
if not past.is_empty():
|
|
655
|
+
answer["pastLength"] = past
|
|
656
|
+
var files: Dictionary = homes[1]
|
|
657
|
+
return _finish(scene, check, files, answer)
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
## Renames an animation within its library, taking along the player's autoplay and every state of
|
|
661
|
+
## the scene's trees that plays it, since a state naming an animation the player lacks plays nothing
|
|
662
|
+
## and says so only in the editor's log.
|
|
663
|
+
func rename_animation(args: Dictionary) -> Dictionary:
|
|
664
|
+
var scene_path: String = GdhSceneFile.res_path("", str(args.get("scenePath", "")))
|
|
665
|
+
var player_node_path: String = str(args.get("playerNodePath", "."))
|
|
666
|
+
var animation_name: String = str(args.get("animationName", ""))
|
|
667
|
+
var new_name: String = str(args.get("newName", "")).strip_edges()
|
|
668
|
+
if scene_path.strip_edges() == "res://":
|
|
669
|
+
return {"ok": false, "error": "Missing scenePath"}
|
|
670
|
+
if animation_name.strip_edges().is_empty():
|
|
671
|
+
return {"ok": false, "error": "Missing animationName"}
|
|
672
|
+
if new_name.is_empty():
|
|
673
|
+
return {"ok": false, "error": "Missing newName"}
|
|
674
|
+
var library_name: String = animation_name.get_slice("/", 0) if animation_name.contains("/") else ""
|
|
675
|
+
var prefix: String = "" if library_name.is_empty() else library_name + "/"
|
|
676
|
+
if not prefix.is_empty() and new_name.begins_with(prefix):
|
|
677
|
+
new_name = new_name.trim_prefix(prefix)
|
|
678
|
+
if new_name.contains("/"):
|
|
679
|
+
return {
|
|
680
|
+
"ok": false,
|
|
681
|
+
"error":
|
|
682
|
+
(
|
|
683
|
+
(
|
|
684
|
+
"rename keeps an animation in its library (%s), so newName is its name there, with "
|
|
685
|
+
+ "no library before a slash."
|
|
686
|
+
)
|
|
687
|
+
% ("the default one" if library_name.is_empty() else library_name)
|
|
688
|
+
),
|
|
689
|
+
}
|
|
690
|
+
for forbidden: String in [":", ",", "["]:
|
|
691
|
+
if new_name.contains(forbidden):
|
|
692
|
+
return {"ok": false, "error": "An animation's name cannot hold : , or [, and %s does." % new_name}
|
|
693
|
+
var new_full: String = prefix + new_name
|
|
694
|
+
|
|
695
|
+
var found: Array = _animation_at(scene_path, player_node_path, animation_name)
|
|
696
|
+
if found[1] == null:
|
|
697
|
+
return found[2]
|
|
698
|
+
var scene: GdhSceneFile = found[0]
|
|
699
|
+
var player: AnimationPlayer = found[1]
|
|
700
|
+
if new_full == animation_name:
|
|
701
|
+
return scene.refuse("%s is already called %s." % [animation_name, new_full])
|
|
702
|
+
if player.has_animation(StringName(new_full)):
|
|
703
|
+
return scene.refuse("%s already has an animation %s." % [player_node_path, new_full])
|
|
704
|
+
var library: AnimationLibrary = _library_of(player, animation_name)
|
|
705
|
+
var states: Array = _states_playing(scene, player, animation_name)
|
|
706
|
+
var held: Array = [[library, "animation library"]]
|
|
707
|
+
for state: Array in states:
|
|
708
|
+
held.append([state[0], "state playing %s" % animation_name])
|
|
709
|
+
var homes: Array = _homes(scene, held)
|
|
710
|
+
if not homes[0]:
|
|
711
|
+
return scene.refuse(str(homes[1]))
|
|
712
|
+
|
|
713
|
+
# Read before the rename, since the player moves its autoplay to the new name on its own; it
|
|
714
|
+
# does not clear one when an animation is removed.
|
|
715
|
+
var autoplayed: bool = str(player.autoplay) == animation_name
|
|
716
|
+
library.rename_animation(StringName(animation_name.trim_prefix(prefix)), StringName(new_name))
|
|
717
|
+
if not player.has_animation(StringName(new_full)):
|
|
718
|
+
return scene.refuse("The library did not take the name %s." % new_full)
|
|
719
|
+
var answer: Dictionary = {"ok": true, "animationName": new_full, "was": animation_name}
|
|
720
|
+
if autoplayed:
|
|
721
|
+
answer["autoplay"] = new_full
|
|
722
|
+
var followed: Array = []
|
|
723
|
+
for state: Array in states:
|
|
724
|
+
var plays: AnimationNodeAnimation = state[0]
|
|
725
|
+
plays.animation = StringName(new_full)
|
|
726
|
+
followed.append(state[1])
|
|
727
|
+
if not followed.is_empty():
|
|
728
|
+
answer["statesFollowed"] = followed
|
|
729
|
+
|
|
730
|
+
var at: String = scene.path_of(player)
|
|
731
|
+
var check: Callable = func(saved: Node) -> String:
|
|
732
|
+
var copy: AnimationPlayer = saved.get_node_or_null(at) as AnimationPlayer
|
|
733
|
+
if copy == null or not copy.has_animation(StringName(new_full)):
|
|
734
|
+
return "%s would not be in the scene when it loads." % new_full
|
|
735
|
+
return ""
|
|
736
|
+
var files: Dictionary = homes[1]
|
|
737
|
+
return _finish(scene, check, files, answer)
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
## Removes an animation, clearing the player's autoplay when it named it and naming the states of
|
|
741
|
+
## the scene's trees left playing nothing.
|
|
742
|
+
func remove_animation(args: Dictionary) -> Dictionary:
|
|
743
|
+
var scene_path: String = GdhSceneFile.res_path("", str(args.get("scenePath", "")))
|
|
744
|
+
var player_node_path: String = str(args.get("playerNodePath", "."))
|
|
745
|
+
var animation_name: String = str(args.get("animationName", ""))
|
|
746
|
+
if scene_path.strip_edges() == "res://":
|
|
747
|
+
return {"ok": false, "error": "Missing scenePath"}
|
|
748
|
+
if animation_name.strip_edges().is_empty():
|
|
749
|
+
return {"ok": false, "error": "Missing animationName"}
|
|
750
|
+
|
|
751
|
+
var found: Array = _animation_at(scene_path, player_node_path, animation_name)
|
|
752
|
+
if found[1] == null:
|
|
753
|
+
return found[2]
|
|
754
|
+
var scene: GdhSceneFile = found[0]
|
|
755
|
+
var player: AnimationPlayer = found[1]
|
|
756
|
+
var library: AnimationLibrary = _library_of(player, animation_name)
|
|
757
|
+
var homes: Array = _homes(scene, [[library, "animation library"]])
|
|
758
|
+
if not homes[0]:
|
|
759
|
+
return scene.refuse(str(homes[1]))
|
|
760
|
+
var called: String = animation_name.get_slice("/", 1) if animation_name.contains("/") else animation_name
|
|
761
|
+
var autoplayed: bool = str(player.autoplay) == animation_name
|
|
762
|
+
library.remove_animation(StringName(called))
|
|
763
|
+
if player.has_animation(StringName(animation_name)):
|
|
764
|
+
return scene.refuse("The library did not let go of %s." % animation_name)
|
|
765
|
+
var answer: Dictionary = {"ok": true, "removed": animation_name}
|
|
766
|
+
if autoplayed:
|
|
767
|
+
player.autoplay = ""
|
|
768
|
+
answer["autoplayCleared"] = true
|
|
769
|
+
var orphaned: Array = []
|
|
770
|
+
for state: Array in _states_playing(scene, player, animation_name):
|
|
771
|
+
orphaned.append(state[1])
|
|
772
|
+
if not orphaned.is_empty():
|
|
773
|
+
answer["statesPlayingNothing"] = orphaned
|
|
774
|
+
|
|
775
|
+
var at: String = scene.path_of(player)
|
|
776
|
+
var check: Callable = func(saved: Node) -> String:
|
|
777
|
+
var copy: AnimationPlayer = saved.get_node_or_null(at) as AnimationPlayer
|
|
778
|
+
if copy == null or copy.has_animation(StringName(animation_name)):
|
|
779
|
+
return "%s would still be in the scene when it loads." % animation_name
|
|
780
|
+
return ""
|
|
781
|
+
var files: Dictionary = homes[1]
|
|
782
|
+
return _finish(scene, check, files, answer)
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
## The states of [param scene]'s AnimationTrees that play [param animation_name] from [param player],
|
|
786
|
+
## as [[node, {animTreePath, state}]], a state named by the path of the nodes leading to it.
|
|
787
|
+
func _states_playing(scene: GdhSceneFile, player: AnimationPlayer, animation_name: String) -> Array:
|
|
788
|
+
var found: Array = []
|
|
789
|
+
var trees: Array[Node] = scene.root.find_children("*", "AnimationTree", true, false)
|
|
790
|
+
if scene.root is AnimationTree:
|
|
791
|
+
trees.append(scene.root)
|
|
792
|
+
for node: Node in trees:
|
|
793
|
+
var tree: AnimationTree = node
|
|
794
|
+
if tree.tree_root == null or tree.get_node_or_null(tree.anim_player) != player:
|
|
795
|
+
continue
|
|
796
|
+
var tree_path: String = str(scene.root.get_path_to(tree))
|
|
797
|
+
_collect_playing(tree.tree_root, "", tree_path, animation_name, found)
|
|
798
|
+
return found
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
static func _collect_playing(
|
|
802
|
+
node: AnimationNode, at: String, tree_path: String, animation_name: String, found: Array
|
|
803
|
+
) -> void:
|
|
804
|
+
if node is AnimationNodeAnimation:
|
|
805
|
+
var plays: AnimationNodeAnimation = node
|
|
806
|
+
if plays.animation == StringName(animation_name):
|
|
807
|
+
found.append([plays, {"animTreePath": tree_path, "state": "." if at.is_empty() else at}])
|
|
808
|
+
return
|
|
809
|
+
var children: Dictionary = {}
|
|
810
|
+
if node is AnimationNodeStateMachine:
|
|
811
|
+
var machine: AnimationNodeStateMachine = node
|
|
812
|
+
for state: StringName in machine.get_node_list():
|
|
813
|
+
children[str(state)] = machine.get_node(state)
|
|
814
|
+
elif node is AnimationNodeBlendTree:
|
|
815
|
+
var blend: AnimationNodeBlendTree = node
|
|
816
|
+
for input: StringName in blend.get_node_list():
|
|
817
|
+
children[str(input)] = blend.get_node(input)
|
|
818
|
+
elif node is AnimationNodeBlendSpace1D:
|
|
819
|
+
var line: AnimationNodeBlendSpace1D = node
|
|
820
|
+
for point: int in line.get_blend_point_count():
|
|
821
|
+
children[str(point)] = line.get_blend_point_node(point)
|
|
822
|
+
elif node is AnimationNodeBlendSpace2D:
|
|
823
|
+
var plane: AnimationNodeBlendSpace2D = node
|
|
824
|
+
for point: int in plane.get_blend_point_count():
|
|
825
|
+
children[str(point)] = plane.get_blend_point_node(point)
|
|
826
|
+
for step: String in children:
|
|
827
|
+
var child: AnimationNode = children[step]
|
|
828
|
+
_collect_playing(child, step if at.is_empty() else at + "/" + step, tree_path, animation_name, found)
|
|
829
|
+
|
|
830
|
+
|
|
499
831
|
# =============================================================================
|
|
500
832
|
# add_animation_state
|
|
501
833
|
# =============================================================================
|
|
502
834
|
func add_animation_state(args: Dictionary) -> Dictionary:
|
|
503
|
-
var scene_path: String =
|
|
835
|
+
var scene_path: String = GdhSceneFile.res_path("", str(args.get("scenePath", "")))
|
|
504
836
|
var anim_tree_path: String = str(args.get("animTreePath", ""))
|
|
505
837
|
var state_name: String = str(args.get("stateName", ""))
|
|
506
838
|
var animation_name: String = str(args.get("animationName", ""))
|
|
@@ -528,15 +860,7 @@ func add_animation_state(args: Dictionary) -> Dictionary:
|
|
|
528
860
|
if sm.has_node(StringName(state_name)):
|
|
529
861
|
return scene.refuse("The state machine already has a state named %s." % state_name)
|
|
530
862
|
var player: AnimationPlayer = anim_tree.get_node_or_null(anim_tree.anim_player) as AnimationPlayer
|
|
531
|
-
|
|
532
|
-
# By its path from the scene root, as playerNodePath takes it; anim_player is relative to the
|
|
533
|
-
# tree ("../Anim"), which no argument takes.
|
|
534
|
-
return scene.refuse(
|
|
535
|
-
(
|
|
536
|
-
"The AnimationPlayer at %s has no animation %s for the state to play."
|
|
537
|
-
% [str(scene.root.get_path_to(player)), animation_name]
|
|
538
|
-
)
|
|
539
|
-
)
|
|
863
|
+
var found: bool = player != null and player.has_animation(StringName(animation_name))
|
|
540
864
|
|
|
541
865
|
var anim_node: AnimationNodeAnimation = AnimationNodeAnimation.new()
|
|
542
866
|
anim_node.animation = StringName(animation_name)
|
|
@@ -556,19 +880,42 @@ func add_animation_state(args: Dictionary) -> Dictionary:
|
|
|
556
880
|
"ok": true,
|
|
557
881
|
"stateName": state_name,
|
|
558
882
|
"animationName": animation_name,
|
|
559
|
-
# False when the tree has no player to look the animation up in
|
|
560
|
-
|
|
883
|
+
# False when the tree has no player to look the animation up in, or its player has none of
|
|
884
|
+
# that name.
|
|
885
|
+
"animationFound": found,
|
|
561
886
|
"position": GdhSerialisation.new().serialize_value(position),
|
|
562
887
|
}
|
|
888
|
+
# Written all the same, as the editor writes one: a state machine is often laid out before its
|
|
889
|
+
# animations are made, and refusing it stopped a tree being built in that order.
|
|
890
|
+
if player != null and not found:
|
|
891
|
+
answer["animationMissing"] = _missing_animation(scene, player, animation_name)
|
|
563
892
|
var files: Dictionary = machine[1]
|
|
564
893
|
return _finish(scene, Callable(), files, answer)
|
|
565
894
|
|
|
566
895
|
|
|
896
|
+
## Why the state playing [param animation_name] plays nothing yet. The player is named by its path
|
|
897
|
+
## from the scene root, as playerNodePath takes it; anim_player is relative to the tree ("../Anim"),
|
|
898
|
+
## which no argument takes.
|
|
899
|
+
static func _missing_animation(
|
|
900
|
+
scene: GdhSceneFile, player: AnimationPlayer, animation_name: String
|
|
901
|
+
) -> String:
|
|
902
|
+
var said: String = (
|
|
903
|
+
"The AnimationPlayer at %s has no animation %s yet, so the state plays nothing until it has one."
|
|
904
|
+
% [str(scene.root.get_path_to(player)), animation_name]
|
|
905
|
+
)
|
|
906
|
+
var close: PackedStringArray = GdhRead.nearest(animation_name, player.get_animation_list())
|
|
907
|
+
if close.is_empty():
|
|
908
|
+
return said
|
|
909
|
+
return (
|
|
910
|
+
said + " The nearest %s %s." % ["it has is" if close.size() == 1 else "it has are", ", ".join(close)]
|
|
911
|
+
)
|
|
912
|
+
|
|
913
|
+
|
|
567
914
|
# =============================================================================
|
|
568
915
|
# connect_animation_states
|
|
569
916
|
# =============================================================================
|
|
570
917
|
func connect_animation_states(args: Dictionary) -> Dictionary:
|
|
571
|
-
var scene_path: String =
|
|
918
|
+
var scene_path: String = GdhSceneFile.res_path("", str(args.get("scenePath", "")))
|
|
572
919
|
var anim_tree_path: String = str(args.get("animTreePath", ""))
|
|
573
920
|
var from_state: String = str(args.get("fromState", ""))
|
|
574
921
|
var to_state: String = str(args.get("toState", ""))
|
|
@@ -7,6 +7,7 @@ const GdhFileReads = preload("../file_reads.gd")
|
|
|
7
7
|
const GdhHeadlessCheck = preload("../headless_check.gd")
|
|
8
8
|
const GdhPropertyValues = preload("../property_values.gd")
|
|
9
9
|
const GdhRead = preload("../reading.gd")
|
|
10
|
+
const GdhSame = preload("../same_values.gd")
|
|
10
11
|
const GdhSceneFile = preload("../scene_file.gd")
|
|
11
12
|
|
|
12
13
|
# The minimal shader of a type: its declaration and the one function the engine calls for it.
|
|
@@ -136,7 +137,7 @@ func _save_checked(resource: Resource, path: String, names: Array, previous: Var
|
|
|
136
137
|
for key: Variant in names:
|
|
137
138
|
var property: String = str(key)
|
|
138
139
|
var read: Variant = _properties.read_at(loaded, property)
|
|
139
|
-
if not
|
|
140
|
+
if not GdhSame.same(read, _properties.read_at(resource, property)):
|
|
140
141
|
lost = (
|
|
141
142
|
(
|
|
142
143
|
"%s.%s would load from the file as %s: it is not a property the file keeps, so "
|
|
@@ -182,7 +183,7 @@ func _unkept_headless(resource: Resource, path: String, names: Array, previous:
|
|
|
182
183
|
var property: String = str(key)
|
|
183
184
|
asked[property] = true
|
|
184
185
|
asked[property.get_slice(":", 0)] = true
|
|
185
|
-
|
|
186
|
+
GdhHeadlessCheck.spare(reached, GdhHeadlessCheck.reached(resource, property))
|
|
186
187
|
return GdhHeadlessCheck.settle_saved(
|
|
187
188
|
path,
|
|
188
189
|
before,
|