gdharness 1.3.3 → 1.3.5

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 (52) hide show
  1. package/build/cli.js +532 -456
  2. package/build/godot/addons/gdharness_editor/animation_reads.gd +3 -2
  3. package/build/godot/addons/gdharness_editor/declared_values.gd +181 -4
  4. package/build/godot/addons/gdharness_editor/file_reads.gd +15 -3
  5. package/build/godot/addons/gdharness_editor/headless_check.gd +44 -23
  6. package/build/godot/addons/gdharness_editor/property_paths.gd +225 -0
  7. package/build/godot/addons/gdharness_editor/property_paths.gd.uid +1 -0
  8. package/build/godot/addons/gdharness_editor/property_values.gd +192 -231
  9. package/build/godot/addons/gdharness_editor/reading.gd +15 -0
  10. package/build/godot/addons/gdharness_editor/same_values.gd +177 -0
  11. package/build/godot/addons/gdharness_editor/same_values.gd.uid +1 -0
  12. package/build/godot/addons/gdharness_editor/scene_file.gd +97 -14
  13. package/build/godot/addons/gdharness_editor/serialisation.gd +75 -3
  14. package/build/godot/addons/gdharness_editor/tool_executor.gd +9 -3
  15. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +38 -35
  16. package/build/godot/addons/gdharness_editor/tools/class_tools.gd +20 -12
  17. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +3 -2
  18. package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +91 -0
  19. package/build/godot/addons/gdharness_editor/tools/scene_groups.gd.uid +1 -0
  20. package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +68 -2
  21. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +94 -333
  22. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +363 -0
  23. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd.uid +1 -0
  24. package/build/godot/addons/gdharness_runtime/declared_values.gd +293 -0
  25. package/build/godot/addons/gdharness_runtime/declared_values.gd.uid +1 -0
  26. package/build/godot/addons/gdharness_runtime/reading.gd +15 -0
  27. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +4 -0
  28. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +29 -1
  29. package/build/godot/addons/gdharness_runtime/runtime_input.gd +26 -39
  30. package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +18 -0
  31. package/build/godot/addons/gdharness_runtime/runtime_pad.gd +3 -2
  32. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +2 -1
  33. package/build/godot/addons/gdharness_runtime/runtime_values.gd +62 -5
  34. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +48 -15
  35. package/build/godot/addons/gdharness_runtime/serialisation.gd +75 -3
  36. package/build/godot/operations/animation_reads.gd +3 -2
  37. package/build/godot/operations/classdb_queries.gd +112 -46
  38. package/build/godot/operations/declared_values.gd +181 -4
  39. package/build/godot/operations/dependencies.gd +14 -39
  40. package/build/godot/operations/editor_tools.gd +4 -3
  41. package/build/godot/operations/file_reads.gd +15 -3
  42. package/build/godot/operations/gdscript_analysis.gd +229 -75
  43. package/build/godot/operations/gdscript_authoring.gd +157 -36
  44. package/build/godot/operations/import_pipeline.gd +17 -6
  45. package/build/godot/operations/logger.gd +7 -0
  46. package/build/godot/operations/property_paths.gd +225 -0
  47. package/build/godot/operations/property_values.gd +192 -231
  48. package/build/godot/operations/reading.gd +15 -0
  49. package/build/godot/operations/same_values.gd +177 -0
  50. package/build/godot/operations/serialisation.gd +75 -3
  51. package/build/keeper.js +83 -69
  52. 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
@@ -195,21 +181,39 @@ func write_all(target: Object, properties: Dictionary) -> String:
195
181
  var unwritten: String = (
196
182
  "" if properties.size() < 2 else " None of the %d properties given was written." % properties.size()
197
183
  )
198
- 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:
199
192
  var property: String = str(key)
200
- var declared: Dictionary = declaration(target, property)
201
- if declared.is_empty() and ":" in property:
202
- 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)
203
207
  if declared.has("refused"):
204
208
  return str(declared["refused"]) + unwritten
205
- if declared.is_empty() and target is ShaderMaterial and property.begins_with("shader_parameter/"):
206
- 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
207
211
  if declared.is_empty():
208
- return "%s has no property %s." % [class_called(target), property] + unwritten
209
- 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])
210
214
  if not value[0]:
211
215
  return str(value[1]) + unwritten
212
- 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]))
213
217
  if not outside.is_empty():
214
218
  _outside_declared[property] = outside
215
219
  _write(target, property, value[1])
@@ -217,16 +221,16 @@ func write_all(target: Object, properties: Dictionary) -> String:
217
221
  declarations[property] = declared
218
222
 
219
223
  for property: String in given:
220
- if not same(read_at(target, property), given[property]):
224
+ if not GdhSame.same(read_at(target, property), given[property]):
221
225
  _write(target, property, given[property])
222
226
  for property: String in given:
223
227
  var held: Variant = read_at(target, property)
224
- if not same(held, given[property]):
228
+ if not GdhSame.same(held, given[property]):
225
229
  var declared: Dictionary = declarations[property]
226
230
  return _not_held(target, property, given[property], held, declared)
227
231
  for property: String in before:
228
232
  var now: Variant = target.get(property)
229
- if not same(now, before[property]):
233
+ if not GdhSame.same(now, before[property]):
230
234
  _also_changed[property] = [before[property], now]
231
235
  return ""
232
236
 
@@ -241,7 +245,7 @@ func _not_held(
241
245
  ) -> String:
242
246
  var said: String = (
243
247
  "%s.%s was given %s and holds %s after it was set"
244
- % [target.get_class(), property, shown(value), shown(held)]
248
+ % [GdhPropertyPaths.class_called(target), property, shown(value), shown(held)]
245
249
  )
246
250
  var type: int = GdhRead.as_int(declared.get("type", TYPE_NIL))
247
251
  var hint: int = GdhRead.as_int(declared.get("hint", PROPERTY_HINT_NONE))
@@ -354,18 +358,24 @@ func _stored(target: Object, leave: Dictionary) -> Dictionary:
354
358
 
355
359
  ## What [param target] holds under [param key], a property or a colon path into one's components.
356
360
  func read_at(target: Object, key: String) -> Variant:
357
- if ":" in key and declaration(target, key).is_empty():
358
- return target.get_indexed(NodePath(key))
359
- 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)
360
367
 
361
368
 
362
369
  ## Writes [param value] under [param key], putting a changed component back into the property it
363
370
  ## was read out of, and that one into the one above it.
364
371
  func _write(target: Object, key: String, value: Variant) -> void:
365
- if ":" in key and declaration(target, key).is_empty():
366
- 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)
367
377
  else:
368
- target.set(key, value)
378
+ holder.set(name, value)
369
379
 
370
380
 
371
381
  ## The declaration the last step of [param path] would have, typed as the component it reaches, so
@@ -373,9 +383,9 @@ func _write(target: Object, key: String, value: Variant) -> void:
373
383
  ## {"refused": why} for the first step that is not there.
374
384
  func _component_slot(target: Object, path: String) -> Dictionary:
375
385
  var steps: PackedStringArray = path.split(":")
376
- var owner: String = target.get_class()
386
+ var owner: String = GdhPropertyPaths.class_called(target)
377
387
  if declaration(target, steps[0]).is_empty():
378
- return {"refused": "%s has no property %s" % [owner, steps[0]]}
388
+ return {"refused": no_property(target, steps[0])}
379
389
  var value: Variant = target.get(steps[0])
380
390
  var walked: String = steps[0]
381
391
  for index: int in range(1, steps.size()):
@@ -396,7 +406,7 @@ func _component_slot(target: Object, path: String) -> Dictionary:
396
406
  if not step in has:
397
407
  return {
398
408
  "refused":
399
- "%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)]
400
410
  }
401
411
  value = value[step]
402
412
  walked += ":" + step
@@ -410,18 +420,12 @@ func value_for(target: Object, declared: Dictionary, raw: Variant) -> Array:
410
420
  var hint: int = GdhRead.as_int(declared.get("hint", PROPERTY_HINT_NONE))
411
421
  if expected_type == TYPE_OBJECT and hint == PROPERTY_HINT_NODE_TYPE:
412
422
  return _node_reference(target, property, raw)
413
- # An enum is written by the name of a value as well as its number, since the name is what the
414
- # script and the inspector show and the number is only where the value happens to sit.
415
423
  if expected_type == TYPE_INT and hint == PROPERTY_HINT_ENUM and typeof(raw) == TYPE_STRING:
416
- var choices: Dictionary = GdhDeclared.enum_of(target, declared)
417
- var chosen: Variant = GdhDeclared.enum_named(choices, str(raw))
418
- if chosen == null:
419
- var hinted: Dictionary = GdhDeclared.enum_values(str(declared.get("hint_string", "")))
420
- chosen = GdhDeclared.enum_named(hinted, str(raw))
421
- if chosen != null:
422
- return [true, chosen]
423
- if not str(raw).strip_edges().is_valid_int():
424
- return [false, GdhDeclared.enum_refusal(class_called(target), property, str(raw), choices)]
424
+ var by_name: Array = GdhDeclared.enum_by_name(
425
+ target, declared, str(raw), GdhPropertyPaths.class_called(target)
426
+ )
427
+ if by_name[0] or not str(by_name[1]).is_empty():
428
+ return by_name
425
429
 
426
430
  # A resource-valued property takes the path of one, which is how a caller names a TileSet,
427
431
  # a material or a theme: there is no other way to hand a tool a Resource.
@@ -432,17 +436,28 @@ func value_for(target: Object, declared: Dictionary, raw: Variant) -> Array:
432
436
  return found
433
437
  value = found[1]
434
438
  else:
435
- var elements: Array = elements_of(declared)
439
+ var elements: Array = GdhDeclared.elements_of(declared)
436
440
  if _fraction(raw, expected_type, elements):
437
441
  return [false, _fraction_refusal(target, property, raw)]
438
442
  var rounded: String = GdhSerialisation.inexact(raw, expected_type)
439
443
  if not rounded.is_empty():
440
- return [false, "%s.%s holds int: %s." % [class_called(target), property, rounded]]
444
+ return [
445
+ false, "%s.%s holds int: %s." % [GdhPropertyPaths.class_called(target), property, rounded]
446
+ ]
441
447
  _refused = ""
442
448
  value = parse(raw, expected_type, elements)
443
449
  if not _refused.is_empty():
444
450
  return [false, _refused]
445
- var refusal: String = cannot_hold(class_called(target), property, value, expected_type, elements)
451
+ if expected_type == TYPE_ARRAY and elements == [TYPE_OBJECT] and value is Array:
452
+ var listed: Array = value
453
+ return _object_list(target, declared, listed)
454
+ if expected_type == TYPE_DICTIONARY and elements.size() == 2 and value is Dictionary:
455
+ if GdhRead.as_int(elements[1], TYPE_NIL) == TYPE_OBJECT:
456
+ var mapped: Dictionary = value
457
+ return _object_entries(target, declared, mapped)
458
+ var refusal: String = cannot_hold(
459
+ GdhPropertyPaths.class_called(target), property, value, expected_type, elements
460
+ )
446
461
  if not refusal.is_empty():
447
462
  return [false, refusal]
448
463
  if value is Object:
@@ -497,60 +512,102 @@ func _wrong_class(value: Object, declared: Dictionary) -> String:
497
512
  return ""
498
513
  var names: PackedStringArray = wanted.split(",", false)
499
514
  for name: String in names:
500
- if value.is_class(name.strip_edges()):
515
+ if GdhDeclared.is_a(value, name.strip_edges()):
501
516
  return ""
502
- var script: Script = value.get_script()
503
- while script != null:
504
- if str(script.get_global_name()) == name.strip_edges():
505
- return ""
506
- script = script.get_base_script()
507
517
  return (
508
518
  "%s holds %s, and the value given is a %s."
509
519
  % [str(declared.get("name", "")), " or ".join(names), value.get_class()]
510
520
  )
511
521
 
512
522
 
513
- ## The element types a typed Array or Dictionary property declares: [element] for an array,
514
- ## [key, value] for a dictionary, [] when nothing is declared.
515
- ##
516
- ## Read from the declaration because the value cannot say: a script's exported `Array[int]` sits
517
- ## behind a placeholder in the editor, and measured on 4.7.2 that placeholder stores [1.5, 2.0]
518
- ## as given, which the game then fails to load into the typed array.
519
- static func elements_of(declared: Dictionary) -> Array:
520
- var type: int = GdhRead.as_int(declared.get("type", TYPE_NIL), TYPE_NIL)
521
- var hint: int = GdhRead.as_int(declared.get("hint", PROPERTY_HINT_NONE))
522
- var wanted: String = str(declared.get("hint_string", ""))
523
- if wanted.is_empty():
524
- return []
525
- if hint == PROPERTY_HINT_TYPE_STRING:
526
- var parts: PackedStringArray = wanted.split(";")
527
- if type == TYPE_ARRAY and parts.size() == 1:
528
- return [_type_in_hint(parts[0])]
529
- if type == TYPE_DICTIONARY and parts.size() == 2:
530
- return [_type_in_hint(parts[0]), _type_in_hint(parts[1])]
531
- return []
532
- if hint == PROPERTY_HINT_ARRAY_TYPE and type == TYPE_ARRAY:
533
- return [_type_named(wanted)]
534
- if hint == PROPERTY_HINT_DICTIONARY_TYPE and type == TYPE_DICTIONARY:
535
- var parts: PackedStringArray = wanted.split(";")
536
- if parts.size() == 2:
537
- return [_type_named(parts[0]), _type_named(parts[1])]
538
- return []
539
-
540
-
541
- ## The type at the front of one entry of a type-string hint, "2:" or "24/17:Texture2D".
542
- static func _type_in_hint(entry: String) -> int:
543
- var number: String = entry.get_slice(":", 0).get_slice("/", 0)
544
- return number.to_int() if number.is_valid_int() else TYPE_NIL
545
-
546
-
547
- ## The Variant type [param name] names, or TYPE_OBJECT for a class.
548
- static func _type_named(name: String) -> int:
549
- var named: String = name.get_slice(":", 0).strip_edges()
550
- for type: int in TYPE_MAX:
551
- if type_string(type) == named:
552
- return type
553
- return TYPE_OBJECT if not named.is_empty() else TYPE_NIL
523
+ ## [param value] as the typed list of objects that [param declared] holds on [param target], as
524
+ ## [ok, list or why not]. Measured on 4.7.2, a scene's node whose script exports Array[Item] holds []
525
+ ## after being given an untyped list of Items, and keeps a list typed as its own is.
526
+ func _object_list(target: Object, declared: Dictionary, value: Array) -> Array:
527
+ var property: String = str(declared.get("name", ""))
528
+ var kind: Array = GdhDeclared.object_kind(declared, 0)
529
+ var holding: String = "%s.%s holds %s" % [GdhPropertyPaths.class_called(target), property, kind[0]]
530
+ var items: Array = []
531
+ for index: int in value.size():
532
+ var place: Array = ["item %d of the list given" % index, "item %d of %s" % [index, property]]
533
+ var item: Array = _object_item(target, value[index], kind, holding, place)
534
+ if not item[0]:
535
+ return item
536
+ items.append(item[1])
537
+ var held: Variant = target.get(property)
538
+ var current: Array = held if held is Array else []
539
+ if current.is_typed():
540
+ return [
541
+ true,
542
+ Array(
543
+ items, current.get_typed_builtin(), current.get_typed_class_name(), current.get_typed_script()
544
+ )
545
+ ]
546
+ var typing: Array = GdhDeclared.object_typing(str(kind[0]))
547
+ if typing.is_empty():
548
+ return [true, items]
549
+ var base: StringName = typing[0]
550
+ var script: Script = typing[1]
551
+ return [true, Array(items, TYPE_OBJECT, base, script)]
552
+
553
+
554
+ ## [param value] as the typed map to objects that [param declared] holds on [param target], as
555
+ ## [ok, map or why not], typed for the same reason as [method _object_list].
556
+ func _object_entries(target: Object, declared: Dictionary, value: Dictionary) -> Array:
557
+ var property: String = str(declared.get("name", ""))
558
+ var kind: Array = GdhDeclared.object_kind(declared, 1)
559
+ var holding: String = "%s.%s holds %s values" % [GdhPropertyPaths.class_called(target), property, kind[0]]
560
+ var entries: Dictionary = {}
561
+ for key: Variant in value:
562
+ var place: Array = ["the one given for %s" % shown(key), "%s[%s]" % [property, shown(key)]]
563
+ var entry: Array = _object_item(target, value[key], kind, holding, place)
564
+ if not entry[0]:
565
+ return entry
566
+ entries[key] = entry[1]
567
+ var held: Variant = target.get(property)
568
+ var current: Dictionary = held if held is Dictionary else {}
569
+ if current.is_typed():
570
+ return [
571
+ true,
572
+ Dictionary(
573
+ entries,
574
+ current.get_typed_key_builtin(),
575
+ current.get_typed_key_class_name(),
576
+ current.get_typed_key_script(),
577
+ current.get_typed_value_builtin(),
578
+ current.get_typed_value_class_name(),
579
+ current.get_typed_value_script()
580
+ )
581
+ ]
582
+ var typing: Array = GdhDeclared.object_typing(str(kind[0]))
583
+ var key_type: int = GdhRead.as_int(GdhDeclared.elements_of(declared)[0], TYPE_NIL)
584
+ if typing.is_empty() or key_type == TYPE_OBJECT:
585
+ return [true, entries]
586
+ var base: StringName = typing[0]
587
+ var script: Script = typing[1]
588
+ return [true, Dictionary(entries, key_type, &"", null, TYPE_OBJECT, base, script)]
589
+
590
+
591
+ ## [param given], an object for a typed list or map, as [ok, object or why not]: a resource's
592
+ ## res:// path is the resource, and for nodes a path from [param target] is the node there.
593
+ ## [param holding] says what the list or map holds, and [param place] is [where the value sits in
594
+ ## what was given, what it is called in the property].
595
+ func _object_item(target: Object, given: Variant, kind: Array, holding: String, place: Array) -> Array:
596
+ var item: Variant = given
597
+ if typeof(item) == TYPE_STRING:
598
+ var found: Array = (
599
+ _node_reference(target, str(place[1]), item)
600
+ if kind[1]
601
+ else _resource_at(str(item), str(place[1]))
602
+ )
603
+ if not found[0]:
604
+ return found
605
+ item = found[1]
606
+ var object: Object = item if item is Object else null
607
+ if item == null or (object != null and GdhDeclared.is_a(object, str(kind[0]))):
608
+ return [true, item]
609
+ var named: String = object.get_class() if object != null else shown(item)
610
+ return [false, "%s, and %s is %s." % [holding, place[0], named]]
554
611
 
555
612
 
556
613
  ## Whether [param value] holds a number with a fractional part where an integer is wanted, in a
@@ -587,7 +644,7 @@ func _fraction_refusal(target: Object, property: String, raw: Variant) -> String
587
644
  "%s.%s holds whole numbers, and %s would be cut to fit. Setting it would save a different "
588
645
  + "value than the one asked for."
589
646
  )
590
- % [class_called(target), property, shown(raw)]
647
+ % [GdhPropertyPaths.class_called(target), property, shown(raw)]
591
648
  )
592
649
 
593
650
 
@@ -612,16 +669,15 @@ func colour_from(raw: Variant, named: String) -> Array:
612
669
  ]
613
670
 
614
671
 
615
- ## What [param target] is called in a refusal: the class_name its script declares, or else its
616
- ## engine class. A resource made as a project class is a Resource to the engine, and "Resource.kind"
617
- ## named a property no Resource has.
618
- static func class_called(target: Object) -> String:
619
- var attached: Variant = target.get_script()
620
- if attached is Script:
621
- var script: Script = attached
622
- if not str(script.get_global_name()).is_empty():
623
- return str(script.get_global_name())
624
- return target.get_class()
672
+ ## The refusal for [param property], which [param target] does not declare, naming the declared
673
+ ## ones nearest it: "StyleBoxFlat has no property shadow_sizee" left the caller nothing to send.
674
+ static func no_property(target: Object, property: String) -> String:
675
+ return (
676
+ GdhRead.near_property(
677
+ "%s has no property %s" % [GdhPropertyPaths.class_called(target), property], target, property
678
+ )
679
+ + "."
680
+ )
625
681
 
626
682
 
627
683
  ## The resource [param path] names, as [found, resource or why not].
@@ -669,15 +725,10 @@ func _resource_at(path: String, property: String) -> Array:
669
725
  ## its name, so without this a typo or a heading saved a file that had not changed and reported it
670
726
  ## as one that had.
671
727
  func declaration(target: Object, property: String) -> Dictionary:
672
- for declared: Dictionary in target.get_property_list():
673
- if str(declared.get("name", "")) != property:
674
- continue
675
- if GdhRead.as_int(declared.get("usage", 0)) & HEADINGS:
676
- continue
677
- return declared
678
- if target is Theme:
728
+ var declared: Dictionary = GdhPropertyPaths.declared_on(target, property)
729
+ if declared.is_empty() and target is Theme:
679
730
  return _theme_item(property)
680
- return {}
731
+ return declared
681
732
 
682
733
 
683
734
  ## The entry a Theme would declare for [param property] once it held that item, or {} when the name
@@ -696,107 +747,6 @@ static func _theme_item(property: String) -> Dictionary:
696
747
  return made
697
748
 
698
749
 
699
- ## Whether [param held] is [param given], as a property read back after a set answers it.
700
- ##
701
- ## Close enough rather than equal for anything made of floats: a Vector2 property stores 32-bit
702
- ## components, so 0.1 comes back as 0.100000001. A packed array read back against the list it was
703
- ## given compares element by element, since `==` between the two is an error rather than false.
704
- static func same(held: Variant, given: Variant) -> bool:
705
- var held_type: int = typeof(held)
706
- var given_type: int = typeof(given)
707
- # An empty object property reads back as an Object that is null, not as nil, and the two are
708
- # the same nothing.
709
- var held_none: bool = held_type == TYPE_NIL or (held_type == TYPE_OBJECT and held == null)
710
- var given_none: bool = given_type == TYPE_NIL or (given_type == TYPE_OBJECT and given == null)
711
- if held_none or given_none:
712
- return held_none == given_none
713
- if held_type == TYPE_OBJECT or given_type == TYPE_OBJECT:
714
- return _same_object(held, given)
715
- if GdhSerialisation.NUMBERS.has(held_type) and GdhSerialisation.NUMBERS.has(given_type):
716
- if held_type == TYPE_BOOL or given_type == TYPE_BOOL:
717
- return held_type == given_type and held == given
718
- var held_number: float = GdhRead.as_float(held)
719
- var given_number: float = GdhRead.as_float(given)
720
- return (is_nan(held_number) and is_nan(given_number)) or is_equal_approx(held_number, given_number)
721
- if GdhSerialisation.TEXTS.has(held_type) and GdhSerialisation.TEXTS.has(given_type):
722
- return str(held) == str(given)
723
- if _listed(held_type) and _listed(given_type):
724
- var held_items: Array = type_convert(held, TYPE_ARRAY)
725
- var given_items: Array = type_convert(given, TYPE_ARRAY)
726
- if held_items.size() != given_items.size():
727
- return false
728
- for index: int in held_items.size():
729
- if not same(held_items[index], given_items[index]):
730
- return false
731
- return true
732
- if held_type == TYPE_DICTIONARY and given_type == TYPE_DICTIONARY:
733
- var held_entries: Dictionary = held
734
- var given_entries: Dictionary = given
735
- return _same_entries(held_entries, given_entries)
736
- if held_type != given_type:
737
- return false
738
- return _same_shape(held, given)
739
-
740
-
741
- static func _same_object(held: Variant, given: Variant) -> bool:
742
- if typeof(held) != TYPE_OBJECT or typeof(given) != TYPE_OBJECT:
743
- return false
744
- if is_same(held, given):
745
- return true
746
- if not held is Resource or not given is Resource:
747
- return false
748
- var held_resource: Resource = held
749
- var given_resource: Resource = given
750
- if not held_resource.resource_path.is_empty():
751
- return held_resource.resource_path == given_resource.resource_path
752
- # A resource local to the scene is duplicated for every instance, so the copy read back is
753
- # another object of the same class.
754
- return held_resource.resource_local_to_scene and held_resource.get_class() == given_resource.get_class()
755
-
756
-
757
- static func _same_entries(held: Dictionary, given: Dictionary) -> bool:
758
- if held.size() != given.size():
759
- return false
760
- for key: Variant in given:
761
- var found: bool = false
762
- for held_key: Variant in held:
763
- if same(held_key, key):
764
- found = same(held[held_key], given[key])
765
- break
766
- if not found:
767
- return false
768
- return true
769
-
770
-
771
- ## Two values of one type made of floats, compared component by component through the type's own
772
- ## text form, which lists every component: each type has its own is_equal_approx, and reaching it
773
- ## through a Variant is a call the typed gate refuses.
774
- static func _same_shape(held: Variant, given: Variant) -> bool:
775
- if typeof(held) not in FLOAT_SHAPES:
776
- return held == given
777
- var held_parts: Array[float] = _components(held)
778
- var given_parts: Array[float] = _components(given)
779
- if held_parts.size() != given_parts.size():
780
- return false
781
- for index: int in held_parts.size():
782
- if not same(held_parts[index], given_parts[index]):
783
- return false
784
- return true
785
-
786
-
787
- static func _components(value: Variant) -> Array[float]:
788
- var parts: Array[float] = []
789
- var text: String = var_to_str(value)
790
- var inner: String = text.substr(text.find("(") + 1).trim_suffix(")")
791
- for part: String in inner.split(",", false):
792
- parts.append(part.strip_edges().to_float())
793
- return parts
794
-
795
-
796
- static func _listed(type: int) -> bool:
797
- return type == TYPE_ARRAY or PACKED_ELEMENTS.has(type)
798
-
799
-
800
750
  ## [param value] as a caller would write it, for a refusal to quote.
801
751
  func shown(value: Variant) -> String:
802
752
  return JSON.stringify(_values.serialize_value(value), "", false)
@@ -833,18 +783,22 @@ func _parse_resource_tag(value: Dictionary) -> Array:
833
783
  _refused = str(found[1])
834
784
  return [true, null]
835
785
  return [true, found[1]]
836
- # What a read answers for a resource built into the scene it was read from: a class and no
786
+ # What a read answers where a resource built into a scene recurs inside itself: a class and no
837
787
  # path. Taken as nothing, it cleared the property it was written back to.
838
788
  if value.has("class"):
839
789
  _refused = (
840
790
  (
841
- "The %s given has no path: it was read from a resource built into a scene, which cannot "
842
- + 'be referred to from elsewhere. Build a new one with "_type" set to its class and its '
843
- + "properties beside it."
791
+ "The %s given has no path: a read names a resource built into a scene by its class "
792
+ + "alone where it recurs inside itself, and nothing elsewhere can refer to it. Build "
793
+ + 'a new one with "_type" set to its class and its properties beside it.'
844
794
  )
845
795
  % str(value["class"])
846
796
  )
847
797
  return [true, null]
798
+ # A plain Resource with a script or values of its own, as a read answers one built into a scene,
799
+ # is one to build; taken as nothing, writing such a read back cleared the property.
800
+ if value.size() > 1:
801
+ return _parse_new_resource("Resource", value)
848
802
  return [true, null]
849
803
 
850
804
 
@@ -852,15 +806,21 @@ func _parse_resource_tag(value: Dictionary) -> Array:
852
806
  ## NavigationRegion2D can arrive with its NavigationPolygon and an AnimationTree with its root
853
807
  ## state machine in the same add as any other property.
854
808
  func _parse_new_resource(type_tag: String, value: Dictionary) -> Array:
809
+ var built: Resource = null
855
810
  if (
856
- type_tag.is_empty()
857
- or not ClassDB.class_exists(type_tag)
858
- or not ClassDB.is_parent_class(type_tag, "Resource")
859
- or not ClassDB.can_instantiate(type_tag)
811
+ ClassDB.class_exists(type_tag)
812
+ and ClassDB.is_parent_class(type_tag, "Resource")
813
+ and ClassDB.can_instantiate(type_tag)
860
814
  ):
815
+ built = ClassDB.instantiate(type_tag)
816
+ elif not type_tag.is_empty() and not ClassDB.class_exists(type_tag):
817
+ # A script's class_name, as a read answers it under scriptClass.
818
+ var script: Script = GdhDeclared.script_class(type_tag)
819
+ if script != null and ClassDB.is_parent_class(script.get_instance_base_type(), "Resource"):
820
+ built = ClassDB.instantiate(script.get_instance_base_type())
821
+ built.set_script(script)
822
+ if built == null:
861
823
  return [false, null]
862
-
863
- var built: Resource = ClassDB.instantiate(type_tag)
864
824
  var wanted: Dictionary = {}
865
825
  for key: Variant in value:
866
826
  var property: String = str(key)
@@ -870,6 +830,7 @@ func _parse_new_resource(type_tag: String, value: Dictionary) -> Array:
870
830
  if not refused.is_empty():
871
831
  _refused = refused
872
832
  return [true, null]
833
+ GdhDeclared.unmade(built, wanted)
873
834
  return [true, built]
874
835
 
875
836
 
@@ -189,6 +189,21 @@ static func offered(said: String, word: String, names: Array) -> String:
189
189
  return "%s; the nearest it has %s %s" % [said, "is" if close.size() == 1 else "are", ", ".join(close)]
190
190
 
191
191
 
192
+ ## [param said], the refusal for [param property], which [param target] does not declare, with the
193
+ ## names it declares nearest it when any are, as a misspelt one is usually one of them. Unlike
194
+ ## [method offered], nothing is listed when none is near, since an object declares a hundred.
195
+ static func near_property(said: String, target: Object, property: String) -> String:
196
+ var headings: int = PROPERTY_USAGE_CATEGORY | PROPERTY_USAGE_GROUP | PROPERTY_USAGE_SUBGROUP
197
+ var names: Array[String] = []
198
+ for declared: Dictionary in target.get_property_list():
199
+ if not as_int(declared.get("usage", 0)) & headings:
200
+ names.append(str(declared.get("name", "")))
201
+ var close: PackedStringArray = nearest(property, PackedStringArray(names))
202
+ if close.is_empty():
203
+ return said
204
+ return "%s; the nearest it has %s %s" % [said, "is" if close.size() == 1 else "are", ", ".join(close)]
205
+
206
+
192
207
  ## The signals [param object] has that a caller can connect, its own class's first: the engine
193
208
  ## lists them from Object down, which puts a Button's pressed after thirty a Control has, and names
194
209
  ## starting with an underscore are the engine's own.