joeylib2/examples/spacetaxi/stSim.h
2026-09-07 15:46:23 -05:00

373 lines
17 KiB
C

// Space Taxi -- the C64 game simulation, host-independent.
//
// This is a routine-for-routine re-expression of the original's game
// tick (the $5F40 main loop) in C, working in the C64's own units:
// VIC sprite coordinates (sprite-X 24 = visible column 0, sprite-Y 50
// = visible row 0), 8-bit sub-pixel velocities, screen RAM character
// codes, the per-level pad table as shipped, and the same state
// machines with the same timers. Nothing in here touches JoeyLib, so
// the same code runs inside a plain host build (tools/stsim) where it
// is checked tick-for-tick against a VICE trace of the real game.
//
// The renderer reads this state (sprite snapshot, screen RAM, charset)
// and the audio layer receives events through the stAudio* hooks
// declared at the bottom, which each host implements.
//
// Address comments ($xxxx) name the original variable or routine a
// field or function mirrors; see stuff/spacetaxi/labels.txt.
#ifndef ST_SIM_H
#define ST_SIM_H
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include "stC64Data.h"
// Screen geometry: screen RAM is 40x25 character cells, all of which
// belong to the level image (the HUD rows included).
#define ST_SCREEN_COLS 40u
#define ST_SCREEN_ROWS 25u
#define ST_SCREEN_CELLS (ST_SCREEN_COLS * ST_SCREEN_ROWS)
#define ST_CELL(row, col) ((uint16_t)((row) * ST_SCREEN_COLS + (col)))
// VIC sprite coordinate origins: the visible picture starts at
// sprite-X 24 and sprite-Y 50.
#define ST_SPRITE_X_ORIGIN 24
#define ST_SPRITE_Y_ORIGIN 50
#define ST_SPRITE_W 24
#define ST_SPRITE_H 21
#define ST_HW_SPRITES 8u
// Fixed screen cells the game writes directly (C64 screen addresses).
#define ST_CELL_TRANSPORTER ST_CELL(0, 18) // $0412..$0415, 4 cells
#define ST_CELL_CAB_ICONS ST_CELL(23, 1) // $0799..$079F
#define ST_CELL_FUEL ST_CELL(23, 14) // $07A6..$07B1, 12 cells
#define ST_CELL_FUEL_LAST ST_CELL(23, 25) // $07B1
#define ST_CELL_SCREENS ST_CELL(23, 32) // $07B8..$07BC
#define ST_CELL_SCORE ST_CELL(24, 2) // $07C2..$07C8, 7 chars
#define ST_CELL_MESSAGE ST_CELL(24, 14) // $07CE, 11 chars
#define ST_CELL_PAD_DIGIT ST_CELL(24, 18) // $07D2
#define ST_CELL_FARE ST_CELL(24, 32) // $07E0..$07E6, 7 chars
#define ST_NUMBER_CHARS 7u
#define ST_FUEL_CELLS 12u
#define ST_LEVEL_NAME_CHARS 22u
// Screen-code glyphs the HUD arithmetic works on ($4345/$4354/$440B):
// inverse-video digits 1..9 are $6B..$73, zero is $74, a leading blank
// is $66 and the decimal point is $77.
#define ST_CHAR_BLANK 0x66u
#define ST_CHAR_DIGIT_BASE 0x6Au // digit d (1..9) = base + d
#define ST_CHAR_ZERO 0x74u
#define ST_CHAR_POINT 0x77u
#define ST_CHAR_SPACE 0x20u
#define ST_CHAR_FUEL_HALF 0x7Bu // half a cell of fuel left
#define ST_CHAR_FUEL_EMPTY 0x7Au
#define ST_CHAR_TRANSPORTER 0x67u // animated top-wall hatch
#define ST_CHAR_CAB_ICON 0xC8u
#define ST_CHAR_MENU_SENTINEL 0x7Bu // $07A6 == $7B: first fuel cell half = out of fuel
// Level image as loaded from a .dat (STL4). Kept in the C64's units.
#define ST_MAX_PADS 10u
#define ST_MAX_LEVEL_SPRITES 16u
// Per-level hook code + tables live at $7D98..$7FFF in the level's data
// blob; the port keeps the bytes (for the tables) and re-expresses the
// code in stHooks.c.
#define ST_HOOK_BASE 0x7D98u
#define ST_HOOK_BYTES (0x8000u - ST_HOOK_BASE)
typedef struct {
uint8_t x1Hi; // slot byte 0: landing X lower bound, high byte
uint8_t x1Lo; // slot byte 1
uint8_t x2Hi; // slot byte 2: landing X upper bound
uint8_t x2Lo; // slot byte 3
uint8_t row; // slot byte 4: sprite-Y of a landed cab
uint8_t passMsb; // slot byte 5: passenger stand X msb
uint8_t passCol; // slot byte 6: passenger stand X column
uint8_t unused; // slot byte 7
} StPadT;
typedef struct {
uint8_t ptr;
uint8_t bitmap[ST_SPRITE_BYTES];
} StLevelSpriteT;
typedef struct {
uint8_t name[ST_LEVEL_NAME_CHARS + 1u]; // $7D78, screen codes
uint8_t header[9]; // $7D00..$7D08 VIC colours
uint8_t padCount; // $7D5A
uint8_t specialPad; // $7D09
uint8_t fuelRate; // $7D60
uint8_t spawn[5]; // $7D5B..$7D5F
uint16_t accelY; // $7D8F/90
uint16_t accelX; // $7D91/92
uint16_t gravY; // $7D93/94
uint16_t gravX; // $7D95/96
StPadT pads[ST_MAX_PADS]; // $7D0A..
uint8_t screen[ST_SCREEN_CELLS]; // decompressed screen RAM
uint8_t color[ST_SCREEN_CELLS]; // decompressed colour RAM
uint8_t levelIndex; // 0..23 = A..X, 24 = title
uint8_t spriteCount;
StLevelSpriteT sprites[ST_MAX_LEVEL_SPRITES];
uint8_t hookData[ST_HOOK_BYTES]; // $7D98..$7FFF as shipped
} StLevelT;
// One VIC sprite as the game sees it through its shadow tables.
typedef struct {
uint8_t col; // $7175,X X low byte
uint8_t msb; // $7185,X X high bit (any non-zero value = set)
uint8_t row; // $717D,X Y
uint8_t enable; // $718E,X
uint8_t ptr; // $7197,X block pointer
uint8_t color; // $719F,X
} StSpriteT;
// The frame the VIC last displayed ($4253 marshal + $4293 flush): the
// renderer draws this and the collision test evaluates it.
typedef struct {
uint16_t x[ST_HW_SPRITES];
uint8_t y[ST_HW_SPRITES];
uint8_t ptr[ST_HW_SPRITES];
uint8_t color[ST_HW_SPRITES];
uint8_t enableMask; // $7196 -> $D015
uint8_t multiMask; // $D01C
} StFrameT;
// Four players ("cabbies") share one game; each keeps their own score
// row, fare row, cab count and bonus-cab latch.
#define ST_MAX_PLAYERS 4u
#define ST_CABS_PER_PLAYER 6u // $4210
// Passenger/fare state machine stages ($7163).
typedef enum {
ST_STAGE_IDLE = 0, // $660B RNG-timed next fare
ST_STAGE_BEAM_IN, // $665F passenger materialises
ST_STAGE_WAIT, // $66B7 waves on the pad
ST_STAGE_WALK_TO_CAB, // $6CE8 walks to the landed cab
ST_STAGE_BOARD, // $66DD shrinks into the cab
ST_STAGE_RIDING, // $6739 aboard
ST_STAGE_DROP_IN, // $6742 materialises beside the cab
ST_STAGE_WALK_TO_PAD, // $6CE8 walks to the stand
ST_STAGE_LEAVE, // $67A6 shrinks away
ST_STAGE_CLEANUP // $67C6
} StStageE;
// What a tick asked the host to do next (the original abandons the
// main loop with PLA/PLA on these).
typedef enum {
ST_TICK_CONTINUE = 0,
ST_TICK_LEVEL_EXIT, // $6BFB cab flew out of the top
ST_TICK_CRASH_DONE, // $6B52 wreck sequence finished (demo)
ST_TICK_LIFE_LOST, // $6BBA wreck sequence finished (game), cabs left
ST_TICK_PLAYER_OUT, // $6BD0 wreck sequence finished, no cabs left
ST_TICK_DEMO_INPUT // $5006 real input during the demo
} StTickResultE;
typedef struct {
// ---- level image (mutable copies: hooks edit the pad table) ----
const StLevelT *level;
StPadT pads[ST_MAX_PADS]; // $7D0A working copy
uint8_t screen[ST_SCREEN_CELLS]; // $0400 screen RAM
uint8_t color[ST_SCREEN_CELLS]; // $D800 colour RAM
uint8_t cellDirty[ST_SCREEN_CELLS]; // renderer clears
uint16_t dirtyList[512]; // cells marked since the last frame
uint16_t dirtyCount;
bool dirtyAll; // list overflowed / whole screen
uint8_t charset[ST_CHARSET_CHARS][8]; // $2800 working copy
uint8_t charDirty[ST_CHARSET_CHARS]; // renderer clears
uint8_t borderColor; // $D020
uint8_t bgColor; // $D021
// ---- input ----
uint8_t inputMask; // $7169 EOR $FF of $DC00: 1 = pressed
uint8_t dirMask; // $716A low 4 bits of the above
uint8_t fireWasHeld; // $71BD
uint8_t rawInput; // the host's joystick byte for this tick
// ---- cab motion ----
uint8_t posXlo; // $7D61
uint8_t posXcol; // $7D62
uint8_t posXmsb; // $7D63
uint8_t posYlo; // $7D64
uint8_t posYrow; // $7D65
int16_t velX; // $714C/4D
int16_t velY; // $714E/4F
int16_t accelX; // $7148/49
int16_t accelY; // $714A/4B
uint16_t accelTemplateX; // $7D91/92 working copy
uint16_t accelTemplateY; // $7D8F/90
uint16_t gravTemplateX; // $7D95/96
uint16_t gravTemplateY; // $7D93/94
uint8_t activePad; // $7150 0 = airborne, else pad number
uint8_t activePadMirror; // $7151 UP-release latch after landing
uint8_t bobTimer; // $716F
uint8_t flameParity; // $716C
uint8_t thrustSweep; // $721B
// ---- collision / crash ----
uint8_t collisionPhase; // $7164
uint8_t phaseTimer; // $7165
uint8_t phaseReload; // $7166
uint8_t hitDispatchResult; // $721D
uint8_t spriteSpriteColl; // $71CA latched $D01E
uint8_t spriteBgColl; // $71CB latched $D01F
// Per-sprite 24-bit row masks of the displayed frame, rebuilt only
// when the sprite's bitmap or multicolour mode changes (derived
// state, not C64 memory).
uint32_t collMask[ST_HW_SPRITES][ST_SPRITE_H];
const uint8_t *collMaskBm[ST_HW_SPRITES];
uint8_t collMaskMulti[ST_HW_SPRITES];
// ---- fuel ----
uint8_t fuelCountdown; // $71C7
uint8_t fuelCells; // $71C9 rightmost non-empty cell index
uint8_t fuelBarTick; // $71BE
uint8_t padAnimTick; // $71C8
// ---- passenger / fare machine ----
uint8_t stage; // $7163
uint8_t deathInProgress; // $7167
uint8_t decayTimer; // $715D
uint8_t decayReload; // $715E
uint8_t hoverXCol; // $715F
uint8_t hoverXFrac; // $7160 (the X msb of the target)
uint8_t walkParity; // $7161
uint8_t waveIdx; // $716E
uint8_t stage0Rng; // $71CC
uint8_t spriteSlots[11]; // $7152..$715C: [n] = pad n reserved
uint8_t fareSlotCount; // $715C
uint8_t activeSpriteIdx; // $7D8E fare's pad (spawn, then destination)
uint8_t activeDyingSlot; // $716D
uint8_t eventDispatchType; // $71CD
// ---- sprites ----
StSpriteT spr[ST_HW_SPRITES];
uint8_t multiColorMask; // $D01C
StFrameT frame;
// ---- players and progression ----
uint8_t playerCount; // $7213 "number of cabbies"
uint8_t player; // $7214 whose turn
uint8_t playersDone; // $71CE
uint8_t cabs[ST_MAX_PLAYERS]; // $71CF
uint8_t bonusLatch[ST_MAX_PLAYERS]; // $7223
uint8_t scoreBackup[ST_MAX_PLAYERS][ST_NUMBER_CHARS]; // $71D3, stride 8
uint8_t fareBackup[ST_MAX_PLAYERS][ST_NUMBER_CHARS]; // $71F3, stride 8
uint8_t levelState; // $7215 screens completed
uint8_t demoMode; // $721C
uint8_t postMortem; // $5E9D
// ---- demo playback ($48F2): the $0902 buffer a recording is
// loaded into; a shorter recording leaves the previous tail behind
// and a long ride reads on into it, as on the C64 ----
uint8_t demoBuf[640];
uint16_t demoOff; // $3F/$40 - $0902
uint8_t demoTimer; // $4740
// ---- RNG ($4080) ----
uint8_t rngT1; // $7171
uint8_t rngT2; // $7172
uint32_t rngHost; // stands in for the SID noise read in a real game
// ---- title intro / level intro ----
uint8_t introFrameCount; // $473F
uint8_t logoColorIdx; // $48A4
uint8_t logoCycleAux; // $48A5
uint8_t logoDiv; // $48A6
uint8_t starTimer[7]; // $44F8
uint8_t starReload[7]; // $4500
uint8_t introLoopCount; // $4508
uint8_t introCabDx; // $450A
uint8_t introCabDxTimer; // $450B
uint8_t spriteMc0; // $D025
uint8_t spriteMc1; // $D026
// ---- per-level hook scratch ($7D9C.. and friends) ----
uint8_t hookRam[ST_HOOK_BYTES]; // $7D98..$7FFF working copy
uint8_t hookLevel; // level index the hook state belongs to
uint8_t warpCounter; // $5A66
uint8_t warpMask; // $5CE8
uint8_t animPhase; // $5CF1
} StSimT;
// ---- simulation API (stSim.c) ----
// Bind a level and run the scene-load + prelude ($62F0 + $5F18) for a
// fresh screen: copy the image, restore the player's HUD rows, draw
// the cab icons, spawn the cab, refill the fuel.
void stSimEnterLevel(StSimT *sim, const StLevelT *level);
// Re-entry after a crash with cabs left ($5F27): spawn + refill only.
void stSimRespawn(StSimT *sim);
// One game tick = one $5F40 iteration (two video frames on the C64).
StTickResultE stSimTick(StSimT *sim);
// Seed the whole struct for a new game ($5EC4 + $4207 + $48AD).
void stSimNewGame(StSimT *sim, uint8_t playerCount, bool demo);
// Seed the playback buffer with the boot-time RAM image, once.
void stSimSeedDemoBuffer(StSimT *sim, const uint8_t *image, uint16_t len);
// Load a recording into the buffer and rewind ($0902 + $4740 seeding).
void stSimSetDemoStream(StSimT *sim, const uint8_t *stream, uint16_t len);
// Per-player bookkeeping at a screen change ($61FB advanceFareSlot):
// returns true when the turn wrapped to player 0 (time for a new screen).
bool stSimAdvancePlayer(StSimT *sim);
// Archive the current player's HUD rows ($704E).
void stSimArchiveHud(StSimT *sim);
// Mark every cell dirty (scene change).
void stSimDirtyAll(StSimT *sim);
// Text and HUD primitives (screen RAM writes).
void stSimDrawText(StSimT *sim, uint8_t col, uint8_t row, const uint8_t *text, uint8_t color);
void stSimPutChar(StSimT *sim, uint8_t col, uint8_t row, uint8_t ch);
void stSimPutColor(StSimT *sim, uint8_t col, uint8_t row, uint8_t color);
void stSimBcdAdd(StSimT *sim, uint16_t dstCell, const uint8_t *blob);
bool stSimDecrementNumber(StSimT *sim, uint16_t cell, uint8_t position);
uint8_t stSimRng(StSimT *sim, uint8_t n);
// Sprite-mask lookup shared with the renderer: 21 rows of 24-bit masks.
const uint8_t *stSimSpriteBitmap(const StSimT *sim, uint8_t ptr);
// The score the current player has on screen, as pennies.
uint32_t stSimScorePennies(const StSimT *sim);
// $6866 / $6877 -- the pad indicator colour cells.
void stSimPadLight(StSimT *sim, bool on);
// $6D6A -- the exhaust sprite (shared with the title lift-off).
void stSimFlameUpdate(StSimT *sim);
// ---- level file (stLevelFile.c) ----
bool stLevelParse(StLevelT *out, FILE *fp);
// ---- passenger machine (stFare.c) ----
void stFareStageDispatch(StSimT *sim); // $65C1
void stFarePostTickGate(StSimT *sim); // $67E3
void stFarePadLightingGate(StSimT *sim); // $6EF0
void stFareUpPlease(StSimT *sim); // $6CB8
uint8_t stFareChooseDestination(StSimT *sim); // $6537: reserves a pad, returns its number
void stFareDrawMessage(StSimT *sim, const uint8_t *text);
void stFareSquashed(StSimT *sim); // $69B4..$69F8
void stFareBoardingSound(StSimT *sim, const uint8_t *speech); // $6C7F
void stFareWaveStep(StSimT *sim); // $66B7
void stFareWalkStep(StSimT *sim); // $6D0D
void stFareLeaveStep(StSimT *sim); // $67A6
// ---- title screen and level intro (stTitle.c) ----
void stTitleEnter(StSimT *sim, const StLevelT *title); // $477A + $47B0..$47E1
bool stTitleTick(StSimT *sim); // $47E4 loop body; true once the intro cab has left
void stIntroEnter(StSimT *sim, const StLevelT *level); // $4523..$4665 level-name screen
bool stIntroStep(StSimT *sim); // one $4666 frame; true when finished
// ---- per-level hooks (stHooks.c) ----
void stHookPrelude0(StSimT *sim); // $7D66
void stHookPrelude1(StSimT *sim); // $7D69
void stHookPerTick2(StSimT *sim); // $7D6C
void stHookPerTick3(StSimT *sim); // $7D6F
uint8_t stHookInput(StSimT *sim, uint8_t input); // $7D72
uint8_t stHookHitVerdict(StSimT *sim); // $7D75: 0 = passenger contact is safe
// ---- audio sink (host provides; stAudio.c / stsim stub) ----
void stAudioSfx(const uint8_t *program9); // $42E9 load
void stAudioNoise(bool on); // $D412 = $81 / $80
void stAudioThrustSweep(uint8_t value); // $D400/$D401 = value
void stAudioSpeech(uint8_t ch); // $9802
void stAudioSilence(void); // voices 1+2 gate off
void stAudioVoice2(uint8_t freqLo, uint8_t freqHi, uint8_t ctrl); // level-hook tones
void stAudioVoice1Freq(uint8_t value); // $D400/$D401 pokes from the fuel pump
#endif