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
@@ -27,12 +27,40 @@ const LOOKS_APART: int = 9
27
27
  ## two looks and a wait for 32 never sees it.
28
28
  const COMPARES: Array[String] = ["==", "!=", ">=", "<=", ">", "<"]
29
29
 
30
- ## The most signal watches kept at once; arming one past this lets the oldest go.
30
+ ## The most signal watches kept at once; arming one past this lets the oldest go. Property watches
31
+ ## are counted apart, to the same number.
31
32
  const MOST_WATCHES: int = 16
32
33
 
34
+ ## The most changes a property watch keeps. Past it, each new one takes the last place, so what is
35
+ ## kept still ends on the latest value.
36
+ const MOST_CHANGES: int = 1000
37
+
38
+ ## The commands that can change the game, by the tool call that sends each, which a property watch
39
+ ## notes as they arrive.
40
+ const CALLS_NAMED: Dictionary[String, String] = {
41
+ "set_property": "runtime_invoke set",
42
+ "call_method": "runtime_invoke call",
43
+ "click": "runtime_input click",
44
+ "choose": "runtime_input choose",
45
+ "inject_action": "runtime_input action",
46
+ "inject_key": "runtime_input key",
47
+ "inject_text": "runtime_input text",
48
+ "inject_mouse_click": "runtime_input mouse_click",
49
+ "inject_mouse_motion": "runtime_input mouse_motion",
50
+ "inject_mouse_drag": "runtime_input mouse_drag",
51
+ "inject_joypad_button": "runtime_input joypad_button",
52
+ "inject_joypad_motion": "runtime_input joypad_motion",
53
+ }
54
+
55
+ ## What a call noted by a property watch carries of its parameters, when it was given them.
56
+ const CALL_FIELDS: Array[String] = [
57
+ "path", "method", "property", "action", "keycode", "button", "pressed", "says"
58
+ ]
59
+
33
60
  var _host: Node
34
61
  var _values: GdhValues
35
62
  var _watches: Dictionary[int, GdhWatch] = {}
63
+ var _traces: Dictionary[int, GdhTrace] = {}
36
64
  var _next_watch: int = 1
37
65
 
38
66
 
@@ -110,9 +138,10 @@ func wait_signal(params: Dictionary) -> Dictionary:
110
138
  }
111
139
  if not catcher.fired:
112
140
  answer["note"] = (
113
- "If a call meant to set the signal off was sent beside this wait, it may have run only after"
114
- + " the wait answered, as a client can run its calls in turn: arm runtime_wait watch first,"
115
- + " make the call, then wait with signal and that watch"
141
+ "A wait hears the signal only from when it begins, so one that fired before it is not"
142
+ + " counted. If a call meant to set the signal off was sent beside this wait, it may have run"
143
+ + " only after the wait answered, as a client can run its calls in turn: arm runtime_wait"
144
+ + " watch first, make the call, then wait with signal and that watch"
116
145
  )
117
146
  return answer
118
147
 
@@ -125,6 +154,18 @@ func wait_signal(params: Dictionary) -> Dictionary:
125
154
  func watch_signal(params: Dictionary) -> Dictionary:
126
155
  var node_path: String = str(params.get("path", ""))
127
156
  var signal_name: String = str(params.get("signal", ""))
157
+ var property: String = str(params.get("property", ""))
158
+ if not signal_name.is_empty() and not property.is_empty():
159
+ return {
160
+ "type": "error",
161
+ "message":
162
+ (
163
+ "A watch records a signal or a property, not both: this one has signal %s and property %s"
164
+ % [signal_name, property]
165
+ )
166
+ }
167
+ if not property.is_empty():
168
+ return _watch_property(node_path, property)
128
169
  if node_path.is_empty() or signal_name.is_empty():
129
170
  return {"type": "error", "message": "Node path and signal name required"}
130
171
  var standing: Dictionary = GdhValues.node_at(_host.get_tree().root, node_path)
@@ -149,6 +190,15 @@ func watch_signal(params: Dictionary) -> Dictionary:
149
190
  ## Answers at once with the first emission recorded since watch [param id] was armed, otherwise
150
191
  ## waits for the next until the time runs out. The watch goes either way.
151
192
  func _collect(id: int, timeout_ms: int) -> Dictionary:
193
+ if _traces.has(id):
194
+ return {
195
+ "type": "error",
196
+ "message":
197
+ (
198
+ "Watch %d records %s:%s, a property, and is collected with runtime_wait recorded"
199
+ % [id, _traces[id].node_path, _traces[id].property]
200
+ )
201
+ }
152
202
  if not _watches.has(id):
153
203
  return {
154
204
  "type": "error",
@@ -194,6 +244,153 @@ func _let_go(id: int) -> void:
194
244
  node.disconnect(watch.signal_name, callable)
195
245
 
196
246
 
247
+ ## Starts recording [param property] every frame and answers at once with the watch and what it
248
+ ## reads now.
249
+ ##
250
+ ## A tween or an animation is over before a client's next call reaches the game: a slide of 0.3
251
+ ## seconds had finished by the time a wait sent after the click began, so its middle could be read
252
+ ## only by slowing the game's clock. Armed before the click, the watch has every frame of it.
253
+ func _watch_property(node_path: String, property: String) -> Dictionary:
254
+ if node_path.is_empty():
255
+ return {"type": "error", "message": "Node path required"}
256
+ var standing: Dictionary = GdhValues.node_at(_host.get_tree().root, node_path)
257
+ if standing.has("message"):
258
+ return standing
259
+ var node: Node = standing["node"]
260
+ var watched: Dictionary = _watched(node, node_path, property)
261
+ if watched.has("message"):
262
+ return watched
263
+ var trace: GdhTrace = GdhTrace.new(node, node_path, property)
264
+ trace.see(0, 0, 0.0, watched["value"], _values)
265
+ while _traces.size() >= MOST_WATCHES:
266
+ var _gone: bool = _traces.erase(_traces.keys().min())
267
+ var id: int = _next_watch
268
+ _next_watch += 1
269
+ _traces[id] = trace
270
+ # Left to run across frames while the answer goes now. Deferred calls run once the frame's
271
+ # _process is done and before its tweens step, so the recording starts in the frame it was armed in.
272
+ _record.call_deferred(trace)
273
+ return {
274
+ "type": "watching",
275
+ "watch": id,
276
+ "path": node_path,
277
+ "property": property,
278
+ "value": _values.serialize(watched["value"]),
279
+ }
280
+
281
+
282
+ ## Reads the watched property at the start of every frame until the watch is collected, the node
283
+ ## goes or the ceiling a wait has is reached.
284
+ ##
285
+ ## Read where a frame begins, which is after the last one's tweens and _process ran: the game time
286
+ ## beside each reading is the time those had been given, which counts the frame the watch was armed
287
+ ## in, since commands run before that frame's tweens step.
288
+ func _record(trace: GdhTrace) -> void:
289
+ var started: int = Time.get_ticks_msec()
290
+ var given: float = _host.get_process_delta_time()
291
+ var game_time: float = 0.0
292
+ var frame: int = 0
293
+ while not trace.collected:
294
+ await _host.get_tree().process_frame
295
+ if trace.collected:
296
+ return
297
+ frame += 1
298
+ game_time += given
299
+ given = _host.get_process_delta_time()
300
+ var elapsed: int = Time.get_ticks_msec() - started
301
+ var held: Object = trace.node.get_ref()
302
+ var node: Node = held as Node
303
+ if node == null:
304
+ trace.ended = "%s was freed %d frames after the watch was armed" % [trace.node_path, frame]
305
+ return
306
+ var watched: Dictionary = _watched(node, trace.node_path, trace.property)
307
+ if watched.has("message"):
308
+ trace.ended = "%d frames after the watch was armed: %s" % [frame, watched["message"]]
309
+ return
310
+ trace.see(frame, elapsed, game_time, watched["value"], _values)
311
+ trace.frames = frame
312
+ trace.elapsed_ms = elapsed
313
+ if elapsed >= CEILING_MSEC:
314
+ trace.ended = (
315
+ "recording stopped %d ms after the watch was armed, the longest a wait lasts" % elapsed
316
+ )
317
+ return
318
+
319
+
320
+ ## Notes [param command] in every property watch still recording, beside the last reading taken
321
+ ## before it ran, so a recording shows where the call that started a change landed. A call two
322
+ ## round trips apart from another lands dozens of frames later, which only this tells apart from
323
+ ## the game's own delay.
324
+ func saw_call(command: String, params: Dictionary) -> void:
325
+ var named: String = CALLS_NAMED.get(command, "")
326
+ if named.is_empty():
327
+ return
328
+ for trace: GdhTrace in _traces.values():
329
+ if trace.collected or not trace.ended.is_empty():
330
+ continue
331
+ if trace.calls.size() >= MOST_CHANGES:
332
+ trace.calls_dropped += 1
333
+ continue
334
+ var noted: Dictionary = {"frame": trace.frames, "call": named}
335
+ for field: String in CALL_FIELDS:
336
+ if params.has(field):
337
+ noted[field] = params[field]
338
+ trace.calls.append(noted)
339
+
340
+
341
+ ## What a property watch recorded, each change with the frame, the time and the game time it was
342
+ ## read at. The watch goes with the answer.
343
+ func recorded(params: Dictionary) -> Dictionary:
344
+ var id: int = GdhRead.as_int(params.get("watch", 0), 0)
345
+ if _watches.has(id):
346
+ return {
347
+ "type": "error",
348
+ "message":
349
+ (
350
+ "Watch %d records the signal %s of %s, and is collected with runtime_wait signal"
351
+ % [id, _watches[id].signal_name, _watches[id].node_path]
352
+ )
353
+ }
354
+ if not _traces.has(id):
355
+ return {
356
+ "type": "error",
357
+ "message":
358
+ (
359
+ (
360
+ "No property watch %d is armed in this game: it was collected already, %d armed after"
361
+ + " it let it go, or the game started again since"
362
+ )
363
+ % [id, MOST_WATCHES]
364
+ ),
365
+ }
366
+ var trace: GdhTrace = _traces[id]
367
+ trace.collected = true
368
+ var _gone: bool = _traces.erase(id)
369
+ var answer: Dictionary = {
370
+ "type": "recorded",
371
+ "watch": id,
372
+ "path": trace.node_path,
373
+ "property": trace.property,
374
+ "changes": trace.changes,
375
+ "calls": trace.calls,
376
+ "frames": trace.frames,
377
+ "elapsed_ms": trace.elapsed_ms,
378
+ }
379
+ if trace.calls_dropped > 0:
380
+ answer["calls_dropped"] = trace.calls_dropped
381
+ if trace.dropped > 0:
382
+ answer["dropped"] = (
383
+ (
384
+ "%d changes are left out: past %d, each new one took the last place, so the list ends on"
385
+ + " the latest"
386
+ )
387
+ % [trace.dropped, MOST_CHANGES]
388
+ )
389
+ if not trace.ended.is_empty():
390
+ answer["ended"] = trace.ended
391
+ return answer
392
+
393
+
197
394
  ## Waits until a property reads as the value given, or until something under the path says what
198
395
  ## was given, or the time runs out, and answers with what it read last either way.
199
396
  func wait_until(params: Dictionary) -> Dictionary:
@@ -305,7 +502,7 @@ func wait_until(params: Dictionary) -> Dictionary:
305
502
  return watched
306
503
  current = watched["value"]
307
504
 
308
- return {
505
+ var answer: Dictionary = {
309
506
  "type": "condition",
310
507
  "path": node_path,
311
508
  "property": property,
@@ -315,6 +512,16 @@ func wait_until(params: Dictionary) -> Dictionary:
315
512
  "elapsed_ms": Time.get_ticks_msec() - started,
316
513
  "frames": frames,
317
514
  }
515
+ # Said rather than refused, since a game may store a number no value has, as set says it stores
516
+ # one: a wait for 7 on an enum of three ran its whole timeout and answered only that it was not met.
517
+ if watched["holder"] is Object:
518
+ var owner: Object = watched["holder"]
519
+ var shown: String = JSON.stringify(_values.serialize(wanted), "", false)
520
+ var declaration: Dictionary = watched["declaration"]
521
+ var outside: String = GdhDeclared.outside(owner, declaration, wanted, shown)
522
+ if not outside.is_empty():
523
+ answer["outside_declared"] = outside
524
+ return answer
318
525
 
319
526
 
320
527
  ## What a property wait is missing or was given wrongly, or "" when it can go ahead.
@@ -589,7 +796,7 @@ func _wait_until_said(node_path: String, said: String, timeout_ms: int, include_
589
796
  looks += 1
590
797
  looked_usec += Time.get_ticks_usec() - last
591
798
 
592
- return {
799
+ var answer: Dictionary = {
593
800
  "type": "condition",
594
801
  "path": node_path,
595
802
  "says": said,
@@ -600,12 +807,24 @@ func _wait_until_said(node_path: String, said: String, timeout_ms: int, include_
600
807
  "looks": looks,
601
808
  "looking_ms": looked_usec / 1000.0,
602
809
  }
810
+ # Words that are there and cannot be read look, from a wait that ran out, like words that never
811
+ # came.
812
+ if not found and not include_hidden and _anything_says(node_path, said, true):
813
+ answer["unseen_note"] = (
814
+ (
815
+ "Something under %s says it, but is hidden or drawn outside the viewport or a"
816
+ + " ScrollContainer, where the player cannot read it; includeHidden true counts it there too"
817
+ )
818
+ % node_path
819
+ )
820
+ return answer
603
821
 
604
822
 
605
- ## Whether anything under [param node_path] says [param said], the node itself included, and shown
606
- ## unless hidden ones are wanted too. Matched as a find and a click match `says`, a glob over the
607
- ## whole text when it has `*` or `?`: read as literal words, a glob a click had just pressed by was
608
- ## waited on for the whole timeout with its label on screen, and answered as words never arriving.
823
+ ## Whether anything under [param node_path] says [param said], the node itself included, and where
824
+ ## the player can read it unless hidden ones are wanted too. Matched as a find and a click match
825
+ ## `says`, a glob over the whole text when it has `*` or `?`: read as literal words, a glob a click
826
+ ## had just pressed by was waited on for the whole timeout with its label on screen, and answered as
827
+ ## words never arriving.
609
828
  func _anything_says(node_path: String, said: String, include_hidden: bool) -> bool:
610
829
  var root: Node = _host.get_tree().root.get_node_or_null(node_path)
611
830
  if root == null:
@@ -613,7 +832,7 @@ func _anything_says(node_path: String, said: String, include_hidden: bool) -> bo
613
832
  var pending: Array[Node] = [root]
614
833
  while not pending.is_empty():
615
834
  var node: Node = pending.pop_back()
616
- if GdhSays.says(node, said) and (include_hidden or GdhQueries.shown(node)):
835
+ if GdhSays.says(node, said) and (include_hidden or GdhQueries.readable(node)):
617
836
  return true
618
837
  pending.append_array(node.get_children(true))
619
838
  return false
@@ -637,6 +856,55 @@ class GdhSignalCatcher:
637
856
  times += 1
638
857
 
639
858
 
859
+ ## A property recorded from when it was armed until it is collected: the reading at arming, then
860
+ ## each frame's that differs from the one before.
861
+ class GdhTrace:
862
+ extends RefCounted
863
+ var node: WeakRef
864
+ var node_path: String
865
+ var property: String
866
+ var changes: Array[Dictionary] = []
867
+ var dropped: int = 0
868
+ var calls: Array[Dictionary] = []
869
+ var calls_dropped: int = 0
870
+ var frames: int = 0
871
+ var elapsed_ms: int = 0
872
+ var ended: String = ""
873
+ var collected: bool = false
874
+ var _last: Variant = null
875
+ var _seen: bool = false
876
+
877
+ func _init(watched: Node, path: String, named: String) -> void:
878
+ node = weakref(watched)
879
+ node_path = path
880
+ property = named
881
+
882
+ func see(frame: int, elapsed: int, game_time: float, value: Variant, values: GdhValues) -> void:
883
+ if _seen and typeof(value) == typeof(_last) and value == _last:
884
+ return
885
+ _seen = true
886
+ # A copy of a list or a map, which the game may change in place: held as it is, the reading
887
+ # kept is the game's own, and it reads the same as every later one.
888
+ _last = value
889
+ if value is Array:
890
+ var list: Array = value
891
+ _last = list.duplicate(true)
892
+ elif value is Dictionary:
893
+ var map: Dictionary = value
894
+ _last = map.duplicate(true)
895
+ var change: Dictionary = {
896
+ "frame": frame,
897
+ "elapsed_ms": elapsed,
898
+ "game_time": snappedf(game_time, 0.0001),
899
+ "value": values.serialize(value),
900
+ }
901
+ if changes.size() < MOST_CHANGES:
902
+ changes.append(change)
903
+ else:
904
+ changes[changes.size() - 1] = change
905
+ dropped += 1
906
+
907
+
640
908
  ## A signal recorded from when it was armed until it is collected.
641
909
  class GdhWatch:
642
910
  extends RefCounted
@@ -55,6 +55,33 @@ static func lines_said_by(node: Node) -> Array[String]:
55
55
  return lines
56
56
 
57
57
 
58
+ ## The node [param node] is drawn as a copy of, or null: the nearest node above it that says
59
+ ## anything, when it says the same words, followed up while that one is a copy in turn. A word drawn
60
+ ## with an outline or a glow is often a label holding copies of itself, offset or tinted, and a
61
+ ## player reads one sign where a find counted four.
62
+ static func copied_from(node: Node) -> Node:
63
+ var chain: Array[Node] = copied_through(node)
64
+ return null if chain.is_empty() else chain.back()
65
+
66
+
67
+ ## Every node above [param node] that it is drawn as a copy of, nearest first: see
68
+ ## [method copied_from], which is the last of them.
69
+ static func copied_through(node: Node) -> Array[Node]:
70
+ var chain: Array[Node] = []
71
+ var words: String = said_by(node)
72
+ if words.is_empty():
73
+ return chain
74
+ var above: Node = node.get_parent()
75
+ while above != null:
76
+ var said: String = said_by(above)
77
+ if not said.is_empty():
78
+ if said != words:
79
+ break
80
+ chain.append(above)
81
+ above = above.get_parent()
82
+ return chain
83
+
84
+
58
85
  ## Each item [param node] draws as one of a list, as {"says", "item"}: a tab, a menu title, a list
59
86
  ## entry or a menu item by its index, and a tree row by its TreeItem. Empty for a node drawing none.
60
87
  ## Held apart rather than joined, so a click on a tab's title presses that tab: the bar's middle is
@@ -142,28 +142,29 @@ func serialize_value(value: Variant) -> Variant:
142
142
  return value
143
143
  var carried: Variant = call(serialiser, value)
144
144
  # The fixed shapes carry their components as bare floats, so an infinite one in a Vector2 is
145
- # tagged here; containers reach their members through this function already.
146
- return carried if typeof(value) in [TYPE_ARRAY, TYPE_DICTIONARY] else _finite(carried)
145
+ # tagged here, and a 32-bit one shortened; containers reach their members through this already.
146
+ var type: int = typeof(value)
147
+ return carried if type in [TYPE_ARRAY, TYPE_DICTIONARY] else _finite(carried, GdhRead.holds_singles(type))
147
148
 
148
149
 
149
150
  # [param carried] with every non-finite float in it tagged. JSON has no spelling for infinity or
150
151
  # NaN: the engine writes 1e99999 and null, and the server, reading 1e99999 as Infinity, writes it
151
152
  # back out as null, so a property holding INF was answered as holding nothing.
152
- func _finite(carried: Variant) -> Variant:
153
+ func _finite(carried: Variant, single: bool = false) -> Variant:
153
154
  if carried is float:
154
155
  var number: float = carried
155
- return _serialize_float(number)
156
+ return _serialize_float(GdhRead.single(number) if single else number)
156
157
  if carried is Dictionary:
157
158
  var fields: Dictionary = carried
158
159
  var tagged: Dictionary = {}
159
160
  for key: Variant in fields:
160
- tagged[key] = _finite(fields[key])
161
+ tagged[key] = _finite(fields[key], single)
161
162
  return tagged
162
163
  if carried is Array:
163
164
  var items: Array = carried
164
165
  var tagged_items: Array = []
165
166
  for item: Variant in items:
166
- tagged_items.append(_finite(item))
167
+ tagged_items.append(_finite(item, single))
167
168
  return tagged_items
168
169
  return carried
169
170
 
@@ -189,7 +190,7 @@ func _serialize_floats(value: Variant) -> Variant:
189
190
  for each: float in numbers:
190
191
  items.append(_serialize_float(each))
191
192
  return items
192
- return value
193
+ return numbers if value is PackedFloat32Array else value
193
194
 
194
195
 
195
196
  # Rebuilds a Godot value from the shape serialize_value gave it.
@@ -222,8 +223,7 @@ func deserialize_value(value: Variant) -> Variant:
222
223
  # under the target project's warning levels, so one such project lost every headless call.
223
224
  var tag: String = str(fields["_type"])
224
225
  var builder: String = BUILDERS.get(tag, "")
225
- var spare: Array = GdhShapes.spare(fields, tag)
226
- if builder.is_empty() or not spare.any(func(left: Array) -> bool: return left.is_empty()):
226
+ if builder.is_empty() or not GdhShapes.buildable(fields, tag):
227
227
  return fields
228
228
  if tag == "float" and str(fields["value"]) not in ["inf", "-inf", "nan"]:
229
229
  return fields
@@ -276,24 +276,7 @@ static func components_of(value: Variant) -> PackedStringArray:
276
276
  TYPE_VECTOR4, TYPE_VECTOR4I, TYPE_QUATERNION:
277
277
  return PackedStringArray(["x", "y", "z", "w"])
278
278
  TYPE_COLOR:
279
- return PackedStringArray(
280
- [
281
- "r",
282
- "g",
283
- "b",
284
- "a",
285
- "r8",
286
- "g8",
287
- "b8",
288
- "a8",
289
- "h",
290
- "s",
291
- "v",
292
- "ok_hsl_h",
293
- "ok_hsl_s",
294
- "ok_hsl_l"
295
- ]
296
- )
279
+ return "r g b a r8 g8 b8 a8 h s v ok_hsl_h ok_hsl_s ok_hsl_l".split(" ")
297
280
  TYPE_RECT2, TYPE_RECT2I, TYPE_AABB:
298
281
  return PackedStringArray(["position", "size", "end"])
299
282
  TYPE_PLANE:
@@ -404,7 +387,8 @@ func fitted(value: Variant, type: int) -> Variant:
404
387
  return built
405
388
 
406
389
  if rebuilt is Array and LIST_TYPES.has(type):
407
- return type_convert(rebuilt, type)
390
+ var items: Array = value if value is Array else rebuilt
391
+ return GdhShapes.packed_from(items, type, fitted)
408
392
 
409
393
  # The inspector and every colour picker give a colour as its html code. Anything else stays the
410
394
  # text it is, for the refusal to name.
@@ -704,7 +688,13 @@ func _describe_built_in(resource: Resource) -> Dictionary:
704
688
  var script: Script = null
705
689
  if attached is Script:
706
690
  script = attached
707
- described["script"] = serialize_value(script)
691
+ # By the class its script declares, which a write takes as the _type and which says what it
692
+ # is: as Resource with the script beside it, one answer named a Shop's built-in item that way
693
+ # and the file item beside it as an Item.
694
+ if script.get_global_name().is_empty():
695
+ described["script"] = serialize_value(script)
696
+ else:
697
+ described["_type"] = str(script.get_global_name())
708
698
  var fresh: Object = _fresh(script)
709
699
  for property: Dictionary in resource.get_property_list():
710
700
  var usage: int = property.get("usage", 0)
@@ -5,6 +5,22 @@ extends RefCounted
5
5
  ##
6
6
  ## This file is the one beside the operations, and `bun run sync:gd` copies it into each addon.
7
7
 
8
+ const GdhRead = preload("reading.gd")
9
+
10
+ ## What each packed array holds, one value of which every element is fitted to.
11
+ const PACKED_ELEMENTS: Dictionary = {
12
+ TYPE_PACKED_BYTE_ARRAY: TYPE_INT,
13
+ TYPE_PACKED_INT32_ARRAY: TYPE_INT,
14
+ TYPE_PACKED_INT64_ARRAY: TYPE_INT,
15
+ TYPE_PACKED_FLOAT32_ARRAY: TYPE_FLOAT,
16
+ TYPE_PACKED_FLOAT64_ARRAY: TYPE_FLOAT,
17
+ TYPE_PACKED_STRING_ARRAY: TYPE_STRING,
18
+ TYPE_PACKED_VECTOR2_ARRAY: TYPE_VECTOR2,
19
+ TYPE_PACKED_VECTOR3_ARRAY: TYPE_VECTOR3,
20
+ TYPE_PACKED_VECTOR4_ARRAY: TYPE_VECTOR4,
21
+ TYPE_PACKED_COLOR_ARRAY: TYPE_COLOR,
22
+ }
23
+
8
24
  # What each tag has to carry before it is built. A tag naming a type whose keys are not there is a
9
25
  # tag somebody wrote by mistake or wrote for a different shape, and building one out of defaults
10
26
  # answers with a Transform2D of zeros where the caller meant something else entirely. Unbuilt, it
@@ -33,6 +49,21 @@ const REQUIRED: Dictionary = {
33
49
  "RID": [["id"]],
34
50
  "Dictionary": [["entries"]],
35
51
  }
52
+ ## The fields each type reads as numbers. A builder reads a word there as 0, so {"x": 0, "y": "far"}
53
+ ## was written as (0, 0) and answered as written.
54
+ const NUMBER_FIELDS: Dictionary = {
55
+ "Vector2": ["x", "y"],
56
+ "Vector2i": ["x", "y"],
57
+ "Vector3": ["x", "y", "z"],
58
+ "Vector3i": ["x", "y", "z"],
59
+ "Vector4": ["x", "y", "z", "w"],
60
+ "Vector4i": ["x", "y", "z", "w"],
61
+ "Rect2": ["x", "y", "width", "height"],
62
+ "Rect2i": ["x", "y", "width", "height"],
63
+ "Plane": ["d"],
64
+ "Quaternion": ["x", "y", "z", "w"],
65
+ "Color": ["r", "g", "b", "a"],
66
+ }
36
67
  ## The fields a builder reads when they are there and does without when not.
37
68
  const OPTIONAL: Dictionary = {
38
69
  "Color": ["a"],
@@ -77,6 +108,86 @@ static func spare(fields: Dictionary, tag: String) -> Array:
77
108
  return lists
78
109
 
79
110
 
111
+ ## Whether a [param tag] built from [param fields] reads every one of them, and each it reads as a
112
+ ## number holds one.
113
+ static func buildable(fields: Dictionary, tag: String) -> bool:
114
+ return (
115
+ spare(fields, tag).any(func(left: Array) -> bool: return left.is_empty())
116
+ and not_a_number(fields, tag).is_empty()
117
+ )
118
+
119
+
120
+ ## The first field of [param fields] that a [param tag] reads as a number and that holds no number,
121
+ ## or "" when there is none. Digits in quotes are a number, as a write takes them for a float.
122
+ static func not_a_number(fields: Dictionary, tag: String) -> String:
123
+ for key: String in NUMBER_FIELDS.get(tag, []):
124
+ if fields.has(key) and not _is_number(fields[key]):
125
+ return key
126
+ return ""
127
+
128
+
129
+ ## Whether every item of [param items] is a number, for a list read by its places as a vector.
130
+ static func all_numbers(items: Array) -> bool:
131
+ return items.all(_is_number)
132
+
133
+
134
+ # A float JSON has no number for, infinity or NaN, crosses tagged as {"_type": "float", "value":
135
+ # "inf"}, and is a number.
136
+ static func _is_number(held: Variant) -> bool:
137
+ if held is int or held is float:
138
+ return true
139
+ if held is String:
140
+ return str(held).is_valid_float()
141
+ if held is Dictionary:
142
+ var tagged: Dictionary = held
143
+ return str(tagged.get("_type", "")) in ["float", "int"] and tagged.has("value")
144
+ return false
145
+
146
+
147
+ ## Whether [param value] holds a number with a fractional part where an integer is wanted, in a
148
+ ## list or a dictionary as much as bare. The conversion would cut 2.7 to 2 without a word, and a
149
+ ## dictionary key arrives as the text "2.7", which reads as the same number.
150
+ static func fraction(value: Variant, type: int, elements: Array = []) -> bool:
151
+ if type == TYPE_INT:
152
+ var number: Variant = value
153
+ if typeof(value) == TYPE_STRING:
154
+ number = GdhRead.json_or_null(str(value))
155
+ if typeof(number) != TYPE_FLOAT:
156
+ return false
157
+ var whole: float = number
158
+ return is_finite(whole) and whole != floorf(whole)
159
+ var element: int = GdhRead.as_int(PACKED_ELEMENTS.get(type, TYPE_NIL), TYPE_NIL)
160
+ if type == TYPE_ARRAY and not elements.is_empty():
161
+ element = GdhRead.as_int(elements[0], TYPE_NIL)
162
+ if value is Array and element != TYPE_NIL:
163
+ var items: Array = value
164
+ return items.any(func(item: Variant) -> bool: return fraction(item, element))
165
+ if type == TYPE_DICTIONARY and elements.size() == 2 and value is Dictionary:
166
+ var entries: Dictionary = value
167
+ var key_type: int = GdhRead.as_int(elements[0], TYPE_NIL)
168
+ var value_type: int = GdhRead.as_int(elements[1], TYPE_NIL)
169
+ for key: Variant in entries:
170
+ if fraction(key, key_type) or fraction(entries[key], value_type):
171
+ return true
172
+ return false
173
+
174
+
175
+ ## The packed array [param type] built from [param items], each fitted by [param fit] to what the
176
+ ## array holds, or [param items] as they are when one cannot become that, for the caller to refuse.
177
+ ## Element by element, so a point given as its fields is read as one: converted whole, a list of
178
+ ## {"x": 3, "y": 4} became zero vectors, and the call ran on them.
179
+ static func packed_from(items: Array, type: int, fit: Callable) -> Variant:
180
+ var element: int = PACKED_ELEMENTS[type]
181
+ var numbers: Array[int] = [TYPE_INT, TYPE_FLOAT]
182
+ var built: Array = []
183
+ for item: Variant in items:
184
+ var one: Variant = fit.call(item, element)
185
+ if typeof(one) != element and not (numbers.has(typeof(one)) and numbers.has(element)):
186
+ return items
187
+ built.append(one)
188
+ return type_convert(built, type)
189
+
190
+
80
191
  ## The sentences a refusal of [param value] where a [param type] goes ends with: which of its keys
81
192
  ## kept it from being read as one, and how one is written. Empty for a type not built from fields.
82
193
  ##
@@ -92,7 +203,23 @@ static func remedy(value: Variant, type: int) -> String:
92
203
  var shape: String = written_as(type)
93
204
  if shape.is_empty():
94
205
  return ""
95
- return _unread(value, type) + " A %s is sent as %s." % [type_string(type), shape]
206
+ return (
207
+ _unread(value, type) + _no_number(value, type) + " A %s is sent as %s." % [type_string(type), shape]
208
+ )
209
+
210
+
211
+ static func _no_number(value: Variant, type: int) -> String:
212
+ var tag: String = type_string(type)
213
+ if value is Dictionary:
214
+ var fields: Dictionary = value
215
+ var key: String = not_a_number(fields, tag)
216
+ if not key.is_empty():
217
+ return " Its %s is %s, which is no number." % [key, JSON.stringify(fields[key])]
218
+ if value is Array and NUMBER_FIELDS.has(tag):
219
+ var items: Array = value
220
+ if not all_numbers(items):
221
+ return " Read by its places, each item is a number."
222
+ return ""
96
223
 
97
224
 
98
225
  static func _unread(value: Variant, type: int) -> String: