gdharness 1.3.8 → 1.3.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/build/cli.js +4668 -3229
  2. package/build/godot/addons/gdharness_editor/animation_reads.gd +115 -4
  3. package/build/godot/addons/gdharness_editor/declared_values.gd +272 -0
  4. package/build/godot/addons/gdharness_editor/file_reads.gd +115 -18
  5. package/build/godot/addons/gdharness_editor/headless_check.gd +49 -2
  6. package/build/godot/addons/gdharness_editor/property_paths.gd +89 -16
  7. package/build/godot/addons/gdharness_editor/property_values.gd +180 -121
  8. package/build/godot/addons/gdharness_editor/reading.gd +112 -6
  9. package/build/godot/addons/gdharness_editor/scene_file.gd +117 -174
  10. package/build/godot/addons/gdharness_editor/scene_uids.gd +107 -0
  11. package/build/godot/addons/gdharness_editor/scene_uids.gd.uid +1 -0
  12. package/build/godot/addons/gdharness_editor/serialisation.gd +19 -29
  13. package/build/godot/addons/gdharness_editor/setting_reads.gd +121 -0
  14. package/build/godot/addons/gdharness_editor/setting_reads.gd.uid +1 -0
  15. package/build/godot/addons/gdharness_editor/tool_executor.gd +39 -6
  16. package/build/godot/addons/gdharness_editor/tools/animation_states.gd +263 -0
  17. package/build/godot/addons/gdharness_editor/tools/animation_states.gd.uid +1 -0
  18. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +49 -414
  19. package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +464 -0
  20. package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd.uid +1 -0
  21. package/build/godot/addons/gdharness_editor/tools/class_tools.gd +37 -0
  22. package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd +221 -0
  23. package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd.uid +1 -0
  24. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +162 -483
  25. package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +16 -0
  26. package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +8 -4
  27. package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +2 -1
  28. package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +10 -0
  29. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +151 -8
  30. package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +7 -30
  31. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +190 -38
  32. package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd +485 -0
  33. package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd.uid +1 -0
  34. package/build/godot/addons/gdharness_editor/value_shapes.gd +128 -1
  35. package/build/godot/addons/gdharness_runtime/call_errors.gd +8 -2
  36. package/build/godot/addons/gdharness_runtime/declared_values.gd +272 -0
  37. package/build/godot/addons/gdharness_runtime/reading.gd +112 -6
  38. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +35 -3
  39. package/build/godot/addons/gdharness_runtime/runtime_capture.gd +5 -0
  40. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +188 -30
  41. package/build/godot/addons/gdharness_runtime/runtime_input.gd +107 -104
  42. package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +17 -0
  43. package/build/godot/addons/gdharness_runtime/runtime_pad.gd +13 -4
  44. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +320 -27
  45. package/build/godot/addons/gdharness_runtime/runtime_queries.gd +165 -20
  46. package/build/godot/addons/gdharness_runtime/runtime_screen.gd +49 -3
  47. package/build/godot/addons/gdharness_runtime/runtime_targets.gd +31 -2
  48. package/build/godot/addons/gdharness_runtime/runtime_values.gd +250 -10
  49. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +279 -11
  50. package/build/godot/addons/gdharness_runtime/runtime_words.gd +27 -0
  51. package/build/godot/addons/gdharness_runtime/serialisation.gd +19 -29
  52. package/build/godot/addons/gdharness_runtime/value_shapes.gd +128 -1
  53. package/build/godot/operations/animation_reads.gd +115 -4
  54. package/build/godot/operations/audio_buses.gd +93 -7
  55. package/build/godot/operations/classdb_queries.gd +119 -7
  56. package/build/godot/operations/declared_values.gd +272 -0
  57. package/build/godot/operations/dependencies.gd +154 -41
  58. package/build/godot/operations/editor_tools.gd +29 -13
  59. package/build/godot/operations/export_presets.gd +602 -4
  60. package/build/godot/operations/file_reads.gd +115 -18
  61. package/build/godot/operations/gdscript_analysis.gd +14 -0
  62. package/build/godot/operations/gdscript_authoring.gd +12 -2
  63. package/build/godot/operations/godot_operations.gd +18 -2
  64. package/build/godot/operations/import_pipeline.gd +20 -221
  65. package/build/godot/operations/input_actions.gd +127 -51
  66. package/build/godot/operations/project_config.gd +113 -68
  67. package/build/godot/operations/project_diagnostics.gd +4 -0
  68. package/build/godot/operations/project_validation.gd +266 -0
  69. package/build/godot/operations/property_paths.gd +89 -16
  70. package/build/godot/operations/property_values.gd +180 -121
  71. package/build/godot/operations/reading.gd +112 -6
  72. package/build/godot/operations/scene_connections.gd +222 -0
  73. package/build/godot/operations/serialisation.gd +19 -29
  74. package/build/godot/operations/setting_reads.gd +121 -0
  75. package/build/godot/operations/value_shapes.gd +128 -1
  76. package/build/godot/tests/listed.gd +20 -13
  77. package/build/keeper.js +1402 -707
  78. package/package.json +1 -1
@@ -9,11 +9,9 @@ const GdhHeadlessCheck = preload("../headless_check.gd")
9
9
  const GdhPropertyValues = preload("../property_values.gd")
10
10
  const GdhRead = preload("../reading.gd")
11
11
  const GdhSceneFile = preload("../scene_file.gd")
12
+ const GdhSceneUids = preload("../scene_uids.gd")
12
13
  const GdhSerialisation = preload("../serialisation.gd")
13
14
 
14
- ## How far right of the rightmost state a new one goes in the editor's graph, wider than a state's
15
- ## box there.
16
- const STATE_SPACING: float = 200.0
17
15
  const LOOP_MODES: Dictionary = {
18
16
  "none": Animation.LOOP_NONE, "linear": Animation.LOOP_LINEAR, "pingpong": Animation.LOOP_PINGPONG
19
17
  }
@@ -102,7 +100,7 @@ func _finish(scene: GdhSceneFile, check: Callable, homes: Dictionary, answer: Di
102
100
  return written
103
101
  for file: Variant in homes:
104
102
  var resource: Resource = homes[file]
105
- var saving: Error = GdhSceneFile.save_keeping_uid(resource, str(file))
103
+ var saving: Error = GdhSceneUids.save_keeping_uid(resource, str(file))
106
104
  if saving != OK:
107
105
  return {"ok": false, "error": "Failed to save %s: %s" % [file, error_string(saving)]}
108
106
  if Engine.is_editor_hint():
@@ -171,22 +169,24 @@ func _homes(scene: GdhSceneFile, held: Array) -> Array:
171
169
  return [true, homes]
172
170
 
173
171
 
174
- ## The library a new animation called [param animation_name] goes in, as [library, why not]: the
175
- ## default one, made when the player has none, or the one named before a slash, as the player
176
- ## names them ("moves/walk").
172
+ ## The library a new animation called [param animation_name] goes in, as [library, why not, made]:
173
+ ## the default one, or the one named before a slash, as the player names them ("moves/walk"). One
174
+ ## the player lacks is made, built into the scene, and made says so; no other op makes a library,
175
+ ## so refusing one left "moves/walk" with no way to be written.
177
176
  func _library_for(player: AnimationPlayer, animation_name: String) -> Array:
178
177
  var library: String = ""
179
178
  if animation_name.contains("/"):
180
179
  library = animation_name.get_slice("/", 0)
181
180
  if player.has_animation_library(StringName(library)):
182
- return [player.get_animation_library(StringName(library)), ""]
183
- if not library.is_empty():
184
- return [null, "%s has no animation library named %s." % [player.name, library]]
181
+ return [player.get_animation_library(StringName(library)), "", false]
185
182
  var made: AnimationLibrary = AnimationLibrary.new()
186
- var adding: Error = player.add_animation_library("", made)
183
+ var adding: Error = player.add_animation_library(StringName(library), made)
187
184
  if adding != OK:
188
- return [null, "Failed to create the default AnimationLibrary: " + error_string(adding)]
189
- return [made, ""]
185
+ var called: String = (
186
+ "the default AnimationLibrary" if library.is_empty() else "the library " + library
187
+ )
188
+ return [null, "Failed to create %s: %s" % [called, error_string(adding)], false]
189
+ return [made, "", true]
190
190
 
191
191
 
192
192
  ## The library [param animation_name] is in on [param player].
@@ -259,7 +259,9 @@ func read_animation_states(args: Dictionary) -> Dictionary:
259
259
  if opened[1] == null:
260
260
  return opened[2]
261
261
  var anim_tree: AnimationTree = opened[1]
262
- return _read_in(scene, GdhAnimationReads.states(anim_tree, state_machine_path))
262
+ var player: AnimationPlayer = anim_tree.get_node_or_null(anim_tree.anim_player) as AnimationPlayer
263
+ var player_path: String = "" if player == null else scene.path_of(player)
264
+ return _read_in(scene, GdhAnimationReads.states(anim_tree, state_machine_path, player, player_path))
263
265
 
264
266
 
265
267
  # =============================================================================
@@ -275,6 +277,9 @@ func create_animation(args: Dictionary) -> Dictionary:
275
277
  return {"ok": false, "error": "Missing scenePath"}
276
278
  if animation_name.strip_edges().is_empty():
277
279
  return {"ok": false, "error": "Missing animationName"}
280
+ var named: String = _name_refusal(animation_name)
281
+ if not named.is_empty():
282
+ return {"ok": false, "error": named}
278
283
  var loop_modes: Dictionary = LOOP_MODES
279
284
  if not loop_modes.has(loop_mode_name):
280
285
  return {"ok": false, "error": "Unsupported loopMode: " + loop_mode_name}
@@ -302,6 +307,8 @@ func create_animation(args: Dictionary) -> Dictionary:
302
307
  anim.loop_mode = GdhRead.as_int(loop_modes[loop_mode_name]) as Animation.LoopMode
303
308
  anim.step = GdhRead.as_float(args.get("step", 0.1), 0.1)
304
309
  var called: String = animation_name.get_slice("/", 1) if animation_name.contains("/") else animation_name
310
+ # As the editor names a new one: the file keeps it as resource_name, which the inspector shows.
311
+ anim.resource_name = called
305
312
  var add_err: Error = anim_lib.add_animation(StringName(called), anim)
306
313
  if add_err != OK:
307
314
  return scene.refuse("Failed to add animation: " + error_string(add_err))
@@ -315,161 +322,30 @@ func create_animation(args: Dictionary) -> Dictionary:
315
322
  var answer: Dictionary = {
316
323
  "ok": true, "animationName": animation_name, "length": anim.length, "loopMode": loop_mode_name
317
324
  }
325
+ if library[2]:
326
+ answer["libraryMade"] = animation_name.get_slice("/", 0) if animation_name.contains("/") else ""
318
327
  var files: Dictionary = homes[1]
319
328
  return _finish(scene, check, files, answer)
320
329
 
321
330
 
322
- # =============================================================================
323
- # add_animation_track
324
- # =============================================================================
325
- func add_animation_track(args: Dictionary) -> Dictionary:
326
- var scene_path: String = GdhSceneFile.res_path("", str(args.get("scenePath", "")))
327
- var player_node_path: String = str(args.get("playerNodePath", "."))
328
- var animation_name: String = str(args.get("animationName", ""))
329
- var given_track: Variant = args.get("track", {})
330
-
331
- if scene_path.strip_edges() == "res://":
332
- return {"ok": false, "error": "Missing scenePath"}
333
- if animation_name.strip_edges().is_empty():
334
- return {"ok": false, "error": "Missing animationName"}
335
- var track: Dictionary = given_track if given_track is Dictionary else {}
336
- if track.is_empty():
337
- return {"ok": false, "error": "Missing track"}
338
- var keyed: Array = _keyframes(track)
339
- if not keyed[0]:
340
- return {"ok": false, "error": keyed[1]}
341
-
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)
345
- var scene: GdhSceneFile = opened[0]
346
- if opened[1] == null:
347
- return opened[2]
348
- var player: AnimationPlayer = opened[1]
349
- var anim: Animation = player.get_animation(StringName(animation_name))
350
- var homes: Array = _homes(
351
- scene, [[_library_of(player, animation_name), "animation library"], [anim, "animation"]]
352
- )
353
- if not homes[0]:
354
- return scene.refuse(str(homes[1]))
355
-
356
- var target_path: String = str(track.get("nodePath", ""))
357
- if target_path.is_empty():
358
- target_path = "."
359
- var target: Node = _animated(player, target_path)
360
- if target == null:
361
- return scene.refuse(
331
+ ## Why [param animation_name] cannot name an animation, or "". The player refuses : , and [ in an
332
+ ## animation's or a library's name, and reads one slash as the end of the library's.
333
+ static func _name_refusal(animation_name: String) -> String:
334
+ if animation_name.count("/") > 1:
335
+ return (
362
336
  (
363
- (
364
- "No node at %s from the player's root node, %s, which is what the track path is read "
365
- + "against."
366
- )
367
- % [target_path, str(player.root_node)]
337
+ "%s names a library and an animation in it with one slash, as moves/walk; a library holds no "
338
+ + "libraries."
368
339
  )
340
+ % animation_name
369
341
  )
370
- var keyframes: Array = keyed[1]
371
- var built: Array = _add_track(anim, track, target, target_path, keyframes)
372
- if not str(built[1]).is_empty():
373
- return scene.refuse(str(built[1]))
374
- var track_idx: int = built[0]
375
-
376
- var at: String = scene.path_of(player)
377
- var keys: int = anim.track_get_key_count(track_idx)
378
- var check: Callable = func(saved: Node) -> String:
379
- var copy: AnimationPlayer = saved.get_node_or_null(at) as AnimationPlayer
380
- if copy == null or not copy.has_animation(StringName(animation_name)):
381
- return "%s would not be in the scene when it loads." % animation_name
382
- var loaded: Animation = copy.get_animation(StringName(animation_name))
383
- if loaded.get_track_count() <= track_idx or loaded.track_get_key_count(track_idx) != keys:
384
- return "the track would not be in %s when the scene loads." % animation_name
385
- return ""
386
- var answer: Dictionary = {
387
- "ok": true,
388
- "trackType": str(track.get("type", "")),
389
- "trackIndex": track_idx,
390
- "keys": keys,
391
- "keysAdded": keyframes.size(),
392
- }
393
- if built[3]:
394
- answer["existingTrack"] = true
395
- if built[2] != null:
396
- answer["methodFound"] = built[2]
397
- var past: String = _past_length(anim, animation_name)
398
- if not past.is_empty():
399
- answer["pastLength"] = past
400
- var files: Dictionary = homes[1]
401
- return _finish(scene, check, files, answer)
402
-
403
-
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.
411
- func _add_track(
412
- anim: Animation, track: Dictionary, target: Node, target_path: String, keyframes: Array
413
- ) -> Array:
414
- var track_type: String = str(track.get("type", ""))
415
- var keys: Array = []
416
- var method_found: Variant = null
417
- var kind: Animation.TrackType = Animation.TYPE_VALUE
418
- var path: NodePath = NodePath(target_path)
419
- if track_type == "property":
420
- var prop_name: String = str(track.get("property", ""))
421
- if prop_name.is_empty():
422
- return [-1, "track.property is required for property track", null, false]
423
- var values: Array = _key_values(target, prop_name, keyframes)
424
- if not values[0]:
425
- return [-1, values[1], null, false]
426
- keys = values[1]
427
- path = NodePath(target_path + ":" + prop_name)
428
- elif track_type == "method":
429
- var method_name: String = str(track.get("method", ""))
430
- if method_name.is_empty():
431
- return [-1, "track.method is required for method track", null, false]
432
- for index: int in keyframes.size():
433
- var keyframe: Dictionary = keyframes[index]
434
- var given: Variant = keyframe.get("args", [])
435
- if not given is Array:
436
- return [-1, "keyframes[%d].args must be a list." % index, null, false]
437
- var arguments: Array = given
438
- keys.append({"method": method_name, "args": _parse_method_args(arguments)})
439
- method_found = target.has_method(method_name)
440
- kind = Animation.TYPE_METHOD
441
- else:
442
- return [-1, "Unsupported track.type: " + track_type, null, false]
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)
468
- for index: int in keyframes.size():
469
- var keyframe: Dictionary = keyframes[index]
470
- if anim.track_insert_key(track_idx, GdhRead.as_float(keyframe["time"]), keys[index]) < 0:
471
- return [-1, "Keyframe %d could not be inserted." % index, null, false]
472
- return [track_idx, "", method_found, existing]
342
+ for forbidden: String in [":", ",", "["]:
343
+ if animation_name.contains(forbidden):
344
+ return "An animation's name cannot hold : , or [, and %s does." % animation_name
345
+ for part: String in animation_name.split("/"):
346
+ if part.strip_edges().is_empty() and animation_name.contains("/"):
347
+ return "%s leaves the library's or the animation's name empty." % animation_name
348
+ return ""
473
349
 
474
350
 
475
351
  ## The keys of [param anim], called [param animation_name], that lie past its end, where they never
@@ -484,6 +360,12 @@ static func _past_length(anim: Animation, animation_name: String) -> String:
484
360
  var _added: bool = past.append(
485
361
  "%s at %s s" % [str(anim.track_get_path(track)), _seconds(time)]
486
362
  )
363
+ return _past_sentence(anim, animation_name, past)
364
+
365
+
366
+ ## The pastLength sentence for [param past], keys of [param anim] each named by its track's path and
367
+ ## time, or "" for none.
368
+ static func _past_sentence(anim: Animation, animation_name: String, past: PackedStringArray) -> String:
487
369
  if past.is_empty():
488
370
  return ""
489
371
  return (
@@ -509,57 +391,6 @@ static func _seconds(seconds: float) -> String:
509
391
  return str(int(rounded)) if rounded == floorf(rounded) else str(rounded)
510
392
 
511
393
 
512
- ## The keyframes of [param track], as [true, list] or [false, why]. Each needs its own time: one
513
- ## that was not a dictionary was skipped, and one without a time went in at 0, where the next such
514
- ## key replaced it, and the answer said nothing of either.
515
- func _keyframes(track: Dictionary) -> Array:
516
- var given: Variant = track.get("keyframes", [])
517
- if not given is Array:
518
- return [false, "track.keyframes must be a list."]
519
- var keyframes: Array = given
520
- var times: Dictionary = {}
521
- for index: int in keyframes.size():
522
- if not keyframes[index] is Dictionary:
523
- return [false, "keyframes[%d] is not an object with a time." % index]
524
- var keyframe: Dictionary = keyframes[index]
525
- var time: Variant = keyframe.get("time")
526
- if not (time is float or time is int):
527
- return [false, "keyframes[%d] has no time." % index]
528
- var seconds: float = GdhRead.as_float(time)
529
- if times.has(seconds):
530
- return [
531
- false, "keyframes[%d] and [%d] are both at %s seconds." % [times[seconds], index, seconds]
532
- ]
533
- times[seconds] = index
534
- return [true, keyframes]
535
-
536
-
537
- ## Each keyframe's value read as the property [param prop_name] on [param target] takes it, as
538
- ## [true, values] or [false, why]. Read without the property, a string "10" keyed on Label.text
539
- ## was stored as the number 10, and a Vector2 given as [1, 2] as a list.
540
- func _key_values(target: Node, prop_name: String, keyframes: Array) -> Array:
541
- var base: String = prop_name.get_slice(":", 0)
542
- var declared: Dictionary = _properties.declaration(target, base)
543
- if declared.is_empty():
544
- return [false, GdhPropertyValues.no_property(target, base)]
545
- var values: Array = []
546
- for index: int in keyframes.size():
547
- var keyframe: Dictionary = keyframes[index]
548
- var raw: Variant = keyframe.get("value")
549
- if base != prop_name:
550
- # A subname such as position:x keys one component, which is a number.
551
- var component: Variant = _properties.parse(raw, TYPE_FLOAT)
552
- if not component is float:
553
- return [false, "keyframes[%d].value keys %s, which is a number." % [index, prop_name]]
554
- values.append(component)
555
- continue
556
- var value: Array = _properties.value_for(target, declared, raw)
557
- if not value[0]:
558
- return [false, "keyframes[%d]: %s" % [index, value[1]]]
559
- values.append(value[1])
560
- return [true, values]
561
-
562
-
563
394
  # =============================================================================
564
395
  # set_animation, rename_animation, remove_animation
565
396
  # =============================================================================
@@ -702,7 +533,7 @@ func rename_animation(args: Dictionary) -> Dictionary:
702
533
  if player.has_animation(StringName(new_full)):
703
534
  return scene.refuse("%s already has an animation %s." % [player_node_path, new_full])
704
535
  var library: AnimationLibrary = _library_of(player, animation_name)
705
- var states: Array = _states_playing(scene, player, animation_name)
536
+ var states: Array = GdhAnimationReads.states_playing(scene.root, player, animation_name)
706
537
  var held: Array = [[library, "animation library"]]
707
538
  for state: Array in states:
708
539
  held.append([state[0], "state playing %s" % animation_name])
@@ -716,6 +547,9 @@ func rename_animation(args: Dictionary) -> Dictionary:
716
547
  library.rename_animation(StringName(animation_name.trim_prefix(prefix)), StringName(new_name))
717
548
  if not player.has_animation(StringName(new_full)):
718
549
  return scene.refuse("The library did not take the name %s." % new_full)
550
+ # As the editor's rename sets it: the library's key alone left the file's resource_name, which
551
+ # the inspector shows, on the old name.
552
+ player.get_animation(StringName(new_full)).resource_name = new_name
719
553
  var answer: Dictionary = {"ok": true, "animationName": new_full, "was": animation_name}
720
554
  if autoplayed:
721
555
  answer["autoplay"] = new_full
@@ -767,7 +601,7 @@ func remove_animation(args: Dictionary) -> Dictionary:
767
601
  player.autoplay = ""
768
602
  answer["autoplayCleared"] = true
769
603
  var orphaned: Array = []
770
- for state: Array in _states_playing(scene, player, animation_name):
604
+ for state: Array in GdhAnimationReads.states_playing(scene.root, player, animation_name):
771
605
  orphaned.append(state[1])
772
606
  if not orphaned.is_empty():
773
607
  answer["statesPlayingNothing"] = orphaned
@@ -780,202 +614,3 @@ func remove_animation(args: Dictionary) -> Dictionary:
780
614
  return ""
781
615
  var files: Dictionary = homes[1]
782
616
  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
-
831
- # =============================================================================
832
- # add_animation_state
833
- # =============================================================================
834
- func add_animation_state(args: Dictionary) -> Dictionary:
835
- var scene_path: String = GdhSceneFile.res_path("", str(args.get("scenePath", "")))
836
- var anim_tree_path: String = str(args.get("animTreePath", ""))
837
- var state_name: String = str(args.get("stateName", ""))
838
- var animation_name: String = str(args.get("animationName", ""))
839
- var state_machine_path: String = str(args.get("stateMachinePath", ""))
840
-
841
- if scene_path.strip_edges() == "res://":
842
- return {"ok": false, "error": "Missing scenePath"}
843
- for required: Array in [
844
- [anim_tree_path, "animTreePath"], [state_name, "stateName"], [animation_name, "animationName"]
845
- ]:
846
- if str(required[0]).is_empty():
847
- return {"ok": false, "error": "Missing " + str(required[1])}
848
-
849
- var opened: Array = _open_at(scene_path, anim_tree_path, "AnimationTree")
850
- var scene: GdhSceneFile = opened[0]
851
- if opened[1] == null:
852
- return opened[2]
853
- var anim_tree: AnimationTree = opened[1]
854
- var machine: Array = _machine(scene, anim_tree, state_machine_path)
855
- if machine[0] == null:
856
- return scene.refuse(str(machine[2]))
857
- var sm: AnimationNodeStateMachine = machine[0]
858
- # add_node fails with nothing but a line in the editor's log on a name in use, and Start and End
859
- # are in use in every state machine.
860
- if sm.has_node(StringName(state_name)):
861
- return scene.refuse("The state machine already has a state named %s." % state_name)
862
- var player: AnimationPlayer = anim_tree.get_node_or_null(anim_tree.anim_player) as AnimationPlayer
863
- var found: bool = player != null and player.has_animation(StringName(animation_name))
864
-
865
- var anim_node: AnimationNodeAnimation = AnimationNodeAnimation.new()
866
- anim_node.animation = StringName(animation_name)
867
- # Beside the rightmost state: add_node puts a state at the origin, where the editor's graph
868
- # draws it over whatever is already there, Start included.
869
- var rightmost: Vector2 = Vector2.ZERO
870
- for state: StringName in sm.get_node_list():
871
- var held: Vector2 = sm.get_node_position(state)
872
- if held.x > rightmost.x:
873
- rightmost = held
874
- var position: Vector2 = rightmost + Vector2(STATE_SPACING, 0)
875
- sm.add_node(StringName(state_name), anim_node, position)
876
- if not sm.has_node(StringName(state_name)):
877
- return scene.refuse("The state machine did not take the state %s." % state_name)
878
-
879
- var answer: Dictionary = {
880
- "ok": true,
881
- "stateName": state_name,
882
- "animationName": animation_name,
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,
886
- "position": GdhSerialisation.new().serialize_value(position),
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)
892
- var files: Dictionary = machine[1]
893
- return _finish(scene, Callable(), files, answer)
894
-
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
-
914
- # =============================================================================
915
- # connect_animation_states
916
- # =============================================================================
917
- func connect_animation_states(args: Dictionary) -> Dictionary:
918
- var scene_path: String = GdhSceneFile.res_path("", str(args.get("scenePath", "")))
919
- var anim_tree_path: String = str(args.get("animTreePath", ""))
920
- var from_state: String = str(args.get("fromState", ""))
921
- var to_state: String = str(args.get("toState", ""))
922
- var transition_type: String = str(args.get("transitionType", "immediate"))
923
- var state_machine_path: String = str(args.get("stateMachinePath", ""))
924
- var advance_condition: String = str(args.get("advanceCondition", ""))
925
-
926
- if scene_path.strip_edges() == "res://":
927
- return {"ok": false, "error": "Missing scenePath"}
928
- if anim_tree_path.is_empty():
929
- return {"ok": false, "error": "Missing animTreePath"}
930
- if from_state.is_empty() or to_state.is_empty():
931
- return {"ok": false, "error": "Missing fromState or toState"}
932
- var modes: Dictionary = {
933
- "sync": AnimationNodeStateMachineTransition.SWITCH_MODE_SYNC,
934
- "at_end": AnimationNodeStateMachineTransition.SWITCH_MODE_AT_END,
935
- "immediate": AnimationNodeStateMachineTransition.SWITCH_MODE_IMMEDIATE,
936
- }
937
- if not modes.has(transition_type):
938
- return {"ok": false, "error": "Unsupported transitionType: " + transition_type}
939
- var transition: AnimationNodeStateMachineTransition = AnimationNodeStateMachineTransition.new()
940
- transition.switch_mode = (
941
- GdhRead.as_int(modes[transition_type]) as AnimationNodeStateMachineTransition.SwitchMode
942
- )
943
- if not advance_condition.is_empty():
944
- transition.advance_condition = StringName(advance_condition)
945
-
946
- var opened: Array = _open_at(scene_path, anim_tree_path, "AnimationTree")
947
- var scene: GdhSceneFile = opened[0]
948
- if opened[1] == null:
949
- return opened[2]
950
- var anim_tree: AnimationTree = opened[1]
951
- var machine: Array = _machine(scene, anim_tree, state_machine_path)
952
- if machine[0] == null:
953
- return scene.refuse(str(machine[2]))
954
- var sm: AnimationNodeStateMachine = machine[0]
955
- var problem: String = _transition_problem(sm, from_state, to_state)
956
- if not problem.is_empty():
957
- return scene.refuse(problem)
958
-
959
- sm.add_transition(StringName(from_state), StringName(to_state), transition)
960
- if not sm.has_transition(StringName(from_state), StringName(to_state)):
961
- return scene.refuse("The state machine did not take the transition.")
962
- var files: Dictionary = machine[1]
963
- return _finish(scene, Callable(), files, {"ok": true, "from": from_state, "to": to_state})
964
-
965
-
966
- ## Why [param sm] cannot take a transition from [param from_state] to [param to_state], or "".
967
- ## Each is a condition add_transition fails on with only a line in the editor's log, and the
968
- ## transition was answered as made.
969
- func _transition_problem(sm: AnimationNodeStateMachine, from_state: String, to_state: String) -> String:
970
- for state: String in [from_state, to_state]:
971
- if not sm.has_node(StringName(state)):
972
- return "The state machine has no state named %s." % state
973
- if from_state == to_state:
974
- return "A transition needs two different states."
975
- if from_state == "End":
976
- return "No transition can leave End."
977
- if to_state == "Start":
978
- return "No transition can arrive at Start."
979
- if sm.has_transition(StringName(from_state), StringName(to_state)):
980
- return "There is already a transition from %s to %s." % [from_state, to_state]
981
- return ""