gdharness 1.3.0 → 1.3.1

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 (68) hide show
  1. package/build/cli.js +14 -13
  2. package/build/godot/addons/gdharness_editor/animation_reads.gd +7 -19
  3. package/build/godot/addons/gdharness_editor/bridge_client.gd +8 -8
  4. package/build/godot/addons/gdharness_editor/file_reads.gd +60 -25
  5. package/build/godot/addons/gdharness_editor/headless_check.gd +7 -7
  6. package/build/godot/addons/gdharness_editor/plugin.gd +6 -6
  7. package/build/godot/addons/gdharness_editor/property_values.gd +120 -53
  8. package/build/godot/addons/gdharness_editor/reading.gd +28 -0
  9. package/build/godot/addons/gdharness_editor/scene_file.gd +28 -23
  10. package/build/godot/addons/gdharness_editor/serialisation.gd +45 -94
  11. package/build/godot/addons/gdharness_editor/tool_executor.gd +24 -24
  12. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +52 -41
  13. package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +4 -4
  14. package/build/godot/addons/gdharness_editor/tools/play_tools.gd +2 -2
  15. package/build/godot/addons/gdharness_editor/tools/profile_tools.gd +14 -14
  16. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +48 -41
  17. package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +17 -17
  18. package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +14 -12
  19. package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +7 -7
  20. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +81 -77
  21. package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +4 -4
  22. package/build/godot/addons/gdharness_editor/value_shapes.gd +111 -0
  23. package/build/godot/addons/gdharness_editor/value_shapes.gd.uid +1 -0
  24. package/build/godot/addons/gdharness_runtime/reading.gd +28 -0
  25. package/build/godot/addons/gdharness_runtime/runtime_accessibility.gd +18 -18
  26. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +29 -29
  27. package/build/godot/addons/gdharness_runtime/runtime_capture.gd +21 -18
  28. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +86 -80
  29. package/build/godot/addons/gdharness_runtime/runtime_input.gd +65 -64
  30. package/build/godot/addons/gdharness_runtime/runtime_items.gd +6 -6
  31. package/build/godot/addons/gdharness_runtime/runtime_menus.gd +8 -8
  32. package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +6 -6
  33. package/build/godot/addons/gdharness_runtime/runtime_pad.gd +10 -10
  34. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +7 -7
  35. package/build/godot/addons/gdharness_runtime/runtime_queries.gd +41 -41
  36. package/build/godot/addons/gdharness_runtime/runtime_says.gd +3 -3
  37. package/build/godot/addons/gdharness_runtime/runtime_targets.gd +33 -33
  38. package/build/godot/addons/gdharness_runtime/runtime_typing.gd +20 -20
  39. package/build/godot/addons/gdharness_runtime/runtime_values.gd +25 -24
  40. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +56 -50
  41. package/build/godot/addons/gdharness_runtime/serialisation.gd +45 -94
  42. package/build/godot/addons/gdharness_runtime/value_shapes.gd +111 -0
  43. package/build/godot/addons/gdharness_runtime/value_shapes.gd.uid +1 -0
  44. package/build/godot/operations/animation_reads.gd +7 -19
  45. package/build/godot/operations/audio_buses.gd +10 -10
  46. package/build/godot/operations/class_cache.gd +16 -16
  47. package/build/godot/operations/classdb_queries.gd +12 -12
  48. package/build/godot/operations/dependencies.gd +33 -29
  49. package/build/godot/operations/editor_tools.gd +12 -12
  50. package/build/godot/operations/export_presets.gd +10 -8
  51. package/build/godot/operations/file_reads.gd +60 -25
  52. package/build/godot/operations/gdscript_analysis.gd +20 -20
  53. package/build/godot/operations/gdscript_authoring.gd +17 -15
  54. package/build/godot/operations/godot_operations.gd +64 -63
  55. package/build/godot/operations/import_pipeline.gd +20 -20
  56. package/build/godot/operations/input_actions.gd +17 -17
  57. package/build/godot/operations/logger.gd +1 -1
  58. package/build/godot/operations/plugins.gd +8 -8
  59. package/build/godot/operations/project_config.gd +24 -17
  60. package/build/godot/operations/project_diagnostics.gd +9 -9
  61. package/build/godot/operations/property_values.gd +120 -53
  62. package/build/godot/operations/reading.gd +28 -0
  63. package/build/godot/operations/rename_checks.gd +3 -3
  64. package/build/godot/operations/resource_files.gd +5 -5
  65. package/build/godot/operations/script_check.gd +7 -7
  66. package/build/godot/operations/serialisation.gd +45 -94
  67. package/build/godot/operations/value_shapes.gd +111 -0
  68. package/package.json +1 -1
@@ -14,8 +14,9 @@ extends RefCounted
14
14
  ## write accepts. What is here on top of it is the part only a property knows: the type wanted, so a
15
15
  ## dictionary shaped like a vector or a bare pair of numbers can be read as one.
16
16
 
17
- const Read = preload("reading.gd")
18
- const Serialisation = preload("serialisation.gd")
17
+ const GdhRead = preload("reading.gd")
18
+ const GdhSerialisation = preload("serialisation.gd")
19
+ const GdhShapes = preload("value_shapes.gd")
19
20
 
20
21
  # The data types a Theme names its items by, as in "Button/colors/font_color", and what each holds.
21
22
  const THEME_ITEMS: Dictionary[String, Array] = {
@@ -65,12 +66,18 @@ const FLOAT_SHAPES: Array[int] = [
65
66
  # names, so a write to "Visibility" or "Transform" changed nothing and was answered as made.
66
67
  const HEADINGS: int = PROPERTY_USAGE_CATEGORY | PROPERTY_USAGE_GROUP | PROPERTY_USAGE_SUBGROUP
67
68
 
68
- var _values: Serialisation = Serialisation.new()
69
+ var _values: GdhSerialisation = GdhSerialisation.new()
69
70
 
70
71
  # Why the value being parsed cannot be written, when the parse is what found out. A resource is
71
72
  # built inside the parse and there is nowhere in a parsed value to put the reason it is wrong.
72
73
  var _refused: String = ""
73
74
 
75
+ # The stored properties the last write_all changed without being given them, by name, as [what it
76
+ # held before, what it holds now]. A setter keeps other properties in step with the one it is given:
77
+ # a Gradient given a third colour gives that point an offset of 0, a Control given a size moves its
78
+ # offsets. An answer naming only what was given leaves the caller to guess the rest.
79
+ var _also_changed: Dictionary = {}
80
+
74
81
 
75
82
  ## Turns what arrived over the wire into the Godot value a property wants.
76
83
  ##
@@ -85,7 +92,7 @@ func parse(value: Variant, expected_type: int = TYPE_NIL, elements: Array = [])
85
92
  var fields: Dictionary = value
86
93
  # A dictionary with keys that are not text arrives as its entries, and is read against the
87
94
  # declared element types like any other.
88
- if str(fields.get("_type", "")) == "Dictionary" and Serialisation.pairs(fields.get("entries")):
95
+ if str(fields.get("_type", "")) == "Dictionary" and GdhSerialisation.pairs(fields.get("entries")):
89
96
  var built: Dictionary = _values.deserialize_value(fields)
90
97
  return _parse_entries(built, elements)
91
98
  var tagged: Array = _parse_tagged_dictionary(fields)
@@ -113,38 +120,39 @@ func cannot_hold(
113
120
  ) -> String:
114
121
  var held: String = type_string(expected_type)
115
122
  var cost: String = " Setting it would save a different value than the one asked for."
116
- if not Serialisation.acceptable(value, expected_type):
123
+ if not GdhSerialisation.acceptable(value, expected_type):
117
124
  var shape: String = "%s.%s is %s and the value given is %s, which cannot become one."
118
- return shape % [holder, property, held, type_string(typeof(value))] + cost
125
+ var remedy: String = GdhShapes.remedy(value, expected_type)
126
+ return shape % [holder, property, held, type_string(typeof(value))] + remedy + cost
119
127
 
120
128
  # And the same question of each element, because a list is accepted for a packed or typed array
121
129
  # whatever is in it: a polygon written as [{"x": 1}] passes the check above and lands as one zero
122
130
  # vector, and [1.5, "x"] for a PackedInt32Array as [1, 0].
123
- var element: int = Read.as_int(PACKED_ELEMENTS.get(expected_type, TYPE_NIL), TYPE_NIL)
131
+ var element: int = GdhRead.as_int(PACKED_ELEMENTS.get(expected_type, TYPE_NIL), TYPE_NIL)
124
132
  if expected_type == TYPE_ARRAY and not elements.is_empty():
125
- element = Read.as_int(elements[0], TYPE_NIL)
133
+ element = GdhRead.as_int(elements[0], TYPE_NIL)
126
134
  if element != TYPE_NIL and value is Array:
127
135
  var items: Array = value
128
136
  for index: int in items.size():
129
- if not Serialisation.acceptable(items[index], element):
137
+ if not GdhSerialisation.acceptable(items[index], element):
130
138
  var shape: String = "%s.%s is %s and item %d of the list given is %s, not %s."
131
139
  var named: Array = [
132
140
  holder, property, held, index, type_string(typeof(items[index])), type_string(element)
133
141
  ]
134
- return shape % named + cost
142
+ return shape % named + GdhShapes.remedy(items[index], element) + cost
135
143
  if expected_type == TYPE_DICTIONARY and elements.size() == 2 and value is Dictionary:
136
144
  var entries: Dictionary = value
137
- var key_type: int = Read.as_int(elements[0], TYPE_NIL)
138
- var value_type: int = Read.as_int(elements[1], TYPE_NIL)
145
+ var key_type: int = GdhRead.as_int(elements[0], TYPE_NIL)
146
+ var value_type: int = GdhRead.as_int(elements[1], TYPE_NIL)
139
147
  for key: Variant in entries:
140
- if not Serialisation.acceptable(key, key_type):
148
+ if not GdhSerialisation.acceptable(key, key_type):
141
149
  var shape: String = "%s.%s has %s keys, and %s is not one."
142
150
  return shape % [holder, property, type_string(key_type), shown(key)] + cost
143
151
  var entry: Variant = entries[key]
144
- if not Serialisation.acceptable(entry, value_type):
152
+ if not GdhSerialisation.acceptable(entry, value_type):
145
153
  var shape: String = "%s.%s holds %s values, and the one given for %s is %s."
146
154
  var named: Array = [holder, property, type_string(value_type), shown(key), shown(entry)]
147
- return shape % named + cost
155
+ return shape % named + GdhShapes.remedy(entry, value_type) + cost
148
156
  return ""
149
157
 
150
158
 
@@ -168,6 +176,11 @@ func cannot_hold(
168
176
  ## Nothing here saves, so a refusal partway leaves the change in memory and the file as it was. The
169
177
  ## caller returns the refusal instead of saving.
170
178
  func write_all(target: Object, properties: Dictionary) -> String:
179
+ _also_changed = {}
180
+ var named: Dictionary = {}
181
+ for key: Variant in properties:
182
+ named[str(key).get_slice(":", 0)] = true
183
+ var before: Dictionary = _stored(target, named)
171
184
  var given: Dictionary = {}
172
185
  for key: Variant in properties:
173
186
  var property: String = str(key)
@@ -197,9 +210,62 @@ func write_all(target: Object, properties: Dictionary) -> String:
197
210
  )
198
211
  % [target.get_class(), property, shown(given[property]), shown(held)]
199
212
  )
213
+ for property: String in before:
214
+ var now: Variant = target.get(property)
215
+ if not same(now, before[property]):
216
+ _also_changed[property] = [before[property], now]
200
217
  return ""
201
218
 
202
219
 
220
+ ## Puts under alsoChanged in [param answer] each property the last [method write_all] changed
221
+ ## without being given it, as {was, now}, when there are any. A node is named by its path from
222
+ ## [param relative_to], as a scene answer names one. Called before the scene is written, since the
223
+ ## write frees the nodes these values can hold.
224
+ func note_also_changed(answer: Dictionary, relative_to: Node = null) -> void:
225
+ if _also_changed.is_empty():
226
+ return
227
+ var noted: Dictionary = {}
228
+ for property: Variant in _also_changed:
229
+ var pair: Array = _also_changed[property]
230
+ noted[property] = {"was": _readable(pair[0], relative_to), "now": _readable(pair[1], relative_to)}
231
+ answer["alsoChanged"] = noted
232
+
233
+
234
+ func _readable(value: Variant, relative_to: Node) -> Variant:
235
+ if value is Node and relative_to != null:
236
+ var target: Node = value
237
+ return _values.serialize_value(relative_to.get_path_to(target))
238
+ if value is Array:
239
+ var items: Array = value
240
+ return items.map(func(item: Variant) -> Variant: return _readable(item, relative_to))
241
+ return _values.serialize_value(value)
242
+
243
+
244
+ ## What [param target] stores, by name, but its script and the properties in [param leave]. A list
245
+ ## or a map is copied, since a setter can change the one it holds in place.
246
+ func _stored(target: Object, leave: Dictionary) -> Dictionary:
247
+ var held: Dictionary = {}
248
+ for entry: Dictionary in target.get_property_list():
249
+ var name: String = str(entry["name"])
250
+ var usage: int = GdhRead.as_int(entry["usage"])
251
+ if (
252
+ usage & PROPERTY_USAGE_STORAGE == 0
253
+ or usage & HEADINGS != 0
254
+ or name == "script"
255
+ or leave.has(name)
256
+ ):
257
+ continue
258
+ var value: Variant = target.get(name)
259
+ if value is Array:
260
+ var listed: Array = value
261
+ value = listed.duplicate(true)
262
+ elif value is Dictionary:
263
+ var mapped: Dictionary = value
264
+ value = mapped.duplicate(true)
265
+ held[name] = value
266
+ return held
267
+
268
+
203
269
  ## What [param target] holds under [param key], a property or a colon path into one's components.
204
270
  func read_at(target: Object, key: String) -> Variant:
205
271
  if ":" in key and declaration(target, key).is_empty():
@@ -228,9 +294,8 @@ func _component_slot(target: Object, path: String) -> Dictionary:
228
294
  var walked: String = steps[0]
229
295
  for index: int in range(1, steps.size()):
230
296
  var step: String = steps[index]
231
- var has: PackedStringArray = Serialisation.components_of(value)
232
- var kind: String = type_string(typeof(value))
233
- var sort: String = ("an " if kind.left(1).to_upper() in ["A", "E", "I", "O", "U"] else "a ") + kind
297
+ var has: PackedStringArray = GdhSerialisation.components_of(value)
298
+ var sort: String = GdhRead.with_article(type_string(typeof(value)))
234
299
  if has.is_empty():
235
300
  return {
236
301
  "refused":
@@ -255,8 +320,8 @@ func _component_slot(target: Object, path: String) -> Dictionary:
255
320
  ## [param raw] as the value [param declared] takes, as [ok, value] or [false, why not].
256
321
  func value_for(target: Object, declared: Dictionary, raw: Variant) -> Array:
257
322
  var property: String = str(declared.get("name", ""))
258
- var expected_type: int = Read.as_int(declared.get("type", TYPE_NIL), TYPE_NIL)
259
- var hint: int = Read.as_int(declared.get("hint", PROPERTY_HINT_NONE))
323
+ var expected_type: int = GdhRead.as_int(declared.get("type", TYPE_NIL), TYPE_NIL)
324
+ var hint: int = GdhRead.as_int(declared.get("hint", PROPERTY_HINT_NONE))
260
325
  if expected_type == TYPE_OBJECT and hint == PROPERTY_HINT_NODE_TYPE:
261
326
  return _node_reference(target, property, raw)
262
327
 
@@ -272,7 +337,7 @@ func value_for(target: Object, declared: Dictionary, raw: Variant) -> Array:
272
337
  var elements: Array = elements_of(declared)
273
338
  if _fraction(raw, expected_type, elements):
274
339
  return [false, _fraction_refusal(target, property, raw)]
275
- var rounded: String = Serialisation.inexact(raw, expected_type)
340
+ var rounded: String = GdhSerialisation.inexact(raw, expected_type)
276
341
  if not rounded.is_empty():
277
342
  return [false, "%s.%s holds int: %s." % [target.get_class(), property, rounded]]
278
343
  _refused = ""
@@ -328,7 +393,7 @@ func _node_reference(target: Object, property: String, raw: Variant) -> Array:
328
393
  ## an object of the wrong class: measured on 4.7.2, a Sprite2D given a Material for its texture
329
394
  ## held nothing afterwards, and the scene was saved without the texture it had.
330
395
  func _wrong_class(value: Object, declared: Dictionary) -> String:
331
- var hint: int = Read.as_int(declared.get("hint", PROPERTY_HINT_NONE))
396
+ var hint: int = GdhRead.as_int(declared.get("hint", PROPERTY_HINT_NONE))
332
397
  var wanted: String = str(declared.get("hint_string", ""))
333
398
  if hint != PROPERTY_HINT_RESOURCE_TYPE or wanted.is_empty():
334
399
  return ""
@@ -354,8 +419,8 @@ func _wrong_class(value: Object, declared: Dictionary) -> String:
354
419
  ## behind a placeholder in the editor, and measured on 4.7.2 that placeholder stores [1.5, 2.0]
355
420
  ## as given, which the game then fails to load into the typed array.
356
421
  static func elements_of(declared: Dictionary) -> Array:
357
- var type: int = Read.as_int(declared.get("type", TYPE_NIL), TYPE_NIL)
358
- var hint: int = Read.as_int(declared.get("hint", PROPERTY_HINT_NONE))
422
+ var type: int = GdhRead.as_int(declared.get("type", TYPE_NIL), TYPE_NIL)
423
+ var hint: int = GdhRead.as_int(declared.get("hint", PROPERTY_HINT_NONE))
359
424
  var wanted: String = str(declared.get("hint_string", ""))
360
425
  if wanted.is_empty():
361
426
  return []
@@ -397,21 +462,21 @@ func _fraction(value: Variant, type: int, elements: Array = []) -> bool:
397
462
  if type == TYPE_INT:
398
463
  var number: Variant = value
399
464
  if typeof(value) == TYPE_STRING:
400
- number = Read.json_or_null(str(value))
465
+ number = GdhRead.json_or_null(str(value))
401
466
  if typeof(number) != TYPE_FLOAT:
402
467
  return false
403
468
  var whole: float = number
404
469
  return is_finite(whole) and whole != floorf(whole)
405
- var element: int = Read.as_int(PACKED_ELEMENTS.get(type, TYPE_NIL), TYPE_NIL)
470
+ var element: int = GdhRead.as_int(PACKED_ELEMENTS.get(type, TYPE_NIL), TYPE_NIL)
406
471
  if type == TYPE_ARRAY and not elements.is_empty():
407
- element = Read.as_int(elements[0], TYPE_NIL)
472
+ element = GdhRead.as_int(elements[0], TYPE_NIL)
408
473
  if value is Array and element != TYPE_NIL:
409
474
  var items: Array = value
410
475
  return items.any(func(item: Variant) -> bool: return _fraction(item, element))
411
476
  if type == TYPE_DICTIONARY and elements.size() == 2 and value is Dictionary:
412
477
  var entries: Dictionary = value
413
- var key_type: int = Read.as_int(elements[0], TYPE_NIL)
414
- var value_type: int = Read.as_int(elements[1], TYPE_NIL)
478
+ var key_type: int = GdhRead.as_int(elements[0], TYPE_NIL)
479
+ var value_type: int = GdhRead.as_int(elements[1], TYPE_NIL)
415
480
  for key: Variant in entries:
416
481
  if _fraction(key, key_type) or _fraction(entries[key], value_type):
417
482
  return true
@@ -463,7 +528,7 @@ func declaration(target: Object, property: String) -> Dictionary:
463
528
  for declared: Dictionary in target.get_property_list():
464
529
  if str(declared.get("name", "")) != property:
465
530
  continue
466
- if Read.as_int(declared.get("usage", 0)) & HEADINGS:
531
+ if GdhRead.as_int(declared.get("usage", 0)) & HEADINGS:
467
532
  continue
468
533
  return declared
469
534
  if target is Theme:
@@ -503,13 +568,13 @@ static func same(held: Variant, given: Variant) -> bool:
503
568
  return held_none == given_none
504
569
  if held_type == TYPE_OBJECT or given_type == TYPE_OBJECT:
505
570
  return _same_object(held, given)
506
- if Serialisation.NUMBERS.has(held_type) and Serialisation.NUMBERS.has(given_type):
571
+ if GdhSerialisation.NUMBERS.has(held_type) and GdhSerialisation.NUMBERS.has(given_type):
507
572
  if held_type == TYPE_BOOL or given_type == TYPE_BOOL:
508
573
  return held_type == given_type and held == given
509
- var held_number: float = Read.as_float(held)
510
- var given_number: float = Read.as_float(given)
574
+ var held_number: float = GdhRead.as_float(held)
575
+ var given_number: float = GdhRead.as_float(given)
511
576
  return (is_nan(held_number) and is_nan(given_number)) or is_equal_approx(held_number, given_number)
512
- if Serialisation.TEXTS.has(held_type) and Serialisation.TEXTS.has(given_type):
577
+ if GdhSerialisation.TEXTS.has(held_type) and GdhSerialisation.TEXTS.has(given_type):
513
578
  return str(held) == str(given)
514
579
  if _listed(held_type) and _listed(given_type):
515
580
  var held_items: Array = type_convert(held, TYPE_ARRAY)
@@ -675,7 +740,7 @@ func _parse_shaped_dictionary(value: Dictionary, expected_type: int, elements: A
675
740
  if expected_type == TYPE_NIL or expected_type == TYPE_DICTIONARY:
676
741
  return _parse_entries(value, elements)
677
742
  var tag: String = type_string(expected_type)
678
- if Serialisation.REQUIRED.has(tag):
743
+ if GdhShapes.REQUIRED.has(tag):
679
744
  var tagged: Dictionary = value.duplicate()
680
745
  tagged["_type"] = tag
681
746
  var built: Variant = _values.deserialize_value(tagged)
@@ -690,8 +755,8 @@ func _parse_entries(value: Dictionary, elements: Array) -> Dictionary:
690
755
  var key_type: int = TYPE_NIL
691
756
  var value_type: int = TYPE_NIL
692
757
  if elements.size() == 2:
693
- key_type = Read.as_int(elements[0], TYPE_NIL)
694
- value_type = Read.as_int(elements[1], TYPE_NIL)
758
+ key_type = GdhRead.as_int(elements[0], TYPE_NIL)
759
+ value_type = GdhRead.as_int(elements[1], TYPE_NIL)
695
760
  var entries: Dictionary = {}
696
761
  for key: Variant in value:
697
762
  entries[_values.fitted(key, key_type)] = parse(value[key], value_type)
@@ -709,7 +774,7 @@ func _typed_dictionary(entries: Dictionary, key_type: int, value_type: int) -> D
709
774
  for key: Variant in entries:
710
775
  if key_type != TYPE_NIL and typeof(key) != key_type:
711
776
  return entries
712
- if not Serialisation.acceptable(entries[key], value_type):
777
+ if not GdhSerialisation.acceptable(entries[key], value_type):
713
778
  return entries
714
779
  var fitted: Dictionary = {}
715
780
  for key: Variant in entries:
@@ -720,46 +785,48 @@ func _typed_dictionary(entries: Dictionary, key_type: int, value_type: int) -> D
720
785
  ## An array, either positional for a vector the property declares, or a list to parse per item.
721
786
  func _parse_array(value: Array, expected_type: int, elements: Array) -> Variant:
722
787
  if PACKED_ELEMENTS.has(expected_type):
723
- var element: int = Read.as_int(PACKED_ELEMENTS[expected_type], TYPE_NIL)
788
+ var element: int = GdhRead.as_int(PACKED_ELEMENTS[expected_type], TYPE_NIL)
724
789
  return value.map(func(item: Variant) -> Variant: return parse(item, element))
725
790
 
726
791
  match expected_type:
727
792
  TYPE_VECTOR2:
728
793
  if value.size() == 2:
729
- return Vector2(Read.as_float(value[0]), Read.as_float(value[1]))
794
+ return Vector2(GdhRead.as_float(value[0]), GdhRead.as_float(value[1]))
730
795
  TYPE_VECTOR2I:
731
796
  if value.size() == 2:
732
- return Vector2i(Read.as_int(value[0]), Read.as_int(value[1]))
797
+ return Vector2i(GdhRead.as_int(value[0]), GdhRead.as_int(value[1]))
733
798
  TYPE_VECTOR3:
734
799
  if value.size() == 3:
735
- return Vector3(Read.as_float(value[0]), Read.as_float(value[1]), Read.as_float(value[2]))
800
+ return Vector3(
801
+ GdhRead.as_float(value[0]), GdhRead.as_float(value[1]), GdhRead.as_float(value[2])
802
+ )
736
803
  TYPE_VECTOR3I:
737
804
  if value.size() == 3:
738
- return Vector3i(Read.as_int(value[0]), Read.as_int(value[1]), Read.as_int(value[2]))
805
+ return Vector3i(GdhRead.as_int(value[0]), GdhRead.as_int(value[1]), GdhRead.as_int(value[2]))
739
806
  TYPE_VECTOR4:
740
807
  if value.size() == 4:
741
808
  return Vector4(
742
- Read.as_float(value[0]),
743
- Read.as_float(value[1]),
744
- Read.as_float(value[2]),
745
- Read.as_float(value[3])
809
+ GdhRead.as_float(value[0]),
810
+ GdhRead.as_float(value[1]),
811
+ GdhRead.as_float(value[2]),
812
+ GdhRead.as_float(value[3])
746
813
  )
747
814
  TYPE_COLOR:
748
815
  if value.size() == 3 or value.size() == 4:
749
816
  return Color(
750
- Read.as_float(value[0]),
751
- Read.as_float(value[1]),
752
- Read.as_float(value[2]),
753
- Read.as_float(value[3]) if value.size() == 4 else 1.0
817
+ GdhRead.as_float(value[0]),
818
+ GdhRead.as_float(value[1]),
819
+ GdhRead.as_float(value[2]),
820
+ GdhRead.as_float(value[3]) if value.size() == 4 else 1.0
754
821
  )
755
822
 
756
823
  var element_type: int = TYPE_NIL
757
824
  if expected_type == TYPE_ARRAY and not elements.is_empty():
758
- element_type = Read.as_int(elements[0], TYPE_NIL)
825
+ element_type = GdhRead.as_int(elements[0], TYPE_NIL)
759
826
  var items: Array = value.map(func(item: Variant) -> Variant: return parse(item, element_type))
760
827
  if element_type == TYPE_NIL or element_type == TYPE_OBJECT:
761
828
  return items
762
- if not items.all(func(item: Variant) -> bool: return Serialisation.acceptable(item, element_type)):
829
+ if not items.all(func(item: Variant) -> bool: return GdhSerialisation.acceptable(item, element_type)):
763
830
  return items
764
831
  var fitted: Array = items.map(func(item: Variant) -> Variant: return type_convert(item, element_type))
765
832
  return Array(fitted, element_type, &"", null)
@@ -139,3 +139,31 @@ static func missing_below(
139
139
  "its children are " + shown if not children.is_empty() else "it has no children",
140
140
  ]
141
141
  )
142
+
143
+
144
+ ## [param said], the refusal for a [param kind] the scene does not have at [param node_path], with
145
+ ## what is there when the scene has a node there, [param found] its class, and [param holders], the
146
+ ## paths of the nodes in the scene that are one, so the caller has the path to pass. Names up to
147
+ ## [constant NAMED_CHILDREN] of them.
148
+ static func not_of_kind(
149
+ said: String, node_path: String, found: String, kind: String, holders: PackedStringArray
150
+ ) -> String:
151
+ var told: String = said + ("" if said.ends_with(".") else ".")
152
+ if not found.is_empty():
153
+ told += ' The node at "%s" is %s, not %s.' % [node_path, with_article(found), with_article(kind)]
154
+ if holders.is_empty():
155
+ return told + " The scene holds no %s." % kind
156
+ var quoted: PackedStringArray = []
157
+ for holder: String in holders.slice(0, NAMED_CHILDREN):
158
+ var _quoted: bool = quoted.append('"%s"' % holder)
159
+ var shown: String = ", ".join(quoted)
160
+ if holders.size() > NAMED_CHILDREN:
161
+ shown += " and %d more" % (holders.size() - NAMED_CHILDREN)
162
+ if holders.size() == 1:
163
+ return told + " The scene's %s is at %s." % [kind, shown]
164
+ return told + " The scene's %ss are at %s." % [kind, shown]
165
+
166
+
167
+ ## [param word] after the article it takes, "an AnimationTree" or "a Node2D".
168
+ static func with_article(word: String) -> String:
169
+ return ("an " if word.left(1).to_upper() in ["A", "E", "I", "O", "U"] else "a ") + word
@@ -23,14 +23,14 @@ extends RefCounted
23
23
  ## setter setting scale were both saved into the file by a write to another node, and a root whose
24
24
  ## script failed in [code]_init[/code] was saved without it. Outside the editor, then, what a node
25
25
  ## stores is put back to what the file holds wherever the tool did not ask for a change, before
26
- ## anything is saved, and a write is refused when that does not hold. See [HeadlessCheck].
26
+ ## anything is saved, and a write is refused when that does not hold. See [GdhHeadlessCheck].
27
27
 
28
- const HeadlessCheck = preload("headless_check.gd")
28
+ const GdhHeadlessCheck = preload("headless_check.gd")
29
29
 
30
30
  ## How many times what scripts changed is put back before a write is refused for it.
31
31
  const PUTTING_BACK_PASSES: int = 4
32
- const PropertyValues = preload("property_values.gd")
33
- const Read = preload("reading.gd")
32
+ const GdhPropertyValues = preload("property_values.gd")
33
+ const GdhRead = preload("reading.gd")
34
34
  ## The names Object.ConnectFlags gives each bit. The number alone left a reader guessing that 2 is
35
35
  ## the persistence every connection in a saved scene carries.
36
36
  const CONNECT_FLAG_NAMES: Dictionary[int, String] = {
@@ -135,12 +135,12 @@ func asked(node: Node, names: Array) -> void:
135
135
  var id: int = node.get_instance_id()
136
136
  var on: Dictionary = _asked.get_or_add(id, {})
137
137
  if not _before.has(id):
138
- _before[id] = HeadlessCheck.stored(node)
138
+ _before[id] = GdhHeadlessCheck.stored(node)
139
139
  for name: Variant in names:
140
140
  var property: String = str(name)
141
141
  on[property] = true
142
142
  on[property.get_slice(":", 0)] = true
143
- _reached.merge(HeadlessCheck.reached(node, property))
143
+ _reached.merge(GdhHeadlessCheck.reached(node, property))
144
144
 
145
145
 
146
146
  ## Takes [param resource], built into the scene, as one the tool changed, which a write saves as it is.
@@ -199,7 +199,7 @@ func not_found(said: String, node_path: String) -> String:
199
199
  if root == null:
200
200
  return said
201
201
  if not root.has_node(NodePath(node_path.get_slice("/", 0))):
202
- var hinted: String = Read.from_the_root(said, node_path, str(root.name))
202
+ var hinted: String = GdhRead.from_the_root(said, node_path, str(root.name))
203
203
  if hinted != said:
204
204
  return hinted
205
205
  var parts: PackedStringArray = node_path.trim_prefix("./").split("/", false)
@@ -216,7 +216,7 @@ func not_found(said: String, node_path: String) -> String:
216
216
  var names: PackedStringArray = []
217
217
  for child: Node in at.get_children():
218
218
  var _named: bool = names.append(str(child.name))
219
- return Read.missing_below(said, str(root.name), reached, parts[reached.size()], names)
219
+ return GdhRead.missing_below(said, str(root.name), reached, parts[reached.size()], names)
220
220
 
221
221
 
222
222
  func close() -> void:
@@ -477,7 +477,7 @@ func kept(saved: Node, loaded: Variant, given: Variant) -> bool:
477
477
  var loaded_node: Node = loaded
478
478
  var given_node: Node = given
479
479
  return str(saved.get_path_to(loaded_node)) == str(root.get_path_to(given_node))
480
- return PropertyValues.same(loaded, given)
480
+ return GdhPropertyValues.same(loaded, given)
481
481
 
482
482
 
483
483
  ## Packs [member root], has [param check] read the result as the file will hold it, and writes the
@@ -508,7 +508,7 @@ func write(check: Callable = Callable()) -> Dictionary:
508
508
  )
509
509
  % path
510
510
  )
511
- + HeadlessCheck.REMEDY
511
+ + GdhHeadlessCheck.REMEDY
512
512
  )
513
513
  )
514
514
  if check.is_valid():
@@ -530,12 +530,14 @@ func write(check: Callable = Callable()) -> Dictionary:
530
530
  var values: Array = []
531
531
  for index: int in state.get_node_count():
532
532
  values.append_array(_stored_at(state, index).values())
533
- var unkept: String = HeadlessCheck.settle_saved(
533
+ var unkept: String = GdhHeadlessCheck.settle_saved(
534
534
  path, _text, values, null, {}, _reached, func() -> Error: return save_keeping_uid(packed, path)
535
535
  )
536
536
  if unkept.is_empty():
537
537
  return {}
538
- return {"ok": false, "error": "Nothing was written: " + unkept + HeadlessCheck.put_back(path, previous)}
538
+ return {
539
+ "ok": false, "error": "Nothing was written: " + unkept + GdhHeadlessCheck.put_back(path, previous)
540
+ }
539
541
 
540
542
 
541
543
  func _holds_scripted_embedded() -> bool:
@@ -543,7 +545,7 @@ func _holds_scripted_embedded() -> bool:
543
545
  var nodes: Array[Node] = [root]
544
546
  while not nodes.is_empty():
545
547
  var node: Node = nodes.pop_back()
546
- if HeadlessCheck.holds_scripted_embedded(node, seen):
548
+ if GdhHeadlessCheck.holds_scripted_embedded(node, seen):
547
549
  return true
548
550
  nodes.append_array(node.get_children())
549
551
  return false
@@ -742,7 +744,7 @@ func _settled(packed: PackedScene) -> Array:
742
744
  var node: Node = found[0]
743
745
  var before: Dictionary = _before[node.get_instance_id()]
744
746
  for name: String in found[1]:
745
- node.set(name, before.get(name, HeadlessCheck.default_of(node, name)))
747
+ node.set(name, before.get(name, GdhHeadlessCheck.default_of(node, name)))
746
748
  current = PackedScene.new()
747
749
  var packing: Error = current.pack(root)
748
750
  if packing != OK:
@@ -766,10 +768,10 @@ func _moved_by_scripts() -> Array[Array]:
766
768
  if node == null or not root.is_ancestor_of(node) and node != root:
767
769
  continue
768
770
  var on: Dictionary = _asked[id]
769
- if not HeadlessCheck.through_script(node, on):
771
+ if not GdhHeadlessCheck.through_script(node, on):
770
772
  continue
771
773
  var before: Dictionary = _before[id]
772
- var changed: Array[String] = HeadlessCheck.differing(node, before, on)
774
+ var changed: Array[String] = GdhHeadlessCheck.differing(node, before, on)
773
775
  if not changed.is_empty():
774
776
  moved.append([node, changed])
775
777
  return moved
@@ -792,7 +794,10 @@ func _strays(state: SceneState) -> Array[Array]:
792
794
  # changes is Godot's doing and the editor's pack does it too: a Control saved without its
793
795
  # grow directions gets them back from anchors_preset's setter, and loses an anchors_preset
794
796
  # its layout mode does not store.
795
- if not HeadlessCheck.untooled(node.get_script()) and not HeadlessCheck.untooled(filed.get("script")):
797
+ if (
798
+ not GdhHeadlessCheck.untooled(node.get_script())
799
+ and not GdhHeadlessCheck.untooled(filed.get("script"))
800
+ ):
796
801
  continue
797
802
  var stored: Dictionary = _stored_at(state, index)
798
803
  var names: Dictionary = filed.duplicate()
@@ -800,9 +805,9 @@ func _strays(state: SceneState) -> Array[Array]:
800
805
  for key: Variant in names:
801
806
  var name: String = str(key)
802
807
  var held: bool = filed.has(name)
803
- if held == stored.has(name) and (not held or PropertyValues.same(stored[name], filed[name])):
808
+ if held == stored.has(name) and (not held or GdhPropertyValues.same(stored[name], filed[name])):
804
809
  continue
805
- if not held and HeadlessCheck.at_default(node, name):
810
+ if not held and GdhHeadlessCheck.at_default(node, name):
806
811
  continue
807
812
  stray.append([node, name, held, filed.get(name)])
808
813
  return stray
@@ -819,7 +824,7 @@ func _stray_text(node: Node, name: String) -> String:
819
824
  )
820
825
  % [at, path]
821
826
  )
822
- + HeadlessCheck.REMEDY
827
+ + GdhHeadlessCheck.REMEDY
823
828
  )
824
829
  return (
825
830
  (
@@ -830,7 +835,7 @@ func _stray_text(node: Node, name: String) -> String:
830
835
  )
831
836
  % [at, name, path]
832
837
  )
833
- + HeadlessCheck.REMEDY
838
+ + GdhHeadlessCheck.REMEDY
834
839
  )
835
840
 
836
841
 
@@ -845,7 +850,7 @@ func _unset_value(node: Node, name: String) -> Variant:
845
850
  while top != root and top.owner != root:
846
851
  top = top.get_parent()
847
852
  if top == root or top.scene_file_path.is_empty():
848
- return HeadlessCheck.default_of(node, name)
853
+ return GdhHeadlessCheck.default_of(node, name)
849
854
  if _base_paths.has(path_of(top)):
850
855
  return _value_in(base, path_of(node), node, name)
851
856
  var instanced: PackedScene = ResourceLoader.load(top.scene_file_path) as PackedScene
@@ -881,7 +886,7 @@ func _value_in(state: SceneState, at: String, node: Node, name: String) -> Varia
881
886
  state = instanced.get_state()
882
887
  else:
883
888
  state = state.get_base_scene_state()
884
- return HeadlessCheck.default_of(node, name)
889
+ return GdhHeadlessCheck.default_of(node, name)
885
890
 
886
891
 
887
892
  func _node_at(node_path: String) -> Node: