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