gdharness 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +125 -0
- package/build/cli.js +30353 -0
- package/build/godot/addons/auto_reload/auto_reload.gd +89 -0
- package/build/godot/addons/auto_reload/plugin.cfg +6 -0
- package/build/godot/addons/gdharness_editor/bridge_client.gd +197 -0
- package/build/godot/addons/gdharness_editor/plugin.cfg +6 -0
- package/build/godot/addons/gdharness_editor/plugin.gd +88 -0
- package/build/godot/addons/gdharness_editor/tool_executor.gd +104 -0
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +397 -0
- package/build/godot/addons/gdharness_editor/tools/play_tools.gd +85 -0
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +427 -0
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +885 -0
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +292 -0
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +77 -0
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +254 -0
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +283 -0
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +169 -0
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +119 -0
- package/build/godot/operations/audio_buses.gd +125 -0
- package/build/godot/operations/class_cache.gd +180 -0
- package/build/godot/operations/classdb_queries.gd +252 -0
- package/build/godot/operations/dependencies.gd +293 -0
- package/build/godot/operations/file_walk.gd +55 -0
- package/build/godot/operations/gdscript_analysis.gd +288 -0
- package/build/godot/operations/gdscript_authoring.gd +419 -0
- package/build/godot/operations/godot_operations.gd +186 -0
- package/build/godot/operations/import_pipeline.gd +390 -0
- package/build/godot/operations/input_actions.gd +237 -0
- package/build/godot/operations/logger.gd +33 -0
- package/build/godot/operations/plugins.gd +159 -0
- package/build/godot/operations/project_config.gd +153 -0
- package/build/godot/operations/project_diagnostics.gd +159 -0
- package/build/godot/operations/resource_files.gd +132 -0
- package/build/godot/operations/serialisation.gd +154 -0
- package/build/index.js +29252 -0
- package/package.json +57 -0
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
@tool
|
|
2
|
+
extends Node
|
|
3
|
+
|
|
4
|
+
## Animations and animation trees, edited in the open editor.
|
|
5
|
+
|
|
6
|
+
var _editor_plugin: EditorPlugin = null
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
func set_editor_plugin(plugin: EditorPlugin) -> void:
|
|
10
|
+
_editor_plugin = plugin
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# =============================================================================
|
|
14
|
+
# Shared helpers
|
|
15
|
+
# =============================================================================
|
|
16
|
+
func _ensure_res_path(path: String) -> String:
|
|
17
|
+
if not path.begins_with("res://"):
|
|
18
|
+
return "res://" + path
|
|
19
|
+
return path
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
func _refresh_and_reload(scene_path: String) -> void:
|
|
23
|
+
_refresh_filesystem()
|
|
24
|
+
_reload_scene_in_editor(scene_path)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
func _refresh_filesystem() -> void:
|
|
28
|
+
if _editor_plugin:
|
|
29
|
+
EditorInterface.get_resource_filesystem().scan()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
func _reload_scene_in_editor(scene_path: String) -> void:
|
|
33
|
+
if not _editor_plugin:
|
|
34
|
+
return
|
|
35
|
+
var edited: Node = EditorInterface.get_edited_scene_root()
|
|
36
|
+
if edited and edited.scene_file_path == scene_path:
|
|
37
|
+
EditorInterface.reload_scene_from_path(scene_path)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
func _load_scene(scene_path: String) -> Array:
|
|
41
|
+
if not FileAccess.file_exists(scene_path):
|
|
42
|
+
return [null, {"ok": false, "error": "Scene not found: " + scene_path}]
|
|
43
|
+
var packed: PackedScene = load(scene_path)
|
|
44
|
+
if not packed:
|
|
45
|
+
return [null, {"ok": false, "error": "Failed to load: " + scene_path}]
|
|
46
|
+
var root: Node = packed.instantiate()
|
|
47
|
+
if not root:
|
|
48
|
+
return [null, {"ok": false, "error": "Failed to instantiate: " + scene_path}]
|
|
49
|
+
return [root, {}]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
func _save_scene(scene_root: Node, scene_path: String) -> Dictionary:
|
|
53
|
+
var packed: PackedScene = PackedScene.new()
|
|
54
|
+
if packed.pack(scene_root) != OK:
|
|
55
|
+
scene_root.queue_free()
|
|
56
|
+
return {"ok": false, "error": "Failed to pack scene"}
|
|
57
|
+
if ResourceSaver.save(packed, scene_path) != OK:
|
|
58
|
+
scene_root.queue_free()
|
|
59
|
+
return {"ok": false, "error": "Failed to save scene"}
|
|
60
|
+
scene_root.queue_free()
|
|
61
|
+
_refresh_and_reload(scene_path)
|
|
62
|
+
return {}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
func _find_node(root: Node, path: String) -> Node:
|
|
66
|
+
if path == "." or path.is_empty():
|
|
67
|
+
return root
|
|
68
|
+
return root.get_node_or_null(path)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
func _parse_value(value: Variant) -> Variant:
|
|
72
|
+
if typeof(value) == TYPE_DICTIONARY:
|
|
73
|
+
var fields: Dictionary = value
|
|
74
|
+
if fields.has("type") or fields.has("_type"):
|
|
75
|
+
var t: Variant = fields.get("type", fields.get("_type", ""))
|
|
76
|
+
match t:
|
|
77
|
+
"Vector2":
|
|
78
|
+
return Vector2(fields.get("x", 0), fields.get("y", 0))
|
|
79
|
+
"Vector3":
|
|
80
|
+
return Vector3(fields.get("x", 0), fields.get("y", 0), fields.get("z", 0))
|
|
81
|
+
"Color":
|
|
82
|
+
return Color(
|
|
83
|
+
fields.get("r", 1), fields.get("g", 1), fields.get("b", 1), fields.get("a", 1)
|
|
84
|
+
)
|
|
85
|
+
if typeof(value) == TYPE_ARRAY:
|
|
86
|
+
var result: Array = []
|
|
87
|
+
for item: Variant in value:
|
|
88
|
+
result.append(_parse_value(item))
|
|
89
|
+
return result
|
|
90
|
+
return value
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
func _parse_json_maybe(value: Variant) -> Variant:
|
|
94
|
+
if typeof(value) != TYPE_STRING:
|
|
95
|
+
return value
|
|
96
|
+
var parsed: Variant = JSON.parse_string(value)
|
|
97
|
+
if parsed == null and value != "null":
|
|
98
|
+
return value
|
|
99
|
+
return parsed
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
func _parse_method_args(raw_args: Array) -> Array:
|
|
103
|
+
var parsed_args: Array = []
|
|
104
|
+
for raw_arg: Variant in raw_args:
|
|
105
|
+
var parsed: Variant = _parse_json_maybe(raw_arg)
|
|
106
|
+
parsed_args.append(_parse_value(parsed))
|
|
107
|
+
return parsed_args
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
func _get_default_animation_library(player: AnimationPlayer) -> AnimationLibrary:
|
|
111
|
+
var anim_lib: AnimationLibrary = player.get_animation_library("")
|
|
112
|
+
if anim_lib:
|
|
113
|
+
return anim_lib
|
|
114
|
+
anim_lib = AnimationLibrary.new()
|
|
115
|
+
var add_lib_err: Error = player.add_animation_library("", anim_lib)
|
|
116
|
+
if add_lib_err != OK:
|
|
117
|
+
return null
|
|
118
|
+
return anim_lib
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
## The state machine at a slash-separated path of nested state machine names, or null when
|
|
122
|
+
## any step of the path is not one. Every node in a tree of state machines is a state machine
|
|
123
|
+
## itself, so the walk asks each step for the child by name and refuses anything else.
|
|
124
|
+
func _get_state_machine(
|
|
125
|
+
anim_tree: AnimationTree, state_machine_path: String = ""
|
|
126
|
+
) -> AnimationNodeStateMachine:
|
|
127
|
+
if not anim_tree:
|
|
128
|
+
return null
|
|
129
|
+
if state_machine_path.is_empty() or state_machine_path == "root":
|
|
130
|
+
return anim_tree.tree_root as AnimationNodeStateMachine
|
|
131
|
+
|
|
132
|
+
var current: AnimationNodeStateMachine = anim_tree.tree_root as AnimationNodeStateMachine
|
|
133
|
+
for segment: String in state_machine_path.split("/", false):
|
|
134
|
+
if segment.is_empty():
|
|
135
|
+
continue
|
|
136
|
+
if current == null or not current.has_node(StringName(segment)):
|
|
137
|
+
return null
|
|
138
|
+
current = current.get_node(StringName(segment)) as AnimationNodeStateMachine
|
|
139
|
+
return current
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# =============================================================================
|
|
143
|
+
# create_animation
|
|
144
|
+
# =============================================================================
|
|
145
|
+
func create_animation(args: Dictionary) -> Dictionary:
|
|
146
|
+
var scene_path: String = _ensure_res_path(str(args.get("scenePath", "")))
|
|
147
|
+
var player_node_path: String = str(args.get("playerNodePath", "."))
|
|
148
|
+
var animation_name: String = str(args.get("animationName", ""))
|
|
149
|
+
var loop_mode_name: String = str(args.get("loopMode", "none"))
|
|
150
|
+
|
|
151
|
+
if scene_path.strip_edges() == "res://":
|
|
152
|
+
return {"ok": false, "error": "Missing scenePath"}
|
|
153
|
+
if animation_name.strip_edges().is_empty():
|
|
154
|
+
return {"ok": false, "error": "Missing animationName"}
|
|
155
|
+
|
|
156
|
+
var loaded: Array = _load_scene(scene_path)
|
|
157
|
+
var refused: Dictionary = loaded[1]
|
|
158
|
+
if not refused.is_empty():
|
|
159
|
+
return refused
|
|
160
|
+
|
|
161
|
+
var scene_root: Node = loaded[0]
|
|
162
|
+
var player: AnimationPlayer = _find_node(scene_root, player_node_path) as AnimationPlayer
|
|
163
|
+
if not player:
|
|
164
|
+
scene_root.queue_free()
|
|
165
|
+
return {"ok": false, "error": "AnimationPlayer not found at: " + player_node_path}
|
|
166
|
+
|
|
167
|
+
var anim_lib: AnimationLibrary = _get_default_animation_library(player)
|
|
168
|
+
if not anim_lib:
|
|
169
|
+
scene_root.queue_free()
|
|
170
|
+
return {"ok": false, "error": "Failed to create default AnimationLibrary"}
|
|
171
|
+
if anim_lib.has_animation(StringName(animation_name)):
|
|
172
|
+
scene_root.queue_free()
|
|
173
|
+
return {"ok": false, "error": "Animation already exists: " + animation_name}
|
|
174
|
+
|
|
175
|
+
var loop_mode: Animation.LoopMode = Animation.LOOP_NONE
|
|
176
|
+
match loop_mode_name:
|
|
177
|
+
"linear":
|
|
178
|
+
loop_mode = Animation.LOOP_LINEAR
|
|
179
|
+
"pingpong":
|
|
180
|
+
loop_mode = Animation.LOOP_PINGPONG
|
|
181
|
+
_:
|
|
182
|
+
loop_mode_name = "none"
|
|
183
|
+
loop_mode = Animation.LOOP_NONE
|
|
184
|
+
|
|
185
|
+
var anim: Animation = Animation.new()
|
|
186
|
+
anim.length = float(args.get("length", 1.0))
|
|
187
|
+
anim.loop_mode = loop_mode
|
|
188
|
+
anim.step = float(args.get("step", 0.1))
|
|
189
|
+
|
|
190
|
+
var add_err: Error = anim_lib.add_animation(StringName(animation_name), anim)
|
|
191
|
+
if add_err != OK:
|
|
192
|
+
scene_root.queue_free()
|
|
193
|
+
return {"ok": false, "error": "Failed to add animation: " + error_string(add_err)}
|
|
194
|
+
|
|
195
|
+
var save_err: Dictionary = _save_scene(scene_root, scene_path)
|
|
196
|
+
if not save_err.is_empty():
|
|
197
|
+
return save_err
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
"ok": true,
|
|
201
|
+
"animationName": animation_name,
|
|
202
|
+
"length": anim.length,
|
|
203
|
+
"loopMode": loop_mode_name,
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# =============================================================================
|
|
208
|
+
# add_animation_track
|
|
209
|
+
# =============================================================================
|
|
210
|
+
func add_animation_track(args: Dictionary) -> Dictionary:
|
|
211
|
+
var scene_path: String = _ensure_res_path(str(args.get("scenePath", "")))
|
|
212
|
+
var player_node_path: String = str(args.get("playerNodePath", "."))
|
|
213
|
+
var animation_name: String = str(args.get("animationName", ""))
|
|
214
|
+
var track: Dictionary = args.get("track", {})
|
|
215
|
+
|
|
216
|
+
if scene_path.strip_edges() == "res://":
|
|
217
|
+
return {"ok": false, "error": "Missing scenePath"}
|
|
218
|
+
if animation_name.strip_edges().is_empty():
|
|
219
|
+
return {"ok": false, "error": "Missing animationName"}
|
|
220
|
+
if track.is_empty():
|
|
221
|
+
return {"ok": false, "error": "Missing track"}
|
|
222
|
+
|
|
223
|
+
var loaded: Array = _load_scene(scene_path)
|
|
224
|
+
var refused: Dictionary = loaded[1]
|
|
225
|
+
if not refused.is_empty():
|
|
226
|
+
return refused
|
|
227
|
+
|
|
228
|
+
var scene_root: Node = loaded[0]
|
|
229
|
+
var player: AnimationPlayer = _find_node(scene_root, player_node_path) as AnimationPlayer
|
|
230
|
+
if not player:
|
|
231
|
+
scene_root.queue_free()
|
|
232
|
+
return {"ok": false, "error": "AnimationPlayer not found at: " + player_node_path}
|
|
233
|
+
|
|
234
|
+
var anim_lib: AnimationLibrary = player.get_animation_library("")
|
|
235
|
+
if not anim_lib:
|
|
236
|
+
scene_root.queue_free()
|
|
237
|
+
return {"ok": false, "error": "Default AnimationLibrary not found"}
|
|
238
|
+
|
|
239
|
+
var anim: Animation = anim_lib.get_animation(StringName(animation_name))
|
|
240
|
+
if not anim:
|
|
241
|
+
scene_root.queue_free()
|
|
242
|
+
return {"ok": false, "error": "Animation not found: " + animation_name}
|
|
243
|
+
|
|
244
|
+
var track_type: String = str(track.get("type", ""))
|
|
245
|
+
var track_idx: int = -1
|
|
246
|
+
var keyframes: Array = track.get("keyframes", [])
|
|
247
|
+
|
|
248
|
+
match track_type:
|
|
249
|
+
"property":
|
|
250
|
+
var node_path_str: String = str(track.get("nodePath", ""))
|
|
251
|
+
var prop_name: String = str(track.get("property", ""))
|
|
252
|
+
if prop_name.is_empty():
|
|
253
|
+
scene_root.queue_free()
|
|
254
|
+
return {"ok": false, "error": "track.property is required for property track"}
|
|
255
|
+
track_idx = anim.add_track(Animation.TYPE_VALUE)
|
|
256
|
+
anim.track_set_path(track_idx, NodePath(node_path_str + ":" + prop_name))
|
|
257
|
+
for entry: Variant in keyframes:
|
|
258
|
+
if typeof(entry) != TYPE_DICTIONARY:
|
|
259
|
+
continue
|
|
260
|
+
var keyframe: Dictionary = entry
|
|
261
|
+
var raw_value: Variant = keyframe.get("value")
|
|
262
|
+
var parsed_value: Variant = (
|
|
263
|
+
_parse_json_maybe(raw_value) if typeof(raw_value) == TYPE_STRING else raw_value
|
|
264
|
+
)
|
|
265
|
+
anim.track_insert_key(track_idx, float(keyframe.get("time", 0.0)), _parse_value(parsed_value))
|
|
266
|
+
|
|
267
|
+
"method":
|
|
268
|
+
var method_node_path: String = str(track.get("nodePath", ""))
|
|
269
|
+
var method_name: String = str(track.get("method", ""))
|
|
270
|
+
if method_name.is_empty():
|
|
271
|
+
scene_root.queue_free()
|
|
272
|
+
return {"ok": false, "error": "track.method is required for method track"}
|
|
273
|
+
track_idx = anim.add_track(Animation.TYPE_METHOD)
|
|
274
|
+
anim.track_set_path(track_idx, NodePath(method_node_path))
|
|
275
|
+
for entry: Variant in keyframes:
|
|
276
|
+
if typeof(entry) != TYPE_DICTIONARY:
|
|
277
|
+
continue
|
|
278
|
+
var keyframe: Dictionary = entry
|
|
279
|
+
var invocation: Dictionary = {
|
|
280
|
+
"method": method_name, "args": _parse_method_args(keyframe.get("args", []))
|
|
281
|
+
}
|
|
282
|
+
anim.track_insert_key(track_idx, float(keyframe.get("time", 0.0)), invocation)
|
|
283
|
+
|
|
284
|
+
_:
|
|
285
|
+
scene_root.queue_free()
|
|
286
|
+
return {"ok": false, "error": "Unsupported track.type: " + track_type}
|
|
287
|
+
|
|
288
|
+
var save_err: Dictionary = _save_scene(scene_root, scene_path)
|
|
289
|
+
if not save_err.is_empty():
|
|
290
|
+
return save_err
|
|
291
|
+
|
|
292
|
+
return {"ok": true, "trackType": track_type, "trackIndex": track_idx}
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
# =============================================================================
|
|
296
|
+
# add_animation_state
|
|
297
|
+
# =============================================================================
|
|
298
|
+
func add_animation_state(args: Dictionary) -> Dictionary:
|
|
299
|
+
var scene_path: String = _ensure_res_path(str(args.get("scenePath", "")))
|
|
300
|
+
var anim_tree_path: String = str(args.get("animTreePath", ""))
|
|
301
|
+
var state_name: String = str(args.get("stateName", ""))
|
|
302
|
+
var animation_name: String = str(args.get("animationName", ""))
|
|
303
|
+
var state_machine_path: String = str(args.get("stateMachinePath", ""))
|
|
304
|
+
|
|
305
|
+
if scene_path.strip_edges() == "res://":
|
|
306
|
+
return {"ok": false, "error": "Missing scenePath"}
|
|
307
|
+
if anim_tree_path.is_empty():
|
|
308
|
+
return {"ok": false, "error": "Missing animTreePath"}
|
|
309
|
+
if state_name.is_empty():
|
|
310
|
+
return {"ok": false, "error": "Missing stateName"}
|
|
311
|
+
if animation_name.is_empty():
|
|
312
|
+
return {"ok": false, "error": "Missing animationName"}
|
|
313
|
+
|
|
314
|
+
var loaded: Array = _load_scene(scene_path)
|
|
315
|
+
var refused: Dictionary = loaded[1]
|
|
316
|
+
if not refused.is_empty():
|
|
317
|
+
return refused
|
|
318
|
+
|
|
319
|
+
var scene_root: Node = loaded[0]
|
|
320
|
+
var anim_tree: AnimationTree = _find_node(scene_root, anim_tree_path) as AnimationTree
|
|
321
|
+
if not anim_tree:
|
|
322
|
+
scene_root.queue_free()
|
|
323
|
+
return {"ok": false, "error": "AnimationTree not found at: " + anim_tree_path}
|
|
324
|
+
|
|
325
|
+
var sm: AnimationNodeStateMachine = _get_state_machine(anim_tree, state_machine_path)
|
|
326
|
+
if not sm:
|
|
327
|
+
scene_root.queue_free()
|
|
328
|
+
return {"ok": false, "error": "AnimationNodeStateMachine not found"}
|
|
329
|
+
|
|
330
|
+
var anim_node: AnimationNodeAnimation = AnimationNodeAnimation.new()
|
|
331
|
+
anim_node.animation = StringName(animation_name)
|
|
332
|
+
sm.add_node(StringName(state_name), anim_node)
|
|
333
|
+
|
|
334
|
+
var save_err: Dictionary = _save_scene(scene_root, scene_path)
|
|
335
|
+
if not save_err.is_empty():
|
|
336
|
+
return save_err
|
|
337
|
+
|
|
338
|
+
return {"ok": true, "stateName": state_name, "animationName": animation_name}
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
# =============================================================================
|
|
342
|
+
# connect_animation_states
|
|
343
|
+
# =============================================================================
|
|
344
|
+
func connect_animation_states(args: Dictionary) -> Dictionary:
|
|
345
|
+
var scene_path: String = _ensure_res_path(str(args.get("scenePath", "")))
|
|
346
|
+
var anim_tree_path: String = str(args.get("animTreePath", ""))
|
|
347
|
+
var from_state: String = str(args.get("fromState", ""))
|
|
348
|
+
var to_state: String = str(args.get("toState", ""))
|
|
349
|
+
var transition_type: String = str(args.get("transitionType", "immediate"))
|
|
350
|
+
var state_machine_path: String = str(args.get("stateMachinePath", ""))
|
|
351
|
+
var advance_condition: String = str(args.get("advanceCondition", ""))
|
|
352
|
+
|
|
353
|
+
if scene_path.strip_edges() == "res://":
|
|
354
|
+
return {"ok": false, "error": "Missing scenePath"}
|
|
355
|
+
if anim_tree_path.is_empty():
|
|
356
|
+
return {"ok": false, "error": "Missing animTreePath"}
|
|
357
|
+
if from_state.is_empty() or to_state.is_empty():
|
|
358
|
+
return {"ok": false, "error": "Missing fromState or toState"}
|
|
359
|
+
|
|
360
|
+
var loaded: Array = _load_scene(scene_path)
|
|
361
|
+
var refused: Dictionary = loaded[1]
|
|
362
|
+
if not refused.is_empty():
|
|
363
|
+
return refused
|
|
364
|
+
|
|
365
|
+
var scene_root: Node = loaded[0]
|
|
366
|
+
var anim_tree: AnimationTree = _find_node(scene_root, anim_tree_path) as AnimationTree
|
|
367
|
+
if not anim_tree:
|
|
368
|
+
scene_root.queue_free()
|
|
369
|
+
return {"ok": false, "error": "AnimationTree not found at: " + anim_tree_path}
|
|
370
|
+
|
|
371
|
+
var sm: AnimationNodeStateMachine = _get_state_machine(anim_tree, state_machine_path)
|
|
372
|
+
if not sm:
|
|
373
|
+
scene_root.queue_free()
|
|
374
|
+
return {"ok": false, "error": "AnimationNodeStateMachine not found"}
|
|
375
|
+
|
|
376
|
+
var transition: AnimationNodeStateMachineTransition = AnimationNodeStateMachineTransition.new()
|
|
377
|
+
match transition_type:
|
|
378
|
+
"sync":
|
|
379
|
+
transition.switch_mode = AnimationNodeStateMachineTransition.SWITCH_MODE_SYNC
|
|
380
|
+
"at_end":
|
|
381
|
+
transition.switch_mode = AnimationNodeStateMachineTransition.SWITCH_MODE_AT_END
|
|
382
|
+
"immediate":
|
|
383
|
+
transition.switch_mode = AnimationNodeStateMachineTransition.SWITCH_MODE_IMMEDIATE
|
|
384
|
+
_:
|
|
385
|
+
scene_root.queue_free()
|
|
386
|
+
return {"ok": false, "error": "Unsupported transitionType: " + transition_type}
|
|
387
|
+
|
|
388
|
+
if not advance_condition.is_empty():
|
|
389
|
+
transition.advance_condition = StringName(advance_condition)
|
|
390
|
+
|
|
391
|
+
sm.add_transition(StringName(from_state), StringName(to_state), transition)
|
|
392
|
+
|
|
393
|
+
var save_err: Dictionary = _save_scene(scene_root, scene_path)
|
|
394
|
+
if not save_err.is_empty():
|
|
395
|
+
return save_err
|
|
396
|
+
|
|
397
|
+
return {"ok": true, "from": from_state, "to": to_state}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
@tool
|
|
2
|
+
extends Node
|
|
3
|
+
|
|
4
|
+
## Running the game from the editor, so the editor is the one debugging it.
|
|
5
|
+
##
|
|
6
|
+
## A game started as its own process is a game nothing is attached to: the editor's debug adapter
|
|
7
|
+
## has no session for it, so breakpoints, stepping and the stack are all unreachable. Asking the
|
|
8
|
+
## editor to play means the debugger it owns is holding the game, which is what makes the debug
|
|
9
|
+
## tools answer at all.
|
|
10
|
+
|
|
11
|
+
var _editor_plugin: EditorPlugin = null
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
func set_editor_plugin(plugin: EditorPlugin) -> void:
|
|
15
|
+
_editor_plugin = plugin
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
func play_scene(args: Dictionary) -> Dictionary:
|
|
19
|
+
var scene_path: String = str(args.get("scenePath", ""))
|
|
20
|
+
|
|
21
|
+
if EditorInterface.is_playing_scene():
|
|
22
|
+
EditorInterface.stop_playing_scene()
|
|
23
|
+
|
|
24
|
+
if scene_path.is_empty():
|
|
25
|
+
EditorInterface.play_main_scene()
|
|
26
|
+
else:
|
|
27
|
+
var full_path: String = scene_path if scene_path.begins_with("res://") else "res://" + scene_path
|
|
28
|
+
if not ResourceLoader.exists(full_path, "PackedScene"):
|
|
29
|
+
return {"ok": false, "error": "No scene at " + full_path}
|
|
30
|
+
EditorInterface.play_custom_scene(full_path)
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
"ok": true,
|
|
34
|
+
"playing": EditorInterface.is_playing_scene(),
|
|
35
|
+
"scenePath": EditorInterface.get_playing_scene()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
func stop_playing(_args: Dictionary) -> Dictionary:
|
|
40
|
+
var was_playing: bool = EditorInterface.is_playing_scene()
|
|
41
|
+
if was_playing:
|
|
42
|
+
EditorInterface.stop_playing_scene()
|
|
43
|
+
return {"ok": true, "wasPlaying": was_playing, "playing": EditorInterface.is_playing_scene()}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
## Restarts the editor, which is how a replaced addon is picked up.
|
|
47
|
+
##
|
|
48
|
+
## An install writes the new files under a running editor, which goes on serving the code it read
|
|
49
|
+
## at startup: the version it reports and the tools it answers are the old ones until it comes
|
|
50
|
+
## back. Scenes are saved on the way out, because the alternative is throwing away somebody's
|
|
51
|
+
## unsaved work to pick up a version.
|
|
52
|
+
func restart_editor(_args: Dictionary) -> Dictionary:
|
|
53
|
+
# Only an editor with a window, because only that one can come back as itself. The engine
|
|
54
|
+
# hands back none of the arguments it consumed: OS.get_cmdline_args() in an editor started
|
|
55
|
+
# with --headless --path <project> --lsp-port <n> answers with none of them, so a headless
|
|
56
|
+
# editor restarted by anybody comes up as a project manager with no project, holding the
|
|
57
|
+
# desktop of whoever was unlucky enough to be watching. A windowed editor was started with
|
|
58
|
+
# none of that, so Godot's own restart brings back the same editor on the same project.
|
|
59
|
+
if not DisplayServer.window_can_draw():
|
|
60
|
+
return {
|
|
61
|
+
"ok": false,
|
|
62
|
+
"error":
|
|
63
|
+
(
|
|
64
|
+
"This editor is headless, and the engine does not hand back the arguments it was "
|
|
65
|
+
+ "started with, so nothing can bring it back as the editor it is. Start it again "
|
|
66
|
+
+ "yourself."
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if EditorInterface.is_playing_scene():
|
|
71
|
+
EditorInterface.stop_playing_scene()
|
|
72
|
+
|
|
73
|
+
EditorInterface.save_all_scenes()
|
|
74
|
+
|
|
75
|
+
# Deferred so this answer is on its way out before the editor goes.
|
|
76
|
+
EditorInterface.restart_editor.call_deferred(true)
|
|
77
|
+
|
|
78
|
+
return {"ok": true, "restarting": true, "saved": true}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
func playing_status(_args: Dictionary) -> Dictionary:
|
|
82
|
+
var playing: bool = EditorInterface.is_playing_scene()
|
|
83
|
+
return {
|
|
84
|
+
"ok": true, "playing": playing, "scenePath": EditorInterface.get_playing_scene() if playing else ""
|
|
85
|
+
}
|