gdharness 1.3.2 → 1.3.4

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 (46) hide show
  1. package/build/cli.js +163 -85
  2. package/build/godot/addons/gdharness_editor/animation_reads.gd +14 -11
  3. package/build/godot/addons/gdharness_editor/declared_values.gd +157 -4
  4. package/build/godot/addons/gdharness_editor/file_reads.gd +15 -3
  5. package/build/godot/addons/gdharness_editor/file_walk.gd +134 -0
  6. package/build/godot/addons/gdharness_editor/file_walk.gd.uid +1 -0
  7. package/build/godot/addons/gdharness_editor/headless_check.gd +44 -23
  8. package/build/godot/addons/gdharness_editor/property_paths.gd +225 -0
  9. package/build/godot/addons/gdharness_editor/property_paths.gd.uid +1 -0
  10. package/build/godot/addons/gdharness_editor/property_values.gd +249 -227
  11. package/build/godot/addons/gdharness_editor/reading.gd +15 -0
  12. package/build/godot/addons/gdharness_editor/same_values.gd +177 -0
  13. package/build/godot/addons/gdharness_editor/same_values.gd.uid +1 -0
  14. package/build/godot/addons/gdharness_editor/scene_file.gd +100 -15
  15. package/build/godot/addons/gdharness_editor/serialisation.gd +75 -3
  16. package/build/godot/addons/gdharness_editor/tool_executor.gd +12 -3
  17. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +402 -55
  18. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +3 -2
  19. package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +91 -0
  20. package/build/godot/addons/gdharness_editor/tools/scene_groups.gd.uid +1 -0
  21. package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +88 -5
  22. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +102 -337
  23. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +363 -0
  24. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd.uid +1 -0
  25. package/build/godot/addons/gdharness_runtime/reading.gd +15 -0
  26. package/build/godot/addons/gdharness_runtime/runtime_input.gd +10 -26
  27. package/build/godot/addons/gdharness_runtime/runtime_pad.gd +3 -2
  28. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +2 -1
  29. package/build/godot/addons/gdharness_runtime/runtime_values.gd +30 -0
  30. package/build/godot/addons/gdharness_runtime/serialisation.gd +75 -3
  31. package/build/godot/operations/animation_reads.gd +14 -11
  32. package/build/godot/operations/declared_values.gd +157 -4
  33. package/build/godot/operations/dependencies.gd +14 -39
  34. package/build/godot/operations/editor_tools.gd +7 -3
  35. package/build/godot/operations/file_reads.gd +15 -3
  36. package/build/godot/operations/file_walk.gd +1 -0
  37. package/build/godot/operations/gdscript_analysis.gd +229 -75
  38. package/build/godot/operations/gdscript_authoring.gd +142 -34
  39. package/build/godot/operations/import_pipeline.gd +17 -6
  40. package/build/godot/operations/logger.gd +7 -0
  41. package/build/godot/operations/property_paths.gd +225 -0
  42. package/build/godot/operations/property_values.gd +249 -227
  43. package/build/godot/operations/reading.gd +15 -0
  44. package/build/godot/operations/same_values.gd +177 -0
  45. package/build/godot/operations/serialisation.gd +75 -3
  46. package/package.json +1 -1
@@ -15,7 +15,9 @@ 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 GdhPropertyPaths = preload("property_paths.gd")
18
19
  const GdhRead = preload("reading.gd")
20
+ const GdhSame = preload("same_values.gd")
19
21
  const GdhSerialisation = preload("serialisation.gd")
20
22
  const GdhShapes = preload("value_shapes.gd")
21
23
 
@@ -46,23 +48,6 @@ const PACKED_ELEMENTS: Dictionary = {
46
48
  TYPE_PACKED_COLOR_ARRAY: TYPE_COLOR,
47
49
  }
48
50
 
49
- # The types made of floats, which a read back compares within float precision: a Vector2 property
50
- # stores 32-bit components, so 0.1 comes back as 0.100000001.
51
- const FLOAT_SHAPES: Array[int] = [
52
- TYPE_VECTOR2,
53
- TYPE_VECTOR3,
54
- TYPE_VECTOR4,
55
- TYPE_QUATERNION,
56
- TYPE_COLOR,
57
- TYPE_RECT2,
58
- TYPE_TRANSFORM2D,
59
- TYPE_BASIS,
60
- TYPE_TRANSFORM3D,
61
- TYPE_PLANE,
62
- TYPE_AABB,
63
- TYPE_PROJECTION,
64
- ]
65
-
66
51
  # Entries in a property list that only head a section of the inspector. Object.set ignores their
67
52
  # names, so a write to "Visibility" or "Transform" changed nothing and was answered as made.
68
53
  const HEADINGS: int = PROPERTY_USAGE_CATEGORY | PROPERTY_USAGE_GROUP | PROPERTY_USAGE_SUBGROUP
@@ -92,7 +77,8 @@ var _outside_declared: Dictionary = {}
92
77
  ## declares, or a bare array positional for a vector. Each is its own function; asking all three
93
78
  ## in one was twenty-four exits deep and impossible to follow.
94
79
  ##
95
- ## [param elements] is what a typed Array or Dictionary holds, as [method elements_of] reads it.
80
+ ## [param elements] is what a typed Array or Dictionary holds, as [method GdhDeclared.elements_of]
81
+ ## reads it.
96
82
  func parse(value: Variant, expected_type: int = TYPE_NIL, elements: Array = []) -> Variant:
97
83
  if value is Dictionary:
98
84
  var fields: Dictionary = value
@@ -189,51 +175,117 @@ func write_all(target: Object, properties: Dictionary) -> String:
189
175
  named[str(key).get_slice(":", 0)] = true
190
176
  var before: Dictionary = _stored(target, named)
191
177
  var given: Dictionary = {}
178
+ var declarations: Dictionary = {}
192
179
  # A refusal of one of several leaves the others unwritten too, which a refusal naming only the
193
180
  # one it refused left a caller to guess.
194
181
  var unwritten: String = (
195
182
  "" if properties.size() < 2 else " None of the %d properties given was written." % properties.size()
196
183
  )
197
- for key: Variant in properties:
184
+ # The script first, wherever the caller put it, since the properties it declares exist only once
185
+ # it is attached.
186
+ var order: Array = properties.keys()
187
+ if order.has("script"):
188
+ order.erase("script")
189
+ order.push_front("script")
190
+ var home: String = GdhPropertyPaths.home_of(target)
191
+ for key: Variant in order:
198
192
  var property: String = str(key)
199
- var declared: Dictionary = declaration(target, property)
200
- if declared.is_empty() and ":" in property:
201
- declared = _component_slot(target, property)
193
+ var place: Array = GdhPropertyPaths.holder_of(target, property, home)
194
+ var holder: Object = place[0]
195
+ var name: String = place[1]
196
+ var declared: Dictionary = declaration(holder, name)
197
+ if declared.is_empty():
198
+ var elsewhere: String = GdhPropertyPaths.held_elsewhere(holder, name, home)
199
+ if not elsewhere.is_empty():
200
+ return elsewhere + unwritten
201
+ var ungrown: String = GdhPropertyPaths.grow(holder, name, GdhPropertyPaths.class_called(holder))
202
+ if not ungrown.is_empty():
203
+ return ungrown + unwritten
204
+ declared = declaration(holder, name)
205
+ if declared.is_empty() and ":" in name:
206
+ declared = _component_slot(holder, name)
202
207
  if declared.has("refused"):
203
208
  return str(declared["refused"]) + unwritten
204
- if declared.is_empty() and target is ShaderMaterial and property.begins_with("shader_parameter/"):
205
- return _no_uniform(target as ShaderMaterial, property) + unwritten
209
+ if declared.is_empty() and holder is ShaderMaterial and name.begins_with("shader_parameter/"):
210
+ return _no_uniform(holder as ShaderMaterial, name) + unwritten
206
211
  if declared.is_empty():
207
- return "%s has no property %s." % [class_called(target), property] + unwritten
208
- var value: Array = value_for(target, declared, properties[key])
212
+ return no_property(holder, name) + unwritten
213
+ var value: Array = value_for(holder, declared, properties[key])
209
214
  if not value[0]:
210
215
  return str(value[1]) + unwritten
211
- var outside: String = GdhDeclared.outside(target, declared, value[1], shown(value[1]))
216
+ var outside: String = GdhDeclared.outside(holder, declared, value[1], shown(value[1]))
212
217
  if not outside.is_empty():
213
218
  _outside_declared[property] = outside
214
219
  _write(target, property, value[1])
215
220
  given[property] = value[1]
221
+ declarations[property] = declared
216
222
 
217
223
  for property: String in given:
218
- if not same(read_at(target, property), given[property]):
224
+ if not GdhSame.same(read_at(target, property), given[property]):
219
225
  _write(target, property, given[property])
220
226
  for property: String in given:
221
227
  var held: Variant = read_at(target, property)
222
- if not same(held, given[property]):
223
- return (
224
- (
225
- "%s.%s was given %s and holds %s after it was set: the engine changed or refused the "
226
- + "value, so nothing was written."
227
- )
228
- % [target.get_class(), property, shown(given[property]), shown(held)]
229
- )
228
+ if not GdhSame.same(held, given[property]):
229
+ var declared: Dictionary = declarations[property]
230
+ return _not_held(target, property, given[property], held, declared)
230
231
  for property: String in before:
231
232
  var now: Variant = target.get(property)
232
- if not same(now, before[property]):
233
+ if not GdhSame.same(now, before[property]):
233
234
  _also_changed[property] = [before[property], now]
234
235
  return ""
235
236
 
236
237
 
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:
267
+ return (
268
+ (
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."
272
+ )
273
+ % [
274
+ said,
275
+ shown(value),
276
+ "the buses are " + ", ".join(names) if nearest.is_empty() else "the nearest " + nearest
277
+ ]
278
+ )
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
+
237
289
  ## Why [param material] has no [param property]: a ShaderMaterial's parameters are its shader's
238
290
  ## uniforms, so the shader is what lacks one, and "ShaderMaterial has no property" named the
239
291
  ## material.
@@ -306,18 +358,24 @@ func _stored(target: Object, leave: Dictionary) -> Dictionary:
306
358
 
307
359
  ## What [param target] holds under [param key], a property or a colon path into one's components.
308
360
  func read_at(target: Object, key: String) -> Variant:
309
- if ":" in key and declaration(target, key).is_empty():
310
- return target.get_indexed(NodePath(key))
311
- return target.get(key)
361
+ var place: Array = GdhPropertyPaths.holder_of(target, key)
362
+ var holder: Object = place[0]
363
+ var name: String = place[1]
364
+ if ":" in name and declaration(holder, name).is_empty():
365
+ return holder.get_indexed(NodePath(name))
366
+ return holder.get(name)
312
367
 
313
368
 
314
369
  ## Writes [param value] under [param key], putting a changed component back into the property it
315
370
  ## was read out of, and that one into the one above it.
316
371
  func _write(target: Object, key: String, value: Variant) -> void:
317
- if ":" in key and declaration(target, key).is_empty():
318
- target.set_indexed(NodePath(key), value)
372
+ var place: Array = GdhPropertyPaths.holder_of(target, key)
373
+ var holder: Object = place[0]
374
+ var name: String = place[1]
375
+ if ":" in name and declaration(holder, name).is_empty():
376
+ holder.set_indexed(NodePath(name), value)
319
377
  else:
320
- target.set(key, value)
378
+ holder.set(name, value)
321
379
 
322
380
 
323
381
  ## The declaration the last step of [param path] would have, typed as the component it reaches, so
@@ -325,9 +383,9 @@ func _write(target: Object, key: String, value: Variant) -> void:
325
383
  ## {"refused": why} for the first step that is not there.
326
384
  func _component_slot(target: Object, path: String) -> Dictionary:
327
385
  var steps: PackedStringArray = path.split(":")
328
- var owner: String = target.get_class()
386
+ var owner: String = GdhPropertyPaths.class_called(target)
329
387
  if declaration(target, steps[0]).is_empty():
330
- return {"refused": "%s has no property %s" % [owner, steps[0]]}
388
+ return {"refused": no_property(target, steps[0])}
331
389
  var value: Variant = target.get(steps[0])
332
390
  var walked: String = steps[0]
333
391
  for index: int in range(1, steps.size()):
@@ -348,7 +406,7 @@ func _component_slot(target: Object, path: String) -> Dictionary:
348
406
  if not step in has:
349
407
  return {
350
408
  "refused":
351
- "%s.%s is %s, so it has no %s; it has %s" % [owner, walked, sort, step, ", ".join(has)]
409
+ "%s.%s is %s, so it has no %s; it has %s." % [owner, walked, sort, step, ", ".join(has)]
352
410
  }
353
411
  value = value[step]
354
412
  walked += ":" + step
@@ -370,10 +428,17 @@ func value_for(target: Object, declared: Dictionary, raw: Variant) -> Array:
370
428
  if chosen == null:
371
429
  var hinted: Dictionary = GdhDeclared.enum_values(str(declared.get("hint_string", "")))
372
430
  chosen = GdhDeclared.enum_named(hinted, str(raw))
431
+ var constants: Dictionary = {}
432
+ if chosen == null:
433
+ constants = GdhDeclared.engine_constants(target, property)
434
+ chosen = GdhDeclared.enum_named(constants, str(raw))
373
435
  if chosen != null:
374
436
  return [true, chosen]
375
437
  if not str(raw).strip_edges().is_valid_int():
376
- return [false, GdhDeclared.enum_refusal(class_called(target), property, str(raw), choices)]
438
+ var why: String = GdhDeclared.enum_refusal(
439
+ GdhPropertyPaths.class_called(target), property, str(raw), choices, constants
440
+ )
441
+ return [false, why]
377
442
 
378
443
  # A resource-valued property takes the path of one, which is how a caller names a TileSet,
379
444
  # a material or a theme: there is no other way to hand a tool a Resource.
@@ -384,17 +449,28 @@ func value_for(target: Object, declared: Dictionary, raw: Variant) -> Array:
384
449
  return found
385
450
  value = found[1]
386
451
  else:
387
- var elements: Array = elements_of(declared)
452
+ var elements: Array = GdhDeclared.elements_of(declared)
388
453
  if _fraction(raw, expected_type, elements):
389
454
  return [false, _fraction_refusal(target, property, raw)]
390
455
  var rounded: String = GdhSerialisation.inexact(raw, expected_type)
391
456
  if not rounded.is_empty():
392
- return [false, "%s.%s holds int: %s." % [class_called(target), property, rounded]]
457
+ return [
458
+ false, "%s.%s holds int: %s." % [GdhPropertyPaths.class_called(target), property, rounded]
459
+ ]
393
460
  _refused = ""
394
461
  value = parse(raw, expected_type, elements)
395
462
  if not _refused.is_empty():
396
463
  return [false, _refused]
397
- var refusal: String = cannot_hold(class_called(target), property, value, expected_type, elements)
464
+ if expected_type == TYPE_ARRAY and elements == [TYPE_OBJECT] and value is Array:
465
+ var listed: Array = value
466
+ return _object_list(target, declared, listed)
467
+ if expected_type == TYPE_DICTIONARY and elements.size() == 2 and value is Dictionary:
468
+ if GdhRead.as_int(elements[1], TYPE_NIL) == TYPE_OBJECT:
469
+ var mapped: Dictionary = value
470
+ return _object_entries(target, declared, mapped)
471
+ var refusal: String = cannot_hold(
472
+ GdhPropertyPaths.class_called(target), property, value, expected_type, elements
473
+ )
398
474
  if not refusal.is_empty():
399
475
  return [false, refusal]
400
476
  if value is Object:
@@ -449,60 +525,102 @@ func _wrong_class(value: Object, declared: Dictionary) -> String:
449
525
  return ""
450
526
  var names: PackedStringArray = wanted.split(",", false)
451
527
  for name: String in names:
452
- if value.is_class(name.strip_edges()):
528
+ if GdhDeclared.is_a(value, name.strip_edges()):
453
529
  return ""
454
- var script: Script = value.get_script()
455
- while script != null:
456
- if str(script.get_global_name()) == name.strip_edges():
457
- return ""
458
- script = script.get_base_script()
459
530
  return (
460
531
  "%s holds %s, and the value given is a %s."
461
532
  % [str(declared.get("name", "")), " or ".join(names), value.get_class()]
462
533
  )
463
534
 
464
535
 
465
- ## The element types a typed Array or Dictionary property declares: [element] for an array,
466
- ## [key, value] for a dictionary, [] when nothing is declared.
467
- ##
468
- ## Read from the declaration because the value cannot say: a script's exported `Array[int]` sits
469
- ## behind a placeholder in the editor, and measured on 4.7.2 that placeholder stores [1.5, 2.0]
470
- ## as given, which the game then fails to load into the typed array.
471
- static func elements_of(declared: Dictionary) -> Array:
472
- var type: int = GdhRead.as_int(declared.get("type", TYPE_NIL), TYPE_NIL)
473
- var hint: int = GdhRead.as_int(declared.get("hint", PROPERTY_HINT_NONE))
474
- var wanted: String = str(declared.get("hint_string", ""))
475
- if wanted.is_empty():
476
- return []
477
- if hint == PROPERTY_HINT_TYPE_STRING:
478
- var parts: PackedStringArray = wanted.split(";")
479
- if type == TYPE_ARRAY and parts.size() == 1:
480
- return [_type_in_hint(parts[0])]
481
- if type == TYPE_DICTIONARY and parts.size() == 2:
482
- return [_type_in_hint(parts[0]), _type_in_hint(parts[1])]
483
- return []
484
- if hint == PROPERTY_HINT_ARRAY_TYPE and type == TYPE_ARRAY:
485
- return [_type_named(wanted)]
486
- if hint == PROPERTY_HINT_DICTIONARY_TYPE and type == TYPE_DICTIONARY:
487
- var parts: PackedStringArray = wanted.split(";")
488
- if parts.size() == 2:
489
- return [_type_named(parts[0]), _type_named(parts[1])]
490
- return []
491
-
492
-
493
- ## The type at the front of one entry of a type-string hint, "2:" or "24/17:Texture2D".
494
- static func _type_in_hint(entry: String) -> int:
495
- var number: String = entry.get_slice(":", 0).get_slice("/", 0)
496
- return number.to_int() if number.is_valid_int() else TYPE_NIL
497
-
498
-
499
- ## The Variant type [param name] names, or TYPE_OBJECT for a class.
500
- static func _type_named(name: String) -> int:
501
- var named: String = name.get_slice(":", 0).strip_edges()
502
- for type: int in TYPE_MAX:
503
- if type_string(type) == named:
504
- return type
505
- return TYPE_OBJECT if not named.is_empty() else TYPE_NIL
536
+ ## [param value] as the typed list of objects that [param declared] holds on [param target], as
537
+ ## [ok, list or why not]. Measured on 4.7.2, a scene's node whose script exports Array[Item] holds []
538
+ ## after being given an untyped list of Items, and keeps a list typed as its own is.
539
+ func _object_list(target: Object, declared: Dictionary, value: Array) -> Array:
540
+ var property: String = str(declared.get("name", ""))
541
+ var kind: Array = GdhDeclared.object_kind(declared, 0)
542
+ var holding: String = "%s.%s holds %s" % [GdhPropertyPaths.class_called(target), property, kind[0]]
543
+ var items: Array = []
544
+ for index: int in value.size():
545
+ var place: Array = ["item %d of the list given" % index, "item %d of %s" % [index, property]]
546
+ var item: Array = _object_item(target, value[index], kind, holding, place)
547
+ if not item[0]:
548
+ return item
549
+ items.append(item[1])
550
+ var held: Variant = target.get(property)
551
+ var current: Array = held if held is Array else []
552
+ if current.is_typed():
553
+ return [
554
+ true,
555
+ Array(
556
+ items, current.get_typed_builtin(), current.get_typed_class_name(), current.get_typed_script()
557
+ )
558
+ ]
559
+ var typing: Array = GdhDeclared.object_typing(str(kind[0]))
560
+ if typing.is_empty():
561
+ return [true, items]
562
+ var base: StringName = typing[0]
563
+ var script: Script = typing[1]
564
+ return [true, Array(items, TYPE_OBJECT, base, script)]
565
+
566
+
567
+ ## [param value] as the typed map to objects that [param declared] holds on [param target], as
568
+ ## [ok, map or why not], typed for the same reason as [method _object_list].
569
+ func _object_entries(target: Object, declared: Dictionary, value: Dictionary) -> Array:
570
+ var property: String = str(declared.get("name", ""))
571
+ var kind: Array = GdhDeclared.object_kind(declared, 1)
572
+ var holding: String = "%s.%s holds %s values" % [GdhPropertyPaths.class_called(target), property, kind[0]]
573
+ var entries: Dictionary = {}
574
+ for key: Variant in value:
575
+ var place: Array = ["the one given for %s" % shown(key), "%s[%s]" % [property, shown(key)]]
576
+ var entry: Array = _object_item(target, value[key], kind, holding, place)
577
+ if not entry[0]:
578
+ return entry
579
+ entries[key] = entry[1]
580
+ var held: Variant = target.get(property)
581
+ var current: Dictionary = held if held is Dictionary else {}
582
+ if current.is_typed():
583
+ return [
584
+ true,
585
+ Dictionary(
586
+ entries,
587
+ current.get_typed_key_builtin(),
588
+ current.get_typed_key_class_name(),
589
+ current.get_typed_key_script(),
590
+ current.get_typed_value_builtin(),
591
+ current.get_typed_value_class_name(),
592
+ current.get_typed_value_script()
593
+ )
594
+ ]
595
+ var typing: Array = GdhDeclared.object_typing(str(kind[0]))
596
+ var key_type: int = GdhRead.as_int(GdhDeclared.elements_of(declared)[0], TYPE_NIL)
597
+ if typing.is_empty() or key_type == TYPE_OBJECT:
598
+ return [true, entries]
599
+ var base: StringName = typing[0]
600
+ var script: Script = typing[1]
601
+ return [true, Dictionary(entries, key_type, &"", null, TYPE_OBJECT, base, script)]
602
+
603
+
604
+ ## [param given], an object for a typed list or map, as [ok, object or why not]: a resource's
605
+ ## res:// path is the resource, and for nodes a path from [param target] is the node there.
606
+ ## [param holding] says what the list or map holds, and [param place] is [where the value sits in
607
+ ## what was given, what it is called in the property].
608
+ func _object_item(target: Object, given: Variant, kind: Array, holding: String, place: Array) -> Array:
609
+ var item: Variant = given
610
+ if typeof(item) == TYPE_STRING:
611
+ var found: Array = (
612
+ _node_reference(target, str(place[1]), item)
613
+ if kind[1]
614
+ else _resource_at(str(item), str(place[1]))
615
+ )
616
+ if not found[0]:
617
+ return found
618
+ item = found[1]
619
+ var object: Object = item if item is Object else null
620
+ if item == null or (object != null and GdhDeclared.is_a(object, str(kind[0]))):
621
+ return [true, item]
622
+ var named: String = object.get_class() if object != null else shown(item)
623
+ return [false, "%s, and %s is %s." % [holding, place[0], named]]
506
624
 
507
625
 
508
626
  ## Whether [param value] holds a number with a fractional part where an integer is wanted, in a
@@ -539,7 +657,7 @@ func _fraction_refusal(target: Object, property: String, raw: Variant) -> String
539
657
  "%s.%s holds whole numbers, and %s would be cut to fit. Setting it would save a different "
540
658
  + "value than the one asked for."
541
659
  )
542
- % [class_called(target), property, shown(raw)]
660
+ % [GdhPropertyPaths.class_called(target), property, shown(raw)]
543
661
  )
544
662
 
545
663
 
@@ -564,16 +682,15 @@ func colour_from(raw: Variant, named: String) -> Array:
564
682
  ]
565
683
 
566
684
 
567
- ## What [param target] is called in a refusal: the class_name its script declares, or else its
568
- ## engine class. A resource made as a project class is a Resource to the engine, and "Resource.kind"
569
- ## named a property no Resource has.
570
- static func class_called(target: Object) -> String:
571
- var attached: Variant = target.get_script()
572
- if attached is Script:
573
- var script: Script = attached
574
- if not str(script.get_global_name()).is_empty():
575
- return str(script.get_global_name())
576
- return target.get_class()
685
+ ## The refusal for [param property], which [param target] does not declare, naming the declared
686
+ ## ones nearest it: "StyleBoxFlat has no property shadow_sizee" left the caller nothing to send.
687
+ static func no_property(target: Object, property: String) -> String:
688
+ return (
689
+ GdhRead.near_property(
690
+ "%s has no property %s" % [GdhPropertyPaths.class_called(target), property], target, property
691
+ )
692
+ + "."
693
+ )
577
694
 
578
695
 
579
696
  ## The resource [param path] names, as [found, resource or why not].
@@ -621,15 +738,10 @@ func _resource_at(path: String, property: String) -> Array:
621
738
  ## its name, so without this a typo or a heading saved a file that had not changed and reported it
622
739
  ## as one that had.
623
740
  func declaration(target: Object, property: String) -> Dictionary:
624
- for declared: Dictionary in target.get_property_list():
625
- if str(declared.get("name", "")) != property:
626
- continue
627
- if GdhRead.as_int(declared.get("usage", 0)) & HEADINGS:
628
- continue
629
- return declared
630
- if target is Theme:
741
+ var declared: Dictionary = GdhPropertyPaths.declared_on(target, property)
742
+ if declared.is_empty() and target is Theme:
631
743
  return _theme_item(property)
632
- return {}
744
+ return declared
633
745
 
634
746
 
635
747
  ## The entry a Theme would declare for [param property] once it held that item, or {} when the name
@@ -648,107 +760,6 @@ static func _theme_item(property: String) -> Dictionary:
648
760
  return made
649
761
 
650
762
 
651
- ## Whether [param held] is [param given], as a property read back after a set answers it.
652
- ##
653
- ## Close enough rather than equal for anything made of floats: a Vector2 property stores 32-bit
654
- ## components, so 0.1 comes back as 0.100000001. A packed array read back against the list it was
655
- ## given compares element by element, since `==` between the two is an error rather than false.
656
- static func same(held: Variant, given: Variant) -> bool:
657
- var held_type: int = typeof(held)
658
- var given_type: int = typeof(given)
659
- # An empty object property reads back as an Object that is null, not as nil, and the two are
660
- # the same nothing.
661
- var held_none: bool = held_type == TYPE_NIL or (held_type == TYPE_OBJECT and held == null)
662
- var given_none: bool = given_type == TYPE_NIL or (given_type == TYPE_OBJECT and given == null)
663
- if held_none or given_none:
664
- return held_none == given_none
665
- if held_type == TYPE_OBJECT or given_type == TYPE_OBJECT:
666
- return _same_object(held, given)
667
- if GdhSerialisation.NUMBERS.has(held_type) and GdhSerialisation.NUMBERS.has(given_type):
668
- if held_type == TYPE_BOOL or given_type == TYPE_BOOL:
669
- return held_type == given_type and held == given
670
- var held_number: float = GdhRead.as_float(held)
671
- var given_number: float = GdhRead.as_float(given)
672
- return (is_nan(held_number) and is_nan(given_number)) or is_equal_approx(held_number, given_number)
673
- if GdhSerialisation.TEXTS.has(held_type) and GdhSerialisation.TEXTS.has(given_type):
674
- return str(held) == str(given)
675
- if _listed(held_type) and _listed(given_type):
676
- var held_items: Array = type_convert(held, TYPE_ARRAY)
677
- var given_items: Array = type_convert(given, TYPE_ARRAY)
678
- if held_items.size() != given_items.size():
679
- return false
680
- for index: int in held_items.size():
681
- if not same(held_items[index], given_items[index]):
682
- return false
683
- return true
684
- if held_type == TYPE_DICTIONARY and given_type == TYPE_DICTIONARY:
685
- var held_entries: Dictionary = held
686
- var given_entries: Dictionary = given
687
- return _same_entries(held_entries, given_entries)
688
- if held_type != given_type:
689
- return false
690
- return _same_shape(held, given)
691
-
692
-
693
- static func _same_object(held: Variant, given: Variant) -> bool:
694
- if typeof(held) != TYPE_OBJECT or typeof(given) != TYPE_OBJECT:
695
- return false
696
- if is_same(held, given):
697
- return true
698
- if not held is Resource or not given is Resource:
699
- return false
700
- var held_resource: Resource = held
701
- var given_resource: Resource = given
702
- if not held_resource.resource_path.is_empty():
703
- return held_resource.resource_path == given_resource.resource_path
704
- # A resource local to the scene is duplicated for every instance, so the copy read back is
705
- # another object of the same class.
706
- return held_resource.resource_local_to_scene and held_resource.get_class() == given_resource.get_class()
707
-
708
-
709
- static func _same_entries(held: Dictionary, given: Dictionary) -> bool:
710
- if held.size() != given.size():
711
- return false
712
- for key: Variant in given:
713
- var found: bool = false
714
- for held_key: Variant in held:
715
- if same(held_key, key):
716
- found = same(held[held_key], given[key])
717
- break
718
- if not found:
719
- return false
720
- return true
721
-
722
-
723
- ## Two values of one type made of floats, compared component by component through the type's own
724
- ## text form, which lists every component: each type has its own is_equal_approx, and reaching it
725
- ## through a Variant is a call the typed gate refuses.
726
- static func _same_shape(held: Variant, given: Variant) -> bool:
727
- if typeof(held) not in FLOAT_SHAPES:
728
- return held == given
729
- var held_parts: Array[float] = _components(held)
730
- var given_parts: Array[float] = _components(given)
731
- if held_parts.size() != given_parts.size():
732
- return false
733
- for index: int in held_parts.size():
734
- if not same(held_parts[index], given_parts[index]):
735
- return false
736
- return true
737
-
738
-
739
- static func _components(value: Variant) -> Array[float]:
740
- var parts: Array[float] = []
741
- var text: String = var_to_str(value)
742
- var inner: String = text.substr(text.find("(") + 1).trim_suffix(")")
743
- for part: String in inner.split(",", false):
744
- parts.append(part.strip_edges().to_float())
745
- return parts
746
-
747
-
748
- static func _listed(type: int) -> bool:
749
- return type == TYPE_ARRAY or PACKED_ELEMENTS.has(type)
750
-
751
-
752
763
  ## [param value] as a caller would write it, for a refusal to quote.
753
764
  func shown(value: Variant) -> String:
754
765
  return JSON.stringify(_values.serialize_value(value), "", false)
@@ -785,18 +796,22 @@ func _parse_resource_tag(value: Dictionary) -> Array:
785
796
  _refused = str(found[1])
786
797
  return [true, null]
787
798
  return [true, found[1]]
788
- # What a read answers for a resource built into the scene it was read from: a class and no
799
+ # What a read answers where a resource built into a scene recurs inside itself: a class and no
789
800
  # path. Taken as nothing, it cleared the property it was written back to.
790
801
  if value.has("class"):
791
802
  _refused = (
792
803
  (
793
- "The %s given has no path: it was read from a resource built into a scene, which cannot "
794
- + 'be referred to from elsewhere. Build a new one with "_type" set to its class and its '
795
- + "properties beside it."
804
+ "The %s given has no path: a read names a resource built into a scene by its class "
805
+ + "alone where it recurs inside itself, and nothing elsewhere can refer to it. Build "
806
+ + 'a new one with "_type" set to its class and its properties beside it.'
796
807
  )
797
808
  % str(value["class"])
798
809
  )
799
810
  return [true, null]
811
+ # A plain Resource with a script or values of its own, as a read answers one built into a scene,
812
+ # is one to build; taken as nothing, writing such a read back cleared the property.
813
+ if value.size() > 1:
814
+ return _parse_new_resource("Resource", value)
800
815
  return [true, null]
801
816
 
802
817
 
@@ -804,15 +819,21 @@ func _parse_resource_tag(value: Dictionary) -> Array:
804
819
  ## NavigationRegion2D can arrive with its NavigationPolygon and an AnimationTree with its root
805
820
  ## state machine in the same add as any other property.
806
821
  func _parse_new_resource(type_tag: String, value: Dictionary) -> Array:
822
+ var built: Resource = null
807
823
  if (
808
- type_tag.is_empty()
809
- or not ClassDB.class_exists(type_tag)
810
- or not ClassDB.is_parent_class(type_tag, "Resource")
811
- or not ClassDB.can_instantiate(type_tag)
824
+ ClassDB.class_exists(type_tag)
825
+ and ClassDB.is_parent_class(type_tag, "Resource")
826
+ and ClassDB.can_instantiate(type_tag)
812
827
  ):
828
+ built = ClassDB.instantiate(type_tag)
829
+ elif not type_tag.is_empty() and not ClassDB.class_exists(type_tag):
830
+ # A script's class_name, as a read answers it under scriptClass.
831
+ var script: Script = GdhDeclared.script_class(type_tag)
832
+ if script != null and ClassDB.is_parent_class(script.get_instance_base_type(), "Resource"):
833
+ built = ClassDB.instantiate(script.get_instance_base_type())
834
+ built.set_script(script)
835
+ if built == null:
813
836
  return [false, null]
814
-
815
- var built: Resource = ClassDB.instantiate(type_tag)
816
837
  var wanted: Dictionary = {}
817
838
  for key: Variant in value:
818
839
  var property: String = str(key)
@@ -822,6 +843,7 @@ func _parse_new_resource(type_tag: String, value: Dictionary) -> Array:
822
843
  if not refused.is_empty():
823
844
  _refused = refused
824
845
  return [true, null]
846
+ GdhDeclared.unmade(built, wanted)
825
847
  return [true, built]
826
848
 
827
849