// Space Taxi -- the game tick ($5F40 main loop), physics, landing, // collision, crash sequence, fuel and HUD arithmetic. // // Every function names the original routine it mirrors. The order of // calls inside stSimTick is the order of the JSRs in the C64 main // loop; the two raster waits that split it are where the VIC latched // the frame, so the sprite snapshot (marshal) and the collision test // sit at the same places. #include #include "stSim.h" // Screen-code strings the engine writes (all ASCII-compatible glyphs). static const uint8_t kTextBlank[] = { 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0 }; // $6C38 static const uint8_t kHudTemplate[7] = { 0x66, 0x66, 0x66, 0x74, 0x77, 0x74, 0x74 }; // $43B1 // Fuel-pump chirp frequencies while refuelling ($6DFA, indexed by the // mod-8 pump tick 1..4). static const uint8_t kPumpFreq[5] = { 0x04, 0x05, 0x07, 0x0A, 0x10 }; static void applyVelocityX(StSimT *sim); static void applyVelocityY(StSimT *sim); static uint32_t backgroundRowMask(const StSimT *sim, int16_t px, int16_t py); static void bitScrollHatch(StSimT *sim); static void collisionDispatch(StSimT *sim); static void collisionPhase1(StSimT *sim); static void collisionPhase2(StSimT *sim); static StTickResultE collisionPhase3(StSimT *sim); static void computeCollisions(StSimT *sim); static void crashStart(StSimT *sim); static void drawCabIcons(StSimT *sim); static void drawScreensCount(StSimT *sim); static void edgeReflect(StSimT *sim); static void fireButtonEdge(StSimT *sim); static void fuelBarHud(StSimT *sim); static void fuelTick(StSimT *sim); static void hudDraw(StSimT *sim); static void hudInit(StSimT *sim); static void landedHandler(StSimT *sim); static bool levelEndCheck(const StSimT *sim); static void marshal(StSimT *sim); static void padDetect(StSimT *sim); static void padLandingBob(StSimT *sim); static void passengerArrTick(StSimT *sim); static void physicsTick(StSimT *sim); static uint8_t readInput(StSimT *sim); static void markCell(StSimT *sim, uint16_t cell); static void setCell(StSimT *sim, uint16_t cell, uint8_t ch); static void setColorCell(StSimT *sim, uint16_t cell, uint8_t color); static void spriteMasks(StSimT *sim, uint8_t idx); static void spritesOffReset(StSimT *sim); static void taxiSpawnInit(StSimT *sim); static void taxiSpriteCelSelect(StSimT *sim); static uint8_t validateDigit(uint8_t ch); // $6112 -- add the X velocity to the 17-bit X position (msb:col:frac) // and refresh the sprite-0 column shadow. static void applyVelocityX(StSimT *sim) { uint16_t pos = (uint16_t)(((uint16_t)sim->posXcol << 8) | sim->posXlo); uint32_t sum = (uint32_t)pos + (uint16_t)sim->velX; uint8_t msb = (uint8_t)(sim->posXmsb + (uint8_t)(sum >> 16)); if (sim->velX < 0) { msb++; } sim->posXmsb = (uint8_t)(msb & 1u); sim->posXlo = (uint8_t)sum; sim->posXcol = (uint8_t)(sum >> 8); sim->spr[0].msb = sim->posXmsb; sim->spr[0].col = sim->posXcol; } // $6145 -- Y: velocity += accel + gravity, position += velocity. static void applyVelocityY(StSimT *sim) { uint16_t pos; sim->velY = (int16_t)((uint16_t)sim->accelY + sim->gravTemplateY + (uint16_t)sim->velY); pos = (uint16_t)(((uint16_t)sim->posYrow << 8) | sim->posYlo); pos = (uint16_t)(pos + (uint16_t)sim->velY); sim->posYlo = (uint8_t)pos; sim->posYrow = (uint8_t)(pos >> 8); sim->spr[0].row = sim->posYrow; } // The 24 background bits under a sprite row whose leftmost pixel sits // at visible pixel (px, py); a set bit is character graphics data. static uint32_t backgroundRowMask(const StSimT *sim, int16_t px, int16_t py) { uint32_t window = 0u; int16_t col; int16_t firstCol; uint8_t k; uint8_t shift; if (py < 0 || py >= (int16_t)(ST_SCREEN_ROWS * 8u)) { return 0u; } // Floor division so a negative px lands on the column to its left. firstCol = (int16_t)((px - (px < 0 ? 7 : 0)) / 8); shift = (uint8_t)(px - firstCol * 8); for (k = 0u; k < 4u; k++) { uint8_t byte = 0u; col = (int16_t)(firstCol + (int16_t)k); if (col >= 0 && col < (int16_t)ST_SCREEN_COLS) { byte = sim->charset[sim->screen[ST_CELL((uint16_t)py >> 3, (uint16_t)col)]][py & 7]; } window = (window << 8) | byte; } return (window >> (8u - shift)) & 0xFFFFFFu; } // $63D0 -- rotate every row of the transporter hatch glyph one pixel // right: the animated energy field in the top-wall opening. static void bitScrollHatch(StSimT *sim) { uint8_t row; for (row = 0u; row < 8u; row++) { uint8_t b = sim->charset[ST_CHAR_TRANSPORTER][row]; sim->charset[ST_CHAR_TRANSPORTER][row] = (uint8_t)((b >> 1) | (b << 7)); } sim->charDirty[ST_CHAR_TRANSPORTER] = 1u; } // $6966 -- latch the frame's collisions and run the crash phases. static void collisionDispatch(StSimT *sim) { computeCollisions(sim); if (sim->collisionPhase != 0u) { // Phases 1..3 dispatch elsewhere (stSimTick handles 3's exit). return; } stFarePadLightingGate(sim); if ((sim->spriteBgColl & 1u) == 0u) { if ((sim->spriteSpriteColl & 1u) == 0u) { return; } if (sim->deathInProgress != 0u) { return; } if ((sim->spriteSpriteColl & 2u) != 0u) { if ((sim->spriteSpriteColl & 0xF8u) != 0u) { return; } stFareSquashed(sim); } if ((sim->spriteSpriteColl & 0xF8u) == 0u) { return; } } // $6A01: the level's verdict on sprite contact; background contact // always kills. sim->hitDispatchResult = stHookHitVerdict(sim); if ((sim->spriteBgColl & 1u) == 0u) { if (sim->hitDispatchResult == 0u) { return; } } crashStart(sim); } // $6A72 -- the wreck falls: every second tick sweep the scream, flip // the debris cel, jitter, reflect off the side walls, drift with the // old X velocity and accelerate down until row $DA. static void collisionPhase1(StSimT *sim) { uint8_t jitter; uint16_t sum; uint16_t colFrac; if ((sim->hitDispatchResult & 0x80u) == 0u) { sim->phaseTimer--; if (sim->phaseTimer != 0u) { return; } sim->phaseTimer = sim->phaseReload; sim->thrustSweep--; stAudioThrustSweep((uint8_t)(sim->thrustSweep >> 1)); sim->spr[0].ptr ^= 1u; // $6A98: rng(5) - 3 added to the sprite column (msb follows). jitter = (uint8_t)(stSimRng(sim, 5u) - 3u); colFrac = (uint16_t)(((uint16_t)sim->spr[0].msb << 8) | sim->spr[0].col); colFrac = (uint16_t)(colFrac + (uint16_t)(int16_t)(int8_t)jitter); sim->spr[0].col = (uint8_t)colFrac; sim->spr[0].msb = (uint8_t)(colFrac >> 8); edgeReflect(sim); applyVelocityX(sim); // vy += 40 (low byte with carry into the high byte), row += vy hi. sum = (uint16_t)((uint16_t)(sim->velY & 0xFFu) + 0x28u); sim->velY = (int16_t)((uint16_t)(((uint16_t)sim->velY & 0xFF00u) + (sum & 0x100u)) | (sum & 0xFFu)); sim->spr[0].row = (uint8_t)(sim->spr[0].row + (uint8_t)((uint16_t)sim->velY >> 8)); if (sim->spr[0].row < 0xDAu) { return; } sim->spr[0].row = 0xDAu; } // $6AD6: hit the floor -> phase 2 with a fresh 2-tick cadence. sim->collisionPhase++; sim->phaseReload = 2u; sim->phaseTimer = 2u; stAudioSfx(stC64SfxProgram(ST_SFX_IMPACT)); } // $6B24 -- walk the wreck cels $CC..$D1 at a slowing rate, then hide. static void collisionPhase2(StSimT *sim) { sim->phaseTimer--; if (sim->phaseTimer != 0u) { return; } sim->phaseReload++; sim->phaseTimer = sim->phaseReload; sim->spr[0].ptr++; if (sim->spr[0].ptr != 0xD1u) { return; } sim->spr[0].enable = 0u; sim->collisionPhase++; sim->phaseTimer = 0x46u; } // $6B4C -- 70-tick pause, then the life bookkeeping. static StTickResultE collisionPhase3(StSimT *sim) { uint8_t p; sim->phaseTimer--; if (sim->phaseTimer != 0u) { return ST_TICK_CONTINUE; } if (sim->demoMode != 0u) { return ST_TICK_CRASH_DONE; } if (sim->stage == ST_STAGE_RIDING) { hudInit(sim); if (sim->fareSlotCount != 0u) { sim->spriteSlots[sim->activeDyingSlot] = 0u; sim->fareSlotCount--; if (sim->activeSpriteIdx == 0x0Bu) { uint8_t k; for (k = 0u; k < 4u; k++) { setCell(sim, (uint16_t)(ST_CELL_TRANSPORTER + k), ST_CHAR_TRANSPORTER); } } else { sim->spriteSlots[sim->activeSpriteIdx] = 0u; sim->fareSlotCount--; } } sim->activeSpriteIdx = 0u; sim->stage = ST_STAGE_IDLE; stSimDrawText(sim, 14u, 24u, kTextBlank, 1u); } // $6BBA p = sim->player; sim->cabs[p]--; if (sim->cabs[p] == 0u) { return ST_TICK_PLAYER_OUT; } setCell(sim, (uint16_t)(ST_CELL_CAB_ICONS + sim->cabs[p]), ST_CHAR_BLANK); return ST_TICK_LIFE_LOST; } // The C64 reads two VIC collision registers each frame: $D01F (a sprite // touched the background) and $D01E (two sprites touched). Only the cab // (sprite 0) can drive a gameplay outcome from them: the crash dispatch // returns unless the cab or the background is touched, and every hook // reads a bit set by cab contact. So the full all-pairs register is not // reproduced -- only sprite 0 versus the background and versus the other // sprites, and the exhaust flame (sprite 2, the cab's own, always // overlapping it) is skipped. Non-cab pairs (hazard vs hazard, passenger // vs hazard with the cab clear) never change the dispatch outcome, so // their bits are left unset. Proven against all four demo traces: the // observable state (positions, crash phase, score) is byte-identical. static void computeCollisions(StSimT *sim) { uint32_t (*mask)[ST_SPRITE_H] = sim->collMask; uint8_t ss = 0u; uint8_t bg = 0u; int16_t cabX; int16_t cabY; uint8_t b; uint8_t r; if ((sim->frame.enableMask & 1u) == 0u) { sim->spriteSpriteColl = 0u; sim->spriteBgColl = 0u; return; } spriteMasks(sim, 0u); cabX = (int16_t)((int16_t)sim->frame.x[0] - ST_SPRITE_X_ORIGIN); cabY = (int16_t)((int16_t)sim->frame.y[0] - ST_SPRITE_Y_ORIGIN); // The background can only touch the cab where a covering character // cell is non-blank. In open flight every covering cell is the space // glyph (all-zero pixels), so the 21-row pixel test is skipped there; // any non-space cell falls through to the exact test, so the result // is unchanged (space is the only zero-pixel glyph the test can hit). { int16_t firstCol = (int16_t)((cabX - (cabX < 0 ? 7 : 0)) / 8); int16_t topRow = (cabY < 0) ? 0 : (int16_t)(cabY / 8); int16_t botRow = (int16_t)((cabY + (int16_t)ST_SPRITE_H - 1) / 8); bool solid = false; int16_t rr; int16_t cc; if (botRow >= (int16_t)ST_SCREEN_ROWS) { botRow = (int16_t)(ST_SCREEN_ROWS - 1u); } for (rr = topRow; rr <= botRow && !solid; rr++) { for (cc = firstCol; cc <= (int16_t)(firstCol + 3); cc++) { if (cc >= 0 && cc < (int16_t)ST_SCREEN_COLS && sim->screen[ST_CELL((uint16_t)rr, (uint16_t)cc)] != ST_CHAR_SPACE) { solid = true; break; } } } if (solid) { for (r = 0u; r < ST_SPRITE_H; r++) { if (mask[0][r] != 0u && (mask[0][r] & backgroundRowMask(sim, cabX, (int16_t)(cabY + r))) != 0u) { bg = 1u; break; } } } } for (b = 1u; b < ST_HW_SPRITES; b++) { int16_t dx; int16_t dy; // Skip the exhaust flame (sprite 2) and any disabled sprite. if (b == 2u || (sim->frame.enableMask & (uint8_t)(1u << b)) == 0u) { continue; } dx = (int16_t)((int16_t)sim->frame.x[b] - (int16_t)sim->frame.x[0]); dy = (int16_t)((int16_t)sim->frame.y[b] - (int16_t)sim->frame.y[0]); if (dx <= -ST_SPRITE_W || dx >= ST_SPRITE_W || dy <= -ST_SPRITE_H || dy >= ST_SPRITE_H) { continue; } spriteMasks(sim, b); for (r = 0u; r < ST_SPRITE_H; r++) { int16_t rb = (int16_t)((int16_t)r - dy); uint32_t mb; if (rb < 0 || rb >= (int16_t)ST_SPRITE_H) { continue; } mb = mask[b][rb]; if (dx >= 0) { mb >>= dx; } else { mb = (mb << (uint8_t)(-dx)) & 0xFFFFFFu; } if ((mask[0][r] & mb) != 0u) { ss |= (uint8_t)(1u | (1u << b)); break; } } } sim->spriteSpriteColl = ss; sim->spriteBgColl = bg; } // $6A2B -- start the wreck sequence. static void crashStart(StSimT *sim) { sim->spr[0].ptr = 0xCCu; sim->collisionPhase++; sim->phaseTimer = 2u; sim->phaseReload = 2u; stAudioNoise(false); sim->eventDispatchType = 0u; sim->activePad = 0u; sim->spr[2].enable = 0u; sim->spr[2].row = 0u; sim->velY = 0x0303; stAudioSfx(stC64SfxProgram(ST_SFX_CRASH)); sim->thrustSweep = 0xA0u; if (sim->stage == ST_STAGE_WALK_TO_CAB) { sim->stage--; } } // $6372 -- one cab icon per spare cab (cabs - 1 of them). static void drawCabIcons(StSimT *sim) { uint8_t k = sim->cabs[sim->player]; while (k > 1u) { k--; setCell(sim, (uint16_t)(ST_CELL_CAB_ICONS + k), ST_CHAR_CAB_ICON); } } // $6384 -- screens completed, or the finished marker past 25. static void drawScreensCount(StSimT *sim) { uint8_t v = sim->levelState; uint8_t tens = 0u; if (v >= 0x19u) { setCell(sim, (uint16_t)(ST_CELL_SCREENS + 0u), 0xCBu); setCell(sim, (uint16_t)(ST_CELL_SCREENS + 1u), 0xCCu); setCell(sim, (uint16_t)(ST_CELL_SCREENS + 2u), 0xCDu); setCell(sim, (uint16_t)(ST_CELL_SCREENS + 3u), 0xCEu); setCell(sim, (uint16_t)(ST_CELL_SCREENS + 4u), 0xCFu); return; } setCell(sim, (uint16_t)(ST_CELL_SCREENS + 2u), 0xCAu); setCell(sim, (uint16_t)(ST_CELL_SCREENS + 3u), ST_CHAR_ZERO); setCell(sim, (uint16_t)(ST_CELL_SCREENS + 4u), ST_CHAR_ZERO); while (v >= 10u) { v = (uint8_t)(v - 10u); tens++; } setCell(sim, (uint16_t)(ST_CELL_SCREENS + 1u), (v == 0u) ? ST_CHAR_ZERO : (uint8_t)(ST_CHAR_DIGIT_BASE + v)); setCell(sim, (uint16_t)(ST_CELL_SCREENS + 0u), (tens == 0u) ? ST_CHAR_BLANK : (uint8_t)(ST_CHAR_DIGIT_BASE + tens)); } // $6AED -- negate the X velocity when the wreck crosses column 23 // leftward or column 65 (in the high half) rightward. static void edgeReflect(StSimT *sim) { bool reflect = false; if (sim->spr[0].msb == 0u) { if (sim->velX < 0 && sim->spr[0].col < 0x17u) { reflect = true; } } else { if (sim->velX >= 0 && sim->spr[0].col >= 0x41u) { reflect = true; } } if (reflect) { sim->velX = (int16_t)(-sim->velX); } } // $63DD -- FIRE press edge while airborne toggles the landing gear. static void fireButtonEdge(StSimT *sim) { if (sim->activePad != 0u) { return; } if (sim->fireWasHeld != 0u) { sim->fireWasHeld = (uint8_t)(sim->inputMask & 0x10u); return; } if ((sim->inputMask & 0x10u) == 0u) { return; } sim->fireWasHeld = 0x10u; sim->spr[0].ptr ^= 1u; stAudioSfx(stC64SfxProgram(ST_SFX_GEAR)); } // $6D6A -- sprite 2 is the exhaust: two columns left of the cab, // visible every other tick while a direction is held. void stSimFlameUpdate(StSimT *sim) { uint16_t sum; if (sim->dirMask == 0u) { stAudioNoise(false); sim->spr[2].enable = 0u; return; } stAudioNoise(true); sum = (uint16_t)((uint16_t)sim->spr[0].col + 0xFEu); sim->spr[2].col = (uint8_t)sum; sim->spr[2].msb = (uint8_t)(sim->spr[0].msb + 0xFFu + (uint8_t)(sum >> 8)); sim->spr[2].row = sim->spr[0].row; sim->flameParity ^= 1u; if (sim->flameParity == 0u) { sim->spr[2].enable = 0u; return; } sim->spr[2].ptr = stC64FlameCel(sim->dirMask & 0x0Fu); sim->spr[2].enable = 1u; } // $6419 -- the climb/descend indicator: colour RAM cells beside the // fuel gauge flash red (rising), cyan (level) or yellow (falling). static void fuelBarHud(StSimT *sim) { uint16_t cell; uint8_t color; setColorCell(sim, ST_CELL(23, 10), 0x0Bu); setColorCell(sim, ST_CELL(23, 11), 0x0Bu); setColorCell(sim, ST_CELL(24, 10), 0x0Bu); setColorCell(sim, ST_CELL(24, 11), 0x0Bu); sim->fuelBarTick = (uint8_t)((sim->fuelBarTick + 1u) & 7u); if (sim->fuelBarTick >= 5u) { return; } if (sim->velY < 0) { color = 0x02u; cell = ST_CELL(24, 10); } else if (sim->velY == 0) { color = 0x03u; cell = ST_CELL(23, 10); } else { color = 0x07u; cell = ST_CELL(23, 10); } setColorCell(sim, cell, color); setColorCell(sim, (uint16_t)(cell + 1u), color); } // $6E23 -- the fuel gauge: half a cell burns every fuelRate ticks in // the air; on the fuel pad (the extra last pad) it refills at ten // cents per half cell. static void fuelTick(StSimT *sim) { const StLevelT *L = sim->level; uint8_t x; if (sim->activePad == L->padCount && sim->activePad != L->specialPad) { if (sim->screen[ST_CELL_FUEL_LAST] == ST_CHAR_BLANK) { return; } if (!stSimDecrementNumber(sim, ST_CELL_SCORE, 5u)) { return; } sim->padAnimTick = (uint8_t)((sim->padAnimTick + 1u) & 7u); if (sim->padAnimTick == 0u) { stAudioSfx(stC64SfxProgram(ST_SFX_CASH)); } else if (sim->padAnimTick < 5u) { stAudioVoice1Freq(kPumpFreq[sim->padAnimTick]); } if (sim->padAnimTick != 0u) { return; } x = sim->fuelCells; if (sim->screen[ST_CELL_FUEL + x] == ST_CHAR_BLANK) { x++; sim->fuelCells = x; setCell(sim, (uint16_t)(ST_CELL_FUEL + x), ST_CHAR_FUEL_HALF); return; } setCell(sim, (uint16_t)(ST_CELL_FUEL + x), ST_CHAR_BLANK); if (sim->screen[ST_CELL_FUEL_LAST] == ST_CHAR_BLANK) { stAudioSfx(stC64SfxProgram(ST_SFX_FUEL_FULL)); } return; } // $6EAA if (sim->activePad != 0u) { return; } sim->fuelCountdown--; if (sim->fuelCountdown != 0u) { return; } if (sim->fuelCells < 3u) { stAudioSfx(stC64SfxProgram(ST_SFX_FUEL_LOW)); } sim->fuelCountdown = L->fuelRate; x = sim->fuelCells; if (sim->screen[ST_CELL_FUEL] == ST_CHAR_FUEL_HALF) { return; } if (sim->screen[ST_CELL_FUEL + x] == ST_CHAR_FUEL_HALF) { setCell(sim, (uint16_t)(ST_CELL_FUEL + x), ST_CHAR_FUEL_EMPTY); x--; sim->fuelCells = x; return; } setCell(sim, (uint16_t)(ST_CELL_FUEL + x), ST_CHAR_FUEL_HALF); } // $43E5 -- the fare meter loses a penny every tick. static void hudDraw(StSimT *sim) { (void)stSimDecrementNumber(sim, ST_CELL_FARE, 6u); } // $43A5 -- blank the fare meter. static void hudInit(StSimT *sim) { uint8_t k; for (k = 0u; k < ST_NUMBER_CHARS; k++) { setCell(sim, (uint16_t)(ST_CELL_FARE + k), kHudTemplate[k]); } } // $657B -- parked on a pad: wait for UP to be released, then UP takes // off (velocities cleared, gear retracted). static void landedHandler(StSimT *sim) { if (sim->activePadMirror != 0u) { if ((sim->inputMask & 1u) == 0u) { sim->activePadMirror = 0u; } return; } if (sim->deathInProgress != 0u) { return; } if ((sim->inputMask & 1u) == 0u) { return; } sim->velY = 0; sim->velX = 0; sim->activePad = 0u; sim->spr[0].ptr &= 0xFEu; if (sim->stage == ST_STAGE_WALK_TO_CAB) { sim->stage--; } } // $6BE7 -- the cab has flown out of the top of the screen. static bool levelEndCheck(const StSimT *sim) { return sim->posYrow < 0x1Bu; } // $4253 + $4293 -- snapshot the shadow tables into the frame the VIC // shows next. static void marshal(StSimT *sim) { uint8_t i; uint8_t enable = 0u; for (i = 0u; i < ST_HW_SPRITES; i++) { sim->frame.x[i] = (uint16_t)(((uint16_t)(sim->spr[i].msb != 0u ? 1u : 0u) << 8) | sim->spr[i].col); sim->frame.y[i] = sim->spr[i].row; sim->frame.ptr[i] = sim->spr[i].ptr; sim->frame.color[i] = sim->spr[i].color; if (sim->spr[i].enable != 0u) { enable |= (uint8_t)(1u << i); } } sim->frame.enableMask = enable; sim->frame.multiMask = sim->multiColorMask; } // $645C -- with the gear down and a slow descent, an exact row match // inside a pad's X bounds is a landing. static void padDetect(StSimT *sim) { int8_t i; if (sim->activePad != 0u) { return; } if ((sim->spr[0].ptr & 1u) == 0u) { return; } if (((uint16_t)sim->velY >> 8) != 0u) { return; } for (i = (int8_t)(sim->level->padCount - 1u); i >= 0; i--) { const StPadT *p = &sim->pads[i]; int16_t taxiX = (int16_t)(((int16_t)sim->posXmsb << 8) | sim->posXcol); int16_t x1 = (int16_t)(((int16_t)p->x1Hi << 8) | p->x1Lo); int16_t x2 = (int16_t)(((int16_t)p->x2Hi << 8) | p->x2Lo); if (sim->spr[0].row != p->row) { continue; } if ((int16_t)(taxiX - x1) < 0) { continue; } if ((int16_t)(x2 - taxiX) < 0) { continue; } sim->activePad = (uint8_t)(i + 1); sim->activePadMirror = sim->activePad; sim->eventDispatchType = 0u; if (sim->activePad != sim->activeSpriteIdx) { if (sim->stage == ST_STAGE_RIDING) { sim->eventDispatchType = (sim->activeSpriteIdx == 0x0Bu) ? 3u : 2u; } else if (sim->stage == ST_STAGE_WAIT) { sim->eventDispatchType = 1u; } } sim->dirMask = 0u; sim->deathInProgress = 1u; stSimPadLight(sim, true); sim->bobTimer = 2u; if (((uint8_t)sim->velY & 0x80u) == 0u) { stAudioSfx(stC64SfxProgram(ST_SFX_LAND_SOFT)); return; } sim->bobTimer = 8u; stAudioSfx(stC64SfxProgram(ST_SFX_LAND_HARD)); if (sim->stage == ST_STAGE_RIDING) { hudInit(sim); } return; } } // $6DFF -- the touchdown bob: the sprite dips and rises while the // timer runs down. static void padLandingBob(StSimT *sim) { if (sim->bobTimer == 0u) { return; } sim->bobTimer--; if ((sim->bobTimer & 1u) != 0u) { sim->spr[0].row--; return; } sim->spr[0].row++; if (sim->bobTimer == 0u) { sim->deathInProgress = 0u; stSimPadLight(sim, false); } } // $70B1 -- one bonus cab when the score's hundreds digit reaches 3. static void passengerArrTick(StSimT *sim) { uint8_t p = sim->player; if (sim->bonusLatch[p] != 0u) { return; } if (sim->screen[ST_CELL_SCORE + 1u] < 0x6Du) { return; } sim->cabs[p]++; sim->bonusLatch[p] = sim->cabs[p]; setCell(sim, (uint16_t)(ST_CELL_CAB_ICONS - 1u + sim->cabs[p]), ST_CHAR_CAB_ICON); } // $6032 -- input, thrust, gravity, integration. static void physicsTick(StSimT *sim) { uint8_t in; sim->accelX = 0; sim->accelY = 0; in = readInput(sim); in = stHookInput(sim, in); sim->inputMask = in; if (sim->activePad != 0u) { landedHandler(sim); return; } if (sim->screen[ST_CELL_FUEL] == ST_CHAR_MENU_SENTINEL) { // Out of fuel: only FIRE survives, no thrust at all. sim->inputMask &= 0x10u; } else { if ((sim->inputMask & 0x0Cu) != 0u) { if ((sim->spr[0].ptr & 1u) != 0u) { sim->inputMask &= 0x13u; } else { sim->accelX = (int16_t)sim->accelTemplateX; if ((sim->inputMask & 0x08u) == 0u) { sim->accelX = (int16_t)(-sim->accelX); } } } if ((sim->inputMask & 0x03u) != 0u) { sim->accelY = (int16_t)sim->accelTemplateY; if ((sim->inputMask & 0x02u) == 0u) { sim->accelY = (int16_t)(-sim->accelY); } } } sim->dirMask = (uint8_t)(sim->inputMask & 0x0Fu); sim->velX = (int16_t)((uint16_t)sim->accelX + sim->gravTemplateX + (uint16_t)sim->velX); applyVelocityX(sim); applyVelocityY(sim); } // $6040 + $48F2 -- the joystick byte, or the recorded demo mask. static uint8_t readInput(StSimT *sim) { uint8_t mask; uint16_t next; if (sim->demoMode == 0u || sim->postMortem != 0u) { return sim->rawInput; } mask = sim->demoBuf[sim->demoOff]; sim->demoTimer--; if (sim->demoTimer == 0u) { next = (uint16_t)(sim->demoOff + 3u); if (next < sizeof(sim->demoBuf)) { sim->demoTimer = sim->demoBuf[next]; } next = (uint16_t)(sim->demoOff + 2u); if (next < sizeof(sim->demoBuf)) { sim->demoOff = next; } } return mask; } static void markCell(StSimT *sim, uint16_t cell) { if (sim->cellDirty[cell] != 0u) { return; } sim->cellDirty[cell] = 1u; if (sim->dirtyCount < (uint16_t)(sizeof(sim->dirtyList) / sizeof(sim->dirtyList[0]))) { sim->dirtyList[sim->dirtyCount++] = cell; } else { sim->dirtyAll = true; } } static void setCell(StSimT *sim, uint16_t cell, uint8_t ch) { if (sim->screen[cell] != ch) { sim->screen[cell] = ch; markCell(sim, cell); } } static void setColorCell(StSimT *sim, uint16_t cell, uint8_t color) { color = (uint8_t)(color & 0x0Fu); if (sim->color[cell] != color) { sim->color[cell] = color; markCell(sim, cell); } } // 21 rows of 24-bit foreground masks for a displayed sprite, kept in // collMask[idx] and rebuilt only when its bitmap or multicolour mode // changes. A multicolour sprite's 2-bit pairs cover two pixels each: a // non-zero pair is two set mask bits (kPairMask maps a nibble's two // pairs at once). static void spriteMasks(StSimT *sim, uint8_t idx) { static const uint8_t kPairMask[16] = { 0x0u, 0x3u, 0x3u, 0x3u, 0xCu, 0xFu, 0xFu, 0xFu, 0xCu, 0xFu, 0xFu, 0xFu, 0xCu, 0xFu, 0xFu, 0xFu }; const uint8_t *bm = stSimSpriteBitmap(sim, sim->frame.ptr[idx]); uint8_t multi = (uint8_t)((sim->frame.multiMask >> idx) & 1u); uint32_t *rows = sim->collMask[idx]; uint8_t r; if (bm != 0 && bm == sim->collMaskBm[idx] && multi == sim->collMaskMulti[idx]) { return; } for (r = 0u; r < ST_SPRITE_H; r++) { uint8_t b0 = 0u; uint8_t b1 = 0u; uint8_t b2 = 0u; if (bm != 0) { b0 = bm[r * 3u]; b1 = bm[r * 3u + 1u]; b2 = bm[r * 3u + 2u]; if (multi != 0u) { b0 = (uint8_t)((kPairMask[b0 >> 4] << 4) | kPairMask[b0 & 0x0Fu]); b1 = (uint8_t)((kPairMask[b1 >> 4] << 4) | kPairMask[b1 & 0x0Fu]); b2 = (uint8_t)((kPairMask[b2 >> 4] << 4) | kPairMask[b2 & 0x0Fu]); } } rows[r] = ((uint32_t)b0 << 16) | ((uint32_t)b1 << 8) | b2; } sim->collMaskBm[idx] = bm; sim->collMaskMulti[idx] = multi; } // $6946 -- all sprites off, latches cleared, nobody on a pad. static void spritesOffReset(StSimT *sim) { uint8_t i; for (i = 0u; i < ST_HW_SPRITES; i++) { sim->spr[i].enable = 0u; } sim->spriteSpriteColl = 0u; sim->spriteBgColl = 0u; sim->activePad = 0u; sim->activeSpriteIdx = 0u; } // $6888 -- place the cab at the level's spawn point with a full tank. static void taxiSpawnInit(StSimT *sim) { const StLevelT *L = sim->level; uint8_t k; sim->spr[0].enable = 1u; sim->multiColorMask = 0x07u; sim->spr[0].ptr = 0xC0u; sim->posYlo = L->spawn[3]; sim->posYrow = L->spawn[4]; sim->posXlo = L->spawn[0]; sim->posXcol = L->spawn[1]; sim->posXmsb = L->spawn[2]; sim->spr[0].msb = L->spawn[2]; sim->spr[0].col = L->spawn[1]; sim->spr[0].row = L->spawn[4]; sim->velX = 0; sim->velY = 0; sim->deathInProgress = 0u; sim->dirMask = 0u; sim->inputMask = 0u; sim->activePad = 0u; sim->collisionPhase = 0u; sim->padAnimTick = 0u; stSimPadLight(sim, false); fuelBarHud(sim); sim->fuelCells = 11u; for (k = 0u; k < ST_FUEL_CELLS; k++) { setCell(sim, (uint16_t)(ST_CELL_FUEL + k), ST_CHAR_BLANK); } sim->fuelCountdown = L->fuelRate; } // $619B -- LEFT/RIGHT pick the facing pair; bit 0 (the gear) survives. static void taxiSpriteCelSelect(StSimT *sim) { uint8_t gear = (uint8_t)(sim->spr[0].ptr & 1u); uint8_t base; if ((sim->dirMask & 0x04u) != 0u) { base = 0xDCu; } else if ((sim->dirMask & 0x08u) != 0u) { base = 0xC0u; } else { return; } sim->spr[0].ptr = (uint8_t)(base | gear); } // $4345 -- a HUD glyph's numeric value: blanks and the zero glyph are 0. static uint8_t validateDigit(uint8_t ch) { uint8_t v; if (ch == ST_CHAR_BLANK) { return 0u; } v = (uint8_t)(ch - ST_CHAR_DIGIT_BASE); if (v == 10u) { return 0u; } return v; } // --------------------------------------------------------------------------- // Public API // --------------------------------------------------------------------------- // $61FB -- next player's turn; wraps to player 0 when everybody has had // this screen (the caller then loads the next one). bool stSimAdvancePlayer(StSimT *sim) { sim->player++; if (sim->player == sim->playerCount) { sim->player = 0u; return true; } return false; } // $704E -- keep the current player's score and fare rows. void stSimArchiveHud(StSimT *sim) { memcpy(sim->scoreBackup[sim->player], &sim->screen[ST_CELL_SCORE], ST_NUMBER_CHARS); memcpy(sim->fareBackup[sim->player], &sim->screen[ST_CELL_FARE], ST_NUMBER_CHARS); } // $4354 -- add a 7-glyph decimal blob onto the number at dstCell, // right to left, skipping the decimal point, with leading blanks. void stSimBcdAdd(StSimT *sim, uint16_t dstCell, const uint8_t *blob) { int8_t y; uint8_t carry = 0u; for (y = 6; y >= 0; y--) { uint8_t sum; uint8_t ch; if (y == 4) { continue; } sum = (uint8_t)(validateDigit(blob[y]) + validateDigit(sim->screen[dstCell + (uint16_t)y]) + carry); carry = 0u; if (sum >= 10u) { carry = 1u; sum = (uint8_t)(sum - 10u); } ch = (uint8_t)(ST_CHAR_DIGIT_BASE + sum); if (ch == ST_CHAR_DIGIT_BASE) { ch = ST_CHAR_ZERO; } setCell(sim, (uint16_t)(dstCell + (uint16_t)y), ch); } // $4393: leading-zero suppression across the integer part. for (y = 0; y < 3; y++) { if (sim->screen[dstCell + (uint16_t)y] != ST_CHAR_ZERO) { break; } setCell(sim, (uint16_t)(dstCell + (uint16_t)y), ST_CHAR_BLANK); } } // $440B -- subtract one from the digit at `position` of the number at // `cell`, borrowing leftward. False when there is nothing to take. bool stSimDecrementNumber(StSimT *sim, uint16_t cell, uint8_t position) { uint8_t y = position; uint8_t ch; for (;;) { ch = sim->screen[cell + y]; if (ch == ST_CHAR_BLANK) { return false; } if (ch != ST_CHAR_ZERO) { break; } do { y--; } while (y == 4u); } y = position; for (;;) { ch = sim->screen[cell + y]; if (ch != ST_CHAR_ZERO) { break; } setCell(sim, (uint16_t)(cell + y), (uint8_t)(ST_CHAR_DIGIT_BASE + 9u)); do { y--; } while (y == 4u); } ch = (uint8_t)(ch - 1u); setCell(sim, (uint16_t)(cell + y), ch); if (ch != ST_CHAR_DIGIT_BASE) { return true; } setCell(sim, (uint16_t)(cell + y), ST_CHAR_ZERO); if (y >= 3u) { return true; } if (y == 0u) { setCell(sim, cell, ST_CHAR_BLANK); return true; } if (sim->screen[cell + y - 1u] != ST_CHAR_BLANK) { return true; } setCell(sim, (uint16_t)(cell + y), ST_CHAR_BLANK); return true; } // $41C2 -- write a screen-code string (terminated by any byte < 6) // with one colour. void stSimDrawText(StSimT *sim, uint8_t col, uint8_t row, const uint8_t *text, uint8_t color) { uint16_t cell = ST_CELL(row, col); while (*text >= 6u) { setCell(sim, cell, *text); setColorCell(sim, cell, color); cell++; text++; } } // $62F0 + $5F18 -- load the screen image and run the level prelude. void stSimEnterLevel(StSimT *sim, const StLevelT *level) { uint8_t k; sim->level = level; memcpy(sim->pads, level->pads, sizeof(sim->pads)); memcpy(sim->screen, level->screen, ST_SCREEN_CELLS); memcpy(sim->color, level->color, ST_SCREEN_CELLS); stSimDirtyAll(sim); memcpy(sim->charset, stC64Charset(), sizeof(sim->charset)); memset(sim->charDirty, 1, ST_CHARSET_CHARS); // Level sprites live in the one level buffer, so the same bitmap // address now holds other pixels: rebuild every collision mask. memset(sim->collMaskBm, 0, sizeof(sim->collMaskBm)); sim->borderColor = level->header[0]; sim->bgColor = level->header[1]; sim->spr[0].color = level->header[7]; sim->spr[1].color = level->header[8]; sim->spriteMc0 = level->header[5]; sim->spriteMc1 = level->header[6]; sim->accelTemplateY = level->accelY; sim->accelTemplateX = level->accelX; sim->gravTemplateY = level->gravY; sim->gravTemplateX = level->gravX; memset(sim->spriteSlots, 0, sizeof(sim->spriteSlots)); sim->fareSlotCount = 0u; // $634D: this player's HUD rows come back. for (k = 0u; k < ST_NUMBER_CHARS; k++) { setCell(sim, (uint16_t)(ST_CELL_SCORE + k), sim->scoreBackup[sim->player][k]); setCell(sim, (uint16_t)(ST_CELL_FARE + k), sim->fareBackup[sim->player][k]); } for (k = 0u; k < 7u; k++) { setCell(sim, (uint16_t)(ST_CELL_CAB_ICONS + k), ST_CHAR_BLANK); } drawCabIcons(sim); drawScreensCount(sim); // Prelude $5F18 (the player-turn bookkeeping ran before the load). for (k = 0u; k < ST_HW_SPRITES; k++) { sim->spr[k].enable = 0u; } sim->hookLevel = level->levelIndex; memcpy(sim->hookRam, level->hookData, ST_HOOK_BYTES); stSimRespawn(sim); } // $5F27 -- the respawn half of the prelude. void stSimRespawn(StSimT *sim) { spritesOffReset(sim); stHookPrelude0(sim); // $6F18 takeoffSetup: from the third screen on the cab starts with // a passenger aboard who wants pad 1. sim->eventDispatchType = 0u; sim->stage = ST_STAGE_IDLE; if (sim->levelState >= 2u) { static const uint8_t kPadPlease[] = { 0x50, 0x41, 0x44, 0x20, 0x31, 0x20, 0x20, 0x50, 0x4C, 0x45, 0x41, 0x53, 0x45, 0 }; sim->stage = ST_STAGE_RIDING; sim->activeSpriteIdx = 1u; sim->activeDyingSlot = 1u; stSimDrawText(sim, 14u, 24u, kPadPlease, 1u); sim->eventDispatchType = 1u; } taxiSpawnInit(sim); stHookPrelude1(sim); // $6906 framePresent: the start-of-screen jingle ends with the SID // (and the demo RNG) reset. marshal(sim); stAudioSilence(); sim->spriteSpriteColl = 0u; sim->spriteBgColl = 0u; sim->rngT1 = 0x06u; sim->rngT2 = 0x17u; } // $5EC4 + $4207 + $48AD -- a fresh game (or the attract demo). void stSimNewGame(StSimT *sim, uint8_t playerCount, bool demo) { uint8_t p; uint8_t keepBuf[sizeof(sim->demoBuf)]; uint8_t keepParity = sim->flameParity; uint8_t keepWave = sim->waveIdx; uint8_t keepWalk = sim->walkParity; // $5EC4 clears the game state but the playback buffer, the flame // parity, the wave index and the walk parity all survive. memcpy(keepBuf, sim->demoBuf, sizeof(keepBuf)); memset(sim, 0, sizeof(*sim)); memcpy(sim->demoBuf, keepBuf, sizeof(keepBuf)); sim->flameParity = keepParity; sim->waveIdx = keepWave; sim->walkParity = keepWalk; sim->stage0Rng = 0x64u; sim->decayReload = 3u; sim->playerCount = playerCount; sim->player = 0u; sim->playersDone = 0u; sim->levelState = 0u; sim->demoMode = demo ? 1u : 0u; sim->rngHost = 0x2545F491u; for (p = 0u; p < ST_MAX_PLAYERS; p++) { sim->cabs[p] = ST_CABS_PER_PLAYER; memcpy(sim->scoreBackup[p], kHudTemplate, ST_NUMBER_CHARS); memcpy(sim->fareBackup[p], kHudTemplate, ST_NUMBER_CHARS); } sim->rngT1 = 0x06u; sim->rngT2 = 0x17u; } // Everything must be repainted. void stSimDirtyAll(StSimT *sim) { memset(sim->cellDirty, 1, ST_SCREEN_CELLS); sim->dirtyCount = 0u; sim->dirtyAll = true; } // $6866 / $6877 -- the pad indicator cells beside the HUD. void stSimPadLight(StSimT *sim, bool on) { if (on) { setColorCell(sim, ST_CELL(23, 28), 0x0Bu); setColorCell(sim, ST_CELL(23, 29), 0x0Bu); setColorCell(sim, ST_CELL(24, 28), 0x02u); setColorCell(sim, ST_CELL(24, 29), 0x02u); } else { setColorCell(sim, ST_CELL(24, 28), 0x0Bu); setColorCell(sim, ST_CELL(24, 29), 0x0Bu); setColorCell(sim, ST_CELL(23, 28), 0x07u); setColorCell(sim, ST_CELL(23, 29), 0x07u); } } // $401B -- one character into screen RAM. void stSimPutChar(StSimT *sim, uint8_t col, uint8_t row, uint8_t ch) { setCell(sim, ST_CELL(row, col), ch); } // $401E -- one colour into colour RAM. void stSimPutColor(StSimT *sim, uint8_t col, uint8_t row, uint8_t color) { setColorCell(sim, ST_CELL(row, col), color); } // $4080 -- 1..n. The demo walks a 64-byte table so its rides replay // exactly; a real game reads the SID noise oscillator, which a host // LCG stands in for. uint8_t stSimRng(StSimT *sim, uint8_t n) { uint8_t r; uint16_t product; if (sim->demoMode != 0u) { sim->rngT1 = (uint8_t)((sim->rngT1 + 1u) & 0x3Fu); sim->rngT2++; r = (uint8_t)(stC64RngByte(sim->rngT1) + sim->rngT2); } else { sim->rngHost = sim->rngHost * 1103515245u + 12345u; r = (uint8_t)(sim->rngHost >> 16); } product = (uint16_t)((uint16_t)n * (uint16_t)r); return (uint8_t)((product >> 8) + 1u); } // The score glyphs as pennies (for the high-score table). uint32_t stSimScorePennies(const StSimT *sim) { uint32_t v = 0u; uint8_t k; for (k = 0u; k < ST_NUMBER_CHARS; k++) { if (k == 4u) { continue; } v = v * 10u + validateDigit(sim->screen[ST_CELL_SCORE + k]); } return v; } // The boot-time content of the playback buffer. void stSimSeedDemoBuffer(StSimT *sim, const uint8_t *image, uint16_t len) { if (len > sizeof(sim->demoBuf)) { len = (uint16_t)sizeof(sim->demoBuf); } memcpy(sim->demoBuf, image, len); } // $0902 / $4740 -- load a recording over the buffer and rewind. void stSimSetDemoStream(StSimT *sim, const uint8_t *stream, uint16_t len) { if (len > sizeof(sim->demoBuf)) { len = (uint16_t)sizeof(sim->demoBuf); } memcpy(sim->demoBuf, stream, len); sim->demoOff = 0u; sim->demoTimer = sim->demoBuf[1]; } // Bitmap for a block pointer: the standard set, or the level's own. const uint8_t *stSimSpriteBitmap(const StSimT *sim, uint8_t ptr) { uint8_t k; if (ptr >= ST_SPRITE_PTR_FIRST && ptr <= ST_SPRITE_PTR_LAST) { return stC64SpriteBitmap((uint8_t)(ptr - ST_SPRITE_PTR_FIRST)); } if (sim->level != 0) { for (k = 0u; k < sim->level->spriteCount; k++) { if (sim->level->sprites[k].ptr == ptr) { return sim->level->sprites[k].bitmap; } } } return 0; } // $5F40 -- one iteration of the main loop. StTickResultE stSimTick(StSimT *sim) { StTickResultE result = ST_TICK_CONTINUE; if (sim->collisionPhase == 0u) { physicsTick(sim); fireButtonEdge(sim); stSimFlameUpdate(sim); padDetect(sim); taxiSpriteCelSelect(sim); if (levelEndCheck(sim)) { stAudioNoise(false); stAudioSilence(); stSimArchiveHud(sim); return ST_TICK_LEVEL_EXIT; } padLandingBob(sim); fuelTick(sim); // $61BD passengerEventDraw only re-voices the pickup/drop-off // messages already drawn by the fare machine. } stFarePostTickGate(sim); stFareStageDispatch(sim); stHookPerTick2(sim); marshal(sim); bitScrollHatch(sim); stHookPerTick3(sim); fuelBarHud(sim); if (sim->collisionPhase == 0u) { collisionDispatch(sim); } else if (sim->collisionPhase == 1u) { computeCollisions(sim); collisionPhase1(sim); } else if (sim->collisionPhase == 2u) { computeCollisions(sim); collisionPhase2(sim); } else { computeCollisions(sim); result = collisionPhase3(sim); if (result != ST_TICK_CONTINUE) { return result; } } hudDraw(sim); // $4FCB runStopWatcher: any joystick input ends the demo. if (sim->demoMode != 0u && (sim->rawInput & 0x1Fu) != 0u) { return ST_TICK_DEMO_INPUT; } passengerArrTick(sim); return ST_TICK_CONTINUE; }