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
130 lines
4.6 KiB
C
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);
|
|
}
|