More optimizing

This commit is contained in:
Scott Duensing 2026-10-13 13:56:35 -05:00
parent 786ff257c3
commit 57dab02e21
27 changed files with 3605 additions and 1856 deletions

View file

@ -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<digit>" |
| 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

View file

@ -3,7 +3,7 @@
# and program.mk describes the game to them.
#
# make every target whose toolchain is installed
# make <port> one target: iigs amiga atarist dos x68000 dvx
# make <port> 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

View file

@ -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

35
TODO.md
View file

@ -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.

View file

@ -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 |

View file

@ -9,12 +9,13 @@
# mkstlevel tool stays in tree for hand-authored .txt levels.
#
# Usage:
# romToLevel.py <raw-dump> <out-dir> # all 24 levels
# romToLevel.py <raw-dump> <out-dir> # all 25 screens + title
# romToLevel.py <raw-dump> <out-dir> <scene_id> # one specific
#
# Writes:
# <out-dir>/level01.dat ... level24.dat (scene 0 = "A" = level01)
# <out-dir>/tbank0.png (charset extracted from $2800)
# <out-dir>/level01.dat ... level25.dat (scene 0 = "A" = level01,
# scene 24 = "Y" = level25)
# <out-dir>/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__':

View file

@ -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

View file

@ -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)

View file

@ -8,6 +8,8 @@
// pad has been used the destination becomes "UP PLEASE" and the top
// hatch opens.
#include <string.h>
#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) {

View file

@ -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

2409
stHooks.c

File diff suppressed because it is too large Load diff

View file

@ -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 <stdio.h>

View file

@ -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;
}

View file

@ -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 <stdio.h>
#include <string.h>
@ -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)) {

View file

@ -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
}

463
stSim.c
View file

@ -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;

106
stSim.h
View file

@ -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

View file

@ -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

442
tools/stsim/c64trace.py Executable file
View file

@ -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 <level letter> <ticks> <demobuf.json> [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> 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('<I', hdr, 2)[0]
return hdr[6], hdr[7], struct.unpack_from('<I', hdr, 8)[0], self.recvExact(length)
def cmd(self, ctype, body=b''):
rid = self.reqId
self.reqId += 1
self.sock.sendall(struct.pack('<BBIIB', STX, API_VERSION, len(body), rid, ctype) + body)
while True:
rtype, err, r, b = self.packet()
if r == rid:
if err != 0:
raise RuntimeError('monitor command %02X failed (%02X)' % (ctype, err))
return b
self.events.append((rtype, b))
def mem(self, start, end, bank):
b = self.cmd(CMD_MEM_GET, struct.pack('<BHHBH', 0, start, end, 0, bank))
return b[2:2 + struct.unpack_from('<H', b, 0)[0]]
def poke(self, start, data, bank):
self.cmd(CMD_MEM_SET, struct.pack('<BHHBH', 0, start, start + len(data) - 1, 0, bank) + bytes(data))
def stopAt(self, addr):
b = self.cmd(CMD_CP_SET, struct.pack('<HHBBBB', addr, addr, 1, 1, OP_EXEC, 0))
return struct.unpack_from('<I', b, 0)[0]
def unstop(self, num):
self.cmd(CMD_CP_DELETE, struct.pack('<I', num))
def pc(self):
b = self.cmd(CMD_REGS_GET, b'\x00')
off = 2
for _ in range(struct.unpack_from('<H', b, 0)[0]):
if b[off + 1] == REG_PC:
return struct.unpack_from('<H', b, off + 2)[0]
off += b[off] + 1
raise RuntimeError('no PC in the register dump')
def setPc(self, value):
self.cmd(CMD_REGS_SET, struct.pack('<BH', 0, 1) + struct.pack('<BBH', 3, REG_PC, value))
def banks(self):
b = self.cmd(CMD_BANKS)
out = {}
off = 2
for _ in range(struct.unpack_from('<H', b, 0)[0]):
nameLen = b[off + 3]
out[b[off + 4:off + 4 + nameLen].decode()] = struct.unpack_from('<H', b, off + 1)[0]
off += b[off] + 1
return out
def go(self):
self.events = []
self.cmd(CMD_EXIT)
def halt(self):
self.cmd(CMD_PING)
def waitStop(self, timeout):
self.sock.settimeout(timeout)
try:
while True:
if self.events:
rtype, b = self.events.pop(0)
else:
rtype, _err, _rid, b = self.packet()
if rtype == EV_STOPPED:
return struct.unpack_from('<H', b, 0)[0]
if rtype == EV_JAM:
raise RuntimeError('the C64 jammed at $%04X' % struct.unpack_from('<H', b, 0)[0])
finally:
self.sock.settimeout(None)
def keys(self, text):
t = text.encode('latin-1')
self.cmd(CMD_KEYS, bytes([len(t)]) + t)
def snapshotSave(self, path):
p = path.encode()
self.cmd(CMD_DUMP, bytes([0, 0, len(p)]) + p)
def snapshotLoad(self, path):
p = path.encode()
self.cmd(CMD_UNDUMP, bytes([len(p)]) + p)
class Vice:
"""x64sc on a private Xvfb display, driven through its monitor."""
def __init__(self, autostart=None):
exe = os.environ.get('VICE') or shutil.which('x64sc')
if not exe or not shutil.which('Xvfb'):
raise FileNotFoundError('VICE (x64sc) or Xvfb not found')
self.procs = []
display = self.freeDisplay()
# Each in a session of its own, so close() can take down the
# timeout guard and what it runs together.
self.procs.append(subprocess.Popen(['timeout', '-s', 'KILL', str(LIFE_S), 'Xvfb', display, '-screen', '0', '640x480x24'],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True))
os.makedirs(os.path.join(CACHE, 'home'), exist_ok=True)
env = dict(os.environ, DISPLAY=display, XDG_CONFIG_HOME=os.path.join(CACHE, 'home'), XDG_CACHE_HOME=os.path.join(CACHE, 'home'))
port = self.freePort()
args = ['timeout', '-s', 'KILL', str(LIFE_S), exe, '-binarymonitor', '-binarymonitoraddress', 'ip4://127.0.0.1:%d' % port,
'-sounddev', 'dummy', '-warp', '+confirmonexit']
if os.environ.get('VICE_DATA'):
args += ['-directory', os.environ['VICE_DATA']]
if autostart:
args += ['-autostart-warp', '-autostart', autostart]
self.procs.append(subprocess.Popen(args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, env=env, start_new_session=True))
try:
self.mon = Monitor(port)
except BaseException:
self.close()
raise
@staticmethod
def freePort():
s = socket.socket()
s.bind(('127.0.0.1', 0))
port = s.getsockname()[1]
s.close()
return port
@staticmethod
def freeDisplay():
n = 100 + os.getpid() % 800
while os.path.exists('/tmp/.X%d-lock' % n):
n += 1
return ':%d' % n
def close(self):
try:
self.mon.cmd(CMD_QUIT)
except Exception:
pass
for p in reversed(self.procs):
try:
os.killpg(p.pid, signal.SIGKILL)
except ProcessLookupError:
pass
p.wait()
def fnv(data):
h = 2166136261
for b in data:
h = ((h ^ b) * 16777619) & 0xFFFFFFFF
return h
def demoOffset(mon, ram):
zp = mon.mem(0x003F, 0x0040, ram)
return ((zp[1] << 8) | zp[0]) - DEMO_BUFFER
def stateLine(mon, tick, ram, io, raw):
"""stsim's printState, from the C64's memory."""
scr = mon.mem(0x0400, 0x07E7, ram)
chs = mon.mem(0x2800, 0x2FFF, ram)
vic = mon.mem(0xD020, 0xD021, io)
col = bytes(b & 0x0F for b in mon.mem(0xD800, 0xDBE7, io))
g = mon.mem(0x7140, 0x71FF, ram)
p = mon.mem(0x7D5A, 0x7D9F, ram)
G = lambda a: g[a - 0x7140]
P = lambda a: p[a - 0x7D5A]
en = G(0x7196)
msb = G(0x718D)
if raw is not None:
raw.write(scr + col + chs)
# A hidden sprite's registers print as dashes, as stsim's do.
sprx = ''
for k in range(8):
x = G(0x71A7 + 2 * k) | (0x100 if (msb >> k) & 1 else 0)
sprx += '%03X%02X' % (x, G(0x71A8 + 2 * k)) if (en >> k) & 1 else '-----'
if en & 2:
spr1 = '"ptr1":"%02X","s1":"%02X%02X.%02X",' % (G(0x7198), G(0x7186), G(0x7176), G(0x717E))
else:
spr1 = '"ptr1":"--","s1":"--",'
s = ('{"tick":%u,"x":"%02X%02X.%02X","y":"%02X.%02X","vx":"%04X","vy":"%04X",'
'"pad":%u,"st":%u,"ph":%u,"in":"%02X","dir":"%02X","ptr0":"%02X",' % (
tick, P(0x7D63), P(0x7D62), P(0x7D61), P(0x7D65), P(0x7D64),
(G(0x714D) << 8) | G(0x714C), (G(0x714F) << 8) | G(0x714E),
G(0x7150), G(0x7163), G(0x7164), G(0x7169), G(0x716A), G(0x7197)))
s += spr1
s += ('"en":"%02X","t4740":"%02X","off":%u,"rng":"%02X%02X","fuel":%u,"slots":%u,"asi":%u,"score":"' % (
en, mon.mem(0x4740, 0x4740, ram)[0], demoOffset(mon, ram), G(0x7171), G(0x7172), G(0x71C9), G(0x715C), P(0x7D8E)))
s += ''.join('%02X' % scr[0x3C2 + k] for k in range(7))
s += '","fare":"' + ''.join('%02X' % scr[0x3E0 + k] for k in range(7))
s += '","fuelc":"' + ''.join('%02X' % scr[0x3A6 + k] for k in range(12))
s += '","sprx":"' + sprx
s += '","scr":"%08X","col":"%08X","chr":"%08X","bd":"%02X","bg":"%02X"}' % (
fnv(scr), fnv(col), fnv(chs), vic[0] & 0x0F, vic[1] & 0x0F)
return s
def snapshot():
"""The cached VICE snapshot of the first attract demo picking its
screen, made from the original disk on first use."""
path = os.path.join(CACHE, 'attract.vsf')
if os.path.exists(path):
return path
if not os.path.exists(D64):
raise FileNotFoundError('no disk image at %s (STAXI_D64)' % D64)
os.makedirs(CACHE, exist_ok=True)
vice = Vice(autostart=D64)
try:
mon = vice.mon
cp = mon.stopAt(INTRO_KEY_TEST)
mon.go()
mon.waitStop(BOOT_WAIT_S)
mon.unstop(cp)
mon.setPc(INTRO_GO)
mon.go()
time.sleep(INTRO_WAIT_S)
mon.halt()
for _ in range(TRAINER_ROWS):
mon.keys(KEY_DOWN)
mon.go()
time.sleep(KEY_WAIT_S)
mon.halt()
mon.keys(KEY_RETURN)
cp = mon.stopAt(DEMO_PICK)
mon.go()
mon.waitStop(BOOT_WAIT_S)
mon.unstop(cp)
mon.snapshotSave(path + '.part')
finally:
vice.close()
os.replace(path + '.part', path)
return path
def ride(letter, ticks, demobuf, out, raw):
vice = Vice()
try:
mon = vice.mon
mon.snapshotLoad(snapshot())
banks = mon.banks()
ram = banks['ram']
io = banks['io']
if mon.pc() != DEMO_PICK:
raise RuntimeError('the snapshot is not at $%04X' % DEMO_PICK)
mon.poke(DEMO_SLOTS, bytes([ord(letter)] * 4), ram)
cp = mon.stopAt(DEMO_LOAD)
mon.go()
mon.waitStop(STOP_WAIT_S)
mon.unstop(cp)
mon.poke(DEMO_BUFFER, demobuf, ram)
for a in CARRIED:
mon.poke(a, b'\x00', ram)
mon.poke(SCREENS_PLAYED, bytes([ord(letter) - ord('A')]), ram)
mon.setPc(DEMO_LOADED)
for a in (TICK_TOP, CAB_LEFT, WRECK_DONE, PLAYER_OUT):
mon.stopAt(a)
tick = 0
exited = False
while tick < ticks:
mon.go()
pc = mon.waitStop(STOP_WAIT_S)
if pc == TICK_TOP:
if demoOffset(mon, ram) >= LAST_PAIR:
out.write('{"tick":%u,"end":"stream"}\n' % (tick - 1))
break
out.write(stateLine(mon, tick, ram, io, raw) + '\n')
tick += 1
elif pc == CAB_LEFT:
exited = True
elif pc == WRECK_DONE:
if exited:
out.write('{"tick":%u,"end":%d}\n' % (tick - 1, END_EXIT))
break
mon.setPc(LOSE_CAB)
elif pc == PLAYER_OUT:
out.write('{"tick":%u,"end":%d}\n' % (tick - 1, END_OUT))
break
else:
raise RuntimeError('unexpected stop at $%04X' % pc)
finally:
vice.close()
def main():
if len(sys.argv) == 2 and sys.argv[1] == '--prepare':
try:
snapshot()
except FileNotFoundError as e:
print('c64trace: %s' % e, file=sys.stderr)
return 2
return 0
if len(sys.argv) < 4:
print('usage: c64trace.py <level letter> <ticks> <demobuf.json> [rawOut] | --prepare', file=sys.stderr)
return 2
letter = sys.argv[1]
ticks = int(sys.argv[2], 0)
demobuf = bytes.fromhex(json.load(open(sys.argv[3]))['demobuf'])
raw = open(sys.argv[4], 'wb') if len(sys.argv) > 4 else None
try:
ride(letter, ticks, demobuf, sys.stdout, raw)
except FileNotFoundError as e:
print('c64trace: %s' % e, file=sys.stderr)
return 2
finally:
if raw is not None:
raw.close()
return 0
if __name__ == '__main__':
sys.exit(main())

View file

@ -5,7 +5,7 @@
# sprites on the fly" hitch. Runs the same rides gate.sh does with the
# stsim census (STSIM_CELS=1) and fails on any MISSING line.
#
# celGate.sh check every hook level
# celGate.sh check every screen's ride
set -u
here=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)

View file

@ -1,79 +1,72 @@
#!/usr/bin/env python3
# Compare stsim output against a VICE trace segment.
# compare.py <trace.jsonl> <startTick> <stsim-output.jsonl> [maxReports]
import json, sys
# Compare two ride traces in stsim's line format -- stsim's against the
# real C64's (c64trace.py), or a golden against the current tree -- field
# by field.
#
# compare.py <a.jsonl> <b.jsonl> [maxReports]
# the first maxReports ticks that differ (default 12), with the
# fields that differ, then how often and from when each field does
# compare.py --picture <a.raw> <b.raw> <tick>
# the screen cells, colour cells and glyphs that differ on one tick,
# from the two machines' picture dumps (STSIM_RAW / c64trace's rawOut)
import json
import sys
trace = [json.loads(l) for l in open(sys.argv[1])]
start = int(sys.argv[2])
sim = [json.loads(l) for l in open(sys.argv[3])]
maxRep = int(sys.argv[4]) if len(sys.argv) > 4 else 12
SCREEN_CELLS = 1000
SCREEN_COLS = 40
CHARSET = 2048
GLYPH_BYTES = 8
RAW_BYTES = SCREEN_CELLS * 2 + CHARSET
def fromTrace(r):
g = bytes.fromhex(r['g7140'])
pos = bytes.fromhex(r['pos'])
hud = bytes.fromhex(r['hud']) # 0x0799 .. 0x07E6
def G(a): return g[a - 0x7140]
def H(a): return hud[a - 0x0799]
d = {}
d['x'] = '%02X%02X.%02X' % (pos[9], pos[8], pos[7]) # 7D63 7D62 7D61
d['y'] = '%02X.%02X' % (pos[11], pos[10])
d['vx'] = '%02X%02X' % (G(0x714D), G(0x714C))
d['vy'] = '%02X%02X' % (G(0x714F), G(0x714E))
d['pad'] = G(0x7150)
d['st'] = G(0x7163)
d['ph'] = G(0x7164)
d['in'] = '%02X' % G(0x7169)
d['dir'] = '%02X' % G(0x716A)
d['ptr0'] = '%02X' % G(0x7197)
d['ptr1'] = '%02X' % G(0x7198)
d['s1'] = '%02X%02X.%02X' % (G(0x7186), G(0x7176), G(0x717E))
d['en'] = '%02X' % G(0x7196)
d['t4740']= r['t4740'].upper()
d['rng'] = '%02X%02X' % (G(0x7171), G(0x7172))
d['fuel'] = G(0x71C9)
d['slots']= G(0x715C)
d['asi'] = pos[0x7D8E - 0x7D5A] if len(pos) > 0x7D8E - 0x7D5A else -1
d['score']= ''.join('%02X' % H(0x07C2 + k) for k in range(7))
d['fare'] = ''.join('%02X' % H(0x07E0 + k) for k in range(7))
d['fuelc']= ''.join('%02X' % H(0x07A6 + k) for k in range(12))
# sprite shadow positions $71A7.. interleaved X,Y for 8 sprites + msb $718D
msb = G(0x718D)
sx = ''
for k in range(8):
x = G(0x71A7 + 2*k) | (0x100 if (msb >> k) & 1 else 0)
sx += '%03X%02X' % (x, G(0x71A8 + 2*k))
d['sprx'] = sx
return d
fields = ['x','y','vx','vy','pad','st','ph','in','dir','ptr0','ptr1','s1','en','t4740','rng','fuel','slots','score','fare','fuelc','sprx']
reports = 0
firstBad = None
for s in sim:
if 'end' in s:
print('sim ended at tick', s['tick'], 'result', s['end'])
break
t = s['tick']
if start + t >= len(trace):
print('trace exhausted at sim tick', t); break
r = fromTrace(trace[start + t])
# Disabled sprites keep stale shadow values on the C64; only compare
# positions/pointers of enabled ones.
en = int(r['en'], 16)
def spr(sx):
return ''.join(sx[k*5:k*5+5] if (en >> k) & 1 else '-----' for k in range(8))
s2 = dict(s); r2 = dict(r)
s2['sprx'] = spr(s['sprx']); r2['sprx'] = spr(r['sprx'])
if not (en & 2):
s2['ptr1'] = r2['ptr1'] = '--'; s2['s1'] = r2['s1'] = '--'
bad = [f for f in fields if f in s2 and str(s2[f]) != str(r2[f])]
s = s2; r = r2
if bad:
if firstBad is None:
firstBad = t
if reports < maxRep:
print(f'tick {t}: ' + ' '.join(f'{f}: sim={s[f]} c64={r[f]}' for f in bad))
def traces(pathA, pathB, maxReports):
a = [json.loads(line) for line in open(pathA)]
b = [json.loads(line) for line in open(pathB)]
counts = {}
first = {}
reports = 0
for i in range(min(len(a), len(b))):
x, y = a[i], b[i]
fields = [k for k in x if x.get(k) != y.get(k)] + [k for k in y if k not in x]
for k in fields:
counts[k] = counts.get(k, 0) + 1
first.setdefault(k, i)
if fields and reports < maxReports:
print('line %d: ' % i + ' '.join('%s: %s vs %s' % (k, x.get(k), y.get(k)) for k in fields))
reports += 1
if firstBad is None:
print('ALL', len(sim), 'ticks match')
else:
print('first mismatch at tick', firstBad)
if len(a) != len(b):
print('lengths differ: %d vs %d lines' % (len(a), len(b)))
if not counts:
print('every common line matches' if len(a) != len(b) else 'identical')
return
print('fields that differ (lines, first line): ' + ', '.join('%s %d @%d' % (k, counts[k], first[k]) for k in sorted(counts, key=first.get)))
def picture(pathA, pathB, tick):
a = open(pathA, 'rb').read()[tick * RAW_BYTES:(tick + 1) * RAW_BYTES]
b = open(pathB, 'rb').read()[tick * RAW_BYTES:(tick + 1) * RAW_BYTES]
for i in range(SCREEN_CELLS):
if a[i] != b[i]:
print('screen row %d col %d: %02X vs %02X' % (i // SCREEN_COLS, i % SCREEN_COLS, a[i], b[i]))
for i in range(SCREEN_CELLS):
if a[SCREEN_CELLS + i] != b[SCREEN_CELLS + i]:
print('colour row %d col %d: %X vs %X' % (i // SCREEN_COLS, i % SCREEN_COLS, a[SCREEN_CELLS + i], b[SCREEN_CELLS + i]))
for ch in range(CHARSET // GLYPH_BYTES):
o = SCREEN_CELLS * 2 + ch * GLYPH_BYTES
if a[o:o + GLYPH_BYTES] != b[o:o + GLYPH_BYTES]:
print('glyph %02X: %s vs %s' % (ch, a[o:o + GLYPH_BYTES].hex(), b[o:o + GLYPH_BYTES].hex()))
def main():
if len(sys.argv) == 5 and sys.argv[1] == '--picture':
picture(sys.argv[2], sys.argv[3], int(sys.argv[4]))
return 0
if len(sys.argv) not in (3, 4):
print('usage: compare.py <a.jsonl> <b.jsonl> [maxReports] | --picture <a.raw> <b.raw> <tick>', file=sys.stderr)
return 2
traces(sys.argv[1], sys.argv[2], int(sys.argv[3]) if len(sys.argv) == 4 else 12)
return 0
if __name__ == '__main__':
sys.exit(main())

View file

@ -1,80 +1,155 @@
#!/usr/bin/env bash
# Behaviour gate for the Space Taxi simulation.
# Behaviour gate for the Space Taxi simulation: every screen's ride --
# A..X and the 25th, Y -- checked against the real C64.
#
# Runs every hook level through stsim and compares the per-tick trace
# against a golden capture. A refactor that changes nothing observable
# leaves all seventeen traces byte-identical; anything else is a regression.
# stsim rides a screen for up to 4000 ticks and prints one line of state
# a tick: the cab, the fare machine, the demo playback, the RNG, the HUD,
# the sprites shown and hashes of screen RAM, colour RAM and the charset.
# c64trace.py rides the same input on the original game in VICE and
# prints the same line from the C64's memory. gateHashes.txt holds the
# md5 of each C64 trace, so `check` -- which needs no emulator -- is a
# byte-for-byte comparison with the real machine.
#
# H, W, T and X ride the C64's own attract recordings (the ones checked
# against VICE traces). The other thirteen ride synthesised fixtures from
# gateStreams.h -- a regression gate only, not evidence of C64 fidelity.
# H, W, T and X ride the C64's own attract recordings; the other screens
# ride synthesised streams from gateStreams.h (tools/stsim/recorder).
#
# gate.sh capture [dir] record goldens from the CURRENT tree
# gate.sh check [dir] compare the current tree against them, or, with
# none captured, against gateHashes.txt (the md5
# of each golden trace, the committed reference)
# gate.sh hashes print one md5 per level (compact reference)
# gate.sh check [dir] every ride's md5 against gateHashes.txt; a ride
# that differs is diffed against its golden in dir,
# if one was captured, to show where
# gate.sh c64 [L...] ride the C64 too (VICE, c64trace.py) and compare
# the two traces byte for byte, showing the first
# fields that differ; every screen by default
# gate.sh c64hashes the C64 traces' md5s in gateHashes.txt's form --
# the only way gateHashes.txt is made
# gate.sh capture [dir] record goldens from the current tree
# gate.sh hashes print the current tree's md5 per screen
#
# Capture from a tree you trust, refactor, then check. The rides run on the
# game's level bake, so build the game first (gateLib.sh finds it).
# The rides run on the game's level bake, so build the game first
# (gateLib.sh finds it). The c64 modes keep their traces in
# build/c64trace/ and run C64_JOBS rides at once (default 4); they exit 2
# when VICE or the original disk is missing (see c64trace.py).
set -u
here=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
mode=${1:-check}
dir=${2:-$here/golden}
source "$here/gateLib.sh"
traces=$here/../../build/c64trace/traces
# simTrace L NN FILE: the current tree's ride.
simTrace() {
"$here/stsim" "$levels/level$2.dat" "$1" "$ticks" > "$3" 2>/dev/null
}
# c64Ride L NN: ride screen L on the C64 into $traces/L.jsonl and compare
# it with stsim's; prints one verdict line, returns 1 on a difference. A
# ride that could not be traced leaves $traces/L.failed behind.
c64Ride() {
local l=$1 n=$2
rm -f "$traces/$l.failed"
STSIM_DEMOBUF=1 "$here/stsim" "$levels/level$n.dat" "$l" 0 > "$traces/$l.buf.json"
simTrace "$l" "$n" "$traces/$l.sim.jsonl"
if ! timeout -s KILL 900 python3 "$here/c64trace.py" "$l" "$ticks" "$traces/$l.buf.json" > "$traces/$l.jsonl"; then
echo " $l: *** the C64 ride failed ***"
touch "$traces/$l.failed"
return 1
fi
if cmp -s "$traces/$l.sim.jsonl" "$traces/$l.jsonl"; then
printf ' %s: IDENTICAL to the C64 (%s lines)\n' "$l" "$(wc -l < "$traces/$l.jsonl")"
return 0
fi
echo " $l: *** DIFFERS from the C64 ***"
python3 "$here/compare.py" "$traces/$l.sim.jsonl" "$traces/$l.jsonl" 3 | sed 's/^/ /'
return 1
}
# c64All [L...]: every named screen (all of them by default), C64_JOBS at
# a time.
c64All() {
local want=" ${*:-} " fail=0 p l n
python3 "$here/c64trace.py" --prepare || exit $?
mkdir -p "$traces"
for p in $pairs; do
l=${p%%:*}; n=${p##*:}
if [ -n "${*:-}" ] && [[ "$want" != *" $l "* ]]; then
continue
fi
while [ "$(jobs -rp | wc -l)" -ge "${C64_JOBS:-4}" ]; do
wait -n || fail=1
done
c64Ride "$l" "$n" &
done
while [ "$(jobs -rp | wc -l)" -gt 0 ]; do
wait -n || fail=1
done
return $fail
}
case "$mode" in
capture)
dir=${2:-$here/golden}
mkdir -p "$dir"
for p in $pairs; do
l=${p%%:*}; n=${p##*:}
"$here/stsim" "$levels/level$n.dat" "$l" "$ticks" > "$dir/demo$l.jsonl" 2>/dev/null
printf ' %s: %s ticks\n' "$l" "$(wc -l < "$dir/demo$l.jsonl")"
simTrace "$l" "$n" "$dir/demo$l.jsonl"
printf ' %s: %s lines\n' "$l" "$(wc -l < "$dir/demo$l.jsonl")"
done
echo "gate: captured to $dir"
;;
check)
dir=${2:-$here/golden}
fail=0
for p in $pairs; do
l=${p%%:*}; n=${p##*:}
got=$(mktemp)
"$here/stsim" "$levels/level$n.dat" "$l" "$ticks" > "$got" 2>/dev/null
if [ -f "$dir/demo$l.jsonl" ]; then
if cmp -s "$dir/demo$l.jsonl" "$got"; then
printf ' %s: IDENTICAL (%s ticks)\n' "$l" "$(wc -l < "$got")"
else
echo " $l: *** DIFFERS ***"
diff "$dir/demo$l.jsonl" "$got" | head -3
fail=1
fi
simTrace "$l" "$n" "$got"
want=$(awk -v l="$l" '$1 == l { print $2 }' "$here/gateHashes.txt")
have=$(md5sum < "$got" | cut -d' ' -f1)
if [ "$have" = "$want" ]; then
printf ' %s: IDENTICAL to the C64 (%s lines, md5 as gateHashes.txt)\n' "$l" "$(wc -l < "$got")"
else
want=$(awk -v l="$l" '$1 == l { print $2 }' "$here/gateHashes.txt")
have=$(md5sum < "$got" | cut -d' ' -f1)
if [ "$have" = "$want" ]; then
printf ' %s: IDENTICAL (%s ticks, md5 as gateHashes.txt)\n' "$l" "$(wc -l < "$got")"
else
echo " $l: *** DIFFERS *** (md5 $have, gateHashes.txt ${want:-has none}; capture goldens from a trusted tree to see where)"
fail=1
echo " $l: *** DIFFERS from the C64 *** (md5 $have, gateHashes.txt ${want:-has none}; gate.sh c64 $l shows where)"
if [ -f "$dir/demo$l.jsonl" ]; then
python3 "$here/compare.py" "$dir/demo$l.jsonl" "$got" 3 | sed 's/^/ golden vs now: /'
fi
fail=1
fi
rm -f "$got"
done
[ $fail -eq 0 ] && echo "gate: PASS" || echo "gate: FAIL"
# The cel census rides the same recordings: a cel a ride displays that
# its manifest does not bake is a mid-ride interpreted sprite.
# The cel census rides the same streams: a cel a ride displays that its
# manifest does not bake is a mid-ride interpreted sprite.
"$here/celGate.sh" || fail=1
exit $fail
;;
c64)
shift
c64All "$@"
fail=$?
[ $fail -eq 0 ] && echo "gate c64: PASS" || echo "gate c64: FAIL"
exit $fail
;;
c64hashes)
c64All >&2
if compgen -G "$traces/*.failed" > /dev/null; then
echo "gate c64hashes: a C64 ride failed; no hashes" >&2
exit 1
fi
for p in $pairs; do
l=${p%%:*}
printf '%s %s\n' "$l" "$(md5sum < "$traces/$l.jsonl" | cut -d' ' -f1)"
done
;;
hashes)
for p in $pairs; do
l=${p%%:*}; n=${p##*:}
h=$("$here/stsim" "$levels/level$n.dat" "$l" "$ticks" 2>/dev/null | md5sum | cut -d' ' -f1)
printf '%s %s\n' "$l" "$h"
got=$(mktemp)
simTrace "$l" "$n" "$got"
printf '%s %s\n' "$l" "$(md5sum < "$got" | cut -d' ' -f1)"
rm -f "$got"
done
;;
*)
sed -n '2,20p' "$0"
sed -n '2,33p' "$0"
exit 2
;;
esac

View file

@ -1,21 +1,31 @@
# Space Taxi behaviour gate -- md5 of each level's 4000-tick trace.
# Regenerate with: tools/stsim/gate.sh hashes
# H/W/T/X ride the C64's attract recordings; the rest ride
# synthesised fixtures from gateStreams.h (regression gate only).
H 760501b0e48f3922bfb0f23fd2831935
T c0b3f9e9a74e0d526703cf9b1875557b
W 92226455989e8326577a6ea20df8c8ea
X 87251d616a1be6e0c9c9317987b0fe20
G 73530651372c60042f1f46b5c0cbada0
I 7fe4415429d2b72a50884424269cb685
K 567be2ed37219356a055866459971345
O 02455280933e915e57803a4059aaefd9
Q a7732866d54ed06d52e5dc7406b5b229
R 066a9be80107446893b4571d247ee5cc
U 02dd2f26245442ab016ef02e10fde611
V cc2cb13c5e393d07fbece9c0c465e387
E 21ff29d69d8318bcb2e870b9602e910e
J 4c22a7515949b1701ed4fedabb7823dd
L a0383cbde2800bb09353df15214934ef
P f345d0adc9e011662d582c4277ee663e
S daa3cd12320e32d3e9b6b97b19b0a6d0
# Space Taxi behaviour gate -- the md5 of each screen's ride as the REAL
# C64 traced it: the original game in VICE, ridden by c64trace.py, one
# line of state per tick (stsim prints the same line from the port).
# `gate.sh check` compares the port's rides against these, so a pass means
# byte-identical to the C64 on all 25 screens.
# Regenerate (needs VICE and the original disk): tools/stsim/gate.sh c64hashes
A e5cc75d60b41a3fb6ad70a8475a21a7a
B 4348bfdde94d3fc6d28d1ee7ca4e73bb
C 6669b71a3cdeebcbab62e1a29eda5faa
D 2df4ea48591d8fc7bafe20638a1eaf00
E 7595460b2296bd863b740060547bdc3d
F 96d1f6589336430399e4fd04941b079d
G 9f07d6ed41c634ba8acf7d0162fca172
H 5682e029bbf147d429bba1f774deca13
I ecb6dbe5b9765bbf3c0bf4638dc9f72f
J 171cfb357d771abd7db4b6d3ac3b4402
K d7bc29828c42cda46f66485d2252bb4c
L 05a2098c950e76c78799227fa748b1d3
M 77fe51edf453736f7000f91ad9c52d19
N 1e7fd970699f408b7e6ea246882fa778
O 0af4d661248ae1e0bf57eaa24973051a
P 740e45a7fe08cfde34d9ee6b6d35c116
Q d4da1893385a16b09f8ea383e7d2c4ce
R 5089cfb129fc29b3c6ebffa2dd2f6923
S 561f7a0c757e82118331dbc8dd582214
T 7223a56296bd4f0023fcc3073afaf955
U b4178bc4c5aef13d4e87a006ed0fab75
V 69976c2e32a03448d8f3adf7a2fe2df8
W 5c0b20833ede9e848660bb98a67be208
X f3182a3975c023b961c1eee85bcab687
Y 67f5ace2d1688717de126b313c436149

View file

@ -1,18 +1,19 @@
# Sourced by gate.sh and celGate.sh, which set $here first: the rides both
# gates replay, and the levels they replay them on.
# Every hook level as letter:level file number, and how long each ride runs.
# Every screen as letter:level file number -- A..X and the 25th, Y -- and
# how long a ride may run.
ticks=4000
pairs="H:08 T:20 W:23 X:24 G:07 I:09 K:11 O:15 Q:17 R:18 U:21 V:22 E:05 J:10 L:12 P:16 S:19"
pairs="A:01 B:02 C:03 D:04 E:05 F:06 G:07 H:08 I:09 J:10 K:11 L:12 M:13 N:14 O:15 P:16 Q:17 R:18 S:19 T:20 U:21 V:22 W:23 X:24 Y:25"
# The game's own level bake (program.mk). It is the same STL4 bytes for every
# The game's own level bake (program.mk). It is the same STL5 bytes for every
# target, so whichever target was built first will do; STSIM_LEVELS names
# another folder.
levels=${STSIM_LEVELS:-}
if [ -z "$levels" ]; then
for port in iigs dos amiga atarist x68000; do
if [ -f "$here/../../build/generated/$port/levels/level01.dat" ]; then
levels=$(cd "$here/../../build/generated/$port/levels" && pwd)
for bake in "$here"/../../build/generated/*/levels; do
if [ -f "$bake/level25.dat" ]; then
levels=$(cd "$bake" && pwd)
break
fi
done

View file

@ -1,89 +1,313 @@
// Synthesised demo input streams -- a TEST FIXTURE, not game content.
//
// The C64 ships attract recordings for four screens only (H, W, T and X);
// those live in stDemoStreams.h and are the streams
// checked against VICE traces. The thirteen streams below were searched for
// by tools/stsim's recorder against this port's own simulation, scored on
// how long the cab survives and how much of the level's hook state, fare
// machine and screen animation the ride reaches.
// The C64 ships attract recordings for four screens only (H, W, T and X;
// stDemoStreams.h). The streams below cover the other twenty-one: each was
// searched for by tools/stsim's recorder against this port's simulation,
// scored on how long the cab survives and how much of the level's hook
// state, fare machine and screen animation the ride reaches.
//
// So they prove one thing and not another: they are a REGRESSION gate --
// refactor the hooks and the tick-by-tick trace must not move -- but they
// are NOT evidence that a hook matches the original, because no C64 trace
// exists for these screens. Treat a change to these bytes as invalidating
// the goldens, and never put them in the attract rotation.
// Every ride, these and the four recordings alike, is then ridden on the
// original game in VICE (gate.sh c64, c64trace.py) and must trace byte
// for byte as the simulation does; gateHashes.txt holds the C64's traces.
// A change to these bytes therefore needs `gate.sh c64hashes` on a
// machine with VICE. Never put them in the attract rotation.
#ifndef ST_GATE_STREAMS_H
#define ST_GATE_STREAMS_H
#include <stdint.h>
// level G -- 1400 ticks, 5 hook bytes, 10 fare stages, 256 pictures, 13 landings
// regenerate: tools/stsim/recorder levels/level??.dat G 20260921
// level A -- 1397 ticks, 0 hook bytes, 8 fare stages, 256 pictures, 8 landings, left through the hatch
// regenerate: tools/stsim/recorder levels/level01.dat A 20261006 60 2000
static const uint8_t kGateStreamA[320] = {
0x88, 0x06, 0x82, 0x0B, 0x84, 0x0E, 0x90, 0x06, 0x84, 0x0B, 0x82, 0x08,
0x88, 0x0C, 0x84, 0x0B, 0x81, 0x0E, 0x90, 0x0E, 0x90, 0x0E, 0x84, 0x0D,
0x81, 0x0C, 0x84, 0x01, 0x88, 0x01, 0x82, 0x03, 0x81, 0x01, 0x88, 0x0C,
0x81, 0x01, 0x88, 0x0C, 0x90, 0x0D, 0x81, 0x01, 0x81, 0x0E, 0x81, 0x0D,
0x81, 0x0C, 0x82, 0x09, 0x84, 0x09, 0x90, 0x0E, 0x81, 0x01, 0x81, 0x04,
0x82, 0x0B, 0x81, 0x0D, 0x81, 0x03, 0x88, 0x01, 0x84, 0x06, 0x82, 0x0D,
0x81, 0x0A, 0x81, 0x08, 0x90, 0x0C, 0x90, 0x0D, 0x84, 0x0E, 0x84, 0x07,
0x90, 0x0B, 0x88, 0x0C, 0x82, 0x08, 0x81, 0x01, 0x81, 0x06, 0x90, 0x0D,
0x84, 0x02, 0x84, 0x0D, 0x88, 0x05, 0x82, 0x09, 0x81, 0x01, 0x88, 0x04,
0x84, 0x09, 0x88, 0x01, 0x81, 0x05, 0x88, 0x06, 0x81, 0x03, 0x81, 0x0A,
0x84, 0x06, 0x81, 0x05, 0x82, 0x01, 0x84, 0x0E, 0x84, 0x09, 0x90, 0x02,
0x81, 0x03, 0x82, 0x0A, 0x84, 0x06, 0x84, 0x0E, 0x82, 0x07, 0x88, 0x04,
0x90, 0x0D, 0x88, 0x09, 0x81, 0x04, 0x88, 0x0E, 0x81, 0x08, 0x88, 0x0C,
0x84, 0x0D, 0x81, 0x08, 0x84, 0x09, 0x82, 0x06, 0x88, 0x0E, 0x84, 0x06,
0x81, 0x04, 0x88, 0x0C, 0x84, 0x03, 0x88, 0x0E, 0x84, 0x09, 0x90, 0x0E,
0x82, 0x0E, 0x88, 0x0C, 0x82, 0x06, 0x81, 0x0C, 0x90, 0x0C, 0x81, 0x07,
0x82, 0x0E, 0x81, 0x04, 0x81, 0x07, 0x84, 0x0D, 0x84, 0x07, 0x81, 0x0C,
0x84, 0x03, 0x81, 0x05, 0x82, 0x0E, 0x90, 0x04, 0x90, 0x0A, 0x90, 0x0E,
0x81, 0x0D, 0x82, 0x08, 0x82, 0x0D, 0x88, 0x0D, 0x81, 0x09, 0x81, 0x0B,
0x81, 0x0C, 0x84, 0x0E, 0x81, 0x0D, 0x82, 0x0C, 0x82, 0x08, 0x90, 0x08,
0x81, 0x0B, 0x82, 0x09, 0x88, 0x0A, 0x88, 0x02, 0x82, 0x03, 0x90, 0x08,
0x81, 0x08, 0x82, 0x08, 0x81, 0x0A, 0x81, 0x06, 0x88, 0x06, 0x81, 0x04,
0x84, 0x01, 0x90, 0x0E, 0x81, 0x07, 0x84, 0x0D, 0x82, 0x08, 0x90, 0x06,
0x82, 0x0E, 0x90, 0x0D, 0x81, 0x03, 0x90, 0x0B, 0x82, 0x0A, 0x88, 0x0A,
0x84, 0x0C, 0x81, 0x03, 0x81, 0x01, 0x81, 0x0B, 0x88, 0x0E, 0x81, 0x07,
0x82, 0x04, 0x85, 0x02, 0x81, 0x0D, 0x81, 0x08, 0x84, 0x0D, 0x82, 0x04,
0x82, 0x0A, 0x81, 0x0C, 0x81, 0x07, 0x88, 0x0A,
};
// level B -- 1400 ticks, 0 hook bytes, 10 fare stages, 256 pictures, 6 landings
// regenerate: tools/stsim/recorder levels/level02.dat B 20261006 60 2000
static const uint8_t kGateStreamB[320] = {
0x81, 0x04, 0x88, 0x02, 0x89, 0x01, 0x89, 0x02, 0x81, 0x01, 0x88, 0x05,
0x81, 0x04, 0x84, 0x0D, 0x84, 0x06, 0x82, 0x0C, 0x84, 0x08, 0x88, 0x07,
0x88, 0x06, 0x81, 0x0B, 0x88, 0x0E, 0x82, 0x07, 0x88, 0x0D, 0x81, 0x07,
0x84, 0x06, 0x81, 0x03, 0x82, 0x0C, 0x88, 0x02, 0x84, 0x04, 0x84, 0x0E,
0x81, 0x02, 0x82, 0x09, 0x80, 0x0B, 0x84, 0x09, 0x88, 0x07, 0x90, 0x0B,
0x81, 0x0C, 0x81, 0x0A, 0x90, 0x0B, 0x81, 0x01, 0x81, 0x07, 0x92, 0x09,
0x81, 0x04, 0x82, 0x09, 0x81, 0x0E, 0x88, 0x09, 0x88, 0x0D, 0x81, 0x02,
0x82, 0x03, 0x84, 0x0A, 0x82, 0x05, 0x90, 0x0C, 0x81, 0x07, 0x81, 0x0D,
0x84, 0x05, 0x90, 0x03, 0x88, 0x0D, 0x82, 0x0C, 0x88, 0x07, 0x90, 0x0E,
0x81, 0x05, 0x90, 0x02, 0x88, 0x06, 0x82, 0x0B, 0x81, 0x05, 0x88, 0x09,
0x82, 0x06, 0x84, 0x07, 0x84, 0x01, 0x88, 0x07, 0x81, 0x0B, 0x90, 0x0C,
0x82, 0x0D, 0x88, 0x07, 0x81, 0x05, 0x88, 0x0C, 0x84, 0x0E, 0x90, 0x0A,
0x84, 0x08, 0x90, 0x09, 0x81, 0x07, 0x84, 0x03, 0x82, 0x02, 0x81, 0x03,
0x88, 0x06, 0x90, 0x08, 0x90, 0x01, 0x81, 0x03, 0x82, 0x0C, 0x81, 0x05,
0x88, 0x05, 0x84, 0x07, 0x84, 0x08, 0x90, 0x09, 0x88, 0x0D, 0x81, 0x0D,
0x90, 0x0C, 0x88, 0x02, 0x88, 0x0E, 0x81, 0x04, 0x81, 0x0E, 0x82, 0x0C,
0x82, 0x0A, 0x90, 0x07, 0x81, 0x06, 0x82, 0x0E, 0x82, 0x01, 0x81, 0x07,
0x90, 0x0D, 0x90, 0x04, 0x81, 0x09, 0x84, 0x0D, 0x84, 0x08, 0x81, 0x07,
0x88, 0x03, 0x84, 0x01, 0x81, 0x01, 0x88, 0x07, 0x88, 0x0C, 0x82, 0x09,
0x81, 0x03, 0x81, 0x03, 0x88, 0x0D, 0x91, 0x01, 0x84, 0x0C, 0x82, 0x08,
0x81, 0x03, 0x90, 0x0C, 0x88, 0x0D, 0x82, 0x06, 0x82, 0x01, 0x81, 0x07,
0x90, 0x06, 0x84, 0x04, 0x90, 0x0A, 0x81, 0x06, 0x88, 0x0E, 0x90, 0x03,
0x81, 0x03, 0x90, 0x0D, 0x84, 0x0C, 0x88, 0x0E, 0x81, 0x04, 0x90, 0x0B,
0x88, 0x0B, 0x82, 0x08, 0x81, 0x0C, 0x81, 0x0B, 0x81, 0x02, 0x84, 0x08,
0x84, 0x04, 0x82, 0x0E, 0x84, 0x04, 0x81, 0x05, 0x90, 0x0C, 0x90, 0x08,
0x8A, 0x0D, 0x88, 0x06, 0x81, 0x01, 0x81, 0x04, 0x81, 0x01, 0x90, 0x0A,
0x88, 0x0E, 0x82, 0x07, 0x81, 0x03, 0x82, 0x04,
};
// level C -- 1400 ticks, 0 hook bytes, 10 fare stages, 256 pictures, 24 landings
// regenerate: tools/stsim/recorder levels/level03.dat C 20261006 60 2000
static const uint8_t kGateStreamC[320] = {
0x81, 0x04, 0x84, 0x01, 0x84, 0x09, 0x89, 0x03, 0x90, 0x05, 0x84, 0x0D,
0x81, 0x04, 0x88, 0x0E, 0x88, 0x0A, 0x82, 0x0C, 0x84, 0x02, 0x84, 0x07,
0x81, 0x06, 0x81, 0x0B, 0x88, 0x08, 0x82, 0x07, 0x84, 0x0A, 0x81, 0x0D,
0x84, 0x0B, 0x81, 0x02, 0x82, 0x0C, 0x88, 0x0E, 0x84, 0x0B, 0x88, 0x0E,
0x81, 0x0C, 0x81, 0x08, 0x81, 0x02, 0x90, 0x0E, 0x81, 0x08, 0x90, 0x0E,
0x84, 0x02, 0x81, 0x03, 0x84, 0x0E, 0x88, 0x04, 0x84, 0x06, 0x90, 0x06,
0x81, 0x01, 0x90, 0x09, 0x81, 0x05, 0x88, 0x09, 0x84, 0x0E, 0x90, 0x05,
0x90, 0x04, 0x82, 0x06, 0x84, 0x0D, 0x81, 0x09, 0x82, 0x07, 0x90, 0x0E,
0x88, 0x0C, 0x84, 0x0E, 0x81, 0x0E, 0x81, 0x0B, 0x84, 0x0B, 0x81, 0x01,
0x88, 0x0B, 0x81, 0x06, 0x88, 0x0B, 0x82, 0x08, 0x88, 0x04, 0x81, 0x09,
0x81, 0x02, 0x81, 0x08, 0x84, 0x06, 0x81, 0x0A, 0x90, 0x07, 0x84, 0x01,
0x82, 0x0D, 0x88, 0x0D, 0x82, 0x06, 0x88, 0x09, 0x88, 0x0A, 0x84, 0x05,
0x84, 0x0E, 0x81, 0x01, 0x88, 0x03, 0x84, 0x09, 0x88, 0x0C, 0x81, 0x0A,
0x82, 0x0A, 0x90, 0x0A, 0x90, 0x0D, 0x81, 0x03, 0x90, 0x0B, 0x82, 0x03,
0x84, 0x03, 0x84, 0x01, 0x81, 0x02, 0x90, 0x01, 0x84, 0x0C, 0x81, 0x01,
0x90, 0x09, 0x81, 0x0B, 0x82, 0x05, 0x82, 0x0D, 0x90, 0x0D, 0x82, 0x0A,
0x90, 0x07, 0x81, 0x03, 0x90, 0x0B, 0x88, 0x02, 0x84, 0x07, 0x84, 0x0E,
0x88, 0x07, 0x88, 0x0D, 0x81, 0x02, 0x88, 0x0B, 0x90, 0x07, 0x81, 0x01,
0x84, 0x0A, 0x90, 0x08, 0x84, 0x08, 0x90, 0x08, 0x81, 0x0A, 0x90, 0x04,
0x88, 0x0B, 0x82, 0x09, 0x84, 0x0E, 0x81, 0x06, 0x84, 0x04, 0x88, 0x0A,
0x81, 0x0B, 0x81, 0x02, 0x88, 0x0C, 0x82, 0x0E, 0x84, 0x07, 0x82, 0x05,
0x88, 0x0A, 0x82, 0x09, 0x81, 0x04, 0x90, 0x03, 0x84, 0x0A, 0x82, 0x0B,
0x81, 0x08, 0x90, 0x06, 0x82, 0x0C, 0x82, 0x03, 0x84, 0x08, 0x81, 0x07,
0x90, 0x0E, 0x82, 0x0C, 0x81, 0x0E, 0x88, 0x09, 0x81, 0x01, 0x90, 0x0E,
0x90, 0x0A, 0x88, 0x09, 0x81, 0x01, 0x90, 0x06, 0x90, 0x09, 0x81, 0x02,
0x90, 0x0E, 0x81, 0x02, 0x90, 0x0C, 0x84, 0x06, 0x84, 0x09, 0x81, 0x03,
0x90, 0x0D, 0x82, 0x0C, 0x81, 0x06, 0x81, 0x0D,
};
// level D -- 1400 ticks, 0 hook bytes, 10 fare stages, 256 pictures, 9 landings
// regenerate: tools/stsim/recorder levels/level04.dat D 20261006 60 2000
static const uint8_t kGateStreamD[320] = {
0x81, 0x04, 0x82, 0x02, 0x81, 0x01, 0x90, 0x02, 0x84, 0x01, 0x88, 0x05,
0x81, 0x04, 0x84, 0x0B, 0x81, 0x0E, 0x84, 0x07, 0x81, 0x0E, 0x82, 0x0D,
0x81, 0x05, 0x81, 0x01, 0x82, 0x01, 0x88, 0x03, 0x84, 0x0C, 0x88, 0x07,
0x88, 0x07, 0x84, 0x06, 0x81, 0x04, 0x81, 0x03, 0x88, 0x06, 0x84, 0x03,
0x81, 0x06, 0x84, 0x0A, 0x81, 0x0D, 0x84, 0x0E, 0x81, 0x0B, 0x81, 0x07,
0x82, 0x06, 0x81, 0x04, 0x84, 0x09, 0x90, 0x06, 0x88, 0x07, 0x80, 0x01,
0x84, 0x0D, 0x84, 0x09, 0x88, 0x0A, 0x90, 0x08, 0x88, 0x05, 0x81, 0x0E,
0x84, 0x0E, 0x84, 0x0E, 0x81, 0x02, 0x90, 0x0C, 0x90, 0x0D, 0x84, 0x06,
0x84, 0x01, 0x88, 0x03, 0x90, 0x09, 0x82, 0x08, 0x85, 0x05, 0x81, 0x04,
0x81, 0x06, 0x81, 0x01, 0x81, 0x05, 0x88, 0x06, 0x84, 0x0A, 0x81, 0x0A,
0x88, 0x01, 0x81, 0x02, 0x82, 0x0B, 0x84, 0x03, 0x90, 0x0B, 0x81, 0x03,
0x84, 0x03, 0x82, 0x03, 0x84, 0x06, 0x88, 0x0E, 0x84, 0x0B, 0x81, 0x01,
0x81, 0x02, 0x81, 0x09, 0x81, 0x04, 0x88, 0x06, 0x81, 0x04, 0x88, 0x0D,
0x81, 0x0D, 0x81, 0x08, 0x88, 0x06, 0x82, 0x0E, 0x81, 0x0E, 0x84, 0x02,
0x81, 0x01, 0x84, 0x0B, 0x81, 0x0E, 0x81, 0x0A, 0x83, 0x07, 0x90, 0x0B,
0x84, 0x0A, 0x88, 0x0D, 0x90, 0x03, 0x88, 0x0B, 0x82, 0x0B, 0x88, 0x01,
0x80, 0x0B, 0x81, 0x07, 0x81, 0x07, 0x81, 0x0C, 0x81, 0x03, 0x90, 0x0C,
0x88, 0x0B, 0x81, 0x0E, 0x82, 0x01, 0x81, 0x0C, 0x81, 0x0E, 0x81, 0x07,
0x90, 0x0C, 0x90, 0x01, 0x88, 0x03, 0x88, 0x03, 0x88, 0x07, 0x81, 0x02,
0x88, 0x06, 0x90, 0x0E, 0x88, 0x08, 0x81, 0x04, 0x81, 0x0D, 0x84, 0x07,
0x90, 0x01, 0x82, 0x07, 0x81, 0x05, 0x81, 0x01, 0x84, 0x0B, 0x84, 0x01,
0x81, 0x02, 0x90, 0x0E, 0x82, 0x02, 0x81, 0x01, 0x84, 0x02, 0x81, 0x01,
0x81, 0x09, 0x81, 0x0D, 0x84, 0x05, 0x8A, 0x0E, 0x81, 0x08, 0x85, 0x0B,
0x81, 0x02, 0x81, 0x0D, 0x90, 0x02, 0x82, 0x0D, 0x84, 0x02, 0x82, 0x06,
0x81, 0x0D, 0x82, 0x03, 0x84, 0x04, 0x81, 0x05, 0x90, 0x0E, 0x81, 0x08,
0x8A, 0x0D, 0x88, 0x0B, 0x90, 0x04, 0x81, 0x06, 0x84, 0x09, 0x81, 0x08,
0x88, 0x0E, 0x80, 0x07, 0x82, 0x05, 0x90, 0x0C,
};
// level E -- 1400 ticks, 4 hook bytes, 10 fare stages, 256 pictures, 3 landings
// regenerate: tools/stsim/recorder levels/level05.dat E 20261006 60 2000
static const uint8_t kGateStreamE[320] = {
0x88, 0x04, 0x81, 0x02, 0x84, 0x01, 0x90, 0x02, 0x88, 0x01, 0x84, 0x08,
0x81, 0x04, 0x88, 0x0B, 0x81, 0x0E, 0x88, 0x07, 0x81, 0x0E, 0x82, 0x0D,
0x81, 0x05, 0x81, 0x02, 0x84, 0x01, 0x84, 0x03, 0x84, 0x0E, 0x88, 0x08,
0x88, 0x07, 0x84, 0x0D, 0x81, 0x07, 0x81, 0x03, 0x84, 0x0B, 0x84, 0x02,
0x81, 0x06, 0x88, 0x09, 0x81, 0x0C, 0x84, 0x0B, 0x81, 0x0B, 0x81, 0x07,
0x82, 0x06, 0x81, 0x04, 0x84, 0x09, 0x84, 0x04, 0x88, 0x05, 0x90, 0x04,
0x84, 0x0D, 0x84, 0x04, 0x88, 0x0C, 0x90, 0x07, 0x81, 0x09, 0x90, 0x0B,
0x81, 0x0A, 0x88, 0x0C, 0x88, 0x02, 0x81, 0x01, 0x81, 0x06, 0x84, 0x0D,
0x90, 0x0E, 0x84, 0x05, 0x84, 0x03, 0x81, 0x09, 0x81, 0x02, 0x81, 0x04,
0x81, 0x0C, 0x81, 0x01, 0x90, 0x05, 0x91, 0x0B, 0x84, 0x0A, 0x81, 0x0A,
0x81, 0x01, 0x81, 0x02, 0x82, 0x01, 0x84, 0x0C, 0x90, 0x0E, 0x84, 0x04,
0x88, 0x0E, 0x82, 0x03, 0x88, 0x02, 0x88, 0x0E, 0x82, 0x0B, 0x82, 0x01,
0x90, 0x02, 0x88, 0x09, 0x81, 0x0B, 0x88, 0x07, 0x81, 0x04, 0x88, 0x05,
0x81, 0x0D, 0x81, 0x0C, 0x88, 0x04, 0x82, 0x0D, 0x81, 0x0E, 0x88, 0x06,
0x81, 0x01, 0x84, 0x0C, 0x81, 0x0E, 0x81, 0x0A, 0x82, 0x07, 0x82, 0x0A,
0x84, 0x02, 0x90, 0x0B, 0x81, 0x05, 0x84, 0x0B, 0x82, 0x03, 0x81, 0x06,
0x84, 0x0D, 0x81, 0x0C, 0x81, 0x05, 0x81, 0x06, 0x88, 0x0E, 0x84, 0x0A,
0x90, 0x0A, 0x81, 0x0A, 0x90, 0x05, 0x82, 0x05, 0x84, 0x06, 0x84, 0x06,
0x81, 0x0B, 0x81, 0x05, 0x88, 0x0E, 0x84, 0x0E, 0x81, 0x0E, 0x81, 0x0C,
0x81, 0x05, 0x90, 0x0E, 0x81, 0x0A, 0x88, 0x0C, 0x8A, 0x05, 0x84, 0x03,
0x81, 0x04, 0x88, 0x02, 0x80, 0x06, 0x84, 0x05, 0x88, 0x06, 0x84, 0x07,
0x82, 0x0B, 0x82, 0x07, 0x82, 0x07, 0x89, 0x05, 0x88, 0x02, 0x81, 0x04,
0x81, 0x01, 0x88, 0x09, 0x81, 0x0B, 0x84, 0x01, 0x81, 0x0D, 0x84, 0x01,
0x81, 0x0C, 0x84, 0x0E, 0x88, 0x09, 0x81, 0x08, 0x90, 0x09, 0x82, 0x09,
0x81, 0x05, 0x84, 0x0C, 0x81, 0x0B, 0x81, 0x0C, 0x90, 0x09, 0x82, 0x0E,
0x81, 0x0E, 0x82, 0x09, 0x81, 0x04, 0x81, 0x02, 0x88, 0x08, 0x84, 0x06,
0x90, 0x06, 0x81, 0x09, 0x82, 0x06, 0x81, 0x01,
};
// level F -- 1400 ticks, 8 hook bytes, 10 fare stages, 256 pictures, 5 landings
// regenerate: tools/stsim/recorder levels/level06.dat F 20261006 60 2000
static const uint8_t kGateStreamF[320] = {
0x81, 0x02, 0x88, 0x02, 0x84, 0x09, 0x89, 0x03, 0x81, 0x01, 0x82, 0x05,
0x81, 0x04, 0x80, 0x0D, 0x84, 0x08, 0x82, 0x05, 0x84, 0x09, 0x88, 0x07,
0x88, 0x06, 0x81, 0x0A, 0x90, 0x0E, 0x82, 0x07, 0x81, 0x07, 0x88, 0x0E,
0x88, 0x0B, 0x88, 0x08, 0x82, 0x0C, 0x84, 0x0B, 0x81, 0x08, 0x81, 0x0B,
0x90, 0x02, 0x88, 0x0E, 0x88, 0x0E, 0x90, 0x04, 0x81, 0x05, 0x84, 0x06,
0x88, 0x01, 0x88, 0x03, 0x81, 0x01, 0x88, 0x04, 0x81, 0x0E, 0x88, 0x04,
0x88, 0x08, 0x82, 0x08, 0x84, 0x0E, 0x88, 0x04, 0x90, 0x0E, 0x81, 0x0C,
0x90, 0x06, 0x84, 0x0B, 0x82, 0x06, 0x88, 0x03, 0x81, 0x01, 0x81, 0x05,
0x81, 0x03, 0x81, 0x06, 0x88, 0x0A, 0x88, 0x01, 0x90, 0x0C, 0x88, 0x02,
0x81, 0x06, 0x81, 0x03, 0x81, 0x03, 0x88, 0x02, 0x82, 0x0B, 0x90, 0x09,
0x88, 0x08, 0x84, 0x0E, 0x90, 0x01, 0x84, 0x0D, 0x81, 0x08, 0x88, 0x05,
0x90, 0x07, 0x81, 0x08, 0x84, 0x05, 0x82, 0x07, 0x90, 0x02, 0x90, 0x06,
0x81, 0x01, 0x82, 0x03, 0x88, 0x09, 0x88, 0x07, 0x82, 0x0D, 0x84, 0x08,
0x81, 0x0B, 0x81, 0x01, 0x81, 0x0E, 0x81, 0x0D, 0x81, 0x0B, 0x82, 0x0C,
0x84, 0x0A, 0x82, 0x0E, 0x88, 0x01, 0x81, 0x05, 0x90, 0x0E, 0x81, 0x06,
0x82, 0x09, 0x90, 0x06, 0x81, 0x09, 0x81, 0x03, 0x82, 0x04, 0x82, 0x0D,
0x84, 0x0E, 0x88, 0x08, 0x88, 0x07, 0x81, 0x0B, 0x81, 0x0A, 0x81, 0x05,
0x84, 0x0A, 0x88, 0x0A, 0x84, 0x0D, 0x82, 0x07, 0x81, 0x08, 0x82, 0x0B,
0x81, 0x08, 0x88, 0x0D, 0x88, 0x0B, 0x88, 0x06, 0x84, 0x0D, 0x82, 0x0C,
0x88, 0x06, 0x90, 0x02, 0x81, 0x03, 0x81, 0x0E, 0x81, 0x0A, 0x88, 0x0A,
0x90, 0x08, 0x81, 0x09, 0x81, 0x05, 0x82, 0x08, 0x84, 0x09, 0x88, 0x01,
0x81, 0x02, 0x84, 0x0D, 0x8A, 0x09, 0x81, 0x01, 0x84, 0x02, 0x81, 0x02,
0x90, 0x0E, 0x81, 0x0D, 0x82, 0x05, 0x8A, 0x0E, 0x81, 0x0A, 0x81, 0x0A,
0x84, 0x02, 0x81, 0x0A, 0x84, 0x04, 0x82, 0x0D, 0x81, 0x0A, 0x82, 0x06,
0x84, 0x06, 0x81, 0x02, 0x84, 0x0C, 0x81, 0x0C, 0x81, 0x03, 0x81, 0x05,
0x84, 0x03, 0x82, 0x08, 0x88, 0x06, 0x82, 0x0C, 0x84, 0x01, 0x88, 0x08,
0x88, 0x0A, 0x81, 0x03, 0x90, 0x0E, 0x84, 0x03,
};
// level G -- 1400 ticks, 5 hook bytes, 10 fare stages, 256 pictures, 11 landings
// regenerate: tools/stsim/recorder levels/level07.dat G 20261006 60 2000
static const uint8_t kGateStreamG[320] = {
0x88, 0x05, 0x81, 0x09, 0x81, 0x05, 0x81, 0x04, 0x81, 0x06, 0x84, 0x0C,
0x81, 0x01, 0x89, 0x02, 0x81, 0x09, 0x82, 0x0D, 0x88, 0x05, 0x81, 0x03,
0x80, 0x09, 0x81, 0x01, 0x90, 0x02, 0x81, 0x04, 0x90, 0x08, 0x81, 0x03,
0x90, 0x0A, 0x88, 0x0E, 0x88, 0x0C, 0x90, 0x0C, 0x81, 0x04, 0x81, 0x0D,
0x90, 0x0A, 0x81, 0x0D, 0x84, 0x0E, 0x81, 0x09, 0x81, 0x08, 0x81, 0x0C,
0x82, 0x04, 0x81, 0x02, 0x81, 0x0E, 0x81, 0x02, 0x84, 0x01, 0x88, 0x0A,
0x88, 0x05, 0x90, 0x09, 0x81, 0x02, 0x84, 0x09, 0x84, 0x05, 0x81, 0x0D,
0x84, 0x09, 0x88, 0x03, 0x84, 0x06, 0x84, 0x0D, 0x81, 0x05, 0x84, 0x0C,
0x88, 0x0C, 0x88, 0x0C, 0x88, 0x03, 0x88, 0x09, 0x81, 0x0A, 0x81, 0x09,
0x81, 0x06, 0x81, 0x07, 0x81, 0x05, 0x88, 0x09, 0x88, 0x02, 0x81, 0x0B,
0x84, 0x0A, 0x84, 0x05, 0x88, 0x03, 0x81, 0x05, 0x81, 0x0B, 0x81, 0x05,
0x90, 0x05, 0x84, 0x09, 0x81, 0x06, 0x81, 0x07, 0x90, 0x0D, 0x81, 0x04,
0x81, 0x0E, 0x88, 0x0D, 0x88, 0x0D, 0x88, 0x06, 0x82, 0x02, 0x88, 0x0D,
0x90, 0x0E, 0x81, 0x04, 0x81, 0x0B, 0x84, 0x0B, 0x81, 0x05, 0x84, 0x07,
0x88, 0x0A, 0x80, 0x04, 0x84, 0x07, 0x84, 0x0B, 0x88, 0x0D, 0x90, 0x0E,
0x90, 0x06, 0x88, 0x08, 0x82, 0x0C, 0x81, 0x02, 0x81, 0x0E, 0x88, 0x0A,
0x90, 0x0B, 0x81, 0x04, 0x88, 0x08, 0x90, 0x07, 0x84, 0x03, 0x82, 0x08,
0x81, 0x05, 0x81, 0x03, 0x81, 0x03, 0x81, 0x06, 0x90, 0x0B, 0x84, 0x0E,
0x82, 0x0E, 0x88, 0x08, 0x88, 0x04, 0x81, 0x0D, 0x88, 0x04, 0x81, 0x08,
0x88, 0x0C, 0x81, 0x06, 0x81, 0x04, 0x90, 0x0B, 0x81, 0x08, 0x84, 0x04,
0x90, 0x08, 0x84, 0x09, 0x88, 0x09, 0x81, 0x0B, 0x81, 0x07, 0x84, 0x0D,
0x88, 0x0A, 0x88, 0x09, 0x90, 0x0B, 0x90, 0x02, 0x84, 0x0D, 0x81, 0x0E,
0x90, 0x0D, 0x84, 0x07, 0x82, 0x0C, 0x81, 0x0E, 0x81, 0x06, 0x88, 0x09,
0x90, 0x02, 0x90, 0x04, 0x81, 0x09, 0x84, 0x0A, 0x90, 0x06, 0x81, 0x04,
0x90, 0x03, 0x90, 0x08, 0x84, 0x0B, 0x81, 0x02, 0x90, 0x04, 0x84, 0x03,
0x81, 0x03, 0x84, 0x05, 0x90, 0x0C, 0x81, 0x01, 0x90, 0x0D, 0x81, 0x02,
0x90, 0x05, 0x90, 0x0E, 0x81, 0x03, 0x90, 0x0A,
0x81, 0x04, 0x88, 0x04, 0x81, 0x01, 0x81, 0x03, 0x88, 0x01, 0x88, 0x06,
0x81, 0x04, 0x88, 0x0D, 0x84, 0x0D, 0x84, 0x03, 0x84, 0x08, 0x81, 0x07,
0x88, 0x06, 0x81, 0x0B, 0x81, 0x0E, 0x88, 0x01, 0x84, 0x0E, 0x81, 0x0D,
0x84, 0x06, 0x81, 0x03, 0x82, 0x08, 0x81, 0x06, 0x84, 0x0B, 0x81, 0x0A,
0x84, 0x02, 0x88, 0x06, 0x81, 0x01, 0x81, 0x09, 0x88, 0x0E, 0x81, 0x0A,
0x81, 0x03, 0x84, 0x05, 0x88, 0x0A, 0x81, 0x01, 0x84, 0x07, 0x88, 0x08,
0x81, 0x0A, 0x81, 0x03, 0x81, 0x09, 0x81, 0x02, 0x81, 0x01, 0x81, 0x09,
0x81, 0x01, 0x81, 0x0C, 0x82, 0x0A, 0x90, 0x0D, 0x88, 0x0E, 0x81, 0x0D,
0x84, 0x03, 0x88, 0x09, 0x84, 0x0D, 0x88, 0x0D, 0x81, 0x05, 0x88, 0x03,
0x81, 0x08, 0x90, 0x0E, 0x81, 0x02, 0x88, 0x0D, 0x82, 0x06, 0x88, 0x08,
0x81, 0x06, 0x82, 0x03, 0x81, 0x01, 0x88, 0x0D, 0x81, 0x05, 0x81, 0x07,
0x81, 0x0B, 0x90, 0x0E, 0x84, 0x01, 0x81, 0x0B, 0x82, 0x0A, 0x90, 0x08,
0x84, 0x02, 0x82, 0x0C, 0x88, 0x05, 0x84, 0x08, 0x81, 0x0D, 0x81, 0x0B,
0x84, 0x0D, 0x88, 0x0B, 0x84, 0x0C, 0x81, 0x07, 0x88, 0x07, 0x88, 0x08,
0x81, 0x05, 0x90, 0x0A, 0x90, 0x0C, 0x82, 0x09, 0x81, 0x07, 0x81, 0x0A,
0x82, 0x0B, 0x88, 0x0D, 0x81, 0x01, 0x90, 0x04, 0x90, 0x01, 0x81, 0x0B,
0x82, 0x05, 0x84, 0x07, 0x81, 0x08, 0x80, 0x04, 0x88, 0x04, 0x81, 0x0A,
0x89, 0x02, 0x88, 0x03, 0x82, 0x05, 0x90, 0x0D, 0x90, 0x0E, 0x81, 0x0B,
0x84, 0x01, 0x82, 0x0A, 0x83, 0x04, 0x81, 0x09, 0x84, 0x0C, 0x81, 0x0E,
0x81, 0x01, 0x82, 0x0D, 0x81, 0x0C, 0x81, 0x03, 0x82, 0x01, 0x81, 0x0C,
0x88, 0x05, 0x81, 0x08, 0x88, 0x0C, 0x82, 0x0E, 0x84, 0x0B, 0x81, 0x0B,
0x88, 0x0C, 0x81, 0x0C, 0x81, 0x05, 0x84, 0x08, 0x82, 0x02, 0x82, 0x0A,
0x81, 0x0C, 0x84, 0x0B, 0x81, 0x02, 0x90, 0x08, 0x81, 0x06, 0x90, 0x0E,
0x88, 0x03, 0x82, 0x0B, 0x84, 0x0E, 0x84, 0x0B, 0x84, 0x0D, 0x88, 0x04,
0x84, 0x05, 0x81, 0x06, 0x84, 0x06, 0x90, 0x0D, 0x90, 0x0C, 0x88, 0x03,
0x81, 0x0E, 0x82, 0x02, 0x84, 0x05, 0x90, 0x0B, 0x82, 0x08, 0x84, 0x09,
0x81, 0x07, 0x84, 0x0D, 0x81, 0x02, 0x90, 0x04,
};
// level I -- 1400 ticks, 20 hook bytes, 10 fare stages, 256 pictures, 22 landings
// regenerate: tools/stsim/recorder levels/level??.dat I 20260918
// level I -- 1400 ticks, 20 hook bytes, 10 fare stages, 256 pictures, 4 landings
// regenerate: tools/stsim/recorder levels/level09.dat I 20261006 60 2000
static const uint8_t kGateStreamI[320] = {
0x84, 0x0E, 0x81, 0x06, 0x84, 0x0E, 0x88, 0x0B, 0x88, 0x05, 0x81, 0x03,
0x82, 0x07, 0x90, 0x0A, 0x81, 0x08, 0x88, 0x0E, 0x88, 0x0E, 0x90, 0x0B,
0x82, 0x0D, 0x81, 0x01, 0x81, 0x0A, 0x90, 0x0A, 0x82, 0x03, 0x81, 0x07,
0x82, 0x05, 0x81, 0x0B, 0x81, 0x05, 0x82, 0x05, 0x82, 0x0C, 0x81, 0x08,
0x88, 0x0C, 0x90, 0x04, 0x81, 0x02, 0x88, 0x08, 0x81, 0x03, 0x81, 0x05,
0x81, 0x0A, 0x88, 0x0E, 0x81, 0x06, 0x84, 0x0E, 0x82, 0x07, 0x82, 0x01,
0x81, 0x04, 0x82, 0x08, 0x90, 0x0C, 0x90, 0x05, 0x81, 0x08, 0x82, 0x0D,
0x90, 0x06, 0x81, 0x0A, 0x84, 0x0B, 0x84, 0x0C, 0x84, 0x0C, 0x88, 0x09,
0x81, 0x06, 0x81, 0x0C, 0x81, 0x04, 0x82, 0x09, 0x88, 0x05, 0x82, 0x0E,
0x81, 0x0C, 0x81, 0x07, 0x88, 0x01, 0x90, 0x0E, 0x81, 0x02, 0x90, 0x0E,
0x81, 0x02, 0x90, 0x07, 0x84, 0x05, 0x84, 0x06, 0x88, 0x06, 0x88, 0x0C,
0x81, 0x0C, 0x84, 0x0B, 0x82, 0x09, 0x81, 0x05, 0x81, 0x0B, 0x82, 0x09,
0x90, 0x0E, 0x90, 0x06, 0x88, 0x0D, 0x84, 0x03, 0x82, 0x0B, 0x84, 0x0D,
0x90, 0x0B, 0x90, 0x04, 0x81, 0x0B, 0x81, 0x08, 0x88, 0x08, 0x90, 0x0A,
0x81, 0x03, 0x81, 0x01, 0x81, 0x0E, 0x81, 0x04, 0x88, 0x02, 0x90, 0x05,
0x81, 0x07, 0x81, 0x01, 0x82, 0x09, 0x90, 0x06, 0x81, 0x09, 0x90, 0x0E,
0x88, 0x05, 0x82, 0x0D, 0x88, 0x03, 0x81, 0x06, 0x84, 0x0C, 0x82, 0x01,
0x81, 0x09, 0x82, 0x0E, 0x90, 0x01, 0x81, 0x01, 0x84, 0x05, 0x82, 0x06,
0x88, 0x0A, 0x81, 0x05, 0x82, 0x03, 0x90, 0x07, 0x90, 0x08, 0x81, 0x02,
0x90, 0x0D, 0x88, 0x0D, 0x81, 0x08, 0x90, 0x0E, 0x82, 0x0D, 0x88, 0x04,
0x81, 0x0C, 0x82, 0x09, 0x90, 0x04, 0x81, 0x0C, 0x82, 0x06, 0x90, 0x0E,
0x90, 0x0D, 0x90, 0x07, 0x90, 0x03, 0x82, 0x03, 0x88, 0x06, 0x81, 0x07,
0x90, 0x08, 0x81, 0x02, 0x90, 0x09, 0x81, 0x04, 0x90, 0x06, 0x82, 0x0A,
0x90, 0x05, 0x88, 0x05, 0x88, 0x06, 0x90, 0x0B, 0x81, 0x03, 0x82, 0x02,
0x90, 0x0B, 0x81, 0x02, 0x90, 0x0C, 0x90, 0x07, 0x81, 0x01, 0x84, 0x08,
0x90, 0x0C, 0x81, 0x0E, 0x88, 0x08, 0x88, 0x0C, 0x81, 0x0C, 0x90, 0x04,
0x84, 0x0D, 0x81, 0x01, 0x88, 0x08, 0x88, 0x09,
0x81, 0x04, 0x88, 0x02, 0x89, 0x01, 0x89, 0x02, 0x81, 0x01, 0x88, 0x05,
0x81, 0x04, 0x80, 0x0E, 0x84, 0x06, 0x82, 0x05, 0x84, 0x08, 0x88, 0x07,
0x88, 0x06, 0x81, 0x0B, 0x82, 0x05, 0x82, 0x07, 0x81, 0x0E, 0x81, 0x02,
0x81, 0x03, 0x82, 0x03, 0x82, 0x0C, 0x88, 0x06, 0x84, 0x0B, 0x84, 0x0E,
0x81, 0x02, 0x81, 0x06, 0x81, 0x02, 0x84, 0x09, 0x90, 0x0E, 0x81, 0x02,
0x81, 0x07, 0x81, 0x02, 0x90, 0x08, 0x81, 0x02, 0x84, 0x04, 0x88, 0x09,
0x88, 0x05, 0x81, 0x0C, 0x88, 0x0E, 0x84, 0x0E, 0x88, 0x08, 0x90, 0x09,
0x90, 0x05, 0x84, 0x09, 0x82, 0x0A, 0x90, 0x0C, 0x88, 0x07, 0x88, 0x09,
0x84, 0x05, 0x81, 0x04, 0x81, 0x02, 0x81, 0x03, 0x81, 0x08, 0x88, 0x0B,
0x90, 0x05, 0x81, 0x0A, 0x81, 0x08, 0x82, 0x0E, 0x82, 0x09, 0x90, 0x09,
0x81, 0x07, 0x81, 0x07, 0x81, 0x08, 0x88, 0x0D, 0x82, 0x0C, 0x81, 0x08,
0x81, 0x02, 0x81, 0x03, 0x82, 0x04, 0x84, 0x0B, 0x81, 0x05, 0x88, 0x02,
0x88, 0x07, 0x90, 0x0D, 0x88, 0x0B, 0x84, 0x02, 0x81, 0x02, 0x84, 0x09,
0x88, 0x0C, 0x81, 0x06, 0x88, 0x09, 0x81, 0x02, 0x84, 0x0A, 0x88, 0x06,
0x88, 0x0C, 0x81, 0x05, 0x82, 0x03, 0x88, 0x02, 0x81, 0x06, 0x81, 0x08,
0x82, 0x03, 0x82, 0x0E, 0x81, 0x03, 0x81, 0x02, 0x81, 0x0C, 0x82, 0x0B,
0x84, 0x05, 0x88, 0x0C, 0x84, 0x0C, 0x81, 0x07, 0x81, 0x06, 0x88, 0x07,
0x88, 0x0C, 0x90, 0x0E, 0x90, 0x03, 0x81, 0x0C, 0x84, 0x0C, 0x81, 0x03,
0x89, 0x01, 0x88, 0x0D, 0x81, 0x05, 0x81, 0x06, 0x90, 0x04, 0x83, 0x0D,
0x81, 0x01, 0x88, 0x05, 0x84, 0x05, 0x81, 0x01, 0x84, 0x09, 0x81, 0x02,
0x90, 0x05, 0x81, 0x0D, 0x80, 0x09, 0x81, 0x0C, 0x88, 0x02, 0x84, 0x0C,
0x82, 0x0E, 0x82, 0x0C, 0x90, 0x02, 0x81, 0x07, 0x88, 0x04, 0x88, 0x0D,
0x88, 0x09, 0x81, 0x0D, 0x88, 0x0C, 0x88, 0x0E, 0x88, 0x0E, 0x82, 0x0B,
0x82, 0x0B, 0x81, 0x0C, 0x82, 0x0C, 0x90, 0x0B, 0x81, 0x0A, 0x84, 0x08,
0x81, 0x08, 0x82, 0x0B, 0x81, 0x09, 0x84, 0x09, 0x81, 0x0B, 0x81, 0x03,
0x8A, 0x0D, 0x88, 0x0B, 0x90, 0x04, 0x81, 0x0E, 0x88, 0x07, 0x81, 0x08,
0x88, 0x03, 0x80, 0x0E, 0x84, 0x05, 0x84, 0x07,
};
// level K -- 1400 ticks, 1 hook bytes, 10 fare stages, 256 pictures, 16 landings
// regenerate: tools/stsim/recorder levels/level??.dat K 11
// level J -- 1400 ticks, 10 hook bytes, 10 fare stages, 256 pictures, 6 landings
// regenerate: tools/stsim/recorder levels/level10.dat J 20261006 60 2000
static const uint8_t kGateStreamJ[320] = {
0x81, 0x04, 0x88, 0x02, 0x84, 0x01, 0x81, 0x02, 0x90, 0x01, 0x82, 0x05,
0x81, 0x04, 0x88, 0x0D, 0x88, 0x0C, 0x82, 0x0C, 0x81, 0x03, 0x84, 0x0A,
0x81, 0x06, 0x84, 0x04, 0x88, 0x05, 0x88, 0x08, 0x81, 0x0E, 0x88, 0x06,
0x81, 0x0A, 0x81, 0x05, 0x81, 0x0C, 0x84, 0x0D, 0x81, 0x09, 0x90, 0x0D,
0x81, 0x0A, 0x81, 0x09, 0x84, 0x0E, 0x88, 0x0C, 0x88, 0x03, 0x88, 0x07,
0x88, 0x0D, 0x82, 0x04, 0x82, 0x0D, 0x88, 0x0B, 0x81, 0x03, 0x81, 0x0A,
0x81, 0x0A, 0x81, 0x0C, 0x81, 0x05, 0x82, 0x04, 0x88, 0x0E, 0x82, 0x0C,
0x82, 0x09, 0x88, 0x07, 0x88, 0x0B, 0x81, 0x09, 0x82, 0x03, 0x81, 0x0A,
0x81, 0x03, 0x88, 0x0C, 0x88, 0x05, 0x81, 0x04, 0x81, 0x01, 0x81, 0x0E,
0x90, 0x07, 0x84, 0x01, 0x84, 0x0C, 0x88, 0x05, 0x88, 0x0C, 0x90, 0x0D,
0x90, 0x0C, 0x82, 0x09, 0x90, 0x03, 0x81, 0x07, 0x90, 0x0C, 0x81, 0x04,
0x81, 0x04, 0x81, 0x0B, 0x88, 0x0C, 0x90, 0x0B, 0x88, 0x0C, 0x81, 0x0C,
0x82, 0x0A, 0x82, 0x07, 0x81, 0x0A, 0x81, 0x01, 0x84, 0x07, 0x82, 0x09,
0x84, 0x0E, 0x90, 0x01, 0x84, 0x01, 0x81, 0x0B, 0x81, 0x05, 0x88, 0x01,
0x82, 0x02, 0x81, 0x05, 0x81, 0x05, 0x80, 0x0E, 0x84, 0x09, 0x90, 0x0D,
0x90, 0x0B, 0x81, 0x05, 0x81, 0x08, 0x84, 0x03, 0x81, 0x08, 0x88, 0x0D,
0x88, 0x0A, 0x81, 0x0A, 0x82, 0x06, 0x88, 0x06, 0x84, 0x0B, 0x81, 0x06,
0x90, 0x0B, 0x81, 0x07, 0x82, 0x0E, 0x84, 0x0A, 0x81, 0x03, 0x82, 0x01,
0x88, 0x09, 0x82, 0x01, 0x82, 0x08, 0x81, 0x06, 0x81, 0x0C, 0x84, 0x07,
0x90, 0x08, 0x81, 0x02, 0x84, 0x0B, 0x84, 0x04, 0x84, 0x0D, 0x81, 0x06,
0x81, 0x07, 0x88, 0x0E, 0x88, 0x04, 0x82, 0x02, 0x90, 0x09, 0x88, 0x03,
0x81, 0x04, 0x90, 0x0C, 0x81, 0x0B, 0x82, 0x0C, 0x81, 0x0B, 0x81, 0x0E,
0x82, 0x0E, 0x81, 0x0D, 0x84, 0x0B, 0x81, 0x0E, 0x88, 0x0E, 0x84, 0x0C,
0x84, 0x03, 0x90, 0x0D, 0x82, 0x0A, 0x82, 0x07, 0x90, 0x0C, 0x82, 0x0A,
0x81, 0x06, 0x81, 0x06, 0x84, 0x08, 0x88, 0x07, 0x90, 0x05, 0x81, 0x01,
0x81, 0x03, 0x82, 0x03, 0x82, 0x0D, 0x84, 0x0B, 0x81, 0x06, 0x90, 0x0A,
0x88, 0x03, 0x90, 0x04, 0x81, 0x04, 0x90, 0x06,
};
// level K -- searched 2026-09 by the recorder of then (seed 11), from a
// fresh game's first screen; kept because, ridden as the shift reaches
// the screen, it covers more than the 2026-10-06 search did
static const uint8_t kGateStreamK[320] = {
0x84, 0x06, 0x82, 0x0B, 0x90, 0x0C, 0x82, 0x0E, 0x90, 0x05, 0x88, 0x03,
0x88, 0x02, 0x82, 0x0E, 0x88, 0x07, 0x84, 0x02, 0x84, 0x0B, 0x84, 0x08,
@ -114,8 +338,105 @@ static const uint8_t kGateStreamK[320] = {
0x82, 0x09, 0x90, 0x01, 0x88, 0x07, 0x88, 0x08,
};
// level O -- 1400 ticks, 1 hook bytes, 3 fare stages, 256 pictures, 0 landings
// regenerate: tools/stsim/recorder levels/level??.dat O 20260918
// level L -- 1400 ticks, 0 hook bytes, 10 fare stages, 256 pictures, 4 landings
// regenerate: tools/stsim/recorder levels/level12.dat L 20261006 60 2000
static const uint8_t kGateStreamL[320] = {
0x81, 0x04, 0x84, 0x02, 0x81, 0x01, 0x81, 0x02, 0x90, 0x01, 0x88, 0x05,
0x81, 0x04, 0x88, 0x0E, 0x84, 0x06, 0x88, 0x01, 0x88, 0x08, 0x88, 0x07,
0x88, 0x06, 0x81, 0x0B, 0x84, 0x0E, 0x82, 0x07, 0x81, 0x0C, 0x88, 0x06,
0x81, 0x0A, 0x81, 0x07, 0x81, 0x0C, 0x81, 0x0D, 0x81, 0x09, 0x84, 0x0B,
0x86, 0x0E, 0x81, 0x0E, 0x84, 0x0E, 0x81, 0x0A, 0x84, 0x05, 0x88, 0x06,
0x84, 0x09, 0x88, 0x01, 0x82, 0x0D, 0x84, 0x0D, 0x81, 0x06, 0x88, 0x09,
0x84, 0x0E, 0x82, 0x08, 0x82, 0x04, 0x82, 0x04, 0x81, 0x09, 0x88, 0x0D,
0x90, 0x05, 0x88, 0x0D, 0x90, 0x0B, 0x84, 0x0C, 0x88, 0x0D, 0x88, 0x09,
0x84, 0x0A, 0x82, 0x0B, 0x84, 0x06, 0x88, 0x0D, 0x88, 0x07, 0x88, 0x09,
0x88, 0x04, 0x81, 0x0B, 0x81, 0x0B, 0x88, 0x0C, 0x82, 0x05, 0x88, 0x0C,
0x81, 0x05, 0x84, 0x09, 0x84, 0x03, 0x88, 0x04, 0x81, 0x05, 0x90, 0x03,
0x90, 0x0A, 0x82, 0x0E, 0x84, 0x06, 0x90, 0x07, 0x90, 0x0C, 0x81, 0x06,
0x82, 0x08, 0x81, 0x09, 0x81, 0x08, 0x81, 0x01, 0x81, 0x07, 0x81, 0x07,
0x90, 0x0E, 0x90, 0x01, 0x81, 0x07, 0x81, 0x0B, 0x81, 0x0C, 0x84, 0x0C,
0x82, 0x0B, 0x82, 0x09, 0x88, 0x05, 0x80, 0x08, 0x90, 0x06, 0x81, 0x04,
0x81, 0x05, 0x81, 0x0C, 0x81, 0x0A, 0x88, 0x03, 0x81, 0x08, 0x90, 0x0D,
0x84, 0x09, 0x81, 0x0B, 0x82, 0x06, 0x81, 0x03, 0x81, 0x0B, 0x81, 0x0E,
0x90, 0x0A, 0x81, 0x0E, 0x84, 0x0E, 0x81, 0x0A, 0x81, 0x0C, 0x82, 0x01,
0x88, 0x02, 0x90, 0x01, 0x82, 0x06, 0x81, 0x07, 0x81, 0x0C, 0x90, 0x02,
0x82, 0x0D, 0x81, 0x0E, 0x82, 0x08, 0x81, 0x09, 0x81, 0x08, 0x81, 0x08,
0x81, 0x0E, 0x81, 0x07, 0x80, 0x05, 0x84, 0x0B, 0x84, 0x0B, 0x81, 0x01,
0x84, 0x0D, 0x8C, 0x06, 0x84, 0x08, 0x81, 0x07, 0x81, 0x0B, 0x88, 0x06,
0x81, 0x06, 0x81, 0x0C, 0x81, 0x03, 0x84, 0x08, 0x81, 0x0C, 0x81, 0x05,
0x88, 0x03, 0x82, 0x07, 0x88, 0x0C, 0x82, 0x05, 0x88, 0x0A, 0x81, 0x05,
0x89, 0x0B, 0x81, 0x06, 0x86, 0x0D, 0x81, 0x0D, 0x90, 0x0A, 0x81, 0x0A,
0x88, 0x0A, 0x81, 0x02, 0x90, 0x08, 0x81, 0x02, 0x90, 0x05, 0x84, 0x09,
0x81, 0x06, 0x81, 0x06, 0x88, 0x02, 0x82, 0x02,
};
// level M -- 1400 ticks, 0 hook bytes, 8 fare stages, 256 pictures, 9 landings
// regenerate: tools/stsim/recorder levels/level13.dat M 20261006 60 2000
static const uint8_t kGateStreamM[320] = {
0x88, 0x06, 0x88, 0x06, 0x84, 0x04, 0x89, 0x03, 0x90, 0x09, 0x82, 0x05,
0x81, 0x04, 0x88, 0x0E, 0x81, 0x07, 0x84, 0x0A, 0x84, 0x09, 0x90, 0x08,
0x84, 0x01, 0x90, 0x03, 0x81, 0x08, 0x82, 0x0A, 0x81, 0x03, 0x84, 0x06,
0x88, 0x02, 0x81, 0x04, 0x81, 0x02, 0x90, 0x0E, 0x84, 0x0E, 0x84, 0x0B,
0x90, 0x0E, 0x84, 0x0E, 0x90, 0x06, 0x82, 0x03, 0x88, 0x07, 0x88, 0x03,
0x88, 0x04, 0x81, 0x05, 0x88, 0x03, 0x81, 0x05, 0x84, 0x0A, 0x81, 0x04,
0x88, 0x07, 0x90, 0x09, 0x88, 0x0E, 0x84, 0x03, 0x84, 0x0A, 0x81, 0x05,
0x88, 0x0B, 0x84, 0x0A, 0x84, 0x0E, 0x90, 0x0C, 0x82, 0x08, 0x84, 0x07,
0x81, 0x05, 0x81, 0x06, 0x84, 0x05, 0x81, 0x01, 0x82, 0x06, 0x84, 0x01,
0x90, 0x08, 0x84, 0x04, 0x81, 0x0B, 0x81, 0x06, 0x88, 0x0C, 0x81, 0x07,
0x90, 0x0B, 0x81, 0x0B, 0x90, 0x04, 0x90, 0x0C, 0x90, 0x09, 0x90, 0x06,
0x84, 0x05, 0x90, 0x0E, 0x81, 0x05, 0x88, 0x01, 0x82, 0x01, 0x90, 0x0C,
0x90, 0x0E, 0x88, 0x01, 0x81, 0x07, 0x84, 0x0E, 0x82, 0x06, 0x88, 0x0C,
0x88, 0x08, 0x90, 0x0B, 0x90, 0x0B, 0x81, 0x0D, 0x84, 0x06, 0x90, 0x0C,
0x90, 0x0C, 0x84, 0x0D, 0x84, 0x0D, 0x88, 0x0C, 0x90, 0x0D, 0x88, 0x0C,
0x88, 0x0C, 0x88, 0x0D, 0x81, 0x02, 0x82, 0x02, 0x90, 0x09, 0x84, 0x0D,
0x88, 0x04, 0x88, 0x01, 0x88, 0x0E, 0x90, 0x0A, 0x81, 0x09, 0x82, 0x09,
0x90, 0x0E, 0x81, 0x07, 0x84, 0x0D, 0x88, 0x05, 0x88, 0x08, 0x82, 0x06,
0x84, 0x0D, 0x84, 0x03, 0x82, 0x0C, 0x84, 0x02, 0x90, 0x03, 0x81, 0x07,
0x82, 0x0B, 0x81, 0x09, 0x84, 0x0C, 0x88, 0x0E, 0x81, 0x05, 0x82, 0x09,
0x81, 0x08, 0x84, 0x0E, 0x90, 0x0E, 0x88, 0x04, 0x84, 0x04, 0x88, 0x01,
0x81, 0x05, 0x82, 0x08, 0x81, 0x0E, 0x82, 0x09, 0x88, 0x06, 0x81, 0x0D,
0x90, 0x0C, 0x84, 0x09, 0x81, 0x05, 0x81, 0x02, 0x82, 0x08, 0x81, 0x01,
0x90, 0x03, 0x90, 0x02, 0x84, 0x0D, 0x84, 0x03, 0x88, 0x0D, 0x88, 0x0D,
0x84, 0x0A, 0x90, 0x06, 0x88, 0x06, 0x82, 0x03, 0x82, 0x0C, 0x88, 0x0E,
0x84, 0x0A, 0x81, 0x06, 0x82, 0x05, 0x90, 0x0E, 0x81, 0x0C, 0x84, 0x0C,
0x88, 0x07, 0x84, 0x0C, 0x88, 0x0E, 0x90, 0x04,
};
// level N -- 1400 ticks, 0 hook bytes, 1 fare stages, 45 pictures, 26 landings
// regenerate: tools/stsim/recorder levels/level14.dat N 20261006 60 2000
static const uint8_t kGateStreamN[320] = {
0x81, 0x04, 0x88, 0x03, 0x82, 0x01, 0x89, 0x02, 0x90, 0x01, 0x84, 0x0B,
0x81, 0x04, 0x88, 0x0D, 0x84, 0x0C, 0x82, 0x03, 0x81, 0x09, 0x81, 0x04,
0x88, 0x06, 0x81, 0x0D, 0x82, 0x0D, 0x90, 0x0A, 0x84, 0x03, 0x81, 0x08,
0x88, 0x06, 0x81, 0x03, 0x88, 0x0C, 0x88, 0x03, 0x84, 0x04, 0x84, 0x0E,
0x82, 0x02, 0x88, 0x09, 0x80, 0x03, 0x84, 0x09, 0x81, 0x07, 0x81, 0x0B,
0x81, 0x05, 0x81, 0x03, 0x84, 0x0C, 0x81, 0x02, 0x81, 0x0D, 0x82, 0x04,
0x90, 0x08, 0x82, 0x09, 0x82, 0x04, 0x84, 0x0A, 0x88, 0x01, 0x81, 0x0B,
0x90, 0x0B, 0x81, 0x0A, 0x82, 0x0B, 0x90, 0x07, 0x81, 0x08, 0x81, 0x04,
0x82, 0x0E, 0x90, 0x09, 0x81, 0x0A, 0x82, 0x08, 0x81, 0x03, 0x90, 0x07,
0x88, 0x0B, 0x81, 0x08, 0x84, 0x04, 0x90, 0x0B, 0x84, 0x06, 0x88, 0x0A,
0x84, 0x0A, 0x81, 0x03, 0x90, 0x0E, 0x81, 0x07, 0x90, 0x0E, 0x84, 0x04,
0x84, 0x06, 0x82, 0x0A, 0x81, 0x0A, 0x90, 0x08, 0x84, 0x0D, 0x82, 0x0B,
0x81, 0x0D, 0x84, 0x08, 0x90, 0x0A, 0x81, 0x09, 0x90, 0x08, 0x88, 0x0A,
0x82, 0x07, 0x84, 0x07, 0x90, 0x0A, 0x81, 0x04, 0x90, 0x0B, 0x84, 0x0A,
0x81, 0x0B, 0x81, 0x01, 0x90, 0x02, 0x82, 0x0D, 0x84, 0x0C, 0x81, 0x08,
0x90, 0x0D, 0x82, 0x07, 0x88, 0x0C, 0x81, 0x03, 0x90, 0x0D, 0x81, 0x07,
0x90, 0x0C, 0x82, 0x0C, 0x81, 0x02, 0x90, 0x08, 0x88, 0x0B, 0x81, 0x0E,
0x82, 0x07, 0x90, 0x0E, 0x82, 0x07, 0x81, 0x06, 0x84, 0x05, 0x81, 0x04,
0x90, 0x0E, 0x84, 0x03, 0x81, 0x0A, 0x82, 0x08, 0x90, 0x03, 0x90, 0x05,
0x84, 0x09, 0x81, 0x03, 0x90, 0x0C, 0x81, 0x03, 0x82, 0x06, 0x81, 0x0A,
0x81, 0x03, 0x88, 0x06, 0x81, 0x02, 0x88, 0x02, 0x90, 0x02, 0x88, 0x0B,
0x84, 0x0A, 0x82, 0x08, 0x88, 0x08, 0x81, 0x03, 0x81, 0x02, 0x82, 0x04,
0x90, 0x06, 0x81, 0x0D, 0x90, 0x0C, 0x82, 0x0E, 0x81, 0x08, 0x81, 0x07,
0x81, 0x0E, 0x81, 0x02, 0x81, 0x07, 0x84, 0x03, 0x88, 0x0C, 0x81, 0x0B,
0x90, 0x0A, 0x84, 0x09, 0x88, 0x04, 0x83, 0x06, 0x84, 0x09, 0x88, 0x09,
0x81, 0x09, 0x81, 0x01, 0x90, 0x0A, 0x84, 0x0B, 0x82, 0x08, 0x81, 0x09,
0x88, 0x08, 0x82, 0x0C, 0x81, 0x06, 0x88, 0x06,
};
// level O -- searched 2026-09 by the recorder of then (seed 20260918), from a
// fresh game's first screen; kept because, ridden as the shift reaches
// the screen, it covers more than the 2026-10-06 search did
static const uint8_t kGateStreamO[320] = {
0x84, 0x0E, 0x90, 0x08, 0x82, 0x07, 0x81, 0x0B, 0x84, 0x02, 0x81, 0x09,
0x82, 0x06, 0x90, 0x0C, 0x81, 0x08, 0x88, 0x01, 0x88, 0x02, 0x90, 0x04,
@ -146,40 +467,73 @@ static const uint8_t kGateStreamO[320] = {
0x81, 0x0E, 0x81, 0x08, 0x90, 0x0D, 0x84, 0x0D,
};
// level Q -- 1400 ticks, 5 hook bytes, 10 fare stages, 256 pictures, 10 landings
// regenerate: tools/stsim/recorder levels/level??.dat Q 20260918
static const uint8_t kGateStreamQ[320] = {
0x84, 0x0E, 0x81, 0x04, 0x81, 0x03, 0x84, 0x09, 0x88, 0x0A, 0x81, 0x09,
0x82, 0x06, 0x88, 0x0C, 0x81, 0x08, 0x82, 0x06, 0x88, 0x0E, 0x84, 0x0B,
0x88, 0x08, 0x81, 0x09, 0x81, 0x0A, 0x84, 0x05, 0x84, 0x01, 0x88, 0x01,
0x81, 0x08, 0x84, 0x0B, 0x81, 0x05, 0x82, 0x05, 0x80, 0x0D, 0x81, 0x08,
0x88, 0x0A, 0x88, 0x04, 0x84, 0x08, 0x81, 0x06, 0x81, 0x0C, 0x81, 0x01,
0x90, 0x0A, 0x84, 0x08, 0x81, 0x0A, 0x81, 0x0A, 0x82, 0x08, 0x81, 0x06,
0x81, 0x09, 0x90, 0x06, 0x81, 0x09, 0x90, 0x0D, 0x88, 0x04, 0x82, 0x05,
0x81, 0x0B, 0x84, 0x08, 0x81, 0x08, 0x81, 0x03, 0x82, 0x0D, 0x82, 0x0A,
0x84, 0x0E, 0x81, 0x05, 0x81, 0x0E, 0x81, 0x08, 0x84, 0x0A, 0x82, 0x0C,
0x84, 0x09, 0x84, 0x01, 0x82, 0x06, 0x88, 0x0E, 0x81, 0x09, 0x90, 0x07,
0x81, 0x09, 0x81, 0x07, 0x84, 0x03, 0x81, 0x0B, 0x84, 0x01, 0x90, 0x08,
0x90, 0x06, 0x82, 0x03, 0x88, 0x06, 0x88, 0x0C, 0x84, 0x0A, 0x81, 0x03,
0x82, 0x0E, 0x81, 0x07, 0x81, 0x04, 0x81, 0x0D, 0x84, 0x02, 0x81, 0x09,
0x82, 0x08, 0x88, 0x04, 0x81, 0x09, 0x84, 0x0C, 0x81, 0x0B, 0x84, 0x0D,
0x81, 0x02, 0x82, 0x03, 0x81, 0x05, 0x88, 0x0E, 0x84, 0x0D, 0x81, 0x0B,
0x88, 0x08, 0x84, 0x0C, 0x90, 0x07, 0x82, 0x0B, 0x81, 0x03, 0x90, 0x05,
0x84, 0x05, 0x84, 0x09, 0x81, 0x04, 0x81, 0x0B, 0x81, 0x08, 0x82, 0x0E,
0x81, 0x0A, 0x81, 0x03, 0x81, 0x06, 0x88, 0x09, 0x84, 0x0A, 0x88, 0x0B,
0x81, 0x05, 0x81, 0x0D, 0x88, 0x03, 0x88, 0x0D, 0x81, 0x01, 0x90, 0x06,
0x88, 0x07, 0x82, 0x02, 0x81, 0x04, 0x84, 0x09, 0x81, 0x07, 0x90, 0x06,
0x82, 0x03, 0x84, 0x07, 0x81, 0x05, 0x90, 0x0B, 0x81, 0x0B, 0x88, 0x05,
0x81, 0x0D, 0x81, 0x0B, 0x92, 0x0E, 0x90, 0x05, 0x82, 0x02, 0x88, 0x0C,
0x84, 0x08, 0x81, 0x0A, 0x81, 0x0D, 0x81, 0x05, 0x81, 0x07, 0x81, 0x01,
0x81, 0x04, 0x88, 0x01, 0x81, 0x0E, 0x84, 0x04, 0x90, 0x03, 0x81, 0x0D,
0x90, 0x09, 0x81, 0x02, 0x81, 0x08, 0x90, 0x07, 0x82, 0x0B, 0x90, 0x08,
0x81, 0x03, 0x90, 0x0C, 0x81, 0x0B, 0x88, 0x08, 0x90, 0x04, 0x81, 0x02,
0x90, 0x0C, 0x88, 0x05, 0x81, 0x01, 0x90, 0x03,
// level P -- 1400 ticks, 11 hook bytes, 10 fare stages, 256 pictures, 5 landings
// regenerate: tools/stsim/recorder levels/level16.dat P 20261006 60 2000
static const uint8_t kGateStreamP[320] = {
0x81, 0x04, 0x88, 0x02, 0x84, 0x05, 0x89, 0x02, 0x81, 0x01, 0x88, 0x05,
0x81, 0x04, 0x80, 0x0D, 0x84, 0x0C, 0x84, 0x03, 0x84, 0x08, 0x88, 0x07,
0x88, 0x01, 0x81, 0x05, 0x88, 0x0E, 0x82, 0x01, 0x81, 0x0C, 0x81, 0x0C,
0x88, 0x03, 0x80, 0x0C, 0x88, 0x0E, 0x88, 0x03, 0x84, 0x04, 0x84, 0x0E,
0x81, 0x07, 0x88, 0x06, 0x80, 0x08, 0x84, 0x09, 0x81, 0x0E, 0x90, 0x0A,
0x81, 0x03, 0x81, 0x05, 0x84, 0x09, 0x81, 0x01, 0x88, 0x07, 0x88, 0x08,
0x81, 0x05, 0x81, 0x0C, 0x82, 0x0D, 0x88, 0x0E, 0x88, 0x07, 0x81, 0x02,
0x81, 0x03, 0x84, 0x0C, 0x88, 0x06, 0x88, 0x05, 0x81, 0x07, 0x81, 0x0A,
0x84, 0x09, 0x90, 0x06, 0x84, 0x09, 0x81, 0x0B, 0x88, 0x01, 0x81, 0x03,
0x81, 0x06, 0x90, 0x0C, 0x88, 0x06, 0x81, 0x0B, 0x81, 0x06, 0x90, 0x06,
0x82, 0x06, 0x82, 0x04, 0x82, 0x01, 0x81, 0x02, 0x81, 0x07, 0x90, 0x0E,
0x90, 0x0B, 0x84, 0x07, 0x81, 0x03, 0x84, 0x0E, 0x84, 0x08, 0x84, 0x0C,
0x81, 0x0D, 0x81, 0x0A, 0x81, 0x0E, 0x88, 0x0C, 0x88, 0x0B, 0x81, 0x05,
0x90, 0x04, 0x88, 0x0A, 0x82, 0x0B, 0x84, 0x0D, 0x81, 0x0C, 0x84, 0x0C,
0x90, 0x05, 0x84, 0x0D, 0x81, 0x02, 0x81, 0x08, 0x81, 0x03, 0x88, 0x06,
0x90, 0x0E, 0x81, 0x05, 0x84, 0x0B, 0x88, 0x08, 0x81, 0x09, 0x84, 0x0E,
0x81, 0x0A, 0x81, 0x05, 0x88, 0x07, 0x81, 0x02, 0x81, 0x0C, 0x81, 0x03,
0x8A, 0x0C, 0x84, 0x0B, 0x80, 0x03, 0x85, 0x04, 0x81, 0x04, 0x82, 0x0A,
0x85, 0x07, 0x84, 0x0D, 0x88, 0x08, 0x80, 0x07, 0x81, 0x01, 0x80, 0x0D,
0x80, 0x04, 0x81, 0x05, 0x82, 0x01, 0x82, 0x0B, 0x84, 0x05, 0x88, 0x0A,
0x90, 0x01, 0x90, 0x08, 0x81, 0x08, 0x81, 0x08, 0x81, 0x05, 0x81, 0x07,
0x81, 0x01, 0x81, 0x0D, 0x88, 0x02, 0x81, 0x01, 0x88, 0x03, 0x90, 0x02,
0x88, 0x0A, 0x81, 0x0D, 0x82, 0x05, 0x88, 0x01, 0x81, 0x02, 0x85, 0x0A,
0x84, 0x0C, 0x88, 0x0C, 0x88, 0x05, 0x88, 0x03, 0x90, 0x08, 0x81, 0x08,
0x84, 0x0E, 0x82, 0x0C, 0x84, 0x0E, 0x90, 0x0C, 0x82, 0x0E, 0x90, 0x08,
0x84, 0x0D, 0x88, 0x0B, 0x84, 0x0C, 0x82, 0x0C, 0x84, 0x08, 0x90, 0x0A,
0x88, 0x0E, 0x81, 0x07, 0x90, 0x03, 0x88, 0x0C,
};
// level R -- 1400 ticks, 0 hook bytes, 3 fare stages, 256 pictures, 0 landings
// regenerate: tools/stsim/recorder levels/level??.dat R 20260918
// level Q -- 1400 ticks, 6 hook bytes, 10 fare stages, 256 pictures, 6 landings
// regenerate: tools/stsim/recorder levels/level17.dat Q 20261006 60 2000
static const uint8_t kGateStreamQ[320] = {
0x81, 0x04, 0x88, 0x02, 0x84, 0x0E, 0x89, 0x06, 0x81, 0x01, 0x82, 0x05,
0x81, 0x04, 0x80, 0x05, 0x84, 0x0C, 0x82, 0x0C, 0x81, 0x06, 0x88, 0x07,
0x81, 0x06, 0x81, 0x0B, 0x88, 0x0E, 0x84, 0x07, 0x88, 0x0E, 0x84, 0x06,
0x81, 0x0A, 0x81, 0x05, 0x81, 0x0C, 0x81, 0x0D, 0x81, 0x09, 0x88, 0x0D,
0x86, 0x0A, 0x81, 0x09, 0x84, 0x09, 0x88, 0x0E, 0x81, 0x05, 0x88, 0x06,
0x80, 0x09, 0x84, 0x04, 0x82, 0x0A, 0x84, 0x0C, 0x81, 0x0E, 0x90, 0x09,
0x81, 0x0D, 0x84, 0x07, 0x90, 0x03, 0x82, 0x04, 0x82, 0x06, 0x81, 0x0E,
0x84, 0x09, 0x88, 0x06, 0x88, 0x0C, 0x88, 0x07, 0x81, 0x02, 0x84, 0x0D,
0x88, 0x04, 0x81, 0x05, 0x88, 0x05, 0x81, 0x05, 0x88, 0x02, 0x90, 0x0E,
0x81, 0x06, 0x84, 0x02, 0x81, 0x02, 0x84, 0x05, 0x81, 0x0C, 0x90, 0x05,
0x81, 0x0C, 0x81, 0x09, 0x81, 0x07, 0x82, 0x04, 0x81, 0x05, 0x81, 0x04,
0x81, 0x0A, 0x82, 0x0E, 0x90, 0x09, 0x81, 0x07, 0x90, 0x0C, 0x82, 0x06,
0x82, 0x08, 0x81, 0x01, 0x82, 0x0B, 0x81, 0x01, 0x81, 0x07, 0x81, 0x07,
0x90, 0x03, 0x81, 0x01, 0x88, 0x01, 0x81, 0x0B, 0x81, 0x07, 0x84, 0x06,
0x88, 0x01, 0x81, 0x05, 0x81, 0x08, 0x88, 0x0E, 0x81, 0x08, 0x90, 0x05,
0x82, 0x07, 0x84, 0x0A, 0x81, 0x01, 0x81, 0x0D, 0x81, 0x0E, 0x84, 0x06,
0x90, 0x0A, 0x81, 0x06, 0x82, 0x06, 0x84, 0x01, 0x90, 0x03, 0x90, 0x04,
0x81, 0x03, 0x81, 0x08, 0x90, 0x05, 0x82, 0x05, 0x88, 0x06, 0x88, 0x06,
0x81, 0x0B, 0x84, 0x05, 0x84, 0x0E, 0x82, 0x0E, 0x81, 0x0E, 0x81, 0x0C,
0x81, 0x05, 0x90, 0x0C, 0x81, 0x0C, 0x88, 0x0B, 0x81, 0x05, 0x88, 0x03,
0x81, 0x04, 0x88, 0x06, 0x88, 0x07, 0x88, 0x05, 0x84, 0x06, 0x90, 0x0A,
0x82, 0x0B, 0x82, 0x07, 0x81, 0x01, 0x89, 0x0D, 0x88, 0x02, 0x84, 0x04,
0x81, 0x01, 0x88, 0x09, 0x84, 0x0B, 0x84, 0x07, 0x90, 0x0E, 0x82, 0x01,
0x81, 0x0C, 0x84, 0x0E, 0x81, 0x02, 0x84, 0x0B, 0x81, 0x08, 0x82, 0x09,
0x81, 0x0E, 0x81, 0x0D, 0x90, 0x06, 0x81, 0x0D, 0x90, 0x0E, 0x81, 0x0A,
0x81, 0x0E, 0x81, 0x02, 0x81, 0x05, 0x88, 0x05, 0x88, 0x08, 0x84, 0x09,
0x84, 0x0D, 0x80, 0x0A, 0x84, 0x02, 0x81, 0x0C,
};
// level R -- searched 2026-09 by the recorder of then (seed 20260918), from a
// fresh game's first screen; kept because, ridden as the shift reaches
// the screen, it covers more than the 2026-10-06 search did
static const uint8_t kGateStreamR[320] = {
0x90, 0x0E, 0x81, 0x02, 0x82, 0x07, 0x81, 0x05, 0x88, 0x0B, 0x88, 0x09,
0x90, 0x0D, 0x90, 0x06, 0x81, 0x08, 0x81, 0x01, 0x90, 0x02, 0x88, 0x04,
@ -210,228 +564,132 @@ static const uint8_t kGateStreamR[320] = {
0x90, 0x0A, 0x90, 0x01, 0x84, 0x0A, 0x81, 0x0B,
};
// level U -- 896 ticks, 21 hook bytes, 3 fare stages, 256 pictures, 0 landings
// regenerate: tools/stsim/recorder levels/level??.dat U 44
static const uint8_t kGateStreamU[320] = {
0x90, 0x01, 0x90, 0x0B, 0x88, 0x08, 0x90, 0x07, 0x88, 0x0D, 0x82, 0x0A,
0x82, 0x06, 0x88, 0x0C, 0x90, 0x03, 0x81, 0x04, 0x84, 0x0B, 0x81, 0x0C,
0x88, 0x02, 0x84, 0x08, 0x84, 0x01, 0x81, 0x04, 0x90, 0x05, 0x84, 0x06,
0x84, 0x08, 0x84, 0x05, 0x81, 0x03, 0x81, 0x09, 0x81, 0x08, 0x88, 0x07,
0x81, 0x08, 0x84, 0x06, 0x81, 0x0B, 0x88, 0x0D, 0x90, 0x08, 0x81, 0x01,
0x82, 0x01, 0x88, 0x03, 0x81, 0x0E, 0x82, 0x01, 0x81, 0x0D, 0x81, 0x07,
0x88, 0x05, 0x88, 0x07, 0x88, 0x0E, 0x84, 0x07, 0x81, 0x03, 0x81, 0x01,
0x84, 0x03, 0x81, 0x04, 0x81, 0x08, 0x90, 0x06, 0x81, 0x0C, 0x82, 0x05,
0x81, 0x08, 0x82, 0x09, 0x84, 0x06, 0x81, 0x06, 0x90, 0x0C, 0x84, 0x0E,
0x82, 0x0C, 0x81, 0x03, 0x81, 0x06, 0x84, 0x0D, 0x88, 0x03, 0x84, 0x0C,
0x81, 0x02, 0x81, 0x09, 0x81, 0x0E, 0x88, 0x01, 0x90, 0x06, 0x88, 0x08,
0x81, 0x08, 0x81, 0x0C, 0x81, 0x0C, 0x82, 0x06, 0x82, 0x08, 0x88, 0x01,
0x81, 0x07, 0x88, 0x0D, 0x84, 0x03, 0x81, 0x02, 0x82, 0x06, 0x81, 0x05,
0x81, 0x0E, 0x81, 0x04, 0x90, 0x01, 0x81, 0x02, 0x90, 0x03, 0x81, 0x0A,
0x81, 0x04, 0x84, 0x02, 0x81, 0x06, 0x81, 0x0E, 0x81, 0x06, 0x84, 0x07,
0x84, 0x04, 0x81, 0x07, 0x81, 0x0E, 0x81, 0x0D, 0x90, 0x0D, 0x90, 0x05,
0x88, 0x0C, 0x88, 0x02, 0x81, 0x0B, 0x90, 0x07, 0x88, 0x06, 0x82, 0x0D,
0x84, 0x08, 0x84, 0x08, 0x81, 0x01, 0x81, 0x06, 0x82, 0x05, 0x84, 0x06,
0x81, 0x06, 0x84, 0x0D, 0x81, 0x04, 0x90, 0x07, 0x88, 0x0D, 0x81, 0x09,
0x88, 0x0D, 0x81, 0x0E, 0x81, 0x03, 0x84, 0x0E, 0x81, 0x03, 0x81, 0x0C,
0x84, 0x0A, 0x81, 0x06, 0x90, 0x02, 0x81, 0x01, 0x88, 0x02, 0x90, 0x02,
0x81, 0x08, 0x84, 0x01, 0x88, 0x0B, 0x88, 0x0B, 0x81, 0x04, 0x81, 0x0C,
0x81, 0x07, 0x82, 0x06, 0x88, 0x0C, 0x81, 0x03, 0x90, 0x02, 0x88, 0x0A,
0x84, 0x08, 0x81, 0x0E, 0x81, 0x05, 0x88, 0x0C, 0x81, 0x0D, 0x81, 0x05,
0x81, 0x04, 0x90, 0x0D, 0x88, 0x03, 0x88, 0x06, 0x81, 0x0B, 0x81, 0x0D,
0x84, 0x0D, 0x81, 0x01, 0x81, 0x0D, 0x81, 0x01, 0x82, 0x07, 0x88, 0x02,
0x81, 0x09, 0x90, 0x0B, 0x88, 0x0D, 0x82, 0x02,
};
// level V -- 1400 ticks, 0 hook bytes, 6 fare stages, 256 pictures, 23 landings
// regenerate: tools/stsim/recorder levels/level??.dat V 20260918
static const uint8_t kGateStreamV[320] = {
0x84, 0x0E, 0x81, 0x04, 0x81, 0x05, 0x88, 0x0B, 0x84, 0x01, 0x81, 0x03,
0x82, 0x06, 0x84, 0x0C, 0x81, 0x08, 0x84, 0x07, 0x82, 0x03, 0x88, 0x0A,
0x90, 0x03, 0x81, 0x0D, 0x81, 0x0D, 0x82, 0x05, 0x81, 0x02, 0x81, 0x0E,
0x90, 0x06, 0x81, 0x05, 0x90, 0x05, 0x81, 0x0E, 0x82, 0x0D, 0x90, 0x0E,
0x88, 0x0E, 0x81, 0x03, 0x90, 0x07, 0x84, 0x0A, 0x81, 0x02, 0x90, 0x07,
0x82, 0x07, 0x81, 0x02, 0x81, 0x06, 0x82, 0x08, 0x81, 0x04, 0x88, 0x0C,
0x81, 0x06, 0x88, 0x0D, 0x90, 0x0D, 0x81, 0x05, 0x90, 0x03, 0x84, 0x0D,
0x84, 0x0A, 0x88, 0x0A, 0x88, 0x0D, 0x81, 0x08, 0x90, 0x08, 0x90, 0x01,
0x84, 0x06, 0x82, 0x0B, 0x81, 0x0D, 0x82, 0x07, 0x82, 0x0C, 0x81, 0x01,
0x90, 0x08, 0x81, 0x02, 0x81, 0x04, 0x90, 0x0C, 0x84, 0x04, 0x88, 0x04,
0x82, 0x03, 0x88, 0x07, 0x81, 0x03, 0x90, 0x05, 0x81, 0x02, 0x81, 0x03,
0x88, 0x01, 0x90, 0x05, 0x81, 0x08, 0x81, 0x06, 0x81, 0x07, 0x81, 0x08,
0x81, 0x06, 0x81, 0x02, 0x84, 0x0B, 0x88, 0x0D, 0x81, 0x06, 0x90, 0x02,
0x81, 0x07, 0x82, 0x0B, 0x88, 0x04, 0x84, 0x04, 0x84, 0x05, 0x81, 0x05,
0x81, 0x02, 0x82, 0x09, 0x81, 0x03, 0x90, 0x0C, 0x88, 0x0B, 0x82, 0x01,
0x88, 0x0D, 0x88, 0x05, 0x81, 0x02, 0x90, 0x0B, 0x81, 0x02, 0x90, 0x0A,
0x88, 0x0C, 0x81, 0x01, 0x90, 0x0A, 0x88, 0x0E, 0x90, 0x0A, 0x84, 0x0B,
0x81, 0x03, 0x90, 0x08, 0x88, 0x03, 0x82, 0x0E, 0x84, 0x06, 0x90, 0x0E,
0x81, 0x03, 0x88, 0x0A, 0x90, 0x0A, 0x82, 0x09, 0x81, 0x04, 0x88, 0x0C,
0x84, 0x04, 0x81, 0x07, 0x90, 0x03, 0x81, 0x01, 0x81, 0x01, 0x81, 0x01,
0x82, 0x0E, 0x81, 0x05, 0x81, 0x06, 0x81, 0x08, 0x84, 0x08, 0x88, 0x0C,
0x82, 0x09, 0x88, 0x05, 0x81, 0x02, 0x88, 0x04, 0x81, 0x04, 0x88, 0x0C,
0x90, 0x07, 0x88, 0x0E, 0x81, 0x03, 0x81, 0x0A, 0x82, 0x0B, 0x81, 0x02,
0x88, 0x02, 0x90, 0x0E, 0x88, 0x0D, 0x81, 0x03, 0x88, 0x09, 0x90, 0x04,
0x84, 0x01, 0x88, 0x04, 0x90, 0x04, 0x84, 0x04, 0x81, 0x03, 0x90, 0x0C,
0x88, 0x07, 0x84, 0x05, 0x81, 0x02, 0x90, 0x0B, 0x90, 0x04, 0x90, 0x09,
0x81, 0x02, 0x81, 0x01, 0x90, 0x0B, 0x88, 0x04,
};
// level E -- 1400 ticks, 4 hook bytes, 8 fare stages, 256 pictures, 11 landings
// regenerate: tools/stsim/recorder levels/level??.dat E 20260921
static const uint8_t kGateStreamE[320] = {
0x84, 0x05, 0x82, 0x03, 0x82, 0x05, 0x81, 0x0B, 0x81, 0x07, 0x81, 0x02,
0x81, 0x01, 0x89, 0x02, 0x90, 0x03, 0x81, 0x0A, 0x81, 0x05, 0x84, 0x0C,
0x81, 0x02, 0x88, 0x07, 0x81, 0x0B, 0x90, 0x08, 0x90, 0x0A, 0x88, 0x02,
0x81, 0x05, 0x84, 0x08, 0x81, 0x05, 0x90, 0x03, 0x84, 0x01, 0x84, 0x08,
0x88, 0x0A, 0x84, 0x07, 0x81, 0x04, 0x84, 0x07, 0x90, 0x0C, 0x80, 0x08,
0x90, 0x07, 0x88, 0x03, 0x90, 0x0B, 0x88, 0x01, 0x90, 0x07, 0x88, 0x04,
0x81, 0x02, 0x88, 0x08, 0x84, 0x05, 0x81, 0x05, 0x81, 0x02, 0x81, 0x09,
0x88, 0x05, 0x90, 0x0E, 0x81, 0x05, 0x81, 0x01, 0x90, 0x04, 0x90, 0x08,
0x81, 0x06, 0x84, 0x0E, 0x81, 0x09, 0x81, 0x01, 0x84, 0x05, 0x81, 0x01,
0x81, 0x02, 0x88, 0x0B, 0x90, 0x08, 0x88, 0x0C, 0x88, 0x05, 0x88, 0x08,
0x84, 0x09, 0x88, 0x05, 0x90, 0x05, 0x82, 0x0A, 0x81, 0x07, 0x90, 0x0A,
0x81, 0x07, 0x81, 0x03, 0x84, 0x0B, 0x84, 0x0E, 0x88, 0x09, 0x90, 0x02,
0x81, 0x0E, 0x81, 0x06, 0x90, 0x0E, 0x81, 0x02, 0x90, 0x08, 0x84, 0x08,
0x81, 0x0B, 0x88, 0x0D, 0x84, 0x0C, 0x81, 0x09, 0x90, 0x08, 0x82, 0x0B,
0x88, 0x08, 0x81, 0x07, 0x82, 0x02, 0x84, 0x0B, 0x88, 0x0B, 0x81, 0x0D,
0x84, 0x0E, 0x82, 0x01, 0x81, 0x03, 0x90, 0x0C, 0x81, 0x05, 0x90, 0x04,
0x82, 0x0C, 0x81, 0x06, 0x90, 0x0E, 0x88, 0x0B, 0x88, 0x09, 0x81, 0x0E,
0x82, 0x0E, 0x90, 0x0C, 0x81, 0x06, 0x81, 0x05, 0x84, 0x0E, 0x81, 0x0C,
0x88, 0x0B, 0x90, 0x06, 0x82, 0x05, 0x81, 0x06, 0x88, 0x0D, 0x88, 0x07,
0x81, 0x07, 0x84, 0x07, 0x88, 0x08, 0x81, 0x07, 0x90, 0x0C, 0x84, 0x04,
0x84, 0x0A, 0x88, 0x0B, 0x80, 0x0A, 0x81, 0x0C, 0x81, 0x07, 0x88, 0x06,
0x88, 0x09, 0x88, 0x09, 0x81, 0x0C, 0x80, 0x06, 0x81, 0x06, 0x81, 0x07,
0x84, 0x08, 0x82, 0x09, 0x84, 0x0C, 0x90, 0x09, 0x90, 0x03, 0x90, 0x0D,
0x81, 0x08, 0x84, 0x05, 0x84, 0x09, 0x88, 0x0B, 0x81, 0x01, 0x81, 0x0A,
0x90, 0x0D, 0x88, 0x0E, 0x88, 0x0D, 0x88, 0x05, 0x84, 0x06, 0x81, 0x0E,
0x81, 0x0A, 0x81, 0x08, 0x84, 0x0C, 0x88, 0x05, 0x81, 0x02, 0x90, 0x0E,
0x84, 0x0D, 0x84, 0x0A, 0x81, 0x05, 0x90, 0x09,
};
// level J -- 1400 ticks, 10 hook bytes, 10 fare stages, 256 pictures, 8 landings
// regenerate: tools/stsim/recorder levels/level??.dat J 20260921
static const uint8_t kGateStreamJ[320] = {
0x88, 0x05, 0x82, 0x09, 0x81, 0x01, 0x81, 0x04, 0x90, 0x06, 0x81, 0x05,
0x81, 0x01, 0x90, 0x09, 0x88, 0x05, 0x82, 0x08, 0x81, 0x0A, 0x81, 0x03,
0x84, 0x07, 0x88, 0x0D, 0x81, 0x06, 0x81, 0x0A, 0x84, 0x05, 0x90, 0x08,
0x81, 0x0B, 0x90, 0x0E, 0x84, 0x0E, 0x81, 0x03, 0x90, 0x09, 0x81, 0x02,
0x88, 0x04, 0x88, 0x08, 0x84, 0x0E, 0x81, 0x04, 0x88, 0x0D, 0x90, 0x0E,
0x84, 0x09, 0x90, 0x05, 0x81, 0x01, 0x81, 0x03, 0x88, 0x03, 0x88, 0x07,
0x81, 0x0B, 0x90, 0x0B, 0x88, 0x08, 0x82, 0x03, 0x90, 0x09, 0x88, 0x03,
0x84, 0x06, 0x88, 0x0A, 0x88, 0x0B, 0x84, 0x04, 0x81, 0x05, 0x88, 0x0E,
0x81, 0x0A, 0x90, 0x0C, 0x85, 0x04, 0x81, 0x0E, 0x90, 0x02, 0x82, 0x0D,
0x84, 0x01, 0x81, 0x02, 0x88, 0x09, 0x81, 0x0B, 0x81, 0x02, 0x82, 0x0C,
0x84, 0x04, 0x81, 0x05, 0x81, 0x04, 0x81, 0x04, 0x81, 0x05, 0x90, 0x05,
0x88, 0x09, 0x81, 0x02, 0x88, 0x01, 0x81, 0x05, 0x90, 0x04, 0x81, 0x0B,
0x81, 0x05, 0x82, 0x0D, 0x88, 0x06, 0x88, 0x07, 0x88, 0x02, 0x84, 0x02,
0x90, 0x07, 0x88, 0x09, 0x84, 0x0C, 0x81, 0x01, 0x88, 0x0B, 0x81, 0x0C,
0x82, 0x0A, 0x81, 0x08, 0x82, 0x01, 0x88, 0x09, 0x84, 0x05, 0x81, 0x08,
0x84, 0x0A, 0x84, 0x09, 0x88, 0x0E, 0x81, 0x01, 0x84, 0x03, 0x82, 0x0C,
0x81, 0x0E, 0x84, 0x0A, 0x88, 0x02, 0x84, 0x0D, 0x81, 0x0A, 0x90, 0x0E,
0x88, 0x09, 0x90, 0x09, 0x81, 0x05, 0x90, 0x03, 0x88, 0x09, 0x84, 0x06,
0x81, 0x01, 0x88, 0x06, 0x81, 0x05, 0x81, 0x09, 0x81, 0x06, 0x90, 0x04,
0x82, 0x0A, 0x90, 0x07, 0x81, 0x07, 0x81, 0x0A, 0x84, 0x0E, 0x84, 0x07,
0x81, 0x0A, 0x90, 0x0C, 0x81, 0x01, 0x90, 0x01, 0x88, 0x0D, 0x81, 0x03,
0x88, 0x05, 0x81, 0x03, 0x88, 0x01, 0x81, 0x01, 0x82, 0x02, 0x88, 0x07,
0x83, 0x02, 0x90, 0x07, 0x81, 0x05, 0x81, 0x04, 0x81, 0x0E, 0x88, 0x09,
0x81, 0x07, 0x90, 0x03, 0x82, 0x0B, 0x82, 0x0B, 0x90, 0x04, 0x81, 0x05,
0x81, 0x03, 0x90, 0x0C, 0x82, 0x07, 0x88, 0x0E, 0x90, 0x04, 0x81, 0x0A,
0x88, 0x02, 0x88, 0x0D, 0x82, 0x07, 0x90, 0x01, 0x81, 0x02, 0x81, 0x01,
0x82, 0x07, 0x81, 0x08, 0x84, 0x0E, 0x81, 0x03,
};
// level L -- 1400 ticks, 0 hook bytes, 10 fare stages, 256 pictures, 8 landings
// regenerate: tools/stsim/recorder levels/level??.dat L 20260921
static const uint8_t kGateStreamL[320] = {
0x84, 0x05, 0x84, 0x09, 0x81, 0x06, 0x81, 0x04, 0x81, 0x08, 0x81, 0x05,
0x81, 0x01, 0x84, 0x09, 0x90, 0x09, 0x82, 0x0D, 0x88, 0x07, 0x81, 0x06,
0x81, 0x09, 0x82, 0x01, 0x81, 0x08, 0x88, 0x0B, 0x84, 0x0E, 0x81, 0x03,
0x82, 0x02, 0x82, 0x06, 0x82, 0x03, 0x88, 0x0E, 0x84, 0x0C, 0x81, 0x06,
0x88, 0x02, 0x81, 0x04, 0x88, 0x0E, 0x81, 0x04, 0x90, 0x0B, 0x81, 0x08,
0x84, 0x09, 0x82, 0x09, 0x81, 0x01, 0x90, 0x03, 0x81, 0x05, 0x84, 0x07,
0x81, 0x07, 0x90, 0x0B, 0x81, 0x07, 0x88, 0x09, 0x81, 0x0E, 0x88, 0x03,
0x81, 0x01, 0x88, 0x09, 0x81, 0x07, 0x81, 0x0C, 0x88, 0x0C, 0x90, 0x0E,
0x81, 0x0E, 0x90, 0x05, 0x84, 0x0E, 0x82, 0x0E, 0x82, 0x01, 0x84, 0x0A,
0x88, 0x03, 0x90, 0x09, 0x82, 0x05, 0x81, 0x0E, 0x81, 0x05, 0x81, 0x07,
0x81, 0x0D, 0x81, 0x0E, 0x90, 0x04, 0x90, 0x04, 0x81, 0x06, 0x81, 0x01,
0x88, 0x0B, 0x81, 0x01, 0x90, 0x0B, 0x81, 0x05, 0x84, 0x0D, 0x81, 0x01,
0x81, 0x0A, 0x84, 0x0D, 0x82, 0x09, 0x88, 0x06, 0x88, 0x04, 0x84, 0x0C,
0x88, 0x0C, 0x84, 0x02, 0x81, 0x07, 0x82, 0x07, 0x84, 0x08, 0x88, 0x09,
0x81, 0x01, 0x88, 0x01, 0x81, 0x0C, 0x81, 0x04, 0x84, 0x06, 0x82, 0x06,
0x90, 0x0D, 0x81, 0x06, 0x81, 0x0D, 0x82, 0x0D, 0x82, 0x08, 0x81, 0x0C,
0x84, 0x05, 0x82, 0x09, 0x81, 0x05, 0x84, 0x07, 0x82, 0x02, 0x84, 0x01,
0x81, 0x02, 0x90, 0x02, 0x82, 0x06, 0x88, 0x03, 0x84, 0x0B, 0x81, 0x0E,
0x88, 0x0E, 0x90, 0x07, 0x82, 0x08, 0x82, 0x02, 0x90, 0x02, 0x82, 0x03,
0x84, 0x07, 0x82, 0x05, 0x84, 0x07, 0x81, 0x0B, 0x82, 0x0E, 0x90, 0x05,
0x88, 0x04, 0x90, 0x05, 0x84, 0x0A, 0x82, 0x0A, 0x88, 0x0C, 0x81, 0x08,
0x82, 0x0C, 0x81, 0x06, 0x84, 0x09, 0x81, 0x04, 0x84, 0x0B, 0x88, 0x0B,
0x81, 0x0C, 0x81, 0x0C, 0x82, 0x0E, 0x81, 0x03, 0x82, 0x09, 0x88, 0x04,
0x81, 0x05, 0x88, 0x04, 0x80, 0x0B, 0x84, 0x06, 0x81, 0x0E, 0x84, 0x0B,
0x90, 0x0B, 0x90, 0x07, 0x82, 0x0D, 0x84, 0x0D, 0x81, 0x08, 0x81, 0x09,
0x84, 0x0E, 0x81, 0x0B, 0x90, 0x04, 0x84, 0x0B, 0x84, 0x03, 0x81, 0x03,
0x88, 0x0A, 0x81, 0x07, 0x84, 0x0B, 0x90, 0x0E,
};
// level P -- 1400 ticks, 11 hook bytes, 10 fare stages, 256 pictures, 4 landings
// regenerate: tools/stsim/recorder levels/level??.dat P 20260921
static const uint8_t kGateStreamP[320] = {
0x88, 0x05, 0x82, 0x09, 0x81, 0x0B, 0x81, 0x04, 0x81, 0x04, 0x84, 0x05,
0x82, 0x01, 0x90, 0x0C, 0x81, 0x03, 0x81, 0x0D, 0x81, 0x01, 0x84, 0x0C,
0x84, 0x0E, 0x88, 0x07, 0x81, 0x0B, 0x88, 0x0D, 0x90, 0x0A, 0x88, 0x02,
0x81, 0x05, 0x84, 0x08, 0x81, 0x05, 0x90, 0x0C, 0x84, 0x01, 0x88, 0x08,
0x88, 0x0A, 0x84, 0x0C, 0x81, 0x04, 0x84, 0x07, 0x90, 0x0C, 0x80, 0x09,
0x90, 0x06, 0x81, 0x03, 0x81, 0x0C, 0x88, 0x01, 0x82, 0x03, 0x81, 0x04,
0x84, 0x0D, 0x88, 0x09, 0x84, 0x05, 0x81, 0x05, 0x81, 0x02, 0x81, 0x09,
0x81, 0x02, 0x90, 0x0E, 0x81, 0x05, 0x81, 0x01, 0x81, 0x03, 0x88, 0x0A,
0x81, 0x06, 0x88, 0x05, 0x90, 0x0C, 0x90, 0x01, 0x84, 0x0E, 0x88, 0x01,
0x81, 0x02, 0x82, 0x05, 0x90, 0x08, 0x88, 0x0A, 0x84, 0x0C, 0x90, 0x08,
0x84, 0x0C, 0x81, 0x06, 0x81, 0x03, 0x88, 0x0A, 0x84, 0x08, 0x90, 0x0E,
0x81, 0x02, 0x88, 0x0C, 0x81, 0x06, 0x84, 0x0C, 0x81, 0x09, 0x80, 0x07,
0x88, 0x04, 0x81, 0x07, 0x81, 0x03, 0x81, 0x09, 0x81, 0x05, 0x86, 0x05,
0x90, 0x09, 0x84, 0x06, 0x81, 0x03, 0x84, 0x09, 0x81, 0x0E, 0x88, 0x0B,
0x90, 0x08, 0x88, 0x0A, 0x81, 0x04, 0x90, 0x0D, 0x88, 0x0E, 0x88, 0x09,
0x81, 0x0B, 0x90, 0x0C, 0x84, 0x05, 0x81, 0x08, 0x84, 0x01, 0x82, 0x02,
0x81, 0x03, 0x82, 0x0E, 0x88, 0x02, 0x88, 0x05, 0x81, 0x01, 0x81, 0x08,
0x82, 0x03, 0x84, 0x0E, 0x81, 0x01, 0x81, 0x08, 0x84, 0x03, 0x82, 0x03,
0x82, 0x05, 0x81, 0x0A, 0x81, 0x02, 0x90, 0x01, 0x81, 0x0E, 0x81, 0x0C,
0x90, 0x0C, 0x88, 0x0A, 0x84, 0x03, 0x81, 0x03, 0x84, 0x0A, 0x84, 0x05,
0x82, 0x09, 0x88, 0x0A, 0x81, 0x06, 0x84, 0x07, 0x84, 0x0C, 0x81, 0x0C,
0x82, 0x07, 0x81, 0x06, 0x81, 0x03, 0x88, 0x09, 0x90, 0x01, 0x84, 0x0E,
0x81, 0x01, 0x88, 0x06, 0x81, 0x0E, 0x81, 0x0A, 0x88, 0x0A, 0x81, 0x08,
0x84, 0x08, 0x88, 0x0E, 0x81, 0x02, 0x84, 0x0E, 0x82, 0x05, 0x81, 0x06,
0x88, 0x0C, 0x81, 0x0B, 0x81, 0x0D, 0x84, 0x02, 0x90, 0x0E, 0x84, 0x0E,
0x81, 0x02, 0x90, 0x07, 0x88, 0x0C, 0x90, 0x0A, 0x81, 0x0E, 0x81, 0x0A,
0x81, 0x0D, 0x81, 0x0B, 0x81, 0x08, 0x81, 0x03,
};
// level S -- 1400 ticks, 0 hook bytes, 10 fare stages, 256 pictures, 23 landings
// regenerate: tools/stsim/recorder levels/level??.dat S 20260921
// level S -- 1400 ticks, 0 hook bytes, 10 fare stages, 256 pictures, 25 landings
// regenerate: tools/stsim/recorder levels/level19.dat S 20261006 60 2000
static const uint8_t kGateStreamS[320] = {
0x90, 0x04, 0x90, 0x0E, 0x88, 0x08, 0x82, 0x0D, 0x88, 0x0C, 0x81, 0x03,
0x81, 0x0A, 0x81, 0x0D, 0x88, 0x09, 0x88, 0x0C, 0x82, 0x0E, 0x81, 0x07,
0x81, 0x0D, 0x81, 0x0A, 0x90, 0x0D, 0x84, 0x01, 0x90, 0x05, 0x88, 0x0E,
0x81, 0x0A, 0x81, 0x0A, 0x90, 0x06, 0x88, 0x03, 0x82, 0x0B, 0x82, 0x0B,
0x88, 0x03, 0x88, 0x0A, 0x90, 0x0C, 0x81, 0x0E, 0x81, 0x0E, 0x90, 0x06,
0x88, 0x02, 0x82, 0x08, 0x81, 0x0A, 0x90, 0x0D, 0x90, 0x05, 0x81, 0x05,
0x88, 0x0B, 0x81, 0x05, 0x90, 0x0B, 0x88, 0x0E, 0x82, 0x09, 0x90, 0x07,
0x81, 0x03, 0x81, 0x0D, 0x88, 0x09, 0x88, 0x0E, 0x84, 0x0A, 0x88, 0x0A,
0x82, 0x01, 0x90, 0x0C, 0x82, 0x01, 0x90, 0x0A, 0x82, 0x0A, 0x84, 0x03,
0x81, 0x02, 0x90, 0x04, 0x84, 0x0E, 0x90, 0x09, 0x81, 0x0D, 0x90, 0x06,
0x81, 0x02, 0x90, 0x0E, 0x90, 0x07, 0x82, 0x05, 0x81, 0x06, 0x90, 0x07,
0x82, 0x03, 0x90, 0x07, 0x90, 0x0B, 0x82, 0x06, 0x88, 0x0E, 0x90, 0x08,
0x84, 0x0A, 0x82, 0x07, 0x81, 0x04, 0x81, 0x03, 0x84, 0x05, 0x90, 0x08,
0x81, 0x0C, 0x82, 0x01, 0x90, 0x0B, 0x81, 0x0A, 0x84, 0x05, 0x81, 0x02,
0x82, 0x09, 0x90, 0x0E, 0x81, 0x02, 0x84, 0x03, 0x81, 0x0C, 0x81, 0x0C,
0x81, 0x0D, 0x82, 0x08, 0x81, 0x03, 0x81, 0x0D, 0x81, 0x0A, 0x81, 0x0A,
0x81, 0x0C, 0x82, 0x06, 0x90, 0x0D, 0x88, 0x0C, 0x84, 0x04, 0x81, 0x01,
0x81, 0x0A, 0x81, 0x03, 0x90, 0x06, 0x88, 0x07, 0x88, 0x0E, 0x84, 0x0E,
0x81, 0x0E, 0x82, 0x03, 0x90, 0x0E, 0x82, 0x05, 0x90, 0x02, 0x88, 0x03,
0x81, 0x07, 0x81, 0x0A, 0x81, 0x0B, 0x81, 0x05, 0x84, 0x0D, 0x81, 0x01,
0x82, 0x02, 0x90, 0x0C, 0x82, 0x03, 0x90, 0x08, 0x81, 0x05, 0x84, 0x08,
0x84, 0x06, 0x88, 0x08, 0x81, 0x0E, 0x82, 0x01, 0x81, 0x01, 0x90, 0x08,
0x82, 0x03, 0x90, 0x0C, 0x84, 0x02, 0x81, 0x09, 0x82, 0x04, 0x88, 0x0D,
0x90, 0x07, 0x88, 0x0E, 0x82, 0x05, 0x81, 0x0C, 0x90, 0x0A, 0x81, 0x04,
0x84, 0x06, 0x82, 0x02, 0x81, 0x09, 0x90, 0x0D, 0x82, 0x06, 0x82, 0x01,
0x90, 0x09, 0x81, 0x0B, 0x84, 0x04, 0x88, 0x07, 0x81, 0x0B, 0x81, 0x05,
0x82, 0x03, 0x81, 0x07, 0x90, 0x06, 0x82, 0x07,
0x82, 0x04, 0x88, 0x0E, 0x88, 0x02, 0x88, 0x0D, 0x81, 0x04, 0x82, 0x06,
0x81, 0x0D, 0x81, 0x09, 0x81, 0x07, 0x88, 0x09, 0x84, 0x07, 0x81, 0x01,
0x90, 0x0C, 0x81, 0x09, 0x82, 0x0A, 0x88, 0x0D, 0x81, 0x0A, 0x84, 0x03,
0x88, 0x0E, 0x82, 0x05, 0x81, 0x08, 0x90, 0x0C, 0x81, 0x0C, 0x88, 0x0A,
0x82, 0x02, 0x81, 0x01, 0x90, 0x05, 0x84, 0x0A, 0x88, 0x0D, 0x82, 0x02,
0x90, 0x02, 0x81, 0x05, 0x88, 0x08, 0x81, 0x08, 0x88, 0x01, 0x82, 0x01,
0x90, 0x08, 0x81, 0x0B, 0x81, 0x0A, 0x88, 0x0A, 0x90, 0x04, 0x81, 0x02,
0x82, 0x01, 0x90, 0x07, 0x82, 0x05, 0x90, 0x07, 0x81, 0x0D, 0x81, 0x0E,
0x81, 0x07, 0x88, 0x09, 0x84, 0x04, 0x82, 0x01, 0x90, 0x0C, 0x81, 0x09,
0x81, 0x0A, 0x81, 0x05, 0x81, 0x03, 0x88, 0x03, 0x88, 0x02, 0x82, 0x01,
0x81, 0x05, 0x90, 0x0B, 0x82, 0x08, 0x88, 0x05, 0x84, 0x0B, 0x90, 0x0A,
0x81, 0x0E, 0x81, 0x05, 0x88, 0x0B, 0x82, 0x0B, 0x81, 0x07, 0x90, 0x08,
0x84, 0x02, 0x81, 0x02, 0x81, 0x0C, 0x82, 0x01, 0x81, 0x05, 0x90, 0x06,
0x82, 0x04, 0x84, 0x04, 0x90, 0x04, 0x84, 0x0C, 0x81, 0x05, 0x81, 0x03,
0x81, 0x01, 0x88, 0x01, 0x90, 0x01, 0x81, 0x0E, 0x88, 0x07, 0x82, 0x02,
0x81, 0x06, 0x90, 0x03, 0x81, 0x0C, 0x82, 0x04, 0x88, 0x0B, 0x90, 0x09,
0x81, 0x0C, 0x88, 0x0D, 0x81, 0x02, 0x82, 0x03, 0x81, 0x06, 0x90, 0x02,
0x90, 0x03, 0x84, 0x0E, 0x82, 0x03, 0x81, 0x05, 0x88, 0x02, 0x90, 0x01,
0x84, 0x0D, 0x84, 0x09, 0x82, 0x08, 0x82, 0x02, 0x84, 0x03, 0x84, 0x07,
0x90, 0x0D, 0x82, 0x02, 0x88, 0x07, 0x84, 0x01, 0x90, 0x05, 0x81, 0x09,
0x82, 0x02, 0x90, 0x06, 0x90, 0x04, 0x81, 0x05, 0x82, 0x06, 0x88, 0x0B,
0x90, 0x03, 0x82, 0x04, 0x81, 0x06, 0x84, 0x08, 0x81, 0x0D, 0x81, 0x03,
0x81, 0x02, 0x82, 0x0B, 0x88, 0x0B, 0x81, 0x0E, 0x90, 0x06, 0x90, 0x08,
0x88, 0x06, 0x81, 0x02, 0x81, 0x0D, 0x88, 0x03, 0x84, 0x07, 0x81, 0x07,
0x81, 0x05, 0x81, 0x06, 0x81, 0x06, 0x81, 0x03, 0x82, 0x06, 0x81, 0x03,
0x88, 0x0A, 0x84, 0x08, 0x90, 0x0B, 0x90, 0x03, 0x82, 0x06, 0x84, 0x02,
0x84, 0x07, 0x90, 0x05, 0x82, 0x0E, 0x90, 0x04,
};
// level U -- 802 ticks, 21 hook bytes, 1 fare stages, 4 pictures, 0 landings
// regenerate: tools/stsim/recorder levels/level21.dat U 20261006 60 2000
static const uint8_t kGateStreamU[320] = {
0x81, 0x04, 0x88, 0x02, 0x84, 0x08, 0x89, 0x02, 0x90, 0x01, 0x84, 0x05,
0x84, 0x08, 0x88, 0x0D, 0x84, 0x0A, 0x82, 0x03, 0x82, 0x08, 0x90, 0x0B,
0x88, 0x07, 0x90, 0x02, 0x90, 0x07, 0x81, 0x07, 0x90, 0x04, 0x81, 0x06,
0x88, 0x05, 0x84, 0x04, 0x81, 0x04, 0x88, 0x02, 0x88, 0x07, 0x84, 0x04,
0x81, 0x0A, 0x81, 0x03, 0x81, 0x0E, 0x82, 0x08, 0x81, 0x0B, 0x84, 0x0A,
0x84, 0x02, 0x81, 0x08, 0x84, 0x03, 0x90, 0x08, 0x88, 0x02, 0x81, 0x09,
0x81, 0x0A, 0x88, 0x03, 0x84, 0x09, 0x90, 0x06, 0x81, 0x06, 0x81, 0x07,
0x88, 0x03, 0x81, 0x07, 0x88, 0x05, 0x81, 0x0D, 0x81, 0x08, 0x88, 0x07,
0x84, 0x07, 0x81, 0x06, 0x81, 0x01, 0x81, 0x0A, 0x88, 0x0E, 0x81, 0x03,
0x81, 0x0C, 0x84, 0x02, 0x88, 0x0D, 0x81, 0x07, 0x84, 0x07, 0x82, 0x0A,
0x90, 0x09, 0x81, 0x0C, 0x84, 0x06, 0x88, 0x04, 0x81, 0x06, 0x81, 0x02,
0x81, 0x0E, 0x82, 0x02, 0x88, 0x0E, 0x81, 0x0B, 0x81, 0x06, 0x81, 0x02,
0x81, 0x09, 0x81, 0x0D, 0x81, 0x05, 0x82, 0x0E, 0x88, 0x04, 0x90, 0x08,
0x81, 0x0A, 0x81, 0x09, 0x84, 0x07, 0x81, 0x03, 0x81, 0x0A, 0x82, 0x09,
0x88, 0x07, 0x90, 0x05, 0x81, 0x09, 0x88, 0x02, 0x82, 0x09, 0x82, 0x0D,
0x81, 0x0C, 0x81, 0x01, 0x81, 0x07, 0x81, 0x0A, 0x84, 0x06, 0x90, 0x01,
0x81, 0x08, 0x81, 0x08, 0x81, 0x0D, 0x81, 0x09, 0x90, 0x01, 0x90, 0x08,
0x88, 0x04, 0x81, 0x02, 0x84, 0x04, 0x81, 0x0D, 0x84, 0x01, 0x88, 0x0A,
0x82, 0x06, 0x81, 0x03, 0x88, 0x0E, 0x88, 0x03, 0x81, 0x0B, 0x81, 0x0D,
0x82, 0x09, 0x88, 0x0C, 0x81, 0x07, 0x88, 0x0B, 0x81, 0x0B, 0x81, 0x09,
0x82, 0x0B, 0x81, 0x03, 0x88, 0x09, 0x81, 0x08, 0x88, 0x0A, 0x81, 0x01,
0x88, 0x05, 0x90, 0x08, 0x82, 0x02, 0x84, 0x05, 0x81, 0x0E, 0x90, 0x0A,
0x81, 0x0B, 0x81, 0x0D, 0x82, 0x05, 0x81, 0x0C, 0x84, 0x07, 0x88, 0x0A,
0x81, 0x09, 0x84, 0x05, 0x81, 0x02, 0x90, 0x0B, 0x82, 0x01, 0x81, 0x0D,
0x81, 0x09, 0x81, 0x05, 0x81, 0x0C, 0x81, 0x0D, 0x84, 0x02, 0x90, 0x08,
0x84, 0x0D, 0x90, 0x0C, 0x84, 0x01, 0x81, 0x0A, 0x82, 0x0E, 0x81, 0x0C,
0x81, 0x0A, 0x81, 0x09, 0x81, 0x04, 0x81, 0x08,
};
// level V -- 1400 ticks, 1 hook bytes, 10 fare stages, 256 pictures, 25 landings
// regenerate: tools/stsim/recorder levels/level22.dat V 20261011 60 2000
static const uint8_t kGateStreamV[320] = {
0x88, 0x0D, 0x81, 0x02, 0x81, 0x09, 0x82, 0x0C, 0x90, 0x04, 0x81, 0x0B,
0x81, 0x01, 0x84, 0x0A, 0x84, 0x06, 0x90, 0x0E, 0x88, 0x0B, 0x81, 0x01,
0x90, 0x0C, 0x81, 0x03, 0x90, 0x02, 0x88, 0x0E, 0x90, 0x0E, 0x82, 0x0D,
0x84, 0x0A, 0x81, 0x06, 0x90, 0x04, 0x82, 0x05, 0x82, 0x02, 0x81, 0x06,
0x82, 0x0C, 0x84, 0x0B, 0x81, 0x05, 0x88, 0x09, 0x81, 0x02, 0x90, 0x0C,
0x81, 0x01, 0x84, 0x08, 0x88, 0x0E, 0x88, 0x08, 0x81, 0x01, 0x81, 0x07,
0x88, 0x07, 0x90, 0x04, 0x84, 0x0C, 0x90, 0x0B, 0x88, 0x0C, 0x88, 0x06,
0x82, 0x03, 0x81, 0x07, 0x84, 0x0C, 0x90, 0x0C, 0x84, 0x0C, 0x88, 0x0B,
0x84, 0x08, 0x81, 0x04, 0x90, 0x06, 0x88, 0x03, 0x82, 0x0D, 0x84, 0x04,
0x88, 0x07, 0x84, 0x04, 0x81, 0x01, 0x90, 0x0E, 0x84, 0x02, 0x88, 0x07,
0x81, 0x0A, 0x82, 0x02, 0x82, 0x09, 0x90, 0x0B, 0x84, 0x0B, 0x81, 0x06,
0x90, 0x0D, 0x90, 0x03, 0x84, 0x02, 0x82, 0x08, 0x81, 0x07, 0x88, 0x03,
0x88, 0x02, 0x81, 0x03, 0x90, 0x0C, 0x90, 0x06, 0x88, 0x08, 0x88, 0x0C,
0x88, 0x08, 0x81, 0x09, 0x82, 0x06, 0x90, 0x0B, 0x88, 0x0D, 0x90, 0x02,
0x81, 0x0C, 0x82, 0x0A, 0x84, 0x0B, 0x90, 0x0D, 0x81, 0x04, 0x90, 0x0C,
0x81, 0x04, 0x88, 0x02, 0x82, 0x06, 0x81, 0x02, 0x88, 0x06, 0x81, 0x07,
0x81, 0x0B, 0x81, 0x0E, 0x90, 0x07, 0x90, 0x06, 0x88, 0x0C, 0x81, 0x01,
0x90, 0x07, 0x90, 0x03, 0x81, 0x04, 0x81, 0x02, 0x84, 0x0A, 0x84, 0x0D,
0x84, 0x01, 0x84, 0x04, 0x88, 0x0B, 0x90, 0x09, 0x88, 0x0C, 0x90, 0x02,
0x84, 0x03, 0x81, 0x01, 0x90, 0x07, 0x90, 0x0A, 0x81, 0x06, 0x90, 0x05,
0x82, 0x08, 0x88, 0x0B, 0x81, 0x08, 0x88, 0x09, 0x84, 0x0C, 0x82, 0x05,
0x90, 0x02, 0x88, 0x0D, 0x81, 0x0E, 0x81, 0x0D, 0x84, 0x04, 0x81, 0x06,
0x82, 0x04, 0x90, 0x08, 0x84, 0x0E, 0x81, 0x01, 0x81, 0x05, 0x82, 0x07,
0x90, 0x09, 0x81, 0x01, 0x90, 0x07, 0x88, 0x07, 0x81, 0x03, 0x90, 0x0E,
0x88, 0x0C, 0x81, 0x01, 0x90, 0x05, 0x81, 0x06, 0x88, 0x01, 0x84, 0x04,
0x84, 0x01, 0x84, 0x0E, 0x90, 0x06, 0x81, 0x02, 0x81, 0x03, 0x88, 0x03,
0x82, 0x0A, 0x84, 0x02, 0x81, 0x04, 0x81, 0x0A,
};
// level Y -- 1400 ticks, 14 hook bytes, 10 fare stages, 256 pictures, 15 landings
// regenerate: tools/stsim/recorder levels/level25.dat Y 20261006 60 2000
static const uint8_t kGateStreamY[320] = {
0x88, 0x04, 0x88, 0x06, 0x84, 0x01, 0x90, 0x01, 0x88, 0x01, 0x84, 0x05,
0x81, 0x03, 0x84, 0x0B, 0x81, 0x0E, 0x88, 0x07, 0x81, 0x0E, 0x82, 0x0D,
0x81, 0x05, 0x81, 0x01, 0x82, 0x01, 0x88, 0x07, 0x84, 0x0A, 0x88, 0x0B,
0x84, 0x07, 0x88, 0x0D, 0x81, 0x07, 0x81, 0x03, 0x84, 0x04, 0x84, 0x02,
0x81, 0x06, 0x88, 0x08, 0x81, 0x0C, 0x82, 0x0E, 0x81, 0x0B, 0x81, 0x08,
0x81, 0x01, 0x82, 0x0D, 0x81, 0x04, 0x90, 0x04, 0x84, 0x06, 0x84, 0x01,
0x84, 0x0C, 0x81, 0x04, 0x81, 0x02, 0x90, 0x08, 0x88, 0x06, 0x81, 0x0B,
0x88, 0x05, 0x88, 0x08, 0x82, 0x0D, 0x81, 0x08, 0x88, 0x02, 0x81, 0x0E,
0x84, 0x07, 0x81, 0x04, 0x90, 0x0B, 0x88, 0x0E, 0x81, 0x05, 0x90, 0x0D,
0x90, 0x0B, 0x81, 0x01, 0x81, 0x05, 0x81, 0x06, 0x90, 0x0A, 0x90, 0x0E,
0x88, 0x01, 0x81, 0x0A, 0x82, 0x01, 0x88, 0x06, 0x90, 0x08, 0x81, 0x03,
0x84, 0x0E, 0x82, 0x03, 0x88, 0x06, 0x84, 0x0E, 0x81, 0x07, 0x88, 0x01,
0x90, 0x02, 0x90, 0x09, 0x81, 0x09, 0x88, 0x06, 0x81, 0x04, 0x88, 0x0B,
0x84, 0x0D, 0x81, 0x08, 0x84, 0x04, 0x82, 0x0B, 0x81, 0x0E, 0x81, 0x0A,
0x84, 0x01, 0x81, 0x0C, 0x81, 0x07, 0x81, 0x0A, 0x81, 0x07, 0x82, 0x0B,
0x84, 0x08, 0x90, 0x0B, 0x88, 0x08, 0x84, 0x0C, 0x90, 0x0C, 0x81, 0x06,
0x80, 0x0D, 0x84, 0x0E, 0x81, 0x05, 0x90, 0x0C, 0x81, 0x02, 0x90, 0x09,
0x82, 0x0D, 0x90, 0x0E, 0x88, 0x03, 0x81, 0x09, 0x81, 0x0D, 0x88, 0x03,
0x88, 0x0C, 0x90, 0x03, 0x81, 0x0E, 0x81, 0x03, 0x81, 0x04, 0x88, 0x05,
0x88, 0x04, 0x88, 0x02, 0x88, 0x01, 0x81, 0x0C, 0x88, 0x09, 0x82, 0x0A,
0x88, 0x09, 0x84, 0x0D, 0x90, 0x0C, 0x84, 0x03, 0x88, 0x07, 0x84, 0x0D,
0x90, 0x02, 0x84, 0x09, 0x82, 0x04, 0x84, 0x0C, 0x81, 0x0E, 0x81, 0x0D,
0x82, 0x0C, 0x81, 0x03, 0x90, 0x0D, 0x81, 0x08, 0x88, 0x0A, 0x81, 0x08,
0x84, 0x0A, 0x90, 0x0D, 0x88, 0x0B, 0x81, 0x0D, 0x82, 0x08, 0x90, 0x0D,
0x90, 0x0A, 0x90, 0x04, 0x81, 0x03, 0x84, 0x02, 0x88, 0x08, 0x90, 0x05,
0x81, 0x0D, 0x81, 0x05, 0x88, 0x06, 0x84, 0x06, 0x90, 0x04, 0x82, 0x01,
0x84, 0x0C, 0x82, 0x0D, 0x88, 0x08, 0x81, 0x0C,
};
typedef struct {
@ -441,19 +699,27 @@ typedef struct {
} StGateStreamT;
static const StGateStreamT kGateStreams[] = {
{ 'A', kGateStreamA, (uint16_t)sizeof(kGateStreamA) },
{ 'B', kGateStreamB, (uint16_t)sizeof(kGateStreamB) },
{ 'C', kGateStreamC, (uint16_t)sizeof(kGateStreamC) },
{ 'D', kGateStreamD, (uint16_t)sizeof(kGateStreamD) },
{ 'E', kGateStreamE, (uint16_t)sizeof(kGateStreamE) },
{ 'F', kGateStreamF, (uint16_t)sizeof(kGateStreamF) },
{ 'G', kGateStreamG, (uint16_t)sizeof(kGateStreamG) },
{ 'I', kGateStreamI, (uint16_t)sizeof(kGateStreamI) },
{ 'J', kGateStreamJ, (uint16_t)sizeof(kGateStreamJ) },
{ 'K', kGateStreamK, (uint16_t)sizeof(kGateStreamK) },
{ 'L', kGateStreamL, (uint16_t)sizeof(kGateStreamL) },
{ 'M', kGateStreamM, (uint16_t)sizeof(kGateStreamM) },
{ 'N', kGateStreamN, (uint16_t)sizeof(kGateStreamN) },
{ 'O', kGateStreamO, (uint16_t)sizeof(kGateStreamO) },
{ 'P', kGateStreamP, (uint16_t)sizeof(kGateStreamP) },
{ 'Q', kGateStreamQ, (uint16_t)sizeof(kGateStreamQ) },
{ 'R', kGateStreamR, (uint16_t)sizeof(kGateStreamR) },
{ 'S', kGateStreamS, (uint16_t)sizeof(kGateStreamS) },
{ 'U', kGateStreamU, (uint16_t)sizeof(kGateStreamU) },
{ 'V', kGateStreamV, (uint16_t)sizeof(kGateStreamV) },
{ 'E', kGateStreamE, (uint16_t)sizeof(kGateStreamE) },
{ 'J', kGateStreamJ, (uint16_t)sizeof(kGateStreamJ) },
{ 'L', kGateStreamL, (uint16_t)sizeof(kGateStreamL) },
{ 'P', kGateStreamP, (uint16_t)sizeof(kGateStreamP) },
{ 'S', kGateStreamS, (uint16_t)sizeof(kGateStreamS) },
{ 'Y', kGateStreamY, (uint16_t)sizeof(kGateStreamY) },
};
#define ST_GATE_STREAMS (sizeof(kGateStreams) / sizeof(kGateStreams[0]))

View file

@ -1,18 +1,17 @@
// recorder -- search for a demo input stream that exercises a level.
//
// The C64 ships attract recordings for four screens (H, W, T, X). The
// other levels have none, so a refactor of their hooks had nothing to be
// checked against. This searches for a synthetic ride instead: random
// restarts plus hill climbing over (joystick mask, duration) pairs,
// scored on how long the cab survives and how much of the level it
// reaches -- hook state bytes written, fare-machine stages entered,
// distinct pictures drawn, pads landed on.
// other levels have none, so this searches for a synthetic ride instead:
// random restarts plus hill climbing over (joystick mask, duration)
// pairs, scored on how long the cab survives its first life and how much
// of the level it reaches -- hook state bytes written, fare-machine
// stages entered, distinct pictures drawn, pads landed on.
//
// The result is a REGRESSION fixture, not a recording: it pins current
// behaviour so a refactor that changes nothing observable leaves the
// trace identical. It says nothing about matching the real C64.
// The search runs on this port's simulation; what proves the ride is
// the original game: gate.sh c64 rides the stream on the C64 in VICE and
// the two traces must match byte for byte.
//
// Usage: recorder <level.dat> <letter> <seed> > stream.h
// Usage: recorder <level.dat> <letter> <seed> [restarts] [iterations] > stream.h
// Then assemble the per-level outputs into gateStreams.h.
//
// Determinism: a candidate is only useful if it replays identically from
@ -41,8 +40,10 @@ void stAudioVoice1Freq(uint8_t v){(void)v;}
#define PAIRS 160
#define STREAMLEN (PAIRS * 2)
#define MAXTICKS 1400
#define RESTARTS 120
#define ITERATIONS 3000
typedef struct { uint32_t score; uint32_t ticks; uint32_t cover; uint32_t lands; uint32_t stages; uint32_t scr; uint32_t near; } ResT;
typedef struct { uint32_t score; uint32_t ticks; uint32_t cover; uint32_t lands; uint32_t stages; uint32_t scr; uint32_t near; uint32_t exited; } ResT;
static uint32_t rs = 1;
static uint32_t rnd(void){ rs = rs*1664525u + 1013904223u; return rs>>8; }
@ -59,6 +60,7 @@ static ResT evaluate(const uint8_t *stream, uint8_t *touchOut)
ResT r;
memset(stageSeen, 0, sizeof(stageSeen));
memset(&r, 0, sizeof(r));
memset(touched, 0, sizeof(touched));
// stSimNewGame deliberately PRESERVES flameParity, waveIdx and
// walkParity (the C64 carries them from screen to screen), so a
@ -67,14 +69,22 @@ static ResT evaluate(const uint8_t *stream, uint8_t *touchOut)
// would then not reproduce from a cold start. Clear the whole struct
// so every evaluation begins exactly where the gate will.
memset(&gSim, 0, sizeof(gSim));
// Exactly as stsim starts a gate ride.
stSimNewGame(&gSim, 1u, true);
stSimSeedDemoBuffer(&gSim, kDemoBufferInit, ST_DEMO_BUFFER_BYTES);
(void)stSimAdvancePlayer(&gSim);
gSim.levelState = (uint8_t)(gLevel.levelIndex + 1u);
stSimEnterLevel(&gSim, &gLevel);
stSimSetDemoStream(&gSim, stream, STREAMLEN);
memcpy(base, &gSim.hookScratch, sizeof(base));
for (t = 0; t < MAXTICKS; t++) {
uint16_t k;
if (stSimTick(&gSim) != ST_TICK_CONTINUE) { break; }
StTickResultE res = stSimTick(&gSim);
if (res != ST_TICK_CONTINUE) {
/* Leaving through the hatch is a path of its own: reward it. */
r.exited = (res == ST_TICK_LEVEL_EXIT) ? 1u : 0u;
break;
}
{ const uint8_t *cur = (const uint8_t *)&gSim.hookScratch;
for (k = 0; k < (uint16_t)sizeof(base); k++) {
if (cur[k] != base[k]) { touched[k] = 1; } } }
@ -116,7 +126,7 @@ static ResT evaluate(const uint8_t *stream, uint8_t *touchOut)
r.ticks = t; r.cover = cover; r.lands = lands; r.stages = stages; r.scr = scrSeen;
r.near = nearest;
r.score = cover * 30000u + stages * 30000u + lands * 4000u + t * 100u + scrSeen * 200u
+ (255u - nearest) * 60u;
+ (255u - nearest) * 60u + r.exited * 300000u;
return r;
}
@ -143,20 +153,28 @@ int main(int argc, char **argv)
ResT bestR, curR;
FILE *fp;
int restart, iter;
int restarts = RESTARTS;
int iterations = ITERATIONS;
if (argc < 4) { fprintf(stderr, "usage: recorder <level.dat> <name> <seed>\n"); return 2; }
if (argc < 4) { fprintf(stderr, "usage: recorder <level.dat> <name> <seed> [restarts] [iterations]\n"); return 2; }
fp = fopen(argv[1], "rb");
if (!fp || !stLevelParse(&gLevel, fp)) { fprintf(stderr, "cannot load %s\n", argv[1]); return 1; }
fclose(fp);
rs = (uint32_t)strtoul(argv[3], NULL, 0);
if (argc > 4) {
restarts = (int)strtol(argv[4], NULL, 0);
}
if (argc > 5) {
iterations = (int)strtol(argv[5], NULL, 0);
}
randomise(best, 0);
bestR = evaluate(best, NULL);
for (restart = 0; restart < 120; restart++) {
for (restart = 0; restart < restarts; restart++) {
memcpy(cur, best, STREAMLEN);
if (restart > 0) { randomise(cur, (int)(rnd() % PAIRS)); }
curR = evaluate(cur, NULL);
for (iter = 0; iter < 3000; iter++) {
for (iter = 0; iter < iterations; iter++) {
uint8_t trial[STREAMLEN];
ResT tr;
int j, nmut = 1 + (int)(rnd() % 3u);
@ -182,10 +200,10 @@ int main(int argc, char **argv)
if (curR.score > bestR.score) { memcpy(best, cur, STREAMLEN); bestR = curR; }
}
bestR = evaluate(best, touched);
fprintf(stderr, "%s: ticks=%u hookBytes=%u stages=%u pictures=%u lands=%u nearPad=%u\n",
argv[2], bestR.ticks, bestR.cover, bestR.stages, bestR.scr, bestR.lands, bestR.near);
printf("// %s: %u ticks, %u hook bytes, %u fare stages, %u pictures, %u landings\n",
argv[2], bestR.ticks, bestR.cover, bestR.stages, bestR.scr, bestR.lands);
fprintf(stderr, "%s: ticks=%u hookBytes=%u stages=%u pictures=%u lands=%u nearPad=%u exited=%u\n",
argv[2], bestR.ticks, bestR.cover, bestR.stages, bestR.scr, bestR.lands, bestR.near, bestR.exited);
printf("// %s: %u ticks, %u hook bytes, %u fare stages, %u pictures, %u landings%s\n",
argv[2], bestR.ticks, bestR.cover, bestR.stages, bestR.scr, bestR.lands, bestR.exited ? ", left through the hatch" : "");
printf("static const uint8_t kGateStream%s[%d] = {", argv[2], STREAMLEN);
{ int i; for (i = 0; i < STREAMLEN; i++) {
if (i % 12 == 0) { printf("\n "); }

View file

@ -1,14 +1,33 @@
// stsim -- host-side driver for the Space Taxi simulation core.
//
// Loads an STL4 level, attaches a recorded demo input stream and runs
// the game tick, printing one line of state per tick in the same shape
// the VICE tracer records (see stuff/ and the compare script),
// so the port's engine can be diffed against the real machine.
// Loads an STL5 level, attaches a recorded demo input stream and runs
// the game tick, printing one line of state per tick -- exactly the line
// c64trace.py prints from the real game in VICE, so the two traces can
// be compared byte for byte.
//
// stsim <level.dat> <level letter> <ticks>
//
// H, W, T and X use the C64's own attract recordings; every other
// letter uses a synthesised gate fixture from gateStreams.h.
//
// A ride is the attract demo's input and random numbers on the screen as
// the 24-hour shift reaches it ($7215 = the screen's place, A = 1 .. Y =
// 25), on the game's crash path: a finished wreck costs a cab and the cab respawns
// (stSimLoseCab, stSimRespawn -- c64trace.py sends the real machine down
// $6B64 too), until the cabs run out, the cab leaves the screen, the
// ticks are done, or the playback reaches the buffer's last pair -- past
// that the C64 would read on into whatever follows $0B81.
//
// STSIM_DEMOBUF=1 prints the ride's whole playback buffer ($0902, the
// boot-time image with the stream laid over it) as one hex line instead
// of running it: c64trace.py loads exactly those bytes into the real
// game, so both machines ride the same input.
//
// STSIM_RAW=<file> also appends each tick's picture to <file> -- screen
// RAM, colour RAM and the effective charset, 1000 + 1000 + 2048 bytes a
// tick -- so a hash mismatch can be traced to the cells and glyphs that
// differ (c64trace.py writes the same layout from the real machine, and
// compare.py --picture lists the differences).
#include <stdio.h>
#include <stdlib.h>
@ -20,9 +39,33 @@
#include "gateStreams.h"
#define ST_CHARSET_BYTES (ST_CHARSET_CHARS * 8u)
// The playback buffer's last (mask, count) pair: reading on from it, the
// sim stops at the buffer's end while the C64 reads past $0B81.
#define ST_DEMO_LAST_PAIR (ST_DEMO_BUFFER_BYTES - 2u)
static void celCensusReport(const StLevelT *level);
static void celCensusTick(const StSimT *sim);
static uint32_t charsetHash(const StSimT *sim);
static void charsetImage(const StSimT *sim, uint8_t *image);
static uint32_t hashBytes(const uint8_t *p, size_t n);
static void printDemoBuffer(const StSimT *sim);
static void printState(const StSimT *sim, uint32_t tick);
static void writeRaw(FILE *raw, const StSimT *sim);
static StLevelT gLevel;
static StSimT gSim;
// STSIM_CELS=1: every level cel (ptr below the baked set) a ride
// displays, as (ptr, colour, multi, mc0, mc1), against what
// stLevelCelList bakes for the level -- a cel shown but not listed is
// built and drawn interpreted mid-ride on the real ports.
#define CENSUS_MAX 64u
static StCelDefT gCensus[CENSUS_MAX];
static uint8_t gCensusCount;
void stAudioSfx(const uint8_t *program9) { (void)program9; }
void stAudioNoise(bool on) { (void)on; }
@ -34,45 +77,24 @@ void stAudioVoice2(uint8_t freqLo, uint8_t freqHi, uint8_t ctrl) { (void)freqLo;
void stAudioVoice1Freq(uint8_t value) { (void)value; }
// FNV-1a over a byte span: the picture the player actually sees. The
// hooks that animate the screen (O, R, V, W, X) change screen, colour or
// charset bytes and nothing else this line used to capture.
static uint32_t hashBytes(const uint8_t *p, size_t n) {
uint32_t h = 2166136261u;
size_t i;
static void celCensusReport(const StLevelT *level) {
StCelDefT listed[ST_LEVEL_CELS_MAX];
uint8_t n = stLevelCelList(level, listed, ST_LEVEL_CELS_MAX);
uint8_t k;
uint8_t j;
for (i = 0u; i < n; i++) {
h = (h ^ p[i]) * 16777619u;
for (k = 0u; k < gCensusCount; k++) {
for (j = 0u; j < n; j++) {
if (memcmp(&listed[j], &gCensus[k], sizeof(StCelDefT)) == 0) {
break;
}
}
fprintf(stderr, "CEL %s ptr=%02X color=%u multi=%u mc=%u/%u\n", (j < n) ? "listed " : "MISSING", gCensus[k].ptr, gCensus[k].color, gCensus[k].multi, gCensus[k].mc0, gCensus[k].mc1);
}
return h;
fprintf(stderr, "CENSUS shown=%u listed=%u\n", gCensusCount, n);
}
// The charset the game would be showing. The sim no longer holds a
// contiguous 2 KB copy -- edited glyphs live in an override table -- so
// rebuild the effective image here. Hashing this keeps the trace value
// identical to the pre-override captures, which is what lets the goldens
// prove the change is behaviour-preserving.
static uint32_t charsetHash(const StSimT *sim) {
uint8_t image[ST_CHARSET_CHARS * 8u];
uint16_t ch;
for (ch = 0u; ch < ST_CHARSET_CHARS; ch++) {
memcpy(&image[ch * 8u], stSimGlyph(sim, (uint8_t)ch), 8u);
}
return hashBytes(image, sizeof(image));
}
// STSIM_CELS=1: every level cel (ptr below the baked set) a ride
// displays, as (ptr, colour, multi, mc0, mc1), against what
// stLevelCelList bakes for the level -- a cel shown but not listed is
// built and drawn interpreted mid-ride on the real ports.
#define CENSUS_MAX 64u
static StCelDefT gCensus[CENSUS_MAX];
static uint8_t gCensusCount;
static void celCensusTick(const StSimT *sim) {
uint8_t i;
@ -105,40 +127,75 @@ static void celCensusTick(const StSimT *sim) {
}
static void celCensusReport(const StLevelT *level) {
StCelDefT listed[ST_LEVEL_CELS_MAX];
uint8_t n = stLevelCelList(level, listed, ST_LEVEL_CELS_MAX);
uint8_t k;
uint8_t j;
// The charset the game would be showing. The sim no longer holds a
// contiguous 2 KB copy -- edited glyphs live in an override table -- so
// rebuild the effective image here. Hashing this keeps the trace value
// identical to the pre-override captures, which is what lets the goldens
// prove the change is behaviour-preserving.
static uint32_t charsetHash(const StSimT *sim) {
uint8_t image[ST_CHARSET_BYTES];
for (k = 0u; k < gCensusCount; k++) {
for (j = 0u; j < n; j++) {
if (memcmp(&listed[j], &gCensus[k], sizeof(StCelDefT)) == 0) {
break;
}
}
fprintf(stderr, "CEL %s ptr=%02X color=%u multi=%u mc=%u/%u\n", (j < n) ? "listed " : "MISSING", gCensus[k].ptr, gCensus[k].color, gCensus[k].multi, gCensus[k].mc0, gCensus[k].mc1);
}
fprintf(stderr, "CENSUS shown=%u listed=%u\n", gCensusCount, n);
charsetImage(sim, image);
return hashBytes(image, sizeof(image));
}
static void celCensusReport(const StLevelT *level);
static void celCensusTick(const StSimT *sim);
static void charsetImage(const StSimT *sim, uint8_t *image) {
uint16_t ch;
for (ch = 0u; ch < ST_CHARSET_CHARS; ch++) {
memcpy(&image[ch * 8u], stSimGlyph(sim, (uint8_t)ch), 8u);
}
}
// FNV-1a over a byte span: the picture the player actually sees. The
// hooks that animate the screen (O, R, V, W, X) change screen, colour or
// charset bytes and nothing else this line used to capture.
static uint32_t hashBytes(const uint8_t *p, size_t n) {
uint32_t h = 2166136261u;
size_t i;
for (i = 0u; i < n; i++) {
h = (h ^ p[i]) * 16777619u;
}
return h;
}
static void printDemoBuffer(const StSimT *sim) {
size_t k;
printf("{\"demobuf\":\"");
for (k = 0u; k < sizeof(sim->demoBuf); k++) {
printf("%02X", sim->demoBuf[k]);
}
printf("\"}\n");
}
static void printState(const StSimT *sim, uint32_t tick) {
bool shown1 = (sim->frame.enableMask & kStBit[1]) != 0u;
uint8_t k;
printf("{\"tick\":%u,\"x\":\"%02X%02X.%02X\",\"y\":\"%02X.%02X\",\"vx\":\"%04X\",\"vy\":\"%04X\","
"\"pad\":%u,\"st\":%u,\"ph\":%u,\"in\":\"%02X\",\"dir\":\"%02X\",\"ptr0\":\"%02X\",\"ptr1\":\"%02X\","
"\"s1\":\"%02X%02X.%02X\",\"en\":\"%02X\",\"t4740\":\"%02X\",\"off\":%u,\"rng\":\"%02X%02X\","
"\"fuel\":%u,\"slots\":%u,\"asi\":%u,\"score\":\"",
"\"pad\":%u,\"st\":%u,\"ph\":%u,\"in\":\"%02X\",\"dir\":\"%02X\",\"ptr0\":\"%02X\",",
tick, sim->posXmsb, sim->posXcol, sim->posXlo, sim->posYrow, sim->posYlo,
(uint16_t)sim->velX, (uint16_t)sim->velY, sim->activePad, sim->stage, sim->collisionPhase,
sim->inputMask, sim->dirMask, sim->spr[0].ptr, sim->spr[1].ptr,
sim->spr[1].msb, sim->spr[1].col, sim->spr[1].row, sim->frame.enableMask,
sim->demoTimer, sim->demoOff, sim->rngT1, sim->rngT2, sim->fuelCells, sim->fareSlotCount, sim->activeSpriteIdx);
sim->inputMask, sim->dirMask, sim->spr[0].ptr);
// A hidden sprite's registers are not part of the picture, and the
// C64 leaves whatever the screen before set in them: sprite 1's
// shadow and every hidden sprite's displayed position print as
// dashes, on the real machine's trace too.
if (shown1) {
printf("\"ptr1\":\"%02X\",\"s1\":\"%02X%02X.%02X\",", sim->spr[1].ptr, sim->spr[1].msb, sim->spr[1].col, sim->spr[1].row);
} else {
printf("\"ptr1\":\"--\",\"s1\":\"--\",");
}
printf("\"en\":\"%02X\",\"t4740\":\"%02X\",\"off\":%u,\"rng\":\"%02X%02X\","
"\"fuel\":%u,\"slots\":%u,\"asi\":%u,\"score\":\"",
sim->frame.enableMask, sim->demoTimer, sim->demoOff, sim->rngT1, sim->rngT2,
sim->fuelCells, sim->fareSlotCount, sim->activeSpriteIdx);
for (k = 0u; k < ST_NUMBER_CHARS; k++) {
printf("%02X", sim->screen[ST_CELL_SCORE + k]);
}
@ -152,7 +209,11 @@ static void printState(const StSimT *sim, uint32_t tick) {
}
printf("\",\"sprx\":\"");
for (k = 0u; k < ST_HW_SPRITES; k++) {
printf("%03X%02X", sim->frame.x[k], sim->frame.y[k]);
if ((sim->frame.enableMask & kStBit[k]) != 0u) {
printf("%03X%02X", sim->frame.x[k], sim->frame.y[k]);
} else {
printf("-----");
}
}
printf("\",\"scr\":\"%08X\",\"col\":\"%08X\",\"chr\":\"%08X\",\"bd\":\"%02X\",\"bg\":\"%02X\"}\n",
hashBytes(sim->screen, ST_SCREEN_CELLS),
@ -162,8 +223,21 @@ static void printState(const StSimT *sim, uint32_t tick) {
}
// STSIM_RAW: one tick's picture (see the top of the file).
static void writeRaw(FILE *raw, const StSimT *sim) {
uint8_t image[ST_CHARSET_BYTES];
charsetImage(sim, image);
fwrite(sim->screen, 1u, ST_SCREEN_CELLS, raw);
fwrite(sim->color, 1u, ST_SCREEN_CELLS, raw);
fwrite(image, 1u, sizeof(image), raw);
}
int main(int argc, char **argv) {
FILE *fp;
FILE *raw = NULL;
const char *rawPath;
const uint8_t *stream = kDemoStreamH;
uint16_t len = (uint16_t)sizeof(kDemoStreamH);
uint8_t want;
@ -205,34 +279,63 @@ int main(int argc, char **argv) {
}
}
ticks = (uint32_t)strtoul(argv[3], NULL, 0);
// A fresh one-player demo, the flame parity, wave index and walk
// parity at zero (c64trace.py zeroes them on the real machine too).
// The recorder starts its rides the same way.
stSimNewGame(&gSim, 1u, true);
stSimSeedDemoBuffer(&gSim, kDemoBufferInit, ST_DEMO_BUFFER_BYTES);
if (argc > 4) {
// carried-over state: flame parity, wave index, walk parity
gSim.flameParity = (uint8_t)strtoul(argv[4], NULL, 16);
}
if (argc > 5) {
gSim.waveIdx = (uint8_t)strtoul(argv[5], NULL, 16);
}
if (argc > 6) {
gSim.walkParity = (uint8_t)strtoul(argv[6], NULL, 16);
}
// $61FB, as startDemoScreen runs it: the turn wraps to player 0 and
// the screens-played count ($7215, this one included) is set before
// the screen is loaded -- to the screen's place in the 24-hour shift
// (A is the first, Y the 25th), so the rides cover the takeoff fare of
// every screen after the second and the finished shift's HUD.
(void)stSimAdvancePlayer(&gSim);
gSim.levelState = (uint8_t)(gLevel.levelIndex + 1u);
stSimEnterLevel(&gSim, &gLevel);
stSimSetDemoStream(&gSim, stream, len);
if (getenv("STSIM_DEMOBUF") != NULL) {
printDemoBuffer(&gSim);
return 0;
}
rawPath = getenv("STSIM_RAW");
if (rawPath != NULL) {
raw = fopen(rawPath, "wb");
if (raw == NULL) {
fprintf(stderr, "stsim: cannot write %s\n", rawPath);
return 1;
}
}
for (t = 0u; t < ticks; t++) {
StTickResultE r;
printState(&gSim, t);
if (raw != NULL) {
writeRaw(raw, &gSim);
}
r = stSimTick(&gSim);
if (getenv("STSIM_CELS") != NULL) {
celCensusTick(&gSim);
}
if (r == ST_TICK_CRASH_DONE) {
r = stSimLoseCab(&gSim);
if (r == ST_TICK_LIFE_LOST) {
stSimRespawn(&gSim);
r = ST_TICK_CONTINUE;
}
}
if (r != ST_TICK_CONTINUE) {
printf("{\"tick\":%u,\"end\":%d}\n", t, (int)r);
break;
}
if (gSim.demoOff >= ST_DEMO_LAST_PAIR) {
printf("{\"tick\":%u,\"end\":\"stream\"}\n", t);
break;
}
}
if (getenv("STSIM_CELS") != NULL) {
celCensusReport(&gLevel);
}
if (raw != NULL) {
fclose(raw);
}
return 0;
}