Controllers: four pads that say what is held

The console says WHICH KEY WENT DOWN, which is the right shape for typing
and the wrong one for playing. A game wants to know what is being held,
this frame, possibly several things at once, and a stream of presses
cannot say that: a key that is down and staying down sends nothing at all.
Lunar Porter's thrust is a burn per press for exactly that reason.

So a pad is its own device on ports 0x60 to 0x6F, reporting a LEVEL. One
read gives every button at once, holding is the natural thing to express,
two directions together cost nothing, and reading does not consume it - a
game may ask twice in a frame and be told the same thing both times.

Four of them, because a party is four. They cost a port each and nothing
at all when unused. The directions are the low nibble so "which way" is an
AND with 0x0F; the buttons are the high nibble for the same reason. 0x64
says which are really there, so a game can ask for a controller rather
than sitting silent while somebody presses things at it. They never
interrupt: a game polls once a frame because that is when it draws.

KEY-UP ON THE CONSOLE WAS THE OTHER WAY TO DO THIS AND WAS REJECTED. A
terminal hands over characters and can never report a key coming up
however it is asked, so it would have been a thing that worked behind a
window and silently did not down a wire. A separate device can honestly
say it is not there.

Voyager drives pad nought from the keyboard as well as from any real
controller, OR-ed rather than chosen between, so a game written for a pad
is playable on a machine with none and unplugging one mid-game does not
leave somebody holding nothing.

And a recorded path, which is what makes any of it testable: --pad names a
file of one byte a frame, and the manifest has an eighth column for it.
A BYTE A FRAME AND NOT A BYTE A READ - a level asked twice in one frame
has to answer the same both times, and a file that advanced per read would
depend on how the program happened to be written. Voyager's own tests run
headless with nobody holding anything, so without this the device would be
exercised only by somebody playing: the state the console's line editing
was in when it broke twice in two days.

0x50 is the timer, not free. The block this went in was chosen after
looking rather than before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
This commit is contained in:
Anachronaut
2026-09-02 21:44:55 -04:00
co-authored by Claude Opus 5
parent 88ecb208f4
commit fed1453e6e
16 changed files with 413 additions and 5 deletions
Binary file not shown.
Binary file not shown.
+13 -1
View File
@@ -4,7 +4,7 @@
# One test per line, fields separated by '|'. Blank lines and lines starting
# with '#' are ignored.
#
# name | source | mode | stdin | limit | disk | keys
# name | source | mode | stdin | limit | disk | keys | pad
#
# source is relative to Programs/. Everything assembles from there with
# Libraries/ on the include path, and the binary is written into Tests/build.
@@ -1000,6 +1000,18 @@ cosmosFlip | CosmOS/Source/cosmos.asm | run | cosmosFli
# the ball never moved at all - the whole 150 frames of it went past in the shell, reading a
# line made of nulls. Through a keyboard a null is silence, which is what makes it a wait.
cosmosSprite | CosmOS/Source/cosmos.asm | run | - | 60000000 | disks/cosmos.img | cosmosSprite.keys
# ---- A controller, which is a level and not an event ----
#
# The eighth column is a pad fixture: one byte a frame, each byte the buttons held during it.
# It exists for the same reason the keyboard fixture does - a device only a person can work is
# a device nothing checks - and it matters more here, because a pad is the ONE THING THE
# CONSOLE CANNOT DO. A terminal reports which key was pressed and can never report one coming
# up, so holding a direction, or holding two buttons at once, has no expression there at all.
#
# The recording presses up, holds it, adds right, lets go of up, lets go of everything, then
# presses A and B together. The program reads each frame twice to show that looking does not
# take it away.
padTest | testPrograms/padTest.asm | run | - | - | - | - | padTest.pad
# Which disk the registers mean. Several disks are one controller with a drive register
# rather than several devices, because a port is an immediate byte inside the instruction
# that names it and a program cannot compute one. Run with a single disk, so drive 1 is a
+16 -1
View File
@@ -171,7 +171,7 @@ uncolour() {
sed -i -E 's/\x1b\[[0-9;]*m//g' "$1"
}
while IFS='|' read -r name src mode stdin limit disk keys; do
while IFS='|' read -r name src mode stdin limit disk keys pad; do
name="$(trim "$name")"
[ -z "$name" ] && continue
case "$name" in \#*) continue ;; esac
@@ -179,8 +179,10 @@ while IFS='|' read -r name src mode stdin limit disk keys; do
mode="$(trim "$mode")"; stdin="$(trim "$stdin")"
limit="$(trim "$limit")"; disk="$(trim "$disk")"
keys="$(trim "${keys:-}")"
pad="$(trim "${pad:-}")"
[ -z "$disk" ] && disk="-"
[ -z "$keys" ] && keys="-"
[ -z "$pad" ] && pad="-"
wanted "$name" || continue
@@ -247,6 +249,19 @@ while IFS='|' read -r name src mode stdin limit disk keys; do
fi
EMUARGS+=(--keyboard "$INPUT/$keys")
fi
# ---- And a controller made of a file ----
#
# A pad reports what is HELD, and a suite has no hands. One byte a frame, which is
# what makes a device that only a person could otherwise exercise into one the
# recordings cover - the same argument as the keyboard fixture above.
if [ "$pad" != "-" ]; then
if [ ! -f "$INPUT/$pad" ]; then
FAIL=$((FAIL + 1)); FAILED_NAMES+=("$name")
report "FAIL" "$name" "missing pad fixture $pad"
continue
fi
EMUARGS+=(--pad "$INPUT/$pad")
fi
[ "$limit" != "-" ] && EMUARGS+=(--cycles "$limit")
# A disk starts fresh for every test, so a test cannot pass because of what a
# previous one left lying on it. How that is arranged differs between a scratch