diff --git a/MECHANICS.md b/MECHANICS.md index 442134b..640fc28 100644 --- a/MECHANICS.md +++ b/MECHANICS.md @@ -275,6 +275,37 @@ The phase byte `$7164` is the dispatcher; `$7165/$7166` form a frame-rate gate (count down each frame, reload when zero) so each phase handler runs every N frames, not every frame. +**What a read sees (VERIFIED 2026-10-06 against VICE, all 25 screens).** +The latches hold until read, and the main loop reads them (`$6966`, after +the second `$404B`) two frames after its last read: one frame still +shows the sprites the previous tick flushed, the next shows this tick's +(`$4293` runs right after the first `$404B`, raster `$FC`). So a read +is the OR of both frames -- a sprite pulling away from a contact is still +touching on the tick it leaves (the pad light at `$6EF0` stays on one +tick longer than one frame would give). The older frame is drawn while +the CPU waits at `$5F94`, i.e. over the picture this tick's work has +already changed: a melting level-P flake whose windsock flipped under it +that tick does not touch it in the old frame either. + +**Ticks that overrun a frame.** All of the above assumes a tick's work +between two reads fits in one frame. When it does not -- level V's row +shift (every 16th tick, `$7DA6`), the speech player (`$6C7F` blocks) -- +the raster latch is already set when `$5F94` is reached, so the flush +lands mid-frame (VICE: raster 36 and 58 on V's shift ticks) and the new +positions are only drawn below that line before the read; a sprite whose +new top line has already passed keeps drawing its old start's remaining +lines at the new X. A collision at the top of the screen on such a tick +reaches the read one tick late. The port does not model this (it would +need the C64's cycle timing); it is the one difference the stsim gate +knows of, seen on level V when the cab hits the top border on a shift +tick. And `$D01E` has the bits of +EVERY pair of shown sprites: the hooks test groups of bits (`AND #$F8` +at `$69F9` and in level G's `$7E35` also catch a hook sprite touching the +exhaust flame), so the cab is not the only side that matters. The +framePresent read at `$6920` clears the latch, but the start-of-screen +jingle then shows that frame for as long as it plays, so the first +tick's read still includes it. + ### Collision dispatch trace (VERIFIED via emulator) Verified by running `$6966` from `Tracer.from_dump('raw.bin')` with @@ -667,18 +698,21 @@ actually 4 cels organized as 2 bases x 2 flicker variants. ### `$61BD` -- passengerEventDraw -If `$716F != 0` (a passenger event just happened): peek `$71CD` -(event type) and dispatch: +CORRECTED 2026-10-06 (checked against VICE by the stsim gate): once the +touchdown bob is over (`$716F == 0`) a landing's event `$71CD`, set by +padDetect at `$64C9..$64F5`, is voiced: -| `$71CD` | Event | Pointer source for $6C7F | -| ------- | ----------- | ---------------------------------- | -| 1 | (drop-off?) | $6A/$6B = $6C6C | -| 2 | (pickup?) | $6A/$6B = $6C7A | -| other | (takeoff?) | $6A/$6B = $6C7D | +| `$71CD` | Landed... | Action | +| ------- | ------------------------------------------- | --------------------------------------- | +| 1 | while a fare waits on another pad ($64F3) | speak `$6C6C` "HT" (HEY TAXI) | +| 2 | with a fare aboard, on the wrong pad ($64DF) | `$43A5` (the meter is LOST), speak `$6C7A` "P" | +| 3 | the same, the fare wanting UP ($64E7) | `$43A5`, speak `$6C7D` "U" | -`$6C7F` is "render text/scoreboard": marshals shadow, flushes, -then draws indirect text. So this routine surfaces a HUD blurb -based on what just happened. +`$6C7F` is the speech player (one sample per character); it zeroes +`$71CD` (`$6C99`) and sets both release timers `$7216/$7217` to 1. So any +phrase spoken before the bob ends cancels the event. `$6F18` raises event +1 for the third screen's takeoff fare too; the prelude speaks it at +`$5F30` ("P1?", `$6C72`) before the first tick. ### `$63DD` -- fireButtonEdge @@ -877,26 +911,25 @@ kills all audio, then either loops waiting for release or transitions. ### `$6419` -- fuelBarHud -``` -LDA #$0B; STA $DBA2,$DBA3,$DBCA,$DBCB ; pad color cells -INC $71BE; AND #$07; STA $71BE ; mod-8 ticker -CMP #$05; BPL RTS ; only first 5 of 8 frames +CORRECTED 2026-10-06 (the old transcript here had the branches wrong): -LDA $714F (Y-vel hi); BMI down ; sign-test -BEQ check_X ; Y vel = 0 -A=$02, X=$28; JMP write -check_X: -LDA $714E -BMI down -A=$03, X=$00; JMP write -down: -A=$07, X=$00 +``` +LDA #$0B; STA $DBA2,$DBA3,$DBCA,$DBCB ; both lamp pairs dark grey +INC $71BE; AND #$07; STA $71BE ; mod-8 ticker +CMP #$05; BPL RTS ; lit on 5 ticks of 8 + +LDA $714F (Y-vel hi); BMI $6449 ; climbing -> cyan +BEQ $6444 +A=$02, X=$28; JMP write ; hi > 0 -> red, row 24 +$6444: LDA $714E; BMI $6450 ; hi 0, lo >= $80 -> yellow +$6449: A=$03, X=$00; JMP write ; else -> cyan +$6450: A=$07, X=$00 write: STA $DBA2,X; STA $DBA3,X; RTS ``` -So the fuel bar's color depends on cab Y-velocity sign (rising vs -descending vs hovering). The `$DBA2-$DBCB` cells form the in-flight -indicator strip. +So it is a descent-rate lamp: red on row 24 when the cab falls a whole +sub-row a tick or faster, yellow on row 23 from half that, cyan on row +23 otherwise (any climb, a hover, a slow fall). ## IRQ handler (`$CF52`) -- music only, no animation diff --git a/Makefile b/Makefile index 97f5b91..37e5c60 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ # and program.mk describes the game to them. # # make every target whose toolchain is installed -# make one target: iigs amiga atarist dos x68000 dvx +# make one target: iigs amiga atarist dos x68000 dvx sdl3 # make check build, then run the gates (scripts/verify.sh) # make clean remove build/ # make help JoeyLib's project targets diff --git a/README.md b/README.md index b8e7551..da7d395 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,10 @@ The game is a routine-for-routine re-expression of the C64 original in C, written from its disassembly (MECHANICS.md): the flight physics, the fare machine, the 24 levels' hook programs, the title and level intros, the high-score table and name entry, the music (captured from the game's own -SID player) and the speech. The simulation (stSim.c) is verified tick for -tick against VICE traces of the C64's four attract-demo rides (VERIFIED.md), -and the gates below hold it there. +SID player) and the speech. The simulation (stSim.c) is checked tick for +tick against the original game running in VICE: a ride on each of the 25 +screens, A..X and the MYSTERY SCREEN, traces byte for byte as on the C64 +(tools/stsim; VERIFIED.md), and the gates below hold it there. ## Setting up @@ -97,7 +98,7 @@ $ scripts/verify.sh disk # or name the ones to run | Gate | What it proves | |------|----------------| -| `stsim` | Every hook level's 4000-tick simulation trace is identical to its golden (`tools/stsim/gate.sh`; the four C64 attract rides and thirteen synthesised ones), and every cel a ride shows is pre-compiled (`celGate.sh`) | +| `stsim` | Every screen's ride (all 25: the four C64 attract recordings and twenty-one synthesised streams, up to 4000 ticks, crashes costing a cab as in a real game) traces exactly as the real C64 traced it -- `gateHashes.txt` holds the md5s of the C64's traces (`tools/stsim/gate.sh`) -- and every cel a ride shows is pre-compiled (`celGate.sh`) | | `harness` | The screens above the simulation -- the high-score table, name entry, its save -- on JoeyLib's host BLANK port (`tools/staxiharness`) | | `disk`, `disk01` | `STAXI.2mg` boots, launches the game and draws on a ROM 3 and a ROM 01 IIgs | | `iigsaudio` | The IIgs sound interrupt is NTP's, the DOC is audible and its sound slots carry waveforms | @@ -109,8 +110,13 @@ missing, or the game data not imported). ## Tools -- `tools/stsim` -- the simulation on the host: prints the per-tick state a - VICE trace records, so `compare.py` can diff the port against the C64. +- `tools/stsim` -- the simulation on the host, and its C64 counterpart: + `stsim` prints one line of state per tick, `c64trace.py` rides the same + input on the original disk in VICE and prints the same line from the + C64's memory, and `gate.sh c64` compares the two for every screen + (`compare.py` shows where they part). That needs VICE's `x64sc` and the + original `SPACETAX.D64` in `stuff/`; `gate.sh c64hashes` is how + `gateHashes.txt` is made. `recorder` searches for the synthesised rides. - `tools/staxiharness` -- the front-end screen checks. - `tools/staxibake` -- writes the cel bank the build pre-compiles per machine. - `assets/` -- the generators behind the committed C64 tables diff --git a/TODO.md b/TODO.md index 3af9240..3130a16 100644 --- a/TODO.md +++ b/TODO.md @@ -19,3 +19,38 @@ moved out of JoeyLib's tree into this repository; JoeyLib's own list per-call overhead in save-and-draw and restore (~1.75k and ~1.25k cycles a star), not bytes moved, so it becomes feasible with a leaner save/draw/restore path for small cels in JoeyLib. + The ST's palette warp was slow for a LIBRARY reason, fixed in JoeyLib + 2026-10-06 with no game change: every jlPaletteSet made the ST HAL + requantize its whole per-scanline palette table (3200 entries, ~38 ms + of 8 MHz 68000 each, Hatari profiler); it now requantizes only the + palette that changed. Warp on the ST, page flips per second inside the + warp from a Hatari io_write trace (same method before and after): + 13.5 -> 53.6 fps; the title's logo 17.4 -> 20.7. (The 23.3 above + predates the 2026-10-05 intro-pacing change; this method gives 13.5 on + the tree just before the fix.) JoeyLib also gained an STe build, which + Space Taxi gets with `make ste` (build/ste, STe and Mega STe only): the + warp runs the same 53.6 on an STe and 56.8 on a Mega STe. Other ports' + palette paths were not touched by this fix. + +- [ ] **Does $4523 really leave sprites 0-6 enabled?** The level-name setup + ($4523..$4665, stIntroEnter in stTitle.c) never clears the enable bits + of sprites 0-6 on the C64, but the port does. Settle it from the + disassembly (every $D015 write on the path into the intro), and check + it in VICE if the bytes leave doubt. Dropped out of JoeyLib's TODO in + the 2026-10-05 split; restored 2026-10-06. + +- [ ] **What the C64 gate cannot reach.** `tools/stsim/gate.sh c64` holds + all 25 screens to the real game, but only along the attract demo's + machinery: its table RNG ($446A; a real game reads the SID noise + oscillator, which the port stands in for with an LCG), one cabbie, and + whatever the searched streams happen to do. Two-to-four-cabbie turns, + the bonus cab at $3.00, refuelling on a fuel pad ($6E23's pump half), + a crash with an UP fare aboard and the shift's end are exercised by no + ride (gcov over the 25 rides shows exactly what is reached; level A's + ride covers the hatch exit, $6BFB). +- [ ] **Ticks that overrun a frame.** When a tick's work does not fit in + one frame (level V's row shift, the speech player), the C64 flushes + its sprites mid-frame and a collision near the top of the screen + reaches the read a tick late (MECHANICS.md). The port assumes every + tick fits; modelling it needs the C64's cycle timing per tick. Seen + only on a level-V ride whose cab hit the top border on a shift tick. diff --git a/VERIFIED.md b/VERIFIED.md index 85d01c8..c36a038 100644 --- a/VERIFIED.md +++ b/VERIFIED.md @@ -68,6 +68,59 @@ plus the inline scripts run during verification sessions. | `$704E` hudArchive copies 7 bytes from `$07C2/$07E0` to `$71D3,X*8` / `$71F3,X*8` | With `$7214 = 2` (fare idx -> X = 16), `$07C2 = AA BB CC DD EE FF 11` and `$07E0 = 22 33 44 55 66 77 88`: after call, `$71E3 = AA BB CC DD EE FF 11 00` and `$7203 = 22 33 44 55 66 77 88 00`. Per-fare 8-byte slot stride confirmed; only 7 bytes written, 8th stays at prior value. | | `$43A5` hudInit copies template `$43B1-$43B7 -> $07E0-$07E6` | After clobbering `$07E0` with `$FF * 7`, calling `$43A5`: `$07E0 = $66 $66 $66 $74 $77 $74 $74` matching template byte-for-byte. | +## VERIFIED against the C64 in VICE: every screen (2026-10-06) + +`tools/stsim/gate.sh c64` rides every screen -- A..X and the MYSTERY +SCREEN, Y -- on the original disk in VICE (`c64trace.py`) and on the port's +simulation (`stsim`), and the two per-tick traces are byte-identical: the +cab's position and velocity, pad, fare stage, crash phase, input, sprite 0 +and 1 shadows, the shown sprites' positions and enable mask, the demo +playback offset and timer, the RNG, fuel, fare slots, destination, the +score, fare and fuel rows, border and background, and FNV hashes of all of +screen RAM, colour RAM and the charset. `gateHashes.txt` is the md5 of each +C64 trace, so `make check` holds the port to the real machine without an +emulator. + +A ride is the attract demo's machinery driven to every screen: the demo +slots ($50C1) name the screen, the playback buffer ($0902) holds the same +640 bytes the sim plays, the screens-played count ($7215) makes it the +screen's place in the 24-hour shift (so every screen after B starts with +the takeoff fare, and Y with the finished-shift HUD), and a finished wreck +takes the real game's path ($6B52 -> $6B64: a cab lost, the cab respawned +at $5F27) instead of the demo's return to the title. H, T, W and X ride +the C64's own attract recordings; the other 21 ride streams searched for by +`tools/stsim/recorder`. + +What the comparison found wrong in the port, each fixed from the asm: + +| Fix | Evidence | +| --- | -------- | +| The descent lamp (`fuelBarHud`) lit by the wrong rule | `$6419`: hi byte of `$714F` negative -> cyan, zero with `$714E` bit 7 -> yellow, positive -> red on row 24 (`X=$28`) | +| Collisions saw one frame; the latch holds two | `$6966` reads two frames after the last read (`$404B` x2), so both the old and new sprite positions are in the read | +| Only cab pairs were latched | Hooks test `AND #$F8` (G `$7E35`, dispatch `$69F9`): level G re-hops when the flame touches an orb | +| Hazards' background bits were wiped when the cab hit too | `bg = 1` replaced the hazards' `$D01F` bits; now OR'd | +| The older frame's `$D01F` used the previous read's picture | VICE: at tick 2743 of level P, `$D01F` was still `$00` at `$5F97` -- the windsock at `$0677` flipped before the wait, so the melting flake no longer touched it | +| Level O had no gaps in its barriers, rolled the wrong glyph, ran at the wrong hook | prelude0 `$7D9C` blanks 4 x 8 cells; `$7E6E` rolls `$2BE0` = glyph `$7C`; per-tick is `$7D6F` | +| Level R's maze flip missed two cells | `$7D9C` writes `$0709..$070D` (five cells, not three) | +| Level K's and U's contact verdicts | `$7DC4` `LDA #$FF` (kill, no wreck); `$7DDA` `LDA #$00` (safe) | +| Level V scrolled every tick, columns 0..38, silently | `$7DA6`: every 16th tick (`AND #$0F`), columns 1..38 (`$7E0E`/`$7E35`), SFX `$7D9D` | +| Level E grew its beanstalk only when the fare wanted UP -- backwards | `$7DD5`: `CMP #$0B; BNE` grow, `RTS` when UP | +| Level I drew its bullet's climb and direction in the wrong order; hires bullets | `$7E21` rng(2) before `$7E2F` rng(4); prelude1 `$7DB1` sets `$D01C = $FF` | +| Level Q cycled cels every tick, hires, and never buzzed | `$7DFF` every other tick; prelude1 `$7D9B` `$D01C = $FF`; `$7E55` buzz SFX with rng(100) while `$7217` is 0 | +| Voice release timers missing | `$42E9` arms `$7216+voice`, `$4320` counts down, `$6C85` sets 1, `$6906` clears; read by Q and Y | +| A landing's event never voiced; wrong-pad landings kept the fare | `$61BD`: event 2/3 runs `$43A5` (meter lost) and speaks `$6C7A`/`$6C7D`; `$6C7F` clears `$71CD` | +| Respawn re-ran the level's first prelude and the takeoff fare | `$6BCD JMP $5F27` skips `$6946`, `$7D66`, `$6F18`: a waiting passenger stays put across a crash | +| The message blank was 11 characters | `$6C38` is 12 spaces; the takeoff text was 13 (`$6C1E` is 12) | +| Level F (TAXI PONG) and Y (MYSTERY SCREEN) had no hooks at all | F `$7D9C`/`$7DDB`; Y `$7D9B`/`$7E02`/`$7E21`, its program running on to `$80CA` (STL5 ships `$7D98..$80CA`); Y's sprites come from extra scene `$23` | +| The gate's own start differed from the C64's | `$61FB` wraps the turn and counts the screen before `$62F0` (the port's game did; stsim did not) | + +Known and not modelled: a tick whose work overruns a frame (level V's row +shift, speech) flushes the sprites mid-frame, so a collision at the top +of the screen can reach the C64's read a tick later than the port's +(MECHANICS.md, "Ticks that overrun a frame"). Found on a level-V ride +whose cab hit the top border on a shift tick (VICE: flush at raster 58); +the gate's V ride does not do that. + ## UNVERIFIED (acknowledged, not yet proven) | Claim | Why unverified | diff --git a/assets/romToLevel.py b/assets/romToLevel.py index 316ec16..3ed1cff 100644 --- a/assets/romToLevel.py +++ b/assets/romToLevel.py @@ -9,12 +9,13 @@ # mkstlevel tool stays in tree for hand-authored .txt levels. # # Usage: -# romToLevel.py # all 24 levels +# romToLevel.py # all 25 screens + title # romToLevel.py # one specific # # Writes: -# /level01.dat ... level24.dat (scene 0 = "A" = level01) -# /tbank0.png (charset extracted from $2800) +# /level01.dat ... level25.dat (scene 0 = "A" = level01, +# scene 24 = "Y" = level25) +# /title.dat import os import struct @@ -49,11 +50,18 @@ ST_TILE_PX = 8 VIC_X_ORIGIN = 24 VIC_Y_ORIGIN = 50 PAD_SURFACE_ORIGIN = 36 -LEVEL_LETTERS = [chr(ord('A') + i) for i in range(24)] +# Screens A..X, then Y: the 25th, " MYSTERY SCREEN !!! ", which $5167 +# loads once the 24-hour shifts have cleared A..X. +LEVEL_LETTERS = [chr(ord('A') + i) for i in range(25)] +# The hook blob each level ships: its six trampolines' targets and +# tables from $7D98 up to the last byte any hook reads. Level Y's +# program runs on past $8000; its last SFX program is $80C2..$80CA. +HOOK_FIRST = 0x7D98 +HOOK_END = 0x80CB def write_level(path, level_index, after, screen, colormap, sprite_blocks): - """Write an STL4 level .dat straight from post-decompression RAM. + """Write an STL5 level .dat straight from post-decompression RAM. Everything stays in the C64's own units so the runtime can mirror the original routines byte for byte: @@ -71,16 +79,16 @@ def write_level(path, level_index, after, screen, colormap, sprite_blocks): passenger X frac, passenger X col, unused tilemap[1000] screen RAM (all 25 rows, the HUD rows included) colormap[1000] colour RAM low nibbles - levelIndex 0..23 = A..X, 24 = title + levelIndex 0..23 = A..X, 24 = Y, 25 = title spriteCount N, then N x (ptr, 63 bytes): the per-level sprite bitmaps the level's extra scene loads at $2000+ - hookData[616] $7D98-$7FFF: the level's hook code and tables + hookData[819] $7D98-$80CA: the level's hook code and tables """ cells = ST_TILEMAP_W * ST_PLAYFIELD_ROWS assert len(screen) == cells, f'screen is {len(screen)}, need {cells}' assert len(colormap) == cells, f'colormap is {len(colormap)}, need {cells}' with open(path, 'wb') as fp: - fp.write(b'STL4') + fp.write(b'STL5') fp.write(bytes(after[0x7D78:0x7D78 + 22])) fp.write(bytes(after[0x7D00:0x7D09])) fp.write(bytes([after[0x7D5A], after[0x7D09], after[0x7D60]])) @@ -100,7 +108,7 @@ def write_level(path, level_index, after, screen, colormap, sprite_blocks): for ptr, data in sprite_blocks: fp.write(bytes([ptr])) fp.write(bytes(data)) - fp.write(bytes(after[0x7D98:0x8000])) + fp.write(bytes(after[HOOK_FIRST:HOOK_END])) # Levels whose data blob flags an extra scene ($7D97 != 0) load a second @@ -138,23 +146,6 @@ def extract_one_level(ram, scene_id, out_dir): return out_path, after[0x7D5A], len(sprite_blocks) -def extract_screen_y(ram, out_dir): - """Screen Y, the 25th screen ("MYSTERY SCREEN !!!"), is scene 24. - - $5010 dispatches on the screens-completed counter: at 24 it jumps to - $5167, which loads #$59 ('Y') as the pending screen; the shift only - ends when the counter reaches 25. So a full 24-hour shift plays A..X - and then Y -- the 25 screens the intro advertises. - """ - screen_buf, color_buf, after = run_scene(ram, 24) - cells = ST_TILEMAP_W * ST_PLAYFIELD_ROWS - screen = list(screen_buf[:cells]) - colormap = [(b & 0x0F) for b in color_buf[:cells]] - out_path = os.path.join(out_dir, 'level25.dat') - write_level(out_path, 24, after, screen, colormap, []) - return out_path - - def extract_title(ram, out_dir): """The title screen is scene 25 (what $477A decompresses). Pads, spawn and templates are dummies.""" @@ -183,22 +174,17 @@ def main(): if len(sys.argv) >= 4: scene_ids = [int(sys.argv[3], 0)] else: - # Levels A..X = scenes 0..23. The title comes from screen RAM - # at $0400 (the dump was taken on the title screen), not from - # a decompressed scene. - scene_ids = list(range(24)) + # Screens A..Y = scenes 0..24, then the title (scene 25). + scene_ids = list(range(len(LEVEL_LETTERS))) for sid in scene_ids: path, pad_count, nspr = extract_one_level(ram, sid, out_dir) size = os.path.getsize(path) print(f' scene {sid:2d} (level {LEVEL_LETTERS[sid]}): pads={pad_count} sprites={nspr} size={size} -> {path}') - # Screen Y and the title are not part of the A..X sweep. if len(sys.argv) < 4: - y_path = extract_screen_y(ram, out_dir) - print(f' scene 24 (screen Y) : -> {y_path} ({os.path.getsize(y_path)} bytes)') title_path = extract_title(ram, out_dir) - print(f' title (screen RAM $0400) : -> {title_path} ({os.path.getsize(title_path)} bytes)') + print(f' title (scene 25) : -> {title_path} ({os.path.getsize(title_path)} bytes)') if __name__ == '__main__': diff --git a/program.mk b/program.mk index 9c79383..02556a0 100644 --- a/program.mk +++ b/program.mk @@ -41,13 +41,14 @@ STAXI_GEN := $(PROJECT_BUILD)/generated/$(ASSET_PLATFORM) STAXI_RUN := $(call programDataDir,STAXI) STAXI_OPT := $(call programOptionalDir,STAXI) -# 24 lettered levels (A..X) from scenes 0..23 plus the title from scene 24, -# extracted by assets/romToLevel.py, which runs the captured decompressor -# through a Python 6502 emulator over the raw RAM dump and writes STL2 .dat -# files directly. (mkstlevel + title.txt is no longer used: the C64 title -# encodes its screen with the game's custom charset, so a hand-authored tile -# layout with a different glyph mapping renders as garbage.) -STAXI_LEVEL_NUMS := 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 +# The 25 screens -- A..X from scenes 0..23 and the MYSTERY SCREEN, Y, from +# scene 24 -- plus the title from scene 25, extracted by assets/romToLevel.py, +# which runs the captured decompressor through a Python 6502 emulator over the +# raw RAM dump and writes STL5 .dat files directly. (mkstlevel + title.txt is +# no longer used: the C64 title encodes its screen with the game's custom +# charset, so a hand-authored tile layout with a different glyph mapping +# renders as garbage.) +STAXI_LEVEL_NUMS := 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 STAXI_LEVELS := $(foreach n,$(STAXI_LEVEL_NUMS),levels/level$(n).dat) levels/title.dat ROMTOLEVEL := $(STAXI_HOME)/assets/romToLevel.py @@ -58,16 +59,17 @@ ROMTOLEVEL := $(STAXI_HOME)/assets/romToLevel.py # cels at entry -- so they are optional data, which the IIgs data disk packs # last, while there is room. # ...except on the X68000, where they do not fit. The Human68k floppy is -# 1232 KB and these eight banks are 787 KB of it (level09 alone is 236 KB, -# level10 175, level17 165), which with the 296 KB binary, the levels and -# the speech put the disk about 300 KB over however small speech gets. +# 1232 KB and, measured when there were eight, the banks were 787 KB of it +# (level09 alone 236 KB, level10 175, level17 165), which with the 296 KB +# binary, the levels and the speech put the disk about 300 KB over however +# small speech gets. # stRenderLevelCels already handles their absence the right way: it builds # and compiles the level's cels at ENTRY, behind the LOADING bar, so the # ride itself still never compiles. That is also what spacetaxi.c's # codegen-arena comment says the 68k ports are meant to do -- their cels # are big and their JIT is fast. Ship them there again if the payload ever # moves to a hard disk image. -STAXI_CEL_LEVELS := $(if $(filter x68000,$(PLATFORM)),,07 08 09 10 12 16 17 21) +STAXI_CEL_LEVELS := $(if $(filter x68000,$(PLATFORM)),,06 07 08 09 10 12 16 17 21 25) # The C64-exact cel bank, generated by staxibake (from the shared cel table # stCels.c + C64 bitmaps) and compiled for this target by the host diff --git a/scripts/verify.sh b/scripts/verify.sh index 765ffa5..361abe7 100755 --- a/scripts/verify.sh +++ b/scripts/verify.sh @@ -1,9 +1,10 @@ #!/usr/bin/env bash # verify.sh - Space Taxi's gates, run one after another, with a summary. # -# stsim every hook level's 4000-tick simulation trace is identical to -# its golden (tools/stsim/gate.sh), and every cel a ride shows -# is baked (tools/stsim/celGate.sh) +# stsim every screen's ride (all 25) traces byte for byte as the real +# C64 traced it in VICE -- gateHashes.txt holds the C64 traces' +# md5s (tools/stsim/gate.sh) -- and every cel a ride shows is +# baked (tools/stsim/celGate.sh) # harness the front-end screens above the simulation -- the high-score # table, name entry and its save -- on JoeyLib's host BLANK # port (tools/staxiharness) diff --git a/stFare.c b/stFare.c index 508f2c1..4041e3f 100644 --- a/stFare.c +++ b/stFare.c @@ -8,6 +8,8 @@ // pad has been used the destination becomes "UP PLEASE" and the top // hatch opens. +#include + #include "stSim.h" // Screen-code strings ($6C1E..$6C7D). All ASCII-compatible glyphs. @@ -21,6 +23,20 @@ static const uint8_t kSpeechUp[] = "U?"; // $6C6F static const uint8_t kSpeechPad[] = "PX?"; // $6C72 (X = digit) static const uint8_t kSpeechThanks[] = "!"; // $6C76 static const uint8_t kSpeechHey[] = "H"; // $6C78 +static const uint8_t kSpeechPadAgain[] = "PX"; // $6C7A (X = digit) +static const uint8_t kSpeechUpAgain[] = "U"; // $6C7D +// $6727/$6F3A/$6F3D patch the destination digit into "PAD PLEASE" +// ($6C22, the message's fifth cell) and the two pad phrases ($6C73, +// $6C7B: the second character). +#define ST_TEXT_PAD_DIGIT_AT 4u +#define ST_SPEECH_DIGIT_AT 1u +#define ST_DIGIT_GLYPH_BASE 0x30u +// $6F20: from the third screen on, the cab starts with a fare aboard +// who wants pad 1. +#define ST_TAKEOFF_LEVEL 2u +#define ST_TAKEOFF_PAD 1u +// $6C85: speaking leaves voices 1 and 2 one tick from their gate-off. +#define ST_SPEECH_RELEASE 1u // Waiting wave cycle ($66D6) and the base-fare blob ($43B9 = +5.00). static const uint8_t kWaveCels[4] = { 0xC6, 0xC7, 0xD9, 0xC7 }; static const uint8_t kFareBlob[7] = { 0x66, 0x66, 0x66, 0x6F, 0x77, 0x74, 0x74 }; @@ -28,9 +44,11 @@ static const uint8_t kFareBlob[7] = { 0x66, 0x66, 0x66, 0x6F, 0x77, 0x74, 0 static void beamIn(StSimT *sim); static bool decayStep(StSimT *sim); +static void drawPadPlease(StSimT *sim); static void idleTicker(StSimT *sim); static void lightOnAndLatch(StSimT *sim); static void newFare(StSimT *sim); +static void sayPadPhrase(StSimT *sim, const uint8_t *phrase); static void stageBoard(StSimT *sim); static void stageCleanup(StSimT *sim); static void stageDropIn(StSimT *sim); @@ -76,6 +94,13 @@ static bool decayStep(StSimT *sim) { } +// "PAD n PLEASE" on the message row, n = the fare's destination. +static void drawPadPlease(StSimT *sim) { + stSimDrawText(sim, ST_MESSAGE_COL, ST_MESSAGE_ROW, kTextPadPlease, 1u); + stSimPutChar(sim, (uint8_t)(ST_MESSAGE_COL + ST_TEXT_PAD_DIGIT_AT), ST_MESSAGE_ROW, (uint8_t)(sim->activeSpriteIdx + ST_DIGIT_GLYPH_BASE)); +} + + // $660B -- a new fare turns up on a 3% roll per tick or when the // 100-tick timer runs out, never mid-crash. static void idleTicker(StSimT *sim) { @@ -123,6 +148,17 @@ static void newFare(StSimT *sim) { } +// $6C7F on a pad phrase ($6C72 "PX?" or $6C7A "PX"), the destination's +// digit in place of the X. +static void sayPadPhrase(StSimT *sim, const uint8_t *phrase) { + uint8_t speech[sizeof(kSpeechPad)]; + + memcpy(speech, phrase, strlen((const char *)phrase) + 1u); + speech[ST_SPEECH_DIGIT_AT] = (uint8_t)(sim->activeSpriteIdx + ST_DIGIT_GLYPH_BASE); + stFareBoardingSound(sim, speech); +} + + // $66DD -- the passenger shrinks into the cab, then picks where to go. static void stageBoard(StSimT *sim) { if (!decayStep(sim)) { @@ -139,16 +175,8 @@ static void stageBoard(StSimT *sim) { if (sim->activeSpriteIdx == ST_FARE_DEST_UP) { return; } - stSimDrawText(sim, 14u, 24u, kTextPadPlease, 1u); - stSimPutChar(sim, 18u, 24u, (uint8_t)(sim->activeSpriteIdx + 0x30u)); - { - uint8_t speech[4]; - speech[0] = kSpeechPad[0]; - speech[1] = (uint8_t)(sim->activeSpriteIdx + 0x30u); - speech[2] = kSpeechPad[2]; - speech[3] = 0u; - stFareBoardingSound(sim, speech); - } + drawPadPlease(sim); + sayPadPhrase(sim, kSpeechPad); } @@ -251,7 +279,8 @@ static void walkToTarget(StSimT *sim) { // $6C7F -- the passenger speaks: one sample per character. void stFareBoardingSound(StSimT *sim, const uint8_t *speech) { - (void)sim; + memset(sim->sidRelTimer, ST_SPEECH_RELEASE, sizeof(sim->sidRelTimer)); // $6C85 + sim->eventDispatchType = ST_EVENT_NONE; // $6C99 stAudioSilence(); stAudioNoise(false); stAudioSpeechPhrase(speech); @@ -283,6 +312,28 @@ uint8_t stFareChooseDestination(StSimT *sim) { } +// $61BD -- once the touchdown bob has settled, a landing that left an +// event ($645C) is voiced: a fare waiting on another pad calls "HEY +// TAXI" ($6C6C); a fare aboard set down on the wrong pad loses what is +// on its meter ($43A5) and names its destination again, "PAD n" ($6C7A) +// or "UP" ($6C7D). +void stFareEventVoice(StSimT *sim) { + if (sim->bobTimer != 0u || sim->eventDispatchType == ST_EVENT_NONE) { + return; + } + if (sim->eventDispatchType == ST_EVENT_HAIL) { + stFareBoardingSound(sim, kSpeechHeyTaxi); + return; + } + stSimHudInit(sim); + if (sim->eventDispatchType == ST_EVENT_WRONG_PAD) { + sayPadPhrase(sim, kSpeechPadAgain); + return; + } + stFareBoardingSound(sim, kSpeechUpAgain); +} + + // $6EF0 -- while the passenger walks (to or from the cab) the pad stays // lit only while the two sprites touch. void stFarePadLightingGate(StSimT *sim) { @@ -410,6 +461,31 @@ void stFareStageDispatch(StSimT *sim) { } +// $6F18 -- from the third screen on, the cab sets out with a fare aboard +// who wants pad 1, and the prelude will have them say so ($5F30). +void stFareTakeoffSetup(StSimT *sim) { + sim->eventDispatchType = ST_EVENT_NONE; + sim->stage = ST_STAGE_IDLE; + if (sim->levelState < ST_TAKEOFF_LEVEL) { + return; + } + sim->stage = ST_STAGE_RIDING; + sim->activeSpriteIdx = ST_TAKEOFF_PAD; + sim->activeDyingSlot = ST_TAKEOFF_PAD; + drawPadPlease(sim); + sim->eventDispatchType = ST_EVENT_HAIL; +} + + +// $5F30 -- the end of the prelude: the takeoff fare names its pad. +void stFareTakeoffVoice(StSimT *sim) { + if (sim->eventDispatchType == ST_EVENT_NONE) { + return; + } + sayPadPhrase(sim, kSpeechPad); +} + + // $6CB8 -- no fare pad left: the passenger wants to leave the screen, // so the top hatch opens. void stFareUpPlease(StSimT *sim) { diff --git a/stHookTables.h b/stHookTables.h index 3b203cd..94d0d3b 100644 --- a/stHookTables.h +++ b/stHookTables.h @@ -1,6 +1,6 @@ // Space Taxi -- where each level's hook program keeps its tables. // -// Every address is a byte in the level's shipped hook blob ($7D98..$7FFF, +// Every address is a byte in the level's shipped hook blob ($7D98..$80CA, // StLevelT.hookData), so the same numbers serve the hook programs // (stHooks.c, reading through the live simulation) and the cel manifest // (stLevelCels.c, reading the level file alone -- the offline cel baker @@ -10,6 +10,7 @@ // Level indices (0 = A). #define ST_LEVEL_E 4u +#define ST_LEVEL_F 5u #define ST_LEVEL_G 6u #define ST_LEVEL_H 7u #define ST_LEVEL_I 8u @@ -26,10 +27,20 @@ #define ST_LEVEL_V 21u #define ST_LEVEL_W 22u #define ST_LEVEL_X 23u +#define ST_LEVEL_Y 24u + +// The level header's shared sprite multicolours ($7D05/$7D06 -> $D025/$D026). +#define ST_HDR_SPRITE_MC0 5u +#define ST_HDR_SPRITE_MC1 6u // Hardware sprites 3..7 are the hook sprites on every level that has any. #define ST_HOOK_FIRST_SPRITE 3u +// ---- level F "TAXI PONG" ---- +#define ST_HK_F_SPRITE 3u // the ball +#define ST_HK_F_CEL 0x80u +#define ST_HK_F_COLOR 1u + // ---- level G "TELEPORTS" ---- #define ST_HK_G_CEL 0x7DD4u // 6 cels the orbs cycle through #define ST_HK_G_CEL_PHASES 6u @@ -82,4 +93,19 @@ #define ST_HK_U_CEL_PHASES 4u #define ST_HK_U_COLOR 7u +// ---- level Y "MYSTERY SCREEN" ---- +// Sprite 3 is a multicolour saucer, sprite 4 a hires walker, sprite 5 +// the walker's shot ($83 rising, then $84/$85 bursting). +#define ST_HK_Y_SAUCER 3u +#define ST_HK_Y_WALKER 4u +#define ST_HK_Y_SHOT 5u +#define ST_HK_Y_CEL_SAUCER 0x80u +#define ST_HK_Y_CEL_WALK 0x81u +#define ST_HK_Y_CEL_WALK_ALT 0x82u +#define ST_HK_Y_CEL_SHOT 0x83u +#define ST_HK_Y_CEL_SHOT_END 0x86u // one past the last burst cel +#define ST_HK_Y_COLOR_SAUCER 1u +#define ST_HK_Y_COLOR_WALKER 0x0Fu +#define ST_HK_Y_MULTICOLOR 0x0Fu // $7E02: $D01C, sprites 0..3 + #endif // ST_HOOK_TABLES_H diff --git a/stHooks.c b/stHooks.c index ce3768d..c4ac41e 100644 --- a/stHooks.c +++ b/stHooks.c @@ -1,10 +1,10 @@ // 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 +// Every level's data blob ends with six trampolines ($7D66..$7D77) and +// a small program from $7D98 (on to $80CA for level Y) 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 in the level blob and re-expresses each program here, // naming the tables it reads at the addresses the original used, so @@ -53,6 +53,10 @@ #define H_SEG_FLAG(sim, id) hookHByte(sim, (uint16_t)(0x7E15u + (id))) #define H_SWITCH_SEGS(sim, i) hookHByte(sim, (uint16_t)(0x7E38u + (i))) +// ---- level F "TAXI PONG" ($7DD1/$7DD2, $7DD5..$7DDA scratch) ---- +#define F_GRAVITY(sim) ((uint16_t)(HKT(sim, 0x7DD3) | ((uint16_t)HKT(sim, 0x7DD4) << 8))) +#define F_SFX_BOUNCE(sim) HKTP(sim, 0x7EAA) + // ---- level G "TELEPORTS" ($7DD1..$7DD3/$7E00/$7F02 scratch) ---- #define G_CEL(sim, ph) HKT(sim, ST_HK_G_CEL + (ph)) #define G_ORB_COL(sim, x) HKT(sim, ST_HK_G_ORB_COL + (x)) @@ -71,6 +75,16 @@ #define I_COLOR(sim, i) HKT(sim, ST_HK_I_COLOR + (i)) #define I_SFX_FIRE(sim) HKTP(sim, 0x7E82) +// ---- level K "MAGNETS" ($7DBD scratch) ---- +#define K_POLE_COL(sim, k) HKT(sim, 0x7DBE + (k)) + +// ---- level O "ELECTROIDS" ($7DBD scratch) ---- +#define O_GAP_COL(sim, k) HKT(sim, 0x7E66 + (k)) +#define O_GAP_ROW(sim, k) HKT(sim, 0x7E6A + (k)) + +// ---- level V "SHIFT-O-RAMA" ($7DE2 scratch) ---- +#define V_SFX_SHIFT(sim) HKTP(sim, 0x7D9D) + // ---- level Q "INTERFERENCE" ($7E83 scratch) ---- #define Q_SPR_COL(sim, x) HKT(sim, 0x7DD2 + (x)) #define Q_SPR_MSB(sim, x) HKT(sim, 0x7DDA + (x)) @@ -78,6 +92,7 @@ #define Q_INIT_PHASE(sim, x) HKT(sim, 0x7DEA + (x)) #define Q_CEL(sim, ph) HKT(sim, ST_HK_Q_CEL + (ph)) #define Q_SCRAMBLE(sim, i) HKT(sim, 0x7E7F + (i)) +#define Q_SFX_BUZZ(sim) HKTP(sim, 0x7E76) // ---- level T "FAST BREAK" ($7D9C scratch) ---- #define T_SFX_BOUNCE(sim) HKTP(sim, 0x7E56) @@ -116,6 +131,21 @@ // ---- level P "BLIZZARD" ($7F0E/$7F16/$7F1F scratch) ---- #define P_SFX_WIND(sim) HKTP(sim, 0x7E3C) +// ---- level Y "MYSTERY SCREEN" (StHookYT scratch; $7168 too) ---- +#define Y_WALK_TURN_COL(sim, d) HKT(sim, 0x7EC3 + (d)) +#define Y_STEP(sim, d) HKT(sim, 0x7EC5 + (d)) +#define Y_TURN_DIR(sim, d) HKT(sim, 0x7EC7 + (d)) +#define Y_BLOCK_TURN_COL(sim, d) HKT(sim, 0x7F67 + (d)) +#define Y_BLOCK_OFFSET(sim, i) HKT(sim, 0x7FB8 + (i)) +#define Y_BLOCK_COLOR(sim, i) HKT(sim, 0x7FC1 + (i)) +#define Y_BLOCK_CHAR(sim, i) HKT(sim, 0x7FCB + (i)) +#define Y_SFX_STEP(sim) HKTP(sim, 0x808E) +#define Y_SFX_SHOT(sim) HKTP(sim, 0x8097) +#define Y_SFX_PARK(sim) HKTP(sim, 0x80A0) +#define Y_SFX_HUM(sim) HKTP(sim, 0x80A9) +#define Y_HUM_PITCH(sim, i) HKT(sim, 0x80B2 + (i)) +#define Y_SFX_WALK(sim) HKTP(sim, 0x80C2) + // ---- level S "THE SWITCH" ---- #define S_DIR_MAP(sim, d) HKT(sim, 0x7DAC + (d)) @@ -133,6 +163,19 @@ typedef struct { static const StHookFallT kFallJ = { 0x7EB9u, 0x7EC1u, ST_HK_J_CEL_BURST, ST_HK_J_BURST_PHASES, true }; static const StHookFallT kFallP = { 0x7EF9u, 0x7F01u, ST_HK_P_CEL_BURST, ST_HK_P_BURST_PHASES, false }; +// Levels Q and Y sound their buzz on voice 2 (SFX voice index 1) only +// while its release timer ($7217) has run out. Q's buzz pitch is +// rng(100) + 100 ($7E5C, the carry clear out of the RNG). +#define ST_HOOK_BUZZ_VOICE 1u +#define ST_HOOK_Q_BUZZ_SPAN 0x64u + +// $D01C with every sprite multicolour (levels I and Q). +#define ST_HOOK_ALL_MULTICOLOR 0xFFu + +// A hit verdict ($7D75) with bit 7 set skips the falling wreck ($6A72 +// tests $721D bit 7): levels K and L kill outright with LDA #$FF. +#define ST_HOOK_KILL_NO_WRECK 0xFFu + // Each level H switch drives four wall segments ($7E24 stride, $7E38 // list), animated over four steps. #define ST_HOOK_H_SEGMENTS 4u @@ -142,7 +185,7 @@ static const StHookFallT kFallP = { 0x7EF9u, 0x7F01u, ST_HK_P_CEL_BURST, ST_HK_P // of their three cels, and a bounce holds off for twelve ticks ($7E0E). #define ST_HOOK_U_COOLDOWN 0x0Cu -// Level E: pad 1 alone at first; while the fare wants UP a leaf cell +// Level E: pad 1 alone at first; unless the fare wants UP a leaf cell // sprouts every $57 ticks (the count starts at $1E, or $46 on the // second screen), four to a side from columns 20/19, and the fifth // step hangs the next pair of pads at the leaf ends, opens them and @@ -189,14 +232,86 @@ static const StHookFallT kFallP = { 0x7EF9u, 0x7F01u, ST_HK_P_CEL_BURST, ST_HK_P // Level L: hardware sprite 3 is the hole at ($AC, $82), cels $80..$82. // Gravity pulls the cab toward it: $10 (Y) and $0C (X) less an eighth // of the distance, X measured in halved sprite units so it fits a -// byte. Touching it returns verdict $FF: a kill with no falling wreck. +// byte. Touching it is a kill with no falling wreck ($7D9B LDA #$FF). #define ST_HOOK_L_HOLE 3u #define ST_HOOK_L_HOLE_COL 0xACu #define ST_HOOK_L_HOLE_ROW 0x82u #define ST_HOOK_L_HOLE_X_HALF 0x5Bu #define ST_HOOK_L_PULL_Y 0x10u #define ST_HOOK_L_PULL_X 0x0Cu -#define ST_HOOK_L_VERDICT 0xFFu + +// Level K: six magnet poles on row 4. Touching a sprite kills outright +// ($7DC4 LDA #$FF), like level L's hole. +#define ST_HOOK_K_POLES 6u +#define ST_HOOK_K_POLE_ROW 4u + +// Level O: the prelude ($7D9C) cuts an eight-cell gap of spaces into +// each of the four barrier rows ($401B puts the first cell, $4021 the +// seven to its right), from the column and row tables at $7E66/$7E6A. +// Every tick ($7DBE) the barrier glyph rolls a pixel right ($7E6E); on +// every fourth tick rows 5 and 13 scroll a cell left, and on every +// eighth rows 9 and 17 as well (hookRowScrollLeft). +#define ST_HOOK_O_GAPS 4u +#define ST_HOOK_O_GAP_CELLS 8u +#define ST_HOOK_O_TICK_MASK 7u +#define ST_HOOK_O_FAST_MASK 3u +#define ST_HOOK_O_SLOW_TICK 4u +#define ST_HOOK_O_ROW_FAST_A 5u // $04C9 +#define ST_HOOK_O_ROW_FAST_B 13u // $0609 +#define ST_HOOK_O_ROW_SLOW_A 9u // $0569 +#define ST_HOOK_O_ROW_SLOW_B 17u // $06A9 + +// Level R: a gate cell holds the bar cells' glyph EOR $46 ($7DA5). +#define ST_HOOK_R_GATE_FLIP 0x46u + +// The rows levels O and V scroll: cells 1..38, the borders stay. +#define ST_HOOK_ROW_FIRST_COL 1u +#define ST_HOOK_ROW_LAST_COL 0x26u + +// Level V: rows 6..17 shift every sixteenth tick ($7DAB AND #$0F on the +// $7DE2 count), bit 1 of the row picking the direction. +#define ST_HOOK_V_RATE_MASK 0x0Fu +#define ST_HOOK_V_FIRST_ROW 6u +#define ST_HOOK_V_END_ROW 0x12u +#define ST_HOOK_V_LEFT_BAND 2u + +// Level F: the ball steps 3 pixels; it drops from row $6E, bounces at +// row $6D or from row $AA, and turns where its X low byte is 0 or $6E. +#define ST_HOOK_F_SPEED 3u +#define ST_HOOK_F_DROP_Y 0x6E00u +#define ST_HOOK_F_CEILING_ROW 0x6Du +#define ST_HOOK_F_FLOOR_ROW 0xAAu +#define ST_HOOK_F_WALL_LO 0x6Eu + +// Level Y (see hookYPerTick). +#define ST_HOOK_Y_SAUCER_ROW 0xA5u +#define ST_HOOK_Y_SAUCER_STEP 2u +#define ST_HOOK_Y_SAUCER_END_COL 0x58u // with the msb set: X $158 +#define ST_HOOK_Y_LAUNCH_ODDS 0xC8u +#define ST_HOOK_Y_LAUNCH_HIT 3u +#define ST_HOOK_Y_HUM_STEPS_MASK 0x0Fu +#define ST_HOOK_Y_WALKER_ROW 0x82u +#define ST_HOOK_Y_WALKER_COL 0xE6u +#define ST_HOOK_Y_WALK_CYCLE_MASK 0x3Fu +#define ST_HOOK_Y_AIM_GAP 6u +#define ST_HOOK_Y_SHOT_DELAY 0x1Eu +#define ST_HOOK_Y_SHOT_ROW 0x7Bu +#define ST_HOOK_Y_SHOT_TOP_ROW 0x2Bu +#define ST_HOOK_Y_SHOT_STEP 2u +#define ST_HOOK_Y_LIGHT_ROW 19u // $06F8 +#define ST_HOOK_Y_LIGHT_CELLS 0x78u +#define ST_HOOK_Y_LIGHT_STEP_MASK 3u +#define ST_HOOK_Y_LIGHT_START 3u +#define ST_HOOK_Y_LIGHT_COLOR 1u +#define ST_HOOK_Y_BLOCK_ROW 9u +#define ST_HOOK_Y_BLOCK_COL 5u +#define ST_HOOK_Y_BLOCK_CELLS 9u +#define ST_HOOK_Y_BLOCK_TICKS 0x0Fu +#define ST_HOOK_Y_BLOCK_STEP_FRAMES 2u // frames 0/1 step left, 2/3 right +#define ST_HOOK_Y_BLOCK_START 2u // the frame $7DF4 draws +#define ST_HOOK_Y_BLOCK_BLANK 4u // the all-space frame +// An SFX program's control byte (the SID waveform/gate register). +#define ST_HOOK_SFX_CTRL_AT 4u // Level P: the wind turns on rng($78) < 3 per tick, flipping the three // windsock cells (glyph ^ 6) and the sign of the X gravity. @@ -209,58 +324,69 @@ static const StHookFallT kFallP = { 0x7EF9u, 0x7F01u, ST_HK_P_CEL_BURST, ST_HK_P #define ST_HOOK_S_DIRS 0x0Fu -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 uint8_t hookHByte(const StSimT *sim, uint16_t addr); -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); -static void hookKPerTick(StSimT *sim); -static void hookOPerTick(StSimT *sim); +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 hookEPerTick(StSimT *sim); +static void hookEPrelude0(StSimT *sim); +static void hookEPut(StSimT *sim, uint8_t col, uint8_t row, uint8_t ch); +static void hookETips(StSimT *sim); +static void hookFPerTick(StSimT *sim); +static void hookFPrelude0(StSimT *sim); +static void hookFallBgHits(StSimT *sim, uint8_t *state, uint8_t *phase, const uint8_t *sfx); +static void hookFallTick(StSimT *sim, uint8_t *state, uint8_t *phase, const StHookFallT *d, uint8_t *parity); +static void hookGHop(StSimT *sim); +static void hookGPerTick(StSimT *sim); +static void hookGPrelude0(StSimT *sim); +static uint8_t hookHByte(const StSimT *sim, uint16_t addr); +static void hookHPerTick(StSimT *sim); +static void hookHPrelude0(StSimT *sim); +static void hookHSegment(StSimT *sim, uint8_t id); +static void hookIPerTick(StSimT *sim); +static void hookIPrelude(StSimT *sim); +static void hookJPerTick(StSimT *sim); +static void hookJPrelude0(StSimT *sim); +static void hookKPerTick(StSimT *sim); +static void hookLPerTick(StSimT *sim); +static void hookLPrelude0(StSimT *sim); +static uint16_t hookLPull(uint8_t base, uint8_t dist); +static void hookOPerTick(StSimT *sim); +static void hookOPrelude0(StSimT *sim); +static void hookPFlipSock(StSimT *sim, uint16_t cell); +static void hookPPerTick(StSimT *sim); +static void hookPPrelude0(StSimT *sim); ST_NOINLINE static void hookPerTickHigh(StSimT *sim); ST_NOINLINE static void hookPerTickLow(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 hookGHop(StSimT *sim); -static void hookGPerTick(StSimT *sim); -static void hookGPrelude0(StSimT *sim); -static void hookUBounce(StSimT *sim); -static void hookUOrbs(StSimT *sim); -static void hookUPerTick(StSimT *sim); -static void hookUPrelude0(StSimT *sim); -static void hookEPerTick(StSimT *sim); -static void hookEPrelude0(StSimT *sim); -static void hookEPut(StSimT *sim, uint8_t col, uint8_t row, uint8_t ch); -static void hookETips(StSimT *sim); -static void hookFallBgHits(StSimT *sim, uint8_t *state, uint8_t *phase, const uint8_t *sfx); -static void hookFallTick(StSimT *sim, uint8_t *state, uint8_t *phase, const StHookFallT *d, uint8_t *parity); -static void hookJPerTick(StSimT *sim); -static void hookJPrelude0(StSimT *sim); -static void hookLPerTick(StSimT *sim); -static void hookLPrelude0(StSimT *sim); -static uint16_t hookLPull(uint8_t base, uint8_t dist); -static void hookPFlipSock(StSimT *sim, uint16_t cell); -static void hookPPerTick(StSimT *sim); -static void hookPPrelude0(StSimT *sim); -static uint8_t hookSInput(StSimT *sim, uint8_t input); +static uint8_t hookQInput(StSimT *sim, uint8_t input); +static void hookQPerTick(StSimT *sim); +static void hookQPrelude(StSimT *sim); +static void hookRMaze(StSimT *sim, uint8_t ch); +static void hookRPerTick(StSimT *sim); +static void hookRowScrollLeft(StSimT *sim, uint8_t row); +static void hookRowScrollRight(StSimT *sim, uint8_t row); +static uint8_t hookSInput(StSimT *sim, uint8_t input); +static void hookTGate(StSimT *sim, uint8_t ch); +static void hookTPerTick(StSimT *sim); +static void hookTPrelude1(StSimT *sim); +static void hookUBounce(StSimT *sim); +static void hookUOrbs(StSimT *sim); +static void hookUPerTick(StSimT *sim); +static void hookUPrelude0(StSimT *sim); +static void hookVPerTick(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 hookYBlockDraw(StSimT *sim, uint8_t frame); +static uint8_t hookYHalfX(const StSpriteT *spr); +static void hookYHum(StSimT *sim); +static void hookYLightAndBlock(StSimT *sim); +static void hookYPerTick(StSimT *sim); +static void hookYPrelude0(StSimT *sim); +static void hookYPrelude1(StSimT *sim); +static void hookYShot(StSimT *sim); +static void setSpriteColors(StSimT *sim, uint8_t color); // $41AD after $401B -- the same character into the `rows` cells below. @@ -287,6 +413,297 @@ static void fillDownColor(StSimT *sim, uint8_t col, uint8_t row, uint8_t color, } +// Level E "BEANSTALK" ($7DA0 prelude0, $7DCA per-tick2): the screen +// opens with pad 1 only. Unless the fare wants UP ($7DD5: CMP #$0B, RTS +// on equal) the stalk grows: every $57 ticks a leaf cell sprouts right +// along row `row` and left along row `row`-2, the green tips one cell +// beyond; the fifth step hangs pads N and N+1 at the leaf ends instead, +// opens them (pad count +2) and moves the growth four rows up, until +// pad 9 is out. +static void hookEPerTick(StSimT *sim) { + StHookET *e = &sim->hookScratch.e; + + if (sim->specialPad == ST_HOOK_E_LAST_PAD || sim->activeSpriteIdx == ST_FARE_DEST_UP) { + return; + } + e->tick++; + if (e->tick != ST_HOOK_E_GROW_TICKS) { + return; + } + e->tick = 0u; + e->seg++; + if (e->seg != ST_HOOK_E_LEAVES) { + hookEPut(sim, (uint8_t)(ST_HOOK_E_STALK_COL + e->seg), e->row, ST_HOOK_E_CHAR_LEAF); + hookEPut(sim, (uint8_t)(ST_HOOK_E_STALK_COL - e->seg), (uint8_t)(e->row - 2u), ST_HOOK_E_CHAR_LEAF); + hookETips(sim); + return; + } + // $7E15: the pads hang from the leaf ends, then the stalk climbs. + hookEPut(sim, ST_HOOK_E_PAD_COL_RIGHT, e->row, (uint8_t)(ST_CHAR_DIGIT_BASE + e->padSlot)); + hookEPut(sim, ST_HOOK_E_PAD_COL_LEFT, (uint8_t)(e->row - 2u), (uint8_t)(ST_CHAR_DIGIT_BASE + e->padSlot + 1u)); + hookETips(sim); + sim->specialPad = (uint8_t)(sim->specialPad + 2u); + sim->padCount = (uint8_t)(sim->padCount + 2u); + e->seg = 0u; + e->row = (uint8_t)(e->row - ST_HOOK_E_ROW_STEP); + e->padSlot = (uint8_t)(e->padSlot + 2u); + stSimSfx(sim, E_SFX_GROW(sim)); +} + + +static void hookEPrelude0(StSimT *sim) { + StHookET *e = &sim->hookScratch.e; + + sim->specialPad = 1u; + sim->padCount = 1u; + e->seg = 0u; + e->padSlot = 2u; + e->row = ST_HOOK_E_FIRST_ROW; + e->tick = ST_HOOK_E_TICK_START; + if (sim->levelState == 1u) { + e->tick = ST_HOOK_E_TICK_SECOND; + } +} + + +// $7E89 -- one green cell. +static void hookEPut(StSimT *sim, uint8_t col, uint8_t row, uint8_t ch) { + stSimPutChar(sim, col, row, ch); + stSimPutColor(sim, col, row, ST_HOOK_E_COLOR); +} + + +// $7E39 -- the tips, one cell beyond this step's leaves. +static void hookETips(StSimT *sim) { + const StHookET *e = &sim->hookScratch.e; + + hookEPut(sim, (uint8_t)(ST_HOOK_E_STALK_COL + 1u + e->seg), e->row, ST_HOOK_E_CHAR_TIP_R); + hookEPut(sim, (uint8_t)(ST_HOOK_E_STALK_COL - 1u - e->seg), (uint8_t)(e->row - 2u), ST_HOOK_E_CHAR_TIP_L); +} + + +static void hookFPerTick(StSimT *sim) { + StHookFT *f = &sim->hookScratch.f; + StSpriteT *ball = &sim->spr[ST_HK_F_SPRITE]; + uint8_t xLo; + + f->x = (uint16_t)(f->x + f->vx); + f->vy = (uint16_t)(f->vy + F_GRAVITY(sim)); + f->y = (uint16_t)(f->y + f->vy); + ball->col = (uint8_t)f->x; + ball->msb = (uint8_t)(f->x >> 8); + ball->row = (uint8_t)(f->y >> 8); + if (ball->row == ST_HOOK_F_CEILING_ROW) { + f->vy = (uint16_t)(0u - f->vy); + } else if (ball->row >= ST_HOOK_F_FLOOR_ROW) { + stSimSfx(sim, F_SFX_BOUNCE(sim)); + f->vy = (uint16_t)(0u - f->vy); + } + xLo = (uint8_t)f->x; + if (xLo == 0u || xLo == ST_HOOK_F_WALL_LO) { + f->vx = (uint16_t)(0u - f->vx); + f->y = ST_HOOK_F_DROP_Y; + f->vy = 0u; + } +} + + +// Level F "TAXI PONG" ($7D9C prelude0, $7DDB per-tick2): a ball +// (sprite 3) crosses the screen three pixels a tick in 16-bit X and falls +// under the blob's own gravity ($7DD3) in 8.8 Y. It bounces off the floor +// (any row from $AA, with a click) and off an invisible ceiling at row +// $6D; whenever its X low byte reaches 0 or $6E -- the left wall at 0 and +// the right one at $16E -- it turns round and drops again from row $6E. +static void hookFPrelude0(StSimT *sim) { + StHookFT *f = &sim->hookScratch.f; + StSpriteT *ball = &sim->spr[ST_HK_F_SPRITE]; + + ball->enable = 1u; + ball->color = ST_HK_F_COLOR; + ball->col = 0u; + ball->msb = 0u; + ball->row = 0u; + ball->ptr = ST_HK_F_CEL; + f->x = 0u; + f->vx = ST_HOOK_F_SPEED; + f->y = ST_HOOK_F_DROP_Y; + f->vy = 0u; +} + + +// $7DAF (J) / $7DFE (P) -- a hazard that touched the scenery below row +// $38 bursts: cel $82, phase 0, dropped four rows onto what it hit. +// Sprites 7 down to 3, every one that hit. +static void hookFallBgHits(StSimT *sim, uint8_t *state, uint8_t *phase, const uint8_t *sfx) { + uint8_t x; + + for (x = (uint8_t)(ST_HW_SPRITES - 1u); x >= ST_HOOK_FIRST_SPRITE; x--) { + if ((sim->spriteBgColl & kStBit[x]) == 0u || sim->spr[x].row < ST_HOOK_FALL_HIT_ROW || state[x] == 2u) { + continue; + } + state[x] = 2u; + sim->spr[x].ptr = ST_HK_FALL_CEL_BURST; + phase[x] = 0u; + sim->spr[x].row = (uint8_t)(sim->spr[x].row + ST_HOOK_FALL_HIT_DROP); + if (sfx != 0) { + stSimSfx(sim, sfx); + } + } +} + + +// $7E01 (J) / $7E45 (P) -- the per-sprite machine, sprites 3..7. Idle: +// spawn on rng($3C) < 3 at rng($FF) + the sprite's column base (J +// rerolls one over the middle band), cel $80 on row $17. Falling: one +// row per tick, cel flipping $80/$81 and drifting sideways on the odd +// cel. Bursting: walk the cel run (P only every other count of a +// shared parity) and go idle at its end. +static void hookFallTick(StSimT *sim, uint8_t *state, uint8_t *phase, const StHookFallT *d, uint8_t *parity) { + uint8_t x; + + for (x = ST_HOOK_FIRST_SPRITE; x < ST_HW_SPRITES; x++) { + if (state[x] == 0u) { + uint16_t col; + if (stSimRng(sim, ST_HOOK_FALL_SPAWN_ODDS) >= ST_HOOK_FALL_SPAWN_HIT) { + continue; + } + do { + col = (uint16_t)(stSimRng(sim, 0xFFu) + HKT(sim, d->startTable + x)); + } while (d->avoidMiddle && col >= ST_HOOK_FALL_BAND_LO && col < ST_HOOK_FALL_BAND_HI); + sim->spr[x].col = (uint8_t)col; + sim->spr[x].msb = (uint8_t)(col >> 8); + sim->spr[x].ptr = ST_HK_FALL_CEL; + sim->spr[x].row = ST_HOOK_FALL_TOP_ROW; + sim->spr[x].enable = 1u; + state[x] = 1u; + } else if (state[x] == 1u) { + sim->spr[x].ptr ^= 1u; + if ((sim->spr[x].ptr & 1u) != 0u) { + stSimSpriteAddX(sim, x, HKT(sim, d->dxTable + x)); + } + sim->spr[x].row++; + } else if (state[x] == 2u) { + if (parity != 0) { + (*parity)++; + if ((*parity & 1u) != 0u) { + continue; + } + } + phase[x]++; + if (phase[x] == d->phaseEnd) { + state[x] = 0u; + sim->spr[x].enable = 0u; + } else { + sim->spr[x].ptr = HKT(sim, d->celTable + phase[x]); + } + } else { + state[x] = 0u; // $7E1C: anything else resets + } + } +} + + +// Level G "TELEPORTS" ($7DA0 prelude0, $7D9A prelude1, $7E01 per-tick2, +// $7DF2 verdict): five orbs on sprites 3..7 sit where the blob puts them +// and cycle their cel every fourth tick. The cab touching one starts a +// hop: it vanishes for $14 ticks with the stick dead and a falling tone, +// then reappears on a landing spot (hookGHop). Touching an orb is never +// a kill -- the verdict is 0 unless the scenery was hit, and a scenery +// hit also puts the cab sprite back. +// +// $7E4A -- land: rng(2) odd with a fare aboard sends the cab to its +// destination pad's spot (UP and beyond = spot 5); otherwise a random +// spot, stepped down if it repeats the last one (wrapping 1 -> 5). +static void hookGHop(StSimT *sim) { + StHookGT *g = &sim->hookScratch.g; + uint8_t spot; + uint8_t i; + + if ((stSimRng(sim, 2u) & 1u) != 0u && sim->activeSpriteIdx != 0u) { + spot = sim->activeSpriteIdx; + if (spot >= ST_HOOK_G_PAD_SPOTS) { + spot = ST_HOOK_G_SPOTS; + } + } else { + spot = stSimRng(sim, ST_HOOK_G_SPOTS); + if (spot == g->lastSpot) { + spot--; + if (spot == 0u) { + spot = ST_HOOK_G_SPOTS; + } + } + } + g->lastSpot = spot; + i = (uint8_t)(spot - 1u); + sim->spr[0].col = G_SPOT_COL(sim, i); + sim->posXcol = sim->spr[0].col; + sim->spr[0].msb = G_SPOT_MSB(sim, i); + sim->posXmsb = sim->spr[0].msb; + sim->spr[0].row = G_SPOT_ROW(sim, i); + sim->posYrow = sim->spr[0].row; + sim->velX = 0; + sim->velY = 0; +} + + +static void hookGPerTick(StSimT *sim) { + StHookGT *g = &sim->hookScratch.g; + uint8_t x; + + g->tick++; + if ((g->tick & 3u) == 0u) { + uint8_t cel; + g->celPhase++; + if (g->celPhase == ST_HK_G_CEL_PHASES) { + g->celPhase = 0u; + } + cel = G_CEL(sim, g->celPhase); + for (x = ST_HOOK_FIRST_SPRITE; x < ST_HW_SPRITES; x++) { + sim->spr[x].ptr = cel; + } + } + if (g->hop != 0u) { + // $7ECB: the tone falls, the stick is dead, and at zero the cab + // is back and lands. + g->sweep = (uint8_t)(g->sweep - ST_HOOK_G_SWEEP_STEP); + stAudioVoice1Freq(g->sweep); + sim->dirMask = 0u; + g->hop--; + if (g->hop == 0u) { + sim->spr[0].enable = 1u; + hookGHop(sim); + } + return; + } + if ((sim->spriteSpriteColl & ST_HOOK_FALL_SPRITES) == 0u) { + return; + } + // $7E3D: an orb took the cab. + sim->spr[0].enable = 0u; + g->hop = ST_HOOK_G_HOP_TICKS; + stSimSfx(sim, G_SFX_HOP(sim)); + g->sweep = ST_HOOK_G_SWEEP_START; + sim->velX = 0; + sim->velY = 0; +} + + +static void hookGPrelude0(StSimT *sim) { + uint8_t x; + + for (x = ST_HOOK_FIRST_SPRITE; x < ST_HW_SPRITES; x++) { + sim->spr[x].color = ST_HK_G_ORB_COLOR; + sim->spr[x].ptr = HKT(sim, ST_HK_G_CEL); + sim->spr[x].col = G_ORB_COL(sim, x); + sim->spr[x].msb = G_ORB_MSB(sim, x); + sim->spr[x].row = G_ORB_ROW(sim, x); + sim->spr[x].enable = 1u; + } + sim->hookScratch.g.celPhase = 0u; + sim->hookScratch.g.hop = 0u; +} + + // Level H's byte-addressed view of its own data block. // // Two of its reads index past the end of the table they name and land on @@ -376,7 +793,7 @@ static void hookHPerTick(StSimT *sim) { sim->hookScratch.h.tick = 0u; sim->hookScratch.h.step = 0xFFu; sim->hookScratch.h.tone = 0x12u; - stAudioSfx(H_SFX_CHIME(sim)); + stSimSfx(sim, H_SFX_CHIME(sim)); } // $7ED9 sim->hookScratch.h.tone++; @@ -456,6 +873,431 @@ static void hookHSegment(StSimT *sim, uint8_t id) { } +// 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 hookIPerTick(StSimT *sim) { + int8_t s; + + for (s = 3; s <= 7; s++) { + uint8_t x = (uint8_t)s; + uint8_t state = sim->hookScratch.i.state[x]; + if (state == 0u) { + uint8_t y; + if (stSimRng(sim, 0x75u) >= 3u) { + continue; + } + // $7E21 draws the climb (rng(2) - 3) before $7E2F draws the + // direction: the demo RNG is a sequence, so the order counts. + sim->hookScratch.i.dy[x] = (uint8_t)(stSimRng(sim, 2u) + 0xFDu); + y = (uint8_t)(stSimRng(sim, 4u) - 1u); // 0..3 direction + sim->hookScratch.i.dx[x] = I_DX(sim, y); // dx + sim->spr[x].col = I_START_COL(sim, y); // start col + sim->spr[x].msb = 0u; + sim->spr[x].row = 0xD1u; + sim->hookScratch.i.phase[x] = 0u; // cel phase + sim->hookScratch.i.state[x] = 1u; + sim->spr[x].enable = 1u; + sim->spr[x].ptr = 0x84u; + stSimSfx(sim, I_SFX_FIRE(sim)); + sim->spr[x].color = I_COLOR(sim, (uint8_t)(stSimRng(sim, 3u) - 1u)); + } else if (state == 1u) { + uint8_t ph = (uint8_t)(sim->hookScratch.i.phase[x] + 1u); + sim->hookScratch.i.phase[x] = ph; + if (ph == 5u) { + sim->hookScratch.i.phase[x] = 0u; + sim->spr[x].ptr = 0x85u; + sim->hookScratch.i.state[x] = 2u; + } else { + if (ph >= 3u) { + stSimSpriteAddX(sim, x, sim->hookScratch.i.dx[x]); + sim->spr[x].row = (uint8_t)(sim->spr[x].row + sim->hookScratch.i.dy[x]); + } + sim->spr[x].ptr = I_CEL_RISE(sim, ph); + } + } else { + uint8_t ph = (uint8_t)((sim->hookScratch.i.phase[x] + 1u) & 7u); + sim->hookScratch.i.phase[x] = ph; + sim->spr[x].ptr = I_CEL_BURST(sim, ph); + stSimSpriteAddX(sim, x, sim->hookScratch.i.dx[x]); + sim->spr[x].row = (uint8_t)(sim->spr[x].row + sim->hookScratch.i.dy[x]); + if (sim->spr[x].row < 0x25u) { + sim->spr[x].enable = 0u; + sim->hookScratch.i.state[x] = 0u; + } + } + } +} + + +static void hookIPrelude(StSimT *sim) { + int8_t x; + + for (x = 7; x >= 3; x--) { + sim->hookScratch.i.state[x] = 0u; // state + sim->spr[x].enable = 0u; + sim->spr[x].color = 0x02u; + } +} + + +// Level J "SHOOTING STARS" ($7D9D prelude0, $7DAF per-tick2): stars fall +// from the top and burst on the scenery (hookFall*). The hook reads the +// stars' own $D01F bits, so the prelude opts them into the collision +// test. +static void hookJPerTick(StSimT *sim) { + StHookJT *j = &sim->hookScratch.j; + + hookFallBgHits(sim, j->state, j->phase, J_SFX_BURST(sim)); + hookFallTick(sim, j->state, j->phase, &kFallJ, 0); +} + + +static void hookJPrelude0(StSimT *sim) { + uint8_t x; + + for (x = ST_HOOK_FIRST_SPRITE; x < ST_HW_SPRITES; x++) { + sim->hookScratch.j.state[x] = 0u; + sim->spr[x].color = ST_HK_J_COLOR; + } + sim->bgCollSprites = ST_HOOK_FALL_SPRITES; +} + + +// Level K "MAGNETS" per-tick ($7D9D, the hook after the sprite flush): +// the level is globally anti-gravity (its yGrav template is -7); the +// hook only animates the six magnet poles on row 4 ($04A0 + the +// columns at $7DBE) by flipping bit 0 of their glyphs every other tick. +static void hookKPerTick(StSimT *sim) { + uint8_t k; + + sim->hookScratch.k.parity ^= 1u; + if ((sim->hookScratch.k.parity & 1u) != 0u) { + return; + } + for (k = 0u; k < ST_HOOK_K_POLES; k++) { + uint8_t col = K_POLE_COL(sim, k); + stSimPutChar(sim, col, ST_HOOK_K_POLE_ROW, (uint8_t)(sim->screen[ST_CELL(ST_HOOK_K_POLE_ROW, col)] ^ 1u)); + } +} + + +// Level L "BLACK HOLE" ($7D9F prelude0, $7DBC per-tick2, $7D9B verdict): +// hardware sprite 3 spins through cels $80..$82 at ($AC, $82) and pulls +// the cab in -- each tick the gravity templates become the pull toward +// it, weaker with distance. +static void hookLPerTick(StSimT *sim) { + StSpriteT *hole = &sim->spr[ST_HOOK_L_HOLE]; + uint8_t dy; + uint8_t dx; + + hole->ptr++; + if (hole->ptr == ST_HK_L_CEL_END) { + hole->ptr = ST_HK_L_CEL_FIRST; + } + dy = (uint8_t)(ST_HOOK_L_HOLE_ROW - sim->spr[0].row); + sim->gravTemplateY = hookLPull(ST_HOOK_L_PULL_Y, dy); + dx = (uint8_t)(ST_HOOK_L_HOLE_X_HALF - (uint8_t)(stSimSpriteX(sim, 0u) >> 1)); + sim->gravTemplateX = hookLPull(ST_HOOK_L_PULL_X, dx); +} + + +static void hookLPrelude0(StSimT *sim) { + StSpriteT *hole = &sim->spr[ST_HOOK_L_HOLE]; + + hole->color = ST_HK_L_COLOR; + hole->msb = 0u; + hole->col = ST_HOOK_L_HOLE_COL; + hole->row = ST_HOOK_L_HOLE_ROW; + hole->ptr = ST_HK_L_CEL_FIRST; + hole->enable = 1u; +} + + +// $7DCB / $7E01 -- base less |dist|/8 ($7E42), signed by the direction. +static uint16_t hookLPull(uint8_t base, uint8_t dist) { + uint8_t mag = dist; + uint16_t pull; + + if ((mag & 0x80u) != 0u) { + mag = (uint8_t)(0u - mag); + } + pull = (uint8_t)(base - (uint8_t)(mag >> 3)); + if ((dist & 0x80u) != 0u) { + pull = (uint16_t)(0u - pull); + } + return pull; +} + + +// Level O per-tick ($7DBE, the hook after the sprite flush): the +// barrier glyph rolls and the rows scroll left (see ST_HOOK_O_GAPS). +static void hookOPerTick(StSimT *sim) { + stSimRotateGlyph(sim, ST_CHAR_ELECTROID); + sim->hookScratch.o.tick = (uint8_t)((sim->hookScratch.o.tick + 1u) & ST_HOOK_O_TICK_MASK); + if ((sim->hookScratch.o.tick & ST_HOOK_O_FAST_MASK) != 0u) { + return; + } + hookRowScrollLeft(sim, ST_HOOK_O_ROW_FAST_A); + hookRowScrollLeft(sim, ST_HOOK_O_ROW_FAST_B); + if (sim->hookScratch.o.tick != ST_HOOK_O_SLOW_TICK) { + return; + } + hookRowScrollLeft(sim, ST_HOOK_O_ROW_SLOW_A); + hookRowScrollLeft(sim, ST_HOOK_O_ROW_SLOW_B); +} + + +// Level O "ELECTROIDS" prelude ($7D9C): the gaps in the barriers. +static void hookOPrelude0(StSimT *sim) { + uint8_t k; + + for (k = 0u; k < ST_HOOK_O_GAPS; k++) { + uint8_t col = O_GAP_COL(sim, k); + uint8_t row = O_GAP_ROW(sim, k); + uint8_t c; + + for (c = 0u; c < ST_HOOK_O_GAP_CELLS; c++) { + stSimPutChar(sim, (uint8_t)(col + c), row, ST_CHAR_SPACE); + } + } +} + + +// Level P "BLIZZARD" ($7DA8 prelude0, $7D9C prelude1, $7DBA per-tick2): +// snow falls and melts on the scenery (hookFall*), and now and then the +// wind turns: the windsocks flip and the X gravity changes sign. +static void hookPFlipSock(StSimT *sim, uint16_t cell) { + stSimPutChar(sim, (uint8_t)(cell % ST_SCREEN_COLS), (uint8_t)(cell / ST_SCREEN_COLS), (uint8_t)(sim->screen[cell] ^ ST_HOOK_P_SOCK_FLIP)); +} + + +static void hookPPerTick(StSimT *sim) { + StHookPT *p = &sim->hookScratch.p; + + if (stSimRng(sim, ST_HOOK_P_WIND_ODDS) < ST_HOOK_P_WIND_HIT) { + stSimSfx(sim, P_SFX_WIND(sim)); + sim->gravTemplateX = (uint16_t)(0u - sim->gravTemplateX); + hookPFlipSock(sim, ST_CELL(7, 6)); // $051E + hookPFlipSock(sim, ST_CELL(9, 21)); // $057D + hookPFlipSock(sim, ST_CELL(15, 31)); // $0677 + } + hookFallBgHits(sim, p->state, p->phase, 0); + hookFallTick(sim, p->state, p->phase, &kFallP, &p->parity); +} + + +static void hookPPrelude0(StSimT *sim) { + uint8_t x; + + for (x = ST_HOOK_FIRST_SPRITE; x < ST_HW_SPRITES; x++) { + sim->hookScratch.p.state[x] = 0u; + sim->spr[x].color = ST_HK_P_COLOR; + } + sim->bgCollSprites = ST_HOOK_FALL_SPRITES; +} + + +// $7D6C -- before the sprite flush. Split in two: one switch over all +// sixteen levels left the 65816's single accumulator with more live +// values than greedy register allocation can place, and the build then +// falls back to the basic allocator for this whole file. +ST_NOINLINE static void hookPerTickHigh(StSimT *sim) { + switch (sim->hookLevel) { + case ST_LEVEL_P: + hookPPerTick(sim); + break; + case ST_LEVEL_Q: + hookQPerTick(sim); + break; + case ST_LEVEL_T: + hookTPerTick(sim); + break; + case ST_LEVEL_U: + hookUPerTick(sim); + break; + case ST_LEVEL_V: + hookVPerTick(sim); + break; + case ST_LEVEL_W: + hookWPerTick(sim); + break; + case ST_LEVEL_X: + hookXPerTick(sim); + break; + case ST_LEVEL_Y: + hookYPerTick(sim); + break; + default: + break; + } +} + + +ST_NOINLINE static void hookPerTickLow(StSimT *sim) { + switch (sim->hookLevel) { + case ST_LEVEL_E: + hookEPerTick(sim); + break; + case ST_LEVEL_F: + hookFPerTick(sim); + break; + case ST_LEVEL_G: + hookGPerTick(sim); + break; + case ST_LEVEL_H: + hookHPerTick(sim); + break; + case ST_LEVEL_I: + hookIPerTick(sim); + break; + case ST_LEVEL_J: + hookJPerTick(sim); + break; + case ST_LEVEL_L: + hookLPerTick(sim); + break; + default: + break; + } +} + + +// Level Q "INTERFERENCE" ($7DA1 prelude0, $7D9B prelude1, $7DFF +// per-tick2, $7E2E input): five interference sprites (hw 3..7) sit at +// fixed spots flickering through cels $80..$8B every other tick, every +// sprite multicolour; while the cab is airborne mid-screen (rows $5A.. +// $B3) the joystick is replaced by one random direction on 3 ticks in +// 10 (rng(10) >= 8), with a buzz. +static uint8_t hookQInput(StSimT *sim, uint8_t input) { + uint8_t scrambled; + + 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; + } + scrambled = Q_SCRAMBLE(sim, (uint8_t)(stSimRng(sim, 4u) - 1u)); + // $7E55: the static buzzes too, unless voice 2 is still sounding. + if (sim->sidRelTimer[ST_HOOK_BUZZ_VOICE] == 0u) { + uint8_t buzz[ST_SFX_PROGRAM_BYTES]; + memcpy(buzz, Q_SFX_BUZZ(sim), sizeof(buzz)); + buzz[0] = (uint8_t)(stSimRng(sim, ST_HOOK_Q_BUZZ_SPAN) + ST_HOOK_Q_BUZZ_SPAN); + buzz[1] = buzz[0]; + stSimSfx(sim, buzz); + } + return scrambled; +} + + +static void hookQPerTick(StSimT *sim) { + int8_t x; + + sim->hookScratch.q.parity = (uint8_t)((sim->hookScratch.q.parity + 1u) & 1u); + if (sim->hookScratch.q.parity != 0u) { + return; + } + for (x = 7; x >= 3; x--) { + sim->hookScratch.q.phase[x]++; + if (sim->hookScratch.q.phase[x] == 0x0Cu) { + sim->hookScratch.q.phase[x] = 0u; + } + sim->spr[x].ptr = Q_CEL(sim, sim->hookScratch.q.phase[x]); + } +} + + +static void hookQPrelude(StSimT *sim) { + int8_t x; + + for (x = 7; x >= 3; x--) { + sim->hookScratch.q.phase[x] = Q_INIT_PHASE(sim, x); + sim->spr[x].ptr = Q_CEL(sim, sim->hookScratch.q.phase[x]); + sim->spr[x].color = 0x06u; + sim->spr[x].col = Q_SPR_COL(sim, x); + sim->spr[x].msb = Q_SPR_MSB(sim, x); + sim->spr[x].row = Q_SPR_ROW(sim, x); + sim->spr[x].enable = 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 +// twelve gate cells ($0439/$0461/$0489, $04A1..$04A4, $0709..$070D); +// 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[] = { 0x3Eu, 0x66u, 0x8Eu }; + static const uint16_t kGate[] = { 0x39u, 0x61u, 0x89u, 0xA1u, 0xA2u, 0xA3u, 0xA4u, 0x309u, 0x30Au, 0x30Bu, 0x30Cu, 0x30Du }; + uint8_t k; + uint8_t alt = (uint8_t)(ch ^ ST_HOOK_R_GATE_FLIP); + + for (k = 0u; k < (uint8_t)(sizeof(kBar) / sizeof(kBar[0])); k++) { + stSimPutChar(sim, (uint8_t)(kBar[k] % ST_SCREEN_COLS), (uint8_t)(kBar[k] / ST_SCREEN_COLS), ch); + } + for (k = 0u; k < (uint8_t)(sizeof(kGate) / sizeof(kGate[0])); k++) { + stSimPutChar(sim, (uint8_t)(kGate[k] % ST_SCREEN_COLS), (uint8_t)(kGate[k] / ST_SCREEN_COLS), alt); + } +} + + +// Level R per-tick ($7DD2, the hook after the sprite flush): 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]); +} + + +// A row's cells 1..38 rotate one cell, screen and colour together, the +// end cell wrapping round; the border columns 0 and 39 stay. Level O's +// barriers ($7DD1..$7E62) and V's left bands ($7E0E) go left, V's other +// bands ($7E35) right. +static void hookRowScrollLeft(StSimT *sim, uint8_t row) { + uint16_t base = ST_CELL(row, 0u); + uint8_t chSave = sim->screen[base + ST_HOOK_ROW_FIRST_COL]; + uint8_t colSave = sim->color[base + ST_HOOK_ROW_FIRST_COL]; + uint8_t col; + + for (col = ST_HOOK_ROW_FIRST_COL; col < ST_HOOK_ROW_LAST_COL; col++) { + stSimPutChar(sim, col, row, sim->screen[base + col + 1u]); + stSimPutColor(sim, col, row, sim->color[base + col + 1u]); + } + stSimPutChar(sim, ST_HOOK_ROW_LAST_COL, row, chSave); + stSimPutColor(sim, ST_HOOK_ROW_LAST_COL, row, colSave); +} + + +static void hookRowScrollRight(StSimT *sim, uint8_t row) { + uint16_t base = ST_CELL(row, 0u); + uint8_t chSave = sim->screen[base + ST_HOOK_ROW_LAST_COL]; + uint8_t colSave = sim->color[base + ST_HOOK_ROW_LAST_COL]; + uint8_t col; + + for (col = ST_HOOK_ROW_LAST_COL; col > ST_HOOK_ROW_FIRST_COL; col--) { + stSimPutChar(sim, col, row, sim->screen[base + col - 1u]); + stSimPutColor(sim, col, row, sim->color[base + col - 1u]); + } + stSimPutChar(sim, ST_HOOK_ROW_FIRST_COL, row, chSave); + stSimPutColor(sim, ST_HOOK_ROW_FIRST_COL, row, colSave); +} + + +// Level S "THE SWITCH" ($7D9D input filter): the stick's direction bits +// come back swapped through the level's table; FIRE passes. +static uint8_t hookSInput(StSimT *sim, uint8_t input) { + return (uint8_t)((input & ST_HOOK_S_FIRE) | S_DIR_MAP(sim, input & ST_HOOK_S_DIRS)); +} + // Level T "FAST BREAK" $7D9D -- the barrier on row 3: `ch` into the // centre opening (cols 18..21), its complement into the side gaps. @@ -494,7 +1336,7 @@ static void hookTPerTick(StSimT *sim) { return; } sim->velY = (int16_t)(-sim->velY); - stAudioSfx(T_SFX_BOUNCE(sim)); + stSimSfx(sim, T_SFX_BOUNCE(sim)); return; } if (sim->spr[0].row >= 0x3Fu) { @@ -504,7 +1346,7 @@ static void hookTPerTick(StSimT *sim) { sim->velX = 0; hookTGate(sim, 0x75u); sim->hookScratch.t.gateShut++; - stAudioSfx(T_SFX_SHUT(sim)); + stSimSfx(sim, T_SFX_SHUT(sim)); } @@ -512,7 +1354,156 @@ static void hookTPerTick(StSimT *sim) { static void hookTPrelude1(StSimT *sim) { hookTGate(sim, ST_CHAR_SPACE); sim->hookScratch.t.gateShut = 0u; - stAudioSfx(T_SFX_OPEN(sim)); + stSimSfx(sim, T_SFX_OPEN(sim)); +} + + +// Level U "REBOUND" ($7D9B prelude0, $7DAD per-tick2). Five orbs drift +// in from the four edges on their own cel walk; touching one bounces the +// cab by negating its velocity. The orb cels are the level's own sprite +// blocks $80..$82, and every table below is read from the shipped hook +// blob at the address the original indexes. +// +// $7DDD -- a cab/orb contact bounces the cab, then holds off for twelve +// ticks. Which axes flip is a 1-in-3 roll: 1 negates both, 2 negates +// neither and 3 negates Y alone -- the original has no X-only case. +static void hookUBounce(StSimT *sim) { + uint8_t coll; + uint8_t hits = 0u; + uint8_t b; + uint8_t pick; + + if (sim->hookScratch.u.cooldown != 0u) { + sim->hookScratch.u.cooldown--; + return; + } + if ((sim->spriteSpriteColl & 1u) == 0u) { + return; + } + // $7DF8: count the orb sprites in the latch (bits 7..3). + coll = sim->spriteSpriteColl; + for (b = (uint8_t)(ST_HW_SPRITES - 1u); b >= ST_HOOK_FIRST_SPRITE; b--) { + if ((coll & 0x80u) != 0u) { + hits++; + } + coll = (uint8_t)(coll << 1); + } + if (hits == 0u) { + return; + } + sim->hookScratch.u.cooldown = ST_HOOK_U_COOLDOWN; + stSimSfx(sim, U_SFX_BOUNCE(sim)); + pick = stSimRng(sim, 3u); + if (pick == 1u) { + sim->velX = (int16_t)(-sim->velX); + } + if (pick == 2u) { + return; + } + sim->velY = (int16_t)(-sim->velY); +} + + +// $7E60 -- the orbs themselves: an idle one appears on a 2-in-30 roll +// from one of the four edges (0 up from the floor, 1 down from the +// ceiling, 2 in from the left, 3 in from the right), an active one +// drifts by its direction's step, walks its cel and expires. +static void hookUOrbs(StSimT *sim) { + uint8_t x; + + for (x = (uint8_t)(ST_HW_SPRITES - 1u); x >= ST_HOOK_FIRST_SPRITE; x--) { + uint8_t state = sim->hookScratch.u.state[x]; + uint8_t dir; + + if (state == 0u) { + // $7E84: nothing here yet -- maybe start one. + if (stSimRng(sim, 0x1Eu) >= 3u) { + continue; + } + dir = (uint8_t)(stSimRng(sim, 4u) - 1u); + sim->hookScratch.u.dir[x] = dir; + sim->hookScratch.u.life[x] = U_LIFE(sim, dir); + if ((dir & 2u) == 0u) { + // $7EA8: fixed row, random column. + uint16_t col = (uint16_t)(((uint16_t)stSimRng(sim, 0x90u) + 0x11u) << 1); + sim->spr[x].row = U_ENTRY(sim, dir); + sim->spr[x].col = (uint8_t)col; + sim->spr[x].msb = (uint8_t)(col >> 8); + } else { + // $7ECF: fixed column, random row. + sim->spr[x].col = U_ENTRY(sim, dir); + sim->spr[x].msb = U_ENTRY_MSB(sim, dir); + sim->spr[x].row = (uint8_t)((uint8_t)(stSimRng(sim, 0x4Fu) + 0x1Du) << 1); + } + sim->hookScratch.u.state[x] = 1u; + sim->spr[x].enable = 1u; + sim->spr[x].ptr = ST_HK_FALL_CEL; + sim->hookScratch.u.celPhase[x] = 0u; + continue; + } + if (state != 1u) { + sim->hookScratch.u.state[x] = 0u; + continue; + } + // $7F02: drift until the lifetime runs out. + sim->hookScratch.u.life[x]--; + if (sim->hookScratch.u.life[x] == 0u) { + sim->spr[x].enable = 0u; + sim->hookScratch.u.state[x] = 0u; + continue; + } + dir = sim->hookScratch.u.dir[x]; + stSimSpriteAddX(sim, x, U_DX(sim, dir)); + sim->spr[x].row = (uint8_t)(sim->spr[x].row + U_DY(sim, dir)); + sim->hookScratch.u.celPhase[x] = (uint8_t)((sim->hookScratch.u.celPhase[x] + 1u) & 3u); + sim->spr[x].ptr = U_CEL(sim, sim->hookScratch.u.celPhase[x]); + } +} + + +static void hookUPerTick(StSimT *sim) { + // $7DAD: the original opens with a ceiling reflect that only runs + // while a crash is in progress and only above row $11. The wreck + // sequence moves sprite 0 downward to $DA and never back up, so this + // cannot fire on the real machine either; it is kept as shipped. + if (sim->collisionPhase != 0u && sim->spr[0].row < 0x11u) { + sim->velY = (int16_t)(-sim->velY); + sim->posYrow = (uint8_t)(sim->posYrow + 2u); + } + hookUBounce(sim); + hookUOrbs(sim); +} + + +// $7D9B -- clear the orb states and colour the five hazard sprites. +static void hookUPrelude0(StSimT *sim) { + uint8_t x; + + for (x = (uint8_t)(ST_HW_SPRITES - 1u); x >= ST_HOOK_FIRST_SPRITE; x--) { + sim->hookScratch.u.state[x] = 0u; + sim->spr[x].color = 0x07u; + } +} + + +// Level V "SHIFT-O-RAMA" per-tick ($7DA6): every sixteenth tick a +// shift sounds and rows 6..17 each move a cell, in bands of two by bit +// 1 of the row: set to the left ($7E0E), clear to the right ($7E35). +static void hookVPerTick(StSimT *sim) { + uint8_t row; + + sim->hookScratch.v.tick++; + if ((sim->hookScratch.v.tick & ST_HOOK_V_RATE_MASK) != 0u) { + return; + } + stSimSfx(sim, V_SFX_SHIFT(sim)); + for (row = ST_HOOK_V_FIRST_ROW; row < ST_HOOK_V_END_ROW; row++) { + if ((row & ST_HOOK_V_LEFT_BAND) != 0u) { + hookRowScrollLeft(sim, row); + } else { + hookRowScrollRight(sim, row); + } + } } @@ -581,7 +1572,7 @@ static void hookWPerTick(StSimT *sim) { for (;;) { uint8_t c; if (sim->hookScratch.w.started != 0u) { - stAudioSfx(W_SFX_HUM(sim)); + stSimSfx(sim, W_SFX_HUM(sim)); sim->hookScratch.w.started = 0u; } c = (uint8_t)(stSimRng(sim, 0x6Eu) + 6u); @@ -769,6 +1760,224 @@ static void hookXShiftRow(StSimT *sim, uint16_t rowCell, uint8_t from, uint8_t t } +// Level Y "MYSTERY SCREEN" ($7D9B prelude0, $7E02 prelude1, $7E21 +// per-tick2). A saucer (sprite 3) waits off the left edge until it is +// launched (rng($C8) < 3) and crosses two pixels a tick to X $158 under a +// hum that restarts whenever voice 2 falls silent. A walker (sprite 4) +// patrols row $82 between X $0C8 and $13C a pixel every fourth tick, +// showing its other cel and sounding once every 128 ticks; whenever the +// cab is above row $82 and within six half-pixels of it, it fires after +// $1E ticks: a shot (sprite 5) rising from row $7B two rows a tick to row +// $2B, where it bursts. A light blinks along rows 19..21, a cell every +// four ticks, and a 3x3 block creature on row 9 steps between columns 2 +// and $0F every fifteen ticks. ($7EC3..$7EC8 holds the two directions' +// turning columns, steps and the direction each turns into, for the +// walker and the block alike.) +static void hookYBlockDraw(StSimT *sim, uint8_t frame) { + const StHookYT *y = &sim->hookScratch.y; + uint8_t i; + + for (i = 0u; i < ST_HOOK_Y_BLOCK_CELLS; i++) { + uint8_t off = Y_BLOCK_OFFSET(sim, i); + uint16_t cell = (uint16_t)(ST_CELL(ST_HOOK_Y_BLOCK_ROW, y->blockCol) + off); + uint8_t col = (uint8_t)(cell % ST_SCREEN_COLS); + uint8_t row = (uint8_t)(cell / ST_SCREEN_COLS); + + stSimPutColor(sim, col, row, Y_BLOCK_COLOR(sim, i)); + stSimPutChar(sim, col, row, Y_BLOCK_CHAR(sim, (uint8_t)(frame * ST_HOOK_Y_BLOCK_CELLS + i))); + } +} + + +// $8009 / $8013 -- a sprite's 9-bit X halved into a byte (LSR the msb, +// ROR the column), so the walker can aim with one subtraction. +static uint8_t hookYHalfX(const StSpriteT *spr) { + return (uint8_t)((((uint16_t)(spr->msb & 1u) << 8) | spr->col) >> 1); +} + + +// $7E10 -- the saucer's hum starts over. +static void hookYHum(StSimT *sim) { + stSimSfx(sim, Y_SFX_HUM(sim)); + sim->hookScratch.y.humStep = 0u; +} + + +// $7ECD..$7F63 -- the blinking light and, every fifteenth tick, a step +// of the block creature. +static void hookYLightAndBlock(StSimT *sim) { + StHookYT *y = &sim->hookScratch.y; + uint8_t col; + uint8_t row; + + y->lightTick++; + if ((y->lightTick & ST_HOOK_Y_LIGHT_STEP_MASK) == 0u) { + y->lightCell++; + if (y->lightCell == ST_HOOK_Y_LIGHT_CELLS) { + y->lightCell = 0u; + } + } + col = (uint8_t)(y->lightCell % ST_SCREEN_COLS); + row = (uint8_t)(ST_HOOK_Y_LIGHT_ROW + y->lightCell / ST_SCREEN_COLS); + if ((y->lightTick & 1u) != 0u) { + stSimPutChar(sim, col, row, y->lightSaveChar); + stSimPutColor(sim, col, row, y->lightSaveColor); + } else { + y->lightSaveChar = sim->screen[ST_CELL(row, col)]; + y->lightSaveColor = sim->color[ST_CELL(row, col)]; + stSimPutColor(sim, col, row, ST_HOOK_Y_LIGHT_COLOR); + stSimPutChar(sim, col, row, ST_CHAR_BLANK); + } + y->blockTimer--; + if (y->blockTimer != 0u) { + return; + } + y->blockTimer = ST_HOOK_Y_BLOCK_TICKS; + if (Y_BLOCK_TURN_COL(sim, y->blockDir) == y->blockCol) { + y->blockDir = Y_TURN_DIR(sim, y->blockDir); + } + hookYBlockDraw(sim, ST_HOOK_Y_BLOCK_BLANK); + y->blockCol = (uint8_t)(y->blockCol + Y_STEP(sim, y->blockDir)); + y->blockFrame++; + hookYBlockDraw(sim, (uint8_t)(y->blockDir * ST_HOOK_Y_BLOCK_STEP_FRAMES + (y->blockFrame & 1u))); + stSimSfx(sim, Y_SFX_STEP(sim)); +} + + +static void hookYPerTick(StSimT *sim) { + StHookYT *y = &sim->hookScratch.y; + StSpriteT *saucer = &sim->spr[ST_HK_Y_SAUCER]; + StSpriteT *walker = &sim->spr[ST_HK_Y_WALKER]; + bool moving = (saucer->msb | saucer->col) != 0u; + + if (!moving && stSimRng(sim, ST_HOOK_Y_LAUNCH_ODDS) < ST_HOOK_Y_LAUNCH_HIT) { + hookYHum(sim); + moving = true; + } + if (moving) { + uint8_t pitch; + stSimSpriteAddX(sim, ST_HK_Y_SAUCER, ST_HOOK_Y_SAUCER_STEP); + y->humStep++; + pitch = Y_HUM_PITCH(sim, y->humStep & ST_HOOK_Y_HUM_STEPS_MASK); + stAudioVoice2(pitch, pitch, Y_SFX_HUM(sim)[ST_HOOK_SFX_CTRL_AT]); + if (sim->sidRelTimer[ST_HOOK_BUZZ_VOICE] == 0u) { + hookYHum(sim); + } + if (saucer->msb != 0u && saucer->col >= ST_HOOK_Y_SAUCER_END_COL) { + saucer->msb = 0u; + saucer->col = 0u; + stSimSfx(sim, Y_SFX_PARK(sim)); + } + } + y->walkerParity++; + if ((y->walkerParity & 1u) != 0u) { + y->walkerCount++; + if ((y->walkerCount & ST_HOOK_Y_WALK_CYCLE_MASK) == 0u) { + walker->ptr = ST_HK_Y_CEL_WALK_ALT; + stSimSfx(sim, Y_SFX_WALK(sim)); + } else if ((y->walkerCount & 1u) == 0u) { + walker->ptr = ST_HK_Y_CEL_WALK; + } + if ((y->walkerCount & 1u) == 0u) { + if (Y_WALK_TURN_COL(sim, y->walkerDir) == walker->col) { + y->walkerDir = Y_TURN_DIR(sim, y->walkerDir); + } + stSimSpriteAddX(sim, ST_HK_Y_WALKER, Y_STEP(sim, y->walkerDir)); + } + } + hookYLightAndBlock(sim); + hookYShot(sim); +} + + +static void hookYPrelude0(StSimT *sim) { + StHookYT *y = &sim->hookScratch.y; + StSpriteT *saucer = &sim->spr[ST_HK_Y_SAUCER]; + StSpriteT *walker = &sim->spr[ST_HK_Y_WALKER]; + StSpriteT *shot = &sim->spr[ST_HK_Y_SHOT]; + + saucer->col = 0u; + saucer->msb = 0u; + y->shotLive = 0u; + y->shotDelay = 0u; + saucer->color = ST_HK_Y_COLOR_SAUCER; + walker->color = ST_HK_Y_COLOR_WALKER; + shot->color = ST_HK_Y_COLOR_WALKER; + saucer->row = ST_HOOK_Y_SAUCER_ROW; + saucer->enable = 1u; + walker->enable = 1u; + saucer->ptr = ST_HK_Y_CEL_SAUCER; + walker->ptr = ST_HK_Y_CEL_WALK; + walker->row = ST_HOOK_Y_WALKER_ROW; + walker->msb = 0u; + y->walkerDir = 0u; + y->walkerCount = 0u; + walker->col = ST_HOOK_Y_WALKER_COL; + y->blockTimer = 1u; + y->blockDir = 1u; + y->blockCol = ST_HOOK_Y_BLOCK_COL; + hookYBlockDraw(sim, ST_HOOK_Y_BLOCK_START); + y->lightCell = 0u; + y->lightTick = ST_HOOK_Y_LIGHT_START; +} + + +// $7E02 -- every sprite up to the saucer is multicolour, and a saucer +// already in flight hums again. +static void hookYPrelude1(StSimT *sim) { + sim->multiColorMask = ST_HK_Y_MULTICOLOR; + if ((sim->spr[ST_HK_Y_SAUCER].col | sim->spr[ST_HK_Y_SAUCER].msb) != 0u) { + hookYHum(sim); + } +} + + +// $7FF8..$808D -- the walker fires when the cab is above it and close; +// the shot rises, then bursts and is gone. +static void hookYShot(StSimT *sim) { + StHookYT *y = &sim->hookScratch.y; + StSpriteT *walker = &sim->spr[ST_HK_Y_WALKER]; + StSpriteT *shot = &sim->spr[ST_HK_Y_SHOT]; + + if (sim->spr[0].row < ST_HOOK_Y_WALKER_ROW && y->shotLive == 0u && y->shotDelay == 0u) { + uint8_t gap = (uint8_t)(hookYHalfX(walker) - hookYHalfX(&sim->spr[0])); + + if ((int8_t)gap < 0) { + gap = (uint8_t)(0u - gap); + } + if (gap < ST_HOOK_Y_AIM_GAP) { + y->shotDelay = ST_HOOK_Y_SHOT_DELAY; + shot->row = ST_HOOK_Y_SHOT_ROW; + shot->ptr = ST_HK_Y_CEL_SHOT; + } + } + if (y->shotDelay != 0u) { + y->shotDelay--; + if (y->shotDelay == 0u) { + y->shotLive = 1u; + shot->enable = 1u; + shot->msb = walker->msb; + shot->col = walker->col; + stSimSfx(sim, Y_SFX_SHOT(sim)); + } + } + if (y->shotLive == 0u) { + return; + } + if (shot->row != ST_HOOK_Y_SHOT_TOP_ROW) { + shot->row = (uint8_t)(shot->row - ST_HOOK_Y_SHOT_STEP); + return; + } + shot->ptr++; + if (shot->ptr == ST_HK_Y_CEL_SHOT_END) { + // $807B falls into the rise at $8083 with A = 0. + y->shotLive = 0u; + shot->enable = 0u; + shot->row = (uint8_t)(0u - ST_HOOK_Y_SHOT_STEP); + } +} + + // Sprites 3..7 share one colour in the puzzle level. static void setSpriteColors(StSimT *sim, uint8_t color) { uint8_t k; @@ -779,748 +1988,160 @@ static void setSpriteColors(StSimT *sim, uint8_t 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). -// 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; - - sim->hookScratch.k.parity ^= 1u; - if ((sim->hookScratch.k.parity & 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) { - stSimRotateGlyph(sim, ST_CHAR_ELECTROID); // $7E6E bitScroll of the barrier glyph - sim->hookScratch.o.tick = (uint8_t)((sim->hookScratch.o.tick + 1u) & 7u); - if ((sim->hookScratch.o.tick & 3u) != 0u) { - return; - } - hookOScrollRow(sim, 0xC9u); // $04C9 row 5 col 1 - hookOScrollRow(sim, 0x209u); // $0609 row 12 col 1 - if (sim->hookScratch.o.tick != 4u) { - return; - } - hookOScrollRow(sim, 0x169u); // $0569 row 9 col 1 - hookOScrollRow(sim, 0x2A9u); // $06A9 row 17 col 1 -} - - -// 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, the 12 -// rows 6..17 each scroll one column, alternating direction in bands of -// two (bit 1 of the row index): bands with the bit set go left, the -// others 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--) { - sim->hookScratch.q.phase[x] = Q_INIT_PHASE(sim, x); - sim->spr[x].ptr = Q_CEL(sim, sim->hookScratch.q.phase[x]); - sim->spr[x].color = 0x06u; - sim->spr[x].col = Q_SPR_COL(sim, x); - sim->spr[x].msb = Q_SPR_MSB(sim, x); - sim->spr[x].row = Q_SPR_ROW(sim, x); - sim->spr[x].enable = 1u; - } -} - - -static void hookQPerTick(StSimT *sim) { - int8_t x; - - for (x = 7; x >= 3; x--) { - sim->hookScratch.q.phase[x]++; - if (sim->hookScratch.q.phase[x] == 0x0Cu) { - sim->hookScratch.q.phase[x] = 0u; - } - sim->spr[x].ptr = Q_CEL(sim, sim->hookScratch.q.phase[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 Q_SCRAMBLE(sim, (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--) { - sim->hookScratch.i.state[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 = sim->hookScratch.i.state[x]; - if (state == 0u) { - uint8_t y; - if (stSimRng(sim, 0x75u) >= 3u) { - continue; - } - y = (uint8_t)(stSimRng(sim, 4u) - 1u); // 0..3 direction - sim->hookScratch.i.dx[x] = I_DX(sim, y); // dx - sim->spr[x].col = I_START_COL(sim, y); // start col - sim->spr[x].msb = 0u; - sim->spr[x].row = 0xD1u; - sim->hookScratch.i.dy[x] = (uint8_t)(stSimRng(sim, 2u) + 0xFDu); // dy = rng(2)-3 - sim->hookScratch.i.phase[x] = 0u; // cel phase - sim->hookScratch.i.state[x] = 1u; - sim->spr[x].enable = 1u; - sim->spr[x].ptr = 0x84u; - stAudioSfx(I_SFX_FIRE(sim)); - sim->spr[x].color = I_COLOR(sim, (uint8_t)(stSimRng(sim, 3u) - 1u)); - } else if (state == 1u) { - uint8_t ph = (uint8_t)(sim->hookScratch.i.phase[x] + 1u); - sim->hookScratch.i.phase[x] = ph; - if (ph == 5u) { - sim->hookScratch.i.phase[x] = 0u; - sim->spr[x].ptr = 0x85u; - sim->hookScratch.i.state[x] = 2u; - } else { - if (ph >= 3u) { - stSimSpriteAddX(sim, x, sim->hookScratch.i.dx[x]); - sim->spr[x].row = (uint8_t)(sim->spr[x].row + sim->hookScratch.i.dy[x]); - } - sim->spr[x].ptr = I_CEL_RISE(sim, ph); - } - } else { - uint8_t ph = (uint8_t)((sim->hookScratch.i.phase[x] + 1u) & 7u); - sim->hookScratch.i.phase[x] = ph; - sim->spr[x].ptr = I_CEL_BURST(sim, ph); - stSimSpriteAddX(sim, x, sim->hookScratch.i.dx[x]); - sim->spr[x].row = (uint8_t)(sim->spr[x].row + sim->hookScratch.i.dy[x]); - if (sim->spr[x].row < 0x25u) { - sim->spr[x].enable = 0u; - sim->hookScratch.i.state[x] = 0u; - } - } - } -} - - -// Level G "TELEPORTS" ($7DA0 prelude0, $7D9A prelude1, $7E01 per-tick2, -// $7DF2 verdict): five orbs on sprites 3..7 sit where the blob puts them -// and cycle their cel every fourth tick. The cab touching one starts a -// hop: it vanishes for $14 ticks with the stick dead and a falling tone, -// then reappears on a landing spot (hookGHop). Touching an orb is never -// a kill -- the verdict is 0 unless the scenery was hit, and a scenery -// hit also puts the cab sprite back. -static void hookGPrelude0(StSimT *sim) { - uint8_t x; - - for (x = ST_HOOK_FIRST_SPRITE; x < ST_HW_SPRITES; x++) { - sim->spr[x].color = ST_HK_G_ORB_COLOR; - sim->spr[x].ptr = HKT(sim, ST_HK_G_CEL); - sim->spr[x].col = G_ORB_COL(sim, x); - sim->spr[x].msb = G_ORB_MSB(sim, x); - sim->spr[x].row = G_ORB_ROW(sim, x); - sim->spr[x].enable = 1u; - } - sim->hookScratch.g.celPhase = 0u; - sim->hookScratch.g.hop = 0u; -} - - -// $7E4A -- land: rng(2) odd with a fare aboard sends the cab to its -// destination pad's spot (UP and beyond = spot 5); otherwise a random -// spot, stepped down if it repeats the last one (wrapping 1 -> 5). -static void hookGHop(StSimT *sim) { - StHookGT *g = &sim->hookScratch.g; - uint8_t spot; - uint8_t i; - - if ((stSimRng(sim, 2u) & 1u) != 0u && sim->activeSpriteIdx != 0u) { - spot = sim->activeSpriteIdx; - if (spot >= ST_HOOK_G_PAD_SPOTS) { - spot = ST_HOOK_G_SPOTS; - } - } else { - spot = stSimRng(sim, ST_HOOK_G_SPOTS); - if (spot == g->lastSpot) { - spot--; - if (spot == 0u) { - spot = ST_HOOK_G_SPOTS; - } - } - } - g->lastSpot = spot; - i = (uint8_t)(spot - 1u); - sim->spr[0].col = G_SPOT_COL(sim, i); - sim->posXcol = sim->spr[0].col; - sim->spr[0].msb = G_SPOT_MSB(sim, i); - sim->posXmsb = sim->spr[0].msb; - sim->spr[0].row = G_SPOT_ROW(sim, i); - sim->posYrow = sim->spr[0].row; - sim->velX = 0; - sim->velY = 0; -} - - -static void hookGPerTick(StSimT *sim) { - StHookGT *g = &sim->hookScratch.g; - uint8_t x; - - g->tick++; - if ((g->tick & 3u) == 0u) { - uint8_t cel; - g->celPhase++; - if (g->celPhase == ST_HK_G_CEL_PHASES) { - g->celPhase = 0u; - } - cel = G_CEL(sim, g->celPhase); - for (x = ST_HOOK_FIRST_SPRITE; x < ST_HW_SPRITES; x++) { - sim->spr[x].ptr = cel; - } - } - if (g->hop != 0u) { - // $7ECB: the tone falls, the stick is dead, and at zero the cab - // is back and lands. - g->sweep = (uint8_t)(g->sweep - ST_HOOK_G_SWEEP_STEP); - stAudioVoice1Freq(g->sweep); - sim->dirMask = 0u; - g->hop--; - if (g->hop == 0u) { - sim->spr[0].enable = 1u; - hookGHop(sim); - } - return; - } - if ((sim->spriteSpriteColl & ST_HOOK_FALL_SPRITES) == 0u) { - return; - } - // $7E3D: an orb took the cab. - sim->spr[0].enable = 0u; - g->hop = ST_HOOK_G_HOP_TICKS; - stAudioSfx(G_SFX_HOP(sim)); - g->sweep = ST_HOOK_G_SWEEP_START; - sim->velX = 0; - sim->velY = 0; -} - - -// Level U "REBOUND" ($7D9B prelude0, $7DAD per-tick2). Five orbs drift -// in from the four edges on their own cel walk; touching one bounces the -// cab by negating its velocity. The orb cels are the level's own sprite -// blocks $80..$82, and every table below is read from the shipped hook -// blob at the address the original indexes. -// -// $7D9B -- clear the orb states and colour the five hazard sprites. -static void hookUPrelude0(StSimT *sim) { - uint8_t x; - - for (x = (uint8_t)(ST_HW_SPRITES - 1u); x >= ST_HOOK_FIRST_SPRITE; x--) { - sim->hookScratch.u.state[x] = 0u; - sim->spr[x].color = 0x07u; - } -} - - -// $7DDD -- a cab/orb contact bounces the cab, then holds off for twelve -// ticks. Which axes flip is a 1-in-3 roll: 1 negates both, 2 negates -// neither and 3 negates Y alone -- the original has no X-only case. -static void hookUBounce(StSimT *sim) { - uint8_t coll; - uint8_t hits = 0u; - uint8_t b; - uint8_t pick; - - if (sim->hookScratch.u.cooldown != 0u) { - sim->hookScratch.u.cooldown--; - return; - } - if ((sim->spriteSpriteColl & 1u) == 0u) { - return; - } - // $7DF8: count the orb sprites in the latch (bits 7..3). - coll = sim->spriteSpriteColl; - for (b = (uint8_t)(ST_HW_SPRITES - 1u); b >= ST_HOOK_FIRST_SPRITE; b--) { - if ((coll & 0x80u) != 0u) { - hits++; - } - coll = (uint8_t)(coll << 1); - } - if (hits == 0u) { - return; - } - sim->hookScratch.u.cooldown = ST_HOOK_U_COOLDOWN; - stAudioSfx(U_SFX_BOUNCE(sim)); - pick = stSimRng(sim, 3u); - if (pick == 1u) { - sim->velX = (int16_t)(-sim->velX); - } - if (pick == 2u) { - return; - } - sim->velY = (int16_t)(-sim->velY); -} - - -// $7E60 -- the orbs themselves: an idle one appears on a 2-in-30 roll -// from one of the four edges (0 up from the floor, 1 down from the -// ceiling, 2 in from the left, 3 in from the right), an active one -// drifts by its direction's step, walks its cel and expires. -static void hookUOrbs(StSimT *sim) { - uint8_t x; - - for (x = (uint8_t)(ST_HW_SPRITES - 1u); x >= ST_HOOK_FIRST_SPRITE; x--) { - uint8_t state = sim->hookScratch.u.state[x]; - uint8_t dir; - - if (state == 0u) { - // $7E84: nothing here yet -- maybe start one. - if (stSimRng(sim, 0x1Eu) >= 3u) { - continue; - } - dir = (uint8_t)(stSimRng(sim, 4u) - 1u); - sim->hookScratch.u.dir[x] = dir; - sim->hookScratch.u.life[x] = U_LIFE(sim, dir); - if ((dir & 2u) == 0u) { - // $7EA8: fixed row, random column. - uint16_t col = (uint16_t)(((uint16_t)stSimRng(sim, 0x90u) + 0x11u) << 1); - sim->spr[x].row = U_ENTRY(sim, dir); - sim->spr[x].col = (uint8_t)col; - sim->spr[x].msb = (uint8_t)(col >> 8); - } else { - // $7ECF: fixed column, random row. - sim->spr[x].col = U_ENTRY(sim, dir); - sim->spr[x].msb = U_ENTRY_MSB(sim, dir); - sim->spr[x].row = (uint8_t)((uint8_t)(stSimRng(sim, 0x4Fu) + 0x1Du) << 1); - } - sim->hookScratch.u.state[x] = 1u; - sim->spr[x].enable = 1u; - sim->spr[x].ptr = ST_HK_FALL_CEL; - sim->hookScratch.u.celPhase[x] = 0u; - continue; - } - if (state != 1u) { - sim->hookScratch.u.state[x] = 0u; - continue; - } - // $7F02: drift until the lifetime runs out. - sim->hookScratch.u.life[x]--; - if (sim->hookScratch.u.life[x] == 0u) { - sim->spr[x].enable = 0u; - sim->hookScratch.u.state[x] = 0u; - continue; - } - dir = sim->hookScratch.u.dir[x]; - stSimSpriteAddX(sim, x, U_DX(sim, dir)); - sim->spr[x].row = (uint8_t)(sim->spr[x].row + U_DY(sim, dir)); - sim->hookScratch.u.celPhase[x] = (uint8_t)((sim->hookScratch.u.celPhase[x] + 1u) & 3u); - sim->spr[x].ptr = U_CEL(sim, sim->hookScratch.u.celPhase[x]); - } -} - - -static void hookUPerTick(StSimT *sim) { - // $7DAD: the original opens with a ceiling reflect that only runs - // while a crash is in progress and only above row $11. The wreck - // sequence moves sprite 0 downward to $DA and never back up, so this - // cannot fire on the real machine either; it is kept as shipped. - if (sim->collisionPhase != 0u && sim->spr[0].row < 0x11u) { - sim->velY = (int16_t)(-sim->velY); - sim->posYrow = (uint8_t)(sim->posYrow + 2u); - } - hookUBounce(sim); - hookUOrbs(sim); -} - - - - // --------------------------------------------------------------------------- // Dispatch // --------------------------------------------------------------------------- + +// $7D75 -- 0 means sprite contact (passenger, level sprites) is safe. +uint8_t stHookHitVerdict(StSimT *sim) { + switch (sim->hookLevel) { + case ST_LEVEL_G: + // $7DF2: only the scenery kills; that also unhides the cab. + if ((sim->spriteBgColl & 1u) != 0u) { + sim->spr[0].enable = 1u; + return 1u; + } + return 0u; + case ST_LEVEL_H: + case ST_LEVEL_U: + // $7E57 / $7DDA: LDA #$00 -- touching a hook sprite is safe. + return 0u; + case ST_LEVEL_K: + case ST_LEVEL_L: + return ST_HOOK_KILL_NO_WRECK; + default: + return 1u; + } +} + + +// $7D72 -- the input filter. +uint8_t stHookInput(StSimT *sim, uint8_t input) { + switch (sim->hookLevel) { + case ST_LEVEL_Q: + return hookQInput(sim, input); + case ST_LEVEL_S: + return hookSInput(sim, input); + default: + break; + } + return input; +} + + +void stHookPerTick2(StSimT *sim) { + if (sim->hookLevel <= ST_LEVEL_L) { + hookPerTickLow(sim); + return; + } + hookPerTickHigh(sim); +} + + +// $7D6F -- after the sprite flush. +void stHookPerTick3(StSimT *sim) { + switch (sim->hookLevel) { + case ST_LEVEL_K: + hookKPerTick(sim); + break; + case ST_LEVEL_O: + hookOPerTick(sim); + break; + case ST_LEVEL_R: + hookRPerTick(sim); + break; + default: + break; + } +} + + +// $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; + case ST_LEVEL_U: + hookUPrelude0(sim); + break; + case ST_LEVEL_Q: + hookQPrelude(sim); + break; + case ST_LEVEL_I: + hookIPrelude(sim); + break; + case ST_LEVEL_W: + memset(sim->hookScratch.w.state, 0, sizeof(sim->hookScratch.w.state)); + break; + case ST_LEVEL_E: + hookEPrelude0(sim); + break; + case ST_LEVEL_G: + hookGPrelude0(sim); + break; + case ST_LEVEL_J: + hookJPrelude0(sim); + break; + case ST_LEVEL_L: + hookLPrelude0(sim); + break; + case ST_LEVEL_F: + hookFPrelude0(sim); + break; + case ST_LEVEL_O: + hookOPrelude0(sim); + break; + case ST_LEVEL_P: + hookPPrelude0(sim); + break; + case ST_LEVEL_Y: + hookYPrelude0(sim); + 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: + sim->hookScratch.w.started = 1u; + break; + case ST_LEVEL_R: + hookRMaze(sim, ST_CHAR_BLANK); + break; + case ST_LEVEL_P: + stSimSfx(sim, P_SFX_WIND(sim)); + break; + case ST_LEVEL_G: + sim->hookScratch.g.lastSpot = ST_HOOK_G_SPOTS; // $7D9A + break; + case ST_LEVEL_I: + case ST_LEVEL_Q: + sim->multiColorMask = ST_HOOK_ALL_MULTICOLOR; // $7DB1 / $7D9B: $D01C = $FF + break; + case ST_LEVEL_Y: + hookYPrelude1(sim); + break; + default: + break; + } +} + + // $62F0 -- a converted level's scratch starts from the bytes the shipped // blob holds at the addresses the original used, so the first tick sees // exactly what the C64's did. -// Level E "BEANSTALK" ($7DA0 prelude0, $7DCA per-tick2): the screen -// opens with pad 1 only. While the fare aboard wants UP the stalk grows: -// every $57 ticks a leaf cell sprouts right along row `row` and left -// along row `row`-2, the green tips one cell beyond; the fifth step -// hangs pads N and N+1 at the leaf ends instead, opens them (pad count -// +2) and moves the growth four rows up, until pad 9 is out. $7DA0 runs -// from the respawn prelude, so a lost cab restarts the count from pad 1 -// while the leaves already drawn stay. -static void hookEPrelude0(StSimT *sim) { - StHookET *e = &sim->hookScratch.e; - - sim->specialPad = 1u; - sim->padCount = 1u; - e->seg = 0u; - e->padSlot = 2u; - e->row = ST_HOOK_E_FIRST_ROW; - e->tick = ST_HOOK_E_TICK_START; - if (sim->levelState == 1u) { - e->tick = ST_HOOK_E_TICK_SECOND; - } -} - - -// $7E89 -- one green cell. -static void hookEPut(StSimT *sim, uint8_t col, uint8_t row, uint8_t ch) { - stSimPutChar(sim, col, row, ch); - stSimPutColor(sim, col, row, ST_HOOK_E_COLOR); -} - - -// $7E39 -- the tips, one cell beyond this step's leaves. -static void hookETips(StSimT *sim) { - const StHookET *e = &sim->hookScratch.e; - - hookEPut(sim, (uint8_t)(ST_HOOK_E_STALK_COL + 1u + e->seg), e->row, ST_HOOK_E_CHAR_TIP_R); - hookEPut(sim, (uint8_t)(ST_HOOK_E_STALK_COL - 1u - e->seg), (uint8_t)(e->row - 2u), ST_HOOK_E_CHAR_TIP_L); -} - - -static void hookEPerTick(StSimT *sim) { - StHookET *e = &sim->hookScratch.e; - - if (sim->specialPad == ST_HOOK_E_LAST_PAD || sim->activeSpriteIdx != ST_FARE_DEST_UP) { - return; - } - e->tick++; - if (e->tick != ST_HOOK_E_GROW_TICKS) { - return; - } - e->tick = 0u; - e->seg++; - if (e->seg != ST_HOOK_E_LEAVES) { - hookEPut(sim, (uint8_t)(ST_HOOK_E_STALK_COL + e->seg), e->row, ST_HOOK_E_CHAR_LEAF); - hookEPut(sim, (uint8_t)(ST_HOOK_E_STALK_COL - e->seg), (uint8_t)(e->row - 2u), ST_HOOK_E_CHAR_LEAF); - hookETips(sim); - return; - } - // $7E15: the pads hang from the leaf ends, then the stalk climbs. - hookEPut(sim, ST_HOOK_E_PAD_COL_RIGHT, e->row, (uint8_t)(ST_CHAR_DIGIT_BASE + e->padSlot)); - hookEPut(sim, ST_HOOK_E_PAD_COL_LEFT, (uint8_t)(e->row - 2u), (uint8_t)(ST_CHAR_DIGIT_BASE + e->padSlot + 1u)); - hookETips(sim); - sim->specialPad = (uint8_t)(sim->specialPad + 2u); - sim->padCount = (uint8_t)(sim->padCount + 2u); - e->seg = 0u; - e->row = (uint8_t)(e->row - ST_HOOK_E_ROW_STEP); - e->padSlot = (uint8_t)(e->padSlot + 2u); - stAudioSfx(E_SFX_GROW(sim)); -} - - -// $7DAF (J) / $7DFE (P) -- a hazard that touched the scenery below row -// $38 bursts: cel $82, phase 0, dropped four rows onto what it hit. -// Sprites 7 down to 3, every one that hit. -static void hookFallBgHits(StSimT *sim, uint8_t *state, uint8_t *phase, const uint8_t *sfx) { - uint8_t x; - - for (x = (uint8_t)(ST_HW_SPRITES - 1u); x >= ST_HOOK_FIRST_SPRITE; x--) { - if ((sim->spriteBgColl & kStBit[x]) == 0u || sim->spr[x].row < ST_HOOK_FALL_HIT_ROW || state[x] == 2u) { - continue; - } - state[x] = 2u; - sim->spr[x].ptr = ST_HK_FALL_CEL_BURST; - phase[x] = 0u; - sim->spr[x].row = (uint8_t)(sim->spr[x].row + ST_HOOK_FALL_HIT_DROP); - if (sfx != 0) { - stAudioSfx(sfx); - } - } -} - - -// $7E01 (J) / $7E45 (P) -- the per-sprite machine, sprites 3..7. Idle: -// spawn on rng($3C) < 3 at rng($FF) + the sprite's column base (J -// rerolls one over the middle band), cel $80 on row $17. Falling: one -// row per tick, cel flipping $80/$81 and drifting sideways on the odd -// cel. Bursting: walk the cel run (P only every other count of a -// shared parity) and go idle at its end. -static void hookFallTick(StSimT *sim, uint8_t *state, uint8_t *phase, const StHookFallT *d, uint8_t *parity) { - uint8_t x; - - for (x = ST_HOOK_FIRST_SPRITE; x < ST_HW_SPRITES; x++) { - if (state[x] == 0u) { - uint16_t col; - if (stSimRng(sim, ST_HOOK_FALL_SPAWN_ODDS) >= ST_HOOK_FALL_SPAWN_HIT) { - continue; - } - do { - col = (uint16_t)(stSimRng(sim, 0xFFu) + HKT(sim, d->startTable + x)); - } while (d->avoidMiddle && col >= ST_HOOK_FALL_BAND_LO && col < ST_HOOK_FALL_BAND_HI); - sim->spr[x].col = (uint8_t)col; - sim->spr[x].msb = (uint8_t)(col >> 8); - sim->spr[x].ptr = ST_HK_FALL_CEL; - sim->spr[x].row = ST_HOOK_FALL_TOP_ROW; - sim->spr[x].enable = 1u; - state[x] = 1u; - } else if (state[x] == 1u) { - sim->spr[x].ptr ^= 1u; - if ((sim->spr[x].ptr & 1u) != 0u) { - stSimSpriteAddX(sim, x, HKT(sim, d->dxTable + x)); - } - sim->spr[x].row++; - } else if (state[x] == 2u) { - if (parity != 0) { - (*parity)++; - if ((*parity & 1u) != 0u) { - continue; - } - } - phase[x]++; - if (phase[x] == d->phaseEnd) { - state[x] = 0u; - sim->spr[x].enable = 0u; - } else { - sim->spr[x].ptr = HKT(sim, d->celTable + phase[x]); - } - } else { - state[x] = 0u; // $7E1C: anything else resets - } - } -} - - -// Level J "SHOOTING STARS" ($7D9D prelude0, $7DAF per-tick2): stars fall -// from the top and burst on the scenery (hookFall*). The hook reads the -// stars' own $D01F bits, so the prelude opts them into the collision -// test. -static void hookJPrelude0(StSimT *sim) { - uint8_t x; - - for (x = ST_HOOK_FIRST_SPRITE; x < ST_HW_SPRITES; x++) { - sim->hookScratch.j.state[x] = 0u; - sim->spr[x].color = ST_HK_J_COLOR; - } - sim->bgCollSprites = ST_HOOK_FALL_SPRITES; -} - - -static void hookJPerTick(StSimT *sim) { - StHookJT *j = &sim->hookScratch.j; - - hookFallBgHits(sim, j->state, j->phase, J_SFX_BURST(sim)); - hookFallTick(sim, j->state, j->phase, &kFallJ, 0); -} - - -// Level L "BLACK HOLE" ($7D9F prelude0, $7DBC per-tick2, $7D9B verdict): -// hardware sprite 3 spins through cels $80..$82 at ($AC, $82) and pulls -// the cab in -- each tick the gravity templates become the pull toward -// it, weaker with distance. -static void hookLPrelude0(StSimT *sim) { - StSpriteT *hole = &sim->spr[ST_HOOK_L_HOLE]; - - hole->color = ST_HK_L_COLOR; - hole->msb = 0u; - hole->col = ST_HOOK_L_HOLE_COL; - hole->row = ST_HOOK_L_HOLE_ROW; - hole->ptr = ST_HK_L_CEL_FIRST; - hole->enable = 1u; -} - - -// $7DCB / $7E01 -- base less |dist|/8 ($7E42), signed by the direction. -static uint16_t hookLPull(uint8_t base, uint8_t dist) { - uint8_t mag = dist; - uint16_t pull; - - if ((mag & 0x80u) != 0u) { - mag = (uint8_t)(0u - mag); - } - pull = (uint8_t)(base - (uint8_t)(mag >> 3)); - if ((dist & 0x80u) != 0u) { - pull = (uint16_t)(0u - pull); - } - return pull; -} - - -static void hookLPerTick(StSimT *sim) { - StSpriteT *hole = &sim->spr[ST_HOOK_L_HOLE]; - uint8_t dy; - uint8_t dx; - - hole->ptr++; - if (hole->ptr == ST_HK_L_CEL_END) { - hole->ptr = ST_HK_L_CEL_FIRST; - } - dy = (uint8_t)(ST_HOOK_L_HOLE_ROW - sim->spr[0].row); - sim->gravTemplateY = hookLPull(ST_HOOK_L_PULL_Y, dy); - dx = (uint8_t)(ST_HOOK_L_HOLE_X_HALF - (uint8_t)(stSimSpriteX(sim, 0u) >> 1)); - sim->gravTemplateX = hookLPull(ST_HOOK_L_PULL_X, dx); -} - - -// Level P "BLIZZARD" ($7DA8 prelude0, $7D9C prelude1, $7DBA per-tick2): -// snow falls and melts on the scenery (hookFall*), and now and then the -// wind turns: the windsocks flip and the X gravity changes sign. -static void hookPPrelude0(StSimT *sim) { - uint8_t x; - - for (x = ST_HOOK_FIRST_SPRITE; x < ST_HW_SPRITES; x++) { - sim->hookScratch.p.state[x] = 0u; - sim->spr[x].color = ST_HK_P_COLOR; - } - sim->bgCollSprites = ST_HOOK_FALL_SPRITES; -} - - -static void hookPFlipSock(StSimT *sim, uint16_t cell) { - stSimPutChar(sim, (uint8_t)(cell % ST_SCREEN_COLS), (uint8_t)(cell / ST_SCREEN_COLS), (uint8_t)(sim->screen[cell] ^ ST_HOOK_P_SOCK_FLIP)); -} - - -static void hookPPerTick(StSimT *sim) { - StHookPT *p = &sim->hookScratch.p; - - if (stSimRng(sim, ST_HOOK_P_WIND_ODDS) < ST_HOOK_P_WIND_HIT) { - stAudioSfx(P_SFX_WIND(sim)); - sim->gravTemplateX = (uint16_t)(0u - sim->gravTemplateX); - hookPFlipSock(sim, ST_CELL(7, 6)); // $051E - hookPFlipSock(sim, ST_CELL(9, 21)); // $057D - hookPFlipSock(sim, ST_CELL(15, 31)); // $0677 - } - hookFallBgHits(sim, p->state, p->phase, 0); - hookFallTick(sim, p->state, p->phase, &kFallP, &p->parity); -} - - -// Level S "THE SWITCH" ($7D9D input filter): the stick's direction bits -// come back swapped through the level's table; FIRE passes. -static uint8_t hookSInput(StSimT *sim, uint8_t input) { - return (uint8_t)((input & ST_HOOK_S_FIRE) | S_DIR_MAP(sim, input & ST_HOOK_S_DIRS)); -} - - void stHookSceneLoad(StSimT *sim) { uint8_t k; @@ -1563,6 +2184,7 @@ void stHookSceneLoad(StSimT *sim) { for (k = 0u; k < ST_HW_SPRITES; k++) { sim->hookScratch.q.phase[k] = HKT(sim, 0x7E83 + k); } + sim->hookScratch.q.parity = HKT(sim, 0x7DFE); break; case ST_LEVEL_T: sim->hookScratch.t.gateShut = HKT(sim, 0x7D9C); @@ -1576,6 +2198,9 @@ void stHookSceneLoad(StSimT *sim) { sim->hookScratch.u.life[k] = HKT(sim, 0x7F6E + k); } break; + case ST_LEVEL_V: + sim->hookScratch.v.tick = HKT(sim, 0x7DE2); + break; case ST_LEVEL_W: sim->hookScratch.w.colorPhase = HKT(sim, 0x7DAB); sim->hookScratch.w.started = HKT(sim, 0x7DE5); @@ -1589,188 +2214,12 @@ void stHookSceneLoad(StSimT *sim) { sim->hookScratch.x.dir = HKT(sim, 0x7D9D); sim->hookScratch.x.tick = HKT(sim, 0x7D9E); break; - default: - break; - } -} - - -// $7D75 -- 0 means sprite contact (passenger, level sprites) is safe. -uint8_t stHookHitVerdict(StSimT *sim) { - switch (sim->hookLevel) { - case ST_LEVEL_G: - // $7DF2: only the scenery kills; that also unhides the cab. - if ((sim->spriteBgColl & 1u) != 0u) { - sim->spr[0].enable = 1u; - return 1u; - } - return 0u; - case ST_LEVEL_H: - return 0u; - case ST_LEVEL_L: - return ST_HOOK_L_VERDICT; - default: - return 1u; - } -} - - -// $7D72 -- the input filter. -uint8_t stHookInput(StSimT *sim, uint8_t input) { - switch (sim->hookLevel) { - case ST_LEVEL_Q: - return hookQInput(sim, input); - case ST_LEVEL_S: - return hookSInput(sim, input); - default: - break; - } - return input; -} - - -// $7D6C -- before the sprite flush. Split in two: one switch over all -// sixteen levels left the 65816's single accumulator with more live -// values than greedy register allocation can place, and the build then -// falls back to the basic allocator for this whole file. -ST_NOINLINE static void hookPerTickLow(StSimT *sim) { - switch (sim->hookLevel) { - case ST_LEVEL_E: - hookEPerTick(sim); - break; - case ST_LEVEL_G: - hookGPerTick(sim); - break; - case ST_LEVEL_H: - hookHPerTick(sim); - break; - case ST_LEVEL_I: - hookIPerTick(sim); - break; - case ST_LEVEL_J: - hookJPerTick(sim); - break; - case ST_LEVEL_K: - hookKPerTick(sim); - break; - case ST_LEVEL_L: - hookLPerTick(sim); - break; - default: - break; - } -} - - -ST_NOINLINE static void hookPerTickHigh(StSimT *sim) { - switch (sim->hookLevel) { - case ST_LEVEL_O: - hookOPerTick(sim); - break; - case ST_LEVEL_P: - hookPPerTick(sim); - break; - case ST_LEVEL_Q: - hookQPerTick(sim); - break; - case ST_LEVEL_R: - hookRPerTick(sim); - break; - case ST_LEVEL_T: - hookTPerTick(sim); - break; - case ST_LEVEL_U: - hookUPerTick(sim); - break; - case ST_LEVEL_V: - hookVPerTick(sim); - break; - case ST_LEVEL_W: - hookWPerTick(sim); - break; - case ST_LEVEL_X: - hookXPerTick(sim); - break; - default: - break; - } -} - - -void stHookPerTick2(StSimT *sim) { - if (sim->hookLevel <= ST_LEVEL_L) { - hookPerTickLow(sim); - return; - } - hookPerTickHigh(sim); -} - - -// $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; - case ST_LEVEL_U: - hookUPrelude0(sim); - break; - case ST_LEVEL_Q: - hookQPrelude(sim); - break; - case ST_LEVEL_I: - hookIPrelude(sim); - break; - case ST_LEVEL_W: - memset(sim->hookScratch.w.state, 0, sizeof(sim->hookScratch.w.state)); - break; - case ST_LEVEL_E: - hookEPrelude0(sim); - break; - case ST_LEVEL_G: - hookGPrelude0(sim); - break; - case ST_LEVEL_J: - hookJPrelude0(sim); - break; - case ST_LEVEL_L: - hookLPrelude0(sim); - break; - case ST_LEVEL_P: - hookPPrelude0(sim); - 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: - sim->hookScratch.w.started = 1u; - break; - case ST_LEVEL_R: - hookRMaze(sim, ST_CHAR_BLANK); - break; - case ST_LEVEL_P: - stAudioSfx(P_SFX_WIND(sim)); - break; - case ST_LEVEL_G: - sim->hookScratch.g.lastSpot = ST_HOOK_G_SPOTS; // $7D9A + case ST_LEVEL_Y: + sim->hookScratch.y.humStep = HKT(sim, 0x7ECC); + sim->hookScratch.y.walkerParity = HKT(sim, 0x7ECB); + sim->hookScratch.y.lightSaveChar = HKT(sim, 0x7F11); + sim->hookScratch.y.lightSaveColor = HKT(sim, 0x7F12); + sim->hookScratch.y.blockFrame = HKT(sim, 0x7F6A); break; default: break; diff --git a/stLevel.c b/stLevel.c index fb23c27..c756bfa 100644 --- a/stLevel.c +++ b/stLevel.c @@ -1,4 +1,4 @@ -// Space Taxi -- level loader (JoeyLib data path + the shared STL4 parser). +// Space Taxi -- level loader (JoeyLib data path + the shared STL5 parser). #include diff --git a/stLevelCels.c b/stLevelCels.c index bb916e7..67cdda3 100644 --- a/stLevelCels.c +++ b/stLevelCels.c @@ -46,6 +46,9 @@ uint8_t stLevelCelList(const StLevelT *level, StCelDefT *out, uint8_t max) { uint8_t c; switch (level->levelIndex) { + case ST_LEVEL_F: + celAdd(out, &n, max, ST_HK_F_CEL, ST_HK_F_COLOR, 0u, 0u, 0u); + break; case ST_LEVEL_G: for (k = 0u; k < ST_HK_G_CEL_PHASES; k++) { celAdd(out, &n, max, HKL(level, ST_HK_G_CEL + k), ST_HK_G_ORB_COLOR, 0u, 0u, 0u); @@ -58,14 +61,15 @@ uint8_t stLevelCelList(const StLevelT *level, StCelDefT *out, uint8_t max) { break; case ST_LEVEL_I: // Every cel in the idle colour and in each of the three the - // launcher picks at random. + // launcher picks at random; multicolour, like every sprite + // on this level ($7DB1). for (c = 0u; c <= ST_HK_I_COLORS; c++) { uint8_t color = (c == 0u) ? ST_HK_I_COLOR_IDLE : HKL(level, ST_HK_I_COLOR + c - 1u); for (k = 0u; k < ST_HK_I_RISE_PHASES; k++) { - celAdd(out, &n, max, HKL(level, ST_HK_I_CEL_RISE + k), color, 0u, 0u, 0u); + celAdd(out, &n, max, HKL(level, ST_HK_I_CEL_RISE + k), color, 1u, level->header[ST_HDR_SPRITE_MC0], level->header[ST_HDR_SPRITE_MC1]); } for (k = 0u; k < ST_HK_I_BURST_PHASES; k++) { - celAdd(out, &n, max, HKL(level, ST_HK_I_CEL_BURST + k), color, 0u, 0u, 0u); + celAdd(out, &n, max, HKL(level, ST_HK_I_CEL_BURST + k), color, 1u, level->header[ST_HDR_SPRITE_MC0], level->header[ST_HDR_SPRITE_MC1]); } } break; @@ -91,15 +95,10 @@ uint8_t stLevelCelList(const StLevelT *level, StCelDefT *out, uint8_t max) { } break; case ST_LEVEL_Q: - // The hook's cels show HIRES in colour 6 for the whole recorded - // ride (tools/stsim STSIM_CELS census, 2026-09-22); the - // multicolour variant (the level's own $D025/$D026) stays - // listed for the phases the census did not reach. + // Multicolour: the level's second prelude hook ($7D9B) sets + // $D01C to $FF after the cab spawn set it to $07. for (k = 0u; k < ST_HK_Q_CEL_PHASES; k++) { - celAdd(out, &n, max, HKL(level, ST_HK_Q_CEL + k), ST_HK_Q_COLOR, 0u, 0u, 0u); - } - for (k = 0u; k < ST_HK_Q_CEL_PHASES; k++) { - celAdd(out, &n, max, HKL(level, ST_HK_Q_CEL + k), ST_HK_Q_COLOR, 1u, level->header[5], level->header[6]); + celAdd(out, &n, max, HKL(level, ST_HK_Q_CEL + k), ST_HK_Q_COLOR, 1u, level->header[ST_HDR_SPRITE_MC0], level->header[ST_HDR_SPRITE_MC1]); } break; case ST_LEVEL_U: @@ -107,6 +106,14 @@ uint8_t stLevelCelList(const StLevelT *level, StCelDefT *out, uint8_t max) { celAdd(out, &n, max, HKL(level, ST_HK_U_CEL + k), ST_HK_U_COLOR, 0u, 0u, 0u); } break; + case ST_LEVEL_Y: + // The saucer is multicolour ($7E02 sets $D01C to $0F); the + // walker and its shot are hires. + celAdd(out, &n, max, ST_HK_Y_CEL_SAUCER, ST_HK_Y_COLOR_SAUCER, 1u, level->header[ST_HDR_SPRITE_MC0], level->header[ST_HDR_SPRITE_MC1]); + for (k = ST_HK_Y_CEL_WALK; k < ST_HK_Y_CEL_SHOT_END; k++) { + celAdd(out, &n, max, k, ST_HK_Y_COLOR_WALKER, 0u, 0u, 0u); + } + break; default: break; } diff --git a/stLevelFile.c b/stLevelFile.c index 65f2755..9333c09 100644 --- a/stLevelFile.c +++ b/stLevelFile.c @@ -1,8 +1,8 @@ -// Space Taxi -- STL4 level file parser (plain stdio, no JoeyLib), shared +// Space Taxi -- STL5 level file parser (plain stdio, no JoeyLib), shared // by the game and the host-side simulation harness. // // Layout (all bytes, written by assets/romToLevel.py): -// "STL4" +// "STL5" // name[22] $7D78 level name in screen codes // header[9] $7D00 VIC colour block // padCount, specialPad, fuelRate @@ -12,7 +12,7 @@ // screen[1000], color[1000] // levelIndex // spriteCount, then spriteCount x (ptr, 63 bytes) -// hookData[616] $7D98..$7FFF +// hookData[819] $7D98..$80CA #include #include @@ -36,7 +36,7 @@ bool stLevelParse(StLevelT *out, FILE *fp) { uint8_t k; memset(out, 0, sizeof(*out)); - if (!readBlock(fp, hdr, 4u) || hdr[0] != 'S' || hdr[1] != 'T' || hdr[2] != 'L' || hdr[3] != '4') { + if (!readBlock(fp, hdr, 4u) || hdr[0] != 'S' || hdr[1] != 'T' || hdr[2] != 'L' || hdr[3] != '5') { return false; } if (!readBlock(fp, out->name, ST_LEVEL_NAME_CHARS)) { diff --git a/stRender.c b/stRender.c index 3d41935..d4c2196 100644 --- a/stRender.c +++ b/stRender.c @@ -1222,7 +1222,13 @@ static bool prewarmReport(uint16_t expect) { ST_LOG("spacetaxi: ! only %u of %u cels built", used, expect); return false; } +#if defined(JOEYLIB_SPRITE_INTERPRETED) + // A port with no sprite emitter (joey/platform.h) interprets every cel + // by design: none compiled is how it always runs. + return true; +#else return hot != 0u; +#endif } diff --git a/stSim.c b/stSim.c index de974ac..c73c286 100644 --- a/stSim.c +++ b/stSim.c @@ -15,9 +15,23 @@ #define ST_JINGLE_SONG_BONUS 4u // level-25 start and the bonus cab #define ST_JINGLE_LEVEL_25 0x19u // $7215 compared against #$19 +// The descent lamp ($6419): two cell pairs beside the fuel gauge, the +// colours they light in, and the Y velocities that pick one. +#define ST_LAMP_CELL_SLOW ST_CELL(23, 10) // $DBA2 +#define ST_LAMP_CELL_FAST ST_CELL(24, 10) // $DBCA +#define ST_LAMP_OFF 0x0Bu // dark grey +#define ST_LAMP_SLOW 0x03u // cyan +#define ST_LAMP_FALL 0x07u // yellow +#define ST_LAMP_FAST 0x02u // red +#define ST_LAMP_FALL_VEL 0x0080 // high byte 0, low bit 7 +#define ST_LAMP_FAST_VEL 0x0100 // high byte positive +#define ST_LAMP_TICK_MASK 7u // $71BE counts mod 8... +#define ST_LAMP_LIT_TICKS 5u // ...and lights on 0..4 + // Screen-code strings the engine writes (all ASCII-compatible glyphs). -// $6C38 -- the 11-character blank that clears the message row. -static const uint8_t kTextBlank[] = " "; +// $6C38 -- the 12-character blank that clears the message row (every +// message is 12 characters: "PAD PLEASE", " UP PLEASE! " and the rest). +static const uint8_t kTextBlank[] = " "; // $43B1 -- the fare meter's blank template: three leading blanks, then // "0.00" in the inverse-video digit glyphs. static const uint8_t kHudTemplate[ST_NUMBER_CHARS] = { 0x66, 0x66, 0x66, 0x74, 0x77, 0x74, 0x74 }; @@ -44,6 +58,8 @@ static void drawCabIcons(StSimT *sim); static void drawScreensCount(StSimT *sim); static void edgeReflect(StSimT *sim); static void fireButtonEdge(StSimT *sim); +static uint8_t frameBgCollisions(StSimT *sim); +static uint8_t frameSpriteCollisions(StSimT *sim); static void fuelBarHud(StSimT *sim); static void fuelTick(StSimT *sim); static void hudDraw(StSimT *sim); @@ -58,13 +74,16 @@ static uint8_t readInput(StSimT *sim); static void rowWindow(const uint8_t *row, uint8_t s, uint8_t *w); static void setCell(StSimT *sim, uint16_t cell, uint8_t ch); static void setColorCell(StSimT *sim, uint16_t cell, uint8_t color); +static void sfxEnvelopeTick(StSimT *sim); static void spriteMasks(StSimT *sim, uint8_t idx); static bool spriteRowsTouchBackground(const StSimT *sim, uint8_t idx, int16_t firstCol, int16_t py, uint8_t phase); static bool spriteTouchesBackground(StSimT *sim, uint8_t idx); static void spritesOffReset(StSimT *sim); +static bool spritesTouch(StSimT *sim, uint8_t a, uint8_t b, int16_t dx, int16_t dy); static void taxiSpawnInit(StSimT *sim); static void taxiSpriteCelSelect(StSimT *sim); static uint8_t validateDigit(uint8_t ch); +static void voicesOff(StSimT *sim); // $6112 -- add the X velocity to the 17-bit X position (msb:col:frac) @@ -250,7 +269,7 @@ static void collisionPhase1(StSimT *sim) { sim->collisionPhase++; sim->phaseReload = 2u; sim->phaseTimer = 2u; - stAudioSfx(stC64SfxProgram(ST_SFX_IMPACT)); + stSimSfx(sim, stC64SfxProgram(ST_SFX_IMPACT)); } @@ -274,8 +293,6 @@ static void collisionPhase2(StSimT *sim) { // $6B4C -- 70-tick pause, then the life bookkeeping. static StTickResultE collisionPhase3(StSimT *sim) { - uint8_t p; - sim->phaseTimer--; if (sim->phaseTimer != 0u) { return ST_TICK_CONTINUE; @@ -283,142 +300,18 @@ static StTickResultE collisionPhase3(StSimT *sim) { if (sim->demoMode != 0u) { return ST_TICK_CRASH_DONE; } - if (sim->stage == ST_STAGE_RIDING) { - stSimHudInit(sim); - if (sim->fareSlotCount != 0u) { - sim->spriteSlots[sim->activeDyingSlot] = 0u; - sim->fareSlotCount--; - if (sim->activeSpriteIdx == ST_FARE_DEST_UP) { - uint8_t k; - for (k = 0u; k < 4u; k++) { - setCell(sim, (uint16_t)(ST_CELL_TRANSPORTER + k), ST_CHAR_TRANSPORTER); - } - } else { - sim->spriteSlots[sim->activeSpriteIdx] = 0u; - sim->fareSlotCount--; - } - } - sim->activeSpriteIdx = 0u; - sim->stage = ST_STAGE_IDLE; - stSimClearMessage(sim); - } - // $6BBA - p = sim->player; - sim->cabs[p]--; - if (sim->cabs[p] == 0u) { - return ST_TICK_PLAYER_OUT; - } - setCell(sim, (uint16_t)(ST_CELL_CAB_ICONS + sim->cabs[p]), ST_CHAR_BLANK); - return ST_TICK_LIFE_LOST; + return stSimLoseCab(sim); } - -// The C64 reads two VIC collision registers each frame: $D01F (a sprite -// touched the background) and $D01E (two sprites touched). Only the cab -// (sprite 0) can drive a gameplay outcome from them: the crash dispatch -// returns unless the cab or the background is touched, and the hooks -// read a bit set by cab contact -- except levels J and P, which watch -// their own hazards hit the scenery and opt those sprites in through -// bgCollSprites. So the full all-pairs register is not reproduced -- -// only sprite 0 versus the background and versus the other sprites, -// plus the opted-in sprites versus the background, and the exhaust -// flame (sprite 2, the cab's own, always overlapping it) is skipped. -// Non-cab pairs (hazard vs hazard, passenger vs hazard with the cab -// clear) never change the dispatch outcome, so their bits are left -// unset. Proven against all four demo traces: the observable state -// (positions, crash phase, score) is byte-identical. +// $6966 -- what its read of $D01E/$D01F sees: this tick's frame OR the +// frame drawn before the flush (see prevFrameSpriteColl in StSimT). static void computeCollisions(StSimT *sim) { - uint8_t ss = 0u; - uint8_t bg = 0u; - uint8_t b; - uint8_t r; + uint8_t ss = frameSpriteCollisions(sim); - for (b = 1u; b < ST_HW_SPRITES; b++) { - if ((sim->bgCollSprites & kStBit[b]) == 0u || (sim->frame.enableMask & kStBit[b]) == 0u) { - continue; - } - spriteMasks(sim, b); - if (spriteTouchesBackground(sim, b)) { - bg |= kStBit[b]; - } - } - if ((sim->frame.enableMask & 1u) == 0u) { - sim->spriteSpriteColl = 0u; - sim->spriteBgColl = bg; - return; - } - spriteMasks(sim, 0u); - if (spriteTouchesBackground(sim, 0u)) { - bg = 1u; - } - for (b = 1u; b < ST_HW_SPRITES; b++) { - int16_t dx; - int16_t dy; - uint8_t s; - int16_t q; - uint8_t kLo; - uint8_t kHi; - uint8_t rLo; - uint8_t rHi; - int16_t lo; - int16_t hi; - const uint8_t *win; - const uint8_t *bRow; - // Skip the exhaust flame (sprite 2) and any disabled sprite. - if (b == 2u || (sim->frame.enableMask & kStBit[b]) == 0u) { - continue; - } - dx = (int16_t)((int16_t)sim->frame.x[b] - (int16_t)sim->frame.x[0]); - dy = (int16_t)((int16_t)sim->frame.y[b] - (int16_t)sim->frame.y[0]); - if (dx <= -ST_SPRITE_W || dx >= ST_SPRITE_W || dy <= -ST_SPRITE_H || dy >= ST_SPRITE_H) { - continue; - } - spriteMasks(sim, b); - // Sprite b sits dx pixels right of the cab; with dx = 8q - s - // (s in 0..7) its row byte k lines up with byte k + q of the - // cab's phase-s windows, and only 0 <= k + q <= 3 can overlap. - s = (uint8_t)((0 - dx) & (ST_ROW_PHASES - 1)); - q = (int16_t)(((dx + (int16_t)s + 32) >> 3) - 4); - kLo = (q < 0) ? (uint8_t)(0 - q) : 0u; - kHi = (q > 1) ? (uint8_t)(3 - q) : 2u; - // Rows r of the cab meet rows r - dy of sprite b; walk only the - // rows where both have pixels, both pointers advancing in step. - if (sim->collRowFirst[b] == ST_SPRITE_H || sim->collRowFirst[0] == ST_SPRITE_H) { - continue; - } - lo = (int16_t)sim->collRowFirst[0]; - hi = (int16_t)(sim->collRowLast[0] + 1); - if ((int16_t)sim->collRowFirst[b] + dy > lo) { - lo = (int16_t)((int16_t)sim->collRowFirst[b] + dy); - } - if ((int16_t)sim->collRowLast[b] + 1 + dy < hi) { - hi = (int16_t)((int16_t)sim->collRowLast[b] + 1 + dy); - } - if (lo >= hi) { - continue; - } - rLo = (uint8_t)lo; - rHi = (uint8_t)hi; - win = cabWindows(sim, s) + (uint16_t)rLo * 4u; - bRow = sim->collMask[b][(uint8_t)((int16_t)rLo - dy)]; - for (r = rLo; r < rHi; r++, win += 4, bRow += 3) { - bool hit = false; - uint8_t k; - for (k = kLo; k <= kHi; k++) { - if ((bRow[k] & win[(uint8_t)((int16_t)k + q)]) != 0u) { - hit = true; - break; - } - } - if (hit) { - ss |= (uint8_t)(1u | kStBit[b]); - break; - } - } - } - sim->spriteSpriteColl = ss; - sim->spriteBgColl = bg; + sim->spriteSpriteColl = (uint8_t)(ss | sim->prevFrameSpriteColl); + sim->spriteBgColl = (uint8_t)(frameBgCollisions(sim) | sim->prevFrameBgColl); + sim->prevFrameSpriteColl = ss; } @@ -446,12 +339,12 @@ static void crashStart(StSimT *sim) { sim->phaseTimer = 2u; sim->phaseReload = 2u; stAudioNoise(false); - sim->eventDispatchType = 0u; + sim->eventDispatchType = ST_EVENT_NONE; sim->activePad = 0u; sim->spr[2].enable = 0u; sim->spr[2].row = 0u; sim->velY = 0x0303; - stAudioSfx(stC64SfxProgram(ST_SFX_CRASH)); + stSimSfx(sim, stC64SfxProgram(ST_SFX_CRASH)); sim->thrustSweep = 0xA0u; if (sim->stage == ST_STAGE_WALK_TO_CAB) { sim->stage--; @@ -529,7 +422,7 @@ static void fireButtonEdge(StSimT *sim) { } sim->fireWasHeld = 0x10u; sim->spr[0].ptr ^= 1u; - stAudioSfx(stC64SfxProgram(ST_SFX_GEAR)); + stSimSfx(sim, stC64SfxProgram(ST_SFX_GEAR)); } @@ -558,29 +451,91 @@ void stSimFlameUpdate(StSimT *sim) { } -// $6419 -- the climb/descend indicator: colour RAM cells beside the -// fuel gauge flash red (rising), cyan (level) or yellow (falling). -static void fuelBarHud(StSimT *sim) { - uint16_t cell; - uint8_t color; +// The displayed frame's $D01F bits over the current picture -- only the +// bits anyone reads: the cab's, and those of the hazards levels J and P +// opt in through bgCollSprites. +static uint8_t frameBgCollisions(StSimT *sim) { + uint8_t bg = 0u; + uint8_t b; - setColorCell(sim, ST_CELL(23, 10), 0x0Bu); - setColorCell(sim, ST_CELL(23, 11), 0x0Bu); - setColorCell(sim, ST_CELL(24, 10), 0x0Bu); - setColorCell(sim, ST_CELL(24, 11), 0x0Bu); - sim->fuelBarTick = (uint8_t)((sim->fuelBarTick + 1u) & 7u); - if (sim->fuelBarTick >= 5u) { + for (b = 0u; b < ST_HW_SPRITES; b++) { + if ((sim->frame.enableMask & kStBit[b]) == 0u) { + continue; + } + if (b != 0u && (sim->bgCollSprites & kStBit[b]) == 0u) { + continue; + } + spriteMasks(sim, b); + if (spriteTouchesBackground(sim, b)) { + bg |= kStBit[b]; + } + } + return bg; +} + + +// The displayed frame's $D01E: both bits of every pair of shown sprites +// whose pixels meet -- any pair, because the hooks test whole groups of +// bits (G's and the dispatch's AND #$F8 also catch a hook sprite touching +// the exhaust flame or another hook sprite, H's and U's walk every bit, +// and the pad light's AND #$02 catches the passenger touching anything). +static uint8_t frameSpriteCollisions(StSimT *sim) { + uint8_t shown = sim->frame.enableMask; + uint8_t ss = 0u; + uint8_t a; + uint8_t b; + + for (a = 0u; a < (uint8_t)(ST_HW_SPRITES - 1u); a++) { + if ((shown & kStBit[a]) == 0u) { + continue; + } + for (b = (uint8_t)(a + 1u); b < ST_HW_SPRITES; b++) { + uint8_t pair = (uint8_t)(kStBit[a] | kStBit[b]); + int16_t dx; + int16_t dy; + // A pair already latched on both sides cannot add a bit, and + // most pairs are too far apart to test at all. + if ((shown & kStBit[b]) == 0u || (ss & pair) == pair) { + continue; + } + dx = (int16_t)((int16_t)sim->frame.x[b] - (int16_t)sim->frame.x[a]); + dy = (int16_t)((int16_t)sim->frame.y[b] - (int16_t)sim->frame.y[a]); + if (dx <= -ST_SPRITE_W || dx >= ST_SPRITE_W || dy <= -ST_SPRITE_H || dy >= ST_SPRITE_H) { + continue; + } + if (spritesTouch(sim, a, b, dx, dy)) { + ss |= pair; + } + } + } + return ss; +} + + +// $6419 -- the descent lamp beside the fuel gauge. Every tick its two +// cell pairs ($DBA2 on row 23, $DBCA on row 24) go dark grey; then, on +// five ticks of every eight, one pair lights by the Y velocity +// ($714F/$714E, positive = falling): red on row 24 when the high byte is +// positive ($643D), yellow on row 23 when it is zero and the low byte has +// bit 7 set ($6450), and cyan on row 23 otherwise ($6449) -- any climb, +// a hover and a slow fall alike. +static void fuelBarHud(StSimT *sim) { + uint16_t cell = ST_LAMP_CELL_SLOW; + uint8_t color = ST_LAMP_SLOW; + + setColorCell(sim, ST_LAMP_CELL_SLOW, ST_LAMP_OFF); + setColorCell(sim, (uint16_t)(ST_LAMP_CELL_SLOW + 1u), ST_LAMP_OFF); + setColorCell(sim, ST_LAMP_CELL_FAST, ST_LAMP_OFF); + setColorCell(sim, (uint16_t)(ST_LAMP_CELL_FAST + 1u), ST_LAMP_OFF); + sim->fuelBarTick = (uint8_t)((sim->fuelBarTick + 1u) & ST_LAMP_TICK_MASK); + if (sim->fuelBarTick >= ST_LAMP_LIT_TICKS) { return; } - if (sim->velY < 0) { - color = 0x02u; - cell = ST_CELL(24, 10); - } else if (sim->velY == 0) { - color = 0x03u; - cell = ST_CELL(23, 10); - } else { - color = 0x07u; - cell = ST_CELL(23, 10); + if (sim->velY >= ST_LAMP_FAST_VEL) { + color = ST_LAMP_FAST; + cell = ST_LAMP_CELL_FAST; + } else if (sim->velY >= ST_LAMP_FALL_VEL) { + color = ST_LAMP_FALL; } setColorCell(sim, cell, color); setColorCell(sim, (uint16_t)(cell + 1u), color); @@ -603,7 +558,7 @@ static void fuelTick(StSimT *sim) { } sim->padAnimTick = (uint8_t)((sim->padAnimTick + 1u) & 7u); if (sim->padAnimTick == 0u) { - stAudioSfx(stC64SfxProgram(ST_SFX_CASH)); + stSimSfx(sim, stC64SfxProgram(ST_SFX_CASH)); } else if (sim->padAnimTick < 5u) { stAudioVoice1Freq(kPumpFreq[sim->padAnimTick]); } @@ -619,7 +574,7 @@ static void fuelTick(StSimT *sim) { } setCell(sim, (uint16_t)(ST_CELL_FUEL + x), ST_CHAR_BLANK); if (sim->screen[ST_CELL_FUEL_LAST] == ST_CHAR_BLANK) { - stAudioSfx(stC64SfxProgram(ST_SFX_FUEL_FULL)); + stSimSfx(sim, stC64SfxProgram(ST_SFX_FUEL_FULL)); } return; } @@ -632,7 +587,7 @@ static void fuelTick(StSimT *sim) { return; } if (sim->fuelCells < 3u) { - stAudioSfx(stC64SfxProgram(ST_SFX_FUEL_LOW)); + stSimSfx(sim, stC64SfxProgram(ST_SFX_FUEL_LOW)); } sim->fuelCountdown = L->fuelRate; x = sim->fuelCells; @@ -716,12 +671,12 @@ static void padDetect(StSimT *sim) { } sim->activePad = (uint8_t)(i + 1); sim->activePadMirror = sim->activePad; - sim->eventDispatchType = 0u; + sim->eventDispatchType = ST_EVENT_NONE; if (sim->activePad != sim->activeSpriteIdx) { if (sim->stage == ST_STAGE_RIDING) { - sim->eventDispatchType = (sim->activeSpriteIdx == ST_FARE_DEST_UP) ? 3u : 2u; + sim->eventDispatchType = (sim->activeSpriteIdx == ST_FARE_DEST_UP) ? ST_EVENT_WRONG_PAD_UP : ST_EVENT_WRONG_PAD; } else if (sim->stage == ST_STAGE_WAIT) { - sim->eventDispatchType = 1u; + sim->eventDispatchType = ST_EVENT_HAIL; } } sim->dirMask = 0u; @@ -729,11 +684,11 @@ static void padDetect(StSimT *sim) { stSimPadLight(sim, true); sim->bobTimer = 2u; if (((uint8_t)sim->velY & 0x80u) == 0u) { - stAudioSfx(stC64SfxProgram(ST_SFX_LAND_SOFT)); + stSimSfx(sim, stC64SfxProgram(ST_SFX_LAND_SOFT)); return; } sim->bobTimer = 8u; - stAudioSfx(stC64SfxProgram(ST_SFX_LAND_HARD)); + stSimSfx(sim, stC64SfxProgram(ST_SFX_LAND_HARD)); if (sim->stage == ST_STAGE_RIDING) { stSimHudInit(sim); } @@ -959,6 +914,20 @@ static void setColorCell(StSimT *sim, uint16_t cell, uint8_t color) { } +// $4320 -- the release countdown of voices 1 and 2. Gating a voice off +// when its count runs out is the audio layer's business; the counts are +// what the game reads. +static void sfxEnvelopeTick(StSimT *sim) { + uint8_t v; + + for (v = 0u; v < ST_SID_TIMED_VOICES; v++) { + if (sim->sidRelTimer[v] != 0u) { + sim->sidRelTimer[v]--; + } + } +} + + // 21 rows of 3-byte foreground masks for a displayed sprite, kept in // collMask[idx] and rebuilt only when its bitmap or multicolour mode // changes. A multicolour sprite's 2-bit pairs cover two pixels each: a @@ -1068,13 +1037,9 @@ static bool spriteTouchesBackground(StSimT *sim, uint8_t idx) { } -// $6946 -- all sprites off, latches cleared, nobody on a pad. +// $6946 -- the VIC's sprites off ($D015; the shadow enables are $4248's +// job), the collision latches cleared, nobody on a pad. static void spritesOffReset(StSimT *sim) { - uint8_t i; - - for (i = 0u; i < ST_HW_SPRITES; i++) { - sim->spr[i].enable = 0u; - } sim->spriteSpriteColl = 0u; sim->spriteBgColl = 0u; sim->activePad = 0u; @@ -1082,6 +1047,71 @@ static void spritesOffReset(StSimT *sim) { } +// Whether shown sprites a < b, b sitting dx right of a and dy below it +// (both inside a sprite's size), have a pixel in common. With dx = 8q - s +// (s in 0..7), b's row byte k lines up with byte k + q of a's row shifted +// to phase s, and only 0 <= k + q <= 3 can overlap; rows r of a meet rows +// r - dy of b, and only the rows where both have pixels are walked. The +// cab's shifted rows are cached (cabWindows) -- it is a side of most +// tests -- any other sprite's are shifted as they are needed. +static bool spritesTouch(StSimT *sim, uint8_t a, uint8_t b, int16_t dx, int16_t dy) { + uint8_t s; + int16_t q; + uint8_t kLo; + uint8_t kHi; + uint8_t r; + uint8_t rHi; + int16_t lo; + int16_t hi; + const uint8_t *win = 0; + const uint8_t *bRow; + uint8_t own[4]; + + spriteMasks(sim, a); + spriteMasks(sim, b); + if (sim->collRowFirst[a] == ST_SPRITE_H || sim->collRowFirst[b] == ST_SPRITE_H) { + return false; + } + s = (uint8_t)((0 - dx) & (ST_ROW_PHASES - 1)); + q = (int16_t)(((dx + (int16_t)s + 32) >> 3) - 4); + kLo = (q < 0) ? (uint8_t)(0 - q) : 0u; + kHi = (q > 1) ? (uint8_t)(3 - q) : 2u; + lo = (int16_t)sim->collRowFirst[a]; + hi = (int16_t)(sim->collRowLast[a] + 1); + if ((int16_t)sim->collRowFirst[b] + dy > lo) { + lo = (int16_t)((int16_t)sim->collRowFirst[b] + dy); + } + if ((int16_t)sim->collRowLast[b] + 1 + dy < hi) { + hi = (int16_t)((int16_t)sim->collRowLast[b] + 1 + dy); + } + if (lo >= hi) { + return false; + } + r = (uint8_t)lo; + rHi = (uint8_t)hi; + bRow = sim->collMask[b][(uint8_t)((int16_t)r - dy)]; + if (a == 0u) { + win = cabWindows(sim, s) + (uint16_t)r * 4u; + } + for (; r < rHi; r++, bRow += 3) { + const uint8_t *w = win; + uint8_t k; + if (a == 0u) { + win += 4; + } else { + rowWindow(sim->collMask[a][r], s, own); + w = own; + } + for (k = kLo; k <= kHi; k++) { + if ((bRow[k] & w[(uint8_t)((int16_t)k + q)]) != 0u) { + return true; + } + } + } + return false; +} + + // $6888 -- place the cab at the level's spawn point with a full tank. static void taxiSpawnInit(StSimT *sim) { const StLevelT *L = sim->level; @@ -1149,6 +1179,13 @@ static uint8_t validateDigit(uint8_t ch) { } +// $6909 / $6C02 -- voices 1 and 2 gated off, their release timers cleared. +static void voicesOff(StSimT *sim) { + stAudioSilence(); + memset(sim->sidRelTimer, 0, sizeof(sim->sidRelTimer)); +} + + // --------------------------------------------------------------------------- // Public API // --------------------------------------------------------------------------- @@ -1382,26 +1419,15 @@ void stSimEnterLevel(StSimT *sim, const StLevelT *level) { } sim->hookLevel = level->levelIndex; stHookSceneLoad(sim); + spritesOffReset(sim); + stHookPrelude0(sim); + stFareTakeoffSetup(sim); stSimRespawn(sim); } // $5F27 -- the respawn half of the prelude. void stSimRespawn(StSimT *sim) { - spritesOffReset(sim); - stHookPrelude0(sim); - // $6F18 takeoffSetup: from the third screen on the cab starts with - // a passenger aboard who wants pad 1. - sim->eventDispatchType = 0u; - sim->stage = ST_STAGE_IDLE; - if (sim->levelState >= 2u) { - static const uint8_t kPadPlease[] = "PAD 1 PLEASE"; - sim->stage = ST_STAGE_RIDING; - sim->activeSpriteIdx = 1u; - sim->activeDyingSlot = 1u; - stSimDrawText(sim, ST_MESSAGE_COL, ST_MESSAGE_ROW, kPadPlease, 1u); - sim->eventDispatchType = 1u; - } taxiSpawnInit(sim); stHookPrelude1(sim); // $6906 framePresent: the start-of-screen jingle, then the SID (and @@ -1409,7 +1435,7 @@ void stSimRespawn(StSimT *sim) { // screen rotates songs 0..3 on $5E9B. The host plays it with the sim // frozen, which is what the C64's blocking play-and-wait amounts to. stSimMarshal(sim); - stAudioSilence(); + voicesOff(sim); sim->jingleRotate++; if (sim->levelState == ST_JINGLE_LEVEL_25) { sim->jingleRequest = (uint8_t)(ST_JINGLE_SONG_BONUS + 1u); @@ -1418,8 +1444,12 @@ void stSimRespawn(StSimT *sim) { } sim->spriteSpriteColl = 0u; sim->spriteBgColl = 0u; + // $6920 cleared the latch, but the VIC goes on showing this frame + // through the jingle, so the first tick's read still sees it. + sim->prevFrameSpriteColl = frameSpriteCollisions(sim); sim->rngT1 = 0x06u; sim->rngT2 = 0x17u; + stFareTakeoffVoice(sim); } @@ -1462,6 +1492,43 @@ void stSimNewGame(StSimT *sim, uint8_t playerCount, bool demo) { } +// $6B64..$6BCD -- the game's side of a finished wreck (the attract +// demo returns to the title instead): a fare aboard is lost -- its meter, +// its pads, an open hatch, its message -- then the player's cab count +// drops. With cabs left the host respawns the cab (stSimRespawn, $5F27). +StTickResultE stSimLoseCab(StSimT *sim) { + uint8_t p; + + if (sim->stage == ST_STAGE_RIDING) { + stSimHudInit(sim); + if (sim->fareSlotCount != 0u) { + sim->spriteSlots[sim->activeDyingSlot] = 0u; + sim->fareSlotCount--; + if (sim->activeSpriteIdx == ST_FARE_DEST_UP) { + uint8_t k; + for (k = 0u; k < ST_TRANSPORTER_CELLS; k++) { + setCell(sim, (uint16_t)(ST_CELL_TRANSPORTER + k), ST_CHAR_TRANSPORTER); + } + } else { + sim->spriteSlots[sim->activeSpriteIdx] = 0u; + sim->fareSlotCount--; + } + } + sim->activeSpriteIdx = 0u; + sim->stage = ST_STAGE_IDLE; + stSimClearMessage(sim); + } + // $6BBA + p = sim->player; + sim->cabs[p]--; + if (sim->cabs[p] == 0u) { + return ST_TICK_PLAYER_OUT; + } + setCell(sim, (uint16_t)(ST_CELL_CAB_ICONS + sim->cabs[p]), ST_CHAR_BLANK); + return ST_TICK_LIFE_LOST; +} + + // Everything must be repainted. // A glyph's bits changed: the renderer has to repaint every cell showing // that character. The list is short (one or two per frame), so a linear @@ -1625,6 +1692,17 @@ void stSimSetDemoStream(StSimT *sim, const uint8_t *stream, uint16_t len) { } +// $42E9 -- an SFX program starts; voices 0 and 1 arm their release timer. +void stSimSfx(StSimT *sim, const uint8_t *program9) { + uint8_t voice = program9[ST_SFX_VOICE_AT]; + + if (voice < ST_SID_TIMED_VOICES) { + sim->sidRelTimer[voice] = program9[ST_SFX_RELEASE_AT]; + } + stAudioSfx(program9); +} + + // $411B -- add a signed byte to a sprite's 17-bit X, carrying into the // msb shadow. void stSimSpriteAddX(StSimT *sim, uint8_t idx, uint8_t delta) { @@ -1671,18 +1749,20 @@ StTickResultE stSimTick(StSimT *sim) { taxiSpriteCelSelect(sim); if (levelEndCheck(sim)) { stAudioNoise(false); - stAudioSilence(); + voicesOff(sim); stSimArchiveHud(sim); return ST_TICK_LEVEL_EXIT; } padLandingBob(sim); fuelTick(sim); - // $61BD passengerEventDraw only re-voices the pickup/drop-off - // messages already drawn by the fare machine. + stFareEventVoice(sim); } stFarePostTickGate(sim); stFareStageDispatch(sim); stHookPerTick2(sim); + // $5F94: the frame drawn while the CPU waits shows the last flush's + // sprites over the picture as this tick has left it. + sim->prevFrameBgColl = frameBgCollisions(sim); stSimMarshal(sim); stSimRotateGlyph(sim, ST_CHAR_TRANSPORTER); stHookPerTick3(sim); @@ -1703,6 +1783,7 @@ StTickResultE stSimTick(StSimT *sim) { } } hudDraw(sim); + sfxEnvelopeTick(sim); // $4FCB runStopWatcher: any joystick input ends the demo. if (sim->demoMode != 0u && (sim->rawInput & 0x1Fu) != 0u) { return ST_TICK_DEMO_INPUT; diff --git a/stSim.h b/stSim.h index 2c2d1b9..a70ac19 100644 --- a/stSim.h +++ b/stSim.h @@ -52,13 +52,14 @@ #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_TRANSPORTER ST_CELL(0, 18) // $0412..$0415 +#define ST_TRANSPORTER_CELLS 4u #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_MESSAGE ST_CELL(24, 14) // $07CE, 12 chars #define ST_MESSAGE_COL 14u #define ST_MESSAGE_ROW 24u #define ST_CELL_PAD_DIGIT ST_CELL(24, 18) // $07D2 @@ -79,7 +80,7 @@ #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_CHAR_ELECTROID 0x7Cu // level O's barrier glyph ($7E6E rolls $2BE0) #define ST_FARE_DEST_UP 0x0Bu // $7D8E: the fare aboard wants UP // ---- level-intro starfield -------------------------------------------- @@ -149,9 +150,16 @@ typedef char stStarRingCountIsPow2[((ST_STAR_RING_COUNT & (ST_STAR_RING_COUNT - // Distinct characters whose glyph can change in one frame before the // renderer gives up and repaints everything. #define ST_CHAR_DIRTY_MAX 8u +// A 9-byte SFX program ($42E9): SID registers 0..6, then the release +// timer and the voice. Only voices 0 and 1 have a timer ($7216/$7217; +// voice 2's lands on $7218, the voice 1 gate mask, which only the audio +// layer uses). +#define ST_SFX_RELEASE_AT 7u +#define ST_SFX_VOICE_AT 8u +#define ST_SID_TIMED_VOICES 2u // 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 +// $7C, 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 @@ -160,14 +168,16 @@ typedef struct { uint8_t rows[8]; } StGlyphT; -// Level image as loaded from a .dat (STL4). Kept in the C64's units. +// Level image as loaded from a .dat (STL5). 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. +// Per-level hook code + tables live from $7D98 in the level's data blob; +// the port keeps the bytes (for the tables) and re-expresses the code in +// stHooks.c. Most levels end by $7FFF, but level Y's program runs on past +// $8000, its last table being the SFX program at $80C2..$80CA. #define ST_HOOK_BASE 0x7D98u -#define ST_HOOK_BYTES (0x8000u - ST_HOOK_BASE) +#define ST_HOOK_END 0x80CBu +#define ST_HOOK_BYTES (ST_HOOK_END - 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 @@ -190,6 +200,13 @@ typedef struct { uint8_t tone; // $7E56 rising chime } StHookHT; +typedef struct { + uint16_t vx; // $7DD1/$7DD2 the ball's X step + uint16_t x; // $7DD5/$7DD6 its 9-bit sprite X + uint16_t y; // $7DD7/$7DD8 its row, 8.8 + uint16_t vy; // $7DD9/$7DDA 8.8 +} StHookFT; + typedef struct { uint8_t celPhase; // $7DD2 orb cel cycle 0..5 uint8_t tick; // $7E00 @@ -215,12 +232,34 @@ typedef struct { typedef struct { uint8_t phase[ST_HW_SPRITES]; // $7E83 interference cel phase + uint8_t parity; // $7DFE the cels step every other tick } StHookQT; typedef struct { uint8_t gateShut; // $7D9C } StHookTT; +typedef struct { + uint8_t tick; // $7DE2 shift every sixteenth +} StHookVT; + +typedef struct { + uint8_t humStep; // $7ECC the saucer's hum pitch index + uint8_t walkerDir; // $7EC2 0 left, 1 right + uint8_t walkerCount; // $7EC9 + uint8_t walkerParity; // $7ECB + uint8_t lightTick; // $7F10 + uint8_t lightCell; // $7F13 0..119 over rows 19..21 + uint8_t lightSaveChar; // $7F11 + uint8_t lightSaveColor; // $7F12 + uint8_t blockTimer; // $7F66 + uint8_t blockDir; // $7168 0 left, 1 right + uint8_t blockCol; // $7F69 + uint8_t blockFrame; // $7F6A + uint8_t shotDelay; // $803B + uint8_t shotLive; // $808C +} StHookYT; + typedef struct { uint8_t colorPhase; // $7DAB uint8_t state[ST_HOOK_W_LASERS]; // $7DAC 0 idle, 1 extending, 2 fading @@ -262,6 +301,7 @@ typedef struct { typedef union { StHookET e; + StHookFT f; StHookGT g; StHookHT h; StHookIT i; @@ -272,8 +312,10 @@ typedef union { StHookQT q; StHookTT t; StHookUT u; + StHookVT v; StHookWT w; StHookXT x; + StHookYT y; } StHookScratchT; typedef struct { @@ -309,7 +351,7 @@ typedef struct { 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 + uint8_t hookData[ST_HOOK_BYTES]; // $7D98..$80CA as shipped } StLevelT; // One VIC sprite as the game sees it through its shadow tables. Padded @@ -358,6 +400,16 @@ typedef enum { ST_STAGE_CLEANUP // $67C6 } StStageE; +// What a landing has left to say ($71CD), voiced once the touchdown bob +// settles ($61BD) and cleared by every phrase the game speaks ($6C7F). +typedef enum { + ST_EVENT_NONE = 0, + ST_EVENT_HAIL, // $64F3: a fare waits on another pad; also + // $6F51: the takeoff fare's call ($5F30) + ST_EVENT_WRONG_PAD, // $64DF: a fare aboard, set down on the wrong pad + ST_EVENT_WRONG_PAD_UP // $64E7: ...and the fare wants UP +} StEventE; + // What a tick asked the host to do next (the original abandons the // main loop with PLA/PLA on these). @@ -460,6 +512,10 @@ typedef struct { uint8_t bobTimer; // $716F uint8_t flameParity; // $716C uint8_t thrustSweep; // $721B + // Voice 1 and 2 release timers ($7216/$7217): $42E9 loads one from an + // SFX program and $4320 counts it down every tick. Game state, not + // just sound: levels Q and Y only buzz while voice 2's has run out. + uint8_t sidRelTimer[ST_SID_TIMED_VOICES]; // ---- collision / crash ---- uint8_t collisionPhase; // $7164 @@ -468,6 +524,18 @@ typedef struct { uint8_t hitDispatchResult; // $721D uint8_t spriteSpriteColl; // $71CA latched $D01E uint8_t spriteBgColl; // $71CB latched $D01F + // The older of the two frames a read sees (derived state, not C64 + // memory). The VIC latches $D01E/$D01F until they are read, and the + // main loop reads them two frames after the last read: one frame still + // shows the previous tick's sprites -- drawn while the CPU waits at + // $5F94, so over the picture this tick's work has already changed -- + // and the next shows this tick's. A read is the OR of both, so a + // sprite pulling away from a contact is still seen touching on the + // tick it leaves. Sprite-sprite contact does not depend on the + // picture and carries over from the last read; sprite-background + // contact is worked out again before the flush (stSimTick). + uint8_t prevFrameSpriteColl; + uint8_t prevFrameBgColl; // 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). @@ -571,8 +639,14 @@ typedef struct { // 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. +// Re-entry after a crash with cabs left ($5F27, where $6BCD jumps): spawn, +// refill, the second prelude hook and the jingle -- the fare machine and +// the hooks' first prelude carry on from where the crash left them. void stSimRespawn(StSimT *sim); +// $6B64 -- a finished wreck costs a fare aboard and a cab: LIFE_LOST +// (respawn with stSimRespawn) or PLAYER_OUT. What a tick returns in a +// real game; the attract demo returns CRASH_DONE instead. +StTickResultE stSimLoseCab(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). @@ -609,7 +683,7 @@ 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. +// $6C38 -- blank the 12-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); @@ -637,8 +711,10 @@ void stFareStageDispatch(StSimT *sim); // $65C1 void stFarePostTickGate(StSimT *sim); // $67E3 void stFarePadLightingGate(StSimT *sim); // $6EF0 void stFareUpPlease(StSimT *sim); // $6CB8 +void stFareEventVoice(StSimT *sim); // $61BD +void stFareTakeoffSetup(StSimT *sim); // $6F18 +void stFareTakeoffVoice(StSimT *sim); // $5F30 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 @@ -661,6 +737,10 @@ uint8_t stHookHitVerdict(StSimT *sim); // $7D75: 0 = passenger contact is // Seed a converted level's scratch from the shipped blob (scene load). void stHookSceneLoad(StSimT *sim); +// $42E9 -- start a 9-byte SFX program (stAudioSfx) and arm its voice's +// release timer. +void stSimSfx(StSimT *sim, const uint8_t *program9); + // ---- audio sink (host provides; stAudio.c / stsim stub) ---- void stAudioSfx(const uint8_t *program9); // $42E9 load void stAudioNoise(bool on); // $D412 = $81 / $80 diff --git a/tools/staxiharness/Makefile b/tools/staxiharness/Makefile index 1dad8ed..c898f25 100644 --- a/tools/staxiharness/Makefile +++ b/tools/staxiharness/Makefile @@ -31,7 +31,7 @@ $(OUT)/harness: harness.c $(REPO)/spacetaxi.c $(SRCS) $(CC) $(CFLAGS) harness.c $(SRCS) $(WRAPS) -o $@ # The title screen's level file is read through DATA/levels (the DOS bake -# is the cross-platform STL4 form), so run from a directory that has one. +# is the cross-platform STL5 form), so run from a directory that has one. run: $(OUT)/harness @test -f $(GEN)/levels/title.dat || { echo "staxiharness: no $(GEN)/levels/title.dat (build the game for DOS first)" >&2; exit 2; } @mkdir -p $(OUT)/DATA diff --git a/tools/stsim/c64trace.py b/tools/stsim/c64trace.py new file mode 100755 index 0000000..30ffa1a --- /dev/null +++ b/tools/stsim/c64trace.py @@ -0,0 +1,442 @@ +#!/usr/bin/env python3 +# c64trace.py -- ride a gate stream on the real game in VICE and print +# the trace stsim prints, line for line, so the two can be compared byte +# for byte (gate.sh c64). +# +# c64trace.py [rawOut] +# c64trace.py --prepare make the cached snapshot (below) and exit +# +# demobuf.json is stsim's STSIM_DEMOBUF line: the whole 640-byte playback +# buffer ($0902) as the sim starts the ride with it. rawOut, if given, +# receives each tick's screen RAM, colour RAM and charset in stsim's +# STSIM_RAW layout. +# +# How the ride is set up on the C64, all from the game's own attract demo: +# * The original disk boots once (the crack's intro is skipped at its +# key test, the trainer menu answered with START GAME, every cheat +# off) to the point the first attract demo picks its screen ($50C6); +# VICE's snapshot of that moment is cached in build/c64trace/. +# * Each ride restores the snapshot, makes every demo slot ($50C1.. +# $50C4) the wanted screen, and when the screen and its recording are +# loaded ($625F, the disk load is skipped) writes the sim's playback +# buffer over $0902, zeroes the flame parity, wave index and walk +# parity ($716C/$716E/$7161) that the title leaves behind -- the sim +# starts its rides from a fresh game -- and sets the screens-played +# count so that $6211's INC makes it the screen's place in the 24-hour +# shift (A = 1 .. Y = 25), as stsim's rides have it. +# * A finished wreck takes the real game's path ($6B52 -> $6B64: the cab +# count drops and the cab respawns at $5F27) instead of the demo's +# return to the title, as stsim's rides do. A ride ends when the cab +# leaves the screen ($6BFB, end 1), the cabs run out ($6BD0, end 4), +# the playback reaches the buffer's last pair (end "stream"), or after +# ticks. +# * Every $5F40 (the main loop's top) the state is read and printed. +# +# Needs VICE's x64sc (VICE=path, else on PATH; VICE_DATA=its ROM folder +# when x64sc cannot find its own), Xvfb, and the original disk image +# (STAXI_D64, default stuff/SPACETAX.D64). Exits 2 when one is missing. +# The disk and everything derived from it stay out of the repository. + +import json +import os +import shutil +import signal +import socket +import struct +import subprocess +import sys +import time + +HERE = os.path.dirname(os.path.abspath(__file__)) +REPO = os.path.normpath(os.path.join(HERE, '..', '..')) +CACHE = os.path.join(REPO, 'build', 'c64trace') +D64 = os.environ.get('STAXI_D64', os.path.join(REPO, 'stuff', 'SPACETAX.D64')) +LIFE_S = 900 # an emulator outlives no ride by more than this + +# VICE binary monitor protocol (src/monitor/monitor_binary.c). +STX = 0x02 +API_VERSION = 0x02 +CMD_MEM_GET = 0x01 +CMD_MEM_SET = 0x02 +CMD_CP_SET = 0x12 +CMD_CP_DELETE = 0x13 +CMD_REGS_SET = 0x32 +CMD_REGS_GET = 0x31 +CMD_DUMP = 0x41 +CMD_UNDUMP = 0x42 +CMD_KEYS = 0x72 +CMD_PING = 0x81 +CMD_BANKS = 0x82 +CMD_EXIT = 0xAA +CMD_QUIT = 0xBB +EV_CHECKPOINT = 0x11 +EV_JAM = 0x61 +EV_STOPPED = 0x62 +REG_PC = 0x03 +OP_EXEC = 0x04 + +# The game's addresses. +INTRO_KEY_TEST = 0x0994 # the crack's "space pressed?" test +INTRO_GO = 0x099E # ...and where it goes when it is +DEMO_PICK = 0x50C6 # $5010's demo branch: a demo picks its screen +DEMO_SLOTS = 0x50C1 # "HWTX", the demo rotation +DEMO_LOAD = 0x625F # JSR $98C4: load the screen's recording +DEMO_LOADED = 0x6262 # ...returned +DEMO_BUFFER = 0x0902 +CARRIED = (0x716C, 0x716E, 0x7161) +SCREENS_PLAYED = 0x7215 +TICK_TOP = 0x5F40 +CAB_LEFT = 0x6BFB +WRECK_DONE = 0x6B52 +LOSE_CAB = 0x6B64 +PLAYER_OUT = 0x6BD0 +LAST_PAIR = 638 # the playback buffer's last (mask, count) pair +END_EXIT = 1 # StTickResultE: ST_TICK_LEVEL_EXIT +END_OUT = 4 # ST_TICK_PLAYER_OUT +TRAINER_ROWS = 12 # cursor-downs from the first option to START GAME +KEY_DOWN = '\x11' +KEY_RETURN = '\x0d' +KEY_WAIT_S = 0.3 +INTRO_WAIT_S = 6 +BOOT_WAIT_S = 300 +STOP_WAIT_S = 120 + + +class Monitor: + """A VICE binary-monitor connection.""" + + def __init__(self, port): + self.sock = None + for _ in range(200): + try: + self.sock = socket.create_connection(('127.0.0.1', port), timeout=2) + break + except OSError: + time.sleep(0.1) + if self.sock is None: + raise RuntimeError('no VICE monitor on port %d' % port) + self.sock.settimeout(None) + self.reqId = 1 + self.events = [] + self.buf = b'' + + def recvExact(self, n): + while len(self.buf) < n: + chunk = self.sock.recv(1 << 20) + if not chunk: + raise EOFError('VICE closed the monitor') + self.buf += chunk + out, self.buf = self.buf[:n], self.buf[n:] + return out + + def packet(self): + hdr = self.recvExact(12) + length = struct.unpack_from('