gdharness 1.3.8 → 1.3.9

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 (35) hide show
  1. package/build/cli.js +137 -66
  2. package/build/godot/addons/gdharness_editor/file_reads.gd +13 -0
  3. package/build/godot/addons/gdharness_editor/reading.gd +61 -5
  4. package/build/godot/addons/gdharness_editor/scene_file.gd +41 -35
  5. package/build/godot/addons/gdharness_editor/serialisation.gd +8 -7
  6. package/build/godot/addons/gdharness_editor/tool_executor.gd +12 -2
  7. package/build/godot/addons/gdharness_editor/tools/animation_states.gd +244 -0
  8. package/build/godot/addons/gdharness_editor/tools/animation_states.gd.uid +1 -0
  9. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +105 -157
  10. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +25 -1
  11. package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +4 -1
  12. package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +10 -0
  13. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +72 -3
  14. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +7 -1
  15. package/build/godot/addons/gdharness_runtime/call_errors.gd +8 -2
  16. package/build/godot/addons/gdharness_runtime/reading.gd +61 -5
  17. package/build/godot/addons/gdharness_runtime/runtime_capture.gd +5 -0
  18. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +3 -0
  19. package/build/godot/addons/gdharness_runtime/runtime_input.gd +27 -31
  20. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +11 -5
  21. package/build/godot/addons/gdharness_runtime/runtime_queries.gd +2 -0
  22. package/build/godot/addons/gdharness_runtime/runtime_targets.gd +26 -0
  23. package/build/godot/addons/gdharness_runtime/runtime_values.gd +105 -6
  24. package/build/godot/addons/gdharness_runtime/serialisation.gd +8 -7
  25. package/build/godot/operations/editor_tools.gd +8 -0
  26. package/build/godot/operations/file_reads.gd +13 -0
  27. package/build/godot/operations/gdscript_analysis.gd +14 -0
  28. package/build/godot/operations/gdscript_authoring.gd +1 -1
  29. package/build/godot/operations/godot_operations.gd +2 -0
  30. package/build/godot/operations/input_actions.gd +22 -7
  31. package/build/godot/operations/project_config.gd +31 -0
  32. package/build/godot/operations/reading.gd +61 -5
  33. package/build/godot/operations/serialisation.gd +8 -7
  34. package/build/keeper.js +43 -13
  35. package/package.json +1 -1
@@ -119,6 +119,9 @@ static func _laid(scene_path: String) -> Dictionary:
119
119
  return {"ok": false, "error": refused}
120
120
  if not nodes.has("."):
121
121
  return {"ok": false, "error": "%s holds no nodes" % scene_path}
122
+ var base: SceneState = packed.get_state().get_base_scene_state()
123
+ if base != null:
124
+ nodes["."]["inherits"] = base.get_path()
122
125
  return {"ok": true, "nodes": nodes}
123
126
 
124
127
 
@@ -175,6 +178,10 @@ static func _lay(
175
178
  nodes[path] = _made(str(state.get_node_type(index)))
176
179
  if ids[index] >= 0:
177
180
  nodes[path]["uniqueId"] = ids[index]
181
+ # Laid at the root with no scene handed in is a scene the one being read inherits, and the
182
+ # deepest is laid first, so this is the file that makes the node.
183
+ if prefix == "." and loaded == null:
184
+ nodes[path]["inheritedFrom"] = state.get_path()
178
185
  # The name is the state's even for an entry laid already, since an inherited scene names its
179
186
  # root itself; a root instanced inside another scene is renamed by that scene once laid.
180
187
  if not recovered:
@@ -324,6 +331,12 @@ static func _entry(
324
331
  data["instanceOf"] = node["instanceOf"]
325
332
  if node.has("editableChildren"):
326
333
  data["editableChildren"] = node["editableChildren"]
334
+ # An inherited scene's file holds only what it changes, so the read says what it inherits and
335
+ # which nodes come from there, as the editor greys them.
336
+ if node.has("inherits"):
337
+ data["inherits"] = node["inherits"]
338
+ elif node.has("inheritedFrom"):
339
+ data["inheritedFrom"] = node["inheritedFrom"]
327
340
  var groups: Array[String] = node["groups"]
328
341
  if not groups.is_empty():
329
342
  var sorted: Array[String] = groups.duplicate()
@@ -24,6 +24,23 @@ extends RefCounted
24
24
 
25
25
  ## How many of a node's children a refusal for a path missing below it names.
26
26
  const NAMED_CHILDREN: int = 12
27
+ ## The types holding their floats as the engine's real numbers, 32-bit unless it was built for 64.
28
+ const SHAPES_OF_REALS: Array[int] = [
29
+ TYPE_VECTOR2,
30
+ TYPE_VECTOR3,
31
+ TYPE_VECTOR4,
32
+ TYPE_RECT2,
33
+ TYPE_PLANE,
34
+ TYPE_QUATERNION,
35
+ TYPE_AABB,
36
+ TYPE_BASIS,
37
+ TYPE_TRANSFORM2D,
38
+ TYPE_TRANSFORM3D,
39
+ TYPE_PROJECTION,
40
+ TYPE_PACKED_VECTOR2_ARRAY,
41
+ TYPE_PACKED_VECTOR3_ARRAY,
42
+ TYPE_PACKED_VECTOR4_ARRAY,
43
+ ]
27
44
 
28
45
 
29
46
  static func as_int(value: Variant, fallback: int = 0) -> int:
@@ -190,18 +207,26 @@ static func offered(said: String, word: String, names: Array) -> String:
190
207
 
191
208
 
192
209
  ## [param said], the refusal for [param property], which [param target] does not declare, with the
193
- ## names it declares nearest it when any are, as a misspelt one is usually one of them. Unlike
194
- ## [method offered], nothing is listed when none is near, since an object declares a hundred.
210
+ ## names it declares nearest it when any are, as a misspelt one is usually one of them, or else the
211
+ ## variables its script declares. The engine's own are not listed when none is near, since an object
212
+ ## has a hundred, while a script's are the few a caller most likely meant.
195
213
  static func near_property(said: String, target: Object, property: String) -> String:
196
214
  var headings: int = PROPERTY_USAGE_CATEGORY | PROPERTY_USAGE_GROUP | PROPERTY_USAGE_SUBGROUP
197
215
  var names: Array[String] = []
216
+ var scripted: Array[String] = []
198
217
  for declared: Dictionary in target.get_property_list():
199
- if not as_int(declared.get("usage", 0)) & headings:
218
+ var usage: int = as_int(declared.get("usage", 0))
219
+ if not usage & headings:
200
220
  names.append(str(declared.get("name", "")))
221
+ if usage & PROPERTY_USAGE_SCRIPT_VARIABLE and not usage & headings:
222
+ scripted.append(str(declared.get("name", "")))
201
223
  var close: PackedStringArray = nearest(property, PackedStringArray(names))
202
- if close.is_empty():
224
+ if not close.is_empty():
225
+ return "%s; the nearest it has %s %s" % [said, "is" if close.size() == 1 else "are", ", ".join(close)]
226
+ if scripted.is_empty():
203
227
  return said
204
- return "%s; the nearest it has %s %s" % [said, "is" if close.size() == 1 else "are", ", ".join(close)]
228
+ scripted.sort()
229
+ return "%s; its script declares %s" % [said, ", ".join(PackedStringArray(scripted))]
205
230
 
206
231
 
207
232
  ## The signals [param object] has that a caller can connect, its own class's first: the engine
@@ -219,3 +244,34 @@ static func signals_of(object: Object) -> Array:
219
244
  ## [param word] after the article it takes, "an AnimationTree" or "a Node2D".
220
245
  static func with_article(word: String) -> String:
221
246
  return ("an " if word.left(1).to_upper() in ["A", "E", "I", "O", "U"] else "a ") + word
247
+
248
+
249
+ ## [param value], which the engine holds as a 32-bit float, as the shortest decimal that is still
250
+ ## that float. JSON carries a double, and the double a 32-bit 0.2 widens to is 0.200000002980232:
251
+ ## the same number, but not the one written or the one the file shows, so a caller comparing what it
252
+ ## set against what it read saw them differ. The shorter one, sent back, stores the same 32 bits. A
253
+ ## plain float is left alone by the serialiser, since nothing says whether the engine holds it in
254
+ ## 32 bits or 64.
255
+ static func single(value: float) -> float:
256
+ if not is_finite(value) or value == 0.0:
257
+ return value
258
+ var held: float = PackedFloat32Array([value])[0]
259
+ var magnitude: int = floori(log(absf(held)) / log(10.0))
260
+ for digits: int in range(1, 10):
261
+ var shift: int = digits - 1 - magnitude
262
+ var tried: float = (
263
+ roundf(held * pow(10.0, shift)) / pow(10.0, shift)
264
+ if shift >= 0
265
+ else roundf(held / pow(10.0, -shift)) * pow(10.0, -shift)
266
+ )
267
+ if PackedFloat32Array([tried])[0] == held:
268
+ return tried
269
+ return held
270
+
271
+
272
+ ## Whether a value of [param type] holds its floats in 32 bits: a colour always, and the vectors and
273
+ ## the shapes built of them unless the engine was built for 64.
274
+ static func holds_singles(type: int) -> bool:
275
+ if type in [TYPE_COLOR, TYPE_PACKED_COLOR_ARRAY, TYPE_PACKED_FLOAT32_ARRAY]:
276
+ return true
277
+ return not OS.has_feature("double") and type in SHAPES_OF_REALS
@@ -50,6 +50,8 @@ static var by_path: PackedStringArray = []
50
50
 
51
51
  var root: Node = null
52
52
  var path: String = ""
53
+ ## What the last write saved, for its answer to read what the file now stores.
54
+ var written_pack: PackedScene = null
53
55
  ## The path of each node a scene this one inherits gives, with the file that makes it.
54
56
  var _base_paths: Dictionary = {}
55
57
  var _state: SceneState = null
@@ -91,18 +93,7 @@ func open(scene_path: String, for_writing: bool) -> Dictionary:
91
93
  if root == null:
92
94
  return {"ok": false, "error": "Failed to instantiate " + path}
93
95
  _state = packed.get_state()
94
- var base: SceneState = _state.get_base_scene_state()
95
- # Each node a base gives, with the file that makes it: the deepest base listing it as more than
96
- # values set on a node from further down.
97
- while base != null:
98
- for index: int in base.get_node_count():
99
- var given: String = _relative(str(base.get_node_path(index)))
100
- var made: bool = (
101
- base.get_node_instance(index) != null or not str(base.get_node_type(index)).is_empty()
102
- )
103
- if made or not _base_paths.has(given):
104
- _base_paths[given] = base.get_path()
105
- base = base.get_base_scene_state()
96
+ _base_paths = makers(_state)
106
97
  # On a read as well, which would otherwise answer what the scripts set as what the file holds.
107
98
  if not Engine.is_editor_hint():
108
99
  if for_writing:
@@ -179,8 +170,7 @@ func refuse(error: String) -> Dictionary:
179
170
  return {"ok": false, "error": error}
180
171
 
181
172
 
182
- ## Each bit set in [param flags], a connection's, by name, and whatever bits are left unnamed as one
183
- ## number.
173
+ ## Each bit set in [param flags], a connection's, by name, and any bits left unnamed as one number.
184
174
  static func flag_names(flags: int) -> Array[String]:
185
175
  var names: Array[String] = []
186
176
  var rest: int = flags
@@ -350,13 +340,20 @@ func _closed_around(node: Node) -> PackedStringArray:
350
340
  func fixed(node: Node, deleting: bool = false) -> String:
351
341
  if node == root:
352
342
  return "The root of %s cannot be moved, renamed, copied or deleted." % path
343
+ if node.owner == null:
344
+ return foreign(node)
353
345
  if node.owner != root:
354
- var why: String = foreign(node)
355
- if why.is_empty():
356
- why = "%s is inside an instanced scene, which creates it again on load." % path_of(node)
357
- if deleting:
358
- why += _deleted_elsewhere(node, node.owner.scene_file_path)
359
- return why
346
+ # Editable children keep values here and no more; the scene instanced may have the node from
347
+ # a scene it inherits, which is the one to edit.
348
+ var instanced: String = node.owner.scene_file_path
349
+ var packed: PackedScene = load(instanced) as PackedScene
350
+ var made_by: Dictionary = makers(packed.get_state()) if packed != null else {}
351
+ var maker: String = str(made_by.get(str(node.owner.get_path_to(node)), instanced))
352
+ var inside: String = (
353
+ "%s is inside the scene instanced at %s (%s), which creates it again on load."
354
+ % [path_of(node), path_of(node.owner), instanced]
355
+ )
356
+ return inside + (_deleted_elsewhere(node, maker) if deleting else " Edit %s instead." % maker)
360
357
  var at: String = path_of(node)
361
358
  if _base_paths.has(at):
362
359
  var maker: String = str(_base_paths[at])
@@ -367,6 +364,23 @@ func fixed(node: Node, deleting: bool = false) -> String:
367
364
  return ""
368
365
 
369
366
 
367
+ ## Each node the scenes [param state] inherits give it, by path, with the deepest file making it more
368
+ ## than values; a node [param state] makes itself is not in it.
369
+ static func makers(state: SceneState) -> Dictionary:
370
+ var made_by: Dictionary = {}
371
+ var at: SceneState = state.get_base_scene_state()
372
+ while at != null:
373
+ for index: int in at.get_node_count():
374
+ var given: String = _relative(str(at.get_node_path(index)))
375
+ var made: bool = (
376
+ at.get_node_instance(index) != null or not str(at.get_node_type(index)).is_empty()
377
+ )
378
+ if made or not made_by.has(given):
379
+ made_by[given] = at.get_path()
380
+ at = at.get_base_scene_state()
381
+ return made_by
382
+
383
+
370
384
  ## What a caller can do with [param node], made by [param maker], instead of deleting it here.
371
385
  static func _deleted_elsewhere(node: Node, maker: String) -> String:
372
386
  var hidden: String = (
@@ -583,19 +597,12 @@ func write(check: Callable = Callable()) -> Dictionary:
583
597
  return refuse("Nothing was written: " + str(settled[1]))
584
598
  packed = settled[0]
585
599
  if not path.ends_with(".tscn") and _holds_scripted_embedded():
586
- return refuse(
587
- (
588
- (
589
- (
590
- "Nothing was written: %s is a binary scene holding an embedded resource with a script "
591
- + "that is not a tool, and what that script changed when a headless engine loaded it "
592
- + "cannot be told apart from the file here. "
593
- )
594
- % path
595
- )
596
- + GdhHeadlessCheck.REMEDY
597
- )
600
+ var binary: String = (
601
+ "Nothing was written: %s is a binary scene holding an embedded resource with a script "
602
+ + "that is not a tool, and what that script changed when a headless engine loaded it "
603
+ + "cannot be told apart from the file here. "
598
604
  )
605
+ return refuse(binary % path + GdhHeadlessCheck.REMEDY)
599
606
  if check.is_valid():
600
607
  var saved: Node = packed.instantiate(PackedScene.GEN_EDIT_STATE_MAIN)
601
608
  if saved == null:
@@ -608,6 +615,7 @@ func write(check: Callable = Callable()) -> Dictionary:
608
615
  var previous: Variant = null
609
616
  if FileAccess.file_exists(path):
610
617
  previous = FileAccess.get_file_as_bytes(path)
618
+ written_pack = packed
611
619
  var written: Dictionary = save_packed(packed, path)
612
620
  if not written.is_empty() or not headless or not path.ends_with(".tscn"):
613
621
  return written
@@ -988,7 +996,5 @@ static func _stored_at(state: SceneState, index: int) -> Dictionary:
988
996
  return stored
989
997
 
990
998
 
991
- func _relative(node_path: String) -> String:
992
- if node_path == ".":
993
- return node_path
999
+ static func _relative(node_path: String) -> String:
994
1000
  return node_path.trim_prefix("./")
@@ -142,28 +142,29 @@ func serialize_value(value: Variant) -> Variant:
142
142
  return value
143
143
  var carried: Variant = call(serialiser, value)
144
144
  # The fixed shapes carry their components as bare floats, so an infinite one in a Vector2 is
145
- # tagged here; containers reach their members through this function already.
146
- return carried if typeof(value) in [TYPE_ARRAY, TYPE_DICTIONARY] else _finite(carried)
145
+ # tagged here, and a 32-bit one shortened; containers reach their members through this already.
146
+ var type: int = typeof(value)
147
+ return carried if type in [TYPE_ARRAY, TYPE_DICTIONARY] else _finite(carried, GdhRead.holds_singles(type))
147
148
 
148
149
 
149
150
  # [param carried] with every non-finite float in it tagged. JSON has no spelling for infinity or
150
151
  # NaN: the engine writes 1e99999 and null, and the server, reading 1e99999 as Infinity, writes it
151
152
  # back out as null, so a property holding INF was answered as holding nothing.
152
- func _finite(carried: Variant) -> Variant:
153
+ func _finite(carried: Variant, single: bool = false) -> Variant:
153
154
  if carried is float:
154
155
  var number: float = carried
155
- return _serialize_float(number)
156
+ return _serialize_float(GdhRead.single(number) if single else number)
156
157
  if carried is Dictionary:
157
158
  var fields: Dictionary = carried
158
159
  var tagged: Dictionary = {}
159
160
  for key: Variant in fields:
160
- tagged[key] = _finite(fields[key])
161
+ tagged[key] = _finite(fields[key], single)
161
162
  return tagged
162
163
  if carried is Array:
163
164
  var items: Array = carried
164
165
  var tagged_items: Array = []
165
166
  for item: Variant in items:
166
- tagged_items.append(_finite(item))
167
+ tagged_items.append(_finite(item, single))
167
168
  return tagged_items
168
169
  return carried
169
170
 
@@ -189,7 +190,7 @@ func _serialize_floats(value: Variant) -> Variant:
189
190
  for each: float in numbers:
190
191
  items.append(_serialize_float(each))
191
192
  return items
192
- return value
193
+ return numbers if value is PackedFloat32Array else value
193
194
 
194
195
 
195
196
  # Rebuilds a Godot value from the shape serialize_value gave it.
@@ -8,6 +8,7 @@ extends Node
8
8
  const GdhSceneTools = preload("tools/scene_tools.gd")
9
9
  const GdhSignalTools = preload("tools/signal_tools.gd")
10
10
  const GdhResourceTools = preload("tools/resource_tools.gd")
11
+ const GdhAnimationStates = preload("tools/animation_states.gd")
11
12
  const GdhAnimationTools = preload("tools/animation_tools.gd")
12
13
  const GdhPlayTools = preload("tools/play_tools.gd")
13
14
  const GdhClassTools = preload("tools/class_tools.gd")
@@ -20,6 +21,7 @@ var _editor_plugin: EditorPlugin = null
20
21
  var _scene_tools: GdhSceneTools = null
21
22
  var _signal_tools: GdhSignalTools = null
22
23
  var _resource_tools: GdhResourceTools = null
24
+ var _animation_states: GdhAnimationStates = null
23
25
  var _animation_tools: GdhAnimationTools = null
24
26
  var _play_tools: GdhPlayTools = null
25
27
  var _class_tools: GdhClassTools = null
@@ -37,6 +39,7 @@ func set_editor_plugin(plugin: EditorPlugin) -> void:
37
39
  _scene_tools.set_editor_plugin(plugin)
38
40
  _resource_tools.set_editor_plugin(plugin)
39
41
  _animation_tools.set_editor_plugin(plugin)
42
+ _animation_states.set_editor_plugin(plugin)
40
43
  _play_tools.set_editor_plugin(plugin)
41
44
  _class_tools.set_editor_plugin(plugin)
42
45
  _filesystem_tools.set_editor_plugin(plugin)
@@ -64,6 +67,10 @@ func _init_tools() -> void:
64
67
  _animation_tools.name = "AnimationTools"
65
68
  add_child(_animation_tools)
66
69
 
70
+ _animation_states = GdhAnimationStates.new()
71
+ _animation_states.name = "AnimationStates"
72
+ add_child(_animation_states)
73
+
67
74
  _play_tools = GdhPlayTools.new()
68
75
  _play_tools.name = "PlayTools"
69
76
  add_child(_play_tools)
@@ -131,11 +138,14 @@ func _init_tools() -> void:
131
138
  "read_animation_states": [_animation_tools, "read_animation_states"],
132
139
  "create_animation": [_animation_tools, "create_animation"],
133
140
  "add_animation_track": [_animation_tools, "add_animation_track"],
141
+ "remove_animation_track": [_animation_tools, "remove_animation_track"],
134
142
  "set_animation": [_animation_tools, "set_animation"],
135
143
  "rename_animation": [_animation_tools, "rename_animation"],
136
144
  "remove_animation": [_animation_tools, "remove_animation"],
137
- "add_animation_state": [_animation_tools, "add_animation_state"],
138
- "connect_animation_states": [_animation_tools, "connect_animation_states"],
145
+ "add_animation_state": [_animation_states, "add_animation_state"],
146
+ "connect_animation_states": [_animation_states, "connect_animation_states"],
147
+ "remove_animation_state": [_animation_states, "remove_animation_state"],
148
+ "disconnect_animation_states": [_animation_states, "disconnect_animation_states"],
139
149
  }
140
150
 
141
151
 
@@ -0,0 +1,244 @@
1
+ @tool
2
+ extends "animation_tools.gd"
3
+
4
+ ## The states and transitions of an AnimationTree's state machines, beside the players and tracks
5
+ ## the script it extends edits, whose helpers it shares.
6
+
7
+ ## How far right of the rightmost state a new one goes in the editor's graph, wider than a state's
8
+ ## box there.
9
+ const STATE_SPACING: float = 200.0
10
+
11
+
12
+ # =============================================================================
13
+ # add_animation_state
14
+ # =============================================================================
15
+ func add_animation_state(args: Dictionary) -> Dictionary:
16
+ var state_name: String = str(args.get("stateName", ""))
17
+ var animation_name: String = str(args.get("animationName", ""))
18
+ if state_name.is_empty():
19
+ return {"ok": false, "error": "Missing stateName"}
20
+ if animation_name.is_empty():
21
+ return {"ok": false, "error": "Missing animationName"}
22
+ var machine: Array = _machine_at(args)
23
+ if machine[1] == null:
24
+ return machine[3]
25
+ var scene: GdhSceneFile = machine[0]
26
+ var sm: AnimationNodeStateMachine = machine[1]
27
+ var anim_tree: AnimationTree = machine[4]
28
+ # add_node fails with nothing but a line in the editor's log on a name in use, and Start and End
29
+ # are in use in every state machine.
30
+ if sm.has_node(StringName(state_name)):
31
+ return scene.refuse("The state machine already has a state named %s." % state_name)
32
+ var player: AnimationPlayer = anim_tree.get_node_or_null(anim_tree.anim_player) as AnimationPlayer
33
+ var found: bool = player != null and player.has_animation(StringName(animation_name))
34
+
35
+ var anim_node: AnimationNodeAnimation = AnimationNodeAnimation.new()
36
+ anim_node.animation = StringName(animation_name)
37
+ # Beside the rightmost state: add_node puts a state at the origin, where the editor's graph
38
+ # draws it over whatever is already there, Start included.
39
+ var rightmost: Vector2 = Vector2.ZERO
40
+ for state: StringName in sm.get_node_list():
41
+ var held: Vector2 = sm.get_node_position(state)
42
+ if held.x > rightmost.x:
43
+ rightmost = held
44
+ var position: Vector2 = rightmost + Vector2(STATE_SPACING, 0)
45
+ sm.add_node(StringName(state_name), anim_node, position)
46
+ if not sm.has_node(StringName(state_name)):
47
+ return scene.refuse("The state machine did not take the state %s." % state_name)
48
+
49
+ var answer: Dictionary = {
50
+ "ok": true,
51
+ "stateName": state_name,
52
+ "animationName": animation_name,
53
+ # False when the tree has no player to look the animation up in, or its player has none of
54
+ # that name.
55
+ "animationFound": found,
56
+ "position": GdhSerialisation.new().serialize_value(position),
57
+ }
58
+ # Written all the same, as the editor writes one: a state machine is often laid out before its
59
+ # animations are made, and refusing it stopped a tree being built in that order.
60
+ if player != null and not found:
61
+ answer["animationMissing"] = _missing_animation(scene, player, animation_name)
62
+ var files: Dictionary = machine[2]
63
+ return _finish(scene, Callable(), files, answer)
64
+
65
+
66
+ ## Why the state playing [param animation_name] plays nothing yet. The player is named by its path
67
+ ## from the scene root, as playerNodePath takes it; anim_player is relative to the tree ("../Anim"),
68
+ ## which no argument takes.
69
+ static func _missing_animation(
70
+ scene: GdhSceneFile, player: AnimationPlayer, animation_name: String
71
+ ) -> String:
72
+ var said: String = (
73
+ "The AnimationPlayer at %s has no animation %s yet, so the state plays nothing until it has one."
74
+ % [str(scene.root.get_path_to(player)), animation_name]
75
+ )
76
+ var close: PackedStringArray = GdhRead.nearest(animation_name, player.get_animation_list())
77
+ if close.is_empty():
78
+ return said
79
+ return (
80
+ said + " The nearest %s %s." % ["it has is" if close.size() == 1 else "it has are", ", ".join(close)]
81
+ )
82
+
83
+
84
+ # =============================================================================
85
+ # connect_animation_states
86
+ # =============================================================================
87
+ func connect_animation_states(args: Dictionary) -> Dictionary:
88
+ var from_state: String = str(args.get("fromState", ""))
89
+ var to_state: String = str(args.get("toState", ""))
90
+ var transition_type: String = str(args.get("transitionType", "immediate"))
91
+ var advance_condition: String = str(args.get("advanceCondition", ""))
92
+
93
+ if from_state.is_empty() or to_state.is_empty():
94
+ return {"ok": false, "error": "Missing fromState or toState"}
95
+ var modes: Dictionary = {
96
+ "sync": AnimationNodeStateMachineTransition.SWITCH_MODE_SYNC,
97
+ "at_end": AnimationNodeStateMachineTransition.SWITCH_MODE_AT_END,
98
+ "immediate": AnimationNodeStateMachineTransition.SWITCH_MODE_IMMEDIATE,
99
+ }
100
+ if not modes.has(transition_type):
101
+ return {"ok": false, "error": "Unsupported transitionType: " + transition_type}
102
+ var transition: AnimationNodeStateMachineTransition = AnimationNodeStateMachineTransition.new()
103
+ transition.switch_mode = (
104
+ GdhRead.as_int(modes[transition_type]) as AnimationNodeStateMachineTransition.SwitchMode
105
+ )
106
+ if not advance_condition.is_empty():
107
+ transition.advance_condition = StringName(advance_condition)
108
+
109
+ var found: Array = _machine_at(args)
110
+ if found[1] == null:
111
+ return found[3]
112
+ var scene: GdhSceneFile = found[0]
113
+ var sm: AnimationNodeStateMachine = found[1]
114
+ var problem: String = _transition_problem(sm, from_state, to_state)
115
+ if not problem.is_empty():
116
+ return scene.refuse(problem)
117
+
118
+ sm.add_transition(StringName(from_state), StringName(to_state), transition)
119
+ if not sm.has_transition(StringName(from_state), StringName(to_state)):
120
+ return scene.refuse("The state machine did not take the transition.")
121
+ var files: Dictionary = found[2]
122
+ return _finish(scene, Callable(), files, {"ok": true, "from": from_state, "to": to_state})
123
+
124
+
125
+ ## Why [param sm] cannot take a transition from [param from_state] to [param to_state], or "".
126
+ ## Each is a condition add_transition fails on with only a line in the editor's log, and the
127
+ ## transition was answered as made.
128
+ func _transition_problem(sm: AnimationNodeStateMachine, from_state: String, to_state: String) -> String:
129
+ for state: String in [from_state, to_state]:
130
+ if not sm.has_node(StringName(state)):
131
+ return _no_state(sm, state)
132
+ if from_state == to_state:
133
+ return "A transition needs two different states."
134
+ if from_state == "End":
135
+ return "No transition can leave End."
136
+ if to_state == "Start":
137
+ return "No transition can arrive at Start."
138
+ if sm.has_transition(StringName(from_state), StringName(to_state)):
139
+ return "There is already a transition from %s to %s." % [from_state, to_state]
140
+ return ""
141
+
142
+
143
+ ## The refusal for [param state_name], which [param sm] has no state of: with the nearest names it
144
+ ## has, or else every state, since a machine has a handful.
145
+ static func _no_state(sm: AnimationNodeStateMachine, state_name: String) -> String:
146
+ var said: String = "The state machine has no state named %s" % state_name
147
+ var states: PackedStringArray = []
148
+ for state: StringName in sm.get_node_list():
149
+ var _listed: bool = states.append(str(state))
150
+ var close: PackedStringArray = GdhRead.nearest(state_name, states)
151
+ if not close.is_empty():
152
+ return (
153
+ "%s; the nearest it has %s %s." % [said, "is" if close.size() == 1 else "are", ", ".join(close)]
154
+ )
155
+ return "%s; its states are %s." % [said, ", ".join(states)]
156
+
157
+
158
+ ## The state machine [param args] names, as [scene, machine, files, refusal, tree]: the machine is
159
+ ## null and the refusal says why when the scene, the tree or the machine is not there.
160
+ func _machine_at(args: Dictionary) -> Array:
161
+ var scene_path: String = GdhSceneFile.res_path("", str(args.get("scenePath", "")))
162
+ var anim_tree_path: String = str(args.get("animTreePath", ""))
163
+ if scene_path.strip_edges() == "res://":
164
+ return [null, null, {}, {"ok": false, "error": "Missing scenePath"}]
165
+ if anim_tree_path.is_empty():
166
+ return [null, null, {}, {"ok": false, "error": "Missing animTreePath"}]
167
+ var opened: Array = _open_at(scene_path, anim_tree_path, "AnimationTree")
168
+ var scene: GdhSceneFile = opened[0]
169
+ if opened[1] == null:
170
+ return [scene, null, {}, opened[2]]
171
+ var anim_tree: AnimationTree = opened[1]
172
+ var machine: Array = _machine(scene, anim_tree, str(args.get("stateMachinePath", "")))
173
+ if machine[0] == null:
174
+ return [scene, null, {}, scene.refuse(str(machine[2]))]
175
+ return [scene, machine[0], machine[1], {}, anim_tree]
176
+
177
+
178
+ # =============================================================================
179
+ # remove_animation_state, disconnect_animation_states
180
+ # =============================================================================
181
+ ## Takes a state out of its state machine with every transition leaving or reaching it, which the
182
+ ## engine drops with the state, and names those under transitionsRemoved. Start and End are part of
183
+ ## every state machine.
184
+ func remove_animation_state(args: Dictionary) -> Dictionary:
185
+ var state_name: String = str(args.get("stateName", ""))
186
+ if state_name.is_empty():
187
+ return {"ok": false, "error": "Missing stateName"}
188
+ var found: Array = _machine_at(args)
189
+ if found[1] == null:
190
+ return found[3]
191
+ var scene: GdhSceneFile = found[0]
192
+ var sm: AnimationNodeStateMachine = found[1]
193
+ if state_name in ["Start", "End"]:
194
+ return scene.refuse("%s is part of every state machine, so it cannot be removed." % state_name)
195
+ if not sm.has_node(StringName(state_name)):
196
+ return scene.refuse(_no_state(sm, state_name))
197
+ var dropped: Array = []
198
+ for index: int in sm.get_transition_count():
199
+ var from: String = str(sm.get_transition_from(index))
200
+ var to: String = str(sm.get_transition_to(index))
201
+ if state_name in [from, to]:
202
+ dropped.append({"from": from, "to": to})
203
+ sm.remove_node(StringName(state_name))
204
+ if sm.has_node(StringName(state_name)):
205
+ return scene.refuse("The state machine did not let go of %s." % state_name)
206
+ for index: int in sm.get_transition_count():
207
+ if state_name in [str(sm.get_transition_from(index)), str(sm.get_transition_to(index))]:
208
+ return scene.refuse(
209
+ "The state machine kept a transition of %s after the state went." % state_name
210
+ )
211
+ var files: Dictionary = found[2]
212
+ return _finish(
213
+ scene, Callable(), files, {"ok": true, "removed": state_name, "transitionsRemoved": dropped}
214
+ )
215
+
216
+
217
+ ## Takes out the transition from one state to another.
218
+ func disconnect_animation_states(args: Dictionary) -> Dictionary:
219
+ var from_state: String = str(args.get("fromState", ""))
220
+ var to_state: String = str(args.get("toState", ""))
221
+ if from_state.is_empty() or to_state.is_empty():
222
+ return {"ok": false, "error": "Missing fromState or toState"}
223
+ var found: Array = _machine_at(args)
224
+ if found[1] == null:
225
+ return found[3]
226
+ var scene: GdhSceneFile = found[0]
227
+ var sm: AnimationNodeStateMachine = found[1]
228
+ for state: String in [from_state, to_state]:
229
+ if not sm.has_node(StringName(state)):
230
+ return scene.refuse(_no_state(sm, state))
231
+ if not sm.has_transition(StringName(from_state), StringName(to_state)):
232
+ var leaving: PackedStringArray = []
233
+ for index: int in sm.get_transition_count():
234
+ if str(sm.get_transition_from(index)) == from_state:
235
+ var _listed: bool = leaving.append(str(sm.get_transition_to(index)))
236
+ var said: String = "There is no transition from %s to %s" % [from_state, to_state]
237
+ if leaving.is_empty():
238
+ return scene.refuse("%s; none leaves %s." % [said, from_state])
239
+ return scene.refuse("%s; the ones leaving %s go to %s." % [said, from_state, ", ".join(leaving)])
240
+ sm.remove_transition(StringName(from_state), StringName(to_state))
241
+ if sm.has_transition(StringName(from_state), StringName(to_state)):
242
+ return scene.refuse("The state machine did not let go of the transition.")
243
+ var files: Dictionary = found[2]
244
+ return _finish(scene, Callable(), files, {"ok": true, "removed": {"from": from_state, "to": to_state}})