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
@@ -0,0 +1,121 @@
1
+ extends RefCounted
2
+
3
+ ## Project settings read the same way headless and from the open editor, because the two are one
4
+ ## question asked of two places: a field one of them left out read as the editor and the file
5
+ ## disagreeing about something they agree about.
6
+
7
+ const GdhRead = preload("reading.gd")
8
+ const GdhSerialisation = preload("serialisation.gd")
9
+
10
+
11
+ ## One setting: whether the engine knows it, its value, whether that is the engine's default, and
12
+ ## the feature overrides that replace it in some builds.
13
+ static func one(setting_path: String, values: GdhSerialisation) -> Dictionary:
14
+ var result: Dictionary = {
15
+ "setting_path": setting_path, "exists": ProjectSettings.has_setting(setting_path)
16
+ }
17
+
18
+ if result["exists"]:
19
+ var value: Variant = ProjectSettings.get_setting(setting_path)
20
+ result["value"] = values.serialize_value(value)
21
+ # A setting the engine registers exists whether or not the project sets it, so exists did
22
+ # not say whether project.godot has a line for it. At its default it has none: the engine
23
+ # drops such a key when it saves.
24
+ var default_value: Variant = ProjectSettings.property_get_revert(setting_path)
25
+ if default_value != null:
26
+ result["at_default"] = value == default_value
27
+ else:
28
+ result["value"] = null
29
+ result["message"] = "Setting does not exist"
30
+
31
+ # An override exists only while project.godot sets it, so one the file does not set read as a
32
+ # setting nobody had heard of, when a build with that feature reads its base setting instead.
33
+ var base: String = override_base(setting_path)
34
+ if not base.is_empty():
35
+ result["override_of"] = base
36
+ if not result["exists"] and ProjectSettings.has_setting(base):
37
+ result["holds"] = values.serialize_value(ProjectSettings.get_setting(base))
38
+ result["message"] = overridden_by_none(setting_path, values)
39
+
40
+ # The value under this name is not the one every build reads: `name.windows` replaces it on
41
+ # Windows, `name.editor` in the editor. Read alone, a caller saw the plain value and not the one
42
+ # their game showed.
43
+ var overrides: Dictionary = overrides_of(setting_path, values)
44
+ if not overrides.is_empty():
45
+ result["overrides"] = overrides
46
+
47
+ return result
48
+
49
+
50
+ ## Every setting whose name starts with [param prefix], with the type the engine registers for
51
+ ## each.
52
+ ##
53
+ ## The type is here because it is the half a name hides. A setting sitting among a family of levels
54
+ ## can be a bool, and a caller that assumes otherwise writes a level over it and sees nothing go
55
+ ## wrong: 2 is as true as true is.
56
+ static func under(prefix: String, values: GdhSerialisation) -> Dictionary:
57
+ var found: Array[Dictionary] = []
58
+ for property: Dictionary in ProjectSettings.get_property_list():
59
+ var name: String = str(property.get("name", ""))
60
+ if not name.begins_with(prefix):
61
+ continue
62
+ var kind: int = GdhRead.as_int(property.get("type", TYPE_NIL), TYPE_NIL)
63
+ var value: Variant = ProjectSettings.get_setting(name)
64
+ var described: Dictionary = {
65
+ "setting": name,
66
+ "type": type_string(kind),
67
+ "value": values.serialize_value(value),
68
+ }
69
+ # As a single get says it, so a family read can be told apart into what the project sets
70
+ # and the engine's defaults: "input/" is ninety actions the engine ships and the project's few.
71
+ var default_value: Variant = ProjectSettings.property_get_revert(name)
72
+ if default_value != null:
73
+ described["at_default"] = value == default_value
74
+ var base: String = override_base(name)
75
+ if not base.is_empty():
76
+ described["override_of"] = base
77
+ found.append(described)
78
+
79
+ found.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return a["setting"] < b["setting"])
80
+ return {"prefix": prefix, "count": found.size(), "settings": found}
81
+
82
+
83
+ ## The setting [param setting_path] overrides for builds with one feature, as
84
+ ## `application/run/max_fps.mobile` does `application/run/max_fps`, or "" for a plain setting.
85
+ static func override_base(setting_path: String) -> String:
86
+ var dot: int = setting_path.find(".", setting_path.rfind("/") + 1)
87
+ return "" if dot == -1 else setting_path.substr(0, dot)
88
+
89
+
90
+ ## The overrides project.godot sets for [param setting_path], by feature.
91
+ static func overrides_of(setting_path: String, values: GdhSerialisation) -> Dictionary:
92
+ var overrides: Dictionary = {}
93
+ for info: Dictionary in ProjectSettings.get_property_list():
94
+ var name: String = str(info.get("name", ""))
95
+ if name.begins_with(setting_path + "."):
96
+ overrides[name.substr(setting_path.length() + 1)] = values.serialize_value(
97
+ ProjectSettings.get_setting(name)
98
+ )
99
+ return overrides
100
+
101
+
102
+ ## What a build with the feature of [param setting_path], an override project.godot does not set,
103
+ ## reads in its place.
104
+ static func overridden_by_none(setting_path: String, values: GdhSerialisation) -> String:
105
+ var base: String = override_base(setting_path)
106
+ var feature: String = setting_path.substr(base.length() + 1)
107
+ var said: String = (
108
+ (
109
+ "project.godot sets no %s override of %s, so a build with the %s feature reads %s, "
110
+ + "the value under holds"
111
+ )
112
+ % [feature, base, feature, base]
113
+ )
114
+ var others: Array[String] = []
115
+ for name: String in overrides_of(base, values).keys():
116
+ if name != feature:
117
+ others.append(name)
118
+ if others.is_empty():
119
+ return said + "."
120
+ others.sort()
121
+ return said + (", unless it also has a feature with an override: %s." % ", ".join(others))
@@ -0,0 +1 @@
1
+ uid://c1ncxrof35y6e
@@ -5,10 +5,14 @@ extends Node
5
5
  ## preloaded rather than looked up on disk at runtime, so a missing one fails to parse here
6
6
  ## instead of leaving a command silently unanswered.
7
7
 
8
+ const GdhSceneFile = preload("scene_file.gd")
8
9
  const GdhSceneTools = preload("tools/scene_tools.gd")
9
10
  const GdhSignalTools = preload("tools/signal_tools.gd")
10
11
  const GdhResourceTools = preload("tools/resource_tools.gd")
12
+ const GdhTilesetTools = preload("tools/tileset_tools.gd")
13
+ const GdhAnimationStates = preload("tools/animation_states.gd")
11
14
  const GdhAnimationTools = preload("tools/animation_tools.gd")
15
+ const GdhAnimationTracks = preload("tools/animation_tracks.gd")
12
16
  const GdhPlayTools = preload("tools/play_tools.gd")
13
17
  const GdhClassTools = preload("tools/class_tools.gd")
14
18
  const GdhSettingsTools = preload("tools/settings_tools.gd")
@@ -20,7 +24,10 @@ var _editor_plugin: EditorPlugin = null
20
24
  var _scene_tools: GdhSceneTools = null
21
25
  var _signal_tools: GdhSignalTools = null
22
26
  var _resource_tools: GdhResourceTools = null
27
+ var _tileset_tools: GdhTilesetTools = null
28
+ var _animation_states: GdhAnimationStates = null
23
29
  var _animation_tools: GdhAnimationTools = null
30
+ var _animation_tracks: GdhAnimationTracks = null
24
31
  var _play_tools: GdhPlayTools = null
25
32
  var _class_tools: GdhClassTools = null
26
33
  var _settings_tools: GdhSettingsTools = null
@@ -36,7 +43,10 @@ func set_editor_plugin(plugin: EditorPlugin) -> void:
36
43
  _init_tools()
37
44
  _scene_tools.set_editor_plugin(plugin)
38
45
  _resource_tools.set_editor_plugin(plugin)
46
+ _tileset_tools.set_editor_plugin(plugin)
39
47
  _animation_tools.set_editor_plugin(plugin)
48
+ _animation_states.set_editor_plugin(plugin)
49
+ _animation_tracks.set_editor_plugin(plugin)
40
50
  _play_tools.set_editor_plugin(plugin)
41
51
  _class_tools.set_editor_plugin(plugin)
42
52
  _filesystem_tools.set_editor_plugin(plugin)
@@ -60,10 +70,22 @@ func _init_tools() -> void:
60
70
  _resource_tools.name = "ResourceTools"
61
71
  add_child(_resource_tools)
62
72
 
73
+ _tileset_tools = GdhTilesetTools.new()
74
+ _tileset_tools.name = "TilesetTools"
75
+ add_child(_tileset_tools)
76
+
63
77
  _animation_tools = GdhAnimationTools.new()
64
78
  _animation_tools.name = "AnimationTools"
65
79
  add_child(_animation_tools)
66
80
 
81
+ _animation_states = GdhAnimationStates.new()
82
+ _animation_states.name = "AnimationStates"
83
+ add_child(_animation_states)
84
+
85
+ _animation_tracks = GdhAnimationTracks.new()
86
+ _animation_tracks.name = "AnimationTracks"
87
+ add_child(_animation_tracks)
88
+
67
89
  _play_tools = GdhPlayTools.new()
68
90
  _play_tools.name = "PlayTools"
69
91
  add_child(_play_tools)
@@ -94,6 +116,7 @@ func _init_tools() -> void:
94
116
  "reparent_node": [_scene_tools, "reparent_node"],
95
117
  "set_node_properties": [_scene_tools, "set_node_properties"],
96
118
  "get_node_properties": [_scene_tools, "get_node_properties"],
119
+ "bake_navigation": [_scene_tools, "bake_navigation"],
97
120
  "save_scene": [_scene_tools, "save_scene"],
98
121
  "connect_signal": [_signal_tools, "connect_signal"],
99
122
  "disconnect_signal": [_signal_tools, "disconnect_signal"],
@@ -104,6 +127,7 @@ func _init_tools() -> void:
104
127
  "files_moved": [_filesystem_tools, "files_moved"],
105
128
  "global_classes": [_class_tools, "global_classes"],
106
129
  "reload_script": [_class_tools, "reload_script"],
130
+ "scripts_behind": [_class_tools, "scripts_behind"],
107
131
  # Settings, for a caller that asked the editor rather than the file on disk
108
132
  "get_project_setting": [_settings_tools, "get_project_setting"],
109
133
  "adopt_project_settings": [_settings_tools, "adopt_project_settings"],
@@ -113,10 +137,11 @@ func _init_tools() -> void:
113
137
  "modify_resource": [_resource_tools, "modify_resource"],
114
138
  "read_resource": [_resource_tools, "read_resource"],
115
139
  "create_shader": [_resource_tools, "create_shader"],
116
- "create_tileset": [_resource_tools, "create_tileset"],
117
- "set_tilemap_cells": [_resource_tools, "set_tilemap_cells"],
140
+ "create_tileset": [_tileset_tools, "create_tileset"],
141
+ "set_tilemap_cells": [_tileset_tools, "set_tilemap_cells"],
118
142
  "set_theme_color": [_resource_tools, "set_theme_color"],
119
143
  "set_theme_font_size": [_resource_tools, "set_theme_font_size"],
144
+ "remove_theme_item": [_resource_tools, "remove_theme_item"],
120
145
  "play_scene": [_play_tools, "play_scene"],
121
146
  "restart_editor": [_play_tools, "restart_editor"],
122
147
  "quit_editor": [_play_tools, "quit_editor"],
@@ -130,12 +155,15 @@ func _init_tools() -> void:
130
155
  "read_animations": [_animation_tools, "read_animations"],
131
156
  "read_animation_states": [_animation_tools, "read_animation_states"],
132
157
  "create_animation": [_animation_tools, "create_animation"],
133
- "add_animation_track": [_animation_tools, "add_animation_track"],
158
+ "add_animation_track": [_animation_tracks, "add_animation_track"],
159
+ "remove_animation_track": [_animation_tracks, "remove_animation_track"],
134
160
  "set_animation": [_animation_tools, "set_animation"],
135
161
  "rename_animation": [_animation_tools, "rename_animation"],
136
162
  "remove_animation": [_animation_tools, "remove_animation"],
137
- "add_animation_state": [_animation_tools, "add_animation_state"],
138
- "connect_animation_states": [_animation_tools, "connect_animation_states"],
163
+ "add_animation_state": [_animation_states, "add_animation_state"],
164
+ "connect_animation_states": [_animation_states, "connect_animation_states"],
165
+ "remove_animation_state": [_animation_states, "remove_animation_state"],
166
+ "disconnect_animation_states": [_animation_states, "disconnect_animation_states"],
139
167
  }
140
168
 
141
169
 
@@ -150,8 +178,13 @@ func execute_tool(tool_name: String, args: Dictionary) -> Dictionary:
150
178
  if not node.has_method(method):
151
179
  return {"ok": false, "error": "Tool method not found: %s.%s" % [node.name, method]}
152
180
 
181
+ GdhSceneFile.also_saved.clear()
153
182
  var result: Variant = node.call(method, args)
154
183
  if result is Dictionary:
155
- return result
184
+ var answered: Dictionary = result
185
+ if not GdhSceneFile.also_saved.is_empty():
186
+ answered["alsoSaved"] = GdhSceneFile.also_saved.duplicate(true)
187
+ GdhSceneFile.also_saved.clear()
188
+ return answered
156
189
 
157
190
  return {"ok": false, "error": "Invalid tool result from: " + tool_name}
@@ -0,0 +1,263 @@
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"] = GdhAnimationReads.missing_animation(
62
+ player, scene.path_of(player), animation_name
63
+ )
64
+ var files: Dictionary = machine[2]
65
+ return _finish(scene, Callable(), files, answer)
66
+
67
+
68
+ # =============================================================================
69
+ # connect_animation_states
70
+ # =============================================================================
71
+ func connect_animation_states(args: Dictionary) -> Dictionary:
72
+ var from_state: String = str(args.get("fromState", ""))
73
+ var to_state: String = str(args.get("toState", ""))
74
+ var transition_type: String = str(args.get("transitionType", "immediate"))
75
+ var advance_condition: String = str(args.get("advanceCondition", ""))
76
+ # Auto where it is the only mode that does what was asked: a condition is checked only on an auto
77
+ # transition, and a machine leaves Start only through one. Written as the default enabled, both
78
+ # did nothing, and the machine sat in Start with its conditions true.
79
+ var auto: bool = not advance_condition.is_empty() or from_state == "Start"
80
+ var advance_mode: String = str(args.get("advanceMode", "auto" if auto else "enabled"))
81
+
82
+ if from_state.is_empty() or to_state.is_empty():
83
+ return {"ok": false, "error": "Missing fromState or toState"}
84
+ var advances: Dictionary = {
85
+ "auto": AnimationNodeStateMachineTransition.ADVANCE_MODE_AUTO,
86
+ "enabled": AnimationNodeStateMachineTransition.ADVANCE_MODE_ENABLED,
87
+ "disabled": AnimationNodeStateMachineTransition.ADVANCE_MODE_DISABLED,
88
+ }
89
+ if not advances.has(advance_mode):
90
+ return {
91
+ "ok": false,
92
+ "error": "Unsupported advanceMode: %s; it is auto, enabled or disabled" % advance_mode
93
+ }
94
+ if not advance_condition.is_empty() and advance_mode != "auto":
95
+ return {
96
+ "ok": false,
97
+ "error":
98
+ (
99
+ (
100
+ "advanceCondition %s would never advance a transition whose advanceMode is %s: the engine"
101
+ + " checks a condition only on an auto one. Leave advanceMode out, or give auto"
102
+ )
103
+ % [advance_condition, advance_mode]
104
+ )
105
+ }
106
+ var modes: Dictionary = {
107
+ "sync": AnimationNodeStateMachineTransition.SWITCH_MODE_SYNC,
108
+ "at_end": AnimationNodeStateMachineTransition.SWITCH_MODE_AT_END,
109
+ "immediate": AnimationNodeStateMachineTransition.SWITCH_MODE_IMMEDIATE,
110
+ }
111
+ if not modes.has(transition_type):
112
+ return {"ok": false, "error": "Unsupported transitionType: " + transition_type}
113
+ var transition: AnimationNodeStateMachineTransition = AnimationNodeStateMachineTransition.new()
114
+ transition.switch_mode = (
115
+ GdhRead.as_int(modes[transition_type]) as AnimationNodeStateMachineTransition.SwitchMode
116
+ )
117
+ transition.advance_mode = (
118
+ GdhRead.as_int(advances[advance_mode]) as AnimationNodeStateMachineTransition.AdvanceMode
119
+ )
120
+ if not advance_condition.is_empty():
121
+ transition.advance_condition = StringName(advance_condition)
122
+
123
+ var found: Array = _machine_at(args)
124
+ if found[1] == null:
125
+ return found[3]
126
+ var scene: GdhSceneFile = found[0]
127
+ var sm: AnimationNodeStateMachine = found[1]
128
+ var problem: String = _transition_problem(sm, from_state, to_state)
129
+ if not problem.is_empty():
130
+ return scene.refuse(problem)
131
+
132
+ sm.add_transition(StringName(from_state), StringName(to_state), transition)
133
+ if not sm.has_transition(StringName(from_state), StringName(to_state)):
134
+ return scene.refuse("The state machine did not take the transition.")
135
+ var files: Dictionary = found[2]
136
+ return _finish(
137
+ scene,
138
+ Callable(),
139
+ files,
140
+ {"ok": true, "from": from_state, "to": to_state, "advanceMode": advance_mode}
141
+ )
142
+
143
+
144
+ ## Why [param sm] cannot take a transition from [param from_state] to [param to_state], or "".
145
+ ## Each is a condition add_transition fails on with only a line in the editor's log, and the
146
+ ## transition was answered as made.
147
+ func _transition_problem(sm: AnimationNodeStateMachine, from_state: String, to_state: String) -> String:
148
+ for state: String in [from_state, to_state]:
149
+ if not sm.has_node(StringName(state)):
150
+ return _no_state(sm, state)
151
+ if from_state == to_state:
152
+ return "A transition needs two different states."
153
+ if from_state == "End":
154
+ return "No transition can leave End."
155
+ if to_state == "Start":
156
+ return "No transition can arrive at Start."
157
+ if sm.has_transition(StringName(from_state), StringName(to_state)):
158
+ return "There is already a transition from %s to %s." % [from_state, to_state]
159
+ return ""
160
+
161
+
162
+ ## The refusal for [param state_name], which [param sm] has no state of: with the nearest names it
163
+ ## has, or else every state, since a machine has a handful.
164
+ static func _no_state(sm: AnimationNodeStateMachine, state_name: String) -> String:
165
+ var said: String = "The state machine has no state named %s" % state_name
166
+ var states: PackedStringArray = []
167
+ for state: StringName in sm.get_node_list():
168
+ var _listed: bool = states.append(str(state))
169
+ var close: PackedStringArray = GdhRead.nearest(state_name, states)
170
+ if not close.is_empty():
171
+ return (
172
+ "%s; the nearest it has %s %s." % [said, "is" if close.size() == 1 else "are", ", ".join(close)]
173
+ )
174
+ return "%s; its states are %s." % [said, ", ".join(states)]
175
+
176
+
177
+ ## The state machine [param args] names, as [scene, machine, files, refusal, tree]: the machine is
178
+ ## null and the refusal says why when the scene, the tree or the machine is not there.
179
+ func _machine_at(args: Dictionary) -> Array:
180
+ var scene_path: String = GdhSceneFile.res_path("", str(args.get("scenePath", "")))
181
+ var anim_tree_path: String = str(args.get("animTreePath", ""))
182
+ if scene_path.strip_edges() == "res://":
183
+ return [null, null, {}, {"ok": false, "error": "Missing scenePath"}]
184
+ if anim_tree_path.is_empty():
185
+ return [null, null, {}, {"ok": false, "error": "Missing animTreePath"}]
186
+ var opened: Array = _open_at(scene_path, anim_tree_path, "AnimationTree")
187
+ var scene: GdhSceneFile = opened[0]
188
+ if opened[1] == null:
189
+ return [scene, null, {}, opened[2]]
190
+ var anim_tree: AnimationTree = opened[1]
191
+ var machine: Array = _machine(scene, anim_tree, str(args.get("stateMachinePath", "")))
192
+ if machine[0] == null:
193
+ return [scene, null, {}, scene.refuse(str(machine[2]))]
194
+ return [scene, machine[0], machine[1], {}, anim_tree]
195
+
196
+
197
+ # =============================================================================
198
+ # remove_animation_state, disconnect_animation_states
199
+ # =============================================================================
200
+ ## Takes a state out of its state machine with every transition leaving or reaching it, which the
201
+ ## engine drops with the state, and names those under transitionsRemoved. Start and End are part of
202
+ ## every state machine.
203
+ func remove_animation_state(args: Dictionary) -> Dictionary:
204
+ var state_name: String = str(args.get("stateName", ""))
205
+ if state_name.is_empty():
206
+ return {"ok": false, "error": "Missing stateName"}
207
+ var found: Array = _machine_at(args)
208
+ if found[1] == null:
209
+ return found[3]
210
+ var scene: GdhSceneFile = found[0]
211
+ var sm: AnimationNodeStateMachine = found[1]
212
+ if state_name in ["Start", "End"]:
213
+ return scene.refuse("%s is part of every state machine, so it cannot be removed." % state_name)
214
+ if not sm.has_node(StringName(state_name)):
215
+ return scene.refuse(_no_state(sm, state_name))
216
+ var dropped: Array = []
217
+ for index: int in sm.get_transition_count():
218
+ var from: String = str(sm.get_transition_from(index))
219
+ var to: String = str(sm.get_transition_to(index))
220
+ if state_name in [from, to]:
221
+ dropped.append({"from": from, "to": to})
222
+ sm.remove_node(StringName(state_name))
223
+ if sm.has_node(StringName(state_name)):
224
+ return scene.refuse("The state machine did not let go of %s." % state_name)
225
+ for index: int in sm.get_transition_count():
226
+ if state_name in [str(sm.get_transition_from(index)), str(sm.get_transition_to(index))]:
227
+ return scene.refuse(
228
+ "The state machine kept a transition of %s after the state went." % state_name
229
+ )
230
+ var files: Dictionary = found[2]
231
+ return _finish(
232
+ scene, Callable(), files, {"ok": true, "removed": state_name, "transitionsRemoved": dropped}
233
+ )
234
+
235
+
236
+ ## Takes out the transition from one state to another.
237
+ func disconnect_animation_states(args: Dictionary) -> Dictionary:
238
+ var from_state: String = str(args.get("fromState", ""))
239
+ var to_state: String = str(args.get("toState", ""))
240
+ if from_state.is_empty() or to_state.is_empty():
241
+ return {"ok": false, "error": "Missing fromState or toState"}
242
+ var found: Array = _machine_at(args)
243
+ if found[1] == null:
244
+ return found[3]
245
+ var scene: GdhSceneFile = found[0]
246
+ var sm: AnimationNodeStateMachine = found[1]
247
+ for state: String in [from_state, to_state]:
248
+ if not sm.has_node(StringName(state)):
249
+ return scene.refuse(_no_state(sm, state))
250
+ if not sm.has_transition(StringName(from_state), StringName(to_state)):
251
+ var leaving: PackedStringArray = []
252
+ for index: int in sm.get_transition_count():
253
+ if str(sm.get_transition_from(index)) == from_state:
254
+ var _listed: bool = leaving.append(str(sm.get_transition_to(index)))
255
+ var said: String = "There is no transition from %s to %s" % [from_state, to_state]
256
+ if leaving.is_empty():
257
+ return scene.refuse("%s; none leaves %s." % [said, from_state])
258
+ return scene.refuse("%s; the ones leaving %s go to %s." % [said, from_state, ", ".join(leaving)])
259
+ sm.remove_transition(StringName(from_state), StringName(to_state))
260
+ if sm.has_transition(StringName(from_state), StringName(to_state)):
261
+ return scene.refuse("The state machine did not let go of the transition.")
262
+ var files: Dictionary = found[2]
263
+ return _finish(scene, Callable(), files, {"ok": true, "removed": {"from": from_state, "to": to_state}})