gdharness 1.3.9 → 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 (73) hide show
  1. package/build/cli.js +4563 -3195
  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 +102 -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 +51 -1
  9. package/build/godot/addons/gdharness_editor/scene_file.gd +77 -140
  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 +11 -22
  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 +28 -5
  16. package/build/godot/addons/gdharness_editor/tools/animation_states.gd +39 -20
  17. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +49 -362
  18. package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +464 -0
  19. package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd.uid +1 -0
  20. package/build/godot/addons/gdharness_editor/tools/class_tools.gd +37 -0
  21. package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd +221 -0
  22. package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd.uid +1 -0
  23. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +147 -492
  24. package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +16 -0
  25. package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +4 -3
  26. package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +2 -1
  27. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +79 -5
  28. package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +7 -30
  29. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +183 -37
  30. package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd +485 -0
  31. package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd.uid +1 -0
  32. package/build/godot/addons/gdharness_editor/value_shapes.gd +128 -1
  33. package/build/godot/addons/gdharness_runtime/declared_values.gd +272 -0
  34. package/build/godot/addons/gdharness_runtime/reading.gd +51 -1
  35. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +35 -3
  36. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +185 -30
  37. package/build/godot/addons/gdharness_runtime/runtime_input.gd +86 -79
  38. package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +17 -0
  39. package/build/godot/addons/gdharness_runtime/runtime_pad.gd +13 -4
  40. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +309 -22
  41. package/build/godot/addons/gdharness_runtime/runtime_queries.gd +163 -20
  42. package/build/godot/addons/gdharness_runtime/runtime_screen.gd +49 -3
  43. package/build/godot/addons/gdharness_runtime/runtime_targets.gd +5 -2
  44. package/build/godot/addons/gdharness_runtime/runtime_values.gd +146 -5
  45. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +279 -11
  46. package/build/godot/addons/gdharness_runtime/runtime_words.gd +27 -0
  47. package/build/godot/addons/gdharness_runtime/serialisation.gd +11 -22
  48. package/build/godot/addons/gdharness_runtime/value_shapes.gd +128 -1
  49. package/build/godot/operations/animation_reads.gd +115 -4
  50. package/build/godot/operations/audio_buses.gd +93 -7
  51. package/build/godot/operations/classdb_queries.gd +119 -7
  52. package/build/godot/operations/declared_values.gd +272 -0
  53. package/build/godot/operations/dependencies.gd +154 -41
  54. package/build/godot/operations/editor_tools.gd +22 -14
  55. package/build/godot/operations/export_presets.gd +602 -4
  56. package/build/godot/operations/file_reads.gd +102 -18
  57. package/build/godot/operations/gdscript_authoring.gd +11 -1
  58. package/build/godot/operations/godot_operations.gd +16 -2
  59. package/build/godot/operations/import_pipeline.gd +20 -221
  60. package/build/godot/operations/input_actions.gd +108 -47
  61. package/build/godot/operations/project_config.gd +83 -69
  62. package/build/godot/operations/project_diagnostics.gd +4 -0
  63. package/build/godot/operations/project_validation.gd +266 -0
  64. package/build/godot/operations/property_paths.gd +89 -16
  65. package/build/godot/operations/property_values.gd +180 -121
  66. package/build/godot/operations/reading.gd +51 -1
  67. package/build/godot/operations/scene_connections.gd +222 -0
  68. package/build/godot/operations/serialisation.gd +11 -22
  69. package/build/godot/operations/setting_reads.gd +121 -0
  70. package/build/godot/operations/value_shapes.gd +128 -1
  71. package/build/godot/tests/listed.gd +20 -13
  72. package/build/keeper.js +1371 -706
  73. package/package.json +1 -1
@@ -15,6 +15,7 @@ extends RefCounted
15
15
  ## dictionary shaped like a vector or a bare pair of numbers can be read as one.
16
16
 
17
17
  const GdhDeclared = preload("declared_values.gd")
18
+ const GdhFileWalk = preload("file_walk.gd")
18
19
  const GdhPropertyPaths = preload("property_paths.gd")
19
20
  const GdhRead = preload("reading.gd")
20
21
  const GdhSame = preload("same_values.gd")
@@ -35,22 +36,27 @@ const THEME_ITEMS: Dictionary[String, Array] = {
35
36
  # written as a list of pairs, and only the property knows that each pair is a Vector2 rather than a
36
37
  # list of two numbers: without being told, every element was read as itself and the engine turned
37
38
  # the lot into zeroes on the way into the property.
38
- const PACKED_ELEMENTS: Dictionary = {
39
- TYPE_PACKED_BYTE_ARRAY: TYPE_INT,
40
- TYPE_PACKED_INT32_ARRAY: TYPE_INT,
41
- TYPE_PACKED_INT64_ARRAY: TYPE_INT,
42
- TYPE_PACKED_FLOAT32_ARRAY: TYPE_FLOAT,
43
- TYPE_PACKED_FLOAT64_ARRAY: TYPE_FLOAT,
44
- TYPE_PACKED_STRING_ARRAY: TYPE_STRING,
45
- TYPE_PACKED_VECTOR2_ARRAY: TYPE_VECTOR2,
46
- TYPE_PACKED_VECTOR3_ARRAY: TYPE_VECTOR3,
47
- TYPE_PACKED_VECTOR4_ARRAY: TYPE_VECTOR4,
48
- TYPE_PACKED_COLOR_ARRAY: TYPE_COLOR,
49
- }
39
+ const PACKED_ELEMENTS: Dictionary = GdhShapes.PACKED_ELEMENTS
50
40
 
51
41
  # Entries in a property list that only head a section of the inspector. Object.set ignores their
52
42
  # names, so a write to "Visibility" or "Transform" changed nothing and was answered as made.
53
43
  const HEADINGS: int = PROPERTY_USAGE_CATEGORY | PROPERTY_USAGE_GROUP | PROPERTY_USAGE_SUBGROUP
44
+ ## Control's layout_mode values, which the engine does not expose as constants.
45
+ const LAYOUT_MODE_POSITION: int = 0
46
+ const LAYOUT_MODE_ANCHORS: int = 1
47
+ ## What a container sets on each child it lays out, beside the anchors and offsets.
48
+ const PLACED_BY_A_CONTAINER: Array[String] = ["position", "size", "global_position"]
49
+
50
+ ## The property the last write_all refused because it did not hold what it was given, or "".
51
+ var unheld: String = ""
52
+
53
+ ## The stored properties the last write_all's own writes moved beside the ones they were given, as a
54
+ ## set: the Anchors mode an anchor needs, and what an engine setter keeps in step, such as the
55
+ ## anchors an anchor preset sets. Only writes no script code runs for count. A headless write that
56
+ ## attaches a script that is not a tool puts back whatever the call did not name, as that script's
57
+ ## doing, which put a panel given a script and a bottom-wide preset back in Position mode, where its
58
+ ## anchors are not stored.
59
+ var moved_by_engine: Dictionary = {}
54
60
 
55
61
  var _values: GdhSerialisation = GdhSerialisation.new()
56
62
 
@@ -69,6 +75,10 @@ var _also_changed: Dictionary = {}
69
75
  # the inspector would not have let it be entered, so a write answered as made said nothing of it.
70
76
  var _outside_declared: Dictionary = {}
71
77
 
78
+ # The node paths the last write_all wrote that name no node from the node written, by property, as
79
+ # a sentence saying so.
80
+ var _names_no_node: Dictionary = {}
81
+
72
82
 
73
83
  ## Turns what arrived over the wire into the Godot value a property wants.
74
84
  ##
@@ -115,7 +125,7 @@ func cannot_hold(
115
125
  if not GdhSerialisation.acceptable(value, expected_type):
116
126
  var shape: String = "%s.%s is %s and the value given is %s, which cannot become one."
117
127
  var remedy: String = GdhShapes.remedy(value, expected_type)
118
- return shape % [holder, property, held, type_string(typeof(value))] + remedy + cost
128
+ return shape % [holder, property, held, _kind(value)] + remedy + cost
119
129
 
120
130
  # And the same question of each element, because a list is accepted for a packed or typed array
121
131
  # whatever is in it: a polygon written as [{"x": 1}] passes the check above and lands as one zero
@@ -128,10 +138,20 @@ func cannot_hold(
128
138
  for index: int in items.size():
129
139
  if not GdhSerialisation.acceptable(items[index], element):
130
140
  var shape: String = "%s.%s is %s and item %d of the list given is %s, not %s."
131
- var named: Array = [
132
- holder, property, held, index, type_string(typeof(items[index])), type_string(element)
133
- ]
141
+ var named: Array = [holder, property, held, index, _kind(items[index]), type_string(element)]
134
142
  return shape % named + GdhShapes.remedy(items[index], element) + cost
143
+ # A list of polygons is a list of lists of points, and each point is read as one too.
144
+ var inner: int = GdhRead.as_int(PACKED_ELEMENTS.get(element, TYPE_NIL), TYPE_NIL)
145
+ if inner == TYPE_NIL or not items[index] is Array:
146
+ continue
147
+ var points: Array = items[index]
148
+ for at: int in points.size():
149
+ if not GdhSerialisation.acceptable(points[at], inner):
150
+ var nested: String = "%s.%s holds lists of %s, and item %d of list %d given is %s, not %s."
151
+ var said: Array = [
152
+ holder, property, type_string(inner), at, index, _kind(points[at]), type_string(inner)
153
+ ]
154
+ return nested % said + GdhShapes.remedy(points[at], inner) + cost
135
155
  if expected_type == TYPE_DICTIONARY and elements.size() == 2 and value is Dictionary:
136
156
  var entries: Dictionary = value
137
157
  var key_type: int = GdhRead.as_int(elements[0], TYPE_NIL)
@@ -148,6 +168,14 @@ func cannot_hold(
148
168
  return ""
149
169
 
150
170
 
171
+ ## What [param value] is, as a refusal names it. A number arrives from JSON as a float whether or not
172
+ ## it has a fraction, so [1, 2] given for a list of words was called a list of floats.
173
+ static func _kind(value: Variant) -> String:
174
+ if value is int or value is float:
175
+ return "a number"
176
+ return type_string(typeof(value))
177
+
178
+
151
179
  ## Writes each of [param properties] onto [param target], stopping at the first it cannot write and
152
180
  ## answering with why, or "" when every one of them went on and holds what was given.
153
181
  ##
@@ -170,12 +198,24 @@ func cannot_hold(
170
198
  func write_all(target: Object, properties: Dictionary) -> String:
171
199
  _also_changed = {}
172
200
  _outside_declared = {}
201
+ _names_no_node = {}
202
+ moved_by_engine = {}
203
+ unheld = ""
173
204
  var named: Dictionary = {}
174
205
  for key: Variant in properties:
175
206
  named[str(key).get_slice(":", 0)] = true
176
207
  var before: Dictionary = _stored(target, named)
177
208
  var given: Dictionary = {}
178
209
  var declarations: Dictionary = {}
210
+ # How many writes had been made when each property was last written. One written last is not
211
+ # written again: the node is as that write left it, so the engine refuses it the same way and
212
+ # says so a second time.
213
+ var written_at: Dictionary[String, int] = {}
214
+ var writes: int = 0
215
+ # Which other property's write last changed each one already written, by name. A Gradient keeps
216
+ # one offset for each colour, so offsets given after colors cut them back, and the refusal said
217
+ # only that the engine had changed the colours.
218
+ var undone_by: Dictionary[String, String] = {}
179
219
  # A refusal of one of several leaves the others unwritten too, which a refusal naming only the
180
220
  # one it refused left a caller to guess.
181
221
  var unwritten: String = (
@@ -198,6 +238,9 @@ func write_all(target: Object, properties: Dictionary) -> String:
198
238
  var elsewhere: String = GdhPropertyPaths.held_elsewhere(holder, name, home)
199
239
  if not elsewhere.is_empty():
200
240
  return elsewhere + unwritten
241
+ var unlisted: String = GdhPropertyPaths.unlisted(holder, name)
242
+ if not unlisted.is_empty():
243
+ return unlisted + unwritten
201
244
  var ungrown: String = GdhPropertyPaths.grow(holder, name, GdhPropertyPaths.class_called(holder))
202
245
  if not ungrown.is_empty():
203
246
  return ungrown + unwritten
@@ -207,9 +250,16 @@ func write_all(target: Object, properties: Dictionary) -> String:
207
250
  if declared.has("refused"):
208
251
  return str(declared["refused"]) + unwritten
209
252
  if declared.is_empty() and holder is ShaderMaterial and name.begins_with("shader_parameter/"):
210
- return _no_uniform(holder as ShaderMaterial, name) + unwritten
253
+ return GdhDeclared.no_uniform(holder as ShaderMaterial, name) + unwritten
211
254
  if declared.is_empty():
212
255
  return no_property(holder, name) + unwritten
256
+ var unscripted: Variant = null
257
+ if holder == target and not GdhDeclared.runs_script_code(target, name):
258
+ unscripted = _stored(target, {})
259
+ if holder is Control:
260
+ var unplaced: String = _laid_out(holder as Control, name)
261
+ if not unplaced.is_empty():
262
+ return unplaced + unwritten
213
263
  var value: Array = value_for(holder, declared, properties[key])
214
264
  if not value[0]:
215
265
  return str(value[1]) + unwritten
@@ -217,91 +267,91 @@ func write_all(target: Object, properties: Dictionary) -> String:
217
267
  if not outside.is_empty():
218
268
  _outside_declared[property] = outside
219
269
  _write(target, property, value[1])
270
+ if unscripted is Dictionary:
271
+ var was: Dictionary = unscripted
272
+ var now: Dictionary = _stored(target, {})
273
+ for moved: String in now:
274
+ if moved != name.get_slice(":", 0) and not GdhSame.same(now[moved], was.get(moved)):
275
+ moved_by_engine[moved] = true
276
+ writes += 1
277
+ written_at[property] = writes
278
+ _note_undone(target, property, given, undone_by)
220
279
  given[property] = value[1]
221
280
  declarations[property] = declared
222
281
 
223
282
  for property: String in given:
224
- if not GdhSame.same(read_at(target, property), given[property]):
283
+ if written_at[property] < writes and not GdhSame.same(read_at(target, property), given[property]):
225
284
  _write(target, property, given[property])
285
+ writes += 1
286
+ written_at[property] = writes
287
+ _note_undone(target, property, given, undone_by)
226
288
  for property: String in given:
227
289
  var held: Variant = read_at(target, property)
228
290
  if not GdhSame.same(held, given[property]):
291
+ unheld = property
292
+ if undone_by.has(property):
293
+ return (
294
+ (
295
+ "%s.%s was given %s and holds %s once %s was set as well: the engine keeps the two "
296
+ + "in step, so they cannot hold these values together. Nothing was written."
297
+ )
298
+ % [
299
+ GdhPropertyPaths.class_called(target),
300
+ property,
301
+ shown(given[property]),
302
+ shown(held),
303
+ undone_by[property]
304
+ ]
305
+ )
229
306
  var declared: Dictionary = declarations[property]
230
- return _not_held(target, property, given[property], held, declared)
307
+ return GdhDeclared.not_held(
308
+ GdhPropertyPaths.class_called(target),
309
+ target.get_class(),
310
+ property,
311
+ declared,
312
+ [given[property], shown(given[property])],
313
+ shown(held)
314
+ )
231
315
  for property: String in before:
232
316
  var now: Variant = target.get(property)
233
317
  if not GdhSame.same(now, before[property]):
234
318
  _also_changed[property] = [before[property], now]
319
+ if target is Node:
320
+ _names_no_node = GdhDeclared.naming_nothing(target as Node, given)
235
321
  return ""
236
322
 
237
323
 
238
- ## Why [param property] of [param target], declared as [param declared], holds [param held] after
239
- ## it was given [param value]. A text property with a list of names holds only one of them, and its
240
- ## getter answers its default for any other. A player's bus is one, though the engine fills its list
241
- ## only in the inspector, so the buses are read from the AudioServer, which holds the project's
242
- ## layout; "the engine changed or refused the value" left a caller to guess the bus did not exist.
243
- func _not_held(
244
- target: Object, property: String, value: Variant, held: Variant, declared: Dictionary
245
- ) -> String:
246
- var said: String = (
247
- "%s.%s was given %s and holds %s after it was set"
248
- % [GdhPropertyPaths.class_called(target), property, shown(value), shown(held)]
249
- )
250
- var type: int = GdhRead.as_int(declared.get("type", TYPE_NIL))
251
- var hint: int = GdhRead.as_int(declared.get("hint", PROPERTY_HINT_NONE))
252
- var names: PackedStringArray = []
253
- if hint == PROPERTY_HINT_ENUM and (type == TYPE_STRING or type == TYPE_STRING_NAME):
254
- names = PackedStringArray(GdhDeclared.enum_values(str(declared.get("hint_string", ""))).keys())
255
- var bus: bool = property == "bus" and target.get_class().begins_with("AudioStreamPlayer")
256
- if bus:
257
- names = []
258
- for index: int in AudioServer.bus_count:
259
- var _added: bool = names.append(AudioServer.get_bus_name(index))
260
- if names.is_empty() or names.has(str(value)):
261
- return said + ": the engine changed or refused the value, so nothing was written."
262
- var close: PackedStringArray = GdhRead.nearest(str(value), names)
263
- var nearest: String = (
264
- "%s %s" % ["is" if close.size() == 1 else "are", ", ".join(close)] if not close.is_empty() else ""
265
- )
266
- if bus:
324
+ ## Notes in [param undone_by] each property of [param given] that writing [param written] on
325
+ ## [param target] left holding something else.
326
+ func _note_undone(
327
+ target: Object, written: String, given: Dictionary, undone_by: Dictionary[String, String]
328
+ ) -> void:
329
+ for other: String in given:
330
+ if other != written and not GdhSame.same(read_at(target, other), given[other]):
331
+ undone_by[other] = written
332
+
333
+
334
+ ## Why [param name] cannot be written on [param control], or "" once it can. A container places and
335
+ ## sizes its children and the scene keeps none of that for them, so their anchors, offsets, position
336
+ ## and size are refused with what a child does set. A control in Position mode ignores an anchor
337
+ ## preset, and the inspector offers anchors only in Anchors mode, so writing one puts it there first,
338
+ ## as picking a preset in the editor does; alsoChanged then names layout_mode.
339
+ func _laid_out(control: Control, name: String) -> String:
340
+ var base: String = name.get_slice(":", 0)
341
+ var anchoring: bool = base == "anchors_preset" or base.begins_with("anchor_")
342
+ var parent: Node = control.get_parent()
343
+ if parent is Container and (anchoring or base.begins_with("offset_") or base in PLACED_BY_A_CONTAINER):
267
344
  return (
268
345
  (
269
- "%s: it takes the name of one of the project's audio buses, and %s is none of them; %s. "
270
- + "project_info include audio_buses lists them, and project_settings add_audio_bus adds "
271
- + "one. Nothing was written."
346
+ "%s's parent, %s, is a %s, which places and sizes it, so its %s is the container's to set "
347
+ + "and the scene keeps none of it. A child of a container is laid out with "
348
+ + "size_flags_horizontal, size_flags_vertical and custom_minimum_size."
272
349
  )
273
- % [
274
- said,
275
- shown(value),
276
- "the buses are " + ", ".join(names) if nearest.is_empty() else "the nearest " + nearest
277
- ]
350
+ % [control.name, parent.name, parent.get_class(), base]
278
351
  )
279
- return (
280
- "%s: it takes one of the names it lists, and %s is none of them; %s. Nothing was written."
281
- % [
282
- said,
283
- shown(value),
284
- "it lists " + ", ".join(names) if nearest.is_empty() else "the nearest it lists " + nearest
285
- ]
286
- )
287
-
288
-
289
- ## Why [param material] has no [param property]: a ShaderMaterial's parameters are its shader's
290
- ## uniforms, so the shader is what lacks one, and "ShaderMaterial has no property" named the
291
- ## material.
292
- static func _no_uniform(material: ShaderMaterial, property: String) -> String:
293
- var uniform: String = property.trim_prefix("shader_parameter/")
294
- if material.shader == null:
295
- return "The ShaderMaterial has no shader, so it has no parameter %s yet; set shader first." % uniform
296
- var shader: String = material.shader.resource_path
297
- var named: String = shader if not shader.is_empty() else "Its shader"
298
- var uniforms: Array = []
299
- for entry: Dictionary in material.shader.get_shader_uniform_list():
300
- uniforms.append(str(entry["name"]))
301
- var said: String = "%s declares no uniform %s, which %s would set" % [named, uniform, property]
302
- if uniforms.is_empty():
303
- return said + "; it declares no uniforms."
304
- return GdhRead.offered(said, uniform, uniforms) + "."
352
+ if anchoring and GdhRead.as_int(control.get("layout_mode")) == LAYOUT_MODE_POSITION:
353
+ control.set("layout_mode", LAYOUT_MODE_ANCHORS)
354
+ return ""
305
355
 
306
356
 
307
357
  ## Puts under alsoChanged in [param answer] each property the last [method write_all] changed
@@ -312,6 +362,8 @@ static func _no_uniform(material: ShaderMaterial, property: String) -> String:
312
362
  func note_write(answer: Dictionary, relative_to: Node = null) -> void:
313
363
  if not _outside_declared.is_empty():
314
364
  answer["outsideDeclared"] = _outside_declared.duplicate()
365
+ if not _names_no_node.is_empty():
366
+ answer["namesNoNode"] = _names_no_node.duplicate()
315
367
  if _also_changed.is_empty():
316
368
  return
317
369
  var noted: Dictionary = {}
@@ -393,16 +445,25 @@ func _component_slot(target: Object, path: String) -> Dictionary:
393
445
  var has: PackedStringArray = GdhSerialisation.components_of(value)
394
446
  var sort: String = GdhRead.with_article(type_string(typeof(value)))
395
447
  if has.is_empty():
396
- return {
397
- "refused":
448
+ var refused: String = (
398
449
  (
399
- (
400
- "%s.%s is %s, which a path does not step into: a path reaches the components of a "
401
- + "vector, a colour, a rectangle or a transform."
402
- )
403
- % [owner, walked, sort]
450
+ "%s.%s is %s, which a path does not step into: a path reaches the components of a "
451
+ + "vector, a colour, a rectangle or a transform."
404
452
  )
405
- }
453
+ % [owner, walked, sort]
454
+ )
455
+ # A resource is reached with a slash, which a caller writing a colon had no way to know.
456
+ if value is Resource:
457
+ refused += (
458
+ " A property of the resource it holds is reached with a slash: %s/%s."
459
+ % [walked, ":".join(steps.slice(index))]
460
+ )
461
+ elif value is Array and index + 1 < steps.size():
462
+ refused += (
463
+ " A property of a resource in it is reached with slashes and the element's index: %s/%s/%s."
464
+ % [walked, step, ":".join(steps.slice(index + 1))]
465
+ )
466
+ return {"refused": refused}
406
467
  if not step in has:
407
468
  return {
408
469
  "refused":
@@ -437,7 +498,16 @@ func value_for(target: Object, declared: Dictionary, raw: Variant) -> Array:
437
498
  value = found[1]
438
499
  else:
439
500
  var elements: Array = GdhDeclared.elements_of(declared)
440
- if _fraction(raw, expected_type, elements):
501
+ # Some engine properties declare a bare Array and hold a typed one: NavigationPolygon's
502
+ # outlines hold PackedVector2Arrays, and points written as {"x", "y"} inside them landed as
503
+ # zero vectors, since nothing said what the inner lists hold.
504
+ if elements.is_empty() and expected_type == TYPE_ARRAY:
505
+ var current: Variant = target.get(property)
506
+ if current is Array:
507
+ var typed: Array = current
508
+ if typed.is_typed() and typed.get_typed_builtin() != TYPE_OBJECT:
509
+ elements = [typed.get_typed_builtin()]
510
+ if GdhShapes.fraction(raw, expected_type, elements):
441
511
  return [false, _fraction_refusal(target, property, raw)]
442
512
  var rounded: String = GdhSerialisation.inexact(raw, expected_type)
443
513
  if not rounded.is_empty():
@@ -610,34 +680,6 @@ func _object_item(target: Object, given: Variant, kind: Array, holding: String,
610
680
  return [false, "%s, and %s is %s." % [holding, place[0], named]]
611
681
 
612
682
 
613
- ## Whether [param value] holds a number with a fractional part where an integer is wanted, in a
614
- ## list or a dictionary as much as bare. The conversion would cut 2.7 to 2 without a word, and a
615
- ## dictionary key arrives as the text "2.7", which reads as the same number.
616
- func _fraction(value: Variant, type: int, elements: Array = []) -> bool:
617
- if type == TYPE_INT:
618
- var number: Variant = value
619
- if typeof(value) == TYPE_STRING:
620
- number = GdhRead.json_or_null(str(value))
621
- if typeof(number) != TYPE_FLOAT:
622
- return false
623
- var whole: float = number
624
- return is_finite(whole) and whole != floorf(whole)
625
- var element: int = GdhRead.as_int(PACKED_ELEMENTS.get(type, TYPE_NIL), TYPE_NIL)
626
- if type == TYPE_ARRAY and not elements.is_empty():
627
- element = GdhRead.as_int(elements[0], TYPE_NIL)
628
- if value is Array and element != TYPE_NIL:
629
- var items: Array = value
630
- return items.any(func(item: Variant) -> bool: return _fraction(item, element))
631
- if type == TYPE_DICTIONARY and elements.size() == 2 and value is Dictionary:
632
- var entries: Dictionary = value
633
- var key_type: int = GdhRead.as_int(elements[0], TYPE_NIL)
634
- var value_type: int = GdhRead.as_int(elements[1], TYPE_NIL)
635
- for key: Variant in entries:
636
- if _fraction(key, key_type) or _fraction(entries[key], value_type):
637
- return true
638
- return false
639
-
640
-
641
683
  func _fraction_refusal(target: Object, property: String, raw: Variant) -> String:
642
684
  return (
643
685
  (
@@ -672,12 +714,15 @@ func colour_from(raw: Variant, named: String) -> Array:
672
714
  ## The refusal for [param property], which [param target] does not declare, naming the declared
673
715
  ## ones nearest it: "StyleBoxFlat has no property shadow_sizee" left the caller nothing to send.
674
716
  static func no_property(target: Object, property: String) -> String:
675
- return (
717
+ var said: String = (
676
718
  GdhRead.near_property(
677
719
  "%s has no property %s" % [GdhPropertyPaths.class_called(target), property], target, property
678
720
  )
679
721
  + "."
680
722
  )
723
+ if not target is Node:
724
+ return said
725
+ return said + GdhDeclared.held_by_children(target as Node, property)
681
726
 
682
727
 
683
728
  ## The resource [param path] names, as [found, resource or why not].
@@ -716,7 +761,11 @@ func _resource_at(path: String, property: String) -> Array:
716
761
  % [path, property, path]
717
762
  )
718
763
  ]
719
- return [false, "No resource at %s for %s" % [path, property]]
764
+ if path.begins_with("uid://"):
765
+ return [false, "No resource at %s for %s" % [path, property]]
766
+ return [
767
+ false, GdhFileWalk.new().with_nearest_files("No resource at %s for %s" % [path, property], path)
768
+ ]
720
769
  return [true, load(path)]
721
770
 
722
771
 
@@ -893,6 +942,9 @@ func _parse_array(value: Array, expected_type: int, elements: Array) -> Variant:
893
942
  var element: int = GdhRead.as_int(PACKED_ELEMENTS[expected_type], TYPE_NIL)
894
943
  return value.map(func(item: Variant) -> Variant: return parse(item, element))
895
944
 
945
+ # Read by its places, a word was read as 0 and ["a", "b"] written as (0, 0).
946
+ if GdhShapes.NUMBER_FIELDS.has(type_string(expected_type)) and not GdhShapes.all_numbers(value):
947
+ return value
896
948
  match expected_type:
897
949
  TYPE_VECTOR2:
898
950
  if value.size() == 2:
@@ -933,5 +985,12 @@ func _parse_array(value: Array, expected_type: int, elements: Array) -> Variant:
933
985
  return items
934
986
  if not items.all(func(item: Variant) -> bool: return GdhSerialisation.acceptable(item, element_type)):
935
987
  return items
988
+ # Converted, a list of points holding a word became a packed list of zeroes before anything read it.
989
+ var inner: int = GdhRead.as_int(PACKED_ELEMENTS.get(element_type, TYPE_NIL), TYPE_NIL)
990
+ if inner != TYPE_NIL:
991
+ for item: Variant in items:
992
+ var points: Array = item if item is Array else []
993
+ if not points.all(func(point: Variant) -> bool: return GdhSerialisation.acceptable(point, inner)):
994
+ return items
936
995
  var fitted: Array = items.map(func(item: Variant) -> Variant: return type_convert(item, element_type))
937
996
  return Array(fitted, element_type, &"", null)
@@ -193,7 +193,13 @@ static func nearest(word: String, names: PackedStringArray) -> PackedStringArray
193
193
  func(a: String, b: String) -> bool:
194
194
  return a.to_lower().similarity(wanted) > b.to_lower().similarity(wanted)
195
195
  )
196
- return PackedStringArray(close.slice(0, 3))
196
+ # Every name that only adds to the word stays, since cutting at three named three corners of
197
+ # corner_radius and left out the fourth as if it were not there.
198
+ var named: PackedStringArray = PackedStringArray(close.slice(0, 3))
199
+ for name: String in close.slice(3):
200
+ if name.to_lower().begins_with(wanted):
201
+ var _added: bool = named.append(name)
202
+ return named
197
203
 
198
204
 
199
205
  ## [param said], the refusal for [param word], which is not among [param names], with those nearest
@@ -216,6 +222,10 @@ static func near_property(said: String, target: Object, property: String) -> Str
216
222
  var scripted: Array[String] = []
217
223
  for declared: Dictionary in target.get_property_list():
218
224
  var usage: int = as_int(declared.get("usage", 0))
225
+ if usage & PROPERTY_USAGE_ARRAY:
226
+ var listed: String = _list_called(target, declared, property)
227
+ if not listed.is_empty():
228
+ return "%s; %s" % [said, listed]
219
229
  if not usage & headings:
220
230
  names.append(str(declared.get("name", "")))
221
231
  if usage & PROPERTY_USAGE_SCRIPT_VARIABLE and not usage & headings:
@@ -229,6 +239,46 @@ static func near_property(said: String, target: Object, property: String) -> Str
229
239
  return "%s; its script declares %s" % [said, ", ".join(PackedStringArray(scripted))]
230
240
 
231
241
 
242
+ ## How [param target] keeps the list [param property] names, when that is the label of one of the
243
+ ## engine's numbered lists, such as an OptionButton's Items; empty when it is not. The engine has no
244
+ ## property by that name, only a count and each entry's fields under a numbered prefix, so "items"
245
+ ## was refused with nothing near it to offer.
246
+ static func _list_called(target: Object, count: Dictionary, property: String) -> String:
247
+ var parts: PackedStringArray = str(count.get("class_name", "")).split(",")
248
+ if parts.size() != 2 or _bare(parts[0]) != _bare(property):
249
+ return ""
250
+ var counted: String = str(count.get("name", ""))
251
+ var first: String = parts[1] + "0/"
252
+ var fields: PackedStringArray = _fields_at(target, first)
253
+ if fields.is_empty() and ClassDB.can_instantiate(target.get_class()):
254
+ var fresh: Object = ClassDB.instantiate(target.get_class())
255
+ fresh.set(counted, 1)
256
+ fields = _fields_at(fresh, first)
257
+ if not fresh is RefCounted:
258
+ fresh.free()
259
+ var each: String = "each one's fields" if fields.is_empty() else "each one's %s" % ", ".join(fields)
260
+ return (
261
+ (
262
+ "it keeps its %s as %s, how many there are, and %s under %s for the first, %s for the second and so on: "
263
+ + "give %s before the fields of the entries it makes"
264
+ )
265
+ % [parts[0].to_lower(), counted, each, first, parts[1] + "1/", counted]
266
+ )
267
+
268
+
269
+ static func _fields_at(target: Object, prefix: String) -> PackedStringArray:
270
+ var fields: PackedStringArray = PackedStringArray()
271
+ for declared: Dictionary in target.get_property_list():
272
+ var name: String = str(declared.get("name", ""))
273
+ if name.begins_with(prefix):
274
+ var _added: bool = fields.append(name.trim_prefix(prefix))
275
+ return fields
276
+
277
+
278
+ static func _bare(word: String) -> String:
279
+ return word.to_lower().replace("_", "").replace(" ", "")
280
+
281
+
232
282
  ## The signals [param object] has that a caller can connect, its own class's first: the engine
233
283
  ## lists them from Object down, which puts a Button's pressed after thirty a Control has, and names
234
284
  ## starting with an underscore are the engine's own.