gdharness 1.3.0 → 1.3.2

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 (72) hide show
  1. package/build/cli.js +322 -96
  2. package/build/godot/addons/gdharness_editor/animation_reads.gd +7 -19
  3. package/build/godot/addons/gdharness_editor/bridge_client.gd +8 -8
  4. package/build/godot/addons/gdharness_editor/declared_values.gd +116 -0
  5. package/build/godot/addons/gdharness_editor/declared_values.gd.uid +1 -0
  6. package/build/godot/addons/gdharness_editor/file_reads.gd +65 -25
  7. package/build/godot/addons/gdharness_editor/headless_check.gd +7 -7
  8. package/build/godot/addons/gdharness_editor/plugin.gd +6 -6
  9. package/build/godot/addons/gdharness_editor/property_values.gd +222 -59
  10. package/build/godot/addons/gdharness_editor/reading.gd +65 -0
  11. package/build/godot/addons/gdharness_editor/scene_file.gd +28 -23
  12. package/build/godot/addons/gdharness_editor/serialisation.gd +49 -94
  13. package/build/godot/addons/gdharness_editor/tool_executor.gd +24 -24
  14. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +52 -41
  15. package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +4 -4
  16. package/build/godot/addons/gdharness_editor/tools/play_tools.gd +2 -2
  17. package/build/godot/addons/gdharness_editor/tools/profile_tools.gd +14 -14
  18. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +175 -74
  19. package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +17 -17
  20. package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +14 -12
  21. package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +7 -7
  22. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +88 -84
  23. package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +4 -4
  24. package/build/godot/addons/gdharness_editor/value_shapes.gd +118 -0
  25. package/build/godot/addons/gdharness_editor/value_shapes.gd.uid +1 -0
  26. package/build/godot/addons/gdharness_runtime/reading.gd +65 -0
  27. package/build/godot/addons/gdharness_runtime/runtime_accessibility.gd +18 -18
  28. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +29 -29
  29. package/build/godot/addons/gdharness_runtime/runtime_capture.gd +21 -18
  30. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +86 -80
  31. package/build/godot/addons/gdharness_runtime/runtime_input.gd +65 -64
  32. package/build/godot/addons/gdharness_runtime/runtime_items.gd +6 -6
  33. package/build/godot/addons/gdharness_runtime/runtime_menus.gd +8 -8
  34. package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +6 -6
  35. package/build/godot/addons/gdharness_runtime/runtime_pad.gd +10 -10
  36. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +7 -7
  37. package/build/godot/addons/gdharness_runtime/runtime_queries.gd +41 -41
  38. package/build/godot/addons/gdharness_runtime/runtime_says.gd +3 -3
  39. package/build/godot/addons/gdharness_runtime/runtime_targets.gd +33 -33
  40. package/build/godot/addons/gdharness_runtime/runtime_typing.gd +20 -20
  41. package/build/godot/addons/gdharness_runtime/runtime_values.gd +52 -25
  42. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +56 -50
  43. package/build/godot/addons/gdharness_runtime/serialisation.gd +49 -94
  44. package/build/godot/addons/gdharness_runtime/value_shapes.gd +118 -0
  45. package/build/godot/addons/gdharness_runtime/value_shapes.gd.uid +1 -0
  46. package/build/godot/operations/animation_reads.gd +7 -19
  47. package/build/godot/operations/audio_buses.gd +10 -10
  48. package/build/godot/operations/class_cache.gd +16 -16
  49. package/build/godot/operations/classdb_queries.gd +25 -15
  50. package/build/godot/operations/declared_values.gd +116 -0
  51. package/build/godot/operations/dependencies.gd +141 -33
  52. package/build/godot/operations/editor_tools.gd +12 -12
  53. package/build/godot/operations/export_presets.gd +14 -14
  54. package/build/godot/operations/file_reads.gd +65 -25
  55. package/build/godot/operations/file_walk.gd +34 -0
  56. package/build/godot/operations/gdscript_analysis.gd +27 -22
  57. package/build/godot/operations/gdscript_authoring.gd +122 -17
  58. package/build/godot/operations/godot_operations.gd +66 -63
  59. package/build/godot/operations/import_pipeline.gd +59 -20
  60. package/build/godot/operations/input_actions.gd +90 -18
  61. package/build/godot/operations/logger.gd +1 -1
  62. package/build/godot/operations/plugins.gd +8 -8
  63. package/build/godot/operations/project_config.gd +83 -20
  64. package/build/godot/operations/project_diagnostics.gd +36 -11
  65. package/build/godot/operations/property_values.gd +222 -59
  66. package/build/godot/operations/reading.gd +65 -0
  67. package/build/godot/operations/rename_checks.gd +3 -3
  68. package/build/godot/operations/resource_files.gd +9 -6
  69. package/build/godot/operations/script_check.gd +23 -7
  70. package/build/godot/operations/serialisation.gd +49 -94
  71. package/build/godot/operations/value_shapes.gd +118 -0
  72. package/package.json +1 -1
@@ -3,11 +3,11 @@ extends Node
3
3
 
4
4
  ## Resource files, shaders, tilesets and themes, written through the open editor.
5
5
 
6
- const FileReads = preload("../file_reads.gd")
7
- const HeadlessCheck = preload("../headless_check.gd")
8
- const PropertyValues = preload("../property_values.gd")
9
- const Read = preload("../reading.gd")
10
- const SceneFile = preload("../scene_file.gd")
6
+ const GdhFileReads = preload("../file_reads.gd")
7
+ const GdhHeadlessCheck = preload("../headless_check.gd")
8
+ const GdhPropertyValues = preload("../property_values.gd")
9
+ const GdhRead = preload("../reading.gd")
10
+ const GdhSceneFile = preload("../scene_file.gd")
11
11
 
12
12
  # The minimal shader of a type: its declaration and the one function the engine calls for it.
13
13
  const SHADER_EMPTY: String = """shader_type %s;
@@ -23,7 +23,7 @@ const SHADER_ENTRY: Dictionary[String, String] = {
23
23
  const CELLS_SHOWN: int = 1000
24
24
 
25
25
  var _editor_plugin: EditorPlugin = null
26
- var _properties: PropertyValues = PropertyValues.new()
26
+ var _properties: GdhPropertyValues = GdhPropertyValues.new()
27
27
 
28
28
 
29
29
  func set_editor_plugin(plugin: EditorPlugin) -> void:
@@ -48,7 +48,7 @@ func _written(path: String) -> void:
48
48
  if ResourceLoader.has_cached(path):
49
49
  var _replaced: Resource = ResourceLoader.load(path, "", ResourceLoader.CACHE_MODE_REPLACE)
50
50
  if _editor_plugin:
51
- SceneFile.scan_later()
51
+ GdhSceneFile.scan_later()
52
52
 
53
53
 
54
54
  ## Why a tool that creates [param path] must not write there, or "". Saving over a file that was
@@ -89,12 +89,15 @@ func _xy(value: Variant, label: String) -> Array:
89
89
  x = pair[0]
90
90
  y = pair[1]
91
91
  for number: Variant in [x, y]:
92
- if not (number is float or number is int) or Read.as_float(number) != floorf(Read.as_float(number)):
92
+ if (
93
+ not (number is float or number is int)
94
+ or GdhRead.as_float(number) != floorf(GdhRead.as_float(number))
95
+ ):
93
96
  return [
94
97
  false,
95
98
  '%s must be two whole numbers, {"x": .., "y": ..}, not %s.' % [label, JSON.stringify(value)]
96
99
  ]
97
- return [true, Vector2i(Read.as_int(x), Read.as_int(y))]
100
+ return [true, Vector2i(GdhRead.as_int(x), GdhRead.as_int(y))]
98
101
 
99
102
 
100
103
  ## The theme on disk at this path, or null when there is not one.
@@ -119,12 +122,12 @@ func _load_theme(theme_path: String) -> Theme:
119
122
  ## Read back because a property can be set and still not be saved: one without storage, a
120
123
  ## script variable that is not exported. Both were answered as changed.
121
124
  func _save_checked(resource: Resource, path: String, names: Array, previous: Variant) -> String:
122
- var saving: Error = SceneFile.save_keeping_uid(resource, path)
125
+ var saving: Error = GdhSceneFile.save_keeping_uid(resource, path)
123
126
  if saving != OK:
124
127
  return "Failed to save %s: %s" % [path, error_string(saving)]
125
128
  var unkept: String = _unkept_headless(resource, path, names, previous)
126
129
  if not unkept.is_empty():
127
- return "Nothing was written: " + unkept + HeadlessCheck.put_back(path, previous)
130
+ return "Nothing was written: " + unkept + GdhHeadlessCheck.put_back(path, previous)
128
131
  var loaded: Resource = ResourceLoader.load(path, "", ResourceLoader.CACHE_MODE_IGNORE)
129
132
  var lost: String = ""
130
133
  if loaded == null:
@@ -133,7 +136,7 @@ func _save_checked(resource: Resource, path: String, names: Array, previous: Var
133
136
  for key: Variant in names:
134
137
  var property: String = str(key)
135
138
  var read: Variant = _properties.read_at(loaded, property)
136
- if not PropertyValues.same(read, _properties.read_at(resource, property)):
139
+ if not GdhPropertyValues.same(read, _properties.read_at(resource, property)):
137
140
  lost = (
138
141
  (
139
142
  "%s.%s would load from the file as %s: it is not a property the file keeps, so "
@@ -145,19 +148,19 @@ func _save_checked(resource: Resource, path: String, names: Array, previous: Var
145
148
  if lost.is_empty():
146
149
  _written(path)
147
150
  return ""
148
- return lost + HeadlessCheck.put_back(path, previous)
151
+ return lost + GdhHeadlessCheck.put_back(path, previous)
149
152
 
150
153
 
151
154
  ## Outside the editor, puts back in the file just saved at [param path] what a script that is not a
152
155
  ## tool changed and the call did not ask for, [param names] being what it asked for, and answers ""
153
- ## or why that did not hold. See [HeadlessCheck].
156
+ ## or why that did not hold. See [GdhHeadlessCheck].
154
157
  func _unkept_headless(resource: Resource, path: String, names: Array, previous: Variant) -> String:
155
158
  if Engine.is_editor_hint():
156
159
  return ""
157
160
  var script: Script = resource.get_script()
158
161
  var scripted: bool = script != null and not script.is_tool()
159
162
  if not path.ends_with(".tres"):
160
- if scripted or HeadlessCheck.holds_scripted_embedded(resource, {}):
163
+ if scripted or GdhHeadlessCheck.holds_scripted_embedded(resource, {}):
161
164
  return (
162
165
  (
163
166
  (
@@ -166,7 +169,7 @@ func _unkept_headless(resource: Resource, path: String, names: Array, previous:
166
169
  )
167
170
  % path
168
171
  )
169
- + HeadlessCheck.REMEDY
172
+ + GdhHeadlessCheck.REMEDY
170
173
  )
171
174
  return ""
172
175
  var before: String = ""
@@ -179,15 +182,15 @@ func _unkept_headless(resource: Resource, path: String, names: Array, previous:
179
182
  var property: String = str(key)
180
183
  asked[property] = true
181
184
  asked[property.get_slice(":", 0)] = true
182
- reached.merge(HeadlessCheck.reached(resource, property))
183
- return HeadlessCheck.settle_saved(
185
+ reached.merge(GdhHeadlessCheck.reached(resource, property))
186
+ return GdhHeadlessCheck.settle_saved(
184
187
  path,
185
188
  before,
186
- HeadlessCheck.stored(resource).values(),
189
+ GdhHeadlessCheck.stored(resource).values(),
187
190
  resource if scripted else null,
188
191
  asked,
189
192
  reached,
190
- func() -> Error: return SceneFile.save_keeping_uid(resource, path)
193
+ func() -> Error: return GdhSceneFile.save_keeping_uid(resource, path)
191
194
  )
192
195
 
193
196
 
@@ -202,7 +205,7 @@ func create_resource(args: Dictionary) -> Dictionary:
202
205
  return {"ok": false, "error": taken}
203
206
 
204
207
  var script_path: String = str(args.get("script", ""))
205
- var made: Dictionary = SceneFile.made_as(
208
+ var made: Dictionary = GdhSceneFile.made_as(
206
209
  resource_type, "" if script_path == "" else _ensure_res_path(script_path), "Resource", "resourceType"
207
210
  )
208
211
  if made.has("error"):
@@ -229,7 +232,7 @@ func create_resource(args: Dictionary) -> Dictionary:
229
232
  var script: Script = load(script_file) as Script
230
233
  if script == null:
231
234
  return {"ok": false, "error": "No script loads from " + script_file}
232
- var uncompiled: String = SceneFile.uncompiled(script, script_file)
235
+ var uncompiled: String = GdhSceneFile.uncompiled(script, script_file)
233
236
  if not uncompiled.is_empty():
234
237
  return {"ok": false, "error": uncompiled}
235
238
  var base: String = str(script.get_instance_base_type())
@@ -245,13 +248,15 @@ func create_resource(args: Dictionary) -> Dictionary:
245
248
  if not refused.is_empty():
246
249
  return {"ok": false, "error": refused}
247
250
 
248
- var folder: String = SceneFile.make_folder_for(res_path)
251
+ var folder: String = GdhSceneFile.make_folder_for(res_path)
249
252
  if not folder.is_empty():
250
253
  return {"ok": false, "error": folder}
254
+ var answer: Dictionary = {"ok": true, "resourcePath": res_path, "resourceType": asked_type}
255
+ _properties.note_write(answer)
251
256
  var failed: String = _save_checked(resource, res_path, properties.keys(), null)
252
257
  if not failed.is_empty():
253
258
  return {"ok": false, "error": failed}
254
- return {"ok": true, "resourcePath": res_path, "resourceType": asked_type}
259
+ return answer
255
260
 
256
261
 
257
262
  func modify_resource(args: Dictionary) -> Dictionary:
@@ -284,11 +289,13 @@ func modify_resource(args: Dictionary) -> Dictionary:
284
289
  var refused: String = _properties.write_all(resource, properties)
285
290
  if not refused.is_empty():
286
291
  return {"ok": false, "error": refused}
292
+ var answer: Dictionary = {"ok": true, "resourcePath": res_path}
293
+ _properties.note_write(answer)
287
294
  var previous: PackedByteArray = FileAccess.get_file_as_bytes(res_path)
288
295
  var failed: String = _save_checked(resource, res_path, properties.keys(), previous)
289
296
  if not failed.is_empty():
290
297
  return {"ok": false, "error": failed}
291
- return {"ok": true, "resourcePath": res_path}
298
+ return answer
292
299
 
293
300
 
294
301
  ## What a resource file holds, read as a headless engine reads it with no editor connected.
@@ -300,7 +307,7 @@ func read_resource(args: Dictionary) -> Dictionary:
300
307
  var inner: String = str(args.get("subResource", ""))
301
308
  if not inner.is_empty():
302
309
  res_path += "::" + inner
303
- return FileReads.resource(res_path, Read.as_bool(args.get("includeDefaults", false)))
310
+ return GdhFileReads.resource(res_path, GdhRead.as_bool(args.get("includeDefaults", false)))
304
311
 
305
312
 
306
313
  func create_shader(args: Dictionary) -> Dictionary:
@@ -316,7 +323,7 @@ func create_shader(args: Dictionary) -> Dictionary:
316
323
  if code == "":
317
324
  code = SHADER_EMPTY % [shader_type, SHADER_ENTRY.get(shader_type, "fragment")]
318
325
 
319
- var folder: String = SceneFile.make_folder_for(shader_path)
326
+ var folder: String = GdhSceneFile.make_folder_for(shader_path)
320
327
  if not folder.is_empty():
321
328
  return {"ok": false, "error": folder}
322
329
  var file: FileAccess = FileAccess.open(shader_path, FileAccess.WRITE)
@@ -339,7 +346,19 @@ func create_tileset(args: Dictionary) -> Dictionary:
339
346
  var tileset_path: String = _ensure_res_path(str(args.get("resourcePath", "")))
340
347
  if tileset_path == "res://":
341
348
  return {"ok": false, "error": "resourcePath is required"}
342
- var taken: String = _taken(tileset_path, "resource_edit op=create_tileset")
349
+ # modify sets properties and builds no atlas, so the other path is the only way on offered.
350
+ var taken: String = (
351
+ ""
352
+ if not FileAccess.file_exists(tileset_path)
353
+ else (
354
+ (
355
+ "%s already exists, and create_tileset builds a whole new TileSet, which would replace "
356
+ + "everything in it, the tiles a scene paints with among them. Give resourcePath a path "
357
+ + "nothing is at."
358
+ )
359
+ % tileset_path
360
+ )
361
+ )
343
362
  if not taken.is_empty():
344
363
  return {"ok": false, "error": taken}
345
364
 
@@ -367,14 +386,21 @@ func create_tileset(args: Dictionary) -> Dictionary:
367
386
  "ok": false,
368
387
  "error": "The TileSet did not take the atlas for %s" % atlas.texture.resource_path
369
388
  }
370
- made.append(
371
- {"sourceId": added, "texture": atlas.texture.resource_path, "tiles": atlas.get_tiles_count()}
372
- )
373
-
374
- var folder: String = SceneFile.make_folder_for(tileset_path)
389
+ var entry: Dictionary = {
390
+ "sourceId": added, "texture": atlas.texture.resource_path, "tiles": atlas.get_tiles_count()
391
+ }
392
+ var blank: Array = built[2]
393
+ if not blank.is_empty():
394
+ entry["leftBlank"] = blank
395
+ var spare: String = built[3]
396
+ if not spare.is_empty():
397
+ entry["notInAnyTile"] = spare
398
+ made.append(entry)
399
+
400
+ var folder: String = GdhSceneFile.make_folder_for(tileset_path)
375
401
  if not folder.is_empty():
376
402
  return {"ok": false, "error": folder}
377
- var saving: Error = SceneFile.save_keeping_uid(tileset, tileset_path)
403
+ var saving: Error = GdhSceneFile.save_keeping_uid(tileset, tileset_path)
378
404
  if saving != OK:
379
405
  return {"ok": false, "error": "Failed to save %s: %s" % [tileset_path, error_string(saving)]}
380
406
 
@@ -382,11 +408,14 @@ func create_tileset(args: Dictionary) -> Dictionary:
382
408
  return {"ok": true, "tilesetPath": tileset_path, "sources": made}
383
409
 
384
410
 
385
- ## One atlas source from its description, with its tiles made, as [true, source] or [false, why].
411
+ ## One atlas source from its description, with its tiles made, as [true, source, the grid cells left
412
+ ## without a tile for being wholly transparent, a sentence on the pixels no tile covers or ""] or
413
+ ## [false, why].
386
414
  ##
387
415
  ## Tiles are made in every cell of the grid that has a pixel that is not transparent, which is
388
416
  ## what the editor offers to do when an atlas gets a texture. Without them the atlas held no tiles,
389
- ## and every cell painted from it drew nothing.
417
+ ## and every cell painted from it drew nothing. The cells passed over and the strip a tile size that
418
+ ## does not divide the picture leaves are said, since a count alone left a caller to work out which.
390
419
  func _atlas(entry: Variant, index: int) -> Array:
391
420
  if not entry is Dictionary:
392
421
  return [false, "sources[%d] must be an object with texture and tileSize" % index]
@@ -448,6 +477,7 @@ func _atlas(entry: Variant, index: int) -> Array:
448
477
  )
449
478
  ]
450
479
  var image: Image = texture.get_image()
480
+ var blank: Array = []
451
481
  for y: int in grid.y:
452
482
  for x: int in grid.x:
453
483
  var cell: Vector2i = Vector2i(x, y)
@@ -457,9 +487,33 @@ func _atlas(entry: Variant, index: int) -> Array:
457
487
  atlas.texture_region_size
458
488
  )
459
489
  if image != null and _blank(image, region):
490
+ blank.append({"x": x, "y": y})
460
491
  continue
461
492
  atlas.create_tile(cell)
462
- return [true, atlas]
493
+ var covered: Vector2i = atlas.margins + grid * tile_size + (grid - Vector2i.ONE) * atlas.separation
494
+ var left: Vector2i = Vector2i(texture.get_size()) - covered
495
+ var sides: PackedStringArray = []
496
+ if left.x > 0:
497
+ var _right: bool = sides.append("%d px on the right" % left.x)
498
+ if left.y > 0:
499
+ var _bottom: bool = sides.append("%d px at the bottom" % left.y)
500
+ var spare: String = (
501
+ ""
502
+ if sides.is_empty()
503
+ else (
504
+ "%s is %d by %d, so %s %s in no %d by %d tile."
505
+ % [
506
+ texture_path,
507
+ texture.get_width(),
508
+ texture.get_height(),
509
+ " and ".join(sides),
510
+ "is" if sides.size() == 1 else "are",
511
+ tile_size.x,
512
+ tile_size.y
513
+ ]
514
+ )
515
+ )
516
+ return [true, atlas, blank, spare]
463
517
 
464
518
 
465
519
  func _blank(image: Image, region: Rect2i) -> bool:
@@ -468,12 +522,15 @@ func _blank(image: Image, region: Rect2i) -> bool:
468
522
  return image.get_region(region).is_invisible()
469
523
 
470
524
 
471
- ## The source ids a tile set holds, for saying what a cell could have named instead.
472
- func _source_ids(tile_set: TileSet) -> Array[int]:
473
- var ids: Array[int] = []
525
+ ## The sources a tile set holds, as "source 0", "sources 0, 2" or "no sources", for saying what a
526
+ ## cell could have named instead.
527
+ func _sources_of(tile_set: TileSet) -> String:
528
+ var ids: PackedStringArray = []
474
529
  for index: int in tile_set.get_source_count():
475
- ids.append(tile_set.get_source_id(index))
476
- return ids
530
+ var _listed: bool = ids.append(str(tile_set.get_source_id(index)))
531
+ if ids.is_empty():
532
+ return "no sources"
533
+ return ("source " if ids.size() == 1 else "sources ") + ", ".join(ids)
477
534
 
478
535
 
479
536
  func set_tilemap_cells(args: Dictionary) -> Dictionary:
@@ -491,12 +548,12 @@ func set_tilemap_cells(args: Dictionary) -> Dictionary:
491
548
  if placed.is_empty():
492
549
  return {"ok": false, "error": "cells is empty, so there is nothing to place"}
493
550
 
494
- var scene: SceneFile = SceneFile.new()
551
+ var scene: GdhSceneFile = GdhSceneFile.new()
495
552
  var refused: Dictionary = scene.open(scene_path, true)
496
553
  if not refused.is_empty():
497
554
  return refused
498
555
 
499
- var layer: int = Read.as_int(args.get("layer", 0))
556
+ var layer: int = GdhRead.as_int(args.get("layer", 0))
500
557
  var found: Array = _tile_node(scene, node_path, layer)
501
558
  if found[0] == null:
502
559
  return scene.refuse(str(found[1]))
@@ -510,11 +567,20 @@ func set_tilemap_cells(args: Dictionary) -> Dictionary:
510
567
  var tile_set: TileSet = layered.tile_set if layered != null else single.tile_set
511
568
 
512
569
  scene.asked(node, ["layer_%d/tile_data" % layer if layered != null else "tile_map_data"])
513
- var wanted: Array = []
570
+ # Every cell is checked before any is placed, and every one refused is named: stopping at the first
571
+ # left a caller fixing one cell a call.
572
+ var checked: Array = []
573
+ var refusals: PackedStringArray = []
514
574
  for index: int in placed.size():
515
575
  var cell: Array = _cell(placed[index], index, tile_set)
516
576
  if not cell[0]:
517
- return scene.refuse(str(cell[1]))
577
+ var _named: bool = refusals.append(str(cell[1]))
578
+ checked.append(cell)
579
+ if not refusals.is_empty():
580
+ return scene.refuse("%s. Nothing was placed." % "; ".join(refusals))
581
+ var wanted: Array = []
582
+ var erased: int = 0
583
+ for cell: Array in checked:
518
584
  var at: Vector2i = cell[1]
519
585
  var source_id: int = cell[2]
520
586
  var atlas_coords: Vector2i = cell[3]
@@ -524,6 +590,8 @@ func set_tilemap_cells(args: Dictionary) -> Dictionary:
524
590
  else:
525
591
  single.set_cell(at, source_id, atlas_coords, alternative)
526
592
  wanted.append([at, source_id, atlas_coords, alternative])
593
+ if source_id == -1:
594
+ erased += 1
527
595
 
528
596
  var at_path: String = scene.path_of(node)
529
597
  var written: Dictionary = scene.write(
@@ -539,12 +607,18 @@ func set_tilemap_cells(args: Dictionary) -> Dictionary:
539
607
  if not written.is_empty():
540
608
  return written
541
609
 
542
- return {"ok": true, "layer": layer, "placed": wanted.size(), "requested": placed.size()}
610
+ return {
611
+ "ok": true,
612
+ "layer": layer,
613
+ "placed": wanted.size() - erased,
614
+ "erased": erased,
615
+ "requested": placed.size()
616
+ }
543
617
 
544
618
 
545
619
  ## The node at [param node_path] cells are painted on, with a TileSet, as [node, why not].
546
620
  ## TileMapLayer is the node since 4.3, and TileMap the one it replaced, which keeps its layers.
547
- func _tile_node(scene: SceneFile, node_path: String, layer: int) -> Array:
621
+ func _tile_node(scene: GdhSceneFile, node_path: String, layer: int) -> Array:
548
622
  var node: Node = (
549
623
  scene.root if node_path == "." or node_path == "" else scene.root.get_node_or_null(node_path)
550
624
  )
@@ -568,40 +642,55 @@ func _tile_node(scene: SceneFile, node_path: String, layer: int) -> Array:
568
642
 
569
643
 
570
644
  ## One cell from its description, checked against [param tile_set], as
571
- ## [true, coords, source id, atlas coords, alternative] or [false, why].
645
+ ## [true, coords, source id, atlas coords, alternative] or [false, why]. A source of -1 erases the
646
+ ## cell, as set_cell takes it, and needs no atlasCoords.
572
647
  func _cell(entry: Variant, index: int, tile_set: TileSet) -> Array:
573
648
  if not entry is Dictionary:
574
649
  return [false, "cells[%d] must be an object with coords, sourceId and atlasCoords" % index]
575
650
  var cell: Dictionary = entry
576
- for field: String in ["coords", "sourceId", "atlasCoords"]:
651
+ for field: String in ["coords", "sourceId"]:
577
652
  if not cell.has(field):
578
653
  return [false, "cells[%d] has no %s" % [index, field]]
579
654
  var at: Array = _xy(cell["coords"], "cells[%d].coords" % index)
580
655
  if not at[0]:
581
656
  return at
657
+ var coords: Vector2i = at[1]
658
+ var named: String = "cells[%d] at (%d, %d)" % [index, coords.x, coords.y]
659
+ var source_id: int = GdhRead.as_int(cell["sourceId"], -2)
660
+ if source_id == -1:
661
+ return [true, coords, -1, Vector2i(-1, -1), -1]
662
+ if not cell.has("atlasCoords"):
663
+ return [
664
+ false, "%s has no atlasCoords, which every cell but an erased one (sourceId -1) needs" % named
665
+ ]
582
666
  var atlas_at: Array = _xy(cell["atlasCoords"], "cells[%d].atlasCoords" % index)
583
667
  if not atlas_at[0]:
584
668
  return atlas_at
585
- var source_id: int = Read.as_int(cell["sourceId"], -1)
586
669
  if not tile_set.has_source(source_id):
587
- return [false, "The TileSet has no source %d; it has %s" % [source_id, _source_ids(tile_set)]]
670
+ return [
671
+ false,
672
+ (
673
+ "%s names source %d, and the TileSet has %s; sourceId -1 erases a cell"
674
+ % [named, source_id, _sources_of(tile_set)]
675
+ )
676
+ ]
588
677
  var source: TileSetSource = tile_set.get_source(source_id)
589
678
  var atlas_coords: Vector2i = atlas_at[1]
590
- var alternative: int = Read.as_int(cell.get("alternativeTile", 0))
679
+ var alternative: int = GdhRead.as_int(cell.get("alternativeTile", 0))
591
680
  if not source.has_tile(atlas_coords):
592
681
  return [
593
682
  false,
594
- (
595
- "cells[%d]: source %d has no tile at %s; it has %s. Nothing was placed."
596
- % [index, source_id, atlas_coords, _tiles_of(source)]
597
- )
683
+ "%s: source %d has no tile at %s; it has %s" % [named, source_id, atlas_coords, _tiles_of(source)]
598
684
  ]
599
685
  if not source.has_alternative_tile(atlas_coords, alternative):
600
686
  return [
601
687
  false,
602
- "The tile at %s in source %d has no alternative %d" % [atlas_coords, source_id, alternative]
688
+ (
689
+ "%s: the tile at %s in source %d has no alternative %d"
690
+ % [named, atlas_coords, source_id, alternative]
691
+ )
603
692
  ]
604
- return [true, at[1], source_id, atlas_coords, alternative]
693
+ return [true, coords, source_id, atlas_coords, alternative]
605
694
 
606
695
 
607
696
  ## The atlas coordinates [param source] has tiles at, the first twelve and how many more, or that it
@@ -691,12 +780,12 @@ func _cell_at(node: Node, layer: int, at: Vector2i) -> Array:
691
780
  ## Saves [param theme] over [param theme_path] with [param item] changed, answering "" or why not.
692
781
  func _save_theme(theme: Theme, theme_path: String, item: String) -> String:
693
782
  var previous: PackedByteArray = FileAccess.get_file_as_bytes(theme_path)
694
- var saving: Error = SceneFile.save_keeping_uid(theme, theme_path)
783
+ var saving: Error = GdhSceneFile.save_keeping_uid(theme, theme_path)
695
784
  if saving != OK:
696
785
  return "Failed to save %s: %s" % [theme_path, error_string(saving)]
697
786
  var unkept: String = _unkept_headless(theme, theme_path, [item], previous)
698
787
  if not unkept.is_empty():
699
- return "Nothing was written: " + unkept + HeadlessCheck.put_back(theme_path, previous)
788
+ return "Nothing was written: " + unkept + GdhHeadlessCheck.put_back(theme_path, previous)
700
789
  _written(theme_path)
701
790
  return ""
702
791
 
@@ -715,13 +804,12 @@ func set_theme_color(args: Dictionary) -> Dictionary:
715
804
  if theme == null:
716
805
  return {"ok": false, "error": "No Theme at %s. Create one there first." % theme_path}
717
806
 
718
- var c: Dictionary = args.get("color", {})
719
- var color: Color = Color(
720
- Read.as_float(c.get("r", 1.0), 1.0),
721
- Read.as_float(c.get("g", 1.0), 1.0),
722
- Read.as_float(c.get("b", 1.0), 1.0),
723
- Read.as_float(c.get("a", 1.0), 1.0)
724
- )
807
+ # Read as a property write reads a colour, html code included, so a value scene_node takes for a
808
+ # Button's modulate is taken for its theme colour too; "#ff8000" was refused here alone.
809
+ var parsed: Array = _properties.colour_from(args.get("color", {}), "color")
810
+ if not parsed[0]:
811
+ return {"ok": false, "error": parsed[1]}
812
+ var color: Color = parsed[1]
725
813
  theme.set_color(color_name, control_type, color)
726
814
 
727
815
  var unsaved: String = _save_theme(theme, theme_path, "%s/colors/%s" % [control_type, color_name])
@@ -760,18 +848,19 @@ func set_theme_font_size(args: Dictionary) -> Dictionary:
760
848
 
761
849
  # Godot reads a size of 0 or less as unset and answers the theme's default, so -3 would be read
762
850
  # back as 16.
763
- var pixels: float = Read.as_float(args.get("size"), 0.0)
764
- if not is_finite(pixels) or pixels < 1 or pixels != floorf(pixels):
765
- var shown: String = str(int(pixels)) if pixels == floorf(pixels) else str(pixels)
851
+ var pixels: float = GdhRead.as_float(args.get("size"), 0.0)
852
+ if not is_finite(pixels) or pixels != floorf(pixels):
853
+ return {"ok": false, "error": "size is a whole number of pixels, and %s is not one." % str(pixels)}
854
+ if pixels < 1:
766
855
  return {
767
856
  "ok": false,
768
857
  "error":
769
858
  (
770
859
  (
771
- "size must be a whole number of pixels, 1 or more, not %s: Godot reads 0 or less as no size "
772
- % shown
860
+ "size must be 1 or more, not %d: the theme would hold it, and Godot reads a size of 0 or "
861
+ + "less as unset and draws the default size instead."
773
862
  )
774
- + "and uses the theme's default instead."
863
+ % int(pixels)
775
864
  )
776
865
  }
777
866
  theme.set_font_size(font_size_name, control_type, int(pixels))
@@ -812,4 +901,16 @@ func _noting_variation(answer: Dictionary) -> Dictionary:
812
901
  )
813
902
  % [type, type]
814
903
  )
904
+ # A variation is any name, so a misspelt class is written all the same, and the classes nearest
905
+ # it are what tells a caller which one was meant.
906
+ var themed: PackedStringArray = []
907
+ for known: String in ClassDB.get_class_list():
908
+ if ClassDB.is_parent_class(known, "Control") or ClassDB.is_parent_class(known, "Window"):
909
+ var _added: bool = themed.append(known)
910
+ var close: PackedStringArray = GdhRead.nearest(type, themed)
911
+ if not close.is_empty():
912
+ answer["note"] += (
913
+ " If a class was meant, the nearest %s %s."
914
+ % ["is" if close.size() == 1 else "are", ", ".join(close)]
915
+ )
815
916
  return answer
@@ -4,9 +4,9 @@ extends RefCounted
4
4
  ## The two ways a scene keeps its own values for nodes another scene makes: by inheriting that scene,
5
5
  ## or by instancing it with its children editable.
6
6
 
7
- const FileReads = preload("../file_reads.gd")
8
- const Paths = preload("scene_paths.gd")
9
- const SceneFile = preload("../scene_file.gd")
7
+ const GdhFileReads = preload("../file_reads.gd")
8
+ const GdhPaths = preload("scene_paths.gd")
9
+ const GdhSceneFile = preload("../scene_file.gd")
10
10
 
11
11
 
12
12
  ## Why a create at [param scene_path] is refused when a file is there already, which saving over
@@ -45,7 +45,7 @@ static func inherited(
45
45
  script = load(script_path) as Script
46
46
  if script == null:
47
47
  return {"ok": false, "error": "No script loads from " + script_path}
48
- var uncompiled: String = SceneFile.uncompiled(script, script_path)
48
+ var uncompiled: String = GdhSceneFile.uncompiled(script, script_path)
49
49
  if not uncompiled.is_empty():
50
50
  return {"ok": false, "error": uncompiled}
51
51
  return inherit(scene_path, base_path, str(args.get("rootNodeName", "")), script)
@@ -59,12 +59,12 @@ static func inherited(
59
59
  ## the base: a headless engine making that instance runs every script in the base, and the pack would
60
60
  ## keep what they set as this scene's own values.
61
61
  static func inherit(scene_path: String, base_path: String, root_name: String, script: Script) -> Dictionary:
62
- var base: Dictionary = FileReads.scene_node(base_path, ".")
62
+ var base: Dictionary = GdhFileReads.scene_node(base_path, ".")
63
63
  if not base.get("ok", false):
64
64
  return {"ok": false, "error": "inherits: " + str(base.get("error", "%s holds no nodes" % base_path))}
65
65
  var root_type: String = str(base["type"])
66
66
  var name: String = str(base["name"]) if root_name.is_empty() else root_name
67
- var unfit: String = SceneFile.unfit_name(name)
67
+ var unfit: String = GdhSceneFile.unfit_name(name)
68
68
  if not unfit.is_empty():
69
69
  return {"ok": false, "error": unfit}
70
70
  if script != null:
@@ -78,7 +78,7 @@ static func inherit(scene_path: String, base_path: String, root_name: String, sc
78
78
  % [script.resource_path, extended, root_type, base_path]
79
79
  )
80
80
  }
81
- var folder: String = SceneFile.make_folder_for(scene_path)
81
+ var folder: String = GdhSceneFile.make_folder_for(scene_path)
82
82
  if not folder.is_empty():
83
83
  return {"ok": false, "error": folder}
84
84
 
@@ -87,14 +87,14 @@ static func inherit(scene_path: String, base_path: String, root_name: String, sc
87
87
  "",
88
88
  (
89
89
  '[ext_resource type="PackedScene"%s path="%s" id="1_base"]'
90
- % [SceneFile.reference_uid(base_path), base_path]
90
+ % [GdhSceneFile.reference_uid(base_path), base_path]
91
91
  ),
92
92
  ]
93
93
  if script != null:
94
94
  var _resourced: bool = lines.append(
95
95
  (
96
96
  '[ext_resource type="Script"%s path="%s" id="2_script"]'
97
- % [SceneFile.reference_uid(script.resource_path), script.resource_path]
97
+ % [GdhSceneFile.reference_uid(script.resource_path), script.resource_path]
98
98
  )
99
99
  )
100
100
  lines.append_array(["", '[node name="%s" instance=ExtResource("1_base")]' % name])
@@ -117,7 +117,7 @@ static func inherit(scene_path: String, base_path: String, root_name: String, sc
117
117
  "error":
118
118
  "%s did not load back as a scene inheriting %s, so it was removed." % [scene_path, base_path]
119
119
  }
120
- SceneFile.refresh(scene_path)
120
+ GdhSceneFile.refresh(scene_path)
121
121
  return {
122
122
  "ok": true,
123
123
  "scenePath": scene_path,
@@ -130,8 +130,8 @@ static func inherit(scene_path: String, base_path: String, root_name: String, sc
130
130
  ## Makes the children of [param node], an instance in [param scene], editable or not, answering why
131
131
  ## it cannot or "". With them editable the scene keeps its own values for them, as the editor's
132
132
  ## Editable Children does; made not editable again, the values it kept for them are dropped. The
133
- ## caller has had [method SceneFile.foreign] refuse a node inside an instance that is not editable.
134
- static func set_editable(scene: SceneFile, node: Node, editable: bool) -> String:
133
+ ## caller has had [method GdhSceneFile.foreign] refuse a node inside an instance that is not editable.
134
+ static func set_editable(scene: GdhSceneFile, node: Node, editable: bool) -> String:
135
135
  if node.owner == null or node.scene_file_path.is_empty():
136
136
  return (
137
137
  "editableChildren is a setting of an instance in this scene, and %s is not one."
@@ -152,7 +152,7 @@ static func set_editable(scene: SceneFile, node: Node, editable: bool) -> String
152
152
  ## for a node deleted, they are only named. Answers followed, the scenes rewritten, and
153
153
  ## notFollowed, each one left as it was with why, each only when it names any, with what
154
154
  ## [param carried] gives.
155
- static func follow(changed: String, old_path: String, new_path: String, carried: Paths) -> Dictionary:
155
+ static func follow(changed: String, old_path: String, new_path: String, carried: GdhPaths) -> Dictionary:
156
156
  var texts: Dictionary = {}
157
157
  _read_scenes("res://", texts)
158
158
  var kin: Array[String] = [changed]
@@ -176,14 +176,14 @@ static func follow(changed: String, old_path: String, new_path: String, carried:
176
176
  var moves: Dictionary = {}
177
177
  if _inherits(scene_path, changed, texts):
178
178
  moves[old_path] = new_path
179
- var read: Dictionary = FileReads.scene_tree(scene_path, -1, false)
179
+ var read: Dictionary = GdhFileReads.scene_tree(scene_path, -1, false)
180
180
  if not read.get("ok", false):
181
181
  not_followed.append({"scenePath": scene_path, "reason": str(read.get("error", ""))})
182
182
  continue
183
183
  var tree: Dictionary = read["tree"]
184
184
  _instances_of(tree, changed, old_path, new_path, moves)
185
185
  var text: String = texts[scene_path]
186
- var unsaved: String = SceneFile.unsaved_in_editor(scene_path)
186
+ var unsaved: String = GdhSceneFile.unsaved_in_editor(scene_path)
187
187
  var rewritten: String = text
188
188
  if not new_path.is_empty():
189
189
  rewritten = _rewritten(text, moves, old_path.get_base_dir() != new_path.get_base_dir())
@@ -198,7 +198,7 @@ static func follow(changed: String, old_path: String, new_path: String, carried:
198
198
  )
199
199
  continue
200
200
  file.close()
201
- SceneFile.refresh(scene_path)
201
+ GdhSceneFile.refresh(scene_path)
202
202
  followed.append(scene_path)
203
203
  if moves.is_empty() or not text.contains("NodePath("):
204
204
  continue
@@ -335,7 +335,7 @@ static func _attribute_moved(
335
335
  var found: RegExMatch = RegEx.create_from_string(' %s="([^"]*)"' % attribute).search(line)
336
336
  if found == null:
337
337
  return line
338
- var moved: String = Paths.moved(found.get_string(1), moves)
338
+ var moved: String = GdhPaths.moved(found.get_string(1), moves)
339
339
  if moved == found.get_string(1):
340
340
  return line
341
341
  var placed: String = _attribute_set(line, attribute, moved)