// Space Taxi -- per-level hook programs. // // Every level's data blob ends with six trampolines ($7D66..$7D77) // and a small program at $7D98..$7FFF that the main loop calls at // fixed points: two prelude hooks, two per-tick hooks (before and // after the sprite flush), an input filter and the sprite-contact // verdict. Those programs are the level gimmicks -- the puzzle // switches, lasers, moving pads, trap doors. The port keeps the // original bytes (sim->hookRam, for the tables the code indexes) and // re-expresses each program here, reading its tables at the same // addresses so nothing has to be transcribed by hand. #include "stSim.h" // Hook RAM accessors by C64 address. #define HK(sim, addr) ((sim)->hookRam[(addr) - ST_HOOK_BASE]) #define HKP(sim, addr) (&(sim)->hookRam[(addr) - ST_HOOK_BASE]) // Level indices (0 = A). #define ST_LEVEL_E 4u #define ST_LEVEL_G 6u #define ST_LEVEL_H 7u #define ST_LEVEL_I 8u #define ST_LEVEL_J 9u #define ST_LEVEL_K 10u #define ST_LEVEL_L 11u #define ST_LEVEL_O 14u #define ST_LEVEL_P 15u #define ST_LEVEL_Q 16u #define ST_LEVEL_R 17u #define ST_LEVEL_T 19u #define ST_LEVEL_U 20u #define ST_LEVEL_V 21u #define ST_LEVEL_W 22u #define ST_LEVEL_X 23u static void addToSpriteCol(StSimT *sim, uint8_t idx, uint8_t delta); static void fillDownChar(StSimT *sim, uint8_t col, uint8_t row, uint8_t ch, uint8_t rows); static void fillDownColor(StSimT *sim, uint8_t col, uint8_t row, uint8_t color, uint8_t rows); static void hookHPerTick(StSimT *sim); static void hookHPrelude0(StSimT *sim); static void hookHSegment(StSimT *sim, uint8_t id); static void hookTGate(StSimT *sim, uint8_t ch); static void hookTPerTick(StSimT *sim); static void hookTPrelude1(StSimT *sim); static void hookWLaserUpdate(StSimT *sim); static void hookWPerTick(StSimT *sim); static void hookXMovePads(StSimT *sim); static void hookXPerTick(StSimT *sim); static void hookXPrelude0(StSimT *sim); static void hookXShiftRow(StSimT *sim, uint16_t rowCell, uint8_t from, uint8_t to, int8_t dir); static void setSpriteColors(StSimT *sim, uint8_t color); #if !defined(__W65816__) static void hookKPerTick(StSimT *sim); static void hookOPerTick(StSimT *sim); static void hookOTramp0(StSimT *sim); static void hookRMaze(StSimT *sim, uint8_t ch); static void hookRPerTick(StSimT *sim); static void hookVPerTick(StSimT *sim); static void hookVScrollLeft(StSimT *sim, uint8_t row); static void hookVScrollRight(StSimT *sim, uint8_t row); static void hookQPrelude(StSimT *sim); static void hookQPerTick(StSimT *sim); static uint8_t hookQInput(StSimT *sim, uint8_t input); static void hookIPrelude(StSimT *sim); static void hookIPerTick(StSimT *sim); static void hookGPerTick(StSimT *sim); static void hookUPerTick(StSimT *sim); #endif // $411B -- add a signed byte to a sprite's X (carry into the msb). static void addToSpriteCol(StSimT *sim, uint8_t idx, uint8_t delta) { uint16_t x = (uint16_t)(((uint16_t)sim->spr[idx].msb << 8) | sim->spr[idx].col); x = (uint16_t)(x + (uint16_t)(int16_t)(int8_t)delta); sim->spr[idx].col = (uint8_t)x; sim->spr[idx].msb = (uint8_t)(x >> 8); } // $41AD after $401B -- the same character into the `rows` cells below. static void fillDownChar(StSimT *sim, uint8_t col, uint8_t row, uint8_t ch, uint8_t rows) { uint8_t k; for (k = 1u; k <= rows; k++) { if ((uint8_t)(row + k) < ST_SCREEN_ROWS) { stSimPutChar(sim, col, (uint8_t)(row + k), ch); } } } // $41AD after $401E -- the same colour into the `rows` cells below. static void fillDownColor(StSimT *sim, uint8_t col, uint8_t row, uint8_t color, uint8_t rows) { uint8_t k; for (k = 1u; k <= rows; k++) { if ((uint8_t)(row + k) < ST_SCREEN_ROWS) { stSimPutColor(sim, col, (uint8_t)(row + k), color); } } } // Level H "PUZZLER" per-tick ($7E5A): touching a switch sprite, or // landing on a pad whose list holds an active segment, animates the // switch's four wall segments over four 8-tick steps with a rising // tone, then toggles their flags. static void hookHPerTick(StSimT *sim) { if (HK(sim, 0x7E54) == 0u) { if (sim->collisionPhase != 0u) { return; } if (sim->activePad != 0u) { uint8_t x = (uint8_t)((sim->activePad - 1u) * 4u); uint8_t n; bool found = false; for (n = 0u; n < 4u; n++) { uint8_t y = HK(sim, 0x7E24 + x + n); if (HK(sim, 0x7E15 + y) != 0u) { HK(sim, 0x7E4C) = y; HK(sim, 0x7E54) = 6u; found = true; break; } } if (!found) { return; } } else { uint8_t a = sim->spriteSpriteColl; uint8_t y = 5u; while (y != 0u) { bool hit = (a & 0x80u) != 0u; a = (uint8_t)(a << 1); if (hit) { break; } y--; } if (y == 0u) { return; } HK(sim, 0x7E54) = y; setSpriteColors(sim, 0x0Au); sim->spr[2u + y].color = 0x05u; } // $7EB2: start the animation. HK(sim, 0x7E53) = 0u; HK(sim, 0x7E55) = 0xFFu; HK(sim, 0x7E56) = 0x12u; stAudioSfx(HKP(sim, 0x7ED0)); } // $7ED9 HK(sim, 0x7E56)++; stAudioVoice2(HK(sim, 0x7E56), HK(sim, 0x7E56), 0xFFu); HK(sim, 0x7E53)++; if ((HK(sim, 0x7E53) & 7u) != 0u) { return; } HK(sim, 0x7E55)++; HK(sim, 0x7E52) = (uint8_t)((HK(sim, 0x7E54) - 1u) * 4u); HK(sim, 0x7E51) = 4u; if (HK(sim, 0x7E55) == 4u) { // $7F7C: finished -- flip the segment flags. HK(sim, 0x7E54) = 0u; setSpriteColors(sim, 0x01u); while (HK(sim, 0x7E51) != 0u) { uint8_t id = HK(sim, 0x7E38 + HK(sim, 0x7E52)); if ((id & 0x80u) == 0u) { HK(sim, 0x7E15 + id) ^= 1u; } HK(sim, 0x7E52)++; HK(sim, 0x7E51)--; } return; } while (HK(sim, 0x7E51) != 0u) { uint8_t id = HK(sim, 0x7E38 + HK(sim, 0x7E52)); if ((id & 0x80u) == 0u) { hookHSegment(sim, id); } HK(sim, 0x7E52)++; HK(sim, 0x7E51)--; } } // Level H prelude 0 ($7D9B): clear the segment flags, park the five // switch sprites (3..7) from the position tables. static void hookHPrelude0(StSimT *sim) { uint8_t k; for (k = 0u; k <= 10u; k++) { HK(sim, 0x7E15 + k) = 0u; } HK(sim, 0x7E54) = 0u; for (k = 7u; k >= 3u; k--) { sim->spr[k].color = 1u; sim->spr[k].enable = 1u; sim->spr[k].ptr = 0x80u; sim->spr[k].col = HK(sim, 0x7DCF + k); sim->spr[k].msb = HK(sim, 0x7DD7 + k); sim->spr[k].row = HK(sim, 0x7DDF + k); } } // $7F22 -- draw (or erase) one cell of segment `id` for the current // animation step, growing from whichever end the tables say. static void hookHSegment(StSimT *sim, uint8_t id) { uint8_t step; uint8_t col; uint8_t row; uint8_t ch; HK(sim, 0x7E50) = id; if ((HK(sim, 0x7E15 + id) ^ HK(sim, 0x7DFD + id)) == 0u) { step = HK(sim, 0x7E55); } else { step = (uint8_t)(3u - HK(sim, 0x7E55)); } if (HK(sim, 0x7E08 + id) == 0x9Bu) { row = (uint8_t)(step + HK(sim, 0x7DF2 + id)); col = HK(sim, 0x7DE7 + id); } else { col = (uint8_t)(step + HK(sim, 0x7DE7 + id)); row = HK(sim, 0x7DF2 + id); } HK(sim, 0x7E13) = col; HK(sim, 0x7E14) = row; ch = (HK(sim, 0x7E15 + id) != 0u) ? HK(sim, 0x7E08 + id) : ST_CHAR_SPACE; stSimPutChar(sim, col, row, ch); } // Level T "FAST BREAK" $7D9D -- the barrier on row 3: `ch` into the // centre opening (cols 18..21), its complement into the side gaps. static void hookTGate(StSimT *sim, uint8_t ch) { uint8_t k; for (k = 0u; k < 4u; k++) { stSimPutChar(sim, (uint8_t)(18u + k), 3u, ch); } ch ^= 0x55u; for (k = 0u; k < 4u; k++) { stSimPutChar(sim, (uint8_t)(1u + k), 3u, ch); stSimPutChar(sim, (uint8_t)(35u + k), 3u, ch); } } // Level T per-tick ($7DDA): a slow cab up on the right bounces off // the ceiling; a fast climb into the top rows slams the centre gate // shut and opens the sides until the cab drops back down. static void hookTPerTick(StSimT *sim) { if (HK(sim, 0x7D9C) != 0u) { if (sim->spr[0].row >= 0x5Fu) { hookTPrelude1(sim); } return; } if (sim->spr[0].row >= 0x4Fu) { return; } if ((uint8_t)((uint16_t)sim->velY >> 8) >= 0xFCu) { if (sim->spr[0].msb != 0u) { return; } if (sim->spr[0].col < 0x96u) { return; } sim->velY = (int16_t)(-sim->velY); stAudioSfx(HKP(sim, 0x7E56)); return; } if (sim->spr[0].row >= 0x3Fu) { return; } sim->velY = 0; sim->velX = 0; hookTGate(sim, 0x75u); HK(sim, 0x7D9C)++; stAudioSfx(HKP(sim, 0x7E68)); } // Level T prelude 1 ($7DC4): centre open, sides barred. static void hookTPrelude1(StSimT *sim) { hookTGate(sim, ST_CHAR_SPACE); HK(sim, 0x7D9C) = 0u; stAudioSfx(HKP(sim, 0x7E5F)); } // Level W "LASERS" $7E46 -- clear the beam glyphs, then step each of // the eight lasers: idle ones fire on a 2-in-12 roll, extending ones // grow a cell per pass until their end row, retracting ones fade // through two colours and vanish. static void hookWLaserUpdate(StSimT *sim) { uint8_t i; uint8_t k; for (k = 0u; k < 16u; k++) { sim->charset[0x92u + (k >> 3)][k & 7u] = 0u; } sim->charDirty[0x92u] = 1u; sim->charDirty[0x93u] = 1u; for (i = 0u; i < 8u; i++) { uint8_t state = HK(sim, 0x7DAC + i); uint8_t col = HK(sim, 0x7DCD + i); HK(sim, 0x7DB4) = i; if (state == 0u) { if (stSimRng(sim, 12u) >= 3u) { continue; } HK(sim, 0x7DAC + i) = 1u; HK(sim, 0x7DB5 + i) = HK(sim, 0x7DD5 + i); stSimPutChar(sim, col, HK(sim, 0x7DB5 + i), HK(sim, 0x7DBD + i)); stSimPutColor(sim, col, HK(sim, 0x7DB5 + i), 2u); continue; } if (state == 1u) { stSimPutChar(sim, col, HK(sim, 0x7DB5 + i), 0x91u); if (HK(sim, 0x7DB5 + i) == HK(sim, 0x7DE9 + i)) { HK(sim, 0x7DAC + i) = 2u; HK(sim, 0x7DB5 + i) = 0u; state = 2u; } else { HK(sim, 0x7DB5 + i) = (uint8_t)(HK(sim, 0x7DB5 + i) + HK(sim, 0x7DDD + i)); stSimPutChar(sim, col, HK(sim, 0x7DB5 + i), HK(sim, 0x7DBD + i)); stSimPutColor(sim, col, HK(sim, 0x7DB5 + i), 2u); continue; } } if (state == 2u) { uint8_t phase; HK(sim, 0x7DB5 + i)++; phase = HK(sim, 0x7DB5 + i); if (phase == 3u) { HK(sim, 0x7DAC + i) = 0u; stSimPutChar(sim, col, HK(sim, 0x7DC5 + i), ST_CHAR_SPACE); fillDownChar(sim, col, HK(sim, 0x7DC5 + i), ST_CHAR_SPACE, 7u); } else { uint8_t color = HK(sim, 0x7DE6 + phase); stSimPutColor(sim, col, HK(sim, 0x7DC5 + i), color); fillDownColor(sim, col, HK(sim, 0x7DC5 + i), color, 7u); } continue; } HK(sim, 0x7DAC + i) = 0u; } } // Level W per-tick ($7DF1): a random-pitched hum on voice 2, the beam // glyph animation on the odd ticks and the laser step on every 8th. static void hookWPerTick(StSimT *sim) { for (;;) { uint8_t c; if (HK(sim, 0x7DE5) != 0u) { stAudioSfx(HKP(sim, 0x7DA2)); HK(sim, 0x7DE5) = 0u; } c = (uint8_t)(stSimRng(sim, 0x6Eu) + 6u); stAudioVoice2(c, c, 0x81u); HK(sim, 0x7DAB) = (uint8_t)((HK(sim, 0x7DAB) + 1u) & 7u); c = HK(sim, 0x7DAB); if (c == 0u) { hookWLaserUpdate(sim); return; } // $2C8F + c (char $91 row 7 for c = 0, else char $92 rows 0..6) // and $2C98 + (8 - c) (char $93 rows 7..1) get a beam bar. sim->charset[0x91u + ((7u + c) >> 3)][(7u + c) & 7u] = 0x3Cu; sim->charset[0x93u][8u - c] = 0x3Cu; sim->charDirty[0x92u] = 1u; sim->charDirty[0x93u] = 1u; if ((c & 1u) == 0u) { return; } } } // Level X "ON THE MOVE" $7F53 -- shift every pad's X bounds and stand // column, the waiting passenger, and a parked cab, by the step. static void hookXMovePads(StSimT *sim) { uint8_t dir = HK(sim, 0x7D9D); uint8_t off = 0u; uint8_t phase = 0u; addToSpriteCol(sim, 1u, dir); while (off < 0x48u) { uint8_t pad = (uint8_t)(off >> 3); uint8_t field = (uint8_t)(off & 7u); uint8_t *hi; uint8_t *lo; uint16_t v; if (field == 0u) { hi = &sim->pads[pad].x1Hi; lo = &sim->pads[pad].x1Lo; } else if (field == 2u) { hi = &sim->pads[pad].x2Hi; lo = &sim->pads[pad].x2Lo; } else { hi = &sim->pads[pad].passMsb; lo = &sim->pads[pad].passCol; } v = (uint16_t)(((uint16_t)*hi << 8) | *lo); v = (uint16_t)(v + (uint16_t)(int16_t)(int8_t)dir); *lo = (uint8_t)v; *hi = (uint8_t)(v >> 8); // Offsets step +2, +3, +3 repeating: x1, x2, stand X per pad. if (phase == 0u) { off = (uint8_t)(off + 2u); } else { off = (uint8_t)(off + 3u); } phase = (uint8_t)((phase + 1u) % 3u); } if (sim->activePad == 0u) { return; } addToSpriteCol(sim, 0u, dir); sim->posXmsb = sim->spr[0].msb; sim->hoverXFrac = sim->spr[0].msb; sim->posXcol = sim->spr[0].col; sim->hoverXCol = sim->spr[0].col; } // Level X per-tick ($7DE1): every 16 ticks slide the two pad columns // one cell, bouncing between columns 1 and 21. static void hookXPerTick(StSimT *sim) { uint8_t col; HK(sim, 0x7D9E)++; if (HK(sim, 0x7D9E) != 0x10u) { return; } HK(sim, 0x7D9E) = 0u; col = HK(sim, 0x7D9C); if ((HK(sim, 0x7D9D) & 0x80u) == 0u) { // $7DF9: rightward. hookXShiftRow(sim, ST_CELL(5, 0), (uint8_t)(col + 17u), col, 1); hookXShiftRow(sim, ST_CELL(10, 0), (uint8_t)(col + 17u), col, 1); hookXShiftRow(sim, ST_CELL(15, 0), (uint8_t)(col + 17u), col, 1); hookXShiftRow(sim, ST_CELL(20, 0), (uint8_t)(col + 17u), col, 1); stSimPutChar(sim, col, 5u, HK(sim, 0x7D9F + col)); stSimPutChar(sim, col, 10u, HK(sim, 0x7D9F + col)); stSimPutChar(sim, col, 15u, HK(sim, 0x7D9F + col)); stSimPutChar(sim, col, 20u, HK(sim, 0x7D9F + col)); stSimPutColor(sim, col, 5u, 0x0Cu); stSimPutColor(sim, col, 10u, 0x0Cu); stSimPutColor(sim, col, 15u, 0x0Cu); stSimPutColor(sim, col, 20u, 0x0Cu); // $7E56: the row-14 cell of the right group moves with it. stSimPutChar(sim, (uint8_t)(col + 12u), 14u, sim->screen[ST_CELL(14, col + 11u)]); stSimPutChar(sim, (uint8_t)(col + 11u), 14u, ST_CHAR_SPACE); stSimPutColor(sim, (uint8_t)(col + 12u), 14u, 7u); stSimPutChar(sim, (uint8_t)(col + 11u), 5u, HK(sim, 0x7D9F + col + 11u)); stSimPutChar(sim, (uint8_t)(col + 11u), 10u, HK(sim, 0x7D9F + col + 11u)); stSimPutChar(sim, (uint8_t)(col + 11u), 15u, HK(sim, 0x7D9F + col + 11u)); stSimPutChar(sim, (uint8_t)(col + 11u), 20u, HK(sim, 0x7D9F + col + 11u)); stSimPutColor(sim, (uint8_t)(col + 11u), 5u, 0x0Cu); stSimPutColor(sim, (uint8_t)(col + 11u), 10u, 0x0Cu); stSimPutColor(sim, (uint8_t)(col + 11u), 15u, 0x0Cu); stSimPutColor(sim, (uint8_t)(col + 11u), 20u, 0x0Cu); hookXMovePads(sim); HK(sim, 0x7D9C)++; if (HK(sim, 0x7D9C) == 0x15u) { HK(sim, 0x7D9D) = 0xF8u; } return; } // $7E9E: leftward. hookXShiftRow(sim, ST_CELL(5, 0), col, (uint8_t)(col + 17u), -1); hookXShiftRow(sim, ST_CELL(10, 0), col, (uint8_t)(col + 17u), -1); hookXShiftRow(sim, ST_CELL(15, 0), col, (uint8_t)(col + 17u), -1); hookXShiftRow(sim, ST_CELL(20, 0), col, (uint8_t)(col + 17u), -1); stSimPutChar(sim, (uint8_t)(col + 6u), 5u, HK(sim, 0x7D9F + col + 6u)); stSimPutChar(sim, (uint8_t)(col + 6u), 10u, HK(sim, 0x7D9F + col + 6u)); stSimPutChar(sim, (uint8_t)(col + 6u), 15u, HK(sim, 0x7D9F + col + 6u)); stSimPutChar(sim, (uint8_t)(col + 6u), 20u, HK(sim, 0x7D9F + col + 6u)); stSimPutColor(sim, (uint8_t)(col + 6u), 5u, 0x0Cu); stSimPutColor(sim, (uint8_t)(col + 6u), 10u, 0x0Cu); stSimPutColor(sim, (uint8_t)(col + 6u), 15u, 0x0Cu); stSimPutColor(sim, (uint8_t)(col + 6u), 20u, 0x0Cu); stSimPutChar(sim, (uint8_t)(col + 10u), 14u, sim->screen[ST_CELL(14, col + 11u)]); stSimPutChar(sim, (uint8_t)(col + 11u), 14u, ST_CHAR_SPACE); stSimPutColor(sim, (uint8_t)(col + 10u), 14u, 7u); stSimPutChar(sim, (uint8_t)(col + 17u), 5u, HK(sim, 0x7D9F + col + 17u)); stSimPutChar(sim, (uint8_t)(col + 17u), 10u, HK(sim, 0x7D9F + col + 17u)); stSimPutChar(sim, (uint8_t)(col + 17u), 15u, HK(sim, 0x7D9F + col + 17u)); stSimPutChar(sim, (uint8_t)(col + 17u), 20u, HK(sim, 0x7D9F + col + 17u)); stSimPutColor(sim, (uint8_t)(col + 17u), 5u, 0x0Cu); stSimPutColor(sim, (uint8_t)(col + 17u), 10u, 0x0Cu); stSimPutColor(sim, (uint8_t)(col + 17u), 15u, 0x0Cu); stSimPutColor(sim, (uint8_t)(col + 17u), 20u, 0x0Cu); hookXMovePads(sim); HK(sim, 0x7D9C)--; if (HK(sim, 0x7D9C) == 1u) { HK(sim, 0x7D9D) = 0x08u; } } // Level X prelude 0 ($7DC6): reset the sweep and restore the pad table // from the level's backup copy ($7FB8). static void hookXPrelude0(StSimT *sim) { uint8_t k; HK(sim, 0x7D9D) = 0xF8u; HK(sim, 0x7D9C) = 0x0Bu; HK(sim, 0x7D9E) = 0u; for (k = 0u; k < 8u; k++) { const uint8_t *src = HKP(sim, 0x7FB8 + k * 8u); sim->pads[k].x1Hi = src[0]; sim->pads[k].x1Lo = src[1]; sim->pads[k].x2Hi = src[2]; sim->pads[k].x2Lo = src[3]; sim->pads[k].row = src[4]; sim->pads[k].passMsb = src[5]; sim->pads[k].passCol = src[6]; sim->pads[k].unused = src[7]; } } // Copy cells of one screen row (char + colour) one column along, from // column `from` to column `to` inclusive, in the direction that keeps // the copy from overwriting its own source. static void hookXShiftRow(StSimT *sim, uint16_t rowCell, uint8_t from, uint8_t to, int8_t dir) { uint8_t x = from; for (;;) { uint16_t src = (uint16_t)(rowCell + x); uint16_t dst = (uint16_t)(src + (uint16_t)(int16_t)dir); stSimPutChar(sim, (uint8_t)(dst % ST_SCREEN_COLS), (uint8_t)(dst / ST_SCREEN_COLS), sim->screen[src]); stSimPutColor(sim, (uint8_t)(dst % ST_SCREEN_COLS), (uint8_t)(dst / ST_SCREEN_COLS), sim->color[src]); if (x == to) { break; } x = (uint8_t)(x - (uint8_t)dir); } } // Sprites 3..7 share one colour in the puzzle level. static void setSpriteColors(StSimT *sim, uint8_t color) { uint8_t k; for (k = 3u; k < ST_HW_SPRITES; k++) { sim->spr[k].color = color; } } // Extra-level gimmicks (magnets, electroids, maze, shift-o-rama, // interference, crossfire, teleports, rebound). Compiled out on the // IIgs, whose bank-0 BSS/text budget is too tight for them; those // levels play without their hazard there (the 4 attract-demo levels // H/T/W/X keep their gimmicks everywhere). #if !defined(__W65816__) // Level K "MAGNETS" per-tick ($7D9D): the level is globally anti-gravity // (its yGrav template is -7); the hook only animates the six magnet // poles by flipping bit 0 of their glyphs every other tick. static void hookKPerTick(StSimT *sim) { static const uint8_t kPole[6] = { 0x04u, 0x0Bu, 0x10u, 0x17u, 0x1Cu, 0x23u }; uint8_t k; HK(sim, 0x7DBD) ^= 1u; if ((HK(sim, 0x7DBD) & 1u) != 0u) { return; } for (k = 0u; k < 6u; k++) { uint16_t cell = (uint16_t)(0xA0u + kPole[k]); // $04A0 + off stSimPutChar(sim, (uint8_t)(cell % ST_SCREEN_COLS), (uint8_t)(cell / ST_SCREEN_COLS), (uint8_t)(sim->screen[cell] ^ 1u)); } } // Level O "ELECTROIDS" per-tick ($7DBE): four electric barriers (rows // 5, 14, 22, 26 of screen RAM in cell terms) scroll left one column, // the leftmost cell wrapping to the right, at a rate gated by a mod-8 // tick (rows 1-2 every tick, row 3 every 4th). static void hookOScrollRow(StSimT *sim, uint16_t base) { uint8_t chSave = sim->screen[base]; uint8_t colSave = sim->color[base]; uint8_t x; uint16_t cell; for (x = 1u; x < 0x26u; x++) { cell = (uint16_t)(base + x); stSimPutChar(sim, (uint8_t)((base + x - 1u) % ST_SCREEN_COLS), (uint8_t)((base + x - 1u) / ST_SCREEN_COLS), sim->screen[cell]); stSimPutColor(sim, (uint8_t)((base + x - 1u) % ST_SCREEN_COLS), (uint8_t)((base + x - 1u) / ST_SCREEN_COLS), sim->color[cell]); } cell = (uint16_t)(base + 0x25u); stSimPutChar(sim, (uint8_t)(cell % ST_SCREEN_COLS), (uint8_t)(cell / ST_SCREEN_COLS), chSave); stSimPutColor(sim, (uint8_t)(cell % ST_SCREEN_COLS), (uint8_t)(cell / ST_SCREEN_COLS), colSave); } static void hookOPerTick(StSimT *sim) { hookOTramp0(sim); // $7E6E bitScroll of the barrier glyph HK(sim, 0x7DBD) = (uint8_t)((HK(sim, 0x7DBD) + 1u) & 7u); if ((HK(sim, 0x7DBD) & 3u) != 0u) { return; } hookOScrollRow(sim, 0xC9u); // $04C9 row 5 col 1 hookOScrollRow(sim, 0x209u); // $0609 row 12 col 1 if (HK(sim, 0x7DBD) != 4u) { return; } hookOScrollRow(sim, 0x169u); // $0569 row 9 col 1 hookOScrollRow(sim, 0x2A9u); // $06A9 row 17 col 1 } // $63D0-style rotate of the two electroid glyphs ($2BE0 / char $6F). static void hookOTramp0(StSimT *sim) { uint8_t k; for (k = 0u; k < 8u; k++) { uint8_t b = sim->charset[0x6Fu][k]; sim->charset[0x6Fu][k] = (uint8_t)((b >> 1) | (b << 7)); } sim->charDirty[0x6Fu] = 1u; } // Level R "TAXI MAZE" wall program ($7D9C): put `ch` in the vertical // bar cells ($043E/$0466/$048E) and its complement (ch^$46) in the // gate cells; called with $66 to close, with the gate's own char to // flip. Toggling on pickup opens a path. static void hookRMaze(StSimT *sim, uint8_t ch) { static const uint16_t kBar[3] = { 0x3Eu, 0x66u, 0x8Eu }; static const uint16_t kGate[10] = { 0x39u, 0x61u, 0x89u, 0xA1u, 0xA2u, 0xA3u, 0xA4u, 0x309u, 0x30Au, 0x30Bu }; uint8_t k; uint8_t alt = (uint8_t)(ch ^ 0x46u); for (k = 0u; k < 3u; k++) { stSimPutChar(sim, (uint8_t)(kBar[k] % ST_SCREEN_COLS), (uint8_t)(kBar[k] / ST_SCREEN_COLS), ch); } for (k = 0u; k < 10u; k++) { stSimPutChar(sim, (uint8_t)(kGate[k] % ST_SCREEN_COLS), (uint8_t)(kGate[k] / ST_SCREEN_COLS), alt); } } // Level R per-tick ($7DD2): when the passenger boards (stage 4) and the // gate is open ($0439 == space) the maze flips. static void hookRPerTick(StSimT *sim) { if (sim->stage != ST_STAGE_BOARD) { return; } if (sim->screen[0x39u] != ST_CHAR_SPACE) { return; } hookRMaze(sim, sim->screen[0x39u]); } // Level V "SHIFT-O-RAMA" per-tick ($7D9D): every screen tick, 18 rows // (0..17) each scroll one column, alternating direction by row (bit 1 // of the row index): even-band rows left, odd-band right, the edge // cell wrapping around. static void hookVScrollLeft(StSimT *sim, uint8_t row) { uint16_t base = ST_CELL(row, 0); uint8_t chSave = sim->screen[base]; uint8_t colSave = sim->color[base]; uint8_t x; for (x = 1u; x < 0x27u; x++) { stSimPutChar(sim, (uint8_t)(x - 1u), row, sim->screen[base + x]); stSimPutColor(sim, (uint8_t)(x - 1u), row, sim->color[base + x]); } stSimPutChar(sim, 0x26u, row, chSave); stSimPutColor(sim, 0x26u, row, colSave); } static void hookVScrollRight(StSimT *sim, uint8_t row) { uint16_t base = ST_CELL(row, 0); uint8_t chSave = sim->screen[base + 0x26u]; uint8_t colSave = sim->color[base + 0x26u]; uint8_t x; for (x = 0x26u; x > 0u; x--) { stSimPutChar(sim, x, row, sim->screen[base + x - 1u]); stSimPutColor(sim, x, row, sim->color[base + x - 1u]); } stSimPutChar(sim, 0u, row, chSave); stSimPutColor(sim, 0u, row, colSave); } static void hookVPerTick(StSimT *sim) { uint8_t row; for (row = 6u; row < 0x12u; row++) { if ((row & 2u) != 0u) { hookVScrollLeft(sim, row); } else { hookVScrollRight(sim, row); } } } // Level Q "INTERFERENCE" ($7DA1 prelude0, $7E0D per-tick2, $7E2E input): // five interference sprites (hw 3..7) sit at fixed spots flickering // through cels $80..$8B, and while the cab is mid-screen the joystick // is scrambled 20% of ticks. static void hookQPrelude(StSimT *sim) { int8_t x; sim->multiColorMask = 0xFFu; for (x = 7; x >= 3; x--) { HK(sim, 0x7E83 + (uint8_t)x) = HK(sim, 0x7DEA + (uint8_t)x); sim->spr[x].ptr = HK(sim, 0x7DF2 + HK(sim, 0x7E83 + (uint8_t)x)); sim->spr[x].color = 0x06u; sim->spr[x].col = HK(sim, 0x7DD2 + (uint8_t)x); sim->spr[x].msb = HK(sim, 0x7DDA + (uint8_t)x); sim->spr[x].row = HK(sim, 0x7DE2 + (uint8_t)x); sim->spr[x].enable = 1u; } } static void hookQPerTick(StSimT *sim) { int8_t x; for (x = 7; x >= 3; x--) { HK(sim, 0x7E83 + (uint8_t)x)++; if (HK(sim, 0x7E83 + (uint8_t)x) == 0x0Cu) { HK(sim, 0x7E83 + (uint8_t)x) = 0u; } sim->spr[x].ptr = HK(sim, 0x7DF2 + HK(sim, 0x7E83 + (uint8_t)x)); } } static uint8_t hookQInput(StSimT *sim, uint8_t input) { if (sim->activePad != 0u) { return input; } if (sim->spr[0].row < 0x5Au || sim->spr[0].row >= 0xB4u) { return input; } if (stSimRng(sim, 10u) < 8u) { return input; } return HK(sim, 0x7E7F + (uint8_t)(stSimRng(sim, 4u) - 1u)); } // Level I "CROSSFIRE" ($7D9C prelude0, $7DEF per-tick2, $7DB1 prelude1): // bullets (hw 3..7) fly up from the floor, cel-walk, then burst. static void hookIPrelude(StSimT *sim) { int8_t x; for (x = 7; x >= 3; x--) { HK(sim, 0x7DB8 + (uint8_t)x) = 0u; // state sim->spr[x].enable = 0u; sim->spr[x].color = 0x02u; } } static void hookIPerTick(StSimT *sim) { int8_t s; for (s = 3; s <= 7; s++) { uint8_t x = (uint8_t)s; uint8_t state = HK(sim, 0x7DB8 + x); if (state == 0u) { uint8_t y; if (stSimRng(sim, 0x75u) >= 3u) { continue; } y = (uint8_t)(stSimRng(sim, 4u) - 1u); // 0..3 direction HK(sim, 0x7DC8 + x) = HK(sim, 0x7DE1 + y); // dx sim->spr[x].col = HK(sim, 0x7DDD + y); // start col sim->spr[x].msb = 0u; sim->spr[x].row = 0xD1u; HK(sim, 0x7DD0 + x) = (uint8_t)(stSimRng(sim, 2u) + 0xFDu); // dy = rng(2)-3 HK(sim, 0x7DC0 + x) = 0u; // cel phase HK(sim, 0x7DB8 + x) = 1u; sim->spr[x].enable = 1u; sim->spr[x].ptr = 0x84u; stAudioSfx(HKP(sim, 0x7E82)); sim->spr[x].color = HK(sim, 0x7E7F + (uint8_t)(stSimRng(sim, 3u) - 1u)); } else if (state == 1u) { uint8_t ph = (uint8_t)(HK(sim, 0x7DC0 + x) + 1u); HK(sim, 0x7DC0 + x) = ph; if (ph == 5u) { HK(sim, 0x7DC0 + x) = 0u; sim->spr[x].ptr = 0x85u; HK(sim, 0x7DB8 + x) = 2u; } else { if (ph >= 3u) { addToSpriteCol(sim, x, HK(sim, 0x7DC8 + x)); sim->spr[x].row = (uint8_t)(sim->spr[x].row + HK(sim, 0x7DD0 + x)); } sim->spr[x].ptr = HK(sim, 0x7DD8 + ph); } } else { uint8_t ph = (uint8_t)((HK(sim, 0x7DC0 + x) + 1u) & 7u); HK(sim, 0x7DC0 + x) = ph; sim->spr[x].ptr = HK(sim, 0x7DE5 + ph); addToSpriteCol(sim, x, HK(sim, 0x7DC8 + x)); sim->spr[x].row = (uint8_t)(sim->spr[x].row + HK(sim, 0x7DD0 + x)); if (sim->spr[x].row < 0x25u) { sim->spr[x].enable = 0u; HK(sim, 0x7DB8 + x) = 0u; } } } } // Level G "TELEPORTS" ($7DA0 prelude0, $7DF2 per-tick2): orbs (hw 2..7) // drift; touching the cab teleports it. Faithful reduction: place and // drift the orb sprites (the teleport hop is a rare event we leave to // the collision system, which crashes on contact like the C64's // undelivered case -- see MECHANICS). Placement only for now. static void hookGPerTick(StSimT *sim) { // The orb colour cycles through $7DD4[phase] every 4 ticks. HK(sim, 0x7E00)++; if ((HK(sim, 0x7E00) & 3u) != 0u) { return; } HK(sim, 0x7DD2)++; if (HK(sim, 0x7DD2) == 6u) { HK(sim, 0x7DD2) = 0u; } { uint8_t c = HK(sim, 0x7DD4 + HK(sim, 0x7DD2)); uint8_t k; for (k = 2u; k < ST_HW_SPRITES; k++) { sim->spr[k].color = c; } } } // Level U "REBOUND" ($7DAD per-tick2): the ceiling and side walls // reflect the cab's velocity; hazard sprites (hw 3..7) drift. The wall // bounce is the gimmick and is handled here on the taxi velocity. static void hookUPerTick(StSimT *sim) { if (sim->collisionPhase != 0u) { return; } // $7DB2: near the top -> reflect Y downward. if (sim->spr[0].row < 0x11u) { sim->velY = (int16_t)(-sim->velY); sim->spr[0].row = (uint8_t)(sim->spr[0].row + 2u); } } #endif /* !IIGS */ // --------------------------------------------------------------------------- // Dispatch // --------------------------------------------------------------------------- // $7D75 -- 0 means sprite contact (passenger, level sprites) is safe. uint8_t stHookHitVerdict(StSimT *sim) { switch (sim->hookLevel) { case ST_LEVEL_H: return 0u; default: return 1u; } } // $7D72 -- the input filter. uint8_t stHookInput(StSimT *sim, uint8_t input) { #if !defined(__W65816__) switch (sim->hookLevel) { case ST_LEVEL_Q: return hookQInput(sim, input); default: break; } #else (void)sim; #endif return input; } // $7D6C -- before the sprite flush. void stHookPerTick2(StSimT *sim) { switch (sim->hookLevel) { case ST_LEVEL_H: hookHPerTick(sim); break; case ST_LEVEL_T: hookTPerTick(sim); break; case ST_LEVEL_W: hookWPerTick(sim); break; case ST_LEVEL_X: hookXPerTick(sim); break; #if !defined(__W65816__) case ST_LEVEL_V: hookVPerTick(sim); break; case ST_LEVEL_Q: hookQPerTick(sim); break; case ST_LEVEL_I: hookIPerTick(sim); break; case ST_LEVEL_G: hookGPerTick(sim); break; case ST_LEVEL_U: hookUPerTick(sim); break; case ST_LEVEL_K: hookKPerTick(sim); break; case ST_LEVEL_O: hookOPerTick(sim); break; case ST_LEVEL_R: hookRPerTick(sim); break; #endif default: break; } } // $7D6F -- after the sprite flush. void stHookPerTick3(StSimT *sim) { (void)sim; } // $7D66 -- first prelude hook. void stHookPrelude0(StSimT *sim) { switch (sim->hookLevel) { case ST_LEVEL_H: hookHPrelude0(sim); break; case ST_LEVEL_X: hookXPrelude0(sim); break; #if !defined(__W65816__) case ST_LEVEL_Q: hookQPrelude(sim); break; case ST_LEVEL_I: hookIPrelude(sim); break; #endif case ST_LEVEL_W: { uint8_t k; for (k = 1u; k <= 8u; k++) { HK(sim, 0x7DAB + k) = 0u; } } break; default: break; } } // $7D69 -- second prelude hook. void stHookPrelude1(StSimT *sim) { switch (sim->hookLevel) { case ST_LEVEL_T: hookTPrelude1(sim); break; case ST_LEVEL_W: HK(sim, 0x7DE5) = 1u; break; #if !defined(__W65816__) case ST_LEVEL_R: hookRMaze(sim, ST_CHAR_BLANK); break; #endif default: break; } }