Merge remote-tracking branch 'origin/human/r1-retro-tick' into auto/port-p6-audio
This commit is contained in:
@@ -97,7 +97,33 @@ reading as progress because each entry is individually rigorous.
|
||||
|
||||
## 7. What needs the human
|
||||
|
||||
1. **The register re-classification (R1).** It changes the file both agents read to
|
||||
decide what *not* to try. Two agents agreeing is not the right authority.
|
||||
1. ✅ **The register re-classification (R1) — DONE 2026-09-01, by the human**, on
|
||||
`docs/re/REFUTED.md` at the Decoder's tip. All **222** entries now carry an
|
||||
`⟨instrument⟩`; the file opens with a reading guide naming which instruments
|
||||
are ours; R1 is now standing text in `PROTOCOL.md`; and
|
||||
`tools/stale-instrument` is the `--stale` query — run it whenever you improve
|
||||
a renderer, a reader or the harness, and it lists what that instrument killed.
|
||||
|
||||
**Ten entries moved ❌ → 🟡**, each naming what would settle it: eight
|
||||
`render-vs-capture`, one `our-reader`, one `harness`.
|
||||
|
||||
Three things the pass turned up that neither self-review had:
|
||||
|
||||
* **The `rest()` question is open, and had been reading as settled in both
|
||||
directions.** *"rest = last keyframe"* was refuted by the sibling argument;
|
||||
that refutation was then refuted by correlating our render against
|
||||
captures. Both legs run through our renderer, so under R1 neither survives
|
||||
— and which one you believed depended on which entry you found first.
|
||||
🔴 **This one is load-bearing for the port**: `rest()` decides the pose
|
||||
every plateau-less element is drawn at.
|
||||
* **A withdrawal never reached its sibling.** *"2 391 frames, max glyph 0"*
|
||||
was withdrawn because a long-lived `x11grab` stream degrades and then
|
||||
repeats a stale frame. The 1 674-sample negative three lines above it —
|
||||
same probe, same instrument, comparable duration — was left standing as a
|
||||
*reinstated measurement*. §1's lesson, inside the register itself.
|
||||
* **83 of 222 entries — 37 % — record no instrument at all.** Not disputed,
|
||||
not safe: **unauditable**. `stale-instrument unrecorded` is the backfill
|
||||
queue, and it is larger than every other group combined.
|
||||
|
||||
2. **P5's gate** — a person clicking through the port. Unchanged, and it is the
|
||||
only thing standing between the milestone and done.
|
||||
|
||||
@@ -84,6 +84,52 @@ command rather than by trusting the date:
|
||||
git log -1 --format=%h -- docs/port/HANDOFF.md # newer than 9ca1eb5? re-reconcile
|
||||
```
|
||||
|
||||
## New asks, 2026-09-01 — from a HUMAN PLAY-TEST on real hardware, port `HEAD` `6b713e8` + input fix
|
||||
|
||||
**The first play-test on a physical controller found four things.** Two were port
|
||||
defects and are fixed; **two are oracle questions and are recorded here unguessed.**
|
||||
|
||||
⚠️ Read the fixed pair first, because the *reason* they survived so long is a
|
||||
method finding that applies to the Decoder's harness as much as this one:
|
||||
|
||||
> **`--script` sends `InputEventAction`, which bypasses the input map.** So every
|
||||
> check this port had asserted the code *below* the map and nothing about the map
|
||||
> — which turned out to have **no joypad binding for `ui_accept` or `ui_cancel`
|
||||
> at all** in Godot 4.7.2, while binding the d-pad *and* the left stick to
|
||||
> `ui_up`/`ui_down`. Ⓐ and Ⓑ were dead on a real pad for the whole of P5 while
|
||||
> the unattended walk passed every iteration. The same blind spot hid the second
|
||||
> defect: an `InputEventAction` is not an analog axis, so nothing could observe
|
||||
> that a held stick fires once per *jitter*. Now asserted by
|
||||
> `tools/port/verify-input`, with a control.
|
||||
|
||||
| # | ask | why the port cannot answer it |
|
||||
|---|---|---|
|
||||
| **H1** | **Does a held direction REPEAT in the menus, and at what rate?** Initial delay and repeat interval, for the d-pad and for the left stick separately if they differ. | Behavioural. The port now does **one step per deflection** — `authored`, and deliberately the conservative half: it cannot run away and it invents no rate. If the game repeats, a human will feel it as *"I have to flick it again"*. |
|
||||
| **H2** | **The splash blur/fade-in does not match the game.** A human watching both says the game's is **more pronounced** than the port's. | Needs a capture of the publisher/developer splash across its fade, at a known frame cadence. The port's ramp comes from the declared keyframes; whether the game applies a **blur** on top — and whether what we read as a fade is one — is not answerable from the export. |
|
||||
| **H3** | **The `PRESS Ⓐ` plate arrives LATE.** The port raises it at `t=236`, derived `238 − 118 = 120 units = 2.000 s`. | 🟡 **Half of this may be the port's, and that half must be settled here first.** Three candidate causes: the **unit→seconds constant** (Decoder's), the **clock origin** — do builds 2 and 4 really start together? — and **`rest.t`** (both ours). `REFUTED.md` records that believing `rest.t` already put a plate **3.97 s late** once. Establish which half it is *before* asking, and say how. |
|
||||
|
||||
**On H2, three things the port can say that narrow it**, none of which settle it:
|
||||
|
||||
* The port draws the splash from the declared keyframe alphas only. It applies
|
||||
**no blur at all**, so "more pronounced in the game" is consistent with a
|
||||
post-effect the export does not describe, with a different ramp shape, or with
|
||||
both.
|
||||
* 🔴 **The `rest()` question is open in both directions** — see
|
||||
[`REFUTED.md`](../re/REFUTED.md)'s `rest()` pair after the 2026-09-01 R1
|
||||
reclassification. The two splashes are the *only* screens that reach the
|
||||
plateau-less fallback (title, main menu and `EXTRAS` reach it zero times), so
|
||||
**H2 lands exactly where our resting-pose heuristic is least trustworthy.**
|
||||
That is not a coincidence worth ignoring.
|
||||
* The register also has *"the declared keyframe timeline reproduces the captured
|
||||
splash"* now sitting at 🟡 `⟨our-reader⟩` rather than ❌, because the
|
||||
record-layout fix re-times a group's final pose and the entry was never
|
||||
re-derived under it. **H2 may already be half-answered by re-running that.**
|
||||
|
||||
**What would settle H2:** a capture of the developer splash across its build-in
|
||||
at a known cadence, compared frame-by-frame against the port's ramp — and, if
|
||||
they differ in shape rather than in extent, a draw capture naming what is
|
||||
submitted per frame.
|
||||
|
||||
## Still open — these block work
|
||||
|
||||
🔴 **This table's rows carry no derivation sha, and that is why they rot.** The standing instruction is to record the HANDOFF commit each row was derived from; every *prose* section added since does, and **every row in this table and the next does not**. On 2026-08-30 an audit found three stale rows here — one of them contradicted by a struck row four lines below it, claiming the boot ends on a plateless title when it had drawn the plate for weeks. The undated rows are exactly the ones that went stale, which is as close to a controlled experiment as this page is going to get.
|
||||
|
||||
@@ -103,6 +103,16 @@ func _ready() -> void:
|
||||
if args.has(flag) and not _has_display(flag):
|
||||
get_tree().quit(4)
|
||||
return
|
||||
|
||||
# 🔴 BEFORE ANYTHING ELSE, and announced. Godot 4.7.2 binds no joypad button
|
||||
# to `ui_accept` or `ui_cancel`, so on a real pad Ⓐ and Ⓑ did nothing while
|
||||
# navigation worked — which reads as a broken controller. `Gamepad` explains
|
||||
# it. Both lines are printed because "the pad is seen" and "Ⓐ is bound" are
|
||||
# different facts and the human debugging this needs to separate them.
|
||||
var _pad_bound := Gamepad.bind_missing()
|
||||
if _pad_bound != "":
|
||||
print(_pad_bound)
|
||||
print(Gamepad.report_devices())
|
||||
var export_tree := ExportTree.locate()
|
||||
_tree = export_tree
|
||||
if export_tree.root == "":
|
||||
@@ -400,6 +410,8 @@ func _ready() -> void:
|
||||
|
||||
|
||||
var _frozen := false
|
||||
## Holds the left stick's latch state. See `gamepad.gd`.
|
||||
var _pad := Gamepad.new()
|
||||
var _flow: Variant = null
|
||||
var _menu: MenuFlow = null
|
||||
var _play := false
|
||||
@@ -774,6 +786,13 @@ func _report_unused_mods() -> void:
|
||||
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
# The left stick is bound to ui_up/ui_down by Godot's own defaults, and an
|
||||
# analog axis is not an edge: held at deflection it emits an event per
|
||||
# jitter, each reporting the action as pressed. `accepts()` latches it to one
|
||||
# step per deflection. Everything already edge-shaped passes through
|
||||
# untouched. See `gamepad.gd` -- including what is authored about it.
|
||||
if not _pad.accepts(event):
|
||||
return
|
||||
# HANDOFF Q9, measured: one (A) press skips a movie -- the title was reached
|
||||
# at 57 s against a 193 s baseline.
|
||||
if _player != null:
|
||||
|
||||
147
port/scripts/gamepad.gd
Normal file
147
port/scripts/gamepad.gd
Normal file
@@ -0,0 +1,147 @@
|
||||
class_name Gamepad
|
||||
extends RefCounted
|
||||
|
||||
## The physical controller: the two buttons Godot does not bind, and the one
|
||||
## input that is not an edge.
|
||||
##
|
||||
## 🔴 BOTH DEFECTS WERE REPORTED BY A HUMAN PLAYING THE PORT (2026-09-01), and
|
||||
## neither could have been caught by the `--script` harness, because that harness
|
||||
## sends `InputEventAction` — which bypasses the input map and is not an analog
|
||||
## axis. The unattended P5 walk passed on every iteration while Ⓐ did nothing at
|
||||
## all on a real pad. **A synthetic-input test asserts the code after the input
|
||||
## map, never the input map itself.**
|
||||
##
|
||||
## ## 1. Godot 4.7.2 binds no joypad button to `ui_accept` or `ui_cancel`
|
||||
##
|
||||
## Measured on this exact build rather than remembered, because the answer has
|
||||
## changed between Godot versions and the remembered one was wrong:
|
||||
##
|
||||
## ```
|
||||
## ui_accept key:Enter, key:Kp Enter, key:Space <- no joypad at all
|
||||
## ui_cancel key:Escape <- no joypad at all
|
||||
## ui_up key:Up, JOYBTN:11, JOYAXIS:1- <- d-pad AND left stick
|
||||
## ui_down key:Down, JOYBTN:12, JOYAXIS:1+
|
||||
## ui_left key:Left, JOYBTN:13, JOYAXIS:0-
|
||||
## ui_right key:Right, JOYBTN:14, JOYAXIS:0+
|
||||
## ```
|
||||
##
|
||||
## That asymmetry is the whole bug report: navigation worked on the pad and Ⓐ/Ⓑ
|
||||
## did nothing, which reads like a broken controller and is a complete input map
|
||||
## for four actions out of six.
|
||||
##
|
||||
## The events are **added to** the built-in actions, never redefined. Declaring
|
||||
## `ui_accept` in `project.godot` replaces the built-in wholesale, so the
|
||||
## keyboard bindings would have to be restated there and would silently rot the
|
||||
## next time Godot changes them.
|
||||
##
|
||||
## ## 2. A stick is not a button
|
||||
##
|
||||
## `ui_up`/`ui_down` are bound to **axis 1**, so the left stick navigates — which
|
||||
## is correct, the real game accepts it too. But an axis emits a fresh
|
||||
## `InputEventJoypadMotion` every time the value *changes*, and a real stick held
|
||||
## at deflection jitters continuously. Every one of those events reports the
|
||||
## action as pressed, so a held stick was one cursor step per jitter: the human's
|
||||
## words were "moves the cursor too fast", and on a five-item menu it crosses
|
||||
## faster than the eye follows.
|
||||
##
|
||||
## So the stick is **latched**: it fires once when it leaves the neutral zone and
|
||||
## not again until it comes back. That makes it behave exactly like the d-pad,
|
||||
## which needs no latch because a button already is an edge.
|
||||
##
|
||||
## ⚠️ **AUTHORED, NOT MEASURED — and deliberately the conservative half.**
|
||||
## Whether the real game *repeats* while a direction is held, and how fast, is
|
||||
## unknown; it is an oracle question. One deflection = one step cannot run away
|
||||
## and cannot invent a rate. If the game does repeat, this is a difference a
|
||||
## human will notice as "I have to flick it again", and the fix is a measured
|
||||
## repeat interval — not a guessed one. Logged for the Decoder as `pad-repeat`.
|
||||
|
||||
## Godot's action deadzone for the `ui_*` actions is 0.50, so the latch must not
|
||||
## arm below it — the action itself would not read as pressed and the step would
|
||||
## be swallowed anyway, leaving the latch armed against a press that never
|
||||
## happened.
|
||||
const ENTER := 0.5
|
||||
|
||||
## Release lower than it arms. Without the gap a stick resting near 0.5 chatters
|
||||
## across the boundary and re-arms on noise, which is the original bug wearing a
|
||||
## smaller number.
|
||||
const RELEASE := 0.4
|
||||
|
||||
## Only the left stick. The triggers are axes too, and latching them here would
|
||||
## silently swallow input the port does not read yet but might.
|
||||
const STICK := [JOY_AXIS_LEFT_X, JOY_AXIS_LEFT_Y]
|
||||
|
||||
var _latched: Dictionary = {}
|
||||
|
||||
|
||||
## Add the joypad buttons the built-in map omits. Returns a human-readable line,
|
||||
## or "" if nothing needed adding — so a future Godot that ships these bindings
|
||||
## makes this quietly stop reporting rather than double-binding.
|
||||
static func bind_missing() -> String:
|
||||
var added := PackedStringArray()
|
||||
for pair in [["ui_accept", JOY_BUTTON_A, "Ⓐ"], ["ui_cancel", JOY_BUTTON_B, "Ⓑ"]]:
|
||||
var action: String = pair[0]
|
||||
var button: int = pair[1]
|
||||
if not InputMap.has_action(action):
|
||||
# Not a warning we can act on, but silence here would present as the
|
||||
# original bug and send the next person back to the controller.
|
||||
push_warning("gamepad: no such action %s -- pad button unbound" % action)
|
||||
continue
|
||||
if _has_button(action, button):
|
||||
continue
|
||||
var ev := InputEventJoypadButton.new()
|
||||
ev.button_index = button
|
||||
InputMap.action_add_event(action, ev)
|
||||
added.append("%s -> %s" % [pair[2], action])
|
||||
if added.is_empty():
|
||||
return ""
|
||||
return "pad: bound %s (Godot 4.7.2 binds no joypad button to either)" % \
|
||||
", ".join(added)
|
||||
|
||||
|
||||
static func _has_button(action: String, button: int) -> bool:
|
||||
for e in InputMap.action_get_events(action):
|
||||
if e is InputEventJoypadButton and e.button_index == button:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
## True if this event should be acted on. Everything that is already an edge —
|
||||
## keys, d-pad, mouse — passes straight through; only the analog stick is
|
||||
## latched, and only on the two axes the navigation actions are bound to.
|
||||
func accepts(event: InputEvent) -> bool:
|
||||
if not (event is InputEventJoypadMotion):
|
||||
return true
|
||||
var axis: int = event.axis
|
||||
if not STICK.has(axis):
|
||||
return true
|
||||
var value: float = event.axis_value
|
||||
var direction := 0
|
||||
if value >= ENTER:
|
||||
direction = 1
|
||||
elif value <= -ENTER:
|
||||
direction = -1
|
||||
|
||||
if direction == 0:
|
||||
# Neutral enough to re-arm? The gap between RELEASE and ENTER is the
|
||||
# hysteresis band: inside it the stick is neither a new press nor
|
||||
# released, so the latch is left exactly as it was.
|
||||
if absf(value) <= RELEASE:
|
||||
_latched[axis] = 0
|
||||
return false
|
||||
if int(_latched.get(axis, 0)) == direction:
|
||||
return false # still held in the same direction: not a new press
|
||||
_latched[axis] = direction
|
||||
return true
|
||||
|
||||
|
||||
## The pads Godot can see, for the startup line. A run where the human believes
|
||||
## a controller is connected and Godot disagrees should say so on its own,
|
||||
## rather than presenting as unresponsive buttons.
|
||||
static func report_devices() -> String:
|
||||
var pads := Input.get_connected_joypads()
|
||||
if pads.is_empty():
|
||||
return "pad: none connected -- keyboard only (Enter/Space = Ⓐ, Escape = Ⓑ)"
|
||||
var names := PackedStringArray()
|
||||
for j in pads:
|
||||
names.append("[%d] %s" % [j, Input.get_joy_name(j)])
|
||||
return "pad: " + ", ".join(names)
|
||||
1
port/scripts/gamepad.gd.uid
Normal file
1
port/scripts/gamepad.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b34gxuqrvncbp
|
||||
@@ -66,6 +66,15 @@ step transcode-bands must-pass tools/port/verify-transcode-fidelity
|
||||
step bands-harness must-pass tools/port/verify-transcode-fidelity --selftest
|
||||
step capture-controls must-pass tools/port/check-capture-controls
|
||||
step menu-audio must-pass env OUT="$OUT/audio" tools/port/verify-menu-audio
|
||||
# 🔴 ADDED 2026-09-01 after a human found Ⓐ dead on a real controller while the
|
||||
# unattended P5 walk passed. `--script` sends `InputEventAction`, which BYPASSES
|
||||
# the input map, so every check here asserted the code BELOW the map and nothing
|
||||
# about the map -- which was missing a joypad binding for `ui_accept` and
|
||||
# `ui_cancel` entirely. The same blind spot hid a second defect: an
|
||||
# `InputEventAction` is not an analog axis, so nothing could see that a held
|
||||
# stick fired once per jitter.
|
||||
step input-map must-pass tools/port/verify-input
|
||||
step input-control must-pass tools/port/verify-input --control
|
||||
# A stale index is worse than none: it answers "is this already decided?" with a
|
||||
# confident no. That is not hypothetical -- see the entry it was built after.
|
||||
step decisions-index must-pass tools/port/index-decisions --check
|
||||
|
||||
212
tools/port/verify-input
Executable file
212
tools/port/verify-input
Executable file
@@ -0,0 +1,212 @@
|
||||
#!/usr/bin/env bash
|
||||
# The input map, and the stick latch -- asserted against Godot, not reasoned about.
|
||||
#
|
||||
# tools/port/verify-input
|
||||
# tools/port/verify-input --control # each check fails when its subject is removed
|
||||
#
|
||||
# 🔴 WHY THIS EXISTS. A human played the port on a real controller and Ⓐ did
|
||||
# nothing. Skipping the intro did nothing; opening a submenu did nothing. The
|
||||
# unattended P5 walk had passed on every iteration while this was true, and the
|
||||
# reason is exact:
|
||||
#
|
||||
# `--script` sends `InputEventAction`, which BYPASSES the input map.
|
||||
#
|
||||
# So the harness asserted every line of code *after* the input map and nothing
|
||||
# about the map itself -- and the map was missing half the actions. Godot 4.7.2
|
||||
# binds NO joypad button to `ui_accept` or `ui_cancel`, while it binds the d-pad
|
||||
# AND the left stick to `ui_up`/`ui_down`. Four actions worked on the pad, two
|
||||
# did not, which reads as a broken controller.
|
||||
#
|
||||
# The second defect had the same blind spot: `InputEventAction` is not an analog
|
||||
# axis, so the harness could not have seen that a held stick fires once per
|
||||
# jitter. The human's words were "moves the cursor too fast".
|
||||
#
|
||||
# ⚠️ THE GENERAL LESSON, worth more than either fix: **a synthetic-input test
|
||||
# cannot assert the input map.** Anything injected below the map is evidence
|
||||
# about the code above it only.
|
||||
#
|
||||
# ## The control, and what it can and cannot cover
|
||||
#
|
||||
# 🔴 The first version of `--control` inverted ALL NINE assertions and demanded
|
||||
# every one fail with the fixup skipped. Seven of them do not depend on the
|
||||
# fixup, so it reported them as broken -- a control that fails a correct check
|
||||
# is the same defect as one that passes a dead check, and this file would have
|
||||
# shipped claiming its checks were untrustworthy. Each check now names its
|
||||
# SUBJECT, and the control removes exactly that subject:
|
||||
#
|
||||
# bind -- skip `Gamepad.bind_missing()`; the check must fail
|
||||
# latch -- run the same events through no latch at all; the count must differ
|
||||
# godot -- NOT CONTROLLABLE HERE, and said so rather than faked. These assert
|
||||
# what Godot itself binds. There is nothing of ours to remove; they
|
||||
# exist to make a future Godot dropping the d-pad a failing check
|
||||
# instead of a bug report.
|
||||
set -euo pipefail
|
||||
cd "${PROJECT_DIR:-$(git rev-parse --show-toplevel)}"
|
||||
GODOT="${GODOT:-godot}"
|
||||
mode="assert"
|
||||
[ "${1:-}" = "--control" ] && mode="control"
|
||||
|
||||
probe="port/.verify-input-probe.gd"
|
||||
trap 'rm -f "$probe" "${probe}.uid"' EXIT INT TERM
|
||||
|
||||
cat > "$probe" <<'GD'
|
||||
extends SceneTree
|
||||
|
||||
var mode := OS.get_environment("VERIFY_INPUT_MODE")
|
||||
var fail := 0
|
||||
var ran := 0
|
||||
|
||||
## `subject` is what the check depends on, and decides whether the control
|
||||
## removes it. A check whose subject cannot be removed is skipped there and
|
||||
## counted, not silently dropped -- a control that quietly tests four of nine
|
||||
## things reports the same green line as one that tests all nine.
|
||||
func ok(name: String, subject: String, cond: bool, detail: String = "") -> void:
|
||||
if mode == "control" and subject == "godot":
|
||||
print(" %-44s -- not controllable (Godot's own binding)" % name)
|
||||
return
|
||||
if mode == "control" and subject == "negative":
|
||||
# 🔴 R4: a NEGATIVE carries a positive control, it does not carry an
|
||||
# inversion. "The latch must not touch buttons" cannot be controlled by
|
||||
# removing the latch -- with no latch, buttons pass, which is the same
|
||||
# answer. What shows the method has power is that the SAME counter, on
|
||||
# the same code path, reduces 6 stick events to 1. That row is the
|
||||
# positive control for this one, and naming it is the honest move;
|
||||
# inverting it would have been a green line that meant nothing.
|
||||
print(" %-44s -- negative; positive control is the stick row (6 -> 1)"
|
||||
% name)
|
||||
return
|
||||
ran += 1
|
||||
var want: bool = cond if mode != "control" else not cond
|
||||
print(" %-44s %s%s" % [name, "ok" if want else "🔴 FAILED",
|
||||
(" " + detail) if detail != "" else ""])
|
||||
if not want:
|
||||
fail = 1
|
||||
|
||||
func has_button(action: String, button: int) -> bool:
|
||||
for e in InputMap.action_get_events(action):
|
||||
if e is InputEventJoypadButton and e.button_index == button:
|
||||
return true
|
||||
return false
|
||||
|
||||
## Feed a run of axis values through a latch (or through none) and count the
|
||||
## presses it would produce.
|
||||
func steps(values: Array, latched: bool) -> int:
|
||||
var pad := Gamepad.new()
|
||||
var n := 0
|
||||
for v: float in values:
|
||||
var e := InputEventJoypadMotion.new()
|
||||
e.axis = JOY_AXIS_LEFT_Y
|
||||
e.axis_value = v
|
||||
# No latch = what the port did before: every event above the action
|
||||
# deadzone is a press. That is the bug, reproduced, as the control.
|
||||
if pad.accepts(e) if latched else absf(v) >= Gamepad.ENTER:
|
||||
n += 1
|
||||
return n
|
||||
|
||||
## The latch as the port actually uses it -- and REMOVED under `--control`, so
|
||||
## the rows that depend on it invert.
|
||||
func nav(values: Array) -> int:
|
||||
return steps(values, mode != "control")
|
||||
|
||||
func _init() -> void:
|
||||
# The control removes the repair. Everything else runs with it applied.
|
||||
if mode != "control":
|
||||
Gamepad.bind_missing()
|
||||
|
||||
# ── 1. subject `bind` -- the two actions Godot leaves unbound ─────────────
|
||||
ok("Ⓐ reaches ui_accept", "bind", has_button("ui_accept", JOY_BUTTON_A),
|
||||
"JOY_BUTTON_A")
|
||||
ok("Ⓑ reaches ui_cancel", "bind", has_button("ui_cancel", JOY_BUTTON_B),
|
||||
"JOY_BUTTON_B")
|
||||
|
||||
# ── 2. subject `godot` -- what the engine binds, and must keep binding ────
|
||||
#
|
||||
# The keyboard events must SURVIVE the fixup: declaring `ui_accept` in
|
||||
# project.godot would have replaced the built-in wholesale and dropped them
|
||||
# silently. Adding to the action must not.
|
||||
var keys := 0
|
||||
for e in InputMap.action_get_events("ui_accept"):
|
||||
if e is InputEventKey:
|
||||
keys += 1
|
||||
ok("ui_accept keeps its keyboard events", "godot", keys >= 2,
|
||||
"%d key event(s)" % keys)
|
||||
ok("d-pad reaches ui_down", "godot", has_button("ui_down", JOY_BUTTON_DPAD_DOWN))
|
||||
var axis := false
|
||||
for e in InputMap.action_get_events("ui_down"):
|
||||
if e is InputEventJoypadMotion and e.axis == JOY_AXIS_LEFT_Y:
|
||||
axis = true
|
||||
ok("left stick reaches ui_down", "godot", axis, "axis %d" % JOY_AXIS_LEFT_Y)
|
||||
|
||||
# ── 3. subject `latch` -- one step per deflection, not one per jitter ─────
|
||||
#
|
||||
# A push to full deflection followed by jitter that never returns to
|
||||
# neutral: what a real stick emits, and what produced "moves the cursor too
|
||||
# fast". The control runs the identical values with no latch and must count
|
||||
# every one of them, which is what makes this a discriminator rather than a
|
||||
# number that happens to be 1.
|
||||
var held := [0.92, 0.95, 0.91, 0.99, 0.93, 0.97]
|
||||
# `nav()` is the latch under control: in `--control` the latch is REMOVED,
|
||||
# which is what makes these rows invert. Reading `steps(..., true)` in both
|
||||
# modes was the earlier defect -- the control ran the repaired code and then
|
||||
# demanded it fail.
|
||||
ok("a held stick is ONE step, not six", "latch", nav(held) == 1,
|
||||
"latched %d, unlatched %d" % [steps(held, true), steps(held, false)])
|
||||
|
||||
# Release, then push again: that IS a second press, or the stick becomes
|
||||
# single-use.
|
||||
ok("release then push is a second step", "latch",
|
||||
nav([0.92, 0.95, 0.10, 0.88]) == 2,
|
||||
"%d step(s)" % nav([0.92, 0.95, 0.10, 0.88]))
|
||||
|
||||
# Hysteresis: drifting back only as far as the release threshold must not
|
||||
# re-arm, or a stick resting near the boundary chatters -- the original bug
|
||||
# with a smaller number.
|
||||
ok("boundary drift does not re-arm", "latch",
|
||||
nav([0.9, 0.45, 0.9, 0.45, 0.9]) == 1,
|
||||
"%d step(s)" % nav([0.9, 0.45, 0.9, 0.45, 0.9]))
|
||||
|
||||
# A button already IS an edge; latching it would swallow the second of two
|
||||
# quick taps.
|
||||
var pad := Gamepad.new()
|
||||
var passed := 0
|
||||
for i in 3:
|
||||
var b := InputEventJoypadButton.new()
|
||||
b.button_index = JOY_BUTTON_DPAD_DOWN
|
||||
b.pressed = true
|
||||
if pad.accepts(b):
|
||||
passed += 1
|
||||
ok("d-pad presses are not latched", "negative", passed == 3, "%d of 3" % passed)
|
||||
|
||||
if ran == 0:
|
||||
print("🔴 no check ran -- the harness asserted nothing")
|
||||
quit(2)
|
||||
quit(fail)
|
||||
GD
|
||||
|
||||
out=$(VERIFY_INPUT_MODE="$mode" "$GODOT" --headless --path port \
|
||||
--script "res://$(basename "$probe")" 2>&1 \
|
||||
| grep -v "^Godot Engine\|^$" || true)
|
||||
rc=0
|
||||
printf '%s' "$out" | grep -q "🔴" && rc=1
|
||||
|
||||
if [ "$mode" = "control" ]; then
|
||||
echo "control -- each check must fail when ITS OWN subject is removed:"
|
||||
printf '%s\n' "$out"
|
||||
echo
|
||||
if [ $rc -eq 0 ]; then
|
||||
echo "every controllable check fails without its subject -- the control holds"
|
||||
exit 0
|
||||
fi
|
||||
echo "🔴 a check did not invert -- it is not testing what it claims to test"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "input map and stick latch:"
|
||||
printf '%s\n' "$out"
|
||||
echo
|
||||
if [ $rc -eq 0 ]; then
|
||||
echo "Ⓐ and Ⓑ reach the game, and a held stick is one step"
|
||||
exit 0
|
||||
fi
|
||||
echo "🔴 the input map is not what the port needs"
|
||||
exit 1
|
||||
Reference in New Issue
Block a user