gdharness 1.3.8 → 1.3.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/build/cli.js +4668 -3229
  2. package/build/godot/addons/gdharness_editor/animation_reads.gd +115 -4
  3. package/build/godot/addons/gdharness_editor/declared_values.gd +272 -0
  4. package/build/godot/addons/gdharness_editor/file_reads.gd +115 -18
  5. package/build/godot/addons/gdharness_editor/headless_check.gd +49 -2
  6. package/build/godot/addons/gdharness_editor/property_paths.gd +89 -16
  7. package/build/godot/addons/gdharness_editor/property_values.gd +180 -121
  8. package/build/godot/addons/gdharness_editor/reading.gd +112 -6
  9. package/build/godot/addons/gdharness_editor/scene_file.gd +117 -174
  10. package/build/godot/addons/gdharness_editor/scene_uids.gd +107 -0
  11. package/build/godot/addons/gdharness_editor/scene_uids.gd.uid +1 -0
  12. package/build/godot/addons/gdharness_editor/serialisation.gd +19 -29
  13. package/build/godot/addons/gdharness_editor/setting_reads.gd +121 -0
  14. package/build/godot/addons/gdharness_editor/setting_reads.gd.uid +1 -0
  15. package/build/godot/addons/gdharness_editor/tool_executor.gd +39 -6
  16. package/build/godot/addons/gdharness_editor/tools/animation_states.gd +263 -0
  17. package/build/godot/addons/gdharness_editor/tools/animation_states.gd.uid +1 -0
  18. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +49 -414
  19. package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +464 -0
  20. package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd.uid +1 -0
  21. package/build/godot/addons/gdharness_editor/tools/class_tools.gd +37 -0
  22. package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd +221 -0
  23. package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd.uid +1 -0
  24. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +162 -483
  25. package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +16 -0
  26. package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +8 -4
  27. package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +2 -1
  28. package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +10 -0
  29. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +151 -8
  30. package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +7 -30
  31. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +190 -38
  32. package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd +485 -0
  33. package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd.uid +1 -0
  34. package/build/godot/addons/gdharness_editor/value_shapes.gd +128 -1
  35. package/build/godot/addons/gdharness_runtime/call_errors.gd +8 -2
  36. package/build/godot/addons/gdharness_runtime/declared_values.gd +272 -0
  37. package/build/godot/addons/gdharness_runtime/reading.gd +112 -6
  38. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +35 -3
  39. package/build/godot/addons/gdharness_runtime/runtime_capture.gd +5 -0
  40. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +188 -30
  41. package/build/godot/addons/gdharness_runtime/runtime_input.gd +107 -104
  42. package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +17 -0
  43. package/build/godot/addons/gdharness_runtime/runtime_pad.gd +13 -4
  44. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +320 -27
  45. package/build/godot/addons/gdharness_runtime/runtime_queries.gd +165 -20
  46. package/build/godot/addons/gdharness_runtime/runtime_screen.gd +49 -3
  47. package/build/godot/addons/gdharness_runtime/runtime_targets.gd +31 -2
  48. package/build/godot/addons/gdharness_runtime/runtime_values.gd +250 -10
  49. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +279 -11
  50. package/build/godot/addons/gdharness_runtime/runtime_words.gd +27 -0
  51. package/build/godot/addons/gdharness_runtime/serialisation.gd +19 -29
  52. package/build/godot/addons/gdharness_runtime/value_shapes.gd +128 -1
  53. package/build/godot/operations/animation_reads.gd +115 -4
  54. package/build/godot/operations/audio_buses.gd +93 -7
  55. package/build/godot/operations/classdb_queries.gd +119 -7
  56. package/build/godot/operations/declared_values.gd +272 -0
  57. package/build/godot/operations/dependencies.gd +154 -41
  58. package/build/godot/operations/editor_tools.gd +29 -13
  59. package/build/godot/operations/export_presets.gd +602 -4
  60. package/build/godot/operations/file_reads.gd +115 -18
  61. package/build/godot/operations/gdscript_analysis.gd +14 -0
  62. package/build/godot/operations/gdscript_authoring.gd +12 -2
  63. package/build/godot/operations/godot_operations.gd +18 -2
  64. package/build/godot/operations/import_pipeline.gd +20 -221
  65. package/build/godot/operations/input_actions.gd +127 -51
  66. package/build/godot/operations/project_config.gd +113 -68
  67. package/build/godot/operations/project_diagnostics.gd +4 -0
  68. package/build/godot/operations/project_validation.gd +266 -0
  69. package/build/godot/operations/property_paths.gd +89 -16
  70. package/build/godot/operations/property_values.gd +180 -121
  71. package/build/godot/operations/reading.gd +112 -6
  72. package/build/godot/operations/scene_connections.gd +222 -0
  73. package/build/godot/operations/serialisation.gd +19 -29
  74. package/build/godot/operations/setting_reads.gd +121 -0
  75. package/build/godot/operations/value_shapes.gd +128 -1
  76. package/build/godot/tests/listed.gd +20 -13
  77. package/build/keeper.js +1402 -707
  78. package/package.json +1 -1
@@ -52,7 +52,108 @@ const MAP_CALLS: Array[String] = [
52
52
  ]
53
53
  ## The calls a path makes on a packed list, which has fewer than a list: measured on 4.7.2, it has no
54
54
  ## front(), back(), max(), min() or hash().
55
- const PACKED_CALLS: Array[String] = ["size", "is_empty", "duplicate", "to_byte_array"]
55
+ const PACKED_CALLS: Array[String] = ["size", "is_empty", "duplicate"]
56
+ ## What a packed list of bytes answers beside those, read as another list or as text. It has no
57
+ ## to_byte_array(), which every other packed list has.
58
+ const BYTE_CALLS: Array[String] = [
59
+ "to_int32_array",
60
+ "to_int64_array",
61
+ "to_float32_array",
62
+ "to_float64_array",
63
+ "get_string_from_utf8",
64
+ "get_string_from_utf32",
65
+ "hex_encode",
66
+ ]
67
+ ## The methods a list, a map and a packed list have that change what they are called on, as the
68
+ ## engine's API marks them, and those that read it given arguments. A path makes neither, and a call
69
+ ## refused for one is pointed at a write or a whole read instead of at an element, since a method of
70
+ ## the list itself was the aim.
71
+ const LIST_CHANGES: Array[String] = [
72
+ "clear",
73
+ "assign",
74
+ "set",
75
+ "push_back",
76
+ "push_front",
77
+ "append",
78
+ "append_array",
79
+ "resize",
80
+ "insert",
81
+ "remove_at",
82
+ "fill",
83
+ "erase",
84
+ "pop_back",
85
+ "pop_front",
86
+ "pop_at",
87
+ "sort",
88
+ "sort_custom",
89
+ "shuffle",
90
+ "reverse",
91
+ "make_read_only",
92
+ ]
93
+ const LIST_READS: Array[String] = [
94
+ "get",
95
+ "find",
96
+ "find_custom",
97
+ "rfind",
98
+ "rfind_custom",
99
+ "count",
100
+ "has",
101
+ "bsearch",
102
+ "bsearch_custom",
103
+ "slice",
104
+ "filter",
105
+ "map",
106
+ "reduce",
107
+ "any",
108
+ "all",
109
+ "is_same_typed",
110
+ ]
111
+ const MAP_CHANGES: Array[String] = [
112
+ "clear", "assign", "sort", "merge", "erase", "get_or_add", "set", "make_read_only"
113
+ ]
114
+ const MAP_READS: Array[String] = [
115
+ "merged",
116
+ "has",
117
+ "has_all",
118
+ "find_key",
119
+ "get",
120
+ "is_same_typed",
121
+ "is_same_typed_key",
122
+ "is_same_typed_value",
123
+ "recursive_equal",
124
+ ]
125
+ const PACKED_CHANGES: Array[String] = [
126
+ "set",
127
+ "push_back",
128
+ "append",
129
+ "append_array",
130
+ "remove_at",
131
+ "insert",
132
+ "fill",
133
+ "resize",
134
+ "clear",
135
+ "reverse",
136
+ "sort",
137
+ "erase",
138
+ ]
139
+ const PACKED_READS: Array[String] = ["get", "has", "slice", "bsearch", "find", "rfind", "count"]
140
+ ## The calls a path makes on a string. A word that looks right and does not match is told apart by
141
+ ## its code points, "label:text:to_utf32_buffer():to_int32_array()", which show a non-breaking space
142
+ ## as 160 where the answer's JSON shows a space.
143
+ const STRING_CALLS: Array[String] = [
144
+ "length",
145
+ "is_empty",
146
+ "strip_edges",
147
+ "to_lower",
148
+ "to_upper",
149
+ "c_escape",
150
+ "json_escape",
151
+ "is_valid_int",
152
+ "to_int",
153
+ "to_float",
154
+ "to_utf8_buffer",
155
+ "to_utf32_buffer",
156
+ ]
56
157
  ## The packed lists, which a game keeps names and points in as often as it keeps them in an Array.
57
158
  const PACKED: Array[int] = [
58
159
  TYPE_PACKED_BYTE_ARRAY,
@@ -66,6 +167,108 @@ const PACKED: Array[int] = [
66
167
  TYPE_PACKED_COLOR_ARRAY,
67
168
  TYPE_PACKED_VECTOR4_ARRAY,
68
169
  ]
170
+ ## The calls a path makes on a vector, a colour, a rectangle or a transform, by its type: those that
171
+ ## take no arguments, as the engine has them on 4.7.2. Named for the reason the list calls are, and
172
+ ## none can change what it is called on, since such a value is a copy wherever it is read from. A
173
+ ## wait for something to come to rest reads "linear_velocity:length()".
174
+ const VALUE_CALLS: Dictionary = {
175
+ TYPE_VECTOR2:
176
+ [
177
+ "length",
178
+ "length_squared",
179
+ "normalized",
180
+ "abs",
181
+ "sign",
182
+ "floor",
183
+ "ceil",
184
+ "round",
185
+ "angle",
186
+ "aspect",
187
+ "is_normalized",
188
+ "is_zero_approx",
189
+ "is_finite",
190
+ "orthogonal",
191
+ "max_axis_index",
192
+ "min_axis_index",
193
+ ],
194
+ TYPE_VECTOR2I: ["length", "length_squared", "abs", "sign", "aspect", "max_axis_index", "min_axis_index"],
195
+ TYPE_VECTOR3:
196
+ [
197
+ "length",
198
+ "length_squared",
199
+ "normalized",
200
+ "abs",
201
+ "sign",
202
+ "floor",
203
+ "ceil",
204
+ "round",
205
+ "is_normalized",
206
+ "is_zero_approx",
207
+ "is_finite",
208
+ "max_axis_index",
209
+ "min_axis_index",
210
+ "inverse",
211
+ ],
212
+ TYPE_VECTOR3I: ["length", "length_squared", "abs", "sign", "max_axis_index", "min_axis_index"],
213
+ TYPE_VECTOR4:
214
+ [
215
+ "length",
216
+ "length_squared",
217
+ "normalized",
218
+ "abs",
219
+ "sign",
220
+ "floor",
221
+ "ceil",
222
+ "round",
223
+ "is_normalized",
224
+ "is_zero_approx",
225
+ "is_finite",
226
+ "max_axis_index",
227
+ "min_axis_index",
228
+ "inverse",
229
+ ],
230
+ TYPE_VECTOR4I: ["length", "length_squared", "abs", "sign", "max_axis_index", "min_axis_index"],
231
+ TYPE_COLOR: ["get_luminance", "inverted", "to_html"],
232
+ TYPE_RECT2: ["abs", "is_finite", "get_area", "get_center", "has_area"],
233
+ TYPE_RECT2I: ["abs", "get_area", "get_center", "has_area"],
234
+ TYPE_TRANSFORM2D:
235
+ [
236
+ "is_finite",
237
+ "get_origin",
238
+ "get_rotation",
239
+ "get_scale",
240
+ "get_skew",
241
+ "inverse",
242
+ "determinant",
243
+ "orthonormalized",
244
+ ],
245
+ TYPE_TRANSFORM3D: ["is_finite", "inverse", "orthonormalized"],
246
+ TYPE_BASIS:
247
+ [
248
+ "is_finite",
249
+ "get_scale",
250
+ "inverse",
251
+ "determinant",
252
+ "orthonormalized",
253
+ "get_euler",
254
+ "get_rotation_quaternion",
255
+ ],
256
+ TYPE_QUATERNION:
257
+ [
258
+ "length",
259
+ "length_squared",
260
+ "normalized",
261
+ "is_normalized",
262
+ "is_finite",
263
+ "inverse",
264
+ "get_euler",
265
+ "get_angle",
266
+ "get_axis",
267
+ ],
268
+ TYPE_PLANE: ["normalized", "is_finite", "get_center"],
269
+ TYPE_AABB: ["abs", "is_finite", "get_center", "get_volume", "get_longest_axis", "get_shortest_axis"],
270
+ TYPE_PROJECTION: ["inverse", "determinant"],
271
+ }
69
272
  ## The calls an empty list has nothing to answer, which the engine reports as an error when asked.
70
273
  const NEEDS_AN_ELEMENT: Array[String] = ["front", "back", "pick_random"]
71
274
 
@@ -97,7 +300,8 @@ const READS_ONLY: String = ", and a path, which a wait reads every frame, only c
97
300
  ##
98
301
  ## A step into a vector, a colour, a rectangle or a transform reads one of its components,
99
302
  ## "size:x" or "global_transform:origin:y", by the names GDScript gives them; see
100
- ## [method GdhSerialisation.components_of].
303
+ ## [method GdhSerialisation.components_of]. It answers the calls in [constant VALUE_CALLS] too,
304
+ ## "linear_velocity:length()".
101
305
  ##
102
306
  ## Answers with a `message` instead when a step along the way is not there or holds something that
103
307
  ## is not an object, naming the step rather than the whole path: "/root/Main:_game has no property
@@ -180,7 +384,53 @@ static func object_at(root: Node, node: Node, node_path: String, given: Variant)
180
384
  ## components can be walked into, and a number or a string is where a path ends whether the caller
181
385
  ## meant it to or not.
182
386
  static func can_hold(value: Variant) -> bool:
183
- return value is Object or value is Array or value is Dictionary or packed(value) or composite(value)
387
+ return (
388
+ value is Object
389
+ or value is Array
390
+ or value is Dictionary
391
+ or packed(value)
392
+ or composite(value)
393
+ or textual(value)
394
+ )
395
+
396
+
397
+ ## Whether [param value] is a string, which a path makes calls on and does not step into.
398
+ static func textual(value: Variant) -> bool:
399
+ return typeof(value) in [TYPE_STRING, TYPE_STRING_NAME]
400
+
401
+
402
+ ## The calls a path makes on [param value], a packed list.
403
+ static func packed_calls(value: Variant) -> Array[String]:
404
+ var calls: Array[String] = PACKED_CALLS.duplicate()
405
+ if typeof(value) == TYPE_PACKED_BYTE_ARRAY:
406
+ calls.append_array(BYTE_CALLS)
407
+ else:
408
+ calls.append("to_byte_array")
409
+ return calls
410
+
411
+
412
+ ## "changes" when [param method] is one of [param holder]'s own that changes it, "reads" when it is
413
+ ## one that reads it given arguments, and "" otherwise. A packed list of bytes has more of both,
414
+ ## encode_ and bswap changing it and decode_ reading it, which are told by their names.
415
+ static func container_method(holder: Variant, method: String) -> String:
416
+ var changes: Array[String] = MAP_CHANGES
417
+ var reads: Array[String] = MAP_READS
418
+ if holder is Array:
419
+ changes = LIST_CHANGES
420
+ reads = LIST_READS
421
+ elif packed(holder):
422
+ changes = PACKED_CHANGES
423
+ reads = PACKED_READS
424
+ if typeof(holder) == TYPE_PACKED_BYTE_ARRAY:
425
+ if method.begins_with("encode_") or method.begins_with("bswap"):
426
+ return "changes"
427
+ if method.begins_with("decode_") or method == "has_encoded_var":
428
+ return "reads"
429
+ if method in changes:
430
+ return "changes"
431
+ if method in reads:
432
+ return "reads"
433
+ return ""
184
434
 
185
435
 
186
436
  ## Whether [param value] is read by its components, a vector or a colour or a transform, and is a
@@ -189,6 +439,11 @@ static func composite(value: Variant) -> bool:
189
439
  return not GdhSerialisation.components_of(value).is_empty()
190
440
 
191
441
 
442
+ ## The calls a path makes on [param value], a vector, a colour, a rectangle or a transform.
443
+ static func value_calls(value: Variant) -> Array:
444
+ return VALUE_CALLS.get(typeof(value), [])
445
+
446
+
192
447
  ## Whether [param value] is a packed list. Stepped into the way a list is, by index, and a write into
193
448
  ## one reaches the game's own copy, since packed lists share their storage until one is duplicated.
194
449
  static func packed(value: Variant) -> bool:
@@ -228,15 +483,19 @@ static func can_read(holder: Variant, named: String) -> bool:
228
483
  return _index_in(named, items.size()) != -1
229
484
  if packed(holder):
230
485
  if not method.is_empty():
231
- return args.is_empty() and method in PACKED_CALLS
486
+ return args.is_empty() and method in packed_calls(holder)
232
487
  return _index_in(named, len(holder)) != -1
488
+ if textual(holder):
489
+ return args.is_empty() and method in STRING_CALLS
233
490
  if holder is Dictionary:
234
491
  var map: Dictionary = holder
235
492
  if not method.is_empty():
236
493
  return args.is_empty() and method in MAP_CALLS
237
494
  return not _key_in(map, named).is_empty()
238
495
  if composite(holder):
239
- return method.is_empty() and named in GdhSerialisation.components_of(holder)
496
+ if not method.is_empty():
497
+ return args.is_empty() and method in value_calls(holder)
498
+ return named in GdhSerialisation.components_of(holder)
240
499
  if holder is Object:
241
500
  var object: Object = holder
242
501
  if not method.is_empty():
@@ -386,7 +645,7 @@ static func _not_literals(named: String) -> String:
386
645
  ## [method Object.has_method], because a method given too few or too many raises inside the game
387
646
  ## rather than answering.
388
647
  static func _arity(object: Object, method: String) -> Dictionary:
389
- for entry: Dictionary in object.get_method_list():
648
+ for entry: Dictionary in GdhValues.methods_of(object):
390
649
  if str(entry.get("name", "")) != method:
391
650
  continue
392
651
  var params: Array = entry.get("args", [])
@@ -405,10 +664,13 @@ static func _arity(object: Object, method: String) -> Dictionary:
405
664
  ##
406
665
  ## A path is read, and a wait reads it every frame, so a call in one has to be a read. The engine
407
666
  ## marks its own reading methods const and `queue_free()`, `free()` and `set_name()` not, measured on
408
- ## 4.7.2. A game's methods carry no such mark, so one declared `-> void` is taken at its word, as
409
- ## doing something and answering nothing, and one left untyped is trusted. Without this a find
410
- ## with property "queue_free()" freed every node it matched, and a wait on "advance_day()" called
411
- ## it once a frame and then reported the state it had driven the game to.
667
+ ## 4.7.2, though it leaves some reads unmarked as well, `Engine.get_frames_drawn()` and
668
+ ## `get_time_scale()` among them; so the refusal says the mark is missing, not that the method
669
+ ## changes anything, and points at runtime_invoke call, which calls it. A game's methods carry no
670
+ ## such mark, so one declared `-> void` is taken at its word, as doing something and answering
671
+ ## nothing, and one left untyped is trusted. Without this a find with property "queue_free()" freed
672
+ ## every node it matched, and a wait on "advance_day()" called it once a frame and then reported the
673
+ ## state it had driven the game to.
412
674
  ##
413
675
  ## Given [param given] arguments, a call is the engine's and marked as reading, or it is refused. A
414
676
  ## game's method taking arguments is trusted on nothing at all: one answering a value can as well
@@ -450,7 +712,10 @@ static func call_refused(object: Object, method: String, called_as: String, give
450
712
  )
451
713
  return ""
452
714
  if _native_flags(object, method) & METHOD_FLAG_CONST == 0:
453
- return "%s.%s changes what it is called on rather than reading it%s" % [called_as, method, READS_ONLY]
715
+ return (
716
+ "%s.%s is not one the engine marks as only reading%s; runtime_invoke call calls it"
717
+ % [called_as, method, READS_ONLY]
718
+ )
454
719
  return ""
455
720
 
456
721
 
@@ -459,15 +724,18 @@ static func call_refused(object: Object, method: String, called_as: String, give
459
724
  ## so they are offered only when one is near.
460
725
  static func with_methods_offered(said: String, object: Object, method: String) -> String:
461
726
  var own: Array[String] = []
462
- var attached: Variant = object.get_script()
727
+ var attached: Variant = object if object is Script else object.get_script()
463
728
  if attached is Script:
464
729
  var script: Script = attached
465
730
  for entry: Dictionary in script.get_script_method_list():
466
731
  var name: String = str(entry.get("name", ""))
467
- if not name.is_empty() and not own.has(name):
732
+ # A class called by its name runs only its static functions, so offering the rest leads to a refusal.
733
+ var flags: int = entry.get("flags", 0)
734
+ var callable: bool = not object is Script or flags & METHOD_FLAG_STATIC
735
+ if callable and not name.is_empty() and not own.has(name):
468
736
  own.append(name)
469
737
  var every: Array[String] = own.duplicate()
470
- for entry: Dictionary in object.get_method_list():
738
+ for entry: Dictionary in GdhValues.methods_of(object):
471
739
  var name: String = str(entry.get("name", ""))
472
740
  if not every.has(name):
473
741
  every.append(name)
@@ -477,7 +745,8 @@ static func with_methods_offered(said: String, object: Object, method: String) -
477
745
  if own.is_empty():
478
746
  return said
479
747
  own.sort()
480
- return "%s; its script declares %s" % [said, ", ".join(PackedStringArray(own))]
748
+ var offered: String = "its static functions are" if object is Script else "its script declares"
749
+ return "%s; %s %s" % [said, offered, ", ".join(PackedStringArray(own))]
481
750
 
482
751
 
483
752
  ## The entry a script attached to [param object] declares for [param method], or an empty one when
@@ -495,7 +764,7 @@ static func _script_method(object: Object, method: String) -> Dictionary:
495
764
 
496
765
  ## The flags the engine declares [param method] with on [param object].
497
766
  static func _native_flags(object: Object, method: String) -> int:
498
- for entry: Dictionary in object.get_method_list():
767
+ for entry: Dictionary in GdhValues.methods_of(object):
499
768
  if str(entry.get("name", "")) == method:
500
769
  var flags: int = entry.get("flags", 0)
501
770
  return flags
@@ -563,7 +832,10 @@ static func write_back(chain: Array[Dictionary], changed: Variant) -> void:
563
832
  ## What [param holder] holds under [param named], or answers to it when [param named] is a call.
564
833
  ## Only ever called once [method can_read] agrees.
565
834
  static func read_under(holder: Variant, named: String) -> Variant:
566
- if (holder is Array or holder is Dictionary or packed(holder)) and not method_of(named).is_empty():
835
+ if (
836
+ (holder is Array or holder is Dictionary or packed(holder) or composite(holder) or textual(holder))
837
+ and not method_of(named).is_empty()
838
+ ):
567
839
  return Callable.create(holder, method_of(named)).call()
568
840
  if holder is Array:
569
841
  var items: Array = holder
@@ -618,8 +890,17 @@ static func nothing_there(holder: Variant, named: String, called_as: String) ->
618
890
  return "%s is a list of %d, so there is no %s in it" % [called_as, items.size(), named]
619
891
  if packed(holder):
620
892
  if not container_call.is_empty():
621
- return container_calls_refused(called_as, "a packed list", container_call, given_count)
893
+ return container_calls_refused(
894
+ called_as, "a packed list", container_call, given_count, packed_calls(holder)
895
+ )
622
896
  return "%s is a packed list of %d, so there is no %s in it" % [called_as, len(holder), named]
897
+ if textual(holder):
898
+ if container_call.is_empty():
899
+ return (
900
+ "%s is a string, so a path makes only calls on it, which read it and take no arguments: %s"
901
+ % [called_as, "(), ".join(STRING_CALLS) + "()"]
902
+ )
903
+ return container_calls_refused(called_as, "a string", container_call, given_count, STRING_CALLS)
623
904
  if holder is Dictionary:
624
905
  if not container_call.is_empty():
625
906
  return container_calls_refused(called_as, "a map", container_call, given_count)
@@ -649,9 +930,21 @@ static func nothing_there(holder: Variant, named: String, called_as: String) ->
649
930
  var sort: String = ("an " if kind.left(1) in ["A", "E", "I", "O", "U"] else "a ") + kind
650
931
  var has: String = ", ".join(GdhSerialisation.components_of(holder))
651
932
  if not container_call.is_empty():
933
+ var calls: Array = value_calls(holder)
934
+ if given_count > 0 and container_call in calls:
935
+ return (
936
+ "%s is %s, and a path calls %s() on one with no arguments"
937
+ % [called_as, sort, container_call]
938
+ )
939
+ var spelled: Array[String] = []
940
+ for each: Variant in calls:
941
+ spelled.append(str(each) + "()")
652
942
  return (
653
- "%s is %s, which a path reads by its components, %s, and makes no call on"
654
- % [called_as, sort, has]
943
+ (
944
+ "%s is %s, and %s() is not one of the calls a path makes on one, which read it"
945
+ + " and take no arguments: %s; its components are %s"
946
+ )
947
+ % [called_as, sort, container_call, ", ".join(spelled), has]
655
948
  )
656
949
  return "%s is %s, so it has no %s; it has %s" % [called_as, sort, named, has]
657
950
  if not holder is Object:
@@ -670,17 +963,17 @@ static func nothing_there(holder: Variant, named: String, called_as: String) ->
670
963
  return GdhRead.near_property("%s has no property %s" % [called_as, named], object, named)
671
964
 
672
965
 
673
- ## Why [param method] is not a call a path makes on [param sort] ("a list", "a packed list" or "a
674
- ## map"), naming the calls it does make, or that it makes this one with no arguments when
675
- ## [param given] were.
966
+ ## Why [param method] is not a call a path makes on [param sort] ("a list" or "a map", or another
967
+ ## sort given its [param made]), naming the calls it does make, or that it makes this one with no
968
+ ## arguments when [param given] were.
676
969
  static func container_calls_refused(
677
- called_as: String, sort: String, method: String, given: int = 0
970
+ called_as: String, sort: String, method: String, given: int = 0, made: Array[String] = []
678
971
  ) -> String:
679
972
  var calls: Array[String] = MAP_CALLS
680
- if sort == "a list":
973
+ if not made.is_empty():
974
+ calls = made
975
+ elif sort == "a list":
681
976
  calls = LIST_CALLS
682
- elif sort == "a packed list":
683
- calls = PACKED_CALLS
684
977
  if given > 0 and method in calls:
685
978
  return "%s is %s, and a path calls %s() on one with no arguments" % [called_as, sort, method]
686
979
  var spelled: Array[String] = []