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,427 @@
1
+ @tool
2
+ extends Node
3
+
4
+ ## Resource files, shaders, tilesets and themes, written through the open editor.
5
+
6
+ # Shader templates. Written as real multi-line source rather than escaped one-liners so that
7
+ # what ends up in the .gdshader can be read here. The %s is the shader type.
8
+ const SHADER_EMPTY: String = """shader_type %s;
9
+
10
+ void fragment() {
11
+ }
12
+ """
13
+
14
+ const SHADER_BASIC: String = """shader_type %s;
15
+
16
+ void fragment() {
17
+ COLOR = vec4(1.0);
18
+ }
19
+ """
20
+
21
+ const SHADER_COLOR_SHIFT: String = """shader_type %s;
22
+
23
+ uniform vec4 color_shift : source_color = vec4(0.1, 0.0, 0.2, 0.0);
24
+
25
+ void fragment() {
26
+ vec4 base = texture(TEXTURE, UV);
27
+ COLOR = vec4(clamp(base.rgb + color_shift.rgb, 0.0, 1.0), base.a);
28
+ }
29
+ """
30
+
31
+ const SHADER_OUTLINE: String = """shader_type %s;
32
+
33
+ uniform vec4 outline_color : source_color = vec4(0.0, 0.0, 0.0, 1.0);
34
+ uniform float outline_width : hint_range(0.0, 8.0) = 1.0;
35
+
36
+ void fragment() {
37
+ vec2 px = TEXTURE_PIXEL_SIZE * outline_width;
38
+ float a = texture(TEXTURE, UV).a;
39
+ float edge = max(
40
+ max(texture(TEXTURE, UV + vec2(px.x, 0.0)).a, texture(TEXTURE, UV - vec2(px.x, 0.0)).a),
41
+ max(texture(TEXTURE, UV + vec2(0.0, px.y)).a, texture(TEXTURE, UV - vec2(0.0, px.y)).a)
42
+ );
43
+ vec4 base = texture(TEXTURE, UV);
44
+ COLOR = mix(outline_color * edge, base, a);
45
+ }
46
+ """
47
+
48
+ var _editor_plugin: EditorPlugin = null
49
+
50
+
51
+ func set_editor_plugin(plugin: EditorPlugin) -> void:
52
+ _editor_plugin = plugin
53
+
54
+
55
+ func _ensure_res_path(path: String) -> String:
56
+ if path.begins_with("res://"):
57
+ return path
58
+ if path.begins_with("/"):
59
+ var project_abs: String = ProjectSettings.globalize_path("res://")
60
+ if path.begins_with(project_abs):
61
+ var rel: String = path.substr(project_abs.length())
62
+ return "res://" + rel
63
+ return "res://" + path
64
+
65
+
66
+ func _refresh_filesystem() -> void:
67
+ if _editor_plugin:
68
+ EditorInterface.get_resource_filesystem().scan()
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
+ "Vector2i":
86
+ return Vector2i(fields.get("x", 0), fields.get("y", 0))
87
+ "Vector3i":
88
+ return Vector3i(fields.get("x", 0), fields.get("y", 0), fields.get("z", 0))
89
+ "Rect2":
90
+ return Rect2(
91
+ fields.get("x", 0),
92
+ fields.get("y", 0),
93
+ fields.get("width", 0),
94
+ fields.get("height", 0)
95
+ )
96
+ "NodePath":
97
+ return NodePath(fields.get("path", ""))
98
+ if typeof(value) == TYPE_ARRAY:
99
+ var result: Array = []
100
+ for item: Variant in value:
101
+ result.append(_parse_value(item))
102
+ return result
103
+ return value
104
+
105
+
106
+ func _set_resource_properties(resource: Resource, properties: Variant) -> void:
107
+ var props: Dictionary = _parse_properties_dict(properties)
108
+ for key: Variant in props:
109
+ var val: Variant = _parse_value(props[key])
110
+ resource.set(key, val)
111
+
112
+
113
+ func _parse_properties_dict(raw: Variant) -> Dictionary:
114
+ if typeof(raw) == TYPE_DICTIONARY:
115
+ return raw
116
+ if typeof(raw) == TYPE_STRING and raw != "":
117
+ var json: JSON = JSON.new()
118
+ if json.parse(raw) == OK and typeof(json.data) == TYPE_DICTIONARY:
119
+ return json.data
120
+ return {}
121
+
122
+
123
+ ## The theme on disk at this path, or null when there is not one.
124
+ ##
125
+ ## Read past the cache, because the caller saves what it gets back and the cached copy is the
126
+ ## one an earlier call already edited. A miss answers null rather than a blank Theme: writing
127
+ ## one over a path holding something else, or holding nothing, is inventing a resource rather
128
+ ## than editing one, and it reads as a success either way.
129
+ func _load_theme(theme_path: String) -> Theme:
130
+ if not ResourceLoader.exists(theme_path, "Theme"):
131
+ return null
132
+ var loaded: Resource = ResourceLoader.load(theme_path, "Theme", ResourceLoader.CACHE_MODE_IGNORE)
133
+ if loaded is Theme:
134
+ return loaded
135
+ return null
136
+
137
+
138
+ func _save_scene_root(root: Node, scene_path: String) -> Error:
139
+ var packed: PackedScene = PackedScene.new()
140
+ var pack_result: Error = packed.pack(root)
141
+ if pack_result != OK:
142
+ return pack_result
143
+ return ResourceSaver.save(packed, scene_path)
144
+
145
+
146
+ func create_resource(args: Dictionary) -> Dictionary:
147
+ var res_path: String = _ensure_res_path(str(args.get("resourcePath", "")))
148
+ var resource_type: String = str(args.get("resourceType", "Resource"))
149
+ if res_path == "res://":
150
+ return {"ok": false, "error": "resourcePath is required"}
151
+
152
+ var instance: Variant = ClassDB.instantiate(resource_type)
153
+ if instance == null or not (instance is Resource):
154
+ return {"ok": false, "error": "Failed to instantiate resource type", "resourceType": resource_type}
155
+ var resource: Resource = instance
156
+
157
+ var script_path: String = str(args.get("script", ""))
158
+ if script_path != "":
159
+ var script_obj: Resource = load(_ensure_res_path(script_path))
160
+ if script_obj:
161
+ resource.set_script(script_obj)
162
+
163
+ if args.has("properties"):
164
+ _set_resource_properties(resource, args.get("properties"))
165
+
166
+ var save_result: Error = ResourceSaver.save(resource, res_path)
167
+ if save_result != OK:
168
+ return {"ok": false, "error": "Failed to save resource", "code": save_result}
169
+
170
+ _refresh_filesystem()
171
+ return {"ok": true, "resourcePath": res_path, "resourceType": args.get("resourceType")}
172
+
173
+
174
+ func modify_resource(args: Dictionary) -> Dictionary:
175
+ var res_path: String = _ensure_res_path(str(args.get("resourcePath", "")))
176
+ if res_path == "res://":
177
+ return {"ok": false, "error": "resourcePath is required"}
178
+
179
+ var resource: Resource = load(res_path)
180
+ if resource == null:
181
+ return {"ok": false, "error": "Resource not found", "resourcePath": res_path}
182
+
183
+ _set_resource_properties(resource, args.get("properties", ""))
184
+ var save_result: Error = ResourceSaver.save(resource, res_path)
185
+ if save_result != OK:
186
+ return {"ok": false, "error": "Failed to save resource", "code": save_result}
187
+
188
+ _refresh_filesystem()
189
+ return {"ok": true, "resourcePath": res_path}
190
+
191
+
192
+ func create_shader(args: Dictionary) -> Dictionary:
193
+ var shader_path: String = _ensure_res_path(str(args.get("shaderPath", "")))
194
+ var shader_type: String = str(args.get("shaderType", "canvas_item"))
195
+ if shader_path == "res://":
196
+ return {"ok": false, "error": "shaderPath is required"}
197
+
198
+ var code: String = str(args.get("code", ""))
199
+ var template: String = str(args.get("template", ""))
200
+
201
+ if code == "":
202
+ match template:
203
+ "basic":
204
+ code = SHADER_BASIC % shader_type
205
+ "color_shift":
206
+ code = SHADER_COLOR_SHIFT % shader_type
207
+ "outline":
208
+ code = SHADER_OUTLINE % shader_type
209
+ _:
210
+ code = SHADER_EMPTY % shader_type
211
+
212
+ var file: FileAccess = FileAccess.open(shader_path, FileAccess.WRITE)
213
+ if file == null:
214
+ return {"ok": false, "error": "Failed to open shader file for writing", "shaderPath": shader_path}
215
+ file.store_string(code)
216
+ file.close()
217
+
218
+ _refresh_filesystem()
219
+ return {"ok": true, "shaderPath": shader_path, "shaderType": shader_type}
220
+
221
+
222
+ func create_tileset(args: Dictionary) -> Dictionary:
223
+ var tileset_path: String = _ensure_res_path(str(args.get("tilesetPath", "")))
224
+ if tileset_path == "res://":
225
+ return {"ok": false, "error": "tilesetPath is required"}
226
+
227
+ var tileset: TileSet = TileSet.new()
228
+ var sources: Variant = args.get("sources", [])
229
+ if typeof(sources) != TYPE_ARRAY:
230
+ sources = []
231
+
232
+ for entry: Variant in sources:
233
+ if typeof(entry) != TYPE_DICTIONARY:
234
+ continue
235
+ var source: Dictionary = entry
236
+ var atlas: TileSetAtlasSource = TileSetAtlasSource.new()
237
+ var tex_path: String = _ensure_res_path(str(source.get("texture", "")))
238
+ var tex: Texture2D = load(tex_path) as Texture2D
239
+ if tex == null:
240
+ continue
241
+ atlas.texture = tex
242
+
243
+ var tile_size: Dictionary = source.get("tileSize", {})
244
+ atlas.texture_region_size = Vector2i(int(tile_size.get("x", 0)), int(tile_size.get("y", 0)))
245
+
246
+ if source.has("separation"):
247
+ var sep: Dictionary = source.get("separation", {})
248
+ atlas.separation = Vector2i(int(sep.get("x", 0)), int(sep.get("y", 0)))
249
+
250
+ if source.has("offset"):
251
+ var off: Dictionary = source.get("offset", {})
252
+ atlas.margins = Vector2i(int(off.get("x", 0)), int(off.get("y", 0)))
253
+
254
+ tileset.add_source(atlas)
255
+
256
+ var save_result: Error = ResourceSaver.save(tileset, tileset_path)
257
+ if save_result != OK:
258
+ return {"ok": false, "error": "Failed to save TileSet", "code": save_result}
259
+
260
+ _refresh_filesystem()
261
+ return {"ok": true, "tilesetPath": tileset_path}
262
+
263
+
264
+ ## The source ids a tile set holds, for saying what a cell could have named instead.
265
+ func _source_ids(tile_set: TileSet) -> PackedInt32Array:
266
+ var ids: PackedInt32Array = PackedInt32Array()
267
+ for index: int in tile_set.get_source_count():
268
+ ids.append(tile_set.get_source_id(index))
269
+ return ids
270
+
271
+
272
+ func set_tilemap_cells(args: Dictionary) -> Dictionary:
273
+ var scene_path: String = _ensure_res_path(str(args.get("scenePath", "")))
274
+ var node_path: String = str(args.get("tilemapNodePath", ""))
275
+ if scene_path == "res://":
276
+ return {"ok": false, "error": "scenePath is required"}
277
+
278
+ var scene_res: PackedScene = load(scene_path) as PackedScene
279
+ if scene_res == null:
280
+ return {"ok": false, "error": "Scene not found", "scenePath": scene_path}
281
+
282
+ var root: Node = scene_res.instantiate()
283
+ if root == null:
284
+ return {"ok": false, "error": "Failed to instantiate scene"}
285
+
286
+ var tilemap: TileMap = null
287
+ if node_path == "." or node_path == "":
288
+ tilemap = root as TileMap
289
+ else:
290
+ tilemap = root.get_node_or_null(node_path) as TileMap
291
+
292
+ if tilemap == null:
293
+ root.queue_free()
294
+ return {"ok": false, "error": "TileMap node not found", "tilemapNodePath": node_path}
295
+
296
+ var layer: int = int(args.get("layer", 0))
297
+ var cells: Variant = args.get("cells", [])
298
+ # Refused rather than skipped: placing nothing and answering success is indistinguishable
299
+ # from placing everything, and the caller only finds out by opening the scene.
300
+ if typeof(cells) != TYPE_ARRAY:
301
+ root.queue_free()
302
+ return {"ok": false, "error": "cells must be an array of cells to place"}
303
+
304
+ var placed: Array = cells
305
+ if placed.is_empty():
306
+ root.queue_free()
307
+ return {"ok": false, "error": "cells is empty, so there is nothing to place"}
308
+
309
+ # set_cell stores a cell whatever source id it is given, and a tile set without that source
310
+ # simply draws nothing, so the source is checked here: the alternative is a call that reports
311
+ # placing tiles and a scene that shows none.
312
+ var tile_set: TileSet = tilemap.tile_set
313
+ if tile_set == null:
314
+ root.queue_free()
315
+ return {"ok": false, "error": "The TileMap has no TileSet, so no cell can name a source"}
316
+
317
+ var written: int = 0
318
+ for entry: Variant in placed:
319
+ if typeof(entry) != TYPE_DICTIONARY:
320
+ root.queue_free()
321
+ return {
322
+ "ok": false, "error": "every cell must be an object with coords, sourceId and atlasCoords"
323
+ }
324
+ var cell: Dictionary = entry
325
+ var source_id: int = int(cell.get("sourceId", -1))
326
+ if not tile_set.has_source(source_id):
327
+ root.queue_free()
328
+ return {
329
+ "ok": false,
330
+ "error": "The TileSet has no source %d; it has %s" % [source_id, _source_ids(tile_set)]
331
+ }
332
+
333
+ var coords: Dictionary = cell.get("coords", {})
334
+ var atlas_coords: Dictionary = cell.get("atlasCoords", {})
335
+ var at: Vector2i = Vector2i(int(coords.get("x", 0)), int(coords.get("y", 0)))
336
+ tilemap.set_cell(
337
+ layer,
338
+ at,
339
+ source_id,
340
+ Vector2i(int(atlas_coords.get("x", 0)), int(atlas_coords.get("y", 0))),
341
+ int(cell.get("alternativeTile", 0))
342
+ )
343
+ if tilemap.get_cell_source_id(layer, at) != -1:
344
+ written += 1
345
+
346
+ var save_result: Error = _save_scene_root(root, scene_path)
347
+ root.queue_free()
348
+ if save_result != OK:
349
+ return {"ok": false, "error": "Failed to save scene", "code": save_result}
350
+
351
+ _refresh_filesystem()
352
+ return {"ok": true, "layer": layer, "placed": written, "requested": placed.size()}
353
+
354
+
355
+ func set_theme_color(args: Dictionary) -> Dictionary:
356
+ var theme_path: String = _ensure_res_path(str(args.get("themePath", "")))
357
+ if theme_path == "res://":
358
+ return {"ok": false, "error": "themePath is required"}
359
+
360
+ var color_name: String = str(args.get("colorName", ""))
361
+ var control_type: String = str(args.get("controlType", ""))
362
+ if color_name.is_empty() or control_type.is_empty():
363
+ return {"ok": false, "error": "colorName and controlType are required"}
364
+
365
+ var theme: Theme = _load_theme(theme_path)
366
+ if theme == null:
367
+ return {"ok": false, "error": "No Theme at %s. Create one there first." % theme_path}
368
+
369
+ var c: Dictionary = args.get("color", {})
370
+ var color: Color = Color(
371
+ float(c.get("r", 1.0)), float(c.get("g", 1.0)), float(c.get("b", 1.0)), float(c.get("a", 1.0))
372
+ )
373
+ theme.set_color(color_name, control_type, color)
374
+
375
+ var save_result: Error = ResourceSaver.save(theme, theme_path)
376
+ if save_result != OK:
377
+ return {"ok": false, "error": "Failed to save theme", "code": save_result}
378
+
379
+ _refresh_filesystem()
380
+
381
+ var saved: Theme = _load_theme(theme_path)
382
+ if saved == null or not saved.has_color(color_name, control_type):
383
+ return {"ok": false, "error": "The theme saved without %s/%s" % [control_type, color_name]}
384
+
385
+ var stored: Color = saved.get_color(color_name, control_type)
386
+ return {
387
+ "ok": true,
388
+ "themePath": theme_path,
389
+ "controlType": control_type,
390
+ "colorName": color_name,
391
+ "color": {"r": stored.r, "g": stored.g, "b": stored.b, "a": stored.a}
392
+ }
393
+
394
+
395
+ func set_theme_font_size(args: Dictionary) -> Dictionary:
396
+ var theme_path: String = _ensure_res_path(str(args.get("themePath", "")))
397
+ if theme_path == "res://":
398
+ return {"ok": false, "error": "themePath is required"}
399
+
400
+ var font_size_name: String = str(args.get("fontSizeName", ""))
401
+ var control_type: String = str(args.get("controlType", ""))
402
+ if font_size_name.is_empty() or control_type.is_empty():
403
+ return {"ok": false, "error": "fontSizeName and controlType are required"}
404
+
405
+ var theme: Theme = _load_theme(theme_path)
406
+ if theme == null:
407
+ return {"ok": false, "error": "No Theme at %s. Create one there first." % theme_path}
408
+
409
+ theme.set_font_size(font_size_name, control_type, int(args.get("size", 0)))
410
+
411
+ var save_result: Error = ResourceSaver.save(theme, theme_path)
412
+ if save_result != OK:
413
+ return {"ok": false, "error": "Failed to save theme", "code": save_result}
414
+
415
+ _refresh_filesystem()
416
+
417
+ var saved: Theme = _load_theme(theme_path)
418
+ if saved == null or not saved.has_font_size(font_size_name, control_type):
419
+ return {"ok": false, "error": "The theme saved without %s/%s" % [control_type, font_size_name]}
420
+
421
+ return {
422
+ "ok": true,
423
+ "themePath": theme_path,
424
+ "controlType": control_type,
425
+ "fontSizeName": font_size_name,
426
+ "size": saved.get_font_size(font_size_name, control_type)
427
+ }