gdharness 1.3.7 → 1.3.9

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 (38) hide show
  1. package/build/cli.js +999 -864
  2. package/build/godot/addons/gdharness_editor/file_reads.gd +13 -0
  3. package/build/godot/addons/gdharness_editor/reading.gd +61 -5
  4. package/build/godot/addons/gdharness_editor/scene_file.gd +41 -35
  5. package/build/godot/addons/gdharness_editor/serialisation.gd +8 -7
  6. package/build/godot/addons/gdharness_editor/tool_executor.gd +12 -2
  7. package/build/godot/addons/gdharness_editor/tools/animation_states.gd +244 -0
  8. package/build/godot/addons/gdharness_editor/tools/animation_states.gd.uid +1 -0
  9. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +105 -157
  10. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +25 -1
  11. package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +4 -1
  12. package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +10 -0
  13. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +72 -3
  14. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +7 -1
  15. package/build/godot/addons/gdharness_runtime/call_errors.gd +8 -2
  16. package/build/godot/addons/gdharness_runtime/reading.gd +61 -5
  17. package/build/godot/addons/gdharness_runtime/runtime_capture.gd +5 -0
  18. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +6 -1
  19. package/build/godot/addons/gdharness_runtime/runtime_input.gd +74 -54
  20. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +38 -6
  21. package/build/godot/addons/gdharness_runtime/runtime_queries.gd +2 -0
  22. package/build/godot/addons/gdharness_runtime/runtime_targets.gd +26 -0
  23. package/build/godot/addons/gdharness_runtime/runtime_values.gd +105 -6
  24. package/build/godot/addons/gdharness_runtime/serialisation.gd +8 -7
  25. package/build/godot/operations/dependencies.gd +61 -10
  26. package/build/godot/operations/editor_tools.gd +8 -0
  27. package/build/godot/operations/file_reads.gd +13 -0
  28. package/build/godot/operations/gdscript_analysis.gd +76 -6
  29. package/build/godot/operations/gdscript_authoring.gd +5 -2
  30. package/build/godot/operations/godot_operations.gd +4 -0
  31. package/build/godot/operations/import_pipeline.gd +45 -1
  32. package/build/godot/operations/input_actions.gd +22 -7
  33. package/build/godot/operations/project_config.gd +31 -0
  34. package/build/godot/operations/project_diagnostics.gd +6 -5
  35. package/build/godot/operations/reading.gd +61 -5
  36. package/build/godot/operations/serialisation.gd +8 -7
  37. package/build/keeper.js +44 -13
  38. package/package.json +1 -1
@@ -172,6 +172,11 @@ func _cut_asked(params: Dictionary, in_window: bool) -> Dictionary:
172
172
  return _refused(shape % JSON.stringify(region_given))
173
173
  asked["region"] = region
174
174
  if not node_path.is_empty():
175
+ # Before any refusal of the window: a headless game refused a path that named nothing for
176
+ # having no window, and the wrong path was found only after a restart with one.
177
+ var standing: Dictionary = GdhValues.node_at(_host.get_tree().root, node_path)
178
+ if standing.has("message"):
179
+ return standing
175
180
  asked["node_path"] = node_path
176
181
  return asked
177
182
 
@@ -493,6 +493,9 @@ func call_method(params: Dictionary) -> Dictionary:
493
493
  answer["awaited"] = true
494
494
  _keep_reported(answer, "errors", caught.raised)
495
495
  _keep_reported(answer, "warnings", caught.warned)
496
+ # Main.explode, ended at a null it called into, answered result 0 as if it had counted something.
497
+ if caught.ended_at.has(named):
498
+ answer["ended_by_error_at"] = caught.ended_at[named]
496
499
  var asked: Variant = params.get("properties")
497
500
  if _reads_the_result(asked):
498
501
  answer.merge(_read_off_result(result, asked))
@@ -524,7 +527,9 @@ static func _not_a_method_to_call(holder: Object, named: String, called: String)
524
527
  % [called, named]
525
528
  )
526
529
  method = GdhPaths.method_of(named)
527
- return "" if holder.has_method(method) else "%s has no method %s" % [called, method]
530
+ if holder.has_method(method):
531
+ return ""
532
+ return GdhPaths.with_methods_offered("%s has no method %s" % [called, method], holder, method)
528
533
 
529
534
 
530
535
  ## Whether a call's `properties` asks for its result to be read: true, or a list of names.
@@ -21,15 +21,6 @@ const GdhMouse = preload("runtime_mouse.gd")
21
21
  ## everything that process starts. Kept in step with `DESKTOP_VARIABLE` in src/desktop.ts.
22
22
  const DESKTOP_VARIABLE: String = "GDHARNESS_DESKTOP"
23
23
 
24
- ## Node.ProcessMode as its names, for a refusal to say which one is in force.
25
- const PROCESS_MODES: Dictionary[int, String] = {
26
- Node.PROCESS_MODE_INHERIT: "INHERIT",
27
- Node.PROCESS_MODE_PAUSABLE: "PAUSABLE",
28
- Node.PROCESS_MODE_WHEN_PAUSED: "WHEN_PAUSED",
29
- Node.PROCESS_MODE_ALWAYS: "ALWAYS",
30
- Node.PROCESS_MODE_DISABLED: "DISABLED",
31
- }
32
-
33
24
  ## Whether this run has had the pointer note in full; the game process is the run.
34
25
  static var _pointer_explained: bool = false
35
26
 
@@ -151,13 +142,17 @@ func inject_action(params: Dictionary) -> Dictionary:
151
142
  return {"type": "error", "message": "Action name required"}
152
143
 
153
144
  if not InputMap.has_action(action):
154
- return {"type": "error", "message": "Action not found: " + action}
145
+ return {"type": "error", "message": GdhValues.no_action(action)}
155
146
  var holding: Variant = GdhValues.held_for(params)
156
147
  if holding is String:
157
148
  return {"type": "error", "message": holding}
158
149
  var frames: int = holding
159
150
 
151
+ var focused: String = _focus_owner()
160
152
  _say_action(action, whole or held, strength)
153
+ var shortcuts: Dictionary = {"shortcuts": [], "shortcut_pressed": null}
154
+ if whole or held:
155
+ shortcuts = await GdhValues.press_shortcuts(_host.get_tree().root, action)
161
156
  var lasted: Variant = null
162
157
  if whole:
163
158
  # A frame between the halves at least, as a click has: a listener that acts on the press and
@@ -166,15 +161,36 @@ func inject_action(params: Dictionary) -> Dictionary:
166
161
  _say_action(action, false, strength)
167
162
  await _host.get_tree().process_frame
168
163
 
169
- return {
170
- "type": "input_injected",
171
- "input_type": "action",
172
- "action": action,
173
- "pressed": held and not whole,
174
- "whole": whole,
175
- "held_frames": GdhValues.held_answer(whole, frames),
176
- "held_for": lasted,
177
- }
164
+ return _focus_noted(
165
+ {
166
+ "type": "input_injected",
167
+ "input_type": "action",
168
+ "action": action,
169
+ "shortcuts": shortcuts["shortcuts"],
170
+ "shortcut_pressed": shortcuts["shortcut_pressed"],
171
+ "pressed": held and not whole,
172
+ "whole": whole,
173
+ "held_frames": GdhValues.held_answer(whole, frames),
174
+ "held_for": lasted,
175
+ },
176
+ focused
177
+ )
178
+
179
+
180
+ ## The path of the Control holding the keyboard focus, or "" when none does. Read before a key or an
181
+ ## action goes in, since pressing a focused button can move the focus.
182
+ func _focus_owner() -> String:
183
+ var holding: Control = _host.get_viewport().gui_get_focus_owner()
184
+ return "" if holding == null else str(holding.get_path())
185
+
186
+
187
+ ## [param said] with [param focused], the Control that had the focus, under focused. A key or an action
188
+ ## reaches that Control before the game's _unhandled_input, so Space on a focused Button pressed it
189
+ ## and the game's own handler counted the key as well, with nothing in the answer saying why.
190
+ static func _focus_noted(said: Dictionary, focused: String) -> Dictionary:
191
+ if not focused.is_empty():
192
+ said["focused"] = focused
193
+ return said
178
194
 
179
195
 
180
196
  func _say_action(action: String, down: bool, strength: float) -> void:
@@ -230,6 +246,7 @@ func inject_key(params: Dictionary) -> Dictionary:
230
246
  # keys are the code points themselves, which is what makes this a mapping and not a table.
231
247
  event.unicode = GdhValues.glyph_of(event.keycode, event.shift_pressed)
232
248
 
249
+ var focused: String = _focus_owner()
233
250
  Input.parse_input_event(event)
234
251
  var lasted: Variant = null
235
252
  if whole:
@@ -239,20 +256,24 @@ func inject_key(params: Dictionary) -> Dictionary:
239
256
  Input.parse_input_event(up)
240
257
  await _host.get_tree().process_frame
241
258
 
242
- return {
243
- "type": "input_injected",
244
- "input_type": "key",
245
- "held_frames": GdhValues.held_answer(whole, frames),
246
- "held_for": lasted,
247
- "keycode": event.keycode,
248
- "physical_keycode": event.physical_keycode,
249
- "shift": event.shift_pressed,
250
- "ctrl": event.ctrl_pressed,
251
- "alt": event.alt_pressed,
252
- "unicode": event.unicode,
253
- "pressed": held and not whole,
254
- "whole": whole,
255
- }
259
+ return _focus_noted(
260
+ {
261
+ "type": "input_injected",
262
+ "input_type": "key",
263
+ "held_frames": GdhValues.held_answer(whole, frames),
264
+ "held_for": lasted,
265
+ "keycode": event.keycode,
266
+ "physical_keycode": event.physical_keycode,
267
+ "shift": event.shift_pressed,
268
+ "ctrl": event.ctrl_pressed,
269
+ "alt": event.alt_pressed,
270
+ "unicode": event.unicode,
271
+ "actions": GdhValues.actions_of(event),
272
+ "pressed": held and not whole,
273
+ "whole": whole,
274
+ },
275
+ focused
276
+ )
256
277
 
257
278
 
258
279
  ## Clicks a mouse button at a position, and lets go of it again unless asked to hold it.
@@ -293,7 +314,8 @@ func inject_mouse_click(params: Dictionary) -> Dictionary:
293
314
  # its drag from the press, which is where a real pointer would be.
294
315
  var _moved: Vector2 = _arrive(position)
295
316
  var supplied: bool = _follow_in_the_window(position)
296
- Input.parse_input_event(_button(position, button, whole or held, double))
317
+ var press: InputEventMouseButton = _button(position, button, whole or held, double)
318
+ Input.parse_input_event(press)
297
319
  var lasted: Variant = null
298
320
  if notch:
299
321
  Input.parse_input_event(_button(position, button, false, false))
@@ -302,7 +324,12 @@ func inject_mouse_click(params: Dictionary) -> Dictionary:
302
324
  # press and one that acts on the release each get a frame to do it in.
303
325
  lasted = await GdhValues.hold(_host.get_tree(), frames)
304
326
  Input.parse_input_event(_button(position, button, false, false))
305
- await _host.get_tree().process_frame
327
+ # Input hands the events on at the next frame, so the control under the pointer is read after it.
328
+ await _host.get_tree().process_frame
329
+ var hovered: Control = _hover.hovered_in(_host.get_viewport())
330
+ var hovered_path: Variant = null
331
+ if hovered != null:
332
+ hovered_path = str(hovered.get_path())
306
333
 
307
334
  var at: Array[Vector2] = [position]
308
335
  var said: Dictionary = _pointer_noted(
@@ -314,6 +341,8 @@ func inject_mouse_click(params: Dictionary) -> Dictionary:
314
341
  "held_for": lasted,
315
342
  "position": [position.x, position.y],
316
343
  "button": button,
344
+ "hovered": hovered_path,
345
+ "actions": GdhValues.actions_of(press),
317
346
  "pressed": held and not whole,
318
347
  "whole": whole,
319
348
  "double": double
@@ -629,7 +658,8 @@ func click(params: Dictionary) -> Dictionary:
629
658
  var was_pressed: Variant = null
630
659
  if toggle != null and toggle.toggle_mode:
631
660
  was_pressed = toggle.button_pressed
632
- _press_button(viewport, _button(position, button, true, double))
661
+ var press: InputEventMouseButton = _button(position, button, true, double)
662
+ _press_button(viewport, press)
633
663
  await _host.get_tree().process_frame
634
664
  _press_button(viewport, _button(position, button, false, false))
635
665
  # The release is what a button acts on, and a queue_free it causes lands at the end of
@@ -651,9 +681,16 @@ func click(params: Dictionary) -> Dictionary:
651
681
  "double": double,
652
682
  "hovered": hovered_path,
653
683
  "landed": landed,
684
+ "actions": GdhValues.actions_of(press),
654
685
  "control_afterwards": afterwards,
655
686
  "scrolled_into_view": scrolled,
656
687
  }
688
+ # A Label and a TextureRect ignore the pointer unless told otherwise, so the click goes to what is
689
+ # under them, and landed false with that control as hovered said nothing of why.
690
+ if not landed and control.mouse_filter == Control.MOUSE_FILTER_IGNORE:
691
+ answer["why_not_landed"] = (
692
+ "%s's mouse_filter is ignore, so the pointer passes through it to what is under it" % node_path
693
+ )
657
694
  if found != null:
658
695
  answer["found"] = found
659
696
  if was_pressed != null:
@@ -694,7 +731,7 @@ func _takes_no_click(control: Control, node_path: String) -> String:
694
731
  % [node_path, on]
695
732
  )
696
733
  if not control.can_process():
697
- var why: String = _why_it_does_not_process(control, node_path)
734
+ var why: String = GdhTargets.why_it_does_not_process(control, node_path)
698
735
  var over: Node = GdhTargets.drawn_over(control)
699
736
  var cover: String = "" if over == null else "; %s is drawn over it" % over.get_path()
700
737
  if _host.get_tree().paused:
@@ -706,23 +743,6 @@ func _takes_no_click(control: Control, node_path: String) -> String:
706
743
  return ""
707
744
 
708
745
 
709
- ## Which node's process_mode keeps [param control] from processing, and what it is. A node set to
710
- ## inherit takes its parent's, so the one deciding can be any node above it: the game turning a
711
- ## whole screen off while a menu shows over it reads, on the button, as a button turned off.
712
- static func _why_it_does_not_process(control: Control, node_path: String) -> String:
713
- var deciding: Node = control
714
- while deciding.process_mode == Node.PROCESS_MODE_INHERIT and deciding.get_parent() != null:
715
- deciding = deciding.get_parent()
716
- # The root inherits from nothing, which processes as pausable.
717
- var value: int = deciding.process_mode
718
- if value == Node.PROCESS_MODE_INHERIT:
719
- value = Node.PROCESS_MODE_PAUSABLE
720
- var mode: String = PROCESS_MODES.get(value, str(value))
721
- if deciding == control:
722
- return "its process_mode is %s" % mode
723
- return "%s inherits process_mode %s from %s" % [node_path, mode, deciding.get_path()]
724
-
725
-
726
746
  ## Why a click at [param control]'s centre would reach nothing, or "" when it would reach it: the
727
747
  ## centre outside the viewport it is drawn in, at [param local]; outside the viewport showing that
728
748
  ## one on the screen, which [param reached] holds with the centre there; or clipped away by a
@@ -405,10 +405,13 @@ static func _arity(object: Object, method: String) -> Dictionary:
405
405
  ##
406
406
  ## A path is read, and a wait reads it every frame, so a call in one has to be a read. The engine
407
407
  ## 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.
408
+ ## 4.7.2, though it leaves some reads unmarked as well, `Engine.get_frames_drawn()` and
409
+ ## `get_time_scale()` among them; so the refusal says the mark is missing, not that the method
410
+ ## changes anything, and points at runtime_invoke call, which calls it. A game's methods carry no
411
+ ## such mark, so one declared `-> void` is taken at its word, as doing something and answering
412
+ ## nothing, and one left untyped is trusted. Without this a find with property "queue_free()" freed
413
+ ## every node it matched, and a wait on "advance_day()" called it once a frame and then reported the
414
+ ## state it had driven the game to.
412
415
  ##
413
416
  ## Given [param given] arguments, a call is the engine's and marked as reading, or it is refused. A
414
417
  ## game's method taking arguments is trusted on nothing at all: one answering a value can as well
@@ -416,7 +419,7 @@ static func _arity(object: Object, method: String) -> Dictionary:
416
419
  static func call_refused(object: Object, method: String, called_as: String, given: int = 0) -> String:
417
420
  var arity: Dictionary = _arity(object, method)
418
421
  if arity.is_empty():
419
- return "%s has no method %s" % [called_as, method]
422
+ return with_methods_offered("%s has no method %s" % [called_as, method], object, method)
420
423
  var least: int = arity["least"]
421
424
  var most: int = arity["most"]
422
425
  var any_more: bool = arity["any_more"]
@@ -450,10 +453,39 @@ static func call_refused(object: Object, method: String, called_as: String, give
450
453
  )
451
454
  return ""
452
455
  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]
456
+ return (
457
+ "%s.%s is not one the engine marks as only reading%s; runtime_invoke call calls it"
458
+ % [called_as, method, READS_ONLY]
459
+ )
454
460
  return ""
455
461
 
456
462
 
463
+ ## [param said], a refusal for [param method], which [param object] lacks, with the methods it has
464
+ ## nearest the name, or else those its script declares. The engine's own run to hundreds on a node,
465
+ ## so they are offered only when one is near.
466
+ static func with_methods_offered(said: String, object: Object, method: String) -> String:
467
+ var own: Array[String] = []
468
+ var attached: Variant = object.get_script()
469
+ if attached is Script:
470
+ var script: Script = attached
471
+ for entry: Dictionary in script.get_script_method_list():
472
+ var name: String = str(entry.get("name", ""))
473
+ if not name.is_empty() and not own.has(name):
474
+ own.append(name)
475
+ var every: Array[String] = own.duplicate()
476
+ for entry: Dictionary in object.get_method_list():
477
+ var name: String = str(entry.get("name", ""))
478
+ if not every.has(name):
479
+ every.append(name)
480
+ var close: PackedStringArray = GdhRead.nearest(method, PackedStringArray(every))
481
+ if not close.is_empty():
482
+ return "%s; the nearest it has %s %s" % [said, "is" if close.size() == 1 else "are", ", ".join(close)]
483
+ if own.is_empty():
484
+ return said
485
+ own.sort()
486
+ return "%s; its script declares %s" % [said, ", ".join(PackedStringArray(own))]
487
+
488
+
457
489
  ## The entry a script attached to [param object] declares for [param method], or an empty one when
458
490
  ## the method is the engine's.
459
491
  static func _script_method(object: Object, method: String) -> Dictionary:
@@ -723,6 +723,8 @@ func get_metrics(params: Dictionary) -> Dictionary:
723
723
  "frame_time": frame_time,
724
724
  "frame_time_max": frame_time_max,
725
725
  "frames_timed": timed,
726
+ "process_frames": Engine.get_process_frames(),
727
+ "physics_frames": Engine.get_physics_frames(),
726
728
  "physics_steps_per_second": physics_steps_per_second,
727
729
  "physics_ticks_per_second": Engine.physics_ticks_per_second,
728
730
  "max_physics_steps_per_frame": Engine.max_physics_steps_per_frame,
@@ -13,6 +13,15 @@ const GdhWords = preload("runtime_words.gd")
13
13
  ## How many of the controls saying the same words a refusal names, with the index for each.
14
14
  const LISTED_CANDIDATES: int = 10
15
15
 
16
+ ## Node.ProcessMode as its names, for a refusal to say which one is in force.
17
+ const PROCESS_MODES: Dictionary[int, String] = {
18
+ Node.PROCESS_MODE_INHERIT: "INHERIT",
19
+ Node.PROCESS_MODE_PAUSABLE: "PAUSABLE",
20
+ Node.PROCESS_MODE_WHEN_PAUSED: "WHEN_PAUSED",
21
+ Node.PROCESS_MODE_ALWAYS: "ALWAYS",
22
+ Node.PROCESS_MODE_DISABLED: "DISABLED",
23
+ }
24
+
16
25
  ## What a game started without a window gets whatever the project settings say, and the usual
17
26
  ## reason a control is out of reach. Only worth telling somebody when it is what they have.
18
27
  const HEADLESS_VIEWPORT: Vector2 = Vector2(64, 64)
@@ -954,3 +963,20 @@ static func _out_of_reach_exactly(nodes: Array[Node], why: Array[String]) -> Str
954
963
  if nodes.size() == 1:
955
964
  return "1 control says exactly that and cannot be clicked: %s" % first
956
965
  return "%d controls say exactly that and cannot be clicked, such as %s" % [nodes.size(), first]
966
+
967
+
968
+ ## Which node's process_mode keeps [param control] from processing, and what it is. A node set to
969
+ ## inherit takes its parent's, so the one deciding can be any node above it: the game turning a
970
+ ## whole screen off while a menu shows over it reads, on the button, as a button turned off.
971
+ static func why_it_does_not_process(control: Control, node_path: String) -> String:
972
+ var deciding: Node = control
973
+ while deciding.process_mode == Node.PROCESS_MODE_INHERIT and deciding.get_parent() != null:
974
+ deciding = deciding.get_parent()
975
+ # The root inherits from nothing, which processes as pausable.
976
+ var value: int = deciding.process_mode
977
+ if value == Node.PROCESS_MODE_INHERIT:
978
+ value = Node.PROCESS_MODE_PAUSABLE
979
+ var mode: String = PROCESS_MODES.get(value, str(value))
980
+ if deciding == control:
981
+ return "its process_mode is %s" % mode
982
+ return "%s inherits process_mode %s from %s" % [node_path, mode, deciding.get_path()]
@@ -31,6 +31,9 @@ const SINGLETONS: Array[String] = [
31
31
  ## The most frames a press is held in one call, as many as a frames wait takes.
32
32
  const MOST_HELD: int = 600
33
33
 
34
+ ## Read by [method _key_names], once a game.
35
+ static var _named_keys: PackedStringArray = []
36
+
34
37
  var _shared: GdhSerialisation = GdhSerialisation.new()
35
38
 
36
39
 
@@ -160,12 +163,7 @@ static func glyph_of(keycode: Key, shifted: bool) -> int:
160
163
  ## engine's own names, printable keys and the special ones above [constant KEY_SPECIAL], since the
161
164
  ## game takes any name OS.find_keycode_from_string does.
162
165
  static func unknown_key(name: String) -> String:
163
- var names: PackedStringArray = []
164
- for code: int in range(32, 127) + range(KEY_SPECIAL + 1, KEY_SPECIAL + 0x100):
165
- var spelled: String = OS.get_keycode_string(code as Key)
166
- if not spelled.is_empty() and OS.find_keycode_from_string(spelled) == code:
167
- var _named: bool = names.append(spelled)
168
- var close: PackedStringArray = GdhRead.nearest(name, names)
166
+ var close: PackedStringArray = GdhRead.nearest(name, _key_names())
169
167
  var said: String = "No key is called %s." % name
170
168
  if close.size() == 1:
171
169
  said += " The nearest key name is %s." % close[0]
@@ -178,6 +176,107 @@ static func unknown_key(name: String) -> String:
178
176
  )
179
177
 
180
178
 
179
+ ## Every key name the engine takes, read once a game. The engine spells a code it has no name for as
180
+ ## the character it would be, and past the last code point that is an error it prints: 136 of them
181
+ ## for the special range, two seconds of console on a fast machine, more than a call's whole wait on
182
+ ## a loaded one, and each in the game's log as an error the game never made. So they are asked with
183
+ ## the engine's error printing off, which no other line of the main thread runs beside.
184
+ static func _key_names() -> PackedStringArray:
185
+ if not _named_keys.is_empty():
186
+ return _named_keys
187
+ var printing: bool = Engine.print_error_messages
188
+ Engine.print_error_messages = false
189
+ for code: int in range(32, 127) + range(KEY_SPECIAL + 1, KEY_SPECIAL + 0x100):
190
+ var spelled: String = OS.get_keycode_string(code as Key)
191
+ if not spelled.is_empty() and OS.find_keycode_from_string(spelled) == code:
192
+ var _named: bool = _named_keys.append(spelled)
193
+ Engine.print_error_messages = printing
194
+ return _named_keys
195
+
196
+
197
+ ## The refusal for [param action], which the input map lacks, with the nearest of the project's own
198
+ ## actions, or else all of them. The engine's ui_ ones are in every project, and offered for any name
199
+ ## they only buried the project's ("dance" is near ui_cancel), so they are offered for a ui_ name only.
200
+ static func no_action(action: String) -> String:
201
+ var engine: PackedStringArray = []
202
+ var own: PackedStringArray = []
203
+ for named: StringName in InputMap.get_actions():
204
+ if str(named).begins_with("ui_"):
205
+ var _listed: bool = engine.append(str(named))
206
+ else:
207
+ var _owned: bool = own.append(str(named))
208
+ own.sort()
209
+ var said: String = "Action not found: %s" % action
210
+ var close: PackedStringArray = GdhRead.nearest(action, engine if action.begins_with("ui_") else own)
211
+ if not close.is_empty():
212
+ return "%s; the nearest %s %s." % [said, "is" if close.size() == 1 else "are", ", ".join(close)]
213
+ if own.is_empty():
214
+ return "%s; the project defines no actions of its own, and the engine's are the ui_ ones." % said
215
+ return "%s; the project's own actions are %s, beside the engine's ui_ ones." % [said, ", ".join(own)]
216
+
217
+
218
+ ## The actions of the input map that [param event] stands for, as Input reads it for
219
+ ## is_action_pressed: a key or a button bound to an action presses that action too, so a game
220
+ ## counting the action counted the key, and the answer had said only that a key went in.
221
+ static func actions_of(event: InputEvent) -> Array[String]:
222
+ var named: Array[String] = []
223
+ for action: StringName in InputMap.get_actions():
224
+ if InputMap.event_is_action(event, action):
225
+ named.append(str(action))
226
+ named.sort()
227
+ return named
228
+
229
+
230
+ ## Presses the buttons under [param root] whose Shortcut [param action] matches, as the player's key
231
+ ## bound to it would, and answers their paths under "shortcuts" and the one that took the press
232
+ ## under "shortcut_pressed". A Viewport hands a shortcut only keys, joypad buttons and
233
+ ## InputEventShortcut, so an action pressed on its own never reached a Button whose Shortcut holds
234
+ ## that action, which is how a game gives a button a binding the player can change.
235
+ ##
236
+ ## A key goes to the shortcuts last in the tree first and stops at the first button that takes it,
237
+ ## while an InputEventShortcut reaches only the buttons holding that one Shortcut. So each Shortcut
238
+ ## is sent in that order, a frame apart, until a button is pressed; a button still decides by its
239
+ ## disabled, its visibility and its shortcut context, as it does for the key.
240
+ static func press_shortcuts(root: Node, action: String) -> Dictionary:
241
+ var pressing: InputEventAction = InputEventAction.new()
242
+ pressing.action = action
243
+ pressing.pressed = true
244
+ var buttons: Array[BaseButton] = []
245
+ for node: Node in root.find_children("*", "BaseButton", true, false):
246
+ var button: BaseButton = node
247
+ if button.shortcut != null and button.shortcut.matches_event(pressing):
248
+ buttons.append(button)
249
+ var paths: Array[String] = []
250
+ for button: BaseButton in buttons:
251
+ paths.append(str(button.get_path()))
252
+ buttons.reverse()
253
+ var taken: Array[String] = []
254
+ var heard: Array[Callable] = []
255
+ for button: BaseButton in buttons:
256
+ var path: String = str(button.get_path())
257
+ var hearing: Callable = func() -> void: taken.append(path)
258
+ heard.append(hearing)
259
+ var _listening: int = button.pressed.connect(hearing)
260
+ var sent: Dictionary = {}
261
+ for button: BaseButton in buttons:
262
+ if not taken.is_empty():
263
+ break
264
+ if sent.has(button.shortcut):
265
+ continue
266
+ sent[button.shortcut] = true
267
+ var through: InputEventShortcut = InputEventShortcut.new()
268
+ through.shortcut = button.shortcut
269
+ Input.parse_input_event(through)
270
+ await root.get_tree().process_frame
271
+ for at: int in buttons.size():
272
+ if is_instance_valid(buttons[at]):
273
+ buttons[at].pressed.disconnect(heard[at])
274
+ var pressed: Variant = null
275
+ if not taken.is_empty():
276
+ pressed = taken[0]
277
+ return {"shortcuts": paths, "shortcut_pressed": pressed}
278
+
279
+
181
280
  ## What [param path] names: a node under [param root], as [method node_at] reads it, or an engine
182
281
  ## singleton by its name. Answers the object under "object", and under "node" the node that objects
183
282
  ## named in a value are found from, which for a singleton is [param root].
@@ -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.