1794 lines
58 KiB
C
1794 lines
58 KiB
C
// 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 <string.h>
|
|
|
|
#include "stSim.h"
|
|
|
|
// Jingle selection ($6935 / $70D4). Song numbers index kStSongs.
|
|
#define ST_JINGLE_SONG_BONUS 4u // level-25 start and the bonus cab
|
|
#define ST_JINGLE_LEVEL_25 0x19u // $7215 compared against #$19
|
|
|
|
// The descent lamp ($6419): two cell pairs beside the fuel gauge, the
|
|
// colors they light in, and the Y velocities that pick one.
|
|
#define ST_LAMP_CELL_SLOW ST_CELL(23, 10) // $DBA2
|
|
#define ST_LAMP_CELL_FAST ST_CELL(24, 10) // $DBCA
|
|
#define ST_LAMP_OFF 0x0Bu // dark gray
|
|
#define ST_LAMP_SLOW 0x03u // cyan
|
|
#define ST_LAMP_FALL 0x07u // yellow
|
|
#define ST_LAMP_FAST 0x02u // red
|
|
#define ST_LAMP_FALL_VEL 0x0080 // high byte 0, low bit 7
|
|
#define ST_LAMP_FAST_VEL 0x0100 // high byte positive
|
|
#define ST_LAMP_TICK_MASK 7u // $71BE counts mod 8...
|
|
#define ST_LAMP_LIT_TICKS 5u // ...and lights on 0..4
|
|
|
|
// Screen-code strings the engine writes (all ASCII-compatible glyphs).
|
|
// $6C38 -- the 12-character blank that clears the message row (every
|
|
// message is 12 characters: "PAD PLEASE", " UP PLEASE! " and the rest).
|
|
static const uint8_t kTextBlank[] = " ";
|
|
// $43B1 -- the fare meter's blank template: three leading blanks, then
|
|
// "0.00" in the inverse-video digit glyphs.
|
|
static const uint8_t kHudTemplate[ST_NUMBER_CHARS] = { 0x66, 0x66, 0x66, 0x74, 0x77, 0x74, 0x74 };
|
|
// 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 };
|
|
const uint8_t kStBit[ST_HW_SPRITES] = { 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80 };
|
|
|
|
|
|
static void applyVelocityX(StSimT *sim);
|
|
static void applyVelocityY(StSimT *sim);
|
|
static bool backgroundRowHits(const StSimT *sim, int16_t firstCol, int16_t py, const uint8_t *w);
|
|
static bool cabRowsTouchBackground(StSimT *sim, int16_t firstCol, int16_t py, uint8_t phase);
|
|
static const uint8_t *cabWindows(StSimT *sim, uint8_t s);
|
|
static int16_t cellOfPixel(int16_t px);
|
|
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 bool coveringCellsSolid(const StSimT *sim, int16_t firstCol, int16_t topRow, int16_t botRow);
|
|
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 uint8_t frameBgCollisions(StSimT *sim);
|
|
static uint8_t frameSpriteCollisions(StSimT *sim);
|
|
static void fuelBarHud(StSimT *sim);
|
|
static void fuelTick(StSimT *sim);
|
|
static void hudDraw(StSimT *sim);
|
|
static void landedHandler(StSimT *sim);
|
|
static bool levelEndCheck(const StSimT *sim);
|
|
static void markCell(StSimT *sim, uint16_t cell);
|
|
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 rowWindow(const uint8_t *row, uint8_t s, uint8_t *w);
|
|
static void setCell(StSimT *sim, uint16_t cell, uint8_t ch);
|
|
static void setColorCell(StSimT *sim, uint16_t cell, uint8_t color);
|
|
static void sfxEnvelopeTick(StSimT *sim);
|
|
static void spriteMasks(StSimT *sim, uint8_t idx);
|
|
static bool spriteRowsTouchBackground(const StSimT *sim, uint8_t idx, int16_t firstCol, int16_t py, uint8_t phase);
|
|
static bool spriteTouchesBackground(StSimT *sim, uint8_t idx);
|
|
static void spritesOffReset(StSimT *sim);
|
|
static bool spritesTouch(StSimT *sim, uint8_t a, uint8_t b, int16_t dx, int16_t dy);
|
|
static void taxiSpawnInit(StSimT *sim);
|
|
static void taxiSpriteCelSelect(StSimT *sim);
|
|
static uint8_t validateDigit(uint8_t ch);
|
|
static void voicesOff(StSimT *sim);
|
|
|
|
|
|
// $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;
|
|
}
|
|
|
|
|
|
// Whether a 4-byte row window (rowWindow: byte j covers character
|
|
// column firstCol + j) meets character graphics on screen pixel row py.
|
|
static bool backgroundRowHits(const StSimT *sim, int16_t firstCol, int16_t py, const uint8_t *w) {
|
|
uint8_t glyphRow;
|
|
uint8_t j;
|
|
|
|
if (py < 0 || py >= (int16_t)(ST_SCREEN_ROWS * 8u)) {
|
|
return false;
|
|
}
|
|
glyphRow = (uint8_t)(py & 7);
|
|
for (j = 0u; j < 4u; j++) {
|
|
int16_t col = (int16_t)(firstCol + (int16_t)j);
|
|
if (w[j] != 0u && col >= 0 && col < (int16_t)ST_SCREEN_COLS) {
|
|
if ((stSimGlyphRow(sim, sim->screen[ST_CELL((uint16_t)py >> 3, (uint16_t)col)], glyphRow) & w[j]) != 0u) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
// The cab's 21 cached row windows at grid phase `phase` against the
|
|
// background rows under it.
|
|
static bool cabRowsTouchBackground(StSimT *sim, int16_t firstCol, int16_t py, uint8_t phase) {
|
|
uint8_t first = sim->collRowFirst[0];
|
|
uint8_t left = (uint8_t)(sim->collRowLast[0] + 1u - first);
|
|
const uint8_t *win;
|
|
|
|
if (first == ST_SPRITE_H) {
|
|
return false;
|
|
}
|
|
win = cabWindows(sim, phase) + (uint16_t)first * 4u;
|
|
py = (int16_t)(py + first);
|
|
// Counted down to zero: `dec a / bne` needs no compare. (It also
|
|
// dodged the llvm816 bug, fixed 2026-09-22, where the up-counting form
|
|
// compiled to `cmp #end / inc a / bne` with the inc trampling the
|
|
// compare's Z, so the loop ran past row 20 into the next phase's
|
|
// windows -- the false landing crash in the IIgs "Puzzles" demo.)
|
|
do {
|
|
if (backgroundRowHits(sim, firstCol, py, win)) {
|
|
return true;
|
|
}
|
|
win += 4;
|
|
py++;
|
|
left--;
|
|
} while (left != 0u);
|
|
return false;
|
|
}
|
|
|
|
|
|
// The cab's 21 row windows at grid phase s, 4 bytes per row, built on
|
|
// first use after the cab's mask changed and cached until it changes
|
|
// again. The cab is one side of every collision test, so no test
|
|
// shifts a row (a variable 32-bit shift is a library bit loop on the
|
|
// 65816 and this was the collision test's whole cost).
|
|
static const uint8_t *cabWindows(StSimT *sim, uint8_t s) {
|
|
uint8_t (*win)[4] = sim->cabWin[s];
|
|
uint8_t r;
|
|
|
|
if ((sim->cabWinValid & (uint8_t)(1u << s)) == 0u) {
|
|
for (r = 0u; r < ST_SPRITE_H; r++) {
|
|
rowWindow(sim->collMask[0][r], s, win[r]);
|
|
}
|
|
sim->cabWinValid |= (uint8_t)(1u << s);
|
|
}
|
|
return &win[0][0];
|
|
}
|
|
|
|
|
|
// Which character column (or row) a pixel coordinate falls in: floor
|
|
// division by 8. A shift, not a divide -- the 65816 has no divider and
|
|
// the compiler's signed-division helper cost more than the collision
|
|
// test that calls it. The +256/-32 bias makes the shift exact for any
|
|
// coordinate down to -256, well past the furthest a sprite can hang off
|
|
// the left or top edge.
|
|
static int16_t cellOfPixel(int16_t px) {
|
|
return (int16_t)(((px + 256) >> 3) - 32);
|
|
}
|
|
|
|
|
|
// $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;
|
|
|
|
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);
|
|
stSimSpriteAddX(sim, 0u, jitter);
|
|
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;
|
|
stSimSfx(sim, 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 != ST_SPRITE_WRECK_LAST) {
|
|
return;
|
|
}
|
|
sim->spr[0].enable = 0u;
|
|
sim->collisionPhase++;
|
|
sim->phaseTimer = 0x46u;
|
|
}
|
|
|
|
|
|
// $6B4C -- 70-tick pause, then the life bookkeeping.
|
|
static StTickResultE collisionPhase3(StSimT *sim) {
|
|
sim->phaseTimer--;
|
|
if (sim->phaseTimer != 0u) {
|
|
return ST_TICK_CONTINUE;
|
|
}
|
|
if (sim->demoMode != 0u) {
|
|
return ST_TICK_CRASH_DONE;
|
|
}
|
|
return stSimLoseCab(sim);
|
|
}
|
|
|
|
|
|
// $6966 -- what its read of $D01E/$D01F sees: this tick's frame OR the
|
|
// frame drawn before the flush (see prevFrameSpriteColl in StSimT).
|
|
static void computeCollisions(StSimT *sim) {
|
|
uint8_t ss = frameSpriteCollisions(sim);
|
|
|
|
sim->spriteSpriteColl = (uint8_t)(ss | sim->prevFrameSpriteColl);
|
|
sim->spriteBgColl = (uint8_t)(frameBgCollisions(sim) | sim->prevFrameBgColl);
|
|
sim->prevFrameSpriteColl = ss;
|
|
}
|
|
|
|
|
|
// Whether any character cell in rows topRow..botRow, columns firstCol..
|
|
// firstCol+3 (the cells a sprite at that spot covers) is non-blank.
|
|
static bool coveringCellsSolid(const StSimT *sim, int16_t firstCol, int16_t topRow, int16_t botRow) {
|
|
int16_t rr;
|
|
int16_t cc;
|
|
|
|
for (rr = topRow; rr <= botRow; 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) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
// $6A2B -- start the wreck sequence.
|
|
static void crashStart(StSimT *sim) {
|
|
sim->spr[0].ptr = ST_SPRITE_WRECK_FIRST;
|
|
sim->collisionPhase++;
|
|
sim->phaseTimer = 2u;
|
|
sim->phaseReload = 2u;
|
|
stAudioNoise(false);
|
|
sim->eventDispatchType = ST_EVENT_NONE;
|
|
sim->activePad = 0u;
|
|
sim->spr[2].enable = 0u;
|
|
sim->spr[2].row = 0u;
|
|
sim->velY = 0x0303;
|
|
stSimSfx(sim, 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;
|
|
stSimSfx(sim, 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 x;
|
|
|
|
if (sim->dirMask == 0u) {
|
|
stAudioNoise(false);
|
|
sim->spr[2].enable = 0u;
|
|
return;
|
|
}
|
|
stAudioNoise(true);
|
|
x = (uint16_t)(stSimSpriteX(sim, 0u) - 2u);
|
|
sim->spr[2].col = (uint8_t)x;
|
|
sim->spr[2].msb = (uint8_t)(x >> 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;
|
|
}
|
|
|
|
|
|
// The displayed frame's $D01F bits over the current picture -- only the
|
|
// bits anyone reads: the cab's, and those of the hazards levels J and P
|
|
// opt in through bgCollSprites.
|
|
static uint8_t frameBgCollisions(StSimT *sim) {
|
|
uint8_t bg = 0u;
|
|
uint8_t b;
|
|
|
|
for (b = 0u; b < ST_HW_SPRITES; b++) {
|
|
if ((sim->frame.enableMask & kStBit[b]) == 0u) {
|
|
continue;
|
|
}
|
|
if (b != 0u && (sim->bgCollSprites & kStBit[b]) == 0u) {
|
|
continue;
|
|
}
|
|
spriteMasks(sim, b);
|
|
if (spriteTouchesBackground(sim, b)) {
|
|
bg |= kStBit[b];
|
|
}
|
|
}
|
|
return bg;
|
|
}
|
|
|
|
|
|
// The displayed frame's $D01E: both bits of every pair of shown sprites
|
|
// whose pixels meet -- any pair, because the hooks test whole groups of
|
|
// bits (G's and the dispatch's AND #$F8 also catch a hook sprite touching
|
|
// the exhaust flame or another hook sprite, H's and U's walk every bit,
|
|
// and the pad light's AND #$02 catches the passenger touching anything).
|
|
static uint8_t frameSpriteCollisions(StSimT *sim) {
|
|
uint8_t shown = sim->frame.enableMask;
|
|
uint8_t ss = 0u;
|
|
uint8_t a;
|
|
uint8_t b;
|
|
|
|
for (a = 0u; a < (uint8_t)(ST_HW_SPRITES - 1u); a++) {
|
|
if ((shown & kStBit[a]) == 0u) {
|
|
continue;
|
|
}
|
|
for (b = (uint8_t)(a + 1u); b < ST_HW_SPRITES; b++) {
|
|
uint8_t pair = (uint8_t)(kStBit[a] | kStBit[b]);
|
|
int16_t dx;
|
|
int16_t dy;
|
|
// A pair already latched on both sides cannot add a bit, and
|
|
// most pairs are too far apart to test at all.
|
|
if ((shown & kStBit[b]) == 0u || (ss & pair) == pair) {
|
|
continue;
|
|
}
|
|
dx = (int16_t)((int16_t)sim->frame.x[b] - (int16_t)sim->frame.x[a]);
|
|
dy = (int16_t)((int16_t)sim->frame.y[b] - (int16_t)sim->frame.y[a]);
|
|
if (dx <= -ST_SPRITE_W || dx >= ST_SPRITE_W || dy <= -ST_SPRITE_H || dy >= ST_SPRITE_H) {
|
|
continue;
|
|
}
|
|
if (spritesTouch(sim, a, b, dx, dy)) {
|
|
ss |= pair;
|
|
}
|
|
}
|
|
}
|
|
return ss;
|
|
}
|
|
|
|
|
|
// $6419 -- the descent lamp beside the fuel gauge. Every tick its two
|
|
// cell pairs ($DBA2 on row 23, $DBCA on row 24) go dark gray; then, on
|
|
// five ticks of every eight, one pair lights by the Y velocity
|
|
// ($714F/$714E, positive = falling): red on row 24 when the high byte is
|
|
// positive ($643D), yellow on row 23 when it is zero and the low byte has
|
|
// bit 7 set ($6450), and cyan on row 23 otherwise ($6449) -- any climb,
|
|
// a hover and a slow fall alike.
|
|
static void fuelBarHud(StSimT *sim) {
|
|
uint16_t cell = ST_LAMP_CELL_SLOW;
|
|
uint8_t color = ST_LAMP_SLOW;
|
|
|
|
setColorCell(sim, ST_LAMP_CELL_SLOW, ST_LAMP_OFF);
|
|
setColorCell(sim, (uint16_t)(ST_LAMP_CELL_SLOW + 1u), ST_LAMP_OFF);
|
|
setColorCell(sim, ST_LAMP_CELL_FAST, ST_LAMP_OFF);
|
|
setColorCell(sim, (uint16_t)(ST_LAMP_CELL_FAST + 1u), ST_LAMP_OFF);
|
|
sim->fuelBarTick = (uint8_t)((sim->fuelBarTick + 1u) & ST_LAMP_TICK_MASK);
|
|
if (sim->fuelBarTick >= ST_LAMP_LIT_TICKS) {
|
|
return;
|
|
}
|
|
if (sim->velY >= ST_LAMP_FAST_VEL) {
|
|
color = ST_LAMP_FAST;
|
|
cell = ST_LAMP_CELL_FAST;
|
|
} else if (sim->velY >= ST_LAMP_FALL_VEL) {
|
|
color = ST_LAMP_FALL;
|
|
}
|
|
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 == sim->padCount && sim->activePad != sim->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) {
|
|
stSimSfx(sim, 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) {
|
|
stSimSfx(sim, stC64SfxProgram(ST_SFX_FUEL_FULL));
|
|
}
|
|
return;
|
|
}
|
|
// $6EAA
|
|
if (sim->activePad != 0u) {
|
|
return;
|
|
}
|
|
sim->fuelCountdown--;
|
|
if (sim->fuelCountdown != 0u) {
|
|
return;
|
|
}
|
|
if (sim->fuelCells < 3u) {
|
|
stSimSfx(sim, 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);
|
|
}
|
|
|
|
|
|
// $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;
|
|
}
|
|
|
|
|
|
// $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->padCount - 1u); i >= 0; i--) {
|
|
const StPadT *p = &sim->pads[i];
|
|
int16_t taxiX = (int16_t)(((uint16_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 = ST_EVENT_NONE;
|
|
if (sim->activePad != sim->activeSpriteIdx) {
|
|
if (sim->stage == ST_STAGE_RIDING) {
|
|
sim->eventDispatchType = (sim->activeSpriteIdx == ST_FARE_DEST_UP) ? ST_EVENT_WRONG_PAD_UP : ST_EVENT_WRONG_PAD;
|
|
} else if (sim->stage == ST_STAGE_WAIT) {
|
|
sim->eventDispatchType = ST_EVENT_HAIL;
|
|
}
|
|
}
|
|
sim->dirMask = 0u;
|
|
sim->deathInProgress = 1u;
|
|
stSimPadLight(sim, true);
|
|
sim->bobTimer = 2u;
|
|
if (((uint8_t)sim->velY & 0x80u) == 0u) {
|
|
stSimSfx(sim, stC64SfxProgram(ST_SFX_LAND_SOFT));
|
|
return;
|
|
}
|
|
sim->bobTimer = 8u;
|
|
stSimSfx(sim, stC64SfxProgram(ST_SFX_LAND_HARD));
|
|
if (sim->stage == ST_STAGE_RIDING) {
|
|
stSimHudInit(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);
|
|
// $70D4: the bonus-cab jingle (song 4), blocking.
|
|
sim->jingleRequest = (uint8_t)(ST_JINGLE_SONG_BONUS + 1u);
|
|
}
|
|
|
|
|
|
// $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_FUEL_HALF) {
|
|
// The leftmost cell is down to its last half: out of fuel, so
|
|
// only FIRE survives and there is 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;
|
|
}
|
|
}
|
|
|
|
|
|
// ---- glyph reverse index (see StSimT in stSim.h) --------------------
|
|
|
|
// Take `cell` out of the list of whatever glyph it currently shows.
|
|
// Reads sim->screen[cell], so call it BEFORE the new character lands.
|
|
static void glyphIndexUnlink(StSimT *sim, uint16_t cell) {
|
|
uint16_t prev = sim->cellPrev[cell];
|
|
uint16_t next = sim->cellNext[cell];
|
|
|
|
if (prev == ST_GLYPH_CELL_NONE) {
|
|
sim->glyphHead[sim->screen[cell]] = next;
|
|
} else {
|
|
sim->cellNext[prev] = next;
|
|
}
|
|
if (next != ST_GLYPH_CELL_NONE) {
|
|
sim->cellPrev[next] = prev;
|
|
}
|
|
}
|
|
|
|
|
|
// Push `cell` onto the front of glyph `ch`'s list.
|
|
static void glyphIndexLink(StSimT *sim, uint16_t cell, uint8_t ch) {
|
|
uint16_t head = sim->glyphHead[ch];
|
|
|
|
sim->cellPrev[cell] = ST_GLYPH_CELL_NONE;
|
|
sim->cellNext[cell] = head;
|
|
if (head != ST_GLYPH_CELL_NONE) {
|
|
sim->cellPrev[head] = cell;
|
|
}
|
|
sim->glyphHead[ch] = cell;
|
|
}
|
|
|
|
|
|
// Re-derive every list from screen[]. Bulk screen writes (level load,
|
|
// title, the blanked intro) bypass setCell, and all of them call
|
|
// stSimDirtyAll, so that is the one place this has to hang off.
|
|
static void glyphIndexRebuild(StSimT *sim) {
|
|
uint16_t cell;
|
|
uint16_t k;
|
|
|
|
for (k = 0u; k < ST_CHARSET_CHARS; k++) {
|
|
sim->glyphHead[k] = ST_GLYPH_CELL_NONE;
|
|
}
|
|
// Built back to front, so each list comes out in ASCENDING cell
|
|
// order and the repaint walk moves forward through the stage the
|
|
// way the old span scan did.
|
|
cell = ST_SCREEN_CELLS;
|
|
while (cell != 0u) {
|
|
cell--;
|
|
glyphIndexLink(sim, cell, sim->screen[cell]);
|
|
}
|
|
}
|
|
|
|
|
|
// A sprite row (3 bytes, leftmost pixel = bit 7 of byte 0) shifted
|
|
// right by s (0..7) into a 4-byte window whose byte j holds the row's
|
|
// pixels 8j-s .. 8j-s+7: the row aligned to a grid of 8-pixel cells
|
|
// that its left edge sits s pixels into. Fixed shifts by 4, 2 and 1
|
|
// on 16-bit halves -- a variable-count shift is a library bit loop on
|
|
// the 65816, and a byte carry chain gets folded back into one.
|
|
static void rowWindow(const uint8_t *row, uint8_t s, uint8_t *w) {
|
|
uint16_t hi = (uint16_t)(((uint16_t)row[0] << 8) | row[1]);
|
|
uint16_t lo = (uint16_t)((uint16_t)row[2] << 8);
|
|
|
|
if ((s & 4u) != 0u) {
|
|
lo = (uint16_t)((lo >> 4) | (uint16_t)(hi << 12));
|
|
hi = (uint16_t)(hi >> 4);
|
|
}
|
|
if ((s & 2u) != 0u) {
|
|
lo = (uint16_t)((lo >> 2) | (uint16_t)(hi << 14));
|
|
hi = (uint16_t)(hi >> 2);
|
|
}
|
|
if ((s & 1u) != 0u) {
|
|
lo = (uint16_t)((lo >> 1) | (uint16_t)(hi << 15));
|
|
hi = (uint16_t)(hi >> 1);
|
|
}
|
|
w[0] = (uint8_t)(hi >> 8);
|
|
w[1] = (uint8_t)hi;
|
|
w[2] = (uint8_t)(lo >> 8);
|
|
w[3] = (uint8_t)lo;
|
|
}
|
|
|
|
|
|
static void setCell(StSimT *sim, uint16_t cell, uint8_t ch) {
|
|
if (sim->screen[cell] != ch) {
|
|
glyphIndexUnlink(sim, cell);
|
|
sim->screen[cell] = ch;
|
|
glyphIndexLink(sim, 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);
|
|
}
|
|
}
|
|
|
|
|
|
// $4320 -- the release countdown of voices 1 and 2. Gating a voice off
|
|
// when its count runs out is the audio layer's business; the counts are
|
|
// what the game reads.
|
|
static void sfxEnvelopeTick(StSimT *sim) {
|
|
uint8_t v;
|
|
|
|
for (v = 0u; v < ST_SID_TIMED_VOICES; v++) {
|
|
if (sim->sidRelTimer[v] != 0u) {
|
|
sim->sidRelTimer[v]--;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// 21 rows of 3-byte foreground masks for a displayed sprite, kept in
|
|
// collMask[idx] and rebuilt only when its bitmap or multicolor mode
|
|
// changes. A multicolor 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). The cab's cached windows die with its mask.
|
|
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 & kStBit[idx]) != 0u ? 1u : 0u);
|
|
uint8_t *row = sim->collMask[idx][0];
|
|
uint16_t n;
|
|
uint8_t first;
|
|
uint8_t last;
|
|
|
|
if (bm != 0 && bm == sim->collMaskBm[idx] && multi == sim->collMaskMulti[idx]) {
|
|
return;
|
|
}
|
|
for (n = 0u; n < (uint16_t)(ST_SPRITE_H * 3u); n++) {
|
|
uint8_t b = 0u;
|
|
if (bm != 0) {
|
|
b = bm[n];
|
|
if (multi != 0u) {
|
|
b = (uint8_t)((kPairMask[b >> 4] << 4) | kPairMask[b & 0x0Fu]);
|
|
}
|
|
}
|
|
row[n] = b;
|
|
}
|
|
first = ST_SPRITE_H;
|
|
last = 0u;
|
|
for (n = 0u; n < ST_SPRITE_H; n++) {
|
|
if ((row[n * 3u] | row[n * 3u + 1u] | row[n * 3u + 2u]) != 0u) {
|
|
if (first == ST_SPRITE_H) {
|
|
first = (uint8_t)n;
|
|
}
|
|
last = (uint8_t)n;
|
|
}
|
|
}
|
|
sim->collRowFirst[idx] = first;
|
|
sim->collRowLast[idx] = last;
|
|
sim->collMaskBm[idx] = bm;
|
|
sim->collMaskMulti[idx] = multi;
|
|
if (idx == 0u) {
|
|
sim->cabWinValid = 0u;
|
|
}
|
|
}
|
|
|
|
|
|
// A non-cab sprite's rows (shifted here, only rows with pixels) against
|
|
// the background rows under it.
|
|
static bool spriteRowsTouchBackground(const StSimT *sim, uint8_t idx, int16_t firstCol, int16_t py, uint8_t phase) {
|
|
uint8_t first = sim->collRowFirst[idx];
|
|
uint8_t left = (uint8_t)(sim->collRowLast[idx] + 1u - first);
|
|
const uint8_t *row;
|
|
|
|
if (first == ST_SPRITE_H) {
|
|
return false;
|
|
}
|
|
row = sim->collMask[idx][first];
|
|
py = (int16_t)(py + first);
|
|
// Counted down like cabRowsTouchBackground.
|
|
do {
|
|
if ((row[0] | row[1] | row[2]) != 0u) {
|
|
uint8_t w[4];
|
|
rowWindow(row, phase, w);
|
|
if (backgroundRowHits(sim, firstCol, py, w)) {
|
|
return true;
|
|
}
|
|
}
|
|
row += 3;
|
|
py++;
|
|
left--;
|
|
} while (left != 0u);
|
|
return false;
|
|
}
|
|
|
|
|
|
// One displayed sprite against the background pixels ($D01F). The
|
|
// background can only touch it 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).
|
|
// Needs spriteMasks(sim, idx) run for this frame first. The row tests
|
|
// live in cabRowsTouchBackground / spriteRowsTouchBackground, whose
|
|
// loops count DOWN -- see the note there before "simplifying" them.
|
|
static bool spriteTouchesBackground(StSimT *sim, uint8_t idx) {
|
|
int16_t px = (int16_t)((int16_t)sim->frame.x[idx] - ST_SPRITE_X_ORIGIN);
|
|
int16_t py = (int16_t)((int16_t)sim->frame.y[idx] - ST_SPRITE_Y_ORIGIN);
|
|
int16_t firstCol = cellOfPixel(px);
|
|
int16_t topRow = (py < 0) ? 0 : cellOfPixel(py);
|
|
int16_t botRow = cellOfPixel((int16_t)(py + (int16_t)ST_SPRITE_H - 1));
|
|
uint8_t phase = (uint8_t)(px - (int16_t)(firstCol << 3));
|
|
|
|
if (botRow >= (int16_t)ST_SCREEN_ROWS) {
|
|
botRow = (int16_t)(ST_SCREEN_ROWS - 1u);
|
|
}
|
|
if (!coveringCellsSolid(sim, firstCol, topRow, botRow)) {
|
|
return false;
|
|
}
|
|
if (idx == 0u) {
|
|
return cabRowsTouchBackground(sim, firstCol, py, phase);
|
|
}
|
|
return spriteRowsTouchBackground(sim, idx, firstCol, py, phase);
|
|
}
|
|
|
|
|
|
// $6946 -- the VIC's sprites off ($D015; the shadow enables are $4248's
|
|
// job), the collision latches cleared, nobody on a pad.
|
|
static void spritesOffReset(StSimT *sim) {
|
|
sim->spriteSpriteColl = 0u;
|
|
sim->spriteBgColl = 0u;
|
|
sim->activePad = 0u;
|
|
sim->activeSpriteIdx = 0u;
|
|
}
|
|
|
|
|
|
// Whether shown sprites a < b, b sitting dx right of a and dy below it
|
|
// (both inside a sprite's size), have a pixel in common. With dx = 8q - s
|
|
// (s in 0..7), b's row byte k lines up with byte k + q of a's row shifted
|
|
// to phase s, and only 0 <= k + q <= 3 can overlap; rows r of a meet rows
|
|
// r - dy of b, and only the rows where both have pixels are walked. The
|
|
// cab's shifted rows are cached (cabWindows) -- it is a side of most
|
|
// tests -- any other sprite's are shifted as they are needed.
|
|
static bool spritesTouch(StSimT *sim, uint8_t a, uint8_t b, int16_t dx, int16_t dy) {
|
|
uint8_t s;
|
|
int16_t q;
|
|
uint8_t kLo;
|
|
uint8_t kHi;
|
|
uint8_t r;
|
|
uint8_t rHi;
|
|
int16_t lo;
|
|
int16_t hi;
|
|
const uint8_t *win = 0;
|
|
const uint8_t *bRow;
|
|
uint8_t own[4];
|
|
|
|
spriteMasks(sim, a);
|
|
spriteMasks(sim, b);
|
|
if (sim->collRowFirst[a] == ST_SPRITE_H || sim->collRowFirst[b] == ST_SPRITE_H) {
|
|
return false;
|
|
}
|
|
s = (uint8_t)((0 - dx) & (ST_ROW_PHASES - 1));
|
|
q = (int16_t)(((dx + (int16_t)s + 32) >> 3) - 4);
|
|
kLo = (q < 0) ? (uint8_t)(0 - q) : 0u;
|
|
kHi = (q > 1) ? (uint8_t)(3 - q) : 2u;
|
|
lo = (int16_t)sim->collRowFirst[a];
|
|
hi = (int16_t)(sim->collRowLast[a] + 1);
|
|
if ((int16_t)sim->collRowFirst[b] + dy > lo) {
|
|
lo = (int16_t)((int16_t)sim->collRowFirst[b] + dy);
|
|
}
|
|
if ((int16_t)sim->collRowLast[b] + 1 + dy < hi) {
|
|
hi = (int16_t)((int16_t)sim->collRowLast[b] + 1 + dy);
|
|
}
|
|
if (lo >= hi) {
|
|
return false;
|
|
}
|
|
r = (uint8_t)lo;
|
|
rHi = (uint8_t)hi;
|
|
bRow = sim->collMask[b][(uint8_t)((int16_t)r - dy)];
|
|
if (a == 0u) {
|
|
win = cabWindows(sim, s) + (uint16_t)r * 4u;
|
|
}
|
|
for (; r < rHi; r++, bRow += 3) {
|
|
const uint8_t *w = win;
|
|
uint8_t k;
|
|
if (a == 0u) {
|
|
win += 4;
|
|
} else {
|
|
rowWindow(sim->collMask[a][r], s, own);
|
|
w = own;
|
|
}
|
|
for (k = kLo; k <= kHi; k++) {
|
|
if ((bRow[k] & w[(uint8_t)((int16_t)k + q)]) != 0u) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
// $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 = ST_SPRITE_CAB_RIGHT;
|
|
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 = ST_SPRITE_CAB_LEFT;
|
|
} else if ((sim->dirMask & 0x08u) != 0u) {
|
|
base = ST_SPRITE_CAB_RIGHT;
|
|
} 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;
|
|
}
|
|
|
|
|
|
// $6909 / $6C02 -- voices 1 and 2 gated off, their release timers cleared.
|
|
static void voicesOff(StSimT *sim) {
|
|
stAudioSilence();
|
|
memset(sim->sidRelTimer, 0, sizeof(sim->sidRelTimer));
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
|
|
// $6C38 -- blank the message row (color 1, as the original writes it).
|
|
void stSimClearMessage(StSimT *sim) {
|
|
stSimDrawText(sim, ST_MESSAGE_COL, ST_MESSAGE_ROW, kTextBlank, 1u);
|
|
}
|
|
|
|
|
|
// $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 color.
|
|
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++;
|
|
}
|
|
}
|
|
|
|
|
|
// A character's eight 1bpp rows: the game's edit of it, or the const ROM
|
|
// glyph when it has never been edited.
|
|
const uint8_t *stSimGlyph(const StSimT *sim, uint8_t ch) {
|
|
uint8_t slot = sim->glyphSlot[ch];
|
|
|
|
if (slot != 0u) {
|
|
return sim->glyphOverride[slot - 1u].rows;
|
|
}
|
|
return &stC64Charset()[(uint16_t)ch << 3];
|
|
}
|
|
|
|
|
|
// The writable eight rows, copied out of ROM on the first edit. A full
|
|
// table parks the edit in the spill entry rather than stealing another
|
|
// character's slot: that glyph then simply stays at its ROM form.
|
|
uint8_t *stSimGlyphMut(StSimT *sim, uint8_t ch) {
|
|
uint8_t slot = sim->glyphSlot[ch];
|
|
|
|
if (slot == 0u) {
|
|
if (sim->glyphOverrideCount >= ST_GLYPH_OVERRIDES) {
|
|
return sim->glyphOverride[ST_GLYPH_OVERRIDES].rows;
|
|
}
|
|
sim->glyphOverrideCount++;
|
|
slot = sim->glyphOverrideCount;
|
|
sim->glyphSlot[ch] = slot;
|
|
memcpy(sim->glyphOverride[slot - 1u].rows, &stC64Charset()[(uint16_t)ch << 3], 8u);
|
|
}
|
|
return sim->glyphOverride[slot - 1u].rows;
|
|
}
|
|
|
|
|
|
// Every glyph back to the ROM set (a scene load).
|
|
void stSimGlyphReset(StSimT *sim) {
|
|
memset(sim->glyphSlot, 0, sizeof(sim->glyphSlot));
|
|
sim->glyphOverrideCount = 0u;
|
|
}
|
|
|
|
|
|
// One row of a character's glyph. The collision background test reads
|
|
// four of these per sprite row, so it skips the pointer round trip.
|
|
uint8_t stSimGlyphRow(const StSimT *sim, uint8_t ch, uint8_t row) {
|
|
uint8_t slot = sim->glyphSlot[ch];
|
|
|
|
if (slot != 0u) {
|
|
return sim->glyphOverride[slot - 1u].rows[row];
|
|
}
|
|
return stC64Charset()[((uint16_t)ch << 3) + row];
|
|
}
|
|
|
|
|
|
// $43A5 -- blank the fare meter back to its template.
|
|
void stSimHudInit(StSimT *sim) {
|
|
uint8_t k;
|
|
|
|
for (k = 0u; k < ST_NUMBER_CHARS; k++) {
|
|
setCell(sim, (uint16_t)(ST_CELL_FARE + k), kHudTemplate[k]);
|
|
}
|
|
}
|
|
|
|
|
|
// $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));
|
|
sim->padCount = level->padCount;
|
|
sim->specialPad = level->specialPad;
|
|
sim->bgCollSprites = 0u;
|
|
memcpy(sim->screen, level->screen, ST_SCREEN_CELLS);
|
|
memcpy(sim->color, level->color, ST_SCREEN_CELLS);
|
|
stSimDirtyAll(sim);
|
|
stSimGlyphReset(sim);
|
|
sim->charDirtyAll = true;
|
|
sim->charDirtyCount = 0u;
|
|
// 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;
|
|
stHookSceneLoad(sim);
|
|
spritesOffReset(sim);
|
|
stHookPrelude0(sim);
|
|
stFareTakeoffSetup(sim);
|
|
stSimRespawn(sim);
|
|
}
|
|
|
|
|
|
// $5F27 -- the respawn half of the prelude.
|
|
void stSimRespawn(StSimT *sim) {
|
|
taxiSpawnInit(sim);
|
|
stHookPrelude1(sim);
|
|
// $6906 framePresent: the start-of-screen jingle, then the SID (and
|
|
// the demo RNG) reset. $6935: level 25 gets song 4, every other
|
|
// screen rotates songs 0..3 on $5E9B. The host plays it with the sim
|
|
// frozen, which is what the C64's blocking play-and-wait amounts to.
|
|
stSimMarshal(sim);
|
|
voicesOff(sim);
|
|
sim->jingleRotate++;
|
|
if (sim->levelState == ST_JINGLE_LEVEL_25) {
|
|
sim->jingleRequest = (uint8_t)(ST_JINGLE_SONG_BONUS + 1u);
|
|
} else {
|
|
sim->jingleRequest = (uint8_t)((sim->jingleRotate & 3u) + 1u);
|
|
}
|
|
sim->spriteSpriteColl = 0u;
|
|
sim->spriteBgColl = 0u;
|
|
// $6920 cleared the latch, but the VIC goes on showing this frame
|
|
// through the jingle, so the first tick's read still sees it.
|
|
sim->prevFrameSpriteColl = frameSpriteCollisions(sim);
|
|
sim->rngT1 = 0x06u;
|
|
sim->rngT2 = 0x17u;
|
|
stFareTakeoffVoice(sim);
|
|
}
|
|
|
|
|
|
// $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. So does
|
|
// the displayed picture -- screen RAM, color RAM and the charset
|
|
// are not game state, and the attract demo's "GET READY" banner is
|
|
// drawn over the still-live title screen -- so the clear starts at
|
|
// ST_SIM_CLEAR_FROM. Every level entry rebinds the image anyway
|
|
// (stSimEnterLevel), so nothing downstream sees the difference.
|
|
memcpy(keepBuf, sim->demoBuf, sizeof(keepBuf));
|
|
memset((uint8_t *)sim + ST_SIM_CLEAR_FROM, 0, sizeof(*sim) - ST_SIM_CLEAR_FROM);
|
|
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;
|
|
}
|
|
|
|
|
|
// $6B64..$6BCD -- the game's side of a finished wreck (the attract
|
|
// demo returns to the title instead): a fare aboard is lost -- its meter,
|
|
// its pads, an open hatch, its message -- then the player's cab count
|
|
// drops. With cabs left the host respawns the cab (stSimRespawn, $5F27).
|
|
StTickResultE stSimLoseCab(StSimT *sim) {
|
|
uint8_t p;
|
|
|
|
if (sim->stage == ST_STAGE_RIDING) {
|
|
stSimHudInit(sim);
|
|
if (sim->fareSlotCount != 0u) {
|
|
sim->spriteSlots[sim->activeDyingSlot] = 0u;
|
|
sim->fareSlotCount--;
|
|
if (sim->activeSpriteIdx == ST_FARE_DEST_UP) {
|
|
uint8_t k;
|
|
for (k = 0u; k < ST_TRANSPORTER_CELLS; 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;
|
|
stSimClearMessage(sim);
|
|
}
|
|
// $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;
|
|
}
|
|
|
|
|
|
// Everything must be repainted.
|
|
// A glyph's bits changed: the renderer has to repaint every cell showing
|
|
// that character. The list is short (one or two per frame), so a linear
|
|
// scan for duplicates is cheaper than any index; an overflow just says
|
|
// "all of them" and costs one full repaint.
|
|
void stSimMarkChar(StSimT *sim, uint8_t ch) {
|
|
uint8_t k;
|
|
|
|
if (sim->charDirtyAll) {
|
|
return;
|
|
}
|
|
for (k = 0u; k < sim->charDirtyCount; k++) {
|
|
if (sim->charDirtyList[k] == ch) {
|
|
return;
|
|
}
|
|
}
|
|
if (sim->charDirtyCount == ST_CHAR_DIRTY_MAX) {
|
|
sim->charDirtyAll = true;
|
|
return;
|
|
}
|
|
sim->charDirtyList[sim->charDirtyCount] = ch;
|
|
sim->charDirtyCount++;
|
|
}
|
|
|
|
|
|
// $4253 + $4293 -- snapshot the sprite shadows into the frame the VIC
|
|
// shows next. The title and level-intro loops flush the same way.
|
|
void stSimMarshal(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 |= kStBit[i];
|
|
}
|
|
}
|
|
sim->frame.enableMask = enable;
|
|
sim->frame.multiMask = sim->multiColorMask;
|
|
}
|
|
|
|
|
|
// Every cell repaints AND the glyph index is rebuilt: for callers that
|
|
// replaced the screen bytes wholesale (a level load, the title scene).
|
|
void stSimDirtyAll(StSimT *sim) {
|
|
stSimDirtyCells(sim);
|
|
// Every bulk screen write calls this, and those bypass setCell, so
|
|
// this is where the glyph reverse index gets re-derived.
|
|
glyphIndexRebuild(sim);
|
|
}
|
|
|
|
|
|
// Every cell repaints; the screen bytes are as they were, so the glyph
|
|
// index stands. A rebuild is ~0.15 s on the IIgs (1000 far-pointer
|
|
// list links), and the level-entry path used to pay it twice.
|
|
void stSimDirtyCells(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 color into color 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);
|
|
}
|
|
|
|
|
|
// $63D0 -- rotate every row of a glyph one pixel right and mark every
|
|
// cell showing it for repaint: the transporter hatch and the level O
|
|
// electroids both animate this way.
|
|
void stSimRotateGlyph(StSimT *sim, uint8_t ch) {
|
|
uint8_t *rows = stSimGlyphMut(sim, ch);
|
|
uint8_t row;
|
|
|
|
for (row = 0u; row < 8u; row++) {
|
|
uint8_t b = rows[row];
|
|
rows[row] = (uint8_t)((b >> 1) | (b << 7));
|
|
}
|
|
stSimMarkChar(sim, ch);
|
|
}
|
|
|
|
|
|
// 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];
|
|
}
|
|
|
|
|
|
// $42E9 -- an SFX program starts; voices 0 and 1 arm their release timer.
|
|
void stSimSfx(StSimT *sim, const uint8_t *program9) {
|
|
uint8_t voice = program9[ST_SFX_VOICE_AT];
|
|
|
|
if (voice < ST_SID_TIMED_VOICES) {
|
|
sim->sidRelTimer[voice] = program9[ST_SFX_RELEASE_AT];
|
|
}
|
|
stAudioSfx(program9);
|
|
}
|
|
|
|
|
|
// $411B -- add a signed byte to a sprite's 17-bit X, carrying into the
|
|
// msb shadow.
|
|
void stSimSpriteAddX(StSimT *sim, uint8_t idx, uint8_t delta) {
|
|
uint16_t x = (uint16_t)(stSimSpriteX(sim, idx) + (uint16_t)(int16_t)(int8_t)delta);
|
|
|
|
sim->spr[idx].col = (uint8_t)x;
|
|
sim->spr[idx].msb = (uint8_t)(x >> 8);
|
|
}
|
|
|
|
|
|
// 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;
|
|
}
|
|
|
|
|
|
// The 17-bit VIC sprite X the shadow tables hold as msb:col.
|
|
uint16_t stSimSpriteX(const StSimT *sim, uint8_t idx) {
|
|
return (uint16_t)(((uint16_t)sim->spr[idx].msb << 8) | sim->spr[idx].col);
|
|
}
|
|
|
|
|
|
// $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);
|
|
voicesOff(sim);
|
|
stSimArchiveHud(sim);
|
|
return ST_TICK_LEVEL_EXIT;
|
|
}
|
|
padLandingBob(sim);
|
|
fuelTick(sim);
|
|
stFareEventVoice(sim);
|
|
}
|
|
stFarePostTickGate(sim);
|
|
stFareStageDispatch(sim);
|
|
stHookPerTick2(sim);
|
|
// $5F94: the frame drawn while the CPU waits shows the last flush's
|
|
// sprites over the picture as this tick has left it.
|
|
sim->prevFrameBgColl = frameBgCollisions(sim);
|
|
stSimMarshal(sim);
|
|
stSimRotateGlyph(sim, ST_CHAR_TRANSPORTER);
|
|
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);
|
|
sfxEnvelopeTick(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;
|
|
}
|
|
|