From 786ff257c33a79a28e8c2f8f54b616ff7f46ee86 Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Mon, 5 Oct 2026 18:50:09 -0500 Subject: [PATCH] Build standalone against JoeyLib. --- .gitattributes | 1 + .gitignore | 22 ++ MECHANICS.md | 12 +- Makefile | 26 ++ PLAN.md | 4 +- README.md | 123 ++++++ TODO.md | 21 + VERIFIED.md | 10 +- assets/depackSpeechRam.py | 4 +- assets/extractSpeech.py | 2 +- assets/genC64Data.py | 10 +- assets/genDemoStreams.py | 4 +- assets/genSongData.py | 9 +- assets/levels/format.md | 10 +- assets/romToLevel.py | 4 +- installer/components/staxiData.sh | 30 -- mkstlevel/mkstlevel.c | 2 +- patches/spriteWarp.patch | 617 ++++++++++++++++++++++++++++++ program.mk | 145 +++++++ scripts/bench-staxi-title.sh | 15 +- scripts/diag-staxi-screen.sh | 11 +- scripts/importData.sh | 92 +++++ scripts/verify.sh | 100 +++++ stC64Data.c | 2 +- stC64Data.h | 2 +- stLevelFile.c | 2 +- stSim.h | 2 +- tools/staxiharness/Makefile | 35 +- tools/stsim/Makefile | 2 +- tools/stsim/celGate.sh | 7 +- tools/stsim/gate.sh | 39 +- tools/stsim/gateLib.sh | 25 ++ tools/stsim/gateStreams.h | 2 +- tools/stsim/stsim.c | 8 +- 34 files changed, 1277 insertions(+), 123 deletions(-) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.md create mode 100644 TODO.md delete mode 100644 installer/components/staxiData.sh create mode 100644 patches/spriteWarp.patch create mode 100644 program.mk create mode 100755 scripts/importData.sh create mode 100755 scripts/verify.sh create mode 100644 tools/stsim/gateLib.sh diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..24a8e87 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.png filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1030342 --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# Everything make writes: programs, bakes, disk images, the harness. +build/ + +# The original game's memory image (scripts/importData.sh installs it as +# stuff/raw.bin) and the disassembly work around it. A copy of a commercial +# game: never committed. +stuff/ + +# stsim's host binaries and its captured golden traces (gate.sh capture; +# gateHashes.txt is the committed reference). +tools/stsim/stsim +tools/stsim/recorder +tools/stsim/golden/ + +screenshots/ +__pycache__/ + +# Editor / OS cruft +*.swp +*.swo +*~ +.DS_Store diff --git a/MECHANICS.md b/MECHANICS.md index 24165ea..442134b 100644 --- a/MECHANICS.md +++ b/MECHANICS.md @@ -1,8 +1,8 @@ # Space Taxi Mechanics (research notes from C64 disassembly) -Source: VICE memory dumps in `stuff/spacetaxi/` decoded via the +Source: VICE memory dumps in `stuff/` decoded via the `dis6502.py` / `disLive.py` helpers. Full live-RAM listings are at -`stuff/spacetaxi/live-level1.lst` (40,069 instructions) and +`stuff/live-level1.lst` (40,069 instructions) and `live-raw.lst` (titlescreen snapshot). This doc captures observed behavior in plain English to drive the JoeyLib port without dragging the original 6502 source into chat or comments. @@ -68,7 +68,7 @@ acceleration always applied to Y. ### Per-frame algorithm (`$6032`, full trace) -- VERIFIED via emulator Verified by running `$6032` through the 6502 emulator with various -joystick / sprite-ptr states. See `stuff/spacetaxi/trace.py`. +joystick / sprite-ptr states. See `stuff/trace.py`. ``` 1. Clear instant accel: $7148/49 = 0; $714A/4B = 0 [$6032-$603F] @@ -1069,7 +1069,7 @@ IEC fast-loader (raw `$FFA8` CIOUT / `$FFAE` UNLSN serial traffic) that pulls the song file off the disk. **The music DATA is not in the resident program at all: each song ships as its own disk file** -- the `SPACETAXI 01..04` / `SPACETAXI HI` PRGs (present in -`stuff/spacetaxi/extract/`). Whatever song was resident when the +`stuff/extract/`). Whatever song was resident when the raw.bin dump was taken is what sits in its load area there. Timing base: `$A2` in the tick compare is the KERNAL jiffy clock low @@ -1770,7 +1770,7 @@ Char-code interpretation (via $4345 validation): - Other chars: undefined behavior Verified by running `$4354` through the 6502 emulator (see -`stuff/spacetaxi/trace.py`) with each blob against an all-blank +`stuff/trace.py`) with each blob against an all-blank HUD. The actual byte-level result for each: | Blob | HUD after BCD-add (chars) | Numeric effect | @@ -2098,7 +2098,7 @@ Key gameplay-affecting observations: - **Border/BG colors** vary; most levels are black (0/0). Level S has yellow/orange (`$07/$08`), level L is dark gray on dark gray. -The .dat files in `examples/spacetaxi/generated/levels/` carry the +The .dat files in `generated/levels/` carry the same xAccel/yAccel/xGrav/yGrav values verbatim (verified by parsing the binary headers). diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..97f5b91 --- /dev/null +++ b/Makefile @@ -0,0 +1,26 @@ +# Space Taxi, built with JoeyLib: a JoeyLib checkout (JOEYLIB, beside this +# one by default) owns the toolchains, the library and every target's rules, +# and program.mk describes the game to them. +# +# make every target whose toolchain is installed +# make one target: iigs amiga atarist dos x68000 dvx +# make check build, then run the gates (scripts/verify.sh) +# make clean remove build/ +# make help JoeyLib's project targets +# +# README.md covers setting up JoeyLib, importing the game data and running. + +JOEYLIB ?= ../joeylib +include $(JOEYLIB)/make/project.mk + +include $(JOEY_PROJECT)/program.mk + +# What DVX's help and resource texts call the program, and its author. +DVX_KIND := JoeyLib game +DVX_AUTHOR := Scott Duensing + +include $(JOEYLIB)/make/programs.mk + +.PHONY: check +check: all + JOEYLIB="$(abspath $(JOEYLIB))" $(JOEY_PROJECT)/scripts/verify.sh diff --git a/PLAN.md b/PLAN.md index 250346f..db6078f 100644 --- a/PLAN.md +++ b/PLAN.md @@ -15,7 +15,7 @@ Same loop every time, so it stays cheap to iterate: 2. **Read** -- pull the routine into a window via `disLive.py `, study the annotated listing. 3. **Document** -- append behavior summary to `MECHANICS.md` in - plain English. Add new labels/vars to `stuff/spacetaxi/labels.txt`. + plain English. Add new labels/vars to `stuff/labels.txt`. 4. **Reannotate** -- `python3 disLive.py mem0000-level1.bin live-level1.lst` to regenerate the (annotated) listing from the dump + labels.txt in one step. The .lst file is the only @@ -30,7 +30,7 @@ Same loop every time, so it stays cheap to iterate: ## Phase 0 -- foundation (DONE) -- Disassembly toolchain in `stuff/spacetaxi/` +- Disassembly toolchain in `stuff/` - Full live-RAM listing + annotated variant - `MECHANICS.md` baseline - Asset pipeline (`tools/assetbake/assetbake.py`, extractor realigned) diff --git a/README.md b/README.md new file mode 100644 index 0000000..b8e7551 --- /dev/null +++ b/README.md @@ -0,0 +1,123 @@ +# Space Taxi + +A port of Space Taxi (John Kutcher, Muse Software, 1984), the Commodore 64 +game, to every machine [JoeyLib](../joeylib) targets: the Apple IIgs, +Commodore Amiga, Atari ST, MS-DOS, Sharp X68000 and DVX. One C codebase +builds for all six. + +The game is a routine-for-routine re-expression of the C64 original in C, +written from its disassembly (MECHANICS.md): the flight physics, the fare +machine, the 24 levels' hook programs, the title and level intros, the +high-score table and name entry, the music (captured from the game's own +SID player) and the speech. The simulation (stSim.c) is verified tick for +tick against VICE traces of the C64's four attract-demo rides (VERIFIED.md), +and the gates below hold it there. + +## Setting up + +Space Taxi builds with JoeyLib, which owns the cross compilers, the +library, the asset tools and every machine's build rules. Check JoeyLib out +beside this repository and install its toolchains (its README covers the +installer): + +``` +games/ + joeylib/ a JoeyLib checkout, set up with ./install.sh + spacetaxi/ this repository +``` + +```console +$ cd ../joeylib +$ ./install.sh install toolchains # the cross compilers +$ ./install.sh install emulators # to run it and for the gates +``` + +`JOEYLIB=PATH` on the make command line (or in the environment, for the +scripts) points at a checkout somewhere else. + +## The game data + +The levels and the title screen are extracted at build time from a memory +image of the original C64 game. It is a copy of a commercial game, so it is +not in the repository; supply your own: + +```console +$ scripts/importData.sh PATH... +``` + +PATH is the image itself, a folder holding it, or a `.zip` or `.tar.gz` +with it inside. It is recognised by its checksum and installed as +`stuff/raw.bin`, which git ignores. Without it the game still builds; only +the levels (and the per-level sprite banks baked from them) are left out, +and make says so. + +## Building + +```console +$ make # every machine whose toolchain is installed +$ make dos # one: iigs amiga atarist dos x68000 dvx +$ make clean +``` + +Everything lands in `build//bin/`: + +| Machine | Program | +|---------|---------| +| Apple IIgs | `STAXI`, and `STAXI.2mg`: a bootable 2 MB volume (stripped GS/OS that launches the game from its own folder) | +| Amiga | `Taxi/Taxi` with its `DATA/` | +| Atari ST | `STAXI/STAXI.PRG` with its `DATA/` | +| MS-DOS | `STAXI/STAXI.EXE` with its `DATA/` | +| Sharp X68000 | `STAXI/STAXI.X` with its `DATA/` | +| DVX | `apps/joeylib/staxi/`: `staxi.app`, its help file and `DATA/` | + +The bakes (levels, the pre-compiled sprite cels, the speech) are made per +machine in `build/generated//`. JoeyLib's library is built on +demand in the JoeyLib checkout's own `build/`. + +## Running + +JoeyLib's `joey-run` starts a build in the right emulator: + +```console +$ ../joeylib/joey-run --project . --port dos staxi +$ ../joeylib/joey-run --project . --port amiga taxi +$ ../joeylib/joey-run build/iigs/bin/STAXI.2mg +$ ../joeylib/joey-run --headless --seconds 90 --snapshot taxi.png --project . --port atarist staxi +``` + +The IIgs volume is larger than an 800 KB floppy, so MAME boots it off a +CFFA2 card (`mame apple2gs -sl7 cffa2 -hard1 build/iigs/bin/STAXI.2mg`). + +## The gates + +```console +$ make check # build, then every gate +$ scripts/verify.sh disk # or name the ones to run +``` + +| Gate | What it proves | +|------|----------------| +| `stsim` | Every hook level's 4000-tick simulation trace is identical to its golden (`tools/stsim/gate.sh`; the four C64 attract rides and thirteen synthesised ones), and every cel a ride shows is pre-compiled (`celGate.sh`) | +| `harness` | The screens above the simulation -- the high-score table, name entry, its save -- on JoeyLib's host BLANK port (`tools/staxiharness`) | +| `disk`, `disk01` | `STAXI.2mg` boots, launches the game and draws on a ROM 3 and a ROM 01 IIgs | +| `iigsaudio` | The IIgs sound interrupt is NTP's, the DOC is audible and its sound slots carry waveforms | +| `x68kaudio` | Every X68000 noise write is a code from the port's table, and the ADPCM runs at 7812 Hz to both speakers while the game speaks | + +`scripts/verify.sh` exits 0 when every gate passed, 1 when one failed, and 3 +when none failed but one was skipped (an emulator or a machine's build +missing, or the game data not imported). + +## Tools + +- `tools/stsim` -- the simulation on the host: prints the per-tick state a + VICE trace records, so `compare.py` can diff the port against the C64. +- `tools/staxiharness` -- the front-end screen checks. +- `tools/staxibake` -- writes the cel bank the build pre-compiles per machine. +- `assets/` -- the generators behind the committed C64 tables + (`genC64Data.py`, `genDemoStreams.py`, `genSongData.py`, `extractSpeech.py`) + and the level extractor the build runs (`romToLevel.py`). +- `scripts/bench-staxi-title.sh`, `scripts/diag-staxi-screen.sh` -- IIgs + measurement and diagnosis under headless MAME. + +MECHANICS.md and VERIFIED.md are the disassembly notes; TODO.md lists what is +still open. diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..3af9240 --- /dev/null +++ b/TODO.md @@ -0,0 +1,21 @@ +# Space Taxi -- open items + +Things worth doing, not commitments. Started 2026-10-05, when Space Taxi +moved out of JoeyLib's tree into this repository; JoeyLib's own list +(library and platform work) stays in JoeyLib. + +- [ ] **The C64's flown-sprite warp.** The level-name intro animates its + warp by cycling the colours of a static star field painted into + screen RAM, on every target (stTitle.c, `starPaletteApply` in + stRender.c). The C64 flies seven star sprites out along its rays + instead. That version, carry-flag exact ($44F8/$4500/$450C/$4513/ + $467A, the star velocity tables back in genC64Data.py), is kept in + `patches/spriteWarp.patch` for reference: it was made against the + working tree of 2026-10-05 before the warp's layout and pacing were + redone, so it no longer applies as it stands. Measured then, the + sprite warp ran 16.6-17.0 fps on the IIgs against the palette's 29.2 + (the bar was 30); ST 29.2 against the palette's 23.3, Amiga PAL 12.3 + against 18.8/13.6, DOS ~29.9 both ways. On the IIgs the cost is + per-call overhead in save-and-draw and restore (~1.75k and ~1.25k + cycles a star), not bytes moved, so it becomes feasible with a leaner + save/draw/restore path for small cels in JoeyLib. diff --git a/VERIFIED.md b/VERIFIED.md index 274f1a1..85d01c8 100644 --- a/VERIFIED.md +++ b/VERIFIED.md @@ -1,7 +1,7 @@ # Space Taxi disassembly: verified claims Each row below references a claim in `MECHANICS.md` and the trace -that verifies it. Trace scripts live in `stuff/spacetaxi/trace.py` +that verifies it. Trace scripts live in `stuff/trace.py` plus the inline scripts run during verification sessions. ## VERIFIED via emulator trace @@ -81,10 +81,10 @@ plus the inline scripts run during verification sessions. ## TRACE INFRASTRUCTURE -- `stuff/spacetaxi/trace.py`: instrumented wrapper around `cpu6502.Cpu6502`. `Tracer.from_dump('raw.bin').call(addr)` runs a subroutine and snapshots memory + JSR chain. -- `examples/spacetaxi/assets/cpu6502.py`: minimal 6502 emulator. Sufficient for all routines tested above; throws `NotImplementedError` for unsupported opcodes (none hit so far in gameplay paths). -- `examples/spacetaxi/assets/dumpScenes.py` + `stuff/spacetaxi/dumpAllLevels.py`: pre-existing scene-decompress drivers. -- `examples/spacetaxi/assets/romToLevel.py`: emits the `.dat` files from raw.bin via the same emulator. +- `stuff/trace.py`: instrumented wrapper around `cpu6502.Cpu6502`. `Tracer.from_dump('raw.bin').call(addr)` runs a subroutine and snapshots memory + JSR chain. +- `assets/cpu6502.py`: minimal 6502 emulator. Sufficient for all routines tested above; throws `NotImplementedError` for unsupported opcodes (none hit so far in gameplay paths). +- `assets/dumpScenes.py` + `stuff/dumpAllLevels.py`: pre-existing scene-decompress drivers. +- `assets/romToLevel.py`: emits the `.dat` files from raw.bin via the same emulator. ## METHODOLOGY (for future verification sessions) diff --git a/assets/depackSpeechRam.py b/assets/depackSpeechRam.py index 22752b0..e03c1d4 100755 --- a/assets/depackSpeechRam.py +++ b/assets/depackSpeechRam.py @@ -16,7 +16,7 @@ entry, and the depacked image IS the RAM -- $A000-$BFFF included. Feed the result to extractSpeech.py. Usage: depackSpeechRam.py [spaceTaxiPlus.prg] [out.bin] - Defaults resolve inside stuff/spacetaxi/ (gitignored research + Defaults resolve inside stuff/ (gitignored research area, same as genC64Data.py's inputs). """ @@ -24,7 +24,7 @@ import os import sys HERE = os.path.dirname(os.path.abspath(__file__)) -STUFF = os.path.join(HERE, "..", "..", "..", "stuff", "spacetaxi") +STUFF = os.path.join(HERE, "..", "stuff") # The BASIC stub is `0B08 D307 9E "2059" 00` -- SYS 2059. ENTRY = 0x080B diff --git a/assets/extractSpeech.py b/assets/extractSpeech.py index e666e7f..621480b 100755 --- a/assets/extractSpeech.py +++ b/assets/extractSpeech.py @@ -2,7 +2,7 @@ """Extract Space Taxi's digitised speech from a C64 memory dump. FORMAT (reverse-engineered from bankedSfxLoad at $9802, see -stuff/spacetaxi/live-level1.lst). It is NOT 4-bit $D418 digi and it is +stuff/live-level1.lst). It is NOT 4-bit $D418 digi and it is NOT the SID speech synth. It is 1-BIT RUN-LENGTH audio: * An index table at $9B00 holds [code, ptrLo, ptrHi] triples, diff --git a/assets/genC64Data.py b/assets/genC64Data.py index 76b25bf..323d8d0 100644 --- a/assets/genC64Data.py +++ b/assets/genC64Data.py @@ -8,16 +8,16 @@ # (The level-intro star velocity tables, $450C/$4513, went with the moving # warp; claudeDocs/spacetaxiSpriteWarp.patch puts them back.) # -# Everything comes straight out of stuff/spacetaxi/raw.bin (a VICE +# Everything comes straight out of stuff/raw.bin (a VICE # "bank ram" dump, 2-byte load-address header) so the port has one # source of truth: the original bytes. Output: -# examples/spacetaxi/stC64Data.c / stC64Data.h (committed, do not edit) +# stC64Data.c / stC64Data.h (committed, do not edit) import os import sys HERE = os.path.dirname(os.path.abspath(__file__)) -RAW = os.path.join(HERE, "..", "..", "..", "stuff", "spacetaxi", "raw.bin") +RAW = os.path.join(HERE, "..", "stuff", "raw.bin") OUT_C = os.path.join(HERE, "..", "stC64Data.c") OUT_H = os.path.join(HERE, "..", "stC64Data.h") @@ -76,7 +76,7 @@ def main(): sprite_count = SPRITE_PTR_LAST - SPRITE_PTR_FIRST + 1 h = [] - h.append("// Generated by assets/genC64Data.py from stuff/spacetaxi/raw.bin.") + h.append("// Generated by assets/genC64Data.py from stuff/raw.bin.") h.append("// Do not hand-edit; re-run the generator.") h.append("") h.append("#ifndef ST_C64_DATA_H") @@ -125,7 +125,7 @@ def main(): h.append("#endif") c = [] - c.append("// Generated by assets/genC64Data.py from stuff/spacetaxi/raw.bin.") + c.append("// Generated by assets/genC64Data.py from stuff/raw.bin.") c.append("// Do not hand-edit; re-run the generator.") c.append("") c.append('#include "stC64Data.h"') diff --git a/assets/genDemoStreams.py b/assets/genDemoStreams.py index 5095393..114b3f8 100644 --- a/assets/genDemoStreams.py +++ b/assets/genDemoStreams.py @@ -16,7 +16,7 @@ import os HERE = os.path.dirname(os.path.abspath(__file__)) -EXTRACT = os.path.join(HERE, "..", "..", "..", "stuff", "spacetaxi", "extract") +EXTRACT = os.path.join(HERE, "..", "stuff", "extract") OUT = os.path.join(HERE, "..", "stDemoStreams.h") # (file, symbol, 0-based level index, screen letter) in ROTATION order. @@ -50,7 +50,7 @@ def main(): # The playback buffer at $0902 as the boot leaves it (demo H # loaded over older RAM): recordings shorter than the previous one # leave its tail in place, and the game reads on into it. - raw = open(os.path.join(HERE, "..", "..", "..", "stuff", "spacetaxi", "raw.bin"), "rb").read()[2:] + raw = open(os.path.join(HERE, "..", "stuff", "raw.bin"), "rb").read()[2:] buf = raw[0x0902:0x0902 + 640] f.write("#define ST_DEMO_BUFFER_BYTES 640u\n") f.write("static const uint8_t kDemoBufferInit[ST_DEMO_BUFFER_BYTES] = {\n") diff --git a/assets/genSongData.py b/assets/genSongData.py index 8610eab..6f1f441 100644 --- a/assets/genSongData.py +++ b/assets/genSongData.py @@ -1,7 +1,8 @@ #!/usr/bin/env python3 -# Bake examples/spacetaxi/assets/songs/song*.song into stSongData.h. +# Bake assets/songs/song*.song into stSongData.h with JoeyLib's songbake +# (JOEYLIB names the checkout, ../joeylib by default). # -# python3 examples/spacetaxi/assets/genSongData.py +# python3 assets/genSongData.py # # The .song sources come from the game's own player run under a 6502 # emulator: assets/sidCapture.py logs every SID write per PAL frame and @@ -12,8 +13,8 @@ import sys import tempfile here = os.path.dirname(os.path.abspath(__file__)) -repo = os.path.abspath(os.path.join(here, "..", "..", "..")) -songbake = os.path.join(repo, "tools", "songbake", "songbake.py") +joeylib = os.environ.get("JOEYLIB", os.path.join(here, "..", "..", "joeylib")) +songbake = os.path.join(joeylib, "tools", "songbake", "songbake.py") out = os.path.join(here, "..", "stSongData.h") count = 9 diff --git a/assets/levels/format.md b/assets/levels/format.md index ea60e44..2a3b768 100644 --- a/assets/levels/format.md +++ b/assets/levels/format.md @@ -1,7 +1,7 @@ # Space Taxi level .dat format (STL2) A small custom binary format that `stLevelLoad()` reads at scene -boot. Output files land in `examples/spacetaxi/generated/levels/` +boot. Output files land in `generated/levels/` (from `romToLevel.py` for the 24 canonical C64 levels, or from `mkstlevel` for hand-authored levels in `assets/levels/*.txt`). The build copies them under `DATA/levels/levelNN.dat` in the runtime @@ -74,15 +74,15 @@ without changes. 1. Author a 256-tile (or smaller) tile sheet as an indexed PNG. Each tile is 8x8 px. Arrange in a grid; the bank loader assumes row- major, tile-index = `ty * tilesPerRow + tx`. Drop the PNG at - `examples/spacetaxi/assets/tiles/tbank.png`. + `assets/tiles/tbank.png`. 2. The Makefile bakes per-target via `tools/assetbake/assetbake.py --type tile --target tbank.png tbank.tbk`. Output - lands in `examples/spacetaxi/generated//tiles/` and is + lands in `generated//tiles/` and is staged into the runtime tree at `build//.../DATA/tiles/`. 3. Author each level layout in a text editor or grid tool, save as a - .dat per this spec. The helper `examples/spacetaxi/mkstlevel` + .dat per this spec. The helper `mkstlevel` converts from a human-readable text grid to .dat. The 24 canonical - C64 levels are emitted by `examples/spacetaxi/assets/romToLevel.py` directly + C64 levels are emitted by `assets/romToLevel.py` directly from a raw VICE dump of the original .prg. ## Naming diff --git a/assets/romToLevel.py b/assets/romToLevel.py index 7c101c9..316ec16 100644 --- a/assets/romToLevel.py +++ b/assets/romToLevel.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # Convert ROM-extracted Space Taxi scenes directly into the runtime -# STL1 binary format (see examples/spacetaxi/stLevel.c). Uses the +# STL1 binary format (see stLevel.c). Uses the # 6502 emulator + raw RAM dump to decompress each scene, then writes # screen + color RAM bytes straight into the .dat layout the JoeyLib # port loads at runtime. @@ -25,7 +25,7 @@ from cpu6502 import Cpu6502 from dumpScenes import load_dump, run_scene -# STL2 binary layout (see examples/spacetaxi/stLevel.c): +# STL2 binary layout (see stLevel.c): # "STL2" # nameLen + name # tileBank, music, bgColor, borderColor, taxiSpawnX, taxiSpawnY diff --git a/installer/components/staxiData.sh b/installer/components/staxiData.sh deleted file mode 100644 index a80ee0c..0000000 --- a/installer/components/staxiData.sh +++ /dev/null @@ -1,30 +0,0 @@ -# Space Taxi's level source: a memory image of the original Commodore 64 -# game. Optional -- without it every port still builds, skipping only the -# Space Taxi data -- so it is not part of `all`; install it by name. - -compDescription="Space Taxi C64 memory image (private, optional: bundle or import)" -compGroups="staxi optional" -compRequires="" -compHostDeps="" -compPrivateFile="stuff/spacetaxi/raw.bin" -compPin="$(privateSha1For "$compPrivateFile")" - - -compInstall() { - privateEnsure "$compPrivateFile" -} - - -compInstalled() { - privatePresent "$compPrivateFile" -} - - -compVerify() { - privatePresent "$compPrivateFile" -} - - -compEnv() { - : -} diff --git a/mkstlevel/mkstlevel.c b/mkstlevel/mkstlevel.c index 57c0f65..a060350 100644 --- a/mkstlevel/mkstlevel.c +++ b/mkstlevel/mkstlevel.c @@ -1,5 +1,5 @@ // mkstlevel: convert a human-readable text level definition into the -// STL3 binary format read by examples/spacetaxi/stLevel.c. +// STL3 binary format read by stLevel.c. // // Usage: // mkstlevel input.txt output.dat diff --git a/patches/spriteWarp.patch b/patches/spriteWarp.patch new file mode 100644 index 0000000..0578f91 --- /dev/null +++ b/patches/spriteWarp.patch @@ -0,0 +1,617 @@ +diff -ruN a/assets/genC64Data.py b/assets/genC64Data.py +--- a/assets/genC64Data.py 2026-10-05 14:05:44.479069311 -0500 ++++ b/assets/genC64Data.py 2026-10-05 14:12:10.086072322 -0500 +@@ -67,6 +67,8 @@ + ("uint8_t stC64RngByte(uint8_t idx);", "kStRngTable[idx]"), + ("const uint8_t *stC64SfxProgram(uint8_t idx);", "kStSfxPrograms[idx]"), + ("uint8_t stC64FlameCel(uint8_t dirMask);", "kStFlameCelByDirMask[dirMask]"), ++ ("int8_t stC64IntroStarDx(uint8_t idx);", "kStIntroStarDx[idx]"), ++ ("int8_t stC64IntroStarDy(uint8_t idx);", "kStIntroStarDy[idx]"), + ] + + +@@ -115,6 +117,10 @@ + h.append("// 8=RIGHT); 0 = no cel for that combination.") + h.append("extern const uint8_t kStFlameCelByDirMask[16];") + h.append("") ++ h.append("// Level-intro star velocities per sprite 0..6 ($450C / $4513).") ++ h.append("extern const int8_t kStIntroStarDx[7];") ++ h.append("extern const int8_t kStIntroStarDy[7];") ++ h.append("") + h.append("// Cross-segment-safe accessors (see stC64Data.c): reach the tables above") + h.append("// by an inter-segment call on the IIgs, where a direct 2-byte data") + h.append("// reference from another bank is not relocatable.") +@@ -160,6 +166,11 @@ + c.append(c_bytes(flame)) + c.append("};") + c.append("") ++ dx = [b - 256 if b >= 128 else b for b in raw[0x450C:0x450C + 7]] ++ dy = [b - 256 if b >= 128 else b for b in raw[0x4513:0x4513 + 7]] ++ c.append("const int8_t kStIntroStarDx[7] = { " + ", ".join(str(v) for v in dx) + " };") ++ c.append("const int8_t kStIntroStarDy[7] = { " + ", ".join(str(v) for v in dy) + " };") ++ c.append("") + c.append("") + c.append("// ---------------------------------------------------------------------------") + c.append("// Accessors. On the IIgs the program is split across banks and a 2-byte") +diff -ruN a/spacetaxi.c b/spacetaxi.c +--- a/spacetaxi.c 2026-10-05 14:05:44.477069267 -0500 ++++ b/spacetaxi.c 2026-10-05 14:12:10.087072342 -0500 +@@ -925,8 +925,6 @@ + static void startIntro(void) { + stIntroEnter(&gSim, &gLevel); + stRenderSceneChanged(&gSim); +- // After the scene change, which turns it back off. +- stRenderIntroStars(); + // $459C: the intro's tune plays under the fly-in, not blocking; the + // C64 leaves it to the IRQ player and stops it at $4709. + stAudioMusic(ST_SONG_INTRO, true); +diff -ruN a/spacetaxi.h b/spacetaxi.h +--- a/spacetaxi.h 2026-10-05 14:05:44.478069289 -0500 ++++ b/spacetaxi.h 2026-10-05 14:12:10.088072362 -0500 +@@ -52,10 +52,6 @@ + // cycle are palette writes instead of repainting 103 (and 407) cells. + // Call after stRenderSceneChanged, which turns it back off. + void stRenderTitleLogo(StSimT *sim); +-// Level intro only: cycle the starfield's ring palette slots so the warp +-// animates without a single pixel moving. Call after +-// stRenderSceneChanged, which turns it back off. +-void stRenderIntroStars(void); + // Fill the stage with a two-line message and present it. Used when an + // asset the game ships with will not load, so a player sees why the + // game stopped instead of a silently degraded ride. +diff -ruN a/stC64Data.c b/stC64Data.c +--- a/stC64Data.c 2026-10-05 14:05:44.475069223 -0500 ++++ b/stC64Data.c 2026-10-05 14:12:10.085072302 -0500 +@@ -1322,6 +1322,9 @@ + 0x00, 0xD8, 0xD4, 0x00, 0xD5, 0xD2, 0xD1, 0x00, 0xD7, 0xD3, 0xD6, 0x00, 0x00, 0x00, 0x00, 0x00, + }; + ++const int8_t kStIntroStarDx[7] = { 0, 6, 6, 6, -6, -6, -6 }; ++const int8_t kStIntroStarDy[7] = { -4, -4, 0, 4, 4, 0, -4 }; ++ + + // --------------------------------------------------------------------------- + // Accessors. On the IIgs the program is split across banks and a 2-byte +@@ -1354,3 +1357,13 @@ + uint8_t stC64FlameCel(uint8_t dirMask) { + return kStFlameCelByDirMask[dirMask]; + } ++ ++ ++int8_t stC64IntroStarDx(uint8_t idx) { ++ return kStIntroStarDx[idx]; ++} ++ ++ ++int8_t stC64IntroStarDy(uint8_t idx) { ++ return kStIntroStarDy[idx]; ++} +diff -ruN a/stC64Data.h b/stC64Data.h +--- a/stC64Data.h 2026-10-05 14:05:44.476069245 -0500 ++++ b/stC64Data.h 2026-10-05 14:12:10.086072322 -0500 +@@ -44,6 +44,10 @@ + // 8=RIGHT); 0 = no cel for that combination. + extern const uint8_t kStFlameCelByDirMask[16]; + ++// Level-intro star velocities per sprite 0..6 ($450C / $4513). ++extern const int8_t kStIntroStarDx[7]; ++extern const int8_t kStIntroStarDy[7]; ++ + // Cross-segment-safe accessors (see stC64Data.c): reach the tables above + // by an inter-segment call on the IIgs, where a direct 2-byte data + // reference from another bank is not relocatable. +@@ -52,5 +56,7 @@ + uint8_t stC64RngByte(uint8_t idx); + const uint8_t *stC64SfxProgram(uint8_t idx); + uint8_t stC64FlameCel(uint8_t dirMask); ++int8_t stC64IntroStarDx(uint8_t idx); ++int8_t stC64IntroStarDy(uint8_t idx); + + #endif +diff -ruN a/stCels.c b/stCels.c +--- a/stCels.c 2026-10-05 14:06:02.361459928 -0500 ++++ b/stCels.c 2026-10-05 14:12:10.090072402 -0500 +@@ -2,9 +2,9 @@ + // See stCels.h. Ordered exactly as stRenderPrewarm built the cels: the + // cab set (flying, wreck and the intro decorations) and passenger set in + // multicolour at the constant level colours (cab/passenger 6, shared +-// $D025/$D026 = 2/7), then the flame set in multicolour colour 7. No +-// intro star: the level intro paints its starfield into the character +-// screen (stTitle.c), so the star sprite is never drawn. ++// $D025/$D026 = 2/7), then the flame set in multicolour colour 7. The ++// intro star is not here: a bank's cels share one size, and the star is ++// built cropped to its one tile at boot (prewarmStar in stRender.c). + + #include "stCels.h" + +diff -ruN a/stRender.c b/stRender.c +--- a/stRender.c 2026-10-05 14:07:17.861084192 -0500 ++++ b/stRender.c 2026-10-05 14:12:10.098072560 -0500 +@@ -37,7 +37,7 @@ + // The IIgs reaches its globals DBR-relative, so all of BSS must fit the + // entry bank below the I/O window; the caches are sized down there. The + // sprite cache must still hold every cel stRenderPrewarm builds (the +-// cab, exhaust and passenger sets) or a cel change mid-play rebuilds ++// cab, exhaust and passenger sets and the intro star) or a cel change mid-play rebuilds + // and recompiles a sprite. Glyphs are direct-mapped by character, so + // the count is a power of two. + // Room for the baked cab/passenger/exhaust set (36) plus the largest +@@ -96,9 +96,6 @@ + // Title logo (stRenderTitleLogo): all four flip-book frames in one + // tile, animated by rewriting logoSlotMask[]'s palette slots. + bool logoMode; +- bool starMode; // level intro: cycling the starfield +- uint8_t starPhase; // which ring the bright band is on +- uint8_t starTick; // frames until the band steps outward + bool logoTileOk; // the live glyph is one of the frames + uint8_t logoSlotCount; + uint8_t logoSlotMask[ST_LOGO_PAL_SLOTS]; // frames each slot is lit in +@@ -141,16 +138,6 @@ + // four of these are used; the rest are headroom if the frames change. + static const uint8_t kLogoPalSlot[ST_LOGO_PAL_SLOTS] = { 4u, 8u, 9u, 10u, 13u, 14u, 15u }; + +-// Level-intro warp: how a star ring looks at each distance BEHIND the +-// bright band, as C64 colour indices (white, light grey, grey, dark +-// grey, then black). A ring this far behind the band or further is +-// unlit, so a lit head with a short fading tail sweeps outward and the +-// rest of the field stays dark. +-static const uint8_t kStarRamp[] = { 1u, 15u, 12u, 11u }; +-#define ST_STAR_RAMP_LEN (sizeof(kStarRamp) / sizeof(kStarRamp[0])) +-// Frames per ring step. The intro runs at the game's 30 Hz tick, so a +-// step every other frame sweeps the eight rings in about half a second. +-#define ST_STAR_STEP_FRAMES 2u + + + static StSpriteCacheT *cachedSprite(StSimT *sim, uint8_t idx); +@@ -175,8 +162,8 @@ + static void paintCells(jlSurfaceT *stage, StSimT *sim); + static void pasteCell(jlSurfaceT *stage, const StSimT *sim, uint16_t cell, uint8_t bx, uint8_t by); + static bool prewarmReport(uint16_t expect); ++static void prewarmStar(StSimT *sim); + static bool spriteOnScreen(int16_t px, int16_t py); +-static void starPaletteApply(void); + static void tileFromChunky(jlTileT *out, const uint8_t *chunky); + static bool tileHasPixels(const uint8_t *chunky); + +@@ -677,29 +664,6 @@ + } + + +-// Light the starfield's ring colours for the current phase: the ring +-// the band is on takes kStarRamp[0], the ones just inside it the rest +-// of the ramp, and everything else the background. One palette write +-// per step and not a single pixel moves -- that is the whole point of +-// ST_STAR_RING_FIRST (stSim.h). +-static void starPaletteApply(void) { +- uint16_t pal[SURFACE_COLORS_PER_PALETTE]; +- uint16_t color; +- uint8_t behind; +- uint8_t ring; +- +- jlPaletteGet(gRender.stage, 0u, pal); +- for (ring = 0u; ring < ST_STAR_RING_COUNT; ring++) { +- // How far this ring sits behind the band, wrapping so the band +- // re-enters at the centre as it leaves the edge. +- behind = (uint8_t)((gRender.starPhase - ring) & (ST_STAR_RING_COUNT - 1u)); +- color = (behind < ST_STAR_RAMP_LEN) ? kC64Palette[kStarRamp[behind]] : kC64Palette[0]; +- pal[ST_STAR_RING_FIRST + ring] = color; +- } +- jlPaletteSet(gRender.stage, 0u, pal); +-} +- +- + static bool spriteOnScreen(int16_t px, int16_t py) { + return px < SURFACE_WIDTH && py < SURFACE_HEIGHT && px > -ST_SPRITE_W && py > -(int16_t)ST_SPRITE_PX; + } +@@ -769,17 +733,6 @@ + } + } + } +- if (gRender.starMode) { +- // The warp is the only thing animating here, and it animates in +- // the palette: no cells change, no sprite moves for it, so the +- // present that ends this frame copies no pixels at all. +- gRender.starTick++; +- if (gRender.starTick >= ST_STAR_STEP_FRAMES) { +- gRender.starTick = 0u; +- gRender.starPhase = (uint8_t)((gRender.starPhase + 1u) & (ST_STAR_RING_COUNT - 1u)); +- starPaletteApply(); +- } +- } + collectGlyphs(sim); + dirtyBands(sim); + for (k = 0u; k < ST_HW_SPRITES; k++) { +@@ -1148,7 +1101,7 @@ + static const uint8_t kCabPtrs[] = { 0xC0, 0xC1, 0xDC, 0xDD, 0xCC, 0xCD, 0xCE, 0xCF, 0xD0, 0xD1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8 }; + static const uint8_t kPassPtrs[] = { 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xCB, 0xD9 }; + static const uint8_t kFlamePtrs[] = { 0xD8, 0xD4, 0xD5, 0xD2, 0xD1, 0xD7, 0xD3, 0xD6 }; +- uint8_t total = (uint8_t)(sizeof(kCabPtrs) + sizeof(kPassPtrs) + sizeof(kFlamePtrs)); ++ uint8_t total = (uint8_t)(sizeof(kCabPtrs) + sizeof(kPassPtrs) + sizeof(kFlamePtrs) + 1u); + uint8_t done = 0u; + bool ok; + uint8_t k; +@@ -1158,7 +1111,8 @@ + // runtime build below on any port without a matching .spc. + if (loadPrecompiledCels()) { + ST_LOG("spacetaxi: cels from sprites/staxicels.spc"); +- return prewarmReport(kStCelCount); ++ prewarmStar(sim); ++ return prewarmReport((uint16_t)(kStCelCount + 1u)); + } + ST_LOG("spacetaxi: ! no usable sprites/staxicels.spc -- JIT prewarm"); + saved = sim->frame; +@@ -1189,6 +1143,7 @@ + } + sim->frame = saved; + gPrewarming = false; ++ prewarmStar(sim); + ok = prewarmReport(total); + jlFillRect(stage, 60, 110, 200u, 6u, 0u); + return ok; +@@ -1231,15 +1186,32 @@ + } + + ++// The intro star is built and compiled here rather than baked: a bank's ++// cels all share one size (3x3 tiles), and the star's nine pixels fit ++// one tile, so a baked star would save, draw, restore and present nine ++// times its area -- seven times a frame. A moving (idx <= 2) cel, so it ++// compiles; the interpreter cannot keep up with seven of them. ++static void prewarmStar(StSimT *sim) { ++ StFrameT saved = sim->frame; ++ ++ gPrewarming = true; ++ sim->frame.multiMask = 0u; ++ sim->frame.ptr[2] = ST_INTRO_STAR_PTR; ++ sim->frame.color[2] = ST_INTRO_STAR_COLOR; ++ (void)cachedSprite(sim, 2u); ++ sim->frame = saved; ++ gPrewarming = false; ++} ++ ++ + // A new scene: nothing drawn is valid any more. + void stRenderSceneChanged(StSimT *sim) { + uint8_t k; + +- if (gRender.logoMode || gRender.starMode) { ++ if (gRender.logoMode) { + // The borrowed slots go back to being C64 colours. + gRender.logoMode = false; + gRender.logoTileOk = false; +- gRender.starMode = false; + jlPaletteSet(gRender.stage, 0u, kC64Palette); + } + for (k = 0u; k < ST_HW_SPRITES; k++) { +@@ -1269,18 +1241,6 @@ + } + + +-// Turn on the level intro's palette-cycled starfield. Call it after +-// stRenderSceneChanged (which turns it back off) on entry to the level +-// intro. The cells were painted by stIntroEnter; from here the warp is +-// one palette write every ST_STAR_STEP_FRAMES frames. +-void stRenderIntroStars(void) { +- gRender.starMode = true; +- gRender.starPhase = 0u; +- gRender.starTick = 0u; +- starPaletteApply(); +-} +- +- + void stRenderShutdown(void) { + dropCache(); + if (gRender.scratch != 0) { +diff -ruN a/stSim.h b/stSim.h +--- a/stSim.h 2026-10-05 14:05:53.055256946 -0500 ++++ b/stSim.h 2026-10-05 14:12:10.089072382 -0500 +@@ -82,42 +82,16 @@ + #define ST_CHAR_ELECTROID 0x6Fu // level O's scrolling barrier glyph + #define ST_FARE_DEST_UP 0x0Bu // $7D8E: the fare aboard wants UP + +-// ---- level-intro starfield -------------------------------------------- ++// ---- level-intro star warp -------------------------------------------- + // +-// The C64's level intro streams seven star SPRITES out of the centre of +-// the screen. That is the most expensive thing this port asks a present +-// for: eight sprites are save-under drawn and restored every frame, and +-// because a row's dirty band is one min..max interval, two stars far +-// apart on the same row widen that row to nearly full width. Radially +-// spreading stars maximise exactly that, so the warp ends up presenting +-// close to the whole framebuffer every frame -- which is what makes it +-// the slowest thing the IIgs does, and wasted work everywhere else. +-// +-// So the warp is not animated in pixels at all. The starfield is painted +-// ONCE into screen RAM and animated by cycling the colours its cells are +-// drawn in -- the same trick the title logo already uses +-// (stRenderTitleLogo). A frame costs one palette upload and NO dirty +-// pixel rows. +-// +-// The contract between the two halves is just this colour range: +-// stTitle.c colours a star's cell ST_STAR_RING_FIRST + ring (ring 0 is +-// the centre), and the renderer cycles that range. These are C64 colour +-// indices the intro screen never uses -- it draws in 0, 3 and, in demo +-// mode, 4 and 5; the cab sprite adds 2, 6 and 7. +-#define ST_STAR_RING_FIRST 8u +-#define ST_STAR_RING_COUNT 8u +- +-// The renderer wraps the sweep's phase with a mask rather than a +-// modulo, so the ring count has to stay a power of two. Changing it to +-// something else must fail to compile here instead of quietly making +-// the warp skip rings. +-typedef char stStarRingCountIsPow2[((ST_STAR_RING_COUNT & (ST_STAR_RING_COUNT - 1u)) == 0u) ? 1 : -1]; +-// Ring 0 is the centre, which in a warp is the VANISHING POINT: those +-// stars are the far ones and get the small glyph. The big glyph belongs +-// to the outer rings, the stars about to rush past the viewer. +-#define ST_STAR_GLYPH_NEAR 0x2Au // '*', the outer rings +-#define ST_STAR_GLYPH_FAR 0x2Eu // '.', the inner rings +-#define ST_STAR_FAR_RINGS 5u // rings 0..4 are still distant ++// The C64's level intro ($4666) streams seven hires star sprites -- ++// hardware sprites 0..6, all showing block $DA -- out of the centre of ++// the screen: each waits out a random spawn delay ($44F8), then flies on ++// its own fixed velocity ($450C / $4513) for $20 frames ($4500) and ++// respawns near the centre. ++#define ST_INTRO_STARS 7u ++#define ST_INTRO_STAR_PTR 0xDAu // a hires 5x3-pixel star ++#define ST_INTRO_STAR_COLOR 0x07u + + // VIC sprite block pointers the game selects by name. The cab's bit 0 is + // the landing gear, so each facing is a pair. +@@ -423,6 +397,10 @@ + // 407 cells of colour RAM); part of the picture, so it survives the + // same way colour RAM does. + uint8_t logoColor; ++ // $4500: frames until each intro star respawns. Neither $5EC4 nor the ++ // intro ($4523 seeds only the spawn delays) clears it, so a star whose ++ // delay draws 0 flies on whatever the previous intro left here. ++ uint8_t starReload[ST_INTRO_STARS]; + + // ---- cleared by stSimNewGame from here down ---- + +@@ -550,6 +528,11 @@ + uint8_t introLoopCount; // $4508 + uint8_t introCabDx; // $450A + uint8_t introCabDxTimer; // $450B ++ uint8_t starTimer[ST_INTRO_STARS]; // $44F8: frames until the star first shows ++ // The 6502 carry flag as the $4666 star loop finds it. The respawn ++ // column is an ADC that never clears it first ($4684), so the flag ++ // left by whatever ran before is part of where a star appears. ++ uint8_t introCarry; + uint8_t spriteMc0; // $D025 + uint8_t spriteMc1; // $D026 + +diff -ruN a/stTitle.c b/stTitle.c +--- a/stTitle.c 2026-10-05 14:05:44.474069201 -0500 ++++ b/stTitle.c 2026-10-05 14:12:10.084072282 -0500 +@@ -11,11 +11,6 @@ + // Level intro ($4523..$4727): a black screen with the level name, the + // cab rising from the bottom and seven star sprites streaming out of + // the centre, until the cab reaches mid-screen. +-// +-// The seven star sprites are replaced by a static starfield whose +-// colours are cycled -- see ST_STAR_RING_FIRST in stSim.h for why and +-// how. The cab still flies, and it is still what ends the intro, so the +-// sequence keeps its length and its cue; only the warp changes. + + #include + +@@ -39,48 +34,36 @@ + #define ST_TITLE_CAB_LIFT_ROW 0x14u + #define ST_TITLE_SPARKLE_TICKS 0x5Au + #define ST_TITLE_HOVER_COL 0x28u +-#define ST_INTRO_STAR_PTR 0xDAu ++#define ST_INTRO_STAR_START_ROW 0x8Cu ++#define ST_INTRO_STAR_START_COL 0xAAu ++#define ST_INTRO_STAR_DELAY 0x20u // $4080 range of the spawn delay ++#define ST_INTRO_STAR_RELOAD 0x20u // frames a star flies before respawning ++// $467F..$4694: a respawning star's position comes from star 0's row -- ++// its low three bits pick the column, bits 4..5 the row. ++#define ST_INTRO_SPAWN_COL 0xA6u ++#define ST_INTRO_SPAWN_COL_BITS 0x07u ++#define ST_INTRO_SPAWN_ROW 0x89u ++#define ST_INTRO_SPAWN_ROW_BITS 0x30u ++#define ST_INTRO_SPAWN_SHIFT 4u ++#define ST_INTRO_CAB_START_ROW 0xE4u + #define ST_INTRO_CAB_END_ROW 0x94u + #define ST_INTRO_CAB_DX_RELOAD 0x28u + +-// The palette-cycled starfield. Seven stars per ring so the bright band +-// stays the same weight all the way out, placed at fixed pseudo-random +-// angles and clear of the three lines the intro screen writes (the level +-// name on row 12, and the demo hints on rows 3 and 24). +-typedef struct { +- uint8_t col; +- uint8_t row; +- uint8_t ring; +-} StStarT; +- +-static const StStarT kIntroStar[] = { +- { 18u, 10u, 0u }, { 19u, 10u, 0u }, { 17u, 11u, 0u }, { 18u, 13u, 0u }, +- { 21u, 13u, 0u }, { 22u, 14u, 0u }, { 18u, 15u, 0u }, { 18u, 8u, 1u }, +- { 19u, 8u, 1u }, { 14u, 10u, 1u }, { 25u, 14u, 1u }, { 21u, 15u, 1u }, +- { 25u, 15u, 1u }, { 20u, 17u, 1u }, { 26u, 7u, 2u }, { 12u, 10u, 2u }, +- { 29u, 11u, 2u }, { 11u, 13u, 2u }, { 29u, 14u, 2u }, { 21u, 17u, 2u }, +- { 23u, 18u, 2u }, { 21u, 4u, 3u }, { 24u, 4u, 3u }, { 11u, 8u, 3u }, +- { 12u, 8u, 3u }, { 10u, 10u, 3u }, { 30u, 16u, 3u }, { 14u, 17u, 3u }, +- { 15u, 2u, 4u }, { 8u, 8u, 4u }, { 32u, 9u, 4u }, { 9u, 17u, 4u }, +- { 32u, 17u, 4u }, { 32u, 18u, 4u }, { 24u, 22u, 4u }, { 16u, 1u, 5u }, +- { 28u, 1u, 5u }, { 31u, 2u, 5u }, { 36u, 13u, 5u }, { 6u, 15u, 5u }, +- { 4u, 16u, 5u }, { 5u, 19u, 5u }, { 8u, 0u, 6u }, { 30u, 0u, 6u }, +- { 1u, 8u, 6u }, { 2u, 12u, 6u }, { 37u, 16u, 6u }, { 7u, 21u, 6u }, +- { 32u, 22u, 6u }, { 0u, 4u, 7u }, { 39u, 7u, 7u }, { 0u, 10u, 7u }, +- { 38u, 20u, 7u }, { 38u, 21u, 7u }, { 4u, 22u, 7u }, { 35u, 22u, 7u }, +-}; +-#define ST_INTRO_STAR_COUNT (sizeof(kIntroStar) / sizeof(kIntroStar[0])) +- +- +-static void addToRow(StSimT *sim, uint8_t idx, uint8_t delta); +-static void logoColorCycle(StSimT *sim); +-static void logoFlip(StSimT *sim); +-static void paintStarfield(StSimT *sim); +- +- +-// $4113 +-static void addToRow(StSimT *sim, uint8_t idx, uint8_t delta) { +- sim->spr[idx].row = (uint8_t)(sim->spr[idx].row + delta); ++ ++static uint8_t addToRow(StSimT *sim, uint8_t idx, uint8_t delta); ++static void logoColorCycle(StSimT *sim); ++static void logoFlip(StSimT *sim); ++static void starRespawn(StSimT *sim, uint8_t idx, uint8_t carry); ++static void starsStep(StSimT *sim); ++ ++ ++// $4113 -- CLC; ADC row. The carry it leaves is the 6502 flag the star ++// loop goes on with (see introCarry in stSim.h). ++static uint8_t addToRow(StSimT *sim, uint8_t idx, uint8_t delta) { ++ uint16_t row = (uint16_t)((uint16_t)sim->spr[idx].row + delta); ++ ++ sim->spr[idx].row = (uint8_t)row; ++ return (uint8_t)(row >> 8); + } + + +@@ -124,20 +107,47 @@ + } + + +-// Paint the level-intro starfield into screen RAM, once. A star's cell +-// is coloured ST_STAR_RING_FIRST + its ring and never touched again -- +-// the renderer animates the warp by cycling those colours, which costs +-// no dirty rows at all. See ST_STAR_RING_FIRST in stSim.h. +-static void paintStarfield(StSimT *sim) { +- const StStarT *star; +- uint8_t one[2]; +- uint8_t k; +- +- one[1] = 0u; // stSimDrawText stops below screen code 6 +- for (k = 0u; k < (uint8_t)ST_INTRO_STAR_COUNT; k++) { +- star = &kIntroStar[k]; +- one[0] = (star->ring < ST_STAR_FAR_RINGS) ? ST_STAR_GLYPH_FAR : ST_STAR_GLYPH_NEAR; +- stSimDrawText(sim, star->col, star->row, one, (uint8_t)(ST_STAR_RING_FIRST + star->ring)); ++// $467A -- a star (re)appears near the centre, placed from star 0's ++// row. The column's ADC adds the carry the loop arrived with; the row's ++// adds the bit the last LSR shifted out, which the mask keeps clear, and ++// its sum stays below $100, so the loop goes on with the carry clear. ++static void starRespawn(StSimT *sim, uint8_t idx, uint8_t carry) { ++ uint8_t seed = sim->spr[0].row; ++ ++ sim->spr[idx].msb = 0u; ++ sim->spr[idx].col = (uint8_t)((seed & ST_INTRO_SPAWN_COL_BITS) + ST_INTRO_SPAWN_COL + carry); ++ sim->spr[idx].row = (uint8_t)(((seed & ST_INTRO_SPAWN_ROW_BITS) >> ST_INTRO_SPAWN_SHIFT) + ST_INTRO_SPAWN_ROW); ++ sim->starReload[idx] = ST_INTRO_STAR_RELOAD; ++} ++ ++ ++// $4662..$46A2 -- the seven stars, sprite 6 down to sprite 0. A star ++// counts down its spawn delay and appears ($4675); from then on it flies ++// on its own velocity ($4711) and respawns when its reload runs out. ++// The carry threads through the loop exactly as the 6502 leaves it: a ++// flight's row add sets it, a respawn clears it, a waiting star keeps it. ++static void starsStep(StSimT *sim) { ++ uint8_t carry = sim->introCarry; ++ uint8_t k; ++ ++ for (k = 0u; k < ST_INTRO_STARS; k++) { ++ uint8_t idx = (uint8_t)(ST_INTRO_STARS - 1u - k); ++ if (sim->starTimer[idx] == 0u) { ++ stSimSpriteAddX(sim, idx, (uint8_t)stC64IntroStarDx(idx)); ++ carry = addToRow(sim, idx, (uint8_t)stC64IntroStarDy(idx)); ++ sim->starReload[idx]--; ++ if (sim->starReload[idx] == 0u) { ++ starRespawn(sim, idx, carry); ++ carry = 0u; ++ } ++ continue; ++ } ++ sim->starTimer[idx]--; ++ if (sim->starTimer[idx] == 0u) { ++ sim->spr[idx].enable = 1u; ++ starRespawn(sim, idx, carry); ++ carry = 0u; ++ } + } + } + +@@ -158,23 +168,20 @@ + sim->spr[7].col = 0xAAu; + sim->spr[7].msb = 0u; + sim->spr[7].color = 0x06u; +- sim->spr[7].row = 0xE4u; ++ sim->spr[7].row = ST_INTRO_CAB_START_ROW; + sim->introCabDxTimer = 0x14u; + sim->introCabDx = 0x01u; +- for (k = 0u; k < 7u; k++) { +- uint8_t idx = (uint8_t)(6u - k); +- sim->spr[idx].row = 0x8Cu; ++ for (k = 0u; k < ST_INTRO_STARS; k++) { ++ uint8_t idx = (uint8_t)(ST_INTRO_STARS - 1u - k); ++ sim->spr[idx].row = ST_INTRO_STAR_START_ROW; + sim->spr[idx].msb = 0u; +- sim->spr[idx].col = 0xAAu; +- sim->spr[idx].color = 0x07u; ++ sim->spr[idx].col = ST_INTRO_STAR_START_COL; ++ sim->spr[idx].color = ST_INTRO_STAR_COLOR; + sim->spr[idx].ptr = ST_INTRO_STAR_PTR; + sim->spr[idx].enable = 0u; +- // $44F8/$4500 held each star's spawn timer and reload. Nothing +- // reads them now that the warp is painted rather than flown, but +- // the seven RNG draws MUST stay: they advance the shared RNG and +- // the level that follows draws from it, so dropping them would +- // change the game that comes after the intro. +- (void)stSimRng(sim, 0x20u); ++ // These draws also advance the shared RNG the level that follows ++ // draws from, so the next ride depends on them too. ++ sim->starTimer[idx] = stSimRng(sim, ST_INTRO_STAR_DELAY); + } + sim->borderColor = 0u; + sim->bgColor = 0u; +@@ -188,23 +195,26 @@ + sim->charDirtyAll = true; + sim->charDirtyCount = 0u; + stSimDrawText(sim, 10u, 12u, level->name, 3u); +- paintStarfield(sim); + if (sim->demoMode != 0u && sim->postMortem == 0u) { + stSimDrawText(sim, 7u, 3u, kTextDemoExit, 5u); + stSimDrawText(sim, 3u, 24u, kTextScreens, 4u); + } + sim->introLoopCount = 0u; ++ // $4662 is reached from $41C2's RTS, whose CMP #$06 on the text's ++ // zero terminator leaves the carry clear. ++ sim->introCarry = 0u; + } + + +-// $4666 -- one frame of the star field; true when the cab has risen ++// $4662 -- one frame of the star field; true when the cab has risen + // to mid-screen and everything is hidden again. + bool stIntroStep(StSimT *sim) { + uint8_t k; + ++ starsStep(sim); + stSimMarshal(sim); + sim->introLoopCount = (uint8_t)((sim->introLoopCount + 1u) & 3u); +- addToRow(sim, 7u, (uint8_t)kIntroCabDy[sim->introLoopCount]); ++ (void)addToRow(sim, 7u, (uint8_t)kIntroCabDy[sim->introLoopCount]); + if ((sim->introLoopCount & 1u) == 0u) { + stSimSpriteAddX(sim, 7u, sim->introCabDx); + sim->introCabDxTimer--; +@@ -214,6 +224,9 @@ + sim->introCabDx ^= 0xFEu; + } + } ++ // $46FC: CMP #$94. Its carry is what the next frame's star loop ++ // starts with; the cab is still above the end row, so it is set. ++ sim->introCarry = (sim->spr[7].row >= ST_INTRO_CAB_END_ROW) ? 1u : 0u; + if (sim->spr[7].row != ST_INTRO_CAB_END_ROW) { + return false; + } +@@ -300,7 +313,7 @@ + break; + case ST_STAGE_RIDING: + // $4A48: lift off under UP thrust until above row $14. +- addToRow(sim, 0u, 0xFEu); ++ (void)addToRow(sim, 0u, 0xFEu); + sim->dirMask = 0x01u; + sim->spr[0].ptr = 0xC0u; + stSimFlameUpdate(sim); diff --git a/program.mk b/program.mk new file mode 100644 index 0000000..9c79383 --- /dev/null +++ b/program.mk @@ -0,0 +1,145 @@ +# Space Taxi, described for JoeyLib's make/programs.mk (which lists what each +# variable means): its sources, how it ships on each port, and how its DATA +# is baked. Included by the Makefile beside it, on JoeyLib's make/project.mk. + +STAXI_HOME := $(abspath $(dir $(lastword $(MAKEFILE_LIST)))) + +# The levels are extracted from a memory image of the original C64 game, +# which is not part of the repository (scripts/importData.sh installs it as +# stuff/raw.bin; STAXI_RAW_DUMP=FILE names another); without it the game +# still builds, and only the levels and the per-level sprite banks made from +# them are left out. +STAXI_RAW_DUMP ?= $(STAXI_HOME)/stuff/raw.bin + +PROGRAMS += STAXI +STAXI_SRCS := $(addprefix $(STAXI_HOME)/,spacetaxi.c stLevel.c stLevelFile.c stRender.c stSim.c stFare.c stHooks.c stTitle.c stC64Data.c stCels.c stAudio.c stLevelCels.c) +# The IIgs links in an order of its own, the one its images were measured in. +STAXI_IIGS_SRCS := $(addprefix $(STAXI_HOME)/,stC64Data.c stCels.c stSim.c spacetaxi.c stLevel.c stLevelFile.c stRender.c stFare.c stHooks.c stTitle.c stAudio.c stLevelCels.c) +# In a folder of its own with its DATA, so its megabyte of data never rides +# onto every other program's disk. +STAXI_FOLDER := yes +STAXI_AMIGA_NAME := Taxi +STAXI_TITLE := Space Taxi +STAXI_HELP := Space Taxi: fly the taxi, pick up the fares, take them where they ask to go. +# IIgs: large overflow segments. 26 bank-aligned segments starved the Memory +# Manager, so NTP's 34K NewHandle failed and the IIgs build ran SILENT +# (gNTPReady stayed 0). Freeing that memory lets audio initialise; the +# latent crash that then surfaced is fixed in src/iigs/audio_full.c +# (sfxTrigger now requires a running replayer). +STAXI_IIGS_OVERFLOW_CAP := 0xF000 +STAXI_IIGS_GAME_DISK := yes +# X68000: stripped, because the image it ships on is a 1232 KB Human68k +# floppy and the unstripped binary is most of the volume on its own. +STAXI_X68000_LDFLAGS := -s + +# ---- DATA --------------------------------------------------------------------- +# Each bake is made in STAXI_GEN (per target; safe to delete, the next build +# recreates it), then what the game loads is copied into its DATA: the +# levels, the cel bank, the per-level cel banks and the speech. Tiles and the +# font come from the C64 data compiled into the binary (stC64Data.c). +STAXI_GEN := $(PROJECT_BUILD)/generated/$(ASSET_PLATFORM) +STAXI_RUN := $(call programDataDir,STAXI) +STAXI_OPT := $(call programOptionalDir,STAXI) + +# 24 lettered levels (A..X) from scenes 0..23 plus the title from scene 24, +# extracted by assets/romToLevel.py, which runs the captured decompressor +# through a Python 6502 emulator over the raw RAM dump and writes STL2 .dat +# files directly. (mkstlevel + title.txt is no longer used: the C64 title +# encodes its screen with the game's custom charset, so a hand-authored tile +# layout with a different glyph mapping renders as garbage.) +STAXI_LEVEL_NUMS := 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 +STAXI_LEVELS := $(foreach n,$(STAXI_LEVEL_NUMS),levels/level$(n).dat) levels/title.dat +ROMTOLEVEL := $(STAXI_HOME)/assets/romToLevel.py + +# Per-level hook-sprite banks: the cels stLevelCelList names for each level +# that has hook sprites, baked from its level file and pre-compiled for this +# target, so no cel is JIT-compiled mid-ride. Levels without hook sprites +# have no bank, and a missing bank only makes the game compile that level's +# cels at entry -- so they are optional data, which the IIgs data disk packs +# last, while there is room. +# ...except on the X68000, where they do not fit. The Human68k floppy is +# 1232 KB and these eight banks are 787 KB of it (level09 alone is 236 KB, +# level10 175, level17 165), which with the 296 KB binary, the levels and +# the speech put the disk about 300 KB over however small speech gets. +# stRenderLevelCels already handles their absence the right way: it builds +# and compiles the level's cels at ENTRY, behind the LOADING bar, so the +# ride itself still never compiles. That is also what spacetaxi.c's +# codegen-arena comment says the 68k ports are meant to do -- their cels +# are big and their JIT is fast. Ship them there again if the payload ever +# moves to a hard disk image. +STAXI_CEL_LEVELS := $(if $(filter x68000,$(PLATFORM)),,07 08 09 10 12 16 17 21) + +# 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 +# spritebake, so the game loads its cels already compiled +# (jlSpriteBankLoadPrecompiled) instead of JIT-compiling them at boot. The ST +# is the one port whose .spc carries a reloc list (the shifted-DRAW thunk's +# jmp to stSpriteDrawShiftWalker), re-stamped by the loader. +STAXIBAKE := $(PROJECT_BUILD)/tools/staxibake +STAXIBAKE_SRCS := $(STAXI_HOME)/tools/staxibake/staxibake.c $(addprefix $(STAXI_HOME)/,stCels.c stC64Data.c stLevelCels.c stLevelFile.c) + +# Speech baked to sample bytes per passenger pitch (assets/genSpeechPcm.py): +# baking them here instead of expanding bits at play time is what keeps "HEY +# TAXI" from stalling the ride; the C64 run-length source stays in +# stSpeechData.c as the input. +# --single one polarity, 60 KB instead of 120 KB; the player negates the +# samples itself when the running polarity is odd. Measured on a +# stock 1 MB ST the two-polarity file cannot be allocated at all +# ("speech alloc FAILED (120828 bytes)": the whole game mute), +# and the X68000's 1232 KB floppy came up 18 KB short of it. +# --biased unsigned samples, as the IIgs DOC plays them. +STAXI_SPEECH_FLAGS_atarist := --single +STAXI_SPEECH_FLAGS_x68000 := --single +STAXI_SPEECH_FLAGS_iigs := --biased + +STAXI_DATA := $(STAXI_RUN)/sprites/staxicels.spc $(STAXI_RUN)/speech.bin +ifneq ($(wildcard $(STAXI_RAW_DUMP)),) +STAXI_DATA += $(addprefix $(STAXI_RUN)/,$(STAXI_LEVELS)) $(foreach n,$(STAXI_CEL_LEVELS),$(STAXI_OPT)/sprites/level$(n).spc) +else +STAXI_DATA += staxi-no-levels +endif + +.PHONY: staxi-no-levels +staxi-no-levels: + @echo "note: Space Taxi levels skipped: $(STAXI_RAW_DUMP) is not present" + +# The bakes are kept: they are what the DATA copies come from. +.SECONDARY: $(addprefix $(STAXI_GEN)/,$(STAXI_LEVELS) sprites/staxicels.spr sprites/staxicels.spc speech.bin) \ + $(foreach n,$(STAXI_CEL_LEVELS),$(STAXI_GEN)/sprites/level$(n).spr $(STAXI_GEN)/sprites/level$(n).spc) + +# Every level and the title in one run of the extractor: a grouped target +# (&:, GNU Make 4.3+) runs its recipe once for the whole list. +$(addprefix $(STAXI_GEN)/,$(STAXI_LEVELS)) &: $(STAXI_RAW_DUMP) $(ROMTOLEVEL) + @mkdir -p $(STAXI_GEN)/levels + python3 $(ROMTOLEVEL) $(STAXI_RAW_DUMP) $(STAXI_GEN)/levels + +$(STAXI_GEN)/sprites/staxicels.spr: $(STAXIBAKE) $(STAXI_HOME)/stCels.c $(STAXI_HOME)/stC64Data.c + @mkdir -p $(dir $@) + $(STAXIBAKE) $@ + +$(STAXI_GEN)/sprites/level%.spr: $(STAXI_GEN)/levels/level%.dat $(STAXIBAKE) $(STAXI_HOME)/stLevelCels.c + @mkdir -p $(dir $@) + $(STAXIBAKE) --level $< $@ + +$(STAXI_GEN)/sprites/%.spc: $(STAXI_GEN)/sprites/%.spr $(SPRITEBAKE) + @mkdir -p $(dir $@) + $(SPRITEBAKE) $< $@ + +$(STAXI_GEN)/speech.bin: $(STAXI_HOME)/stSpeechData.c $(STAXI_HOME)/assets/genSpeechPcm.py + @mkdir -p $(dir $@) + python3 $(STAXI_HOME)/assets/genSpeechPcm.py $< $@ $(STAXI_SPEECH_FLAGS_$(ASSET_PLATFORM)) + +$(STAXI_RUN)/%: $(STAXI_GEN)/% + @mkdir -p $(dir $@) + cp $< $@ + +ifneq ($(STAXI_OPT),$(STAXI_RUN)) +$(STAXI_OPT)/%: $(STAXI_GEN)/% + @mkdir -p $(dir $@) + cp $< $@ +endif + +# A plain host tool: the .spr it writes is the same for every target. +$(STAXIBAKE): $(STAXIBAKE_SRCS) + @mkdir -p $(dir $@) + $(HOST_CC) $(HOST_CFLAGS) -I$(STAXI_HOME) $^ -o $@ diff --git a/scripts/bench-staxi-title.sh b/scripts/bench-staxi-title.sh index d1ba47f..006eda0 100755 --- a/scripts/bench-staxi-title.sh +++ b/scripts/bench-staxi-title.sh @@ -4,10 +4,9 @@ # jlTilePasteMono calls, intended every 4th frame -- stRender.c # titleAnimateLogo's `& 3u` divider mirroring the C64 original). # -# Boots GS/OS under headless MAME with a WORK COPY of joey.2mg (must -# contain STAXI + its DATA tree; build with -# JOEY_DISK_EXAMPLES="STAXI" scripts/make-iigs-disk.sh), -# launches STAXI via the standard Finder keystroke timeline, then +# Boots GS/OS under headless MAME with a WORK COPY of the game's data +# disk, build/iigs/bin/STAXI-data.2mg (STAXI + its DATA tree; make iigs +# builds it), launches STAXI via the standard Finder keystroke timeline, then # samples 8 known logo tile cells from the SHR framebuffer EVERY frame. # Each frame the sampled 256 bytes fold into a rolling hash; a hash # change means the logo visibly changed on screen (the flip-book tile @@ -22,10 +21,12 @@ # MAME_ROMPATH override ROM dir (default ~/.mame/roms) # STAXI_MAX_FRAMES frame cap (default 15000) # STAXI_MAX_CHANGES stop after this many logo changes (default 80) +# JOEYLIB the JoeyLib checkout (default ../joeylib) set -euo pipefail repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) -source "$repo/scripts/lib/joeyEnv.sh" +JOEYLIB=${JOEYLIB:-$repo/../joeylib} +source "$JOEYLIB/scripts/lib/joeyEnv.sh" joeyNeed LLVM816_ROOT toolchain:iigs joeyNeed CADIUS cadius joeyNeed MAME mame @@ -33,7 +34,7 @@ joeyNeed IIGS_ROMPATH iigsFirmware joeyNeed GSOS_SYSTEM_DISK iigsFirmware sys_disk=$GSOS_SYSTEM_DISK -data_disk=$repo/build/iigs/bin/joey.2mg +data_disk=$repo/build/iigs/bin/STAXI-data.2mg rompath="$IIGS_ROMPATH" maxFrames="${STAXI_MAX_FRAMES:-15000}" maxChanges="${STAXI_MAX_CHANGES:-80}" @@ -43,7 +44,7 @@ for f in "$sys_disk" "$data_disk"; do [ -f "$f" ] || { echo "bench-staxi-title: missing $f" >&2; exit 2; } done if ! "$CADIUS" CATALOG "$data_disk" 2>/dev/null | grep -qE '^ STAXI +S16'; then - echo "bench-staxi-title: STAXI not on joey.2mg (JOEY_DISK_EXAMPLES=\"STAXI\" scripts/make-iigs-disk.sh)" >&2 + echo "bench-staxi-title: STAXI not on $data_disk (make iigs)" >&2 exit 2 fi diff --git a/scripts/diag-staxi-screen.sh b/scripts/diag-staxi-screen.sh index 2d2f16b..b52e9ef 100755 --- a/scripts/diag-staxi-screen.sh +++ b/scripts/diag-staxi-screen.sh @@ -2,12 +2,15 @@ # diag-staxi-screen.sh - one-off diagnostic for the STAXI title test: # boot + launch STAXI, dump the full SHR framebuffer + palettes as hex # at a target frame, then extract JOEYLOG.TXT from the work disk so the -# game's own init/asset-load log is visible. Outputs land in -# build/iigs/bin/ (staxi-shr.hex, staxi-joeylog.txt). +# game's own init/asset-load log is visible. Boots a work copy of the +# game's data disk, build/iigs/bin/STAXI-data.2mg (make iigs). Outputs land +# in build/iigs/bin/ (staxi-shr.hex, staxi-joeylog.txt). JOEYLIB names the +# JoeyLib checkout (default ../joeylib). set -euo pipefail repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) -source "$repo/scripts/lib/joeyEnv.sh" +JOEYLIB=${JOEYLIB:-$repo/../joeylib} +source "$JOEYLIB/scripts/lib/joeyEnv.sh" joeyNeed LLVM816_ROOT toolchain:iigs joeyNeed CADIUS cadius joeyNeed MAME mame @@ -15,7 +18,7 @@ joeyNeed IIGS_ROMPATH iigsFirmware joeyNeed GSOS_SYSTEM_DISK iigsFirmware sys_disk=$GSOS_SYSTEM_DISK -data_disk=$repo/build/iigs/bin/joey.2mg +data_disk=$repo/build/iigs/bin/STAXI-data.2mg rompath="$IIGS_ROMPATH" dumpFrame="${STAXI_DUMP_FRAME:-9000}" diff --git a/scripts/importData.sh b/scripts/importData.sh new file mode 100755 index 0000000..6fbcb76 --- /dev/null +++ b/scripts/importData.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash +# importData.sh - install the one file Space Taxi needs that is not in the +# repository: a memory image of the original Commodore 64 game, which the +# build extracts the 24 levels and the title screen from (program.mk). It is +# never downloaded from anywhere: you supply your copy, and it is recognised +# by its checksum, so its file name does not matter. +# +# Usage: scripts/importData.sh PATH... +# PATH the image itself, a folder holding it, or a .zip or +# .tar.gz archive with it inside (such as the owner's +# private bundle) +# Installs it as stuff/raw.bin. stuff/ is never committed: the image is a +# copy of a commercial game. +set -euo pipefail + +repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +dest=$repo/stuff/raw.bin +# sha1 of the C64 memory image the levels are extracted from. +wantSha1=5bfa4ba70431135575dab610a73dde02b8095953 +work="" +# The PATH being scanned, for the messages. +origin="" + + +# cleanup -- remove the scratch folder archives are unpacked into. +cleanup() { + if [ -n "$work" ]; then + rm -rf "$work" + fi +} + + +# isImage FILE -- true when FILE is the image. +isImage() { + [ "$(sha1sum < "$1" | cut -d' ' -f1)" = "$wantSha1" ] +} + + +# scanFile FILE -- install FILE if it is the image, or the image from inside +# FILE when it is an archive. +scanFile() { + local unpacked + if isImage "$1"; then + install -D -m 644 "$1" "$dest" + echo "importData: installed $dest from $origin" + exit 0 + fi + case "$1" in + *.zip|*.ZIP|*.tar.gz|*.tgz) + unpacked=$(mktemp -d -p "$work") + case "$1" in + *.zip|*.ZIP) unzip -qq -o "$1" -d "$unpacked" 2>/dev/null || true ;; + *) tar -xzf "$1" -C "$unpacked" 2>/dev/null || true ;; + esac + scanTree "$unpacked" + ;; + esac +} + + +# scanTree DIR -- install the image if any file under DIR is it. +scanTree() { + local file + while IFS= read -r -d '' file; do + scanFile "$file" + done < <(find "$1" -type f -print0) +} + + +if [ $# -eq 0 ]; then + sed -n '2,13p' "$0" | sed 's/^# \{0,1\}//' + exit 2 +fi +if [ -f "$dest" ] && isImage "$dest"; then + echo "importData: $dest is already in place" + exit 0 +fi +work=$(mktemp -d) +trap cleanup EXIT +for path in "$@"; do + origin=$path + if [ -d "$path" ]; then + scanTree "$path" + elif [ -f "$path" ]; then + scanFile "$path" + else + echo "importData: no such file or folder: $path" >&2 + exit 2 + fi +done +echo "importData: none of $* is the Space Taxi image (sha1 $wantSha1)" >&2 +exit 1 diff --git a/scripts/verify.sh b/scripts/verify.sh new file mode 100755 index 0000000..765ffa5 --- /dev/null +++ b/scripts/verify.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +# verify.sh - Space Taxi's gates, run one after another, with a summary. +# +# stsim every hook level's 4000-tick simulation trace is identical to +# its golden (tools/stsim/gate.sh), and every cel a ride shows +# is baked (tools/stsim/celGate.sh) +# harness the front-end screens above the simulation -- the high-score +# table, name entry and its save -- on JoeyLib's host BLANK +# port (tools/staxiharness) +# disk the bootable IIgs volume boots on a ROM 3 machine, launches +# the game and draws (JoeyLib's verify-iigs-game-disk.sh) +# disk01 the same on ROM 01: the volume keeps the ROM 01 tool patches +# iigsaudio the IIgs sound interrupt is NTP's, the DOC's master volume is +# up and the sound slots carry waveforms (JoeyLib's +# verify-iigs-audio.sh) +# x68kaudio every YM2151 noise write is a code from the port's table and +# the ADPCM is fed at 7812 Hz to both speakers (JoeyLib's +# verify-x68000-audio.sh); the attract demo plays the whole +# noise curve and speaks +# +# Usage: scripts/verify.sh [gate...] every gate when none is named +# Env: JOEYLIB the JoeyLib checkout (default ../joeylib) +# The gates check what is built: run make first (make check does both). +# +# Exit codes: +# 0 every gate passed +# 1 a gate failed +# 3 none failed, but one was skipped for a missing prerequisite (an +# emulator, a target not built, the game data not imported) -- not 0, +# so a run that tested nothing cannot look green +# A gate exits 2 to say it was skipped. +set -uo pipefail + +repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +JOEYLIB=$(cd "${JOEYLIB:-$repo/../joeylib}" && pwd) || { echo "verify: no JoeyLib checkout (set JOEYLIB)" >&2; exit 2; } +export JOEYLIB +bin=$repo/build +harnessLevels=$bin/generated/dos/levels/title.dat + +# name command...: every gate, in run order. +GATES=$(cat <&2; exit 2; }; make -s -C tools/staxiharness JOEYLIB=$JOEYLIB || exit 1 +disk $JOEYLIB/scripts/verify-iigs-game-disk.sh $bin/iigs/bin/STAXI.2mg +disk01 GAME_VERIFY_MACHINE=apple2gsr1 $JOEYLIB/scripts/verify-iigs-game-disk.sh $bin/iigs/bin/STAXI.2mg +iigsaudio $JOEYLIB/scripts/verify-iigs-audio.sh $bin/iigs/bin/STAXI +x68kaudio $JOEYLIB/scripts/verify-x68000-audio.sh $bin/x68000/bin/STAXI/STAXI.X +EOF +) + +names=$(echo "$GATES" | awk '{ print $1 }' | tr '\n' ' ') +for want in "$@"; do + case " $names " in + *" $want "*) ;; + *) echo "verify: unknown gate '$want' (choose from: $names)" >&2; exit 2 ;; + esac +done +selected=${*:-$names} + +gateNames=() +gateVerdicts=() +gateSeconds=() +passed=0 +failed=0 +skipped=0 + +while read -r name cmd; do + case " $selected " in *" $name "*) ;; *) continue ;; esac + echo + echo "=== verify: $name ($cmd) ===" + start=$SECONDS + ( cd "$repo" && bash -c "$cmd" ) < /dev/null + case $? in + 0) verdict=PASS; passed=$((passed + 1)) ;; + 2) verdict="SKIP (prerequisite missing)"; skipped=$((skipped + 1)) ;; + *) verdict=FAIL; failed=$((failed + 1)) ;; + esac + gateNames+=("$name") + gateVerdicts+=("$verdict") + gateSeconds+=("$((SECONDS - start))") +done <<< "$GATES" + +echo +echo "================ verify summary ================" +for i in "${!gateNames[@]}"; do + printf " %-10s %-28s %4ds\n" "${gateNames[$i]}" "${gateVerdicts[$i]}" "${gateSeconds[$i]}" +done +echo " ----" +printf " %d passed, %d failed, %d skipped\n" "$passed" "$failed" "$skipped" + +if [ "$failed" -gt 0 ]; then + echo "verify: FAIL" >&2 + exit 1 +fi +if [ "$skipped" -gt 0 ]; then + echo "verify: INCOMPLETE - some gates skipped (see above)" >&2 + exit 3 +fi +echo "verify: PASS (all $passed gates)" +exit 0 diff --git a/stC64Data.c b/stC64Data.c index b8fa792..c90dc0e 100644 --- a/stC64Data.c +++ b/stC64Data.c @@ -1,4 +1,4 @@ -// Generated by assets/genC64Data.py from stuff/spacetaxi/raw.bin. +// Generated by assets/genC64Data.py from stuff/raw.bin. // Do not hand-edit; re-run the generator. #include "stC64Data.h" diff --git a/stC64Data.h b/stC64Data.h index c30c083..c839ff5 100644 --- a/stC64Data.h +++ b/stC64Data.h @@ -1,4 +1,4 @@ -// Generated by assets/genC64Data.py from stuff/spacetaxi/raw.bin. +// Generated by assets/genC64Data.py from stuff/raw.bin. // Do not hand-edit; re-run the generator. #ifndef ST_C64_DATA_H diff --git a/stLevelFile.c b/stLevelFile.c index 5ca896e..65f2755 100644 --- a/stLevelFile.c +++ b/stLevelFile.c @@ -1,7 +1,7 @@ // Space Taxi -- STL4 level file parser (plain stdio, no JoeyLib), shared // by the game and the host-side simulation harness. // -// Layout (all bytes, written by examples/spacetaxi/assets/romToLevel.py): +// Layout (all bytes, written by assets/romToLevel.py): // "STL4" // name[22] $7D78 level name in screen codes // header[9] $7D00 VIC colour block diff --git a/stSim.h b/stSim.h index 5991fe3..2c2d1b9 100644 --- a/stSim.h +++ b/stSim.h @@ -14,7 +14,7 @@ // declared at the bottom, which each host implements. // // Address comments ($xxxx) name the original variable or routine a -// field or function mirrors; see stuff/spacetaxi/labels.txt. +// field or function mirrors; see stuff/labels.txt. #ifndef ST_SIM_H #define ST_SIM_H diff --git a/tools/staxiharness/Makefile b/tools/staxiharness/Makefile index 09c44a7..1dad8ed 100644 --- a/tools/staxiharness/Makefile +++ b/tools/staxiharness/Makefile @@ -1,35 +1,42 @@ # Host-side checks of Space Taxi's front-end screens (the parts tools/stsim # cannot reach: they live in spacetaxi.c, above the simulation). Builds the -# game on the BLANK port with spacetaxi.c INCLUDED into the harness so its -# statics are reachable, and wraps the keyboard queue and the save file +# game on JoeyLib's BLANK port with spacetaxi.c INCLUDED into the harness so +# its statics are reachable, and wraps the keyboard queue and the save file # with scripted stand-ins. # # make -C tools/staxiharness build + run +# +# JOEYLIB is the JoeyLib checkout, as an absolute path (default: joeylib +# beside this repository). The title screen comes from the game's DOS bake, +# so build the game for DOS first. -REPO := $(abspath $(dir $(lastword $(MAKEFILE_LIST)))/../..) -ST := $(REPO)/examples/spacetaxi -OUT := $(REPO)/build/tools/staxiharness -SRCS := $(filter-out $(ST)/spacetaxi.c,$(wildcard $(ST)/*.c)) \ - $(wildcard $(REPO)/src/core/*.c) $(wildcard $(REPO)/src/generic/*.c) \ - $(REPO)/src/blank/blank.c -CFLAGS := -O0 -g -w -DJOEYLIB_PLATFORM_BLANK -I$(REPO)/include -I$(REPO)/src/core \ - -I$(REPO)/src/codegen -I$(ST) -WRAPS := -Wl,--wrap=jlInputGetChar -Wl,--wrap=jlSaveWrite -Wl,--wrap=jlSaveRead +REPO := $(abspath $(dir $(lastword $(MAKEFILE_LIST)))/../..) +JOEYLIB ?= $(REPO)/../joeylib +JL := $(abspath $(JOEYLIB)) +GEN := $(REPO)/build/generated/dos +OUT := $(REPO)/build/tools/staxiharness +SRCS := $(filter-out $(REPO)/spacetaxi.c,$(wildcard $(REPO)/*.c)) \ + $(wildcard $(JL)/src/core/*.c) $(wildcard $(JL)/src/generic/*.c) \ + $(JL)/src/blank/blank.c +CFLAGS := -O0 -g -w -DJOEYLIB_PLATFORM_BLANK -I$(JL)/include -I$(JL)/src/core \ + -I$(JL)/src/codegen -I$(REPO) +WRAPS := -Wl,--wrap=jlInputGetChar -Wl,--wrap=jlSaveWrite -Wl,--wrap=jlSaveRead .PHONY: all run clean all: run -$(OUT)/harness: harness.c $(ST)/spacetaxi.c $(SRCS) +$(OUT)/harness: harness.c $(REPO)/spacetaxi.c $(SRCS) @mkdir -p $(OUT) $(CC) $(CFLAGS) harness.c $(SRCS) $(WRAPS) -o $@ # The title screen's level file is read through DATA/levels (the DOS bake # is the cross-platform STL4 form), so run from a directory that has one. run: $(OUT)/harness + @test -f $(GEN)/levels/title.dat || { echo "staxiharness: no $(GEN)/levels/title.dat (build the game for DOS first)" >&2; exit 2; } @mkdir -p $(OUT)/DATA - @ln -sfn $(ST)/generated/dos/levels $(OUT)/DATA/levels - @ln -sfn $(ST)/generated/dos/speech.bin $(OUT)/DATA/speech.bin + @ln -sfn $(GEN)/levels $(OUT)/DATA/levels + @ln -sfn $(GEN)/speech.bin $(OUT)/DATA/speech.bin cd $(OUT) && ./harness clean: diff --git a/tools/stsim/Makefile b/tools/stsim/Makefile index 0101e3f..3766278 100644 --- a/tools/stsim/Makefile +++ b/tools/stsim/Makefile @@ -1,5 +1,5 @@ # Host build of the Space Taxi simulation core for trace comparison. -ST := ../../examples/spacetaxi +ST := ../.. CC ?= gcc CFLAGS := -O1 -g -Wall -Wextra -std=c99 -I$(ST) SRCS := stsim.c $(ST)/stSim.c $(ST)/stFare.c $(ST)/stHooks.c $(ST)/stLevelFile.c $(ST)/stTitle.c $(ST)/stC64Data.c $(ST)/stLevelCels.c diff --git a/tools/stsim/celGate.sh b/tools/stsim/celGate.sh index 68f5e72..fe9f429 100755 --- a/tools/stsim/celGate.sh +++ b/tools/stsim/celGate.sh @@ -9,12 +9,7 @@ set -u here=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) -repo=$(cd "$here/../.." && pwd) -levels=$repo/examples/spacetaxi/generated/iigs/levels -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" - -make -C "$here" >/dev/null || { echo "celGate: build failed" >&2; exit 2; } +source "$here/gateLib.sh" fail=0 for p in $pairs; do diff --git a/tools/stsim/gate.sh b/tools/stsim/gate.sh index 8320eb1..d1dd099 100755 --- a/tools/stsim/gate.sh +++ b/tools/stsim/gate.sh @@ -10,23 +10,19 @@ # 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] compare the current tree against them +# gate.sh check [dir] compare the current tree against them, or, with +# none captured, against gateHashes.txt (the md5 +# of each golden trace, the committed reference) # gate.sh hashes print one md5 per level (compact reference) # -# Capture from a tree you trust, refactor, then check. +# Capture from a tree you trust, refactor, then check. The rides run on the +# game's level bake, so build the game first (gateLib.sh finds it). set -u here=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) -repo=$(cd "$here/../.." && pwd) -levels=$repo/examples/spacetaxi/generated/iigs/levels mode=${1:-check} dir=${2:-$here/golden} -ticks=4000 - -# level letter -> level file number -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" - -make -C "$here" >/dev/null || { echo "gate: build failed" >&2; exit 2; } +source "$here/gateLib.sh" case "$mode" in capture) @@ -44,14 +40,23 @@ check) l=${p%%:*}; n=${p##*:} got=$(mktemp) "$here/stsim" "$levels/level$n.dat" "$l" "$ticks" > "$got" 2>/dev/null - if [ ! -f "$dir/demo$l.jsonl" ]; then - echo " $l: no golden (run: gate.sh capture)" - elif cmp -s "$dir/demo$l.jsonl" "$got"; then - printf ' %s: IDENTICAL (%s ticks)\n' "$l" "$(wc -l < "$got")" + if [ -f "$dir/demo$l.jsonl" ]; then + if cmp -s "$dir/demo$l.jsonl" "$got"; then + printf ' %s: IDENTICAL (%s ticks)\n' "$l" "$(wc -l < "$got")" + else + echo " $l: *** DIFFERS ***" + diff "$dir/demo$l.jsonl" "$got" | head -3 + fail=1 + fi else - echo " $l: *** DIFFERS ***" - diff "$dir/demo$l.jsonl" "$got" | head -3 - fail=1 + want=$(awk -v l="$l" '$1 == l { print $2 }' "$here/gateHashes.txt") + have=$(md5sum < "$got" | cut -d' ' -f1) + if [ "$have" = "$want" ]; then + printf ' %s: IDENTICAL (%s ticks, md5 as gateHashes.txt)\n' "$l" "$(wc -l < "$got")" + else + echo " $l: *** DIFFERS *** (md5 $have, gateHashes.txt ${want:-has none}; capture goldens from a trusted tree to see where)" + fail=1 + fi fi rm -f "$got" done diff --git a/tools/stsim/gateLib.sh b/tools/stsim/gateLib.sh new file mode 100644 index 0000000..c706b49 --- /dev/null +++ b/tools/stsim/gateLib.sh @@ -0,0 +1,25 @@ +# Sourced by gate.sh and celGate.sh, which set $here first: the rides both +# gates replay, and the levels they replay them on. + +# Every hook level as letter:level file number, and how long each ride runs. +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" + +# The game's own level bake (program.mk). It is the same STL4 bytes for every +# target, so whichever target was built first will do; STSIM_LEVELS names +# another folder. +levels=${STSIM_LEVELS:-} +if [ -z "$levels" ]; then + for port in iigs dos amiga atarist x68000; do + if [ -f "$here/../../build/generated/$port/levels/level01.dat" ]; then + levels=$(cd "$here/../../build/generated/$port/levels" && pwd) + break + fi + done +fi +if [ -z "$levels" ]; then + echo "$(basename "$0"): no level bake in build/generated//levels (scripts/importData.sh, then make)" >&2 + exit 2 +fi + +make -C "$here" >/dev/null || { echo "$(basename "$0"): building stsim failed" >&2; exit 2; } diff --git a/tools/stsim/gateStreams.h b/tools/stsim/gateStreams.h index 8d179b5..c6d3161 100644 --- a/tools/stsim/gateStreams.h +++ b/tools/stsim/gateStreams.h @@ -1,7 +1,7 @@ // Synthesised demo input streams -- a TEST FIXTURE, not game content. // // The C64 ships attract recordings for four screens only (H, W, T and X); -// those live in examples/spacetaxi/stDemoStreams.h and are the streams +// those live in stDemoStreams.h and are the streams // checked against VICE traces. The thirteen streams below were searched for // by tools/stsim's recorder against this port's own simulation, scored on // how long the cab survives and how much of the level's hook state, fare diff --git a/tools/stsim/stsim.c b/tools/stsim/stsim.c index 7791b46..f69bae1 100644 --- a/tools/stsim/stsim.c +++ b/tools/stsim/stsim.c @@ -2,7 +2,7 @@ // // Loads an STL4 level, attaches a recorded demo input stream and runs // the game tick, printing one line of state per tick in the same shape -// the VICE tracer records (see stuff/spacetaxi and the compare script), +// the VICE tracer records (see stuff/ and the compare script), // so the port's engine can be diffed against the real machine. // // stsim @@ -14,9 +14,9 @@ #include #include -#include "../../examples/spacetaxi/stSim.h" -#include "../../examples/spacetaxi/stCels.h" -#include "../../examples/spacetaxi/stDemoStreams.h" +#include "stSim.h" +#include "stCels.h" +#include "stDemoStreams.h" #include "gateStreams.h"