Files
SplitBit-Emulator/Source/Emulator/pad.c
T
AnachronautandClaude Opus 5 de1857f5f7 Record every pad, not the one that happened to be first
The first recording ever made with this came back 1,766 frames of nothing.
It recorded pad NOUGHT and the controller was somewhere else - which pad
one lands on is an accident of the host, the same accident that made Lunar
Porter read all four in the first place - and a flight flown for the
purpose was lost to it.

So every pad is or-ed into the byte. A demo is a record of what somebody
DID, and on a machine one person is playing the number it arrived on is
not part of that. It plays back on pad nought, where --pad puts the first
file given, and any program that reads more than one pad reads them or-ed
anyway for exactly the same reason.

--record-pad takes one file now rather than filling pads in turn, because
there is nothing left for the second one to mean.

The check for it plays a recording on pad ONE with nought holding nothing
and requires the bytes back. That is the case that was missing: the round
trip was tested and passed, on pad nought, which is the only pad it could
not have gone wrong on.

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

130 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;
// ---- 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(FILE *file) {
recording = 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.
if (recording != NULL) {
uint8_t all = 0;
for (int n = 0; n < PAD_COUNT; n++) {
all |= effective(n);
}
fputc(all, recording);
fflush(recording);
}
}
}
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);
}