// 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 #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) // End-of-list / not-linked sentinel for the glyph reverse index below. // ST_SCREEN_CELLS is 1000, so no real cell index can collide with it. #define ST_GLYPH_CELL_NONE 0xFFFFu // row * 40 + col. Written as shifts (40 == 32 + 8) because the 65816 has // no multiply: with a variable row the * form called the compiler's // software multiply helper, and the collision pre-check does that a // dozen times a tick. Constant rows still fold to a constant. #define ST_CELL(row, col) ((uint16_t)((((uint16_t)(row)) << 5) + (((uint16_t)(row)) << 3) + (uint16_t)(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 // A sprite row shifted onto the 8-pixel character grid has one of 8 // phases (its left edge 0..7 pixels into a cell) -- see rowWindow. #define ST_ROW_PHASES 8 #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_MESSAGE_COL 14u #define ST_MESSAGE_ROW 24u #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_ELECTROID 0x6Fu // level O's scrolling barrier glyph #define ST_FARE_DEST_UP 0x0Bu // $7D8E: the fare aboard wants UP // ---- level-intro starfield -------------------------------------------- // // The C64's level intro streams seven star SPRITES out of the centre of // the screen. That is the most expensive thing this port asks a present // for: eight sprites are save-under drawn and restored every frame, and // because a row's dirty band is one min..max interval, two stars far // apart on the same row widen that row to nearly full width. Radially // spreading stars maximise exactly that, so the warp ends up presenting // close to the whole framebuffer every frame -- which is what makes it // the slowest thing the IIgs does, and wasted work everywhere else. // // So the warp is not animated in pixels at all. The starfield is painted // ONCE into screen RAM and animated by cycling the colours its cells are // drawn in -- the same trick the title logo already uses // (stRenderTitleLogo). A frame costs one palette upload and NO dirty // pixel rows. // // The contract between the two halves is just this colour range: // stTitle.c colours a star's cell ST_STAR_RING_FIRST + ring (ring 0 is // the centre), and the renderer cycles that range. These are C64 colour // indices the intro screen never uses -- it draws in 0, 3 and, in demo // mode, 4 and 5; the cab sprite adds 2, 6 and 7. #define ST_STAR_RING_FIRST 8u #define ST_STAR_RING_COUNT 8u // The renderer wraps the sweep's phase with a mask rather than a // modulo, so the ring count has to stay a power of two. Changing it to // something else must fail to compile here instead of quietly making // the warp skip rings. typedef char stStarRingCountIsPow2[((ST_STAR_RING_COUNT & (ST_STAR_RING_COUNT - 1u)) == 0u) ? 1 : -1]; // Ring 0 is the centre, which in a warp is the VANISHING POINT: those // stars are the far ones and get the small glyph. The big glyph belongs // to the outer rings, the stars about to rush past the viewer. #define ST_STAR_GLYPH_NEAR 0x2Au // '*', the outer rings #define ST_STAR_GLYPH_FAR 0x2Eu // '.', the inner rings #define ST_STAR_FAR_RINGS 5u // rings 0..4 are still distant // VIC sprite block pointers the game selects by name. The cab's bit 0 is // the landing gear, so each facing is a pair. #define ST_SPRITE_CAB_RIGHT 0xC0u // +1 = gear down #define ST_SPRITE_CAB_LEFT 0xDCu // +1 = gear down #define ST_SPRITE_PASS_WALK_R 0xC2u // +walkParity #define ST_SPRITE_PASS_WALK_L 0xC4u // +walkParity #define ST_SPRITE_PASS_STAND 0xC7u #define ST_SPRITE_PASS_BEAM 0xCBu // counts down to PASS_STAND #define ST_SPRITE_PASS_GONE 0xCCu // one past PASS_BEAM: fully shrunk #define ST_SPRITE_WRECK_FIRST 0xCCu // the wreck cel walk, FIRST..LAST-1 #define ST_SPRITE_WRECK_LAST 0xD1u // Title logo ($4827): the logo is one character repeated over rows // 1..11, and the flip-book animates it by copying one of four frame // glyphs over it. The glyph stays the simulation's own record of which // frame is showing; the renderer draws all four frames as one tile in // reserved palette slots and animates them by rewriting those entries, // so a flip and a colour cycle both cost no repaint at all. #define ST_LOGO_CHAR 0x84u #define ST_LOGO_FRAME_FIRST 0x85u // $85..$88 = frames 0..3 #define ST_LOGO_FRAMES 4u // Offset stSimNewGame clears from: the block above it is the displayed // picture and survives a new game (see StSimT). #define ST_SIM_CLEAR_FROM offsetof(StSimT, level) // Distinct characters whose glyph can change in one frame before the // renderer gives up and repaints everything. #define ST_CHAR_DIRTY_MAX 8u // Characters that may hold an edited glyph at once. The game edits six // across the whole game (the transporter hatch $67, level O's barrier // $6F, the title logo $84 and level W's three laser glyphs $91..$93), so // this is generous; an overflow parks the edit in the spill entry, where // it is simply never displayed rather than corrupting another character. #define ST_GLYPH_OVERRIDES 16u typedef struct { uint8_t rows[8]; } StGlyphT; // 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) // Per-level hook scratch. The original keeps a hook's running state // inside the level's own data block, at fixed C64 addresses interleaved // with its tables -- and, for some levels, on top of the hook's own 6502 // code, which this port never executes. A level reads its TABLES straight // out of the shipped blob (the single source of truth) and keeps only the // bytes it WRITES here. Seeded from // the blob at scene load, so the first tick starts where the original's // did. Every level is converted, so no C64-addressed working copy of the // blob exists any more. #define ST_HOOK_H_SEGS 11u // level H segment ids 0..10 #define ST_HOOK_W_LASERS 8u typedef struct { uint8_t segFlag[ST_HOOK_H_SEGS]; // $7E15..$7E1F wall segment up/down uint8_t switchId; // $7E4C also read back as a table entry uint8_t tick; // $7E53 uint8_t active; // $7E54 switch being animated uint8_t step; // $7E55 0..3 of the animation uint8_t tone; // $7E56 rising chime } StHookHT; typedef struct { uint8_t celPhase; // $7DD2 orb cel cycle 0..5 uint8_t tick; // $7E00 uint8_t lastSpot; // $7DD1 landing spot used last, 1..5 uint8_t hop; // $7DD3 ticks left in a teleport uint8_t sweep; // $7F02 the hop's falling tone } StHookGT; typedef struct { uint8_t state[ST_HW_SPRITES]; // $7DB8 0 idle, 1 rising, 2 burst uint8_t phase[ST_HW_SPRITES]; // $7DC0 cel phase uint8_t dx[ST_HW_SPRITES]; // $7DC8 uint8_t dy[ST_HW_SPRITES]; // $7DD0 } StHookIT; typedef struct { uint8_t parity; // $7DBD magnet pole flip } StHookKT; typedef struct { uint8_t tick; // $7DBD mod-8 scroll gate } StHookOT; typedef struct { uint8_t phase[ST_HW_SPRITES]; // $7E83 interference cel phase } StHookQT; typedef struct { uint8_t gateShut; // $7D9C } StHookTT; typedef struct { uint8_t colorPhase; // $7DAB uint8_t state[ST_HOOK_W_LASERS]; // $7DAC 0 idle, 1 extending, 2 fading uint8_t row[ST_HOOK_W_LASERS]; // $7DB5 beam head / fade phase uint8_t started; // $7DE5 } StHookWT; typedef struct { uint8_t celPhase[ST_HW_SPRITES]; // $7F4F uint8_t state[ST_HW_SPRITES]; // $7F57 0 idle, 1 active uint8_t dir[ST_HW_SPRITES]; // $7F5F entry edge 0..3 uint8_t life[ST_HW_SPRITES]; // $7F6E uint8_t cooldown; // $7F6D ticks before the next bounce } StHookUT; typedef struct { uint8_t col; // $7D9C uint8_t dir; // $7D9D uint8_t tick; // $7D9E } StHookXT; typedef struct { uint8_t row; // $7D98 row the leaves grow along uint8_t seg; // $7D99 leaves grown this cycle 0..4 uint8_t tick; // $7D9A ticks toward the next leaf uint8_t padSlot; // $7D9B number of the next pad to hang } StHookET; typedef struct { uint8_t state[ST_HW_SPRITES]; // $7EDB 0 idle, 1 falling, 2 bursting uint8_t phase[ST_HW_SPRITES]; // $7ED3 burst cel phase } StHookJT; typedef struct { uint8_t state[ST_HW_SPRITES]; // $7F16 0 idle, 1 falling, 2 melting uint8_t phase[ST_HW_SPRITES]; // $7F0E melt cel phase uint8_t parity; // $7F1F a melt advances every other count } StHookPT; typedef union { StHookET e; StHookGT g; StHookHT h; StHookIT i; StHookJT j; StHookKT k; StHookOT o; StHookPT p; StHookQT q; StHookTT t; StHookUT u; StHookWT w; StHookXT x; } StHookScratchT; 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 = Y, 25 = 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 { // ---- the displayed picture: screen RAM, colour RAM, the charset // and the renderer's dirty bookkeeping for them. stSimNewGame // ($5EC4) does NOT clear this block -- the C64 resets its game // variables with the previous picture still on screen, which is how // the attract demo's "GET READY" banner comes up over the live // title. Everything from `level` down IS cleared; keep new fields on // the right side of that line ---- 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 // Reverse index: for each glyph, the exact set of cells showing it, // as doubly-linked lists threaded through the cell array. // // The renderer has to repaint every cell showing a glyph whose // BITMAP changed -- a chunky framebuffer bakes the pixels in at // paste time, so unlike the C64's character mode there is no free // re-composite. It used to find those cells by scanning a per-glyph // [first, last] span, which PC profiling showed was the single // hottest loop in the program (22% of all samples on level H). This // gives the exact set instead: no scan, cost proportional to the // cells that really show the glyph. // // screen[] is the only truth and these mirror it: setCell keeps them // in step one cell at a time, and a bulk screen write re-derives // them via stSimDirtyAll, which every bulk writer already calls. // They live in the NOT-cleared block for the same reason screen[] // does -- stSimNewGame must leave the live picture alone. uint16_t glyphHead[ST_CHARSET_CHARS]; // ST_GLYPH_CELL_NONE = none uint16_t cellNext[ST_SCREEN_CELLS]; uint16_t cellPrev[ST_SCREEN_CELLS]; // $2800. The charset is NOT copied: the ROM set is const and only the // handful of glyphs the game rewrites at runtime are stored here, so // this costs ~400 bytes instead of 2 KB and a scene load clears an // index rather than copying 2 KB. Reach glyphs through stSimGlyph / // stSimGlyphRow / stSimGlyphMut, never by indexing directly. uint8_t glyphSlot[ST_CHARSET_CHARS]; // 0 = ROM glyph, else slot + 1 StGlyphT glyphOverride[ST_GLYPH_OVERRIDES + 1u]; // last = spill uint8_t glyphOverrideCount; // Characters whose glyph bits the game has edited since the last // frame -- the transporter hatch, the title logo flip, the laser // beams. A short list, not a 256-entry flag array: the renderer had // to scan all 256 every single frame to find the one or two that // ever change. Overflow (or a scene load) sets charDirtyAll. uint8_t charDirtyList[ST_CHAR_DIRTY_MAX]; uint8_t charDirtyCount; bool charDirtyAll; uint8_t borderColor; // $D020 uint8_t bgColor; // $D021 // The colour the logo band's cells would carry ($4861 writes it into // 407 cells of colour RAM); part of the picture, so it survives the // same way colour RAM does. uint8_t logoColor; // ---- cleared by stSimNewGame from here down ---- // ---- level image (mutable copies: hooks edit the pad table) ---- const StLevelT *level; StPadT pads[ST_MAX_PADS]; // $7D0A working copy uint8_t padCount; // $7D5A working copy (level E opens pads) uint8_t specialPad; // $7D09 working copy // ---- 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 // Sprites besides the cab whose $D01F bit a hook reads (levels J // and P watch their hazards hit the scenery); the rest are never // tested (derived state, not C64 memory). uint8_t bgCollSprites; // Per-sprite row masks of the displayed frame (3 bytes per row, the // leftmost pixel is bit 7 of byte 0), rebuilt only when the sprite's // bitmap or multicolour mode changes (derived state, not C64 memory). uint8_t collMask[ST_HW_SPRITES][ST_SPRITE_H][3]; const uint8_t *collMaskBm[ST_HW_SPRITES]; uint8_t collMaskMulti[ST_HW_SPRITES]; // First and last rows of collMask[idx] with any pixel (first == // ST_SPRITE_H when the mask is empty): the row tests walk only these. uint8_t collRowFirst[ST_HW_SPRITES]; uint8_t collRowLast[ST_HW_SPRITES]; // The cab's rows pre-shifted to each grid phase (cabWindows), built // per phase on first use; bit s of cabWinValid says cabWin[s] // matches collMask[0]. uint8_t cabWin[ST_ROW_PHASES][ST_SPRITE_H][4]; uint8_t cabWinValid; // ---- 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 // Jingles. The C64 plays its tunes BLOCKING ($44CF spins until the // song ends), so the sim only RAISES a request here and the host // plays it while holding the sim frozen -- tick-exactness survives // because no tick happens while the tune sounds. uint8_t jingleRequest; // song number + 1 to play now; 0 = none uint8_t jingleRotate; // $5E9B: start-of-screen tune = ++this & 3 // ---- 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 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) ---- StHookScratchT hookScratch; // per-level hook state 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); void stSimDirtyCells(StSimT *sim); // repaint all, glyph index kept // $43A5 -- blank the fare meter back to its template. void stSimHudInit(StSimT *sim); // $4253 + $4293 -- snapshot the sprite shadows into the displayed frame. void stSimMarshal(StSimT *sim); // $63D0 -- rotate every row of a glyph one pixel right and mark it dirty. void stSimRotateGlyph(StSimT *sim, uint8_t ch); // A character's eight 1bpp rows: the ROM glyph, or the game's edit of it. // kStBit[i] == 1 << i. A variable shift is a library bit loop on the // 65816, so sprite-index bit tests go through this table instead. extern const uint8_t kStBit[ST_HW_SPRITES]; const uint8_t *stSimGlyph(const StSimT *sim, uint8_t ch); uint8_t stSimGlyphRow(const StSimT *sim, uint8_t ch, uint8_t row); // The same eight rows, writable (copied from ROM on the first edit). uint8_t *stSimGlyphMut(StSimT *sim, uint8_t ch); // Drop every edit: the whole charset is the ROM set again (scene load). void stSimGlyphReset(StSimT *sim); // The 17-bit VIC sprite X (msb:col), read and signed-byte add ($411B). uint16_t stSimSpriteX(const StSimT *sim, uint8_t idx); void stSimSpriteAddX(StSimT *sim, uint8_t idx, uint8_t delta); // $6C38 -- blank the 11-character message row. void stSimClearMessage(StSimT *sim); // A glyph's bits changed: every cell showing it has to be repainted. void stSimMarkChar(StSimT *sim, uint8_t ch); // 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 // Seed a converted level's scratch from the shipped blob (scene load). void stHookSceneLoad(StSimT *sim); // ---- 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 stAudioSpeechPhrase(const uint8_t *speech); void stAudioSilence(void); // voices 1+2 gate off void stAudioJingle(uint8_t song); // $44CF: silence, then play song (caller blocks) bool stAudioJingleActive(void); // still sounding? void stAudioMusic(uint8_t song, bool loop); // $CB02: silence, then play song in the background 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