gdharness 1.3.9 → 1.3.11

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 +4740 -3212
  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 +73 -13
  72. package/build/keeper.js +1371 -706
  73. package/package.json +1 -1
@@ -26,38 +26,109 @@ static func declared_on(target: Object, property: String) -> Dictionary:
26
26
  ## start with the name of one holding a resource built into it, then a slash and a key of that
27
27
  ## resource: "sources/0/0:0/0/probability" is the probability of tile 0:0 in the atlas source a
28
28
  ## TileSet keeps as sources/0. A colon path cannot reach it, since the tile's own name holds colons.
29
- ## A resource kept in a file of its own is not reached, nor, given the file [param home] that
30
- ## [param target] is saved in, one built into another file, since writing it here would leave that
31
- ## file as it was.
29
+ ## A list of resources is stepped into by an element's index: "lines/1/text" is the text of the
30
+ ## second resource in lines. A resource kept in a file of its own is not reached, nor, given the
31
+ ## file [param home] that [param target] is saved in, one built into another file, since writing it
32
+ ## here would leave that file as it was.
32
33
  static func holder_of(target: Object, key: String, home: String = "") -> Array:
33
34
  var through: Array[Resource] = []
34
35
  var holder: Object = target
35
36
  var name: String = key
36
37
  while declared_on(holder, name).is_empty():
37
38
  var reached: Resource = null
39
+ var rest: int = -1
38
40
  var at: int = name.find("/")
39
41
  while at > 0:
40
- var held: Resource = built_in_at(holder, name.left(at), home)
41
- if held != null:
42
+ var stepped: Array = _stepped(holder, name, at)
43
+ var held: Resource = stepped[0]
44
+ if held != null and _built_into(held, home):
42
45
  reached = held
46
+ rest = GdhRead.as_int(stepped[1])
43
47
  break
44
48
  at = name.find("/", at + 1)
45
49
  if reached == null:
46
50
  break
47
51
  through.append(reached)
48
52
  holder = reached
49
- name = name.substr(at + 1)
53
+ name = name.substr(rest)
50
54
  return [holder, name, through]
51
55
 
52
56
 
53
- ## The resource [param target] holds under [param property] when it is built into a file, or null;
54
- ## given [param home], only one built into that file or into none yet.
55
- static func built_in_at(target: Object, property: String, home: String = "") -> Resource:
56
- var held: Resource = held_at(target, property)
57
- if held == null or not held.is_built_in():
58
- return null
57
+ ## The resource the part of [param name] before the slash at [param at] reaches on [param target],
58
+ ## as [resource, where the rest of [param name] starts], or [null, -1]: the one held under that key,
59
+ ## or the element of the list held there that the step after the slash indexes.
60
+ static func _stepped(target: Object, name: String, at: int) -> Array:
61
+ var head: String = name.left(at)
62
+ var held: Resource = held_at(target, head)
63
+ if held != null:
64
+ return [held, at + 1]
65
+ var end: int = name.find("/", at + 1)
66
+ if end < 0 or declared_on(target, head).is_empty():
67
+ return [null, -1]
68
+ var index: String = name.substr(at + 1, end - at - 1)
69
+ var listed: Variant = target.get(head)
70
+ if not listed is Array or not index.is_valid_int():
71
+ return [null, -1]
72
+ var items: Array = listed
73
+ var place: int = index.to_int()
74
+ if place < 0 or place >= items.size() or not items[place] is Resource:
75
+ return [null, -1]
76
+ return [items[place], end + 1]
77
+
78
+
79
+ ## The refusal for [param name], which [param target] does not declare, when a step of it indexes a
80
+ ## list [param target] holds and names no resource in it; or "".
81
+ static func unlisted(target: Object, name: String) -> String:
82
+ var at: int = name.find("/")
83
+ while at > 0:
84
+ var head: String = name.left(at)
85
+ var listed: Variant = null if declared_on(target, head).is_empty() else target.get(head)
86
+ if listed is Array:
87
+ var items: Array = listed
88
+ return _unlisted_in(items, "%s.%s" % [class_called(target), head], name, at)
89
+ at = name.find("/", at + 1)
90
+ return ""
91
+
92
+
93
+ static func _unlisted_in(items: Array, called: String, name: String, at: int) -> String:
94
+ var head: String = name.left(at)
95
+ var end: int = name.find("/", at + 1)
96
+ var index: String = name.substr(at + 1) if end < 0 else name.substr(at + 1, end - at - 1)
97
+ if items.is_empty():
98
+ return "%s holds no elements, so %s names none: give %s a whole list first." % [called, name, head]
99
+ var last: int = items.size() - 1
100
+ if not index.is_valid_int():
101
+ return (
102
+ "%s is a list, so the step after it is the index of one of its elements, from 0 to %d, not %s."
103
+ % [called, last, index]
104
+ )
105
+ var place: int = index.to_int()
106
+ if place < 0 or place > last:
107
+ return (
108
+ "%s holds %d element%s, so %s names none: an index goes from 0 to %d."
109
+ % [called, items.size(), "" if items.size() == 1 else "s", name, last]
110
+ )
111
+ if not items[place] is Resource:
112
+ return (
113
+ "%s/%d holds %s, which has no properties to write: give %s a whole list instead."
114
+ % [head, place, type_string(typeof(items[place])), head]
115
+ )
116
+ if end < 0:
117
+ return (
118
+ (
119
+ "%s/%d is an element of %s, which a path does not replace alone: give %s a whole list, or "
120
+ + "write a property of the element, as %s/%d/<property>."
121
+ )
122
+ % [head, place, called, head, head, place]
123
+ )
124
+ return ""
125
+
126
+
127
+ static func _built_into(held: Resource, home: String) -> bool:
128
+ if not held.is_built_in():
129
+ return false
59
130
  var file: String = held.resource_path.get_slice("::", 0)
60
- return held if home.is_empty() or file.is_empty() or file == home else null
131
+ return home.is_empty() or file.is_empty() or file == home
61
132
 
62
133
 
63
134
  ## The file [param target] is saved in: its scene's for a node, as the nodes of an instance are
@@ -184,9 +255,11 @@ static func class_called(target: Object) -> String:
184
255
  static func held_elsewhere(target: Object, name: String, home: String) -> String:
185
256
  var at: int = name.find("/")
186
257
  while at > 0:
187
- var head: String = name.left(at)
188
- var rest: String = name.substr(at + 1)
189
- var held: Resource = held_at(target, head)
258
+ var stepped: Array = _stepped(target, name, at)
259
+ var held: Resource = stepped[0]
260
+ var past: int = GdhRead.as_int(stepped[1])
261
+ var head: String = "" if held == null else name.left(past - 1)
262
+ var rest: String = "" if held == null else name.substr(past)
190
263
  var file: String = "" if held == null else held.resource_path.get_slice("::", 0)
191
264
  if held != null and not held.is_built_in():
192
265
  return (
@@ -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)