Files
SplitBit-Emulator/Source/Emulator/pad.c
T
AnachronautandClaude Opus 5 a163c670d0 A demo recorder: --record-pad writes what --pad reads
One byte a frame, in exactly the format the player takes, so a recording
needs no conversion and there is no second format to keep in step. That
symmetry is the feature, and it makes the strongest form of the claim
testable: a recording is made OF a playback, and the bytes coming out have
to be the bytes that went in.

It exists because some inputs cannot sensibly be written by hand. Flying a
lander from one base to another is a few hundred frames of steering that
has to arrive somewhere eight cells wide, and several attempts at
authoring one got within two columns and no closer. That is a piloting
exercise rather than a test. Playing it once and keeping what happened is
the answer.

A BYTE FOR EVERY FRAME, written inside the loop that advances the
recordings rather than after it, so a machine that jumped several frames
at once still writes one for each. A recording is a timeline: one that
skipped the frames nobody looked at would play back faster than it was
flown.

What is recorded is what the DEVICE WOULD REPORT, not the live state - a
recording of a playback that wrote the live state would be a file of
noughts. And it is flushed as it goes, because a recording is usually
stopped by whoever is playing rather than by the program ending, and a
demo lost to a buffer is a demo flown twice.

Tests/replay.sh is where this and whatever follows it are checked. Twelve
scripts now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-03 12:38:43 -04:00

131 lines
4.6 KiB
C

// 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];
static int connected[PAD_COUNT];
static FILE *recording[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;
connected[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;
}
// What the device would report for this pad, which is what a recording has to hold: a
// recording of a playback that wrote the LIVE state would be a file of noughts.
static uint8_t effective(int which) {
return (recorded[which] != NULL) ? held[which] : live[which];
}
void padRecordTo(int which, FILE *file) {
if (which < 0 || which >= PAD_COUNT) {
return;
}
recording[which] = file;
}
void padFromFile(int which, FILE *file) {
if (which < 0 || which >= PAD_COUNT) {
return;
}
recorded[which] = file;
}
void padSet(int which, int isConnected, uint8_t heldNow) {
if (which < 0 || which >= PAD_COUNT) {
return;
}
connected[which] = isConnected;
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;
}
// ---- And a byte written for every frame that went by ----
//
// Inside the loop rather than after it, so a machine that jumped several frames at
// once still writes one byte for each of them. A recording is a TIMELINE, and one
// that skipped the frames nobody was looking at would play back faster than it was
// flown.
//
// Flushed as it goes, because a recording is usually stopped by whoever is playing
// rather than by the program ending, and a demo lost to a buffer would be a demo
// flown twice.
for (int n = 0; n < PAD_COUNT; n++) {
if (recording[n] != NULL) {
fputc(effective(n), recording[n]);
fflush(recording[n]);
}
}
}
}
uint8_t padRead(uint8_t port) {
if (port == PAD_PRESENT) {
uint8_t there = 0;
for (int n = 0; n < PAD_COUNT; n++) {
// A recording is a pad, and so is anything the front end says is plugged in.
// Counting only the recordings meant this said nought on the one machine that
// has real controllers, which is the only machine where the answer matters.
if (recorded[n] != NULL || connected[n]) {
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 effective(which);
}