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:
co-authored by
Claude Opus 5
parent
88ecb208f4
commit
fed1453e6e
@@ -0,0 +1,94 @@
|
||||
// pad.c
|
||||
// Game controllers for the Voyager.
|
||||
// Written by Anachronaut
|
||||
|
||||
#include "pad.h"
|
||||
#include "video.h"
|
||||
|
||||
// What each pad is holding, and where each one gets it from.
|
||||
static uint8_t held[PAD_COUNT];
|
||||
static FILE *recorded[PAD_COUNT];
|
||||
static uint8_t live[PAD_COUNT];
|
||||
|
||||
// ---- The frame the recordings advance on ----
|
||||
//
|
||||
// The screen's frame, and it is the same one on purpose: a game reads its pad once a frame
|
||||
// because that is when it draws, so a byte a frame is a byte a poll for anything written the
|
||||
// ordinary way - without making it a byte a READ, which would answer a game that asked twice
|
||||
// differently from one that asked once.
|
||||
//
|
||||
// On the machine's clock, so a recording plays back the same over the same cycles however
|
||||
// fast the host ran.
|
||||
static unsigned long lastFrame;
|
||||
static int started;
|
||||
|
||||
void padReset(void) {
|
||||
for (int n = 0; n < PAD_COUNT; n++) {
|
||||
held[n] = 0;
|
||||
live[n] = 0;
|
||||
// The files are NOT closed or forgotten. They were named on the command line and
|
||||
// outlive a reset, the same as a disk image does: a machine that restarted itself
|
||||
// and lost its controllers would be a strange thing to debug.
|
||||
}
|
||||
lastFrame = 0;
|
||||
started = 0;
|
||||
}
|
||||
|
||||
void padFromFile(int which, FILE *file) {
|
||||
if (which < 0 || which >= PAD_COUNT) {
|
||||
return;
|
||||
}
|
||||
recorded[which] = file;
|
||||
}
|
||||
|
||||
void padSet(int which, uint8_t heldNow) {
|
||||
if (which < 0 || which >= PAD_COUNT) {
|
||||
return;
|
||||
}
|
||||
live[which] = heldNow;
|
||||
}
|
||||
|
||||
void padTick(unsigned long now) {
|
||||
// The first tick sets the clock rather than counting a frame from nought, or a machine
|
||||
// that started late would take a run of bytes all at once.
|
||||
if (!started) {
|
||||
lastFrame = now;
|
||||
started = 1;
|
||||
}
|
||||
while (now - lastFrame >= VIDEO_FRAME_CYCLES) {
|
||||
lastFrame += VIDEO_FRAME_CYCLES;
|
||||
for (int n = 0; n < PAD_COUNT; n++) {
|
||||
if (recorded[n] == NULL) {
|
||||
continue;
|
||||
}
|
||||
const int byte = fgetc(recorded[n]);
|
||||
// ---- The end of a recording is nothing held ----
|
||||
//
|
||||
// Not a pad that vanishes and not the last frame repeating for ever. A recording
|
||||
// that ran out and left a direction pressed would send whatever it was driving
|
||||
// off the edge of the world long after the test meant to stop.
|
||||
held[n] = (byte == EOF) ? 0 : (uint8_t)byte;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t padRead(uint8_t port) {
|
||||
if (port == PAD_PRESENT) {
|
||||
uint8_t there = 0;
|
||||
for (int n = 0; n < PAD_COUNT; n++) {
|
||||
if (recorded[n] != NULL) {
|
||||
there |= (uint8_t)(1u << n);
|
||||
}
|
||||
}
|
||||
return there;
|
||||
}
|
||||
const int which = port - PORT_PAD;
|
||||
if (which < 0 || which >= PAD_COUNT) {
|
||||
// Everything else in the block is reserved and reads as nothing, which is what a
|
||||
// port block being kept for later should do.
|
||||
return 0;
|
||||
}
|
||||
// A recording wins over a live pad, so a test is not at the mercy of whatever somebody
|
||||
// is leaning on while it runs.
|
||||
return (recorded[which] != NULL) ? held[which] : live[which];
|
||||
}
|
||||
Reference in New Issue
Block a user