366 lines
17 KiB
C
366 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[128]; // cells marked since the last frame
|
|
uint8_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
|
|
|
|
// ---- 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
|