Files
Sylpheed/tools/port/verify-input
MechaCat02 72f116a3b4 port: Ⓐ was never bound to the pad, and the stick is not an edge
Both found by a human playing the port on a real controller. Both were
invisible to every check this port has, for one reason:

  `--script` sends InputEventAction, which BYPASSES the input map.

So the harness asserted every line of code AFTER the map and nothing about the
map itself. Measured on this Godot, not remembered -- the remembered answer 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+

Four actions worked on the pad and two did not, which presents as a broken
controller: navigation moved, Ⓐ skipped nothing and opened nothing. Godot
4.7.2 binds no joypad button to ui_accept or ui_cancel.

Gamepad.bind_missing() ADDS the two buttons to the built-in actions rather than
redefining them in project.godot, which would replace the built-ins wholesale
and drop the keyboard bindings silently.

Second defect, same blind spot: an InputEventAction is not an analog axis. The
left stick is bound to axis 1, and an axis is not an edge -- held at deflection
it emits an event per jitter, each reporting the action pressed. That was one
cursor step per jitter ("moves the cursor too fast"). The stick is now latched
to one step per deflection, with hysteresis so a stick resting near the
threshold does not chatter.

AUTHORED, and deliberately the conservative half: whether the game REPEATS a
held direction, and how fast, is an oracle question. One deflection one step
cannot run away and invents no rate. Logged as BLOCKED H1.

tools/port/verify-input asserts the map and the latch, with a control that
removes each check's OWN subject -- its first version inverted all nine
assertions when only two depended on the fixup, and reported seven correct
checks as broken. Three rows say plainly they are not controllable (they assert
Godot's own bindings) and one is a negative carrying a positive control (R4),
rather than faking an inversion for either.

Also logged BLOCKED H2, unguessed: the splash blur/fade-in is more pronounced
in the game than in the port. The port applies no blur at all. Noted there that
the two splashes are the only screens reaching the rest() plateau-less
fallback, which the R1 pass just re-opened in both directions.
2026-09-01 17:45:36 +02:00

213 lines
8.6 KiB
Bash
Executable File

#!/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