// 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 // // 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= also appends each tick's picture to -- screen // RAM, color 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 #include #include #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, color, 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 behavior-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, color 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 \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; }