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.
Files changed (37) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +125 -0
  3. package/build/cli.js +30353 -0
  4. package/build/godot/addons/auto_reload/auto_reload.gd +89 -0
  5. package/build/godot/addons/auto_reload/plugin.cfg +6 -0
  6. package/build/godot/addons/gdharness_editor/bridge_client.gd +197 -0
  7. package/build/godot/addons/gdharness_editor/plugin.cfg +6 -0
  8. package/build/godot/addons/gdharness_editor/plugin.gd +88 -0
  9. package/build/godot/addons/gdharness_editor/tool_executor.gd +104 -0
  10. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +397 -0
  11. package/build/godot/addons/gdharness_editor/tools/play_tools.gd +85 -0
  12. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +427 -0
  13. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +885 -0
  14. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +292 -0
  15. package/build/godot/addons/gdharness_runtime/runtime_capture.gd +77 -0
  16. package/build/godot/addons/gdharness_runtime/runtime_input.gd +254 -0
  17. package/build/godot/addons/gdharness_runtime/runtime_queries.gd +283 -0
  18. package/build/godot/addons/gdharness_runtime/runtime_values.gd +169 -0
  19. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +119 -0
  20. package/build/godot/operations/audio_buses.gd +125 -0
  21. package/build/godot/operations/class_cache.gd +180 -0
  22. package/build/godot/operations/classdb_queries.gd +252 -0
  23. package/build/godot/operations/dependencies.gd +293 -0
  24. package/build/godot/operations/file_walk.gd +55 -0
  25. package/build/godot/operations/gdscript_analysis.gd +288 -0
  26. package/build/godot/operations/gdscript_authoring.gd +419 -0
  27. package/build/godot/operations/godot_operations.gd +186 -0
  28. package/build/godot/operations/import_pipeline.gd +390 -0
  29. package/build/godot/operations/input_actions.gd +237 -0
  30. package/build/godot/operations/logger.gd +33 -0
  31. package/build/godot/operations/plugins.gd +159 -0
  32. package/build/godot/operations/project_config.gd +153 -0
  33. package/build/godot/operations/project_diagnostics.gd +159 -0
  34. package/build/godot/operations/resource_files.gd +132 -0
  35. package/build/godot/operations/serialisation.gd +154 -0
  36. package/build/index.js +29252 -0
  37. 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
+ }