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 frame-rate gate (count down each frame, reload when zero) so each
phase handler runs every N frames, not every frame. 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) ### Collision dispatch trace (VERIFIED via emulator)
Verified by running `$6966` from `Tracer.from_dump('raw.bin')` with 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 ### `$61BD` -- passengerEventDraw
If `$716F != 0` (a passenger event just happened): peek `$71CD` CORRECTED 2026-10-06 (checked against VICE by the stsim gate): once the
(event type) and dispatch: 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 | | `$71CD` | Landed... | Action |
| ------- | ----------- | ---------------------------------- | | ------- | ------------------------------------------- | --------------------------------------- |
| 1 | (drop-off?) | $6A/$6B = $6C6C | | 1 | while a fare waits on another pad ($64F3) | speak `$6C6C` "HT" (HEY TAXI) |
| 2 | (pickup?) | $6A/$6B = $6C7A | | 2 | with a fare aboard, on the wrong pad ($64DF) | `$43A5` (the meter is LOST), speak `$6C7A` "P<digit>" |
| other | (takeoff?) | $6A/$6B = $6C7D | | 3 | the same, the fare wanting UP ($64E7) | `$43A5`, speak `$6C7D` "U" |
`$6C7F` is "render text/scoreboard": marshals shadow, flushes, `$6C7F` is the speech player (one sample per character); it zeroes
then draws indirect text. So this routine surfaces a HUD blurb `$71CD` (`$6C99`) and sets both release timers `$7216/$7217` to 1. So any
based on what just happened. 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 ### `$63DD` -- fireButtonEdge
@ -877,26 +911,25 @@ kills all audio, then either loops waiting for release or transitions.
### `$6419` -- fuelBarHud ### `$6419` -- fuelBarHud
``` CORRECTED 2026-10-06 (the old transcript here had the branches wrong):
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
LDA $714F (Y-vel hi); BMI down ; sign-test ```
BEQ check_X ; Y vel = 0 LDA #$0B; STA $DBA2,$DBA3,$DBCA,$DBCB ; both lamp pairs dark grey
A=$02, X=$28; JMP write INC $71BE; AND #$07; STA $71BE ; mod-8 ticker
check_X: CMP #$05; BPL RTS ; lit on 5 ticks of 8
LDA $714E
BMI down LDA $714F (Y-vel hi); BMI $6449 ; climbing -> cyan
A=$03, X=$00; JMP write BEQ $6444
down: A=$02, X=$28; JMP write ; hi > 0 -> red, row 24
A=$07, X=$00 $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 write: STA $DBA2,X; STA $DBA3,X; RTS
``` ```
So the fuel bar's color depends on cab Y-velocity sign (rising vs So it is a descent-rate lamp: red on row 24 when the cab falls a whole
descending vs hovering). The `$DBA2-$DBCB` cells form the in-flight sub-row a tick or faster, yellow on row 23 from half that, cyan on row
indicator strip. 23 otherwise (any climb, a hover, a slow fall).
## IRQ handler (`$CF52`) -- music only, no animation ## IRQ handler (`$CF52`) -- music only, no animation

View file

@ -3,7 +3,7 @@
# and program.mk describes the game to them. # and program.mk describes the game to them.
# #
# make every target whose toolchain is installed # 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 check build, then run the gates (scripts/verify.sh)
# make clean remove build/ # make clean remove build/
# make help JoeyLib's project targets # 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 written from its disassembly (MECHANICS.md): the flight physics, the fare
machine, the 24 levels' hook programs, the title and level intros, the 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 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 SID player) and the speech. The simulation (stSim.c) is checked tick for
tick against VICE traces of the C64's four attract-demo rides (VERIFIED.md), tick against the original game running in VICE: a ride on each of the 25
and the gates below hold it there. 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 ## Setting up
@ -97,7 +98,7 @@ $ scripts/verify.sh disk # or name the ones to run
| Gate | What it proves | | 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`) | | `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 | | `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 | | `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
- `tools/stsim` -- the simulation on the host: prints the per-tick state a - `tools/stsim` -- the simulation on the host, and its C64 counterpart:
VICE trace records, so `compare.py` can diff the port against the C64. `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/staxiharness` -- the front-end screen checks.
- `tools/staxibake` -- writes the cel bank the build pre-compiles per machine. - `tools/staxibake` -- writes the cel bank the build pre-compiles per machine.
- `assets/` -- the generators behind the committed C64 tables - `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 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 cycles a star), not bytes moved, so it becomes feasible with a leaner
save/draw/restore path for small cels in JoeyLib. 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. | | `$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. | | `$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) ## UNVERIFIED (acknowledged, not yet proven)
| Claim | Why unverified | | Claim | Why unverified |

View file

@ -9,12 +9,13 @@
# mkstlevel tool stays in tree for hand-authored .txt levels. # mkstlevel tool stays in tree for hand-authored .txt levels.
# #
# Usage: # 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 # romToLevel.py <raw-dump> <out-dir> <scene_id> # one specific
# #
# Writes: # Writes:
# <out-dir>/level01.dat ... level24.dat (scene 0 = "A" = level01) # <out-dir>/level01.dat ... level25.dat (scene 0 = "A" = level01,
# <out-dir>/tbank0.png (charset extracted from $2800) # scene 24 = "Y" = level25)
# <out-dir>/title.dat
import os import os
import struct import struct
@ -49,11 +50,18 @@ ST_TILE_PX = 8
VIC_X_ORIGIN = 24 VIC_X_ORIGIN = 24
VIC_Y_ORIGIN = 50 VIC_Y_ORIGIN = 50
PAD_SURFACE_ORIGIN = 36 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): 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 Everything stays in the C64's own units so the runtime can mirror the
original routines byte for byte: 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 passenger X frac, passenger X col, unused
tilemap[1000] screen RAM (all 25 rows, the HUD rows included) tilemap[1000] screen RAM (all 25 rows, the HUD rows included)
colormap[1000] colour RAM low nibbles 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 spriteCount N, then N x (ptr, 63 bytes): the per-level sprite
bitmaps the level's extra scene loads at $2000+ 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 cells = ST_TILEMAP_W * ST_PLAYFIELD_ROWS
assert len(screen) == cells, f'screen is {len(screen)}, need {cells}' assert len(screen) == cells, f'screen is {len(screen)}, need {cells}'
assert len(colormap) == cells, f'colormap is {len(colormap)}, need {cells}' assert len(colormap) == cells, f'colormap is {len(colormap)}, need {cells}'
with open(path, 'wb') as fp: 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[0x7D78:0x7D78 + 22]))
fp.write(bytes(after[0x7D00:0x7D09])) fp.write(bytes(after[0x7D00:0x7D09]))
fp.write(bytes([after[0x7D5A], after[0x7D09], after[0x7D60]])) 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: for ptr, data in sprite_blocks:
fp.write(bytes([ptr])) fp.write(bytes([ptr]))
fp.write(bytes(data)) 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 # 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) 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): def extract_title(ram, out_dir):
"""The title screen is scene 25 (what $477A decompresses). """The title screen is scene 25 (what $477A decompresses).
Pads, spawn and templates are dummies.""" Pads, spawn and templates are dummies."""
@ -183,22 +174,17 @@ def main():
if len(sys.argv) >= 4: if len(sys.argv) >= 4:
scene_ids = [int(sys.argv[3], 0)] scene_ids = [int(sys.argv[3], 0)]
else: else:
# Levels A..X = scenes 0..23. The title comes from screen RAM # Screens A..Y = scenes 0..24, then the title (scene 25).
# at $0400 (the dump was taken on the title screen), not from scene_ids = list(range(len(LEVEL_LETTERS)))
# a decompressed scene.
scene_ids = list(range(24))
for sid in scene_ids: for sid in scene_ids:
path, pad_count, nspr = extract_one_level(ram, sid, out_dir) path, pad_count, nspr = extract_one_level(ram, sid, out_dir)
size = os.path.getsize(path) size = os.path.getsize(path)
print(f' scene {sid:2d} (level {LEVEL_LETTERS[sid]}): pads={pad_count} sprites={nspr} size={size} -> {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: 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) 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__': if __name__ == '__main__':

View file

@ -41,13 +41,14 @@ STAXI_GEN := $(PROJECT_BUILD)/generated/$(ASSET_PLATFORM)
STAXI_RUN := $(call programDataDir,STAXI) STAXI_RUN := $(call programDataDir,STAXI)
STAXI_OPT := $(call programOptionalDir,STAXI) STAXI_OPT := $(call programOptionalDir,STAXI)
# 24 lettered levels (A..X) from scenes 0..23 plus the title from scene 24, # The 25 screens -- A..X from scenes 0..23 and the MYSTERY SCREEN, Y, from
# extracted by assets/romToLevel.py, which runs the captured decompressor # scene 24 -- plus the title from scene 25, extracted by assets/romToLevel.py,
# through a Python 6502 emulator over the raw RAM dump and writes STL2 .dat # which runs the captured decompressor through a Python 6502 emulator over the
# files directly. (mkstlevel + title.txt is no longer used: the C64 title # raw RAM dump and writes STL5 .dat files directly. (mkstlevel + title.txt is
# encodes its screen with the game's custom charset, so a hand-authored tile # no longer used: the C64 title encodes its screen with the game's custom
# layout with a different glyph mapping renders as garbage.) # charset, so a hand-authored tile layout with a different glyph mapping
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 # 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 STAXI_LEVELS := $(foreach n,$(STAXI_LEVEL_NUMS),levels/level$(n).dat) levels/title.dat
ROMTOLEVEL := $(STAXI_HOME)/assets/romToLevel.py 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 # cels at entry -- so they are optional data, which the IIgs data disk packs
# last, while there is room. # last, while there is room.
# ...except on the X68000, where they do not fit. The Human68k floppy is # ...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, # 1232 KB and, measured when there were eight, the banks were 787 KB of it
# level10 175, level17 165), which with the 296 KB binary, the levels and # (level09 alone 236 KB, level10 175, level17 165), which with the 296 KB
# the speech put the disk about 300 KB over however small speech gets. # 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 # stRenderLevelCels already handles their absence the right way: it builds
# and compiles the level's cels at ENTRY, behind the LOADING bar, so the # 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 # 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 # 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 # are big and their JIT is fast. Ship them there again if the payload ever
# moves to a hard disk image. # 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 # 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 # stCels.c + C64 bitmaps) and compiled for this target by the host

View file

@ -1,9 +1,10 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# verify.sh - Space Taxi's gates, run one after another, with a summary. # 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 # stsim every screen's ride (all 25) traces byte for byte as the real
# its golden (tools/stsim/gate.sh), and every cel a ride shows # C64 traced it in VICE -- gateHashes.txt holds the C64 traces'
# is baked (tools/stsim/celGate.sh) # 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 # harness the front-end screens above the simulation -- the high-score
# table, name entry and its save -- on JoeyLib's host BLANK # table, name entry and its save -- on JoeyLib's host BLANK
# port (tools/staxiharness) # port (tools/staxiharness)

View file

@ -8,6 +8,8 @@
// pad has been used the destination becomes "UP PLEASE" and the top // pad has been used the destination becomes "UP PLEASE" and the top
// hatch opens. // hatch opens.
#include <string.h>
#include "stSim.h" #include "stSim.h"
// Screen-code strings ($6C1E..$6C7D). All ASCII-compatible glyphs. // 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 kSpeechPad[] = "PX?"; // $6C72 (X = digit)
static const uint8_t kSpeechThanks[] = "!"; // $6C76 static const uint8_t kSpeechThanks[] = "!"; // $6C76
static const uint8_t kSpeechHey[] = "H"; // $6C78 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). // 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 kWaveCels[4] = { 0xC6, 0xC7, 0xD9, 0xC7 };
static const uint8_t kFareBlob[7] = { 0x66, 0x66, 0x66, 0x6F, 0x77, 0x74, 0x74 }; 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 void beamIn(StSimT *sim);
static bool decayStep(StSimT *sim); static bool decayStep(StSimT *sim);
static void drawPadPlease(StSimT *sim);
static void idleTicker(StSimT *sim); static void idleTicker(StSimT *sim);
static void lightOnAndLatch(StSimT *sim); static void lightOnAndLatch(StSimT *sim);
static void newFare(StSimT *sim); static void newFare(StSimT *sim);
static void sayPadPhrase(StSimT *sim, const uint8_t *phrase);
static void stageBoard(StSimT *sim); static void stageBoard(StSimT *sim);
static void stageCleanup(StSimT *sim); static void stageCleanup(StSimT *sim);
static void stageDropIn(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 // $660B -- a new fare turns up on a 3% roll per tick or when the
// 100-tick timer runs out, never mid-crash. // 100-tick timer runs out, never mid-crash.
static void idleTicker(StSimT *sim) { 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. // $66DD -- the passenger shrinks into the cab, then picks where to go.
static void stageBoard(StSimT *sim) { static void stageBoard(StSimT *sim) {
if (!decayStep(sim)) { if (!decayStep(sim)) {
@ -139,16 +175,8 @@ static void stageBoard(StSimT *sim) {
if (sim->activeSpriteIdx == ST_FARE_DEST_UP) { if (sim->activeSpriteIdx == ST_FARE_DEST_UP) {
return; return;
} }
stSimDrawText(sim, 14u, 24u, kTextPadPlease, 1u); drawPadPlease(sim);
stSimPutChar(sim, 18u, 24u, (uint8_t)(sim->activeSpriteIdx + 0x30u)); sayPadPhrase(sim, kSpeechPad);
{
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);
}
} }
@ -251,7 +279,8 @@ static void walkToTarget(StSimT *sim) {
// $6C7F -- the passenger speaks: one sample per character. // $6C7F -- the passenger speaks: one sample per character.
void stFareBoardingSound(StSimT *sim, const uint8_t *speech) { 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(); stAudioSilence();
stAudioNoise(false); stAudioNoise(false);
stAudioSpeechPhrase(speech); 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 // $6EF0 -- while the passenger walks (to or from the cab) the pad stays
// lit only while the two sprites touch. // lit only while the two sprites touch.
void stFarePadLightingGate(StSimT *sim) { 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, // $6CB8 -- no fare pad left: the passenger wants to leave the screen,
// so the top hatch opens. // so the top hatch opens.
void stFareUpPlease(StSimT *sim) { void stFareUpPlease(StSimT *sim) {

View file

@ -1,6 +1,6 @@
// Space Taxi -- where each level's hook program keeps its tables. // 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 // StLevelT.hookData), so the same numbers serve the hook programs
// (stHooks.c, reading through the live simulation) and the cel manifest // (stHooks.c, reading through the live simulation) and the cel manifest
// (stLevelCels.c, reading the level file alone -- the offline cel baker // (stLevelCels.c, reading the level file alone -- the offline cel baker
@ -10,6 +10,7 @@
// Level indices (0 = A). // Level indices (0 = A).
#define ST_LEVEL_E 4u #define ST_LEVEL_E 4u
#define ST_LEVEL_F 5u
#define ST_LEVEL_G 6u #define ST_LEVEL_G 6u
#define ST_LEVEL_H 7u #define ST_LEVEL_H 7u
#define ST_LEVEL_I 8u #define ST_LEVEL_I 8u
@ -26,10 +27,20 @@
#define ST_LEVEL_V 21u #define ST_LEVEL_V 21u
#define ST_LEVEL_W 22u #define ST_LEVEL_W 22u
#define ST_LEVEL_X 23u #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. // Hardware sprites 3..7 are the hook sprites on every level that has any.
#define ST_HOOK_FIRST_SPRITE 3u #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" ---- // ---- level G "TELEPORTS" ----
#define ST_HK_G_CEL 0x7DD4u // 6 cels the orbs cycle through #define ST_HK_G_CEL 0x7DD4u // 6 cels the orbs cycle through
#define ST_HK_G_CEL_PHASES 6u #define ST_HK_G_CEL_PHASES 6u
@ -82,4 +93,19 @@
#define ST_HK_U_CEL_PHASES 4u #define ST_HK_U_CEL_PHASES 4u
#define ST_HK_U_COLOR 7u #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 #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> #include <stdio.h>

View file

@ -46,6 +46,9 @@ uint8_t stLevelCelList(const StLevelT *level, StCelDefT *out, uint8_t max) {
uint8_t c; uint8_t c;
switch (level->levelIndex) { 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: case ST_LEVEL_G:
for (k = 0u; k < ST_HK_G_CEL_PHASES; k++) { 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); 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; break;
case ST_LEVEL_I: case ST_LEVEL_I:
// Every cel in the idle colour and in each of the three the // 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++) { 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); 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++) { 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++) { 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; break;
@ -91,15 +95,10 @@ uint8_t stLevelCelList(const StLevelT *level, StCelDefT *out, uint8_t max) {
} }
break; break;
case ST_LEVEL_Q: case ST_LEVEL_Q:
// The hook's cels show HIRES in colour 6 for the whole recorded // Multicolour: the level's second prelude hook ($7D9B) sets
// ride (tools/stsim STSIM_CELS census, 2026-09-22); the // $D01C to $FF after the cab spawn set it to $07.
// multicolour variant (the level's own $D025/$D026) stays
// listed for the phases the census did not reach.
for (k = 0u; k < ST_HK_Q_CEL_PHASES; k++) { 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); 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]);
}
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]);
} }
break; break;
case ST_LEVEL_U: 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); celAdd(out, &n, max, HKL(level, ST_HK_U_CEL + k), ST_HK_U_COLOR, 0u, 0u, 0u);
} }
break; 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: default:
break; 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. // by the game and the host-side simulation harness.
// //
// Layout (all bytes, written by assets/romToLevel.py): // Layout (all bytes, written by assets/romToLevel.py):
// "STL4" // "STL5"
// name[22] $7D78 level name in screen codes // name[22] $7D78 level name in screen codes
// header[9] $7D00 VIC colour block // header[9] $7D00 VIC colour block
// padCount, specialPad, fuelRate // padCount, specialPad, fuelRate
@ -12,7 +12,7 @@
// screen[1000], color[1000] // screen[1000], color[1000]
// levelIndex // levelIndex
// spriteCount, then spriteCount x (ptr, 63 bytes) // spriteCount, then spriteCount x (ptr, 63 bytes)
// hookData[616] $7D98..$7FFF // hookData[819] $7D98..$80CA
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
@ -36,7 +36,7 @@ bool stLevelParse(StLevelT *out, FILE *fp) {
uint8_t k; uint8_t k;
memset(out, 0, sizeof(*out)); 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; return false;
} }
if (!readBlock(fp, out->name, ST_LEVEL_NAME_CHARS)) { 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); ST_LOG("spacetaxi: ! only %u of %u cels built", used, expect);
return false; 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; 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_SONG_BONUS 4u // level-25 start and the bonus cab
#define ST_JINGLE_LEVEL_25 0x19u // $7215 compared against #$19 #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). // Screen-code strings the engine writes (all ASCII-compatible glyphs).
// $6C38 -- the 11-character blank that clears the message row. // $6C38 -- the 12-character blank that clears the message row (every
static const uint8_t kTextBlank[] = " "; // 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 // $43B1 -- the fare meter's blank template: three leading blanks, then
// "0.00" in the inverse-video digit glyphs. // "0.00" in the inverse-video digit glyphs.
static const uint8_t kHudTemplate[ST_NUMBER_CHARS] = { 0x66, 0x66, 0x66, 0x74, 0x77, 0x74, 0x74 }; 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 drawScreensCount(StSimT *sim);
static void edgeReflect(StSimT *sim); static void edgeReflect(StSimT *sim);
static void fireButtonEdge(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 fuelBarHud(StSimT *sim);
static void fuelTick(StSimT *sim); static void fuelTick(StSimT *sim);
static void hudDraw(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 rowWindow(const uint8_t *row, uint8_t s, uint8_t *w);
static void setCell(StSimT *sim, uint16_t cell, uint8_t ch); static void setCell(StSimT *sim, uint16_t cell, uint8_t ch);
static void setColorCell(StSimT *sim, uint16_t cell, uint8_t color); 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 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 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 bool spriteTouchesBackground(StSimT *sim, uint8_t idx);
static void spritesOffReset(StSimT *sim); 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 taxiSpawnInit(StSimT *sim);
static void taxiSpriteCelSelect(StSimT *sim); static void taxiSpriteCelSelect(StSimT *sim);
static uint8_t validateDigit(uint8_t ch); 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) // $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->collisionPhase++;
sim->phaseReload = 2u; sim->phaseReload = 2u;
sim->phaseTimer = 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. // $6B4C -- 70-tick pause, then the life bookkeeping.
static StTickResultE collisionPhase3(StSimT *sim) { static StTickResultE collisionPhase3(StSimT *sim) {
uint8_t p;
sim->phaseTimer--; sim->phaseTimer--;
if (sim->phaseTimer != 0u) { if (sim->phaseTimer != 0u) {
return ST_TICK_CONTINUE; return ST_TICK_CONTINUE;
@ -283,142 +300,18 @@ static StTickResultE collisionPhase3(StSimT *sim) {
if (sim->demoMode != 0u) { if (sim->demoMode != 0u) {
return ST_TICK_CRASH_DONE; return ST_TICK_CRASH_DONE;
} }
if (sim->stage == ST_STAGE_RIDING) { return stSimLoseCab(sim);
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;
} }
// $6966 -- what its read of $D01E/$D01F sees: this tick's frame OR the
// The C64 reads two VIC collision registers each frame: $D01F (a sprite // frame drawn before the flush (see prevFrameSpriteColl in StSimT).
// 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.
static void computeCollisions(StSimT *sim) { static void computeCollisions(StSimT *sim) {
uint8_t ss = 0u; uint8_t ss = frameSpriteCollisions(sim);
uint8_t bg = 0u;
uint8_t b;
uint8_t r;
for (b = 1u; b < ST_HW_SPRITES; b++) { sim->spriteSpriteColl = (uint8_t)(ss | sim->prevFrameSpriteColl);
if ((sim->bgCollSprites & kStBit[b]) == 0u || (sim->frame.enableMask & kStBit[b]) == 0u) { sim->spriteBgColl = (uint8_t)(frameBgCollisions(sim) | sim->prevFrameBgColl);
continue; sim->prevFrameSpriteColl = ss;
}
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;
} }
@ -446,12 +339,12 @@ static void crashStart(StSimT *sim) {
sim->phaseTimer = 2u; sim->phaseTimer = 2u;
sim->phaseReload = 2u; sim->phaseReload = 2u;
stAudioNoise(false); stAudioNoise(false);
sim->eventDispatchType = 0u; sim->eventDispatchType = ST_EVENT_NONE;
sim->activePad = 0u; sim->activePad = 0u;
sim->spr[2].enable = 0u; sim->spr[2].enable = 0u;
sim->spr[2].row = 0u; sim->spr[2].row = 0u;
sim->velY = 0x0303; sim->velY = 0x0303;
stAudioSfx(stC64SfxProgram(ST_SFX_CRASH)); stSimSfx(sim, stC64SfxProgram(ST_SFX_CRASH));
sim->thrustSweep = 0xA0u; sim->thrustSweep = 0xA0u;
if (sim->stage == ST_STAGE_WALK_TO_CAB) { if (sim->stage == ST_STAGE_WALK_TO_CAB) {
sim->stage--; sim->stage--;
@ -529,7 +422,7 @@ static void fireButtonEdge(StSimT *sim) {
} }
sim->fireWasHeld = 0x10u; sim->fireWasHeld = 0x10u;
sim->spr[0].ptr ^= 1u; 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 // The displayed frame's $D01F bits over the current picture -- only the
// fuel gauge flash red (rising), cyan (level) or yellow (falling). // bits anyone reads: the cab's, and those of the hazards levels J and P
static void fuelBarHud(StSimT *sim) { // opt in through bgCollSprites.
uint16_t cell; static uint8_t frameBgCollisions(StSimT *sim) {
uint8_t color; uint8_t bg = 0u;
uint8_t b;
setColorCell(sim, ST_CELL(23, 10), 0x0Bu); for (b = 0u; b < ST_HW_SPRITES; b++) {
setColorCell(sim, ST_CELL(23, 11), 0x0Bu); if ((sim->frame.enableMask & kStBit[b]) == 0u) {
setColorCell(sim, ST_CELL(24, 10), 0x0Bu); continue;
setColorCell(sim, ST_CELL(24, 11), 0x0Bu); }
sim->fuelBarTick = (uint8_t)((sim->fuelBarTick + 1u) & 7u); if (b != 0u && (sim->bgCollSprites & kStBit[b]) == 0u) {
if (sim->fuelBarTick >= 5u) { 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; return;
} }
if (sim->velY < 0) { if (sim->velY >= ST_LAMP_FAST_VEL) {
color = 0x02u; color = ST_LAMP_FAST;
cell = ST_CELL(24, 10); cell = ST_LAMP_CELL_FAST;
} else if (sim->velY == 0) { } else if (sim->velY >= ST_LAMP_FALL_VEL) {
color = 0x03u; color = ST_LAMP_FALL;
cell = ST_CELL(23, 10);
} else {
color = 0x07u;
cell = ST_CELL(23, 10);
} }
setColorCell(sim, cell, color); setColorCell(sim, cell, color);
setColorCell(sim, (uint16_t)(cell + 1u), 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); sim->padAnimTick = (uint8_t)((sim->padAnimTick + 1u) & 7u);
if (sim->padAnimTick == 0u) { if (sim->padAnimTick == 0u) {
stAudioSfx(stC64SfxProgram(ST_SFX_CASH)); stSimSfx(sim, stC64SfxProgram(ST_SFX_CASH));
} else if (sim->padAnimTick < 5u) { } else if (sim->padAnimTick < 5u) {
stAudioVoice1Freq(kPumpFreq[sim->padAnimTick]); stAudioVoice1Freq(kPumpFreq[sim->padAnimTick]);
} }
@ -619,7 +574,7 @@ static void fuelTick(StSimT *sim) {
} }
setCell(sim, (uint16_t)(ST_CELL_FUEL + x), ST_CHAR_BLANK); setCell(sim, (uint16_t)(ST_CELL_FUEL + x), ST_CHAR_BLANK);
if (sim->screen[ST_CELL_FUEL_LAST] == 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; return;
} }
@ -632,7 +587,7 @@ static void fuelTick(StSimT *sim) {
return; return;
} }
if (sim->fuelCells < 3u) { if (sim->fuelCells < 3u) {
stAudioSfx(stC64SfxProgram(ST_SFX_FUEL_LOW)); stSimSfx(sim, stC64SfxProgram(ST_SFX_FUEL_LOW));
} }
sim->fuelCountdown = L->fuelRate; sim->fuelCountdown = L->fuelRate;
x = sim->fuelCells; x = sim->fuelCells;
@ -716,12 +671,12 @@ static void padDetect(StSimT *sim) {
} }
sim->activePad = (uint8_t)(i + 1); sim->activePad = (uint8_t)(i + 1);
sim->activePadMirror = sim->activePad; sim->activePadMirror = sim->activePad;
sim->eventDispatchType = 0u; sim->eventDispatchType = ST_EVENT_NONE;
if (sim->activePad != sim->activeSpriteIdx) { if (sim->activePad != sim->activeSpriteIdx) {
if (sim->stage == ST_STAGE_RIDING) { 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) { } else if (sim->stage == ST_STAGE_WAIT) {
sim->eventDispatchType = 1u; sim->eventDispatchType = ST_EVENT_HAIL;
} }
} }
sim->dirMask = 0u; sim->dirMask = 0u;
@ -729,11 +684,11 @@ static void padDetect(StSimT *sim) {
stSimPadLight(sim, true); stSimPadLight(sim, true);
sim->bobTimer = 2u; sim->bobTimer = 2u;
if (((uint8_t)sim->velY & 0x80u) == 0u) { if (((uint8_t)sim->velY & 0x80u) == 0u) {
stAudioSfx(stC64SfxProgram(ST_SFX_LAND_SOFT)); stSimSfx(sim, stC64SfxProgram(ST_SFX_LAND_SOFT));
return; return;
} }
sim->bobTimer = 8u; sim->bobTimer = 8u;
stAudioSfx(stC64SfxProgram(ST_SFX_LAND_HARD)); stSimSfx(sim, stC64SfxProgram(ST_SFX_LAND_HARD));
if (sim->stage == ST_STAGE_RIDING) { if (sim->stage == ST_STAGE_RIDING) {
stSimHudInit(sim); 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 // 21 rows of 3-byte foreground masks for a displayed sprite, kept in
// collMask[idx] and rebuilt only when its bitmap or multicolour mode // collMask[idx] and rebuilt only when its bitmap or multicolour mode
// changes. A multicolour sprite's 2-bit pairs cover two pixels each: a // 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) { 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->spriteSpriteColl = 0u;
sim->spriteBgColl = 0u; sim->spriteBgColl = 0u;
sim->activePad = 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. // $6888 -- place the cab at the level's spawn point with a full tank.
static void taxiSpawnInit(StSimT *sim) { static void taxiSpawnInit(StSimT *sim) {
const StLevelT *L = sim->level; 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 // Public API
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -1382,26 +1419,15 @@ void stSimEnterLevel(StSimT *sim, const StLevelT *level) {
} }
sim->hookLevel = level->levelIndex; sim->hookLevel = level->levelIndex;
stHookSceneLoad(sim); stHookSceneLoad(sim);
spritesOffReset(sim);
stHookPrelude0(sim);
stFareTakeoffSetup(sim);
stSimRespawn(sim); stSimRespawn(sim);
} }
// $5F27 -- the respawn half of the prelude. // $5F27 -- the respawn half of the prelude.
void stSimRespawn(StSimT *sim) { 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); taxiSpawnInit(sim);
stHookPrelude1(sim); stHookPrelude1(sim);
// $6906 framePresent: the start-of-screen jingle, then the SID (and // $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 // 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. // frozen, which is what the C64's blocking play-and-wait amounts to.
stSimMarshal(sim); stSimMarshal(sim);
stAudioSilence(); voicesOff(sim);
sim->jingleRotate++; sim->jingleRotate++;
if (sim->levelState == ST_JINGLE_LEVEL_25) { if (sim->levelState == ST_JINGLE_LEVEL_25) {
sim->jingleRequest = (uint8_t)(ST_JINGLE_SONG_BONUS + 1u); sim->jingleRequest = (uint8_t)(ST_JINGLE_SONG_BONUS + 1u);
@ -1418,8 +1444,12 @@ void stSimRespawn(StSimT *sim) {
} }
sim->spriteSpriteColl = 0u; sim->spriteSpriteColl = 0u;
sim->spriteBgColl = 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->rngT1 = 0x06u;
sim->rngT2 = 0x17u; 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. // Everything must be repainted.
// A glyph's bits changed: the renderer has to repaint every cell showing // 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 // 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 // $411B -- add a signed byte to a sprite's 17-bit X, carrying into the
// msb shadow. // msb shadow.
void stSimSpriteAddX(StSimT *sim, uint8_t idx, uint8_t delta) { void stSimSpriteAddX(StSimT *sim, uint8_t idx, uint8_t delta) {
@ -1671,18 +1749,20 @@ StTickResultE stSimTick(StSimT *sim) {
taxiSpriteCelSelect(sim); taxiSpriteCelSelect(sim);
if (levelEndCheck(sim)) { if (levelEndCheck(sim)) {
stAudioNoise(false); stAudioNoise(false);
stAudioSilence(); voicesOff(sim);
stSimArchiveHud(sim); stSimArchiveHud(sim);
return ST_TICK_LEVEL_EXIT; return ST_TICK_LEVEL_EXIT;
} }
padLandingBob(sim); padLandingBob(sim);
fuelTick(sim); fuelTick(sim);
// $61BD passengerEventDraw only re-voices the pickup/drop-off stFareEventVoice(sim);
// messages already drawn by the fare machine.
} }
stFarePostTickGate(sim); stFarePostTickGate(sim);
stFareStageDispatch(sim); stFareStageDispatch(sim);
stHookPerTick2(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); stSimMarshal(sim);
stSimRotateGlyph(sim, ST_CHAR_TRANSPORTER); stSimRotateGlyph(sim, ST_CHAR_TRANSPORTER);
stHookPerTick3(sim); stHookPerTick3(sim);
@ -1703,6 +1783,7 @@ StTickResultE stSimTick(StSimT *sim) {
} }
} }
hudDraw(sim); hudDraw(sim);
sfxEnvelopeTick(sim);
// $4FCB runStopWatcher: any joystick input ends the demo. // $4FCB runStopWatcher: any joystick input ends the demo.
if (sim->demoMode != 0u && (sim->rawInput & 0x1Fu) != 0u) { if (sim->demoMode != 0u && (sim->rawInput & 0x1Fu) != 0u) {
return ST_TICK_DEMO_INPUT; return ST_TICK_DEMO_INPUT;

106
stSim.h
View file

@ -52,13 +52,14 @@
#define ST_HW_SPRITES 8u #define ST_HW_SPRITES 8u
// Fixed screen cells the game writes directly (C64 screen addresses). // 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_CAB_ICONS ST_CELL(23, 1) // $0799..$079F
#define ST_CELL_FUEL ST_CELL(23, 14) // $07A6..$07B1, 12 cells #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_FUEL_LAST ST_CELL(23, 25) // $07B1
#define ST_CELL_SCREENS ST_CELL(23, 32) // $07B8..$07BC #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_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_COL 14u
#define ST_MESSAGE_ROW 24u #define ST_MESSAGE_ROW 24u
#define ST_CELL_PAD_DIGIT ST_CELL(24, 18) // $07D2 #define ST_CELL_PAD_DIGIT ST_CELL(24, 18) // $07D2
@ -79,7 +80,7 @@
#define ST_CHAR_FUEL_EMPTY 0x7Au #define ST_CHAR_FUEL_EMPTY 0x7Au
#define ST_CHAR_TRANSPORTER 0x67u // animated top-wall hatch #define ST_CHAR_TRANSPORTER 0x67u // animated top-wall hatch
#define ST_CHAR_CAB_ICON 0xC8u #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 #define ST_FARE_DEST_UP 0x0Bu // $7D8E: the fare aboard wants UP
// ---- level-intro starfield -------------------------------------------- // ---- 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 // Distinct characters whose glyph can change in one frame before the
// renderer gives up and repaints everything. // renderer gives up and repaints everything.
#define ST_CHAR_DIRTY_MAX 8u #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 // 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 // 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 // this is generous; an overflow parks the edit in the spill entry, where
// it is simply never displayed rather than corrupting another character. // it is simply never displayed rather than corrupting another character.
#define ST_GLYPH_OVERRIDES 16u #define ST_GLYPH_OVERRIDES 16u
@ -160,14 +168,16 @@ typedef struct {
uint8_t rows[8]; uint8_t rows[8];
} StGlyphT; } 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_PADS 10u
#define ST_MAX_LEVEL_SPRITES 16u #define ST_MAX_LEVEL_SPRITES 16u
// Per-level hook code + tables live at $7D98..$7FFF in the level's data // Per-level hook code + tables live from $7D98 in the level's data blob;
// blob; the port keeps the bytes (for the tables) and re-expresses the // the port keeps the bytes (for the tables) and re-expresses the code in
// code in stHooks.c. // 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_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 // Per-level hook scratch. The original keeps a hook's running state
// inside the level's own data block, at fixed C64 addresses interleaved // inside the level's own data block, at fixed C64 addresses interleaved
@ -190,6 +200,13 @@ typedef struct {
uint8_t tone; // $7E56 rising chime uint8_t tone; // $7E56 rising chime
} StHookHT; } 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 { typedef struct {
uint8_t celPhase; // $7DD2 orb cel cycle 0..5 uint8_t celPhase; // $7DD2 orb cel cycle 0..5
uint8_t tick; // $7E00 uint8_t tick; // $7E00
@ -215,12 +232,34 @@ typedef struct {
typedef struct { typedef struct {
uint8_t phase[ST_HW_SPRITES]; // $7E83 interference cel phase uint8_t phase[ST_HW_SPRITES]; // $7E83 interference cel phase
uint8_t parity; // $7DFE the cels step every other tick
} StHookQT; } StHookQT;
typedef struct { typedef struct {
uint8_t gateShut; // $7D9C uint8_t gateShut; // $7D9C
} StHookTT; } 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 { typedef struct {
uint8_t colorPhase; // $7DAB uint8_t colorPhase; // $7DAB
uint8_t state[ST_HOOK_W_LASERS]; // $7DAC 0 idle, 1 extending, 2 fading uint8_t state[ST_HOOK_W_LASERS]; // $7DAC 0 idle, 1 extending, 2 fading
@ -262,6 +301,7 @@ typedef struct {
typedef union { typedef union {
StHookET e; StHookET e;
StHookFT f;
StHookGT g; StHookGT g;
StHookHT h; StHookHT h;
StHookIT i; StHookIT i;
@ -272,8 +312,10 @@ typedef union {
StHookQT q; StHookQT q;
StHookTT t; StHookTT t;
StHookUT u; StHookUT u;
StHookVT v;
StHookWT w; StHookWT w;
StHookXT x; StHookXT x;
StHookYT y;
} StHookScratchT; } StHookScratchT;
typedef struct { typedef struct {
@ -309,7 +351,7 @@ typedef struct {
uint8_t levelIndex; // 0..23 = A..X, 24 = Y, 25 = title uint8_t levelIndex; // 0..23 = A..X, 24 = Y, 25 = title
uint8_t spriteCount; uint8_t spriteCount;
StLevelSpriteT sprites[ST_MAX_LEVEL_SPRITES]; 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; } StLevelT;
// One VIC sprite as the game sees it through its shadow tables. Padded // One VIC sprite as the game sees it through its shadow tables. Padded
@ -358,6 +400,16 @@ typedef enum {
ST_STAGE_CLEANUP // $67C6 ST_STAGE_CLEANUP // $67C6
} StStageE; } 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 // What a tick asked the host to do next (the original abandons the
// main loop with PLA/PLA on these). // main loop with PLA/PLA on these).
@ -460,6 +512,10 @@ typedef struct {
uint8_t bobTimer; // $716F uint8_t bobTimer; // $716F
uint8_t flameParity; // $716C uint8_t flameParity; // $716C
uint8_t thrustSweep; // $721B 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 ---- // ---- collision / crash ----
uint8_t collisionPhase; // $7164 uint8_t collisionPhase; // $7164
@ -468,6 +524,18 @@ typedef struct {
uint8_t hitDispatchResult; // $721D uint8_t hitDispatchResult; // $721D
uint8_t spriteSpriteColl; // $71CA latched $D01E uint8_t spriteSpriteColl; // $71CA latched $D01E
uint8_t spriteBgColl; // $71CB latched $D01F 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 // Sprites besides the cab whose $D01F bit a hook reads (levels J
// and P watch their hazards hit the scenery); the rest are never // and P watch their hazards hit the scenery); the rest are never
// tested (derived state, not C64 memory). // tested (derived state, not C64 memory).
@ -571,8 +639,14 @@ typedef struct {
// fresh screen: copy the image, restore the player's HUD rows, draw // fresh screen: copy the image, restore the player's HUD rows, draw
// the cab icons, spawn the cab, refill the fuel. // the cab icons, spawn the cab, refill the fuel.
void stSimEnterLevel(StSimT *sim, const StLevelT *level); 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); 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). // One game tick = one $5F40 iteration (two video frames on the C64).
StTickResultE stSimTick(StSimT *sim); StTickResultE stSimTick(StSimT *sim);
// Seed the whole struct for a new game ($5EC4 + $4207 + $48AD). // 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). // The 17-bit VIC sprite X (msb:col), read and signed-byte add ($411B).
uint16_t stSimSpriteX(const StSimT *sim, uint8_t idx); uint16_t stSimSpriteX(const StSimT *sim, uint8_t idx);
void stSimSpriteAddX(StSimT *sim, uint8_t idx, uint8_t delta); 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); void stSimClearMessage(StSimT *sim);
// A glyph's bits changed: every cell showing it has to be repainted. // A glyph's bits changed: every cell showing it has to be repainted.
void stSimMarkChar(StSimT *sim, uint8_t ch); void stSimMarkChar(StSimT *sim, uint8_t ch);
@ -637,8 +711,10 @@ void stFareStageDispatch(StSimT *sim); // $65C1
void stFarePostTickGate(StSimT *sim); // $67E3 void stFarePostTickGate(StSimT *sim); // $67E3
void stFarePadLightingGate(StSimT *sim); // $6EF0 void stFarePadLightingGate(StSimT *sim); // $6EF0
void stFareUpPlease(StSimT *sim); // $6CB8 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 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 stFareSquashed(StSimT *sim); // $69B4..$69F8
void stFareBoardingSound(StSimT *sim, const uint8_t *speech); // $6C7F void stFareBoardingSound(StSimT *sim, const uint8_t *speech); // $6C7F
void stFareWaveStep(StSimT *sim); // $66B7 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). // Seed a converted level's scratch from the shipped blob (scene load).
void stHookSceneLoad(StSimT *sim); 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) ---- // ---- audio sink (host provides; stAudio.c / stsim stub) ----
void stAudioSfx(const uint8_t *program9); // $42E9 load void stAudioSfx(const uint8_t *program9); // $42E9 load
void stAudioNoise(bool on); // $D412 = $81 / $80 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 $@ $(CC) $(CFLAGS) harness.c $(SRCS) $(WRAPS) -o $@
# The title screen's level file is read through DATA/levels (the DOS bake # 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 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; } @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 @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 # 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. # 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 set -u
here=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) here=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)

View file

@ -1,79 +1,72 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# Compare stsim output against a VICE trace segment. # Compare two ride traces in stsim's line format -- stsim's against the
# compare.py <trace.jsonl> <startTick> <stsim-output.jsonl> [maxReports] # real C64's (c64trace.py), or a golden against the current tree -- field
import json, sys # 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])] SCREEN_CELLS = 1000
start = int(sys.argv[2]) SCREEN_COLS = 40
sim = [json.loads(l) for l in open(sys.argv[3])] CHARSET = 2048
maxRep = int(sys.argv[4]) if len(sys.argv) > 4 else 12 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'] def traces(pathA, pathB, maxReports):
reports = 0 a = [json.loads(line) for line in open(pathA)]
firstBad = None b = [json.loads(line) for line in open(pathB)]
for s in sim: counts = {}
if 'end' in s: first = {}
print('sim ended at tick', s['tick'], 'result', s['end']) reports = 0
break for i in range(min(len(a), len(b))):
t = s['tick'] x, y = a[i], b[i]
if start + t >= len(trace): fields = [k for k in x if x.get(k) != y.get(k)] + [k for k in y if k not in x]
print('trace exhausted at sim tick', t); break for k in fields:
r = fromTrace(trace[start + t]) counts[k] = counts.get(k, 0) + 1
# Disabled sprites keep stale shadow values on the C64; only compare first.setdefault(k, i)
# positions/pointers of enabled ones. if fields and reports < maxReports:
en = int(r['en'], 16) print('line %d: ' % i + ' '.join('%s: %s vs %s' % (k, x.get(k), y.get(k)) for k in fields))
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))
reports += 1 reports += 1
if firstBad is None: if len(a) != len(b):
print('ALL', len(sim), 'ticks match') print('lengths differ: %d vs %d lines' % (len(a), len(b)))
else: if not counts:
print('first mismatch at tick', firstBad) 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 #!/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 # stsim rides a screen for up to 4000 ticks and prints one line of state
# against a golden capture. A refactor that changes nothing observable # a tick: the cab, the fare machine, the demo playback, the RNG, the HUD,
# leaves all seventeen traces byte-identical; anything else is a regression. # 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 # H, W, T and X ride the C64's own attract recordings; the other screens
# against VICE traces). The other thirteen ride synthesised fixtures from # ride synthesised streams from gateStreams.h (tools/stsim/recorder).
# gateStreams.h -- a regression gate only, not evidence of C64 fidelity.
# #
# gate.sh capture [dir] record goldens from the CURRENT tree # gate.sh check [dir] every ride's md5 against gateHashes.txt; a ride
# gate.sh check [dir] compare the current tree against them, or, with # that differs is diffed against its golden in dir,
# none captured, against gateHashes.txt (the md5 # if one was captured, to show where
# of each golden trace, the committed reference) # gate.sh c64 [L...] ride the C64 too (VICE, c64trace.py) and compare
# gate.sh hashes print one md5 per level (compact reference) # 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 # The rides run on the game's level bake, so build the game first
# game's level bake, so build the game first (gateLib.sh finds it). # (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 set -u
here=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) here=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
mode=${1:-check} mode=${1:-check}
dir=${2:-$here/golden}
source "$here/gateLib.sh" 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 case "$mode" in
capture) capture)
dir=${2:-$here/golden}
mkdir -p "$dir" mkdir -p "$dir"
for p in $pairs; do for p in $pairs; do
l=${p%%:*}; n=${p##*:} l=${p%%:*}; n=${p##*:}
"$here/stsim" "$levels/level$n.dat" "$l" "$ticks" > "$dir/demo$l.jsonl" 2>/dev/null simTrace "$l" "$n" "$dir/demo$l.jsonl"
printf ' %s: %s ticks\n' "$l" "$(wc -l < "$dir/demo$l.jsonl")" printf ' %s: %s lines\n' "$l" "$(wc -l < "$dir/demo$l.jsonl")"
done done
echo "gate: captured to $dir" echo "gate: captured to $dir"
;; ;;
check) check)
dir=${2:-$here/golden}
fail=0 fail=0
for p in $pairs; do for p in $pairs; do
l=${p%%:*}; n=${p##*:} l=${p%%:*}; n=${p##*:}
got=$(mktemp) got=$(mktemp)
"$here/stsim" "$levels/level$n.dat" "$l" "$ticks" > "$got" 2>/dev/null simTrace "$l" "$n" "$got"
if [ -f "$dir/demo$l.jsonl" ]; then want=$(awk -v l="$l" '$1 == l { print $2 }' "$here/gateHashes.txt")
if cmp -s "$dir/demo$l.jsonl" "$got"; then have=$(md5sum < "$got" | cut -d' ' -f1)
printf ' %s: IDENTICAL (%s ticks)\n' "$l" "$(wc -l < "$got")" if [ "$have" = "$want" ]; then
else printf ' %s: IDENTICAL to the C64 (%s lines, md5 as gateHashes.txt)\n' "$l" "$(wc -l < "$got")"
echo " $l: *** DIFFERS ***"
diff "$dir/demo$l.jsonl" "$got" | head -3
fail=1
fi
else else
want=$(awk -v l="$l" '$1 == l { print $2 }' "$here/gateHashes.txt") echo " $l: *** DIFFERS from the C64 *** (md5 $have, gateHashes.txt ${want:-has none}; gate.sh c64 $l shows where)"
have=$(md5sum < "$got" | cut -d' ' -f1) if [ -f "$dir/demo$l.jsonl" ]; then
if [ "$have" = "$want" ]; then python3 "$here/compare.py" "$dir/demo$l.jsonl" "$got" 3 | sed 's/^/ golden vs now: /'
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
fi fi
fail=1
fi fi
rm -f "$got" rm -f "$got"
done done
[ $fail -eq 0 ] && echo "gate: PASS" || echo "gate: FAIL" [ $fail -eq 0 ] && echo "gate: PASS" || echo "gate: FAIL"
# The cel census rides the same recordings: a cel a ride displays that # The cel census rides the same streams: a cel a ride displays that its
# its manifest does not bake is a mid-ride interpreted sprite. # manifest does not bake is a mid-ride interpreted sprite.
"$here/celGate.sh" || fail=1 "$here/celGate.sh" || fail=1
exit $fail 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) hashes)
for p in $pairs; do for p in $pairs; do
l=${p%%:*}; n=${p##*:} l=${p%%:*}; n=${p##*:}
h=$("$here/stsim" "$levels/level$n.dat" "$l" "$ticks" 2>/dev/null | md5sum | cut -d' ' -f1) got=$(mktemp)
printf '%s %s\n' "$l" "$h" simTrace "$l" "$n" "$got"
printf '%s %s\n' "$l" "$(md5sum < "$got" | cut -d' ' -f1)"
rm -f "$got"
done done
;; ;;
*) *)
sed -n '2,20p' "$0" sed -n '2,33p' "$0"
exit 2 exit 2
;; ;;
esac esac

View file

@ -1,21 +1,31 @@
# Space Taxi behaviour gate -- md5 of each level's 4000-tick trace. # Space Taxi behaviour gate -- the md5 of each screen's ride as the REAL
# Regenerate with: tools/stsim/gate.sh hashes # C64 traced it: the original game in VICE, ridden by c64trace.py, one
# H/W/T/X ride the C64's attract recordings; the rest ride # line of state per tick (stsim prints the same line from the port).
# synthesised fixtures from gateStreams.h (regression gate only). # `gate.sh check` compares the port's rides against these, so a pass means
H 760501b0e48f3922bfb0f23fd2831935 # byte-identical to the C64 on all 25 screens.
T c0b3f9e9a74e0d526703cf9b1875557b # Regenerate (needs VICE and the original disk): tools/stsim/gate.sh c64hashes
W 92226455989e8326577a6ea20df8c8ea A e5cc75d60b41a3fb6ad70a8475a21a7a
X 87251d616a1be6e0c9c9317987b0fe20 B 4348bfdde94d3fc6d28d1ee7ca4e73bb
G 73530651372c60042f1f46b5c0cbada0 C 6669b71a3cdeebcbab62e1a29eda5faa
I 7fe4415429d2b72a50884424269cb685 D 2df4ea48591d8fc7bafe20638a1eaf00
K 567be2ed37219356a055866459971345 E 7595460b2296bd863b740060547bdc3d
O 02455280933e915e57803a4059aaefd9 F 96d1f6589336430399e4fd04941b079d
Q a7732866d54ed06d52e5dc7406b5b229 G 9f07d6ed41c634ba8acf7d0162fca172
R 066a9be80107446893b4571d247ee5cc H 5682e029bbf147d429bba1f774deca13
U 02dd2f26245442ab016ef02e10fde611 I ecb6dbe5b9765bbf3c0bf4638dc9f72f
V cc2cb13c5e393d07fbece9c0c465e387 J 171cfb357d771abd7db4b6d3ac3b4402
E 21ff29d69d8318bcb2e870b9602e910e K d7bc29828c42cda46f66485d2252bb4c
J 4c22a7515949b1701ed4fedabb7823dd L 05a2098c950e76c78799227fa748b1d3
L a0383cbde2800bb09353df15214934ef M 77fe51edf453736f7000f91ad9c52d19
P f345d0adc9e011662d582c4277ee663e N 1e7fd970699f408b7e6ea246882fa778
S daa3cd12320e32d3e9b6b97b19b0a6d0 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 # Sourced by gate.sh and celGate.sh, which set $here first: the rides both
# gates replay, and the levels they replay them on. # 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 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 # target, so whichever target was built first will do; STSIM_LEVELS names
# another folder. # another folder.
levels=${STSIM_LEVELS:-} levels=${STSIM_LEVELS:-}
if [ -z "$levels" ]; then if [ -z "$levels" ]; then
for port in iigs dos amiga atarist x68000; do for bake in "$here"/../../build/generated/*/levels; do
if [ -f "$here/../../build/generated/$port/levels/level01.dat" ]; then if [ -f "$bake/level25.dat" ]; then
levels=$(cd "$here/../../build/generated/$port/levels" && pwd) levels=$(cd "$bake" && pwd)
break break
fi fi
done done

View file

@ -1,89 +1,313 @@
// Synthesised demo input streams -- a TEST FIXTURE, not game content. // Synthesised demo input streams -- a TEST FIXTURE, not game content.
// //
// The C64 ships attract recordings for four screens only (H, W, T and X); // The C64 ships attract recordings for four screens only (H, W, T and X;
// those live in stDemoStreams.h and are the streams // stDemoStreams.h). The streams below cover the other twenty-one: each was
// checked against VICE traces. The thirteen streams below were searched for // searched for by tools/stsim's recorder against this port's simulation,
// by tools/stsim's recorder against this port's own simulation, scored on // scored on how long the cab survives and how much of the level's hook
// how long the cab survives and how much of the level's hook state, fare // state, fare machine and screen animation the ride reaches.
// machine and screen animation the ride reaches.
// //
// So they prove one thing and not another: they are a REGRESSION gate -- // Every ride, these and the four recordings alike, is then ridden on the
// refactor the hooks and the tick-by-tick trace must not move -- but they // original game in VICE (gate.sh c64, c64trace.py) and must trace byte
// are NOT evidence that a hook matches the original, because no C64 trace // for byte as the simulation does; gateHashes.txt holds the C64's traces.
// exists for these screens. Treat a change to these bytes as invalidating // A change to these bytes therefore needs `gate.sh c64hashes` on a
// the goldens, and never put them in the attract rotation. // machine with VICE. Never put them in the attract rotation.
#ifndef ST_GATE_STREAMS_H #ifndef ST_GATE_STREAMS_H
#define ST_GATE_STREAMS_H #define ST_GATE_STREAMS_H
#include <stdint.h> #include <stdint.h>
// level G -- 1400 ticks, 5 hook bytes, 10 fare stages, 256 pictures, 13 landings // level A -- 1397 ticks, 0 hook bytes, 8 fare stages, 256 pictures, 8 landings, left through the hatch
// regenerate: tools/stsim/recorder levels/level??.dat G 20260921 // 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] = { static const uint8_t kGateStreamG[320] = {
0x88, 0x05, 0x81, 0x09, 0x81, 0x05, 0x81, 0x04, 0x81, 0x06, 0x84, 0x0C, 0x81, 0x04, 0x88, 0x04, 0x81, 0x01, 0x81, 0x03, 0x88, 0x01, 0x88, 0x06,
0x81, 0x01, 0x89, 0x02, 0x81, 0x09, 0x82, 0x0D, 0x88, 0x05, 0x81, 0x03, 0x81, 0x04, 0x88, 0x0D, 0x84, 0x0D, 0x84, 0x03, 0x84, 0x08, 0x81, 0x07,
0x80, 0x09, 0x81, 0x01, 0x90, 0x02, 0x81, 0x04, 0x90, 0x08, 0x81, 0x03, 0x88, 0x06, 0x81, 0x0B, 0x81, 0x0E, 0x88, 0x01, 0x84, 0x0E, 0x81, 0x0D,
0x90, 0x0A, 0x88, 0x0E, 0x88, 0x0C, 0x90, 0x0C, 0x81, 0x04, 0x81, 0x0D, 0x84, 0x06, 0x81, 0x03, 0x82, 0x08, 0x81, 0x06, 0x84, 0x0B, 0x81, 0x0A,
0x90, 0x0A, 0x81, 0x0D, 0x84, 0x0E, 0x81, 0x09, 0x81, 0x08, 0x81, 0x0C, 0x84, 0x02, 0x88, 0x06, 0x81, 0x01, 0x81, 0x09, 0x88, 0x0E, 0x81, 0x0A,
0x82, 0x04, 0x81, 0x02, 0x81, 0x0E, 0x81, 0x02, 0x84, 0x01, 0x88, 0x0A, 0x81, 0x03, 0x84, 0x05, 0x88, 0x0A, 0x81, 0x01, 0x84, 0x07, 0x88, 0x08,
0x88, 0x05, 0x90, 0x09, 0x81, 0x02, 0x84, 0x09, 0x84, 0x05, 0x81, 0x0D, 0x81, 0x0A, 0x81, 0x03, 0x81, 0x09, 0x81, 0x02, 0x81, 0x01, 0x81, 0x09,
0x84, 0x09, 0x88, 0x03, 0x84, 0x06, 0x84, 0x0D, 0x81, 0x05, 0x84, 0x0C, 0x81, 0x01, 0x81, 0x0C, 0x82, 0x0A, 0x90, 0x0D, 0x88, 0x0E, 0x81, 0x0D,
0x88, 0x0C, 0x88, 0x0C, 0x88, 0x03, 0x88, 0x09, 0x81, 0x0A, 0x81, 0x09, 0x84, 0x03, 0x88, 0x09, 0x84, 0x0D, 0x88, 0x0D, 0x81, 0x05, 0x88, 0x03,
0x81, 0x06, 0x81, 0x07, 0x81, 0x05, 0x88, 0x09, 0x88, 0x02, 0x81, 0x0B, 0x81, 0x08, 0x90, 0x0E, 0x81, 0x02, 0x88, 0x0D, 0x82, 0x06, 0x88, 0x08,
0x84, 0x0A, 0x84, 0x05, 0x88, 0x03, 0x81, 0x05, 0x81, 0x0B, 0x81, 0x05, 0x81, 0x06, 0x82, 0x03, 0x81, 0x01, 0x88, 0x0D, 0x81, 0x05, 0x81, 0x07,
0x90, 0x05, 0x84, 0x09, 0x81, 0x06, 0x81, 0x07, 0x90, 0x0D, 0x81, 0x04, 0x81, 0x0B, 0x90, 0x0E, 0x84, 0x01, 0x81, 0x0B, 0x82, 0x0A, 0x90, 0x08,
0x81, 0x0E, 0x88, 0x0D, 0x88, 0x0D, 0x88, 0x06, 0x82, 0x02, 0x88, 0x0D, 0x84, 0x02, 0x82, 0x0C, 0x88, 0x05, 0x84, 0x08, 0x81, 0x0D, 0x81, 0x0B,
0x90, 0x0E, 0x81, 0x04, 0x81, 0x0B, 0x84, 0x0B, 0x81, 0x05, 0x84, 0x07, 0x84, 0x0D, 0x88, 0x0B, 0x84, 0x0C, 0x81, 0x07, 0x88, 0x07, 0x88, 0x08,
0x88, 0x0A, 0x80, 0x04, 0x84, 0x07, 0x84, 0x0B, 0x88, 0x0D, 0x90, 0x0E, 0x81, 0x05, 0x90, 0x0A, 0x90, 0x0C, 0x82, 0x09, 0x81, 0x07, 0x81, 0x0A,
0x90, 0x06, 0x88, 0x08, 0x82, 0x0C, 0x81, 0x02, 0x81, 0x0E, 0x88, 0x0A, 0x82, 0x0B, 0x88, 0x0D, 0x81, 0x01, 0x90, 0x04, 0x90, 0x01, 0x81, 0x0B,
0x90, 0x0B, 0x81, 0x04, 0x88, 0x08, 0x90, 0x07, 0x84, 0x03, 0x82, 0x08, 0x82, 0x05, 0x84, 0x07, 0x81, 0x08, 0x80, 0x04, 0x88, 0x04, 0x81, 0x0A,
0x81, 0x05, 0x81, 0x03, 0x81, 0x03, 0x81, 0x06, 0x90, 0x0B, 0x84, 0x0E, 0x89, 0x02, 0x88, 0x03, 0x82, 0x05, 0x90, 0x0D, 0x90, 0x0E, 0x81, 0x0B,
0x82, 0x0E, 0x88, 0x08, 0x88, 0x04, 0x81, 0x0D, 0x88, 0x04, 0x81, 0x08, 0x84, 0x01, 0x82, 0x0A, 0x83, 0x04, 0x81, 0x09, 0x84, 0x0C, 0x81, 0x0E,
0x88, 0x0C, 0x81, 0x06, 0x81, 0x04, 0x90, 0x0B, 0x81, 0x08, 0x84, 0x04, 0x81, 0x01, 0x82, 0x0D, 0x81, 0x0C, 0x81, 0x03, 0x82, 0x01, 0x81, 0x0C,
0x90, 0x08, 0x84, 0x09, 0x88, 0x09, 0x81, 0x0B, 0x81, 0x07, 0x84, 0x0D, 0x88, 0x05, 0x81, 0x08, 0x88, 0x0C, 0x82, 0x0E, 0x84, 0x0B, 0x81, 0x0B,
0x88, 0x0A, 0x88, 0x09, 0x90, 0x0B, 0x90, 0x02, 0x84, 0x0D, 0x81, 0x0E, 0x88, 0x0C, 0x81, 0x0C, 0x81, 0x05, 0x84, 0x08, 0x82, 0x02, 0x82, 0x0A,
0x90, 0x0D, 0x84, 0x07, 0x82, 0x0C, 0x81, 0x0E, 0x81, 0x06, 0x88, 0x09, 0x81, 0x0C, 0x84, 0x0B, 0x81, 0x02, 0x90, 0x08, 0x81, 0x06, 0x90, 0x0E,
0x90, 0x02, 0x90, 0x04, 0x81, 0x09, 0x84, 0x0A, 0x90, 0x06, 0x81, 0x04, 0x88, 0x03, 0x82, 0x0B, 0x84, 0x0E, 0x84, 0x0B, 0x84, 0x0D, 0x88, 0x04,
0x90, 0x03, 0x90, 0x08, 0x84, 0x0B, 0x81, 0x02, 0x90, 0x04, 0x84, 0x03, 0x84, 0x05, 0x81, 0x06, 0x84, 0x06, 0x90, 0x0D, 0x90, 0x0C, 0x88, 0x03,
0x81, 0x03, 0x84, 0x05, 0x90, 0x0C, 0x81, 0x01, 0x90, 0x0D, 0x81, 0x02, 0x81, 0x0E, 0x82, 0x02, 0x84, 0x05, 0x90, 0x0B, 0x82, 0x08, 0x84, 0x09,
0x90, 0x05, 0x90, 0x0E, 0x81, 0x03, 0x90, 0x0A, 0x81, 0x07, 0x84, 0x0D, 0x81, 0x02, 0x90, 0x04,
}; };
// level I -- 1400 ticks, 20 hook bytes, 10 fare stages, 256 pictures, 22 landings // level I -- 1400 ticks, 20 hook bytes, 10 fare stages, 256 pictures, 4 landings
// regenerate: tools/stsim/recorder levels/level??.dat I 20260918 // regenerate: tools/stsim/recorder levels/level09.dat I 20261006 60 2000
static const uint8_t kGateStreamI[320] = { static const uint8_t kGateStreamI[320] = {
0x84, 0x0E, 0x81, 0x06, 0x84, 0x0E, 0x88, 0x0B, 0x88, 0x05, 0x81, 0x03, 0x81, 0x04, 0x88, 0x02, 0x89, 0x01, 0x89, 0x02, 0x81, 0x01, 0x88, 0x05,
0x82, 0x07, 0x90, 0x0A, 0x81, 0x08, 0x88, 0x0E, 0x88, 0x0E, 0x90, 0x0B, 0x81, 0x04, 0x80, 0x0E, 0x84, 0x06, 0x82, 0x05, 0x84, 0x08, 0x88, 0x07,
0x82, 0x0D, 0x81, 0x01, 0x81, 0x0A, 0x90, 0x0A, 0x82, 0x03, 0x81, 0x07, 0x88, 0x06, 0x81, 0x0B, 0x82, 0x05, 0x82, 0x07, 0x81, 0x0E, 0x81, 0x02,
0x82, 0x05, 0x81, 0x0B, 0x81, 0x05, 0x82, 0x05, 0x82, 0x0C, 0x81, 0x08, 0x81, 0x03, 0x82, 0x03, 0x82, 0x0C, 0x88, 0x06, 0x84, 0x0B, 0x84, 0x0E,
0x88, 0x0C, 0x90, 0x04, 0x81, 0x02, 0x88, 0x08, 0x81, 0x03, 0x81, 0x05, 0x81, 0x02, 0x81, 0x06, 0x81, 0x02, 0x84, 0x09, 0x90, 0x0E, 0x81, 0x02,
0x81, 0x0A, 0x88, 0x0E, 0x81, 0x06, 0x84, 0x0E, 0x82, 0x07, 0x82, 0x01, 0x81, 0x07, 0x81, 0x02, 0x90, 0x08, 0x81, 0x02, 0x84, 0x04, 0x88, 0x09,
0x81, 0x04, 0x82, 0x08, 0x90, 0x0C, 0x90, 0x05, 0x81, 0x08, 0x82, 0x0D, 0x88, 0x05, 0x81, 0x0C, 0x88, 0x0E, 0x84, 0x0E, 0x88, 0x08, 0x90, 0x09,
0x90, 0x06, 0x81, 0x0A, 0x84, 0x0B, 0x84, 0x0C, 0x84, 0x0C, 0x88, 0x09, 0x90, 0x05, 0x84, 0x09, 0x82, 0x0A, 0x90, 0x0C, 0x88, 0x07, 0x88, 0x09,
0x81, 0x06, 0x81, 0x0C, 0x81, 0x04, 0x82, 0x09, 0x88, 0x05, 0x82, 0x0E, 0x84, 0x05, 0x81, 0x04, 0x81, 0x02, 0x81, 0x03, 0x81, 0x08, 0x88, 0x0B,
0x81, 0x0C, 0x81, 0x07, 0x88, 0x01, 0x90, 0x0E, 0x81, 0x02, 0x90, 0x0E, 0x90, 0x05, 0x81, 0x0A, 0x81, 0x08, 0x82, 0x0E, 0x82, 0x09, 0x90, 0x09,
0x81, 0x02, 0x90, 0x07, 0x84, 0x05, 0x84, 0x06, 0x88, 0x06, 0x88, 0x0C, 0x81, 0x07, 0x81, 0x07, 0x81, 0x08, 0x88, 0x0D, 0x82, 0x0C, 0x81, 0x08,
0x81, 0x0C, 0x84, 0x0B, 0x82, 0x09, 0x81, 0x05, 0x81, 0x0B, 0x82, 0x09, 0x81, 0x02, 0x81, 0x03, 0x82, 0x04, 0x84, 0x0B, 0x81, 0x05, 0x88, 0x02,
0x90, 0x0E, 0x90, 0x06, 0x88, 0x0D, 0x84, 0x03, 0x82, 0x0B, 0x84, 0x0D, 0x88, 0x07, 0x90, 0x0D, 0x88, 0x0B, 0x84, 0x02, 0x81, 0x02, 0x84, 0x09,
0x90, 0x0B, 0x90, 0x04, 0x81, 0x0B, 0x81, 0x08, 0x88, 0x08, 0x90, 0x0A, 0x88, 0x0C, 0x81, 0x06, 0x88, 0x09, 0x81, 0x02, 0x84, 0x0A, 0x88, 0x06,
0x81, 0x03, 0x81, 0x01, 0x81, 0x0E, 0x81, 0x04, 0x88, 0x02, 0x90, 0x05, 0x88, 0x0C, 0x81, 0x05, 0x82, 0x03, 0x88, 0x02, 0x81, 0x06, 0x81, 0x08,
0x81, 0x07, 0x81, 0x01, 0x82, 0x09, 0x90, 0x06, 0x81, 0x09, 0x90, 0x0E, 0x82, 0x03, 0x82, 0x0E, 0x81, 0x03, 0x81, 0x02, 0x81, 0x0C, 0x82, 0x0B,
0x88, 0x05, 0x82, 0x0D, 0x88, 0x03, 0x81, 0x06, 0x84, 0x0C, 0x82, 0x01, 0x84, 0x05, 0x88, 0x0C, 0x84, 0x0C, 0x81, 0x07, 0x81, 0x06, 0x88, 0x07,
0x81, 0x09, 0x82, 0x0E, 0x90, 0x01, 0x81, 0x01, 0x84, 0x05, 0x82, 0x06, 0x88, 0x0C, 0x90, 0x0E, 0x90, 0x03, 0x81, 0x0C, 0x84, 0x0C, 0x81, 0x03,
0x88, 0x0A, 0x81, 0x05, 0x82, 0x03, 0x90, 0x07, 0x90, 0x08, 0x81, 0x02, 0x89, 0x01, 0x88, 0x0D, 0x81, 0x05, 0x81, 0x06, 0x90, 0x04, 0x83, 0x0D,
0x90, 0x0D, 0x88, 0x0D, 0x81, 0x08, 0x90, 0x0E, 0x82, 0x0D, 0x88, 0x04, 0x81, 0x01, 0x88, 0x05, 0x84, 0x05, 0x81, 0x01, 0x84, 0x09, 0x81, 0x02,
0x81, 0x0C, 0x82, 0x09, 0x90, 0x04, 0x81, 0x0C, 0x82, 0x06, 0x90, 0x0E, 0x90, 0x05, 0x81, 0x0D, 0x80, 0x09, 0x81, 0x0C, 0x88, 0x02, 0x84, 0x0C,
0x90, 0x0D, 0x90, 0x07, 0x90, 0x03, 0x82, 0x03, 0x88, 0x06, 0x81, 0x07, 0x82, 0x0E, 0x82, 0x0C, 0x90, 0x02, 0x81, 0x07, 0x88, 0x04, 0x88, 0x0D,
0x90, 0x08, 0x81, 0x02, 0x90, 0x09, 0x81, 0x04, 0x90, 0x06, 0x82, 0x0A, 0x88, 0x09, 0x81, 0x0D, 0x88, 0x0C, 0x88, 0x0E, 0x88, 0x0E, 0x82, 0x0B,
0x90, 0x05, 0x88, 0x05, 0x88, 0x06, 0x90, 0x0B, 0x81, 0x03, 0x82, 0x02, 0x82, 0x0B, 0x81, 0x0C, 0x82, 0x0C, 0x90, 0x0B, 0x81, 0x0A, 0x84, 0x08,
0x90, 0x0B, 0x81, 0x02, 0x90, 0x0C, 0x90, 0x07, 0x81, 0x01, 0x84, 0x08, 0x81, 0x08, 0x82, 0x0B, 0x81, 0x09, 0x84, 0x09, 0x81, 0x0B, 0x81, 0x03,
0x90, 0x0C, 0x81, 0x0E, 0x88, 0x08, 0x88, 0x0C, 0x81, 0x0C, 0x90, 0x04, 0x8A, 0x0D, 0x88, 0x0B, 0x90, 0x04, 0x81, 0x0E, 0x88, 0x07, 0x81, 0x08,
0x84, 0x0D, 0x81, 0x01, 0x88, 0x08, 0x88, 0x09, 0x88, 0x03, 0x80, 0x0E, 0x84, 0x05, 0x84, 0x07,
}; };
// level K -- 1400 ticks, 1 hook bytes, 10 fare stages, 256 pictures, 16 landings // level J -- 1400 ticks, 10 hook bytes, 10 fare stages, 256 pictures, 6 landings
// regenerate: tools/stsim/recorder levels/level??.dat K 11 // 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] = { static const uint8_t kGateStreamK[320] = {
0x84, 0x06, 0x82, 0x0B, 0x90, 0x0C, 0x82, 0x0E, 0x90, 0x05, 0x88, 0x03, 0x84, 0x06, 0x82, 0x0B, 0x90, 0x0C, 0x82, 0x0E, 0x90, 0x05, 0x88, 0x03,
0x88, 0x02, 0x82, 0x0E, 0x88, 0x07, 0x84, 0x02, 0x84, 0x0B, 0x84, 0x08, 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, 0x82, 0x09, 0x90, 0x01, 0x88, 0x07, 0x88, 0x08,
}; };
// level O -- 1400 ticks, 1 hook bytes, 3 fare stages, 256 pictures, 0 landings // level L -- 1400 ticks, 0 hook bytes, 10 fare stages, 256 pictures, 4 landings
// regenerate: tools/stsim/recorder levels/level??.dat O 20260918 // 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] = { static const uint8_t kGateStreamO[320] = {
0x84, 0x0E, 0x90, 0x08, 0x82, 0x07, 0x81, 0x0B, 0x84, 0x02, 0x81, 0x09, 0x84, 0x0E, 0x90, 0x08, 0x82, 0x07, 0x81, 0x0B, 0x84, 0x02, 0x81, 0x09,
0x82, 0x06, 0x90, 0x0C, 0x81, 0x08, 0x88, 0x01, 0x88, 0x02, 0x90, 0x04, 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, 0x81, 0x0E, 0x81, 0x08, 0x90, 0x0D, 0x84, 0x0D,
}; };
// level Q -- 1400 ticks, 5 hook bytes, 10 fare stages, 256 pictures, 10 landings // level P -- 1400 ticks, 11 hook bytes, 10 fare stages, 256 pictures, 5 landings
// regenerate: tools/stsim/recorder levels/level??.dat Q 20260918 // regenerate: tools/stsim/recorder levels/level16.dat P 20261006 60 2000
static const uint8_t kGateStreamQ[320] = { static const uint8_t kGateStreamP[320] = {
0x84, 0x0E, 0x81, 0x04, 0x81, 0x03, 0x84, 0x09, 0x88, 0x0A, 0x81, 0x09, 0x81, 0x04, 0x88, 0x02, 0x84, 0x05, 0x89, 0x02, 0x81, 0x01, 0x88, 0x05,
0x82, 0x06, 0x88, 0x0C, 0x81, 0x08, 0x82, 0x06, 0x88, 0x0E, 0x84, 0x0B, 0x81, 0x04, 0x80, 0x0D, 0x84, 0x0C, 0x84, 0x03, 0x84, 0x08, 0x88, 0x07,
0x88, 0x08, 0x81, 0x09, 0x81, 0x0A, 0x84, 0x05, 0x84, 0x01, 0x88, 0x01, 0x88, 0x01, 0x81, 0x05, 0x88, 0x0E, 0x82, 0x01, 0x81, 0x0C, 0x81, 0x0C,
0x81, 0x08, 0x84, 0x0B, 0x81, 0x05, 0x82, 0x05, 0x80, 0x0D, 0x81, 0x08, 0x88, 0x03, 0x80, 0x0C, 0x88, 0x0E, 0x88, 0x03, 0x84, 0x04, 0x84, 0x0E,
0x88, 0x0A, 0x88, 0x04, 0x84, 0x08, 0x81, 0x06, 0x81, 0x0C, 0x81, 0x01, 0x81, 0x07, 0x88, 0x06, 0x80, 0x08, 0x84, 0x09, 0x81, 0x0E, 0x90, 0x0A,
0x90, 0x0A, 0x84, 0x08, 0x81, 0x0A, 0x81, 0x0A, 0x82, 0x08, 0x81, 0x06, 0x81, 0x03, 0x81, 0x05, 0x84, 0x09, 0x81, 0x01, 0x88, 0x07, 0x88, 0x08,
0x81, 0x09, 0x90, 0x06, 0x81, 0x09, 0x90, 0x0D, 0x88, 0x04, 0x82, 0x05, 0x81, 0x05, 0x81, 0x0C, 0x82, 0x0D, 0x88, 0x0E, 0x88, 0x07, 0x81, 0x02,
0x81, 0x0B, 0x84, 0x08, 0x81, 0x08, 0x81, 0x03, 0x82, 0x0D, 0x82, 0x0A, 0x81, 0x03, 0x84, 0x0C, 0x88, 0x06, 0x88, 0x05, 0x81, 0x07, 0x81, 0x0A,
0x84, 0x0E, 0x81, 0x05, 0x81, 0x0E, 0x81, 0x08, 0x84, 0x0A, 0x82, 0x0C, 0x84, 0x09, 0x90, 0x06, 0x84, 0x09, 0x81, 0x0B, 0x88, 0x01, 0x81, 0x03,
0x84, 0x09, 0x84, 0x01, 0x82, 0x06, 0x88, 0x0E, 0x81, 0x09, 0x90, 0x07, 0x81, 0x06, 0x90, 0x0C, 0x88, 0x06, 0x81, 0x0B, 0x81, 0x06, 0x90, 0x06,
0x81, 0x09, 0x81, 0x07, 0x84, 0x03, 0x81, 0x0B, 0x84, 0x01, 0x90, 0x08, 0x82, 0x06, 0x82, 0x04, 0x82, 0x01, 0x81, 0x02, 0x81, 0x07, 0x90, 0x0E,
0x90, 0x06, 0x82, 0x03, 0x88, 0x06, 0x88, 0x0C, 0x84, 0x0A, 0x81, 0x03, 0x90, 0x0B, 0x84, 0x07, 0x81, 0x03, 0x84, 0x0E, 0x84, 0x08, 0x84, 0x0C,
0x82, 0x0E, 0x81, 0x07, 0x81, 0x04, 0x81, 0x0D, 0x84, 0x02, 0x81, 0x09, 0x81, 0x0D, 0x81, 0x0A, 0x81, 0x0E, 0x88, 0x0C, 0x88, 0x0B, 0x81, 0x05,
0x82, 0x08, 0x88, 0x04, 0x81, 0x09, 0x84, 0x0C, 0x81, 0x0B, 0x84, 0x0D, 0x90, 0x04, 0x88, 0x0A, 0x82, 0x0B, 0x84, 0x0D, 0x81, 0x0C, 0x84, 0x0C,
0x81, 0x02, 0x82, 0x03, 0x81, 0x05, 0x88, 0x0E, 0x84, 0x0D, 0x81, 0x0B, 0x90, 0x05, 0x84, 0x0D, 0x81, 0x02, 0x81, 0x08, 0x81, 0x03, 0x88, 0x06,
0x88, 0x08, 0x84, 0x0C, 0x90, 0x07, 0x82, 0x0B, 0x81, 0x03, 0x90, 0x05, 0x90, 0x0E, 0x81, 0x05, 0x84, 0x0B, 0x88, 0x08, 0x81, 0x09, 0x84, 0x0E,
0x84, 0x05, 0x84, 0x09, 0x81, 0x04, 0x81, 0x0B, 0x81, 0x08, 0x82, 0x0E, 0x81, 0x0A, 0x81, 0x05, 0x88, 0x07, 0x81, 0x02, 0x81, 0x0C, 0x81, 0x03,
0x81, 0x0A, 0x81, 0x03, 0x81, 0x06, 0x88, 0x09, 0x84, 0x0A, 0x88, 0x0B, 0x8A, 0x0C, 0x84, 0x0B, 0x80, 0x03, 0x85, 0x04, 0x81, 0x04, 0x82, 0x0A,
0x81, 0x05, 0x81, 0x0D, 0x88, 0x03, 0x88, 0x0D, 0x81, 0x01, 0x90, 0x06, 0x85, 0x07, 0x84, 0x0D, 0x88, 0x08, 0x80, 0x07, 0x81, 0x01, 0x80, 0x0D,
0x88, 0x07, 0x82, 0x02, 0x81, 0x04, 0x84, 0x09, 0x81, 0x07, 0x90, 0x06, 0x80, 0x04, 0x81, 0x05, 0x82, 0x01, 0x82, 0x0B, 0x84, 0x05, 0x88, 0x0A,
0x82, 0x03, 0x84, 0x07, 0x81, 0x05, 0x90, 0x0B, 0x81, 0x0B, 0x88, 0x05, 0x90, 0x01, 0x90, 0x08, 0x81, 0x08, 0x81, 0x08, 0x81, 0x05, 0x81, 0x07,
0x81, 0x0D, 0x81, 0x0B, 0x92, 0x0E, 0x90, 0x05, 0x82, 0x02, 0x88, 0x0C, 0x81, 0x01, 0x81, 0x0D, 0x88, 0x02, 0x81, 0x01, 0x88, 0x03, 0x90, 0x02,
0x84, 0x08, 0x81, 0x0A, 0x81, 0x0D, 0x81, 0x05, 0x81, 0x07, 0x81, 0x01, 0x88, 0x0A, 0x81, 0x0D, 0x82, 0x05, 0x88, 0x01, 0x81, 0x02, 0x85, 0x0A,
0x81, 0x04, 0x88, 0x01, 0x81, 0x0E, 0x84, 0x04, 0x90, 0x03, 0x81, 0x0D, 0x84, 0x0C, 0x88, 0x0C, 0x88, 0x05, 0x88, 0x03, 0x90, 0x08, 0x81, 0x08,
0x90, 0x09, 0x81, 0x02, 0x81, 0x08, 0x90, 0x07, 0x82, 0x0B, 0x90, 0x08, 0x84, 0x0E, 0x82, 0x0C, 0x84, 0x0E, 0x90, 0x0C, 0x82, 0x0E, 0x90, 0x08,
0x81, 0x03, 0x90, 0x0C, 0x81, 0x0B, 0x88, 0x08, 0x90, 0x04, 0x81, 0x02, 0x84, 0x0D, 0x88, 0x0B, 0x84, 0x0C, 0x82, 0x0C, 0x84, 0x08, 0x90, 0x0A,
0x90, 0x0C, 0x88, 0x05, 0x81, 0x01, 0x90, 0x03, 0x88, 0x0E, 0x81, 0x07, 0x90, 0x03, 0x88, 0x0C,
}; };
// level R -- 1400 ticks, 0 hook bytes, 3 fare stages, 256 pictures, 0 landings // level Q -- 1400 ticks, 6 hook bytes, 10 fare stages, 256 pictures, 6 landings
// regenerate: tools/stsim/recorder levels/level??.dat R 20260918 // 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] = { static const uint8_t kGateStreamR[320] = {
0x90, 0x0E, 0x81, 0x02, 0x82, 0x07, 0x81, 0x05, 0x88, 0x0B, 0x88, 0x09, 0x90, 0x0E, 0x81, 0x02, 0x82, 0x07, 0x81, 0x05, 0x88, 0x0B, 0x88, 0x09,
0x90, 0x0D, 0x90, 0x06, 0x81, 0x08, 0x81, 0x01, 0x90, 0x02, 0x88, 0x04, 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, 0x90, 0x0A, 0x90, 0x01, 0x84, 0x0A, 0x81, 0x0B,
}; };
// level U -- 896 ticks, 21 hook bytes, 3 fare stages, 256 pictures, 0 landings // level S -- 1400 ticks, 0 hook bytes, 10 fare stages, 256 pictures, 25 landings
// regenerate: tools/stsim/recorder levels/level??.dat U 44 // regenerate: tools/stsim/recorder levels/level19.dat S 20261006 60 2000
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
static const uint8_t kGateStreamS[320] = { static const uint8_t kGateStreamS[320] = {
0x90, 0x04, 0x90, 0x0E, 0x88, 0x08, 0x82, 0x0D, 0x88, 0x0C, 0x81, 0x03, 0x82, 0x04, 0x88, 0x0E, 0x88, 0x02, 0x88, 0x0D, 0x81, 0x04, 0x82, 0x06,
0x81, 0x0A, 0x81, 0x0D, 0x88, 0x09, 0x88, 0x0C, 0x82, 0x0E, 0x81, 0x07, 0x81, 0x0D, 0x81, 0x09, 0x81, 0x07, 0x88, 0x09, 0x84, 0x07, 0x81, 0x01,
0x81, 0x0D, 0x81, 0x0A, 0x90, 0x0D, 0x84, 0x01, 0x90, 0x05, 0x88, 0x0E, 0x90, 0x0C, 0x81, 0x09, 0x82, 0x0A, 0x88, 0x0D, 0x81, 0x0A, 0x84, 0x03,
0x81, 0x0A, 0x81, 0x0A, 0x90, 0x06, 0x88, 0x03, 0x82, 0x0B, 0x82, 0x0B, 0x88, 0x0E, 0x82, 0x05, 0x81, 0x08, 0x90, 0x0C, 0x81, 0x0C, 0x88, 0x0A,
0x88, 0x03, 0x88, 0x0A, 0x90, 0x0C, 0x81, 0x0E, 0x81, 0x0E, 0x90, 0x06, 0x82, 0x02, 0x81, 0x01, 0x90, 0x05, 0x84, 0x0A, 0x88, 0x0D, 0x82, 0x02,
0x88, 0x02, 0x82, 0x08, 0x81, 0x0A, 0x90, 0x0D, 0x90, 0x05, 0x81, 0x05, 0x90, 0x02, 0x81, 0x05, 0x88, 0x08, 0x81, 0x08, 0x88, 0x01, 0x82, 0x01,
0x88, 0x0B, 0x81, 0x05, 0x90, 0x0B, 0x88, 0x0E, 0x82, 0x09, 0x90, 0x07, 0x90, 0x08, 0x81, 0x0B, 0x81, 0x0A, 0x88, 0x0A, 0x90, 0x04, 0x81, 0x02,
0x81, 0x03, 0x81, 0x0D, 0x88, 0x09, 0x88, 0x0E, 0x84, 0x0A, 0x88, 0x0A, 0x82, 0x01, 0x90, 0x07, 0x82, 0x05, 0x90, 0x07, 0x81, 0x0D, 0x81, 0x0E,
0x82, 0x01, 0x90, 0x0C, 0x82, 0x01, 0x90, 0x0A, 0x82, 0x0A, 0x84, 0x03, 0x81, 0x07, 0x88, 0x09, 0x84, 0x04, 0x82, 0x01, 0x90, 0x0C, 0x81, 0x09,
0x81, 0x02, 0x90, 0x04, 0x84, 0x0E, 0x90, 0x09, 0x81, 0x0D, 0x90, 0x06, 0x81, 0x0A, 0x81, 0x05, 0x81, 0x03, 0x88, 0x03, 0x88, 0x02, 0x82, 0x01,
0x81, 0x02, 0x90, 0x0E, 0x90, 0x07, 0x82, 0x05, 0x81, 0x06, 0x90, 0x07, 0x81, 0x05, 0x90, 0x0B, 0x82, 0x08, 0x88, 0x05, 0x84, 0x0B, 0x90, 0x0A,
0x82, 0x03, 0x90, 0x07, 0x90, 0x0B, 0x82, 0x06, 0x88, 0x0E, 0x90, 0x08, 0x81, 0x0E, 0x81, 0x05, 0x88, 0x0B, 0x82, 0x0B, 0x81, 0x07, 0x90, 0x08,
0x84, 0x0A, 0x82, 0x07, 0x81, 0x04, 0x81, 0x03, 0x84, 0x05, 0x90, 0x08, 0x84, 0x02, 0x81, 0x02, 0x81, 0x0C, 0x82, 0x01, 0x81, 0x05, 0x90, 0x06,
0x81, 0x0C, 0x82, 0x01, 0x90, 0x0B, 0x81, 0x0A, 0x84, 0x05, 0x81, 0x02, 0x82, 0x04, 0x84, 0x04, 0x90, 0x04, 0x84, 0x0C, 0x81, 0x05, 0x81, 0x03,
0x82, 0x09, 0x90, 0x0E, 0x81, 0x02, 0x84, 0x03, 0x81, 0x0C, 0x81, 0x0C, 0x81, 0x01, 0x88, 0x01, 0x90, 0x01, 0x81, 0x0E, 0x88, 0x07, 0x82, 0x02,
0x81, 0x0D, 0x82, 0x08, 0x81, 0x03, 0x81, 0x0D, 0x81, 0x0A, 0x81, 0x0A, 0x81, 0x06, 0x90, 0x03, 0x81, 0x0C, 0x82, 0x04, 0x88, 0x0B, 0x90, 0x09,
0x81, 0x0C, 0x82, 0x06, 0x90, 0x0D, 0x88, 0x0C, 0x84, 0x04, 0x81, 0x01, 0x81, 0x0C, 0x88, 0x0D, 0x81, 0x02, 0x82, 0x03, 0x81, 0x06, 0x90, 0x02,
0x81, 0x0A, 0x81, 0x03, 0x90, 0x06, 0x88, 0x07, 0x88, 0x0E, 0x84, 0x0E, 0x90, 0x03, 0x84, 0x0E, 0x82, 0x03, 0x81, 0x05, 0x88, 0x02, 0x90, 0x01,
0x81, 0x0E, 0x82, 0x03, 0x90, 0x0E, 0x82, 0x05, 0x90, 0x02, 0x88, 0x03, 0x84, 0x0D, 0x84, 0x09, 0x82, 0x08, 0x82, 0x02, 0x84, 0x03, 0x84, 0x07,
0x81, 0x07, 0x81, 0x0A, 0x81, 0x0B, 0x81, 0x05, 0x84, 0x0D, 0x81, 0x01, 0x90, 0x0D, 0x82, 0x02, 0x88, 0x07, 0x84, 0x01, 0x90, 0x05, 0x81, 0x09,
0x82, 0x02, 0x90, 0x0C, 0x82, 0x03, 0x90, 0x08, 0x81, 0x05, 0x84, 0x08, 0x82, 0x02, 0x90, 0x06, 0x90, 0x04, 0x81, 0x05, 0x82, 0x06, 0x88, 0x0B,
0x84, 0x06, 0x88, 0x08, 0x81, 0x0E, 0x82, 0x01, 0x81, 0x01, 0x90, 0x08, 0x90, 0x03, 0x82, 0x04, 0x81, 0x06, 0x84, 0x08, 0x81, 0x0D, 0x81, 0x03,
0x82, 0x03, 0x90, 0x0C, 0x84, 0x02, 0x81, 0x09, 0x82, 0x04, 0x88, 0x0D, 0x81, 0x02, 0x82, 0x0B, 0x88, 0x0B, 0x81, 0x0E, 0x90, 0x06, 0x90, 0x08,
0x90, 0x07, 0x88, 0x0E, 0x82, 0x05, 0x81, 0x0C, 0x90, 0x0A, 0x81, 0x04, 0x88, 0x06, 0x81, 0x02, 0x81, 0x0D, 0x88, 0x03, 0x84, 0x07, 0x81, 0x07,
0x84, 0x06, 0x82, 0x02, 0x81, 0x09, 0x90, 0x0D, 0x82, 0x06, 0x82, 0x01, 0x81, 0x05, 0x81, 0x06, 0x81, 0x06, 0x81, 0x03, 0x82, 0x06, 0x81, 0x03,
0x90, 0x09, 0x81, 0x0B, 0x84, 0x04, 0x88, 0x07, 0x81, 0x0B, 0x81, 0x05, 0x88, 0x0A, 0x84, 0x08, 0x90, 0x0B, 0x90, 0x03, 0x82, 0x06, 0x84, 0x02,
0x82, 0x03, 0x81, 0x07, 0x90, 0x06, 0x82, 0x07, 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 { typedef struct {
@ -441,19 +699,27 @@ typedef struct {
} StGateStreamT; } StGateStreamT;
static const StGateStreamT kGateStreams[] = { 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) }, { 'G', kGateStreamG, (uint16_t)sizeof(kGateStreamG) },
{ 'I', kGateStreamI, (uint16_t)sizeof(kGateStreamI) }, { 'I', kGateStreamI, (uint16_t)sizeof(kGateStreamI) },
{ 'J', kGateStreamJ, (uint16_t)sizeof(kGateStreamJ) },
{ 'K', kGateStreamK, (uint16_t)sizeof(kGateStreamK) }, { '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) }, { 'O', kGateStreamO, (uint16_t)sizeof(kGateStreamO) },
{ 'P', kGateStreamP, (uint16_t)sizeof(kGateStreamP) },
{ 'Q', kGateStreamQ, (uint16_t)sizeof(kGateStreamQ) }, { 'Q', kGateStreamQ, (uint16_t)sizeof(kGateStreamQ) },
{ 'R', kGateStreamR, (uint16_t)sizeof(kGateStreamR) }, { 'R', kGateStreamR, (uint16_t)sizeof(kGateStreamR) },
{ 'S', kGateStreamS, (uint16_t)sizeof(kGateStreamS) },
{ 'U', kGateStreamU, (uint16_t)sizeof(kGateStreamU) }, { 'U', kGateStreamU, (uint16_t)sizeof(kGateStreamU) },
{ 'V', kGateStreamV, (uint16_t)sizeof(kGateStreamV) }, { 'V', kGateStreamV, (uint16_t)sizeof(kGateStreamV) },
{ 'E', kGateStreamE, (uint16_t)sizeof(kGateStreamE) }, { 'Y', kGateStreamY, (uint16_t)sizeof(kGateStreamY) },
{ 'J', kGateStreamJ, (uint16_t)sizeof(kGateStreamJ) },
{ 'L', kGateStreamL, (uint16_t)sizeof(kGateStreamL) },
{ 'P', kGateStreamP, (uint16_t)sizeof(kGateStreamP) },
{ 'S', kGateStreamS, (uint16_t)sizeof(kGateStreamS) },
}; };
#define ST_GATE_STREAMS (sizeof(kGateStreams) / sizeof(kGateStreams[0])) #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. // recorder -- search for a demo input stream that exercises a level.
// //
// The C64 ships attract recordings for four screens (H, W, T, X). The // 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 // other levels have none, so this searches for a synthetic ride instead:
// checked against. This searches for a synthetic ride instead: random // random restarts plus hill climbing over (joystick mask, duration)
// restarts plus hill climbing over (joystick mask, duration) pairs, // pairs, scored on how long the cab survives its first life and how much
// scored on how long the cab survives and how much of the level it // of the level it reaches -- hook state bytes written, fare-machine
// reaches -- hook state bytes written, fare-machine stages entered, // stages entered, distinct pictures drawn, pads landed on.
// distinct pictures drawn, pads landed on.
// //
// The result is a REGRESSION fixture, not a recording: it pins current // The search runs on this port's simulation; what proves the ride is
// behaviour so a refactor that changes nothing observable leaves the // the original game: gate.sh c64 rides the stream on the C64 in VICE and
// trace identical. It says nothing about matching the real C64. // 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. // Then assemble the per-level outputs into gateStreams.h.
// //
// Determinism: a candidate is only useful if it replays identically from // 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 PAIRS 160
#define STREAMLEN (PAIRS * 2) #define STREAMLEN (PAIRS * 2)
#define MAXTICKS 1400 #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 rs = 1;
static uint32_t rnd(void){ rs = rs*1664525u + 1013904223u; return rs>>8; } 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; ResT r;
memset(stageSeen, 0, sizeof(stageSeen)); memset(stageSeen, 0, sizeof(stageSeen));
memset(&r, 0, sizeof(r));
memset(touched, 0, sizeof(touched)); memset(touched, 0, sizeof(touched));
// stSimNewGame deliberately PRESERVES flameParity, waveIdx and // stSimNewGame deliberately PRESERVES flameParity, waveIdx and
// walkParity (the C64 carries them from screen to screen), so a // 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 // would then not reproduce from a cold start. Clear the whole struct
// so every evaluation begins exactly where the gate will. // so every evaluation begins exactly where the gate will.
memset(&gSim, 0, sizeof(gSim)); memset(&gSim, 0, sizeof(gSim));
// Exactly as stsim starts a gate ride.
stSimNewGame(&gSim, 1u, true); stSimNewGame(&gSim, 1u, true);
stSimSeedDemoBuffer(&gSim, kDemoBufferInit, ST_DEMO_BUFFER_BYTES); stSimSeedDemoBuffer(&gSim, kDemoBufferInit, ST_DEMO_BUFFER_BYTES);
(void)stSimAdvancePlayer(&gSim);
gSim.levelState = (uint8_t)(gLevel.levelIndex + 1u);
stSimEnterLevel(&gSim, &gLevel); stSimEnterLevel(&gSim, &gLevel);
stSimSetDemoStream(&gSim, stream, STREAMLEN); stSimSetDemoStream(&gSim, stream, STREAMLEN);
memcpy(base, &gSim.hookScratch, sizeof(base)); memcpy(base, &gSim.hookScratch, sizeof(base));
for (t = 0; t < MAXTICKS; t++) { for (t = 0; t < MAXTICKS; t++) {
uint16_t k; 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; { const uint8_t *cur = (const uint8_t *)&gSim.hookScratch;
for (k = 0; k < (uint16_t)sizeof(base); k++) { for (k = 0; k < (uint16_t)sizeof(base); k++) {
if (cur[k] != base[k]) { touched[k] = 1; } } } 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.ticks = t; r.cover = cover; r.lands = lands; r.stages = stages; r.scr = scrSeen;
r.near = nearest; r.near = nearest;
r.score = cover * 30000u + stages * 30000u + lands * 4000u + t * 100u + scrSeen * 200u r.score = cover * 30000u + stages * 30000u + lands * 4000u + t * 100u + scrSeen * 200u
+ (255u - nearest) * 60u; + (255u - nearest) * 60u + r.exited * 300000u;
return r; return r;
} }
@ -143,20 +153,28 @@ int main(int argc, char **argv)
ResT bestR, curR; ResT bestR, curR;
FILE *fp; FILE *fp;
int restart, iter; 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"); fp = fopen(argv[1], "rb");
if (!fp || !stLevelParse(&gLevel, fp)) { fprintf(stderr, "cannot load %s\n", argv[1]); return 1; } if (!fp || !stLevelParse(&gLevel, fp)) { fprintf(stderr, "cannot load %s\n", argv[1]); return 1; }
fclose(fp); fclose(fp);
rs = (uint32_t)strtoul(argv[3], NULL, 0); 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); randomise(best, 0);
bestR = evaluate(best, NULL); bestR = evaluate(best, NULL);
for (restart = 0; restart < 120; restart++) { for (restart = 0; restart < restarts; restart++) {
memcpy(cur, best, STREAMLEN); memcpy(cur, best, STREAMLEN);
if (restart > 0) { randomise(cur, (int)(rnd() % PAIRS)); } if (restart > 0) { randomise(cur, (int)(rnd() % PAIRS)); }
curR = evaluate(cur, NULL); curR = evaluate(cur, NULL);
for (iter = 0; iter < 3000; iter++) { for (iter = 0; iter < iterations; iter++) {
uint8_t trial[STREAMLEN]; uint8_t trial[STREAMLEN];
ResT tr; ResT tr;
int j, nmut = 1 + (int)(rnd() % 3u); 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; } if (curR.score > bestR.score) { memcpy(best, cur, STREAMLEN); bestR = curR; }
} }
bestR = evaluate(best, touched); bestR = evaluate(best, touched);
fprintf(stderr, "%s: ticks=%u hookBytes=%u stages=%u pictures=%u lands=%u nearPad=%u\n", 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); 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\n", 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); 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); printf("static const uint8_t kGateStream%s[%d] = {", argv[2], STREAMLEN);
{ int i; for (i = 0; i < STREAMLEN; i++) { { int i; for (i = 0; i < STREAMLEN; i++) {
if (i % 12 == 0) { printf("\n "); } if (i % 12 == 0) { printf("\n "); }

View file

@ -1,14 +1,33 @@
// stsim -- host-side driver for the Space Taxi simulation core. // stsim -- host-side driver for the Space Taxi simulation core.
// //
// Loads an STL4 level, attaches a recorded demo input stream and runs // Loads an STL5 level, attaches a recorded demo input stream and runs
// the game tick, printing one line of state per tick in the same shape // the game tick, printing one line of state per tick -- exactly the line
// the VICE tracer records (see stuff/ and the compare script), // c64trace.py prints from the real game in VICE, so the two traces can
// so the port's engine can be diffed against the real machine. // be compared byte for byte.
// //
// stsim <level.dat> <level letter> <ticks> // stsim <level.dat> <level letter> <ticks>
// //
// H, W, T and X use the C64's own attract recordings; every other // H, W, T and X use the C64's own attract recordings; every other
// letter uses a synthesised gate fixture from gateStreams.h. // 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 <stdio.h>
#include <stdlib.h> #include <stdlib.h>
@ -20,9 +39,33 @@
#include "gateStreams.h" #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 StLevelT gLevel;
static StSimT gSim; 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 stAudioSfx(const uint8_t *program9) { (void)program9; }
void stAudioNoise(bool on) { (void)on; } 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; } void stAudioVoice1Freq(uint8_t value) { (void)value; }
// FNV-1a over a byte span: the picture the player actually sees. The static void celCensusReport(const StLevelT *level) {
// hooks that animate the screen (O, R, V, W, X) change screen, colour or StCelDefT listed[ST_LEVEL_CELS_MAX];
// charset bytes and nothing else this line used to capture. uint8_t n = stLevelCelList(level, listed, ST_LEVEL_CELS_MAX);
static uint32_t hashBytes(const uint8_t *p, size_t n) { uint8_t k;
uint32_t h = 2166136261u; uint8_t j;
size_t i;
for (i = 0u; i < n; i++) { for (k = 0u; k < gCensusCount; k++) {
h = (h ^ p[i]) * 16777619u; 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) { static void celCensusTick(const StSimT *sim) {
uint8_t i; uint8_t i;
@ -105,40 +127,75 @@ static void celCensusTick(const StSimT *sim) {
} }
static void celCensusReport(const StLevelT *level) { // The charset the game would be showing. The sim no longer holds a
StCelDefT listed[ST_LEVEL_CELS_MAX]; // contiguous 2 KB copy -- edited glyphs live in an override table -- so
uint8_t n = stLevelCelList(level, listed, ST_LEVEL_CELS_MAX); // rebuild the effective image here. Hashing this keeps the trace value
uint8_t k; // identical to the pre-override captures, which is what lets the goldens
uint8_t j; // 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++) { charsetImage(sim, image);
for (j = 0u; j < n; j++) { return hashBytes(image, sizeof(image));
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);
} }
static void celCensusReport(const StLevelT *level); static void charsetImage(const StSimT *sim, uint8_t *image) {
static void celCensusTick(const StSimT *sim); 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) { static void printState(const StSimT *sim, uint32_t tick) {
bool shown1 = (sim->frame.enableMask & kStBit[1]) != 0u;
uint8_t k; uint8_t k;
printf("{\"tick\":%u,\"x\":\"%02X%02X.%02X\",\"y\":\"%02X.%02X\",\"vx\":\"%04X\",\"vy\":\"%04X\"," 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\"," "\"pad\":%u,\"st\":%u,\"ph\":%u,\"in\":\"%02X\",\"dir\":\"%02X\",\"ptr0\":\"%02X\",",
"\"s1\":\"%02X%02X.%02X\",\"en\":\"%02X\",\"t4740\":\"%02X\",\"off\":%u,\"rng\":\"%02X%02X\","
"\"fuel\":%u,\"slots\":%u,\"asi\":%u,\"score\":\"",
tick, sim->posXmsb, sim->posXcol, sim->posXlo, sim->posYrow, sim->posYlo, 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, (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->inputMask, sim->dirMask, sim->spr[0].ptr);
sim->spr[1].msb, sim->spr[1].col, sim->spr[1].row, sim->frame.enableMask, // A hidden sprite's registers are not part of the picture, and the
sim->demoTimer, sim->demoOff, sim->rngT1, sim->rngT2, sim->fuelCells, sim->fareSlotCount, sim->activeSpriteIdx); // 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++) { for (k = 0u; k < ST_NUMBER_CHARS; k++) {
printf("%02X", sim->screen[ST_CELL_SCORE + k]); printf("%02X", sim->screen[ST_CELL_SCORE + k]);
} }
@ -152,7 +209,11 @@ static void printState(const StSimT *sim, uint32_t tick) {
} }
printf("\",\"sprx\":\""); printf("\",\"sprx\":\"");
for (k = 0u; k < ST_HW_SPRITES; k++) { 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", printf("\",\"scr\":\"%08X\",\"col\":\"%08X\",\"chr\":\"%08X\",\"bd\":\"%02X\",\"bg\":\"%02X\"}\n",
hashBytes(sim->screen, ST_SCREEN_CELLS), 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) { int main(int argc, char **argv) {
FILE *fp; FILE *fp;
FILE *raw = NULL;
const char *rawPath;
const uint8_t *stream = kDemoStreamH; const uint8_t *stream = kDemoStreamH;
uint16_t len = (uint16_t)sizeof(kDemoStreamH); uint16_t len = (uint16_t)sizeof(kDemoStreamH);
uint8_t want; uint8_t want;
@ -205,34 +279,63 @@ int main(int argc, char **argv) {
} }
} }
ticks = (uint32_t)strtoul(argv[3], NULL, 0); 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); stSimNewGame(&gSim, 1u, true);
stSimSeedDemoBuffer(&gSim, kDemoBufferInit, ST_DEMO_BUFFER_BYTES); stSimSeedDemoBuffer(&gSim, kDemoBufferInit, ST_DEMO_BUFFER_BYTES);
if (argc > 4) { // $61FB, as startDemoScreen runs it: the turn wraps to player 0 and
// carried-over state: flame parity, wave index, walk parity // the screens-played count ($7215, this one included) is set before
gSim.flameParity = (uint8_t)strtoul(argv[4], NULL, 16); // 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
if (argc > 5) { // every screen after the second and the finished shift's HUD.
gSim.waveIdx = (uint8_t)strtoul(argv[5], NULL, 16); (void)stSimAdvancePlayer(&gSim);
} gSim.levelState = (uint8_t)(gLevel.levelIndex + 1u);
if (argc > 6) {
gSim.walkParity = (uint8_t)strtoul(argv[6], NULL, 16);
}
stSimEnterLevel(&gSim, &gLevel); stSimEnterLevel(&gSim, &gLevel);
stSimSetDemoStream(&gSim, stream, len); 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++) { for (t = 0u; t < ticks; t++) {
StTickResultE r; StTickResultE r;
printState(&gSim, t); printState(&gSim, t);
if (raw != NULL) {
writeRaw(raw, &gSim);
}
r = stSimTick(&gSim); r = stSimTick(&gSim);
if (getenv("STSIM_CELS") != NULL) { if (getenv("STSIM_CELS") != NULL) {
celCensusTick(&gSim); 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) { if (r != ST_TICK_CONTINUE) {
printf("{\"tick\":%u,\"end\":%d}\n", t, (int)r); printf("{\"tick\":%u,\"end\":%d}\n", t, (int)r);
break; break;
} }
if (gSim.demoOff >= ST_DEMO_LAST_PAIR) {
printf("{\"tick\":%u,\"end\":\"stream\"}\n", t);
break;
}
} }
if (getenv("STSIM_CELS") != NULL) { if (getenv("STSIM_CELS") != NULL) {
celCensusReport(&gLevel); celCensusReport(&gLevel);
} }
if (raw != NULL) {
fclose(raw);
}
return 0; return 0;
} }