Build standalone against JoeyLib.

This commit is contained in:
Scott Duensing 2026-10-05 18:50:09 -05:00
parent a05958bd86
commit 786ff257c3
34 changed files with 1277 additions and 123 deletions

1
.gitattributes vendored Normal file
View file

@ -0,0 +1 @@
*.png filter=lfs diff=lfs merge=lfs -text

22
.gitignore vendored Normal file
View file

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

View file

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

26
Makefile Normal file
View file

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

View file

@ -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
<dump> <out> <start> <end>`, 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)

123
README.md Normal file
View file

@ -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/<machine>/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/<machine>/`. 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.

21
TODO.md Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<N>.png`.
`assets/tiles/tbank<N>.png`.
2. The Makefile bakes per-target via `tools/assetbake/assetbake.py
--type tile --target <port> tbank<N>.png tbank<N>.tbk`. Output
lands in `examples/spacetaxi/generated/<port>/tiles/` and is
lands in `generated/<port>/tiles/` and is
staged into the runtime tree at `build/<port>/.../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

View file

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

View file

@ -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() {
:
}

View file

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

617
patches/spriteWarp.patch Normal file
View file

@ -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 <string.h>
@@ -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);

145
program.mk Normal file
View file

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

View file

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

View file

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

92
scripts/importData.sh Executable file
View file

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

100
scripts/verify.sh Executable file
View file

@ -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 <<EOF
stsim tools/stsim/gate.sh check
harness [ -f $harnessLevels ] || { echo "harness: the DOS bake is not built (make dos)" >&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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

25
tools/stsim/gateLib.sh Normal file
View file

@ -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/<port>/levels (scripts/importData.sh, then make)" >&2
exit 2
fi
make -C "$here" >/dev/null || { echo "$(basename "$0"): building stsim failed" >&2; exit 2; }

View file

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

View file

@ -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 <level.dat> <level letter> <ticks>
@ -14,9 +14,9 @@
#include <stdlib.h>
#include <string.h>
#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"