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
@@ -1,14 +1,17 @@
1
1
  @tool
2
2
  extends Node
3
3
 
4
- ## Resource files, shaders, tilesets and themes, written through the open editor.
4
+ ## Resource files, shaders and themes, written through the open editor.
5
5
 
6
6
  const GdhFileReads = preload("../file_reads.gd")
7
7
  const GdhHeadlessCheck = preload("../headless_check.gd")
8
+ const GdhPropertyPaths = preload("../property_paths.gd")
8
9
  const GdhPropertyValues = preload("../property_values.gd")
9
10
  const GdhRead = preload("../reading.gd")
10
11
  const GdhSame = preload("../same_values.gd")
11
12
  const GdhSceneFile = preload("../scene_file.gd")
13
+ const GdhSceneUids = preload("../scene_uids.gd")
14
+ const GdhSerialisation = preload("../serialisation.gd")
12
15
 
13
16
  # The minimal shader of a type: its declaration and the one function the engine calls for it.
14
17
  const SHADER_EMPTY: String = """shader_type %s;
@@ -20,11 +23,9 @@ const SHADER_ENTRY: Dictionary[String, String] = {
20
23
  "canvas_item": "fragment", "spatial": "fragment", "particles": "process", "sky": "sky", "fog": "fog"
21
24
  }
22
25
 
23
- ## How many cells a read of a tile map lists, enough for a level's worth and not a whole world's.
24
- const CELLS_SHOWN: int = 1000
25
-
26
26
  var _editor_plugin: EditorPlugin = null
27
27
  var _properties: GdhPropertyValues = GdhPropertyValues.new()
28
+ var _values: GdhSerialisation = GdhSerialisation.new()
28
29
 
29
30
 
30
31
  func set_editor_plugin(plugin: EditorPlugin) -> void:
@@ -74,33 +75,6 @@ func _parse_properties_dict(raw: Variant) -> Dictionary:
74
75
  return {}
75
76
 
76
77
 
77
- ## A pair of whole numbers given as {"x": 1, "y": 2} or [1, 2], as [true, Vector2i] or
78
- ## [false, why]. The server checks the arguments it is given but not what is inside them, and a
79
- ## pair given the other way went into a typed local and failed as "Invalid tool result".
80
- func _xy(value: Variant, label: String) -> Array:
81
- var x: Variant = null
82
- var y: Variant = null
83
- if value is Dictionary:
84
- var fields: Dictionary = value
85
- x = fields.get("x")
86
- y = fields.get("y")
87
- elif value is Array:
88
- var pair: Array = value
89
- if pair.size() == 2:
90
- x = pair[0]
91
- y = pair[1]
92
- for number: Variant in [x, y]:
93
- if (
94
- not (number is float or number is int)
95
- or GdhRead.as_float(number) != floorf(GdhRead.as_float(number))
96
- ):
97
- return [
98
- false,
99
- '%s must be two whole numbers, {"x": .., "y": ..}, not %s.' % [label, JSON.stringify(value)]
100
- ]
101
- return [true, Vector2i(GdhRead.as_int(x), GdhRead.as_int(y))]
102
-
103
-
104
78
  ## The theme on disk at this path, or null when there is not one.
105
79
  ##
106
80
  ## Read past the cache, because the caller saves what it gets back and the cached copy is the
@@ -123,10 +97,13 @@ func _load_theme(theme_path: String) -> Theme:
123
97
  ## Read back because a property can be set and still not be saved: one without storage, a
124
98
  ## script variable that is not exported. Both were answered as changed.
125
99
  func _save_checked(resource: Resource, path: String, names: Array, previous: Variant) -> String:
126
- var saving: Error = GdhSceneFile.save_keeping_uid(resource, path)
100
+ var carried: Dictionary = GdhHeadlessCheck.embedded_in(GdhHeadlessCheck.stored(resource).values(), {})
101
+ var saving: Error = GdhSceneUids.save_keeping_uid(resource, path)
127
102
  if saving != OK:
128
103
  return "Failed to save %s: %s" % [path, error_string(saving)]
129
- var unkept: String = _unkept_headless(resource, path, names, previous)
104
+ var unkept: String = _unkept_headless(
105
+ resource, path, names + _properties.moved_by_engine.keys(), previous, carried
106
+ )
130
107
  if not unkept.is_empty():
131
108
  return "Nothing was written: " + unkept + GdhHeadlessCheck.put_back(path, previous)
132
109
  var loaded: Resource = ResourceLoader.load(path, "", ResourceLoader.CACHE_MODE_IGNORE)
@@ -154,8 +131,11 @@ func _save_checked(resource: Resource, path: String, names: Array, previous: Var
154
131
 
155
132
  ## Outside the editor, puts back in the file just saved at [param path] what a script that is not a
156
133
  ## tool changed and the call did not ask for, [param names] being what it asked for, and answers ""
157
- ## or why that did not hold. See [GdhHeadlessCheck].
158
- func _unkept_headless(resource: Resource, path: String, names: Array, previous: Variant) -> String:
134
+ ## or why that did not hold. [param carried] is what each embedded id named before the save. See
135
+ ## [GdhHeadlessCheck].
136
+ func _unkept_headless(
137
+ resource: Resource, path: String, names: Array, previous: Variant, carried: Dictionary
138
+ ) -> String:
159
139
  if Engine.is_editor_hint():
160
140
  return ""
161
141
  var script: Script = resource.get_script()
@@ -191,14 +171,14 @@ func _unkept_headless(resource: Resource, path: String, names: Array, previous:
191
171
  resource if scripted else null,
192
172
  asked,
193
173
  reached,
194
- func() -> Error: return GdhSceneFile.save_keeping_uid(resource, path)
174
+ carried,
175
+ func() -> Error: return GdhSceneUids.save_keeping_uid(resource, path)
195
176
  )
196
177
 
197
178
 
198
179
  func create_resource(args: Dictionary) -> Dictionary:
199
180
  var res_path: String = _ensure_res_path(str(args.get("resourcePath", "")))
200
181
  var resource_type: String = str(args.get("resourceType", "Resource"))
201
- var asked_type: String = resource_type
202
182
  if res_path == "res://":
203
183
  return {"ok": false, "error": "resourcePath is required"}
204
184
  var taken: String = _taken(res_path, "resource_edit op=create")
@@ -247,16 +227,27 @@ func create_resource(args: Dictionary) -> Dictionary:
247
227
  var properties: Dictionary = _parse_properties_dict(args.get("properties", {}))
248
228
  var refused: String = _properties.write_all(resource, properties)
249
229
  if not refused.is_empty():
250
- return {"ok": false, "error": refused}
230
+ return {"ok": false, "error": GdhHeadlessCheck.setter_ran(resource, _properties.unheld, refused)}
251
231
 
252
232
  var folder: String = GdhSceneFile.make_folder_for(res_path)
253
233
  if not folder.is_empty():
254
234
  return {"ok": false, "error": folder}
255
- var answer: Dictionary = {"ok": true, "resourcePath": res_path, "resourceType": asked_type}
235
+ # As a read names it, the engine class under resourceType and the script's class beside it, since
236
+ # a create given resourceType Resource and a script answered nothing of the class it had made, and
237
+ # one given a class_name answered it where a read of the same file answers Resource.
238
+ var answer: Dictionary = {"ok": true, "resourcePath": res_path, "resourceType": resource_type}
239
+ var scripted: Variant = resource.get_script()
240
+ if scripted is Script:
241
+ var attached: Script = scripted
242
+ answer["script"] = attached.resource_path
243
+ if not attached.get_global_name().is_empty():
244
+ answer["scriptClass"] = str(attached.get_global_name())
256
245
  _properties.note_write(answer)
257
246
  var failed: String = _save_checked(resource, res_path, properties.keys(), null)
258
247
  if not failed.is_empty():
259
248
  return {"ok": false, "error": failed}
249
+ if not properties.is_empty():
250
+ answer["properties"] = _held(resource, properties.keys())
260
251
  return answer
261
252
 
262
253
 
@@ -265,7 +256,7 @@ func modify_resource(args: Dictionary) -> Dictionary:
265
256
  if res_path == "res://":
266
257
  return {"ok": false, "error": "resourcePath is required"}
267
258
  if not FileAccess.file_exists(res_path):
268
- return {"ok": false, "error": "No resource at " + res_path}
259
+ return {"ok": false, "error": "No resource at %s; op=create makes one there." % res_path}
269
260
  # The editor's copy of an imported file is rebuilt from the source on every import, and a save
270
261
  # to it fails; edited through the cache, the editor kept the changed copy after the refusal.
271
262
  if FileAccess.file_exists(res_path + ".import"):
@@ -299,6 +290,10 @@ func modify_resource(args: Dictionary) -> Dictionary:
299
290
 
300
291
  var properties: Dictionary = _parse_properties_dict(args.get("properties", ""))
301
292
  var refused: String = _properties.write_all(resource, properties)
293
+ if refused.is_empty():
294
+ refused = _holding_itself(resource, res_path, properties.keys())
295
+ else:
296
+ refused = GdhHeadlessCheck.setter_ran(resource, _properties.unheld, refused)
302
297
  if not refused.is_empty():
303
298
  return {"ok": false, "error": refused}
304
299
  var answer: Dictionary = {"ok": true, "resourcePath": res_path}
@@ -307,9 +302,39 @@ func modify_resource(args: Dictionary) -> Dictionary:
307
302
  var failed: String = _save_checked(resource, res_path, properties.keys(), previous)
308
303
  if not failed.is_empty():
309
304
  return {"ok": false, "error": failed}
305
+ answer["properties"] = _held(resource, properties.keys())
310
306
  return answer
311
307
 
312
308
 
309
+ ## Why [param resource], saved at [param path], cannot hold what [param names] were given, or "": a
310
+ ## resource cannot hold the file it is saved as. Godot saves the reference as null with only a
311
+ ## warning in its log, and the read back after the save reported the property as one the file does
312
+ ## not keep.
313
+ func _holding_itself(resource: Resource, path: String, names: Array) -> String:
314
+ for key: Variant in names:
315
+ var property: String = str(key)
316
+ for held: Resource in GdhHeadlessCheck.resources_in(_properties.read_at(resource, property)):
317
+ if held == resource or held.resource_path == path:
318
+ return (
319
+ (
320
+ "%s.%s would hold %s itself, and a resource cannot hold the file it is saved as: "
321
+ + "Godot saves the reference as null. Nothing was written."
322
+ )
323
+ % [GdhPropertyPaths.class_called(resource), property, path]
324
+ )
325
+ return ""
326
+
327
+
328
+ ## What [param resource] holds at each property or path in [param asked], once the save has been
329
+ ## checked against it: a write saying only that it was written left the caller a second call to
330
+ ## learn what the engine kept.
331
+ func _held(resource: Resource, asked: Array) -> Dictionary:
332
+ var held: Dictionary = {}
333
+ for key: Variant in asked:
334
+ held[str(key)] = _values.serialize_value(_properties.read_at(resource, str(key)))
335
+ return held
336
+
337
+
313
338
  ## What a resource file holds, read as a headless engine reads it with no editor connected.
314
339
  func read_resource(args: Dictionary) -> Dictionary:
315
340
  var res_path: String = _ensure_res_path(str(args.get("resourcePath", "")))
@@ -351,451 +376,60 @@ func create_shader(args: Dictionary) -> Dictionary:
351
376
  return {"ok": false, "error": "Failed to write the shader file " + shader_path}
352
377
 
353
378
  _written(shader_path)
354
- return {"ok": true, "shaderPath": shader_path, "shaderType": shader_type}
355
-
356
-
357
- func create_tileset(args: Dictionary) -> Dictionary:
358
- var tileset_path: String = _ensure_res_path(str(args.get("resourcePath", "")))
359
- if tileset_path == "res://":
360
- return {"ok": false, "error": "resourcePath is required"}
361
- # modify sets properties and builds no atlas, so the other path is the only way on offered.
362
- var taken: String = (
363
- ""
364
- if not FileAccess.file_exists(tileset_path)
365
- else (
366
- (
367
- "%s already exists, and create_tileset builds a whole new TileSet, which would replace "
368
- + "everything in it, the tiles a scene paints with among them. Give resourcePath a path "
369
- + "nothing is at."
370
- )
371
- % tileset_path
372
- )
373
- )
374
- if not taken.is_empty():
375
- return {"ok": false, "error": taken}
376
-
377
- var given: Variant = args.get("sources", [])
378
- if not given is Array:
379
- return {"ok": false, "error": "sources must list at least one atlas"}
380
- var sources: Array = given
381
- if sources.is_empty():
382
- return {"ok": false, "error": "sources must list at least one atlas"}
383
-
384
- var tileset: TileSet = TileSet.new()
385
- var made: Array = []
386
- for index: int in sources.size():
387
- var built: Array = _atlas(sources[index], index)
388
- if not built[0]:
389
- return {"ok": false, "error": built[1]}
390
- var atlas: TileSetAtlasSource = built[1]
391
- # The first source sets the tile set's own tile size, which is what a TileMapLayer draws
392
- # cells at; left at its default of 16 it did not match the atlas.
393
- if index == 0:
394
- tileset.tile_size = atlas.texture_region_size
395
- var added: int = tileset.add_source(atlas)
396
- if added < 0:
397
- return {
398
- "ok": false,
399
- "error": "The TileSet did not take the atlas for %s" % atlas.texture.resource_path
400
- }
401
- var entry: Dictionary = {
402
- "sourceId": added, "texture": atlas.texture.resource_path, "tiles": atlas.get_tiles_count()
403
- }
404
- var blank: Array = built[2]
405
- if not blank.is_empty():
406
- entry["leftBlank"] = blank
407
- var spare: String = built[3]
408
- if not spare.is_empty():
409
- entry["notInAnyTile"] = spare
410
- made.append(entry)
411
-
412
- var folder: String = GdhSceneFile.make_folder_for(tileset_path)
413
- if not folder.is_empty():
414
- return {"ok": false, "error": folder}
415
- var saving: Error = GdhSceneFile.save_keeping_uid(tileset, tileset_path)
416
- if saving != OK:
417
- return {"ok": false, "error": "Failed to save %s: %s" % [tileset_path, error_string(saving)]}
418
-
419
- _written(tileset_path)
420
- return {"ok": true, "tilesetPath": tileset_path, "sources": made}
421
-
422
-
423
- ## One atlas source from its description, with its tiles made, as [true, source, the grid cells left
424
- ## without a tile for being wholly transparent, a sentence on the pixels no tile covers or ""] or
425
- ## [false, why].
426
- ##
427
- ## Tiles are made in every cell of the grid that has a pixel that is not transparent, which is
428
- ## what the editor offers to do when an atlas gets a texture. Without them the atlas held no tiles,
429
- ## and every cell painted from it drew nothing. The cells passed over and the strip a tile size that
430
- ## does not divide the picture leaves are said, since a count alone left a caller to work out which.
431
- func _atlas(entry: Variant, index: int) -> Array:
432
- if not entry is Dictionary:
433
- return [false, "sources[%d] must be an object with texture and tileSize" % index]
434
- var source: Dictionary = entry
435
- var texture_path: String = str(source.get("texture", ""))
436
- if not texture_path.begins_with("res://") and not texture_path.begins_with("uid://"):
437
- texture_path = _ensure_res_path(texture_path)
438
- # Contained here as well as on the server: a relative path climbing out of the project became
439
- # res://../../ and was loaded.
440
- if texture_path.split("/").has(".."):
441
- return [false, "sources[%d].texture leaves the project: %s" % [index, texture_path]]
442
- if not ResourceLoader.exists(texture_path):
443
- # An image the engine has not imported loads as nothing, where "no texture" would send a caller
444
- # looking for a file that is there.
445
- if FileAccess.file_exists(texture_path) and not FileAccess.file_exists(texture_path + ".import"):
446
- return [
447
- false,
448
- (
449
- (
450
- "sources[%d].texture: %s has not been imported, so the engine has no texture for it "
451
- + "yet. project_import reimport with resourcePath %s imports it."
452
- )
453
- % [index, texture_path, texture_path]
454
- )
455
- ]
456
- return [false, "sources[%d].texture: no texture at %s" % [index, texture_path]]
457
- var texture: Texture2D = load(texture_path) as Texture2D
458
- if texture == null:
459
- return [false, "sources[%d].texture: %s is not a texture" % [index, texture_path]]
460
-
461
- var sized: Array = _xy(source.get("tileSize"), "sources[%d].tileSize" % index)
462
- if not sized[0]:
463
- return sized
464
- var tile_size: Vector2i = sized[1]
465
- if tile_size.x <= 0 or tile_size.y <= 0:
466
- return [false, "sources[%d].tileSize must be above zero on both sides" % index]
467
-
468
- var atlas: TileSetAtlasSource = TileSetAtlasSource.new()
469
- atlas.texture = texture
470
- atlas.texture_region_size = tile_size
471
- for field: String in ["separation", "offset"]:
472
- if not source.has(field):
473
- continue
474
- var pair: Array = _xy(source[field], "sources[%d].%s" % [index, field])
475
- if not pair[0]:
476
- return pair
477
- if field == "separation":
478
- atlas.separation = pair[1]
479
- else:
480
- atlas.margins = pair[1]
481
-
482
- var grid: Vector2i = atlas.get_atlas_grid_size()
483
- if grid.x <= 0 or grid.y <= 0:
484
- return [
485
- false,
486
- (
487
- "sources[%d]: a %s tile does not fit in %s, which is %s"
488
- % [index, tile_size, texture_path, texture.get_size()]
489
- )
490
- ]
491
- var image: Image = texture.get_image()
492
- var blank: Array = []
493
- for y: int in grid.y:
494
- for x: int in grid.x:
495
- var cell: Vector2i = Vector2i(x, y)
496
- # Worked out here: the atlas answers a region only for a tile it already has.
497
- var region: Rect2i = Rect2i(
498
- atlas.margins + cell * (atlas.texture_region_size + atlas.separation),
499
- atlas.texture_region_size
500
- )
501
- if image != null and _blank(image, region):
502
- blank.append({"x": x, "y": y})
503
- continue
504
- atlas.create_tile(cell)
505
- var covered: Vector2i = atlas.margins + grid * tile_size + (grid - Vector2i.ONE) * atlas.separation
506
- var left: Vector2i = Vector2i(texture.get_size()) - covered
507
- var sides: PackedStringArray = []
508
- if left.x > 0:
509
- var _right: bool = sides.append("%d px on the right" % left.x)
510
- if left.y > 0:
511
- var _bottom: bool = sides.append("%d px at the bottom" % left.y)
512
- var spare: String = (
513
- ""
514
- if sides.is_empty()
515
- else (
516
- "%s is %d by %d, so %s %s in no %d by %d tile."
517
- % [
518
- texture_path,
519
- texture.get_width(),
520
- texture.get_height(),
521
- " and ".join(sides),
522
- "is" if sides.size() == 1 else "are",
523
- tile_size.x,
524
- tile_size.y
525
- ]
526
- )
527
- )
528
- return [true, atlas, blank, spare]
529
-
530
-
531
- func _blank(image: Image, region: Rect2i) -> bool:
532
- if image.is_compressed():
533
- return false
534
- return image.get_region(region).is_invisible()
535
-
536
-
537
- ## The sources a tile set holds, as "source 0", "sources 0, 2" or "no sources", for saying what a
538
- ## cell could have named instead.
539
- func _sources_of(tile_set: TileSet) -> String:
540
- var ids: PackedStringArray = []
541
- for index: int in tile_set.get_source_count():
542
- var _listed: bool = ids.append(str(tile_set.get_source_id(index)))
543
- if ids.is_empty():
544
- return "no sources"
545
- return ("source " if ids.size() == 1 else "sources ") + ", ".join(ids)
546
-
547
-
548
- func set_tilemap_cells(args: Dictionary) -> Dictionary:
549
- var scene_path: String = _ensure_res_path(str(args.get("scenePath", "")))
550
- var node_path: String = str(args.get("nodePath", ""))
551
- if scene_path == "res://":
552
- return {"ok": false, "error": "scenePath is required"}
553
-
554
- var cells: Variant = args.get("cells", [])
555
- # Refused rather than skipped: placing nothing and answering success is indistinguishable
556
- # from placing everything, and the caller only finds out by opening the scene.
557
- if typeof(cells) != TYPE_ARRAY:
558
- return {"ok": false, "error": "cells must be an array of cells to place"}
559
- var placed: Array = cells
560
- if placed.is_empty():
561
- return {"ok": false, "error": "cells is empty, so there is nothing to place"}
562
-
563
- var scene: GdhSceneFile = GdhSceneFile.new()
564
- var refused: Dictionary = scene.open(scene_path, true)
565
- if not refused.is_empty():
566
- return refused
567
-
568
- var layer: int = GdhRead.as_int(args.get("layer", 0))
569
- var found: Array = _tile_node(scene, node_path, layer)
570
- if found[0] == null:
571
- return scene.refuse(str(found[1]))
572
- var node: Node = found[0]
573
- var layered: TileMap = node as TileMap
574
- var single: TileMapLayer = node as TileMapLayer
575
-
576
- # set_cell stores a cell whatever it is given, and a cell naming no tile simply draws nothing, so
577
- # the source, the tile and its alternative are checked here: the alternative is a call that
578
- # reports placing tiles and a scene that shows none.
579
- var tile_set: TileSet = layered.tile_set if layered != null else single.tile_set
580
-
581
- scene.asked(node, ["layer_%d/tile_data" % layer if layered != null else "tile_map_data"])
582
- # Every cell is checked before any is placed, and every one refused is named: stopping at the first
583
- # left a caller fixing one cell a call.
584
- var checked: Array = []
585
- var refusals: PackedStringArray = []
586
- for index: int in placed.size():
587
- var cell: Array = _cell(placed[index], index, tile_set)
588
- if not cell[0]:
589
- var _named: bool = refusals.append(str(cell[1]))
590
- checked.append(cell)
591
- if not refusals.is_empty():
592
- return scene.refuse("%s. Nothing was placed." % "; ".join(refusals))
593
- var wanted: Array = []
594
- var erased: int = 0
595
- for cell: Array in checked:
596
- var at: Vector2i = cell[1]
597
- var source_id: int = cell[2]
598
- var atlas_coords: Vector2i = cell[3]
599
- var alternative: int = cell[4]
600
- if layered != null:
601
- layered.set_cell(layer, at, source_id, atlas_coords, alternative)
602
- else:
603
- single.set_cell(at, source_id, atlas_coords, alternative)
604
- wanted.append([at, source_id, atlas_coords, alternative])
605
- if source_id == -1:
606
- erased += 1
607
-
608
- var at_path: String = scene.path_of(node)
609
- var written: Dictionary = scene.write(
610
- func(saved: Node) -> String:
611
- var copy: Node = saved.get_node_or_null(at_path)
612
- for entry: Array in wanted:
613
- var at: Vector2i = entry[0]
614
- var held: Array = _cell_at(copy, layer, at)
615
- if held != entry.slice(1):
616
- return "the cell at %s would load as %s, not %s." % [at, held, entry.slice(1)]
617
- return ""
618
- )
619
- if not written.is_empty():
620
- return written
621
-
622
- return {
623
- "ok": true,
624
- "layer": layer,
625
- "placed": wanted.size() - erased,
626
- "erased": erased,
627
- "requested": placed.size()
628
- }
629
-
630
-
631
- ## The node at [param node_path] cells are painted on, with a TileSet, as [node, why not].
632
- ## TileMapLayer is the node since 4.3, and TileMap the one it replaced, which keeps its layers.
633
- func _tile_node(scene: GdhSceneFile, node_path: String, layer: int) -> Array:
634
- var node: Node = (
635
- scene.root if node_path == "." or node_path == "" else scene.root.get_node_or_null(node_path)
636
- )
637
- if node == null:
638
- return [null, "No node at %s in %s" % [node_path, scene.path]]
639
- var layered: TileMap = node as TileMap
640
- var single: TileMapLayer = node as TileMapLayer
641
- if layered == null and single == null:
642
- return [null, "%s is a %s, not a TileMapLayer or a TileMap" % [node_path, node.get_class()]]
643
- var foreign: String = scene.foreign(node)
644
- if not foreign.is_empty():
645
- return [null, foreign]
646
- if single != null and layer != 0:
647
- return [null, "%s is a TileMapLayer, which is one layer; layer applies to a TileMap" % node_path]
648
- if layered != null and (layer < 0 or layer >= layered.get_layers_count()):
649
- return [null, "%s has %d layers, and no layer %d" % [node_path, layered.get_layers_count(), layer]]
650
- var tile_set: TileSet = layered.tile_set if layered != null else single.tile_set
651
- if tile_set == null:
652
- return [null, "%s has no TileSet, so no cell can name a source" % node_path]
653
- return [node, ""]
654
-
655
-
656
- ## One cell from its description, checked against [param tile_set], as
657
- ## [true, coords, source id, atlas coords, alternative] or [false, why]. A source of -1 erases the
658
- ## cell, as set_cell takes it, and needs no atlasCoords.
659
- func _cell(entry: Variant, index: int, tile_set: TileSet) -> Array:
660
- if not entry is Dictionary:
661
- return [false, "cells[%d] must be an object with coords, sourceId and atlasCoords" % index]
662
- var cell: Dictionary = entry
663
- for field: String in ["coords", "sourceId"]:
664
- if not cell.has(field):
665
- return [false, "cells[%d] has no %s" % [index, field]]
666
- var at: Array = _xy(cell["coords"], "cells[%d].coords" % index)
667
- if not at[0]:
668
- return at
669
- var coords: Vector2i = at[1]
670
- var named: String = "cells[%d] at (%d, %d)" % [index, coords.x, coords.y]
671
- var source_id: int = GdhRead.as_int(cell["sourceId"], -2)
672
- if source_id == -1:
673
- return [true, coords, -1, Vector2i(-1, -1), -1]
674
- if not cell.has("atlasCoords"):
675
- return [
676
- false, "%s has no atlasCoords, which every cell but an erased one (sourceId -1) needs" % named
677
- ]
678
- var atlas_at: Array = _xy(cell["atlasCoords"], "cells[%d].atlasCoords" % index)
679
- if not atlas_at[0]:
680
- return atlas_at
681
- if not tile_set.has_source(source_id):
682
- return [
683
- false,
684
- (
685
- "%s names source %d, and the TileSet has %s; sourceId -1 erases a cell"
686
- % [named, source_id, _sources_of(tile_set)]
687
- )
688
- ]
689
- var source: TileSetSource = tile_set.get_source(source_id)
690
- var atlas_coords: Vector2i = atlas_at[1]
691
- var alternative: int = GdhRead.as_int(cell.get("alternativeTile", 0))
692
- if not source.has_tile(atlas_coords):
693
- return [
694
- false,
695
- "%s: source %d has no tile at %s; it has %s" % [named, source_id, atlas_coords, _tiles_of(source)]
696
- ]
697
- if not source.has_alternative_tile(atlas_coords, alternative):
698
- return [
699
- false,
700
- (
701
- "%s: the tile at %s in source %d has no alternative %d"
702
- % [named, atlas_coords, source_id, alternative]
703
- )
704
- ]
705
- return [true, coords, source_id, atlas_coords, alternative]
706
-
707
-
708
- ## The atlas coordinates [param source] has tiles at, the first twelve and how many more, or that it
709
- ## has none: a refusal naming the one missing sent a caller guessing which of the others exist.
710
- static func _tiles_of(source: TileSetSource) -> String:
711
- var count: int = source.get_tiles_count()
712
- if count == 0:
713
- return "no tiles"
714
- var named: Array[String] = []
715
- for index: int in mini(count, 12):
716
- named.append(str(source.get_tile_id(index)))
717
- var more: String = "" if count <= 12 else " and %d more" % (count - 12)
718
- return "tiles at " + ", ".join(PackedStringArray(named)) + more
719
-
720
-
721
- ## The cells a TileMapLayer or a TileMap holds, as {"cellCount", "cells"} in the form
722
- ## set_tilemap_cells takes, or {} for any other node. The scene keeps them as packed bytes, which a
723
- ## read answered with and nobody could read: a probe counted three cells by decoding the base64 by
724
- ## hand. At most CELLS_SHOWN are listed, the count saying how many there are.
725
- static func cells_of(node: Node) -> Dictionary:
726
- var listed: Array[Dictionary] = []
727
- var count: int = 0
728
- var single: TileMapLayer = node as TileMapLayer
729
- var layered: TileMap = node as TileMap
730
- if single != null:
731
- var used: Array[Vector2i] = single.get_used_cells()
732
- count = used.size()
733
- for at: Vector2i in used.slice(0, CELLS_SHOWN):
734
- listed.append(
735
- _cell_entry(
736
- at,
737
- single.get_cell_source_id(at),
738
- single.get_cell_atlas_coords(at),
739
- single.get_cell_alternative_tile(at)
740
- )
741
- )
742
- elif layered != null:
743
- for layer: int in layered.get_layers_count():
744
- var used: Array[Vector2i] = layered.get_used_cells(layer)
745
- count += used.size()
746
- for at: Vector2i in used:
747
- if listed.size() >= CELLS_SHOWN:
748
- break
749
- var entry: Dictionary = _cell_entry(
750
- at,
751
- layered.get_cell_source_id(layer, at),
752
- layered.get_cell_atlas_coords(layer, at),
753
- layered.get_cell_alternative_tile(layer, at)
754
- )
755
- entry["layer"] = layer
756
- listed.append(entry)
757
- else:
758
- return {}
759
- var answer: Dictionary = {"cellCount": count, "cells": listed}
760
- if count > listed.size():
761
- answer["cellsNote"] = "The first %d of %d cells are listed." % [listed.size(), count]
379
+ var answer: Dictionary = {"ok": true, "shaderPath": shader_path, "shaderType": shader_type}
380
+ var errors: Array[Dictionary] = _shader_errors(shader_path)
381
+ answer["compiles"] = errors.is_empty()
382
+ if not errors.is_empty():
383
+ answer["compileErrors"] = errors
762
384
  return answer
763
385
 
764
386
 
765
- static func _cell_entry(at: Vector2i, source_id: int, atlas: Vector2i, alternative: int) -> Dictionary:
766
- return {
767
- "coords": {"x": at.x, "y": at.y},
768
- "sourceId": source_id,
769
- "atlasCoords": {"x": atlas.x, "y": atlas.y},
770
- "alternativeTile": alternative,
771
- }
772
-
773
-
774
- func _cell_at(node: Node, layer: int, at: Vector2i) -> Array:
775
- var layered: TileMap = node as TileMap
776
- if layered != null:
777
- return [
778
- layered.get_cell_source_id(layer, at),
779
- layered.get_cell_atlas_coords(layer, at),
780
- layered.get_cell_alternative_tile(layer, at)
781
- ]
782
- var single: TileMapLayer = node as TileMapLayer
783
- if single != null:
784
- return [
785
- single.get_cell_source_id(at),
786
- single.get_cell_atlas_coords(at),
787
- single.get_cell_alternative_tile(at)
788
- ]
789
- return []
790
-
791
-
792
- ## Saves [param theme] over [param theme_path] with [param item] changed, answering "" or why not.
793
- func _save_theme(theme: Theme, theme_path: String, item: String) -> String:
387
+ ## The errors the engine raises compiling the shader at [param path], as {line, message} in order.
388
+ ## A shader is compiled once something reads what it declares, and a create answering only the path
389
+ ## left a missing semicolon to show up wherever the shader was first drawn with.
390
+ func _shader_errors(path: String) -> Array[Dictionary]:
391
+ var caught: GdhShaderErrors = GdhShaderErrors.new()
392
+ OS.add_logger(caught)
393
+ var shader: Shader = ResourceLoader.load(path, "Shader", ResourceLoader.CACHE_MODE_IGNORE) as Shader
394
+ if shader != null:
395
+ var _uniforms: Array = shader.get_shader_uniform_list()
396
+ OS.remove_logger(caught)
397
+ return caught.raised
398
+
399
+
400
+ ## Every shader error the engine raises while it is attached, in order.
401
+ class GdhShaderErrors:
402
+ extends Logger
403
+
404
+ var raised: Array[Dictionary] = []
405
+
406
+ func _log_message(_message: String, _error: bool) -> void:
407
+ pass
408
+
409
+ # The engine's own signature for a Logger, eight parameters and not this project's to shape.
410
+ # gdlint:ignore = function-arguments-number
411
+ func _log_error(
412
+ _function: String,
413
+ _file: String,
414
+ line: int,
415
+ code: String,
416
+ rationale: String,
417
+ _editor_notify: bool,
418
+ error_type: int,
419
+ _script_backtraces: Array[ScriptBacktrace],
420
+ ) -> void:
421
+ if error_type == Logger.ERROR_TYPE_SHADER:
422
+ raised.append({"line": line, "message": code if rationale.is_empty() else rationale})
423
+
424
+
425
+ ## Saves [param theme] over [param theme_path] with [param items] changed, answering "" or why not.
426
+ func _save_theme(theme: Theme, theme_path: String, items: Array) -> String:
794
427
  var previous: PackedByteArray = FileAccess.get_file_as_bytes(theme_path)
795
- var saving: Error = GdhSceneFile.save_keeping_uid(theme, theme_path)
428
+ var carried: Dictionary = GdhHeadlessCheck.embedded_in(GdhHeadlessCheck.stored(theme).values(), {})
429
+ var saving: Error = GdhSceneUids.save_keeping_uid(theme, theme_path)
796
430
  if saving != OK:
797
431
  return "Failed to save %s: %s" % [theme_path, error_string(saving)]
798
- var unkept: String = _unkept_headless(theme, theme_path, [item], previous)
432
+ var unkept: String = _unkept_headless(theme, theme_path, items, previous, carried)
799
433
  if not unkept.is_empty():
800
434
  return "Nothing was written: " + unkept + GdhHeadlessCheck.put_back(theme_path, previous)
801
435
  _written(theme_path)
@@ -824,7 +458,7 @@ func set_theme_color(args: Dictionary) -> Dictionary:
824
458
  var color: Color = parsed[1]
825
459
  theme.set_color(color_name, control_type, color)
826
460
 
827
- var unsaved: String = _save_theme(theme, theme_path, "%s/colors/%s" % [control_type, color_name])
461
+ var unsaved: String = _save_theme(theme, theme_path, ["%s/colors/%s" % [control_type, color_name]])
828
462
  if not unsaved.is_empty():
829
463
  return {"ok": false, "error": unsaved}
830
464
 
@@ -877,7 +511,9 @@ func set_theme_font_size(args: Dictionary) -> Dictionary:
877
511
  }
878
512
  theme.set_font_size(font_size_name, control_type, int(pixels))
879
513
 
880
- var unsaved: String = _save_theme(theme, theme_path, "%s/font_sizes/%s" % [control_type, font_size_name])
514
+ var unsaved: String = _save_theme(
515
+ theme, theme_path, ["%s/font_sizes/%s" % [control_type, font_size_name]]
516
+ )
881
517
  if not unsaved.is_empty():
882
518
  return {"ok": false, "error": unsaved}
883
519
 
@@ -896,6 +532,49 @@ func set_theme_font_size(args: Dictionary) -> Dictionary:
896
532
  )
897
533
 
898
534
 
535
+ ## Takes one item, such as "colors/font_color", or with none every item of a theme type, out of a
536
+ ## Theme: a type is any name, so a misspelt one is written as given, and this is the way back.
537
+ func remove_theme_item(args: Dictionary) -> Dictionary:
538
+ var theme_path: String = _ensure_res_path(str(args.get("resourcePath", "")))
539
+ var control_type: String = str(args.get("controlType", ""))
540
+ var theme: Theme = _load_theme(theme_path)
541
+ if theme == null:
542
+ return {"ok": false, "error": "No Theme at %s." % theme_path}
543
+ if not theme.get_type_list().has(control_type):
544
+ var types: PackedStringArray = theme.get_type_list()
545
+ var said: String = "%s holds no type %s" % [theme_path, control_type]
546
+ return {"ok": false, "error": GdhRead.offered(said, control_type, Array(types))}
547
+ var kinds: Array[String] = ["colors", "constants", "fonts", "font_sizes", "icons", "styles"]
548
+ var item: String = str(args.get("item", ""))
549
+ var removed: Array[String] = []
550
+ var held: Array[String] = []
551
+ for kind: int in kinds.size():
552
+ for called: String in theme.get_theme_item_list(kind as Theme.DataType, control_type):
553
+ var entry: String = "%s/%s" % [kinds[kind], called]
554
+ held.append(entry)
555
+ if item.is_empty() or item == entry:
556
+ removed.append("%s/%s" % [control_type, entry])
557
+ theme.clear_theme_item(kind as Theme.DataType, called, control_type)
558
+ var base: String = theme.get_type_variation_base(control_type)
559
+ if item.is_empty():
560
+ # A variation's base is stored apart from its items and would outlive them in the file.
561
+ if not base.is_empty():
562
+ removed.append("%s/base_type" % control_type)
563
+ theme.remove_type(control_type)
564
+ elif removed.is_empty() and held.is_empty():
565
+ var only: String = (
566
+ "%s holds no items, only the type it varies, %s; "
567
+ + "without item, remove_theme_item takes it out."
568
+ )
569
+ return {"ok": false, "error": only % [control_type, base]}
570
+ elif removed.is_empty():
571
+ return {"ok": false, "error": GdhRead.offered("%s has no item %s" % [control_type, item], item, held)}
572
+ var unsaved: String = _save_theme(theme, theme_path, removed)
573
+ if not unsaved.is_empty():
574
+ return {"ok": false, "error": unsaved}
575
+ return {"ok": true, "themePath": theme_path, "controlType": control_type, "removed": removed}
576
+
577
+
899
578
  ## [param answer] with a note when its controlType names no class a theme styles. Any name is a
900
579
  ## theme type, so one that is no class is a type variation, which styles only a control whose
901
580
  ## theme_type_variation names it, and a misspelt "Buton" would otherwise read as Button set.
@@ -922,7 +601,7 @@ func _noting_variation(answer: Dictionary) -> Dictionary:
922
601
  var close: PackedStringArray = GdhRead.nearest(type, themed)
923
602
  if not close.is_empty():
924
603
  answer["note"] += (
925
- " If a class was meant, the nearest %s %s."
926
- % ["is" if close.size() == 1 else "are", ", ".join(close)]
604
+ (" If a class was meant, the nearest %s %s, and remove_theme_item with controlType %s takes this out.")
605
+ % ["is" if close.size() == 1 else "are", ", ".join(close), type]
927
606
  )
928
607
  return answer