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
@@ -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)
@@ -24,6 +24,23 @@ extends RefCounted
24
24
 
25
25
  ## How many of a node's children a refusal for a path missing below it names.
26
26
  const NAMED_CHILDREN: int = 12
27
+ ## The types holding their floats as the engine's real numbers, 32-bit unless it was built for 64.
28
+ const SHAPES_OF_REALS: Array[int] = [
29
+ TYPE_VECTOR2,
30
+ TYPE_VECTOR3,
31
+ TYPE_VECTOR4,
32
+ TYPE_RECT2,
33
+ TYPE_PLANE,
34
+ TYPE_QUATERNION,
35
+ TYPE_AABB,
36
+ TYPE_BASIS,
37
+ TYPE_TRANSFORM2D,
38
+ TYPE_TRANSFORM3D,
39
+ TYPE_PROJECTION,
40
+ TYPE_PACKED_VECTOR2_ARRAY,
41
+ TYPE_PACKED_VECTOR3_ARRAY,
42
+ TYPE_PACKED_VECTOR4_ARRAY,
43
+ ]
27
44
 
28
45
 
29
46
  static func as_int(value: Variant, fallback: int = 0) -> int:
@@ -176,7 +193,13 @@ static func nearest(word: String, names: PackedStringArray) -> PackedStringArray
176
193
  func(a: String, b: String) -> bool:
177
194
  return a.to_lower().similarity(wanted) > b.to_lower().similarity(wanted)
178
195
  )
179
- 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
180
203
 
181
204
 
182
205
  ## [param said], the refusal for [param word], which is not among [param names], with those nearest
@@ -190,18 +213,70 @@ static func offered(said: String, word: String, names: Array) -> String:
190
213
 
191
214
 
192
215
  ## [param said], the refusal for [param property], which [param target] does not declare, with the
193
- ## names it declares nearest it when any are, as a misspelt one is usually one of them. Unlike
194
- ## [method offered], nothing is listed when none is near, since an object declares a hundred.
216
+ ## names it declares nearest it when any are, as a misspelt one is usually one of them, or else the
217
+ ## variables its script declares. The engine's own are not listed when none is near, since an object
218
+ ## has a hundred, while a script's are the few a caller most likely meant.
195
219
  static func near_property(said: String, target: Object, property: String) -> String:
196
220
  var headings: int = PROPERTY_USAGE_CATEGORY | PROPERTY_USAGE_GROUP | PROPERTY_USAGE_SUBGROUP
197
221
  var names: Array[String] = []
222
+ var scripted: Array[String] = []
198
223
  for declared: Dictionary in target.get_property_list():
199
- if not as_int(declared.get("usage", 0)) & headings:
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]
229
+ if not usage & headings:
200
230
  names.append(str(declared.get("name", "")))
231
+ if usage & PROPERTY_USAGE_SCRIPT_VARIABLE and not usage & headings:
232
+ scripted.append(str(declared.get("name", "")))
201
233
  var close: PackedStringArray = nearest(property, PackedStringArray(names))
202
- if close.is_empty():
234
+ if not close.is_empty():
235
+ return "%s; the nearest it has %s %s" % [said, "is" if close.size() == 1 else "are", ", ".join(close)]
236
+ if scripted.is_empty():
203
237
  return said
204
- return "%s; the nearest it has %s %s" % [said, "is" if close.size() == 1 else "are", ", ".join(close)]
238
+ scripted.sort()
239
+ return "%s; its script declares %s" % [said, ", ".join(PackedStringArray(scripted))]
240
+
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(" ", "")
205
280
 
206
281
 
207
282
  ## The signals [param object] has that a caller can connect, its own class's first: the engine
@@ -219,3 +294,34 @@ static func signals_of(object: Object) -> Array:
219
294
  ## [param word] after the article it takes, "an AnimationTree" or "a Node2D".
220
295
  static func with_article(word: String) -> String:
221
296
  return ("an " if word.left(1).to_upper() in ["A", "E", "I", "O", "U"] else "a ") + word
297
+
298
+
299
+ ## [param value], which the engine holds as a 32-bit float, as the shortest decimal that is still
300
+ ## that float. JSON carries a double, and the double a 32-bit 0.2 widens to is 0.200000002980232:
301
+ ## the same number, but not the one written or the one the file shows, so a caller comparing what it
302
+ ## set against what it read saw them differ. The shorter one, sent back, stores the same 32 bits. A
303
+ ## plain float is left alone by the serialiser, since nothing says whether the engine holds it in
304
+ ## 32 bits or 64.
305
+ static func single(value: float) -> float:
306
+ if not is_finite(value) or value == 0.0:
307
+ return value
308
+ var held: float = PackedFloat32Array([value])[0]
309
+ var magnitude: int = floori(log(absf(held)) / log(10.0))
310
+ for digits: int in range(1, 10):
311
+ var shift: int = digits - 1 - magnitude
312
+ var tried: float = (
313
+ roundf(held * pow(10.0, shift)) / pow(10.0, shift)
314
+ if shift >= 0
315
+ else roundf(held / pow(10.0, -shift)) * pow(10.0, -shift)
316
+ )
317
+ if PackedFloat32Array([tried])[0] == held:
318
+ return tried
319
+ return held
320
+
321
+
322
+ ## Whether a value of [param type] holds its floats in 32 bits: a colour always, and the vectors and
323
+ ## the shapes built of them unless the engine was built for 64.
324
+ static func holds_singles(type: int) -> bool:
325
+ if type in [TYPE_COLOR, TYPE_PACKED_COLOR_ARRAY, TYPE_PACKED_FLOAT32_ARRAY]:
326
+ return true
327
+ return not OS.has_feature("double") and type in SHAPES_OF_REALS