341 lines
12 KiB
C
341 lines
12 KiB
C
// stsim -- host-side driver for the Space Taxi simulation core.
|
|
//
|
|
// Loads an STL5 level, attaches a recorded demo input stream and runs
|
|
// the game tick, printing one line of state per tick -- exactly the line
|
|
// c64trace.py prints from the real game in VICE, so the two traces can
|
|
// be compared byte for byte.
|
|
//
|
|
// stsim <level.dat> <level letter> <ticks>
|
|
//
|
|
// H, W, T and X use the C64's own attract recordings; every other
|
|
// letter uses a synthesised gate fixture from gateStreams.h.
|
|
//
|
|
// A ride is the attract demo's input and random numbers on the screen as
|
|
// the 24-hour shift reaches it ($7215 = the screen's place, A = 1 .. Y =
|
|
// 25), on the game's crash path: a finished wreck costs a cab and the cab respawns
|
|
// (stSimLoseCab, stSimRespawn -- c64trace.py sends the real machine down
|
|
// $6B64 too), until the cabs run out, the cab leaves the screen, the
|
|
// ticks are done, or the playback reaches the buffer's last pair -- past
|
|
// that the C64 would read on into whatever follows $0B81.
|
|
//
|
|
// STSIM_DEMOBUF=1 prints the ride's whole playback buffer ($0902, the
|
|
// boot-time image with the stream laid over it) as one hex line instead
|
|
// of running it: c64trace.py loads exactly those bytes into the real
|
|
// game, so both machines ride the same input.
|
|
//
|
|
// STSIM_RAW=<file> also appends each tick's picture to <file> -- screen
|
|
// RAM, colour RAM and the effective charset, 1000 + 1000 + 2048 bytes a
|
|
// tick -- so a hash mismatch can be traced to the cells and glyphs that
|
|
// differ (c64trace.py writes the same layout from the real machine, and
|
|
// compare.py --picture lists the differences).
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "stSim.h"
|
|
#include "stCels.h"
|
|
#include "stDemoStreams.h"
|
|
#include "gateStreams.h"
|
|
|
|
|
|
#define ST_CHARSET_BYTES (ST_CHARSET_CHARS * 8u)
|
|
// The playback buffer's last (mask, count) pair: reading on from it, the
|
|
// sim stops at the buffer's end while the C64 reads past $0B81.
|
|
#define ST_DEMO_LAST_PAIR (ST_DEMO_BUFFER_BYTES - 2u)
|
|
|
|
|
|
static void celCensusReport(const StLevelT *level);
|
|
static void celCensusTick(const StSimT *sim);
|
|
static uint32_t charsetHash(const StSimT *sim);
|
|
static void charsetImage(const StSimT *sim, uint8_t *image);
|
|
static uint32_t hashBytes(const uint8_t *p, size_t n);
|
|
static void printDemoBuffer(const StSimT *sim);
|
|
static void printState(const StSimT *sim, uint32_t tick);
|
|
static void writeRaw(FILE *raw, const StSimT *sim);
|
|
|
|
|
|
static StLevelT gLevel;
|
|
static StSimT gSim;
|
|
|
|
// STSIM_CELS=1: every level cel (ptr below the baked set) a ride
|
|
// displays, as (ptr, colour, multi, mc0, mc1), against what
|
|
// stLevelCelList bakes for the level -- a cel shown but not listed is
|
|
// built and drawn interpreted mid-ride on the real ports.
|
|
#define CENSUS_MAX 64u
|
|
static StCelDefT gCensus[CENSUS_MAX];
|
|
static uint8_t gCensusCount;
|
|
|
|
|
|
void stAudioSfx(const uint8_t *program9) { (void)program9; }
|
|
void stAudioNoise(bool on) { (void)on; }
|
|
void stAudioThrustSweep(uint8_t value) { (void)value; }
|
|
void stAudioSpeech(uint8_t ch) { (void)ch; }
|
|
void stAudioSpeechPhrase(const uint8_t *speech) { (void)speech; }
|
|
void stAudioSilence(void) { }
|
|
void stAudioVoice2(uint8_t freqLo, uint8_t freqHi, uint8_t ctrl) { (void)freqLo; (void)freqHi; (void)ctrl; }
|
|
void stAudioVoice1Freq(uint8_t value) { (void)value; }
|
|
|
|
|
|
static void celCensusReport(const StLevelT *level) {
|
|
StCelDefT listed[ST_LEVEL_CELS_MAX];
|
|
uint8_t n = stLevelCelList(level, listed, ST_LEVEL_CELS_MAX);
|
|
uint8_t k;
|
|
uint8_t j;
|
|
|
|
for (k = 0u; k < gCensusCount; k++) {
|
|
for (j = 0u; j < n; j++) {
|
|
if (memcmp(&listed[j], &gCensus[k], sizeof(StCelDefT)) == 0) {
|
|
break;
|
|
}
|
|
}
|
|
fprintf(stderr, "CEL %s ptr=%02X color=%u multi=%u mc=%u/%u\n", (j < n) ? "listed " : "MISSING", gCensus[k].ptr, gCensus[k].color, gCensus[k].multi, gCensus[k].mc0, gCensus[k].mc1);
|
|
}
|
|
fprintf(stderr, "CENSUS shown=%u listed=%u\n", gCensusCount, n);
|
|
}
|
|
|
|
|
|
static void celCensusTick(const StSimT *sim) {
|
|
uint8_t i;
|
|
|
|
for (i = 1u; i < ST_HW_SPRITES; i++) {
|
|
StCelDefT d;
|
|
uint8_t k;
|
|
|
|
if ((sim->frame.enableMask & kStBit[i]) == 0u || sim->frame.ptr[i] >= ST_SPRITE_PTR_FIRST) {
|
|
continue;
|
|
}
|
|
// A pointer with no bitmap (a hook enables a sprite a tick before
|
|
// aiming it) draws nothing on every port -- not a cel.
|
|
if (stSimSpriteBitmap(sim, sim->frame.ptr[i]) == 0) {
|
|
continue;
|
|
}
|
|
d.ptr = sim->frame.ptr[i];
|
|
d.color = sim->frame.color[i];
|
|
d.multi = (sim->frame.multiMask & kStBit[i]) != 0u ? 1u : 0u;
|
|
d.mc0 = d.multi ? sim->spriteMc0 : 0u;
|
|
d.mc1 = d.multi ? sim->spriteMc1 : 0u;
|
|
for (k = 0u; k < gCensusCount; k++) {
|
|
if (memcmp(&gCensus[k], &d, sizeof(d)) == 0) {
|
|
break;
|
|
}
|
|
}
|
|
if (k == gCensusCount && gCensusCount < CENSUS_MAX) {
|
|
gCensus[gCensusCount++] = d;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// The charset the game would be showing. The sim no longer holds a
|
|
// contiguous 2 KB copy -- edited glyphs live in an override table -- so
|
|
// rebuild the effective image here. Hashing this keeps the trace value
|
|
// identical to the pre-override captures, which is what lets the goldens
|
|
// prove the change is behaviour-preserving.
|
|
static uint32_t charsetHash(const StSimT *sim) {
|
|
uint8_t image[ST_CHARSET_BYTES];
|
|
|
|
charsetImage(sim, image);
|
|
return hashBytes(image, sizeof(image));
|
|
}
|
|
|
|
|
|
static void charsetImage(const StSimT *sim, uint8_t *image) {
|
|
uint16_t ch;
|
|
|
|
for (ch = 0u; ch < ST_CHARSET_CHARS; ch++) {
|
|
memcpy(&image[ch * 8u], stSimGlyph(sim, (uint8_t)ch), 8u);
|
|
}
|
|
}
|
|
|
|
|
|
// FNV-1a over a byte span: the picture the player actually sees. The
|
|
// hooks that animate the screen (O, R, V, W, X) change screen, colour or
|
|
// charset bytes and nothing else this line used to capture.
|
|
static uint32_t hashBytes(const uint8_t *p, size_t n) {
|
|
uint32_t h = 2166136261u;
|
|
size_t i;
|
|
|
|
for (i = 0u; i < n; i++) {
|
|
h = (h ^ p[i]) * 16777619u;
|
|
}
|
|
return h;
|
|
}
|
|
|
|
|
|
static void printDemoBuffer(const StSimT *sim) {
|
|
size_t k;
|
|
|
|
printf("{\"demobuf\":\"");
|
|
for (k = 0u; k < sizeof(sim->demoBuf); k++) {
|
|
printf("%02X", sim->demoBuf[k]);
|
|
}
|
|
printf("\"}\n");
|
|
}
|
|
|
|
|
|
static void printState(const StSimT *sim, uint32_t tick) {
|
|
bool shown1 = (sim->frame.enableMask & kStBit[1]) != 0u;
|
|
uint8_t k;
|
|
|
|
printf("{\"tick\":%u,\"x\":\"%02X%02X.%02X\",\"y\":\"%02X.%02X\",\"vx\":\"%04X\",\"vy\":\"%04X\","
|
|
"\"pad\":%u,\"st\":%u,\"ph\":%u,\"in\":\"%02X\",\"dir\":\"%02X\",\"ptr0\":\"%02X\",",
|
|
tick, sim->posXmsb, sim->posXcol, sim->posXlo, sim->posYrow, sim->posYlo,
|
|
(uint16_t)sim->velX, (uint16_t)sim->velY, sim->activePad, sim->stage, sim->collisionPhase,
|
|
sim->inputMask, sim->dirMask, sim->spr[0].ptr);
|
|
// A hidden sprite's registers are not part of the picture, and the
|
|
// C64 leaves whatever the screen before set in them: sprite 1's
|
|
// shadow and every hidden sprite's displayed position print as
|
|
// dashes, on the real machine's trace too.
|
|
if (shown1) {
|
|
printf("\"ptr1\":\"%02X\",\"s1\":\"%02X%02X.%02X\",", sim->spr[1].ptr, sim->spr[1].msb, sim->spr[1].col, sim->spr[1].row);
|
|
} else {
|
|
printf("\"ptr1\":\"--\",\"s1\":\"--\",");
|
|
}
|
|
printf("\"en\":\"%02X\",\"t4740\":\"%02X\",\"off\":%u,\"rng\":\"%02X%02X\","
|
|
"\"fuel\":%u,\"slots\":%u,\"asi\":%u,\"score\":\"",
|
|
sim->frame.enableMask, sim->demoTimer, sim->demoOff, sim->rngT1, sim->rngT2,
|
|
sim->fuelCells, sim->fareSlotCount, sim->activeSpriteIdx);
|
|
for (k = 0u; k < ST_NUMBER_CHARS; k++) {
|
|
printf("%02X", sim->screen[ST_CELL_SCORE + k]);
|
|
}
|
|
printf("\",\"fare\":\"");
|
|
for (k = 0u; k < ST_NUMBER_CHARS; k++) {
|
|
printf("%02X", sim->screen[ST_CELL_FARE + k]);
|
|
}
|
|
printf("\",\"fuelc\":\"");
|
|
for (k = 0u; k < ST_FUEL_CELLS; k++) {
|
|
printf("%02X", sim->screen[ST_CELL_FUEL + k]);
|
|
}
|
|
printf("\",\"sprx\":\"");
|
|
for (k = 0u; k < ST_HW_SPRITES; k++) {
|
|
if ((sim->frame.enableMask & kStBit[k]) != 0u) {
|
|
printf("%03X%02X", sim->frame.x[k], sim->frame.y[k]);
|
|
} else {
|
|
printf("-----");
|
|
}
|
|
}
|
|
printf("\",\"scr\":\"%08X\",\"col\":\"%08X\",\"chr\":\"%08X\",\"bd\":\"%02X\",\"bg\":\"%02X\"}\n",
|
|
hashBytes(sim->screen, ST_SCREEN_CELLS),
|
|
hashBytes(sim->color, ST_SCREEN_CELLS),
|
|
charsetHash(sim),
|
|
sim->borderColor, sim->bgColor);
|
|
}
|
|
|
|
|
|
// STSIM_RAW: one tick's picture (see the top of the file).
|
|
static void writeRaw(FILE *raw, const StSimT *sim) {
|
|
uint8_t image[ST_CHARSET_BYTES];
|
|
|
|
charsetImage(sim, image);
|
|
fwrite(sim->screen, 1u, ST_SCREEN_CELLS, raw);
|
|
fwrite(sim->color, 1u, ST_SCREEN_CELLS, raw);
|
|
fwrite(image, 1u, sizeof(image), raw);
|
|
}
|
|
|
|
|
|
int main(int argc, char **argv) {
|
|
FILE *fp;
|
|
FILE *raw = NULL;
|
|
const char *rawPath;
|
|
const uint8_t *stream = kDemoStreamH;
|
|
uint16_t len = (uint16_t)sizeof(kDemoStreamH);
|
|
uint8_t want;
|
|
uint32_t ticks;
|
|
uint32_t t;
|
|
|
|
if (argc < 4) {
|
|
fprintf(stderr, "usage: stsim <level.dat> <level letter> <ticks>\n");
|
|
return 2;
|
|
}
|
|
fp = fopen(argv[1], "rb");
|
|
if (fp == NULL || !stLevelParse(&gLevel, fp)) {
|
|
fprintf(stderr, "stsim: cannot load %s\n", argv[1]);
|
|
return 1;
|
|
}
|
|
fclose(fp);
|
|
want = (uint8_t)argv[2][0];
|
|
switch (want) {
|
|
case 'W': stream = kDemoStreamW; len = (uint16_t)sizeof(kDemoStreamW); break;
|
|
case 'T': stream = kDemoStreamT; len = (uint16_t)sizeof(kDemoStreamT); break;
|
|
case 'X': stream = kDemoStreamX; len = (uint16_t)sizeof(kDemoStreamX); break;
|
|
case 'H': break;
|
|
default: {
|
|
// A synthesised gate fixture for a level the C64 has no
|
|
// attract recording of (see gateStreams.h).
|
|
uint8_t g;
|
|
for (g = 0u; g < ST_GATE_STREAMS; g++) {
|
|
if (kGateStreams[g].level == want) {
|
|
stream = kGateStreams[g].stream;
|
|
len = kGateStreams[g].length;
|
|
break;
|
|
}
|
|
}
|
|
if (g == ST_GATE_STREAMS) {
|
|
fprintf(stderr, "stsim: no stream for '%c'\n", (char)want);
|
|
return 2;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
ticks = (uint32_t)strtoul(argv[3], NULL, 0);
|
|
// A fresh one-player demo, the flame parity, wave index and walk
|
|
// parity at zero (c64trace.py zeroes them on the real machine too).
|
|
// The recorder starts its rides the same way.
|
|
stSimNewGame(&gSim, 1u, true);
|
|
stSimSeedDemoBuffer(&gSim, kDemoBufferInit, ST_DEMO_BUFFER_BYTES);
|
|
// $61FB, as startDemoScreen runs it: the turn wraps to player 0 and
|
|
// the screens-played count ($7215, this one included) is set before
|
|
// the screen is loaded -- to the screen's place in the 24-hour shift
|
|
// (A is the first, Y the 25th), so the rides cover the takeoff fare of
|
|
// every screen after the second and the finished shift's HUD.
|
|
(void)stSimAdvancePlayer(&gSim);
|
|
gSim.levelState = (uint8_t)(gLevel.levelIndex + 1u);
|
|
stSimEnterLevel(&gSim, &gLevel);
|
|
stSimSetDemoStream(&gSim, stream, len);
|
|
if (getenv("STSIM_DEMOBUF") != NULL) {
|
|
printDemoBuffer(&gSim);
|
|
return 0;
|
|
}
|
|
rawPath = getenv("STSIM_RAW");
|
|
if (rawPath != NULL) {
|
|
raw = fopen(rawPath, "wb");
|
|
if (raw == NULL) {
|
|
fprintf(stderr, "stsim: cannot write %s\n", rawPath);
|
|
return 1;
|
|
}
|
|
}
|
|
for (t = 0u; t < ticks; t++) {
|
|
StTickResultE r;
|
|
printState(&gSim, t);
|
|
if (raw != NULL) {
|
|
writeRaw(raw, &gSim);
|
|
}
|
|
r = stSimTick(&gSim);
|
|
if (getenv("STSIM_CELS") != NULL) {
|
|
celCensusTick(&gSim);
|
|
}
|
|
if (r == ST_TICK_CRASH_DONE) {
|
|
r = stSimLoseCab(&gSim);
|
|
if (r == ST_TICK_LIFE_LOST) {
|
|
stSimRespawn(&gSim);
|
|
r = ST_TICK_CONTINUE;
|
|
}
|
|
}
|
|
if (r != ST_TICK_CONTINUE) {
|
|
printf("{\"tick\":%u,\"end\":%d}\n", t, (int)r);
|
|
break;
|
|
}
|
|
if (gSim.demoOff >= ST_DEMO_LAST_PAIR) {
|
|
printf("{\"tick\":%u,\"end\":\"stream\"}\n", t);
|
|
break;
|
|
}
|
|
}
|
|
if (getenv("STSIM_CELS") != NULL) {
|
|
celCensusReport(&gLevel);
|
|
}
|
|
if (raw != NULL) {
|
|
fclose(raw);
|
|
}
|
|
return 0;
|
|
}
|