// 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 #include #include #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