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.
- package/build/cli.js +999 -864
- package/build/godot/addons/gdharness_editor/file_reads.gd +13 -0
- package/build/godot/addons/gdharness_editor/reading.gd +61 -5
- package/build/godot/addons/gdharness_editor/scene_file.gd +41 -35
- package/build/godot/addons/gdharness_editor/serialisation.gd +8 -7
- package/build/godot/addons/gdharness_editor/tool_executor.gd +12 -2
- package/build/godot/addons/gdharness_editor/tools/animation_states.gd +244 -0
- package/build/godot/addons/gdharness_editor/tools/animation_states.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +105 -157
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +25 -1
- package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +4 -1
- package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +10 -0
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +72 -3
- package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +7 -1
- package/build/godot/addons/gdharness_runtime/call_errors.gd +8 -2
- package/build/godot/addons/gdharness_runtime/reading.gd +61 -5
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +5 -0
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +6 -1
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +74 -54
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +38 -6
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +2 -0
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +26 -0
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +105 -6
- package/build/godot/addons/gdharness_runtime/serialisation.gd +8 -7
- package/build/godot/operations/dependencies.gd +61 -10
- package/build/godot/operations/editor_tools.gd +8 -0
- package/build/godot/operations/file_reads.gd +13 -0
- package/build/godot/operations/gdscript_analysis.gd +76 -6
- package/build/godot/operations/gdscript_authoring.gd +5 -2
- package/build/godot/operations/godot_operations.gd +4 -0
- package/build/godot/operations/import_pipeline.gd +45 -1
- package/build/godot/operations/input_actions.gd +22 -7
- package/build/godot/operations/project_config.gd +31 -0
- package/build/godot/operations/project_diagnostics.gd +6 -5
- package/build/godot/operations/reading.gd +61 -5
- package/build/godot/operations/serialisation.gd +8 -7
- package/build/keeper.js +44 -13
- 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
|
-
|
|
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":
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
409
|
-
##
|
|
410
|
-
##
|
|
411
|
-
##
|
|
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
|
|
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
|
|
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
|
|
146
|
-
|
|
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.
|