Compare commits

...

10 commits

Author SHA1 Message Date
Scott Duensing
786ff257c3 Build standalone against JoeyLib. 2026-10-05 18:50:09 -05:00
Scott Duensing
a05958bd86 More bug fixing and optimizations. 2026-10-05 16:47:00 -05:00
Scott Duensing
efa9fecad9 Start of DVX target! 2026-09-29 19:31:34 -05:00
Scott Duensing
55be676c0c Tile generation tests. 2026-09-29 17:23:29 -05:00
Scott Duensing
dcddbbfe97 Lots of work, but mostly serial related. 2026-09-28 21:03:46 -05:00
Scott Duensing
4c11efe83f Major changes to setup. Now installable by mere mortals on Debian, Fedora, and WSL. 2026-09-28 15:27:29 -05:00
Scott Duensing
f6c4ad3747 X68k, ST, and Amiga optimizations. 2026-09-24 22:01:56 -05:00
Scott Duensing
5adf55e80f Space Taxi is finally starting to look viable on the GS! 2026-09-22 18:12:37 -05:00
Scott Duensing
46baf7ad5f Space Taxi work. 2026-09-17 17:46:47 -05:00
b08c5bfae0 Repo cleanup 2026-09-07 15:46:23 -05:00
220 changed files with 14181 additions and 1976 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

BIN
21_!.wav Normal file

Binary file not shown.

BIN
31_1.wav Normal file

Binary file not shown.

BIN
32_2.wav Normal file

Binary file not shown.

BIN
33_3.wav Normal file

Binary file not shown.

BIN
34_4.wav Normal file

Binary file not shown.

BIN
35_5.wav Normal file

Binary file not shown.

BIN
36_6.wav Normal file

Binary file not shown.

BIN
37_7.wav Normal file

Binary file not shown.

BIN
38_8.wav Normal file

Binary file not shown.

BIN
39_9.wav Normal file

Binary file not shown.

BIN
3F_x.wav Normal file

Binary file not shown.

BIN
48_H.wav Normal file

Binary file not shown.

BIN
50_P.wav Normal file

Binary file not shown.

BIN
54_T.wav Normal file

Binary file not shown.

BIN
55_U.wav Normal file

Binary file not shown.

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
@ -25,12 +25,12 @@ Same loop every time, so it stays cheap to iterate:
`stLevel.c`, `spacetaxi.c`).
6. **Build + ship** -- `make -f make/dos.mk EXAMPLE=spacetaxi`
first, then once stable also amiga / atarist / iigs.
7. **User test** -- run `scripts/run-dos.sh staxi`, report
7. **User test** -- run `./joey-run --port dos staxi`, report
behavior. Do not start the next phase before this.
## 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.
- `stuff/spacetaxi/cpu6502.py`: minimal 6502 emulator. Sufficient for all routines tested above; throws `NotImplementedError` for unsupported opcodes (none hit so far in gameplay paths).
- `stuff/spacetaxi/dumpScenes.py` + `dumpAllLevels.py`: pre-existing scene-decompress drivers.
- `stuff/spacetaxi/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)

BIN
assets/RawTitle.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/TitleShr.png (Stored with Git LFS) Normal file

Binary file not shown.

560
assets/cpu6502.py Normal file
View file

@ -0,0 +1,560 @@
#!/usr/bin/env python3
# Minimal 6502 CPU emulator -- just enough to run the Space Taxi
# level decompressor at $6283 against a captured 64KB RAM dump.
#
# Scope: implements the opcodes used by $6283 / $9656 / $964A / $96A3
# and their helpers. Not a general-purpose 6502 emulator; it has no
# decimal mode, no BRK handling, no NMI/IRQ -- the routine runs with
# SEI in effect and never returns control.
#
# Flags tracked: N Z C. V (overflow) is computed for ADC/SBC since
# those instructions set it, even if no branch tests it. D (decimal)
# is ignored.
class Cpu6502:
def __init__(self, ram):
# ram is a bytearray of 65536 bytes. Caller pre-loaded the dump.
self.ram = ram
self.a = 0
self.x = 0
self.y = 0
self.sp = 0xFF
self.pc = 0
self.n = self.z = self.c = self.v = False
# Stop tracking when RTS unwinds to this depth (set by run()).
self._depth_floor = 0
# ---- memory ----
def read8(self, addr):
return self.ram[addr & 0xFFFF]
def write8(self, addr, val):
self.ram[addr & 0xFFFF] = val & 0xFF
def read16(self, addr):
return self.ram[addr & 0xFFFF] | (self.ram[(addr + 1) & 0xFFFF] << 8)
# ---- stack ----
def push8(self, val):
self.ram[0x100 + self.sp] = val & 0xFF
self.sp = (self.sp - 1) & 0xFF
def pop8(self):
self.sp = (self.sp + 1) & 0xFF
return self.ram[0x100 + self.sp]
def push16(self, val):
self.push8((val >> 8) & 0xFF)
self.push8(val & 0xFF)
def pop16(self):
lo = self.pop8()
hi = self.pop8()
return (hi << 8) | lo
# ---- flag helpers ----
def set_nz(self, val):
val &= 0xFF
self.z = (val == 0)
self.n = (val & 0x80) != 0
return val
def pack_p(self):
# B and U are usually set when pushed by PHP/BRK.
p = 0x20
if self.n: p |= 0x80
if self.v: p |= 0x40
if self.z: p |= 0x02
if self.c: p |= 0x01
return p | 0x10 # B = 1 when pushed by PHP
def unpack_p(self, p):
self.n = (p & 0x80) != 0
self.v = (p & 0x40) != 0
self.z = (p & 0x02) != 0
self.c = (p & 0x01) != 0
# ---- addressing helpers (return effective address) ----
def addr_imm(self):
a = self.pc; self.pc = (self.pc + 1) & 0xFFFF; return a
def addr_zp(self):
a = self.read8(self.pc); self.pc = (self.pc + 1) & 0xFFFF; return a
def addr_zpx(self):
a = (self.read8(self.pc) + self.x) & 0xFF; self.pc = (self.pc + 1) & 0xFFFF; return a
def addr_abs(self):
a = self.read16(self.pc); self.pc = (self.pc + 2) & 0xFFFF; return a
def addr_absx(self):
a = (self.read16(self.pc) + self.x) & 0xFFFF; self.pc = (self.pc + 2) & 0xFFFF; return a
def addr_absy(self):
a = (self.read16(self.pc) + self.y) & 0xFFFF; self.pc = (self.pc + 2) & 0xFFFF; return a
def addr_indy(self):
zp = self.read8(self.pc); self.pc = (self.pc + 1) & 0xFFFF
base = self.ram[zp] | (self.ram[(zp + 1) & 0xFF] << 8)
return (base + self.y) & 0xFFFF
# ---- branch helper ----
def branch(self, cond):
off = self.read8(self.pc); self.pc = (self.pc + 1) & 0xFFFF
if off >= 0x80: off -= 0x100
if cond:
self.pc = (self.pc + off) & 0xFFFF
# ---- step ----
def step(self):
op = self.read8(self.pc)
self.pc = (self.pc + 1) & 0xFFFF
f = _DISPATCH.get(op)
if f is None:
raise NotImplementedError(f"opcode ${op:02X} at PC ${(self.pc-1)&0xFFFF:04X}")
f(self)
def call(self, target, max_steps=10_000_000):
"""Call subroutine at `target` as if via JSR. Returns when the
matching RTS pops the stack back below the original SP. Hard
cap on steps to avoid runaway."""
# Push a sentinel return address. When RTS pops to this, stop.
# Use $FFFF as sentinel: subsequent step would attempt to fetch
# from $FFFF; we detect via depth floor instead.
original_sp = self.sp
self.push16(0xFFFE) # RTS will pop this, then step will land at $FFFF
self.pc = target & 0xFFFF
for _ in range(max_steps):
if self.pc == 0xFFFF:
return
self.step()
raise RuntimeError(f"6502 call exceeded {max_steps} steps; PC=${self.pc:04X}")
# ---- opcode implementations ----
# Each takes the cpu and reads remaining operand bytes via cpu.addr_*.
def _NOP(cpu): pass
def _SEI(cpu): pass # we don't model interrupts
def _CLD(cpu): pass # no decimal mode
def _SEC(cpu): cpu.c = True
def _CLC(cpu): cpu.c = False
def _PHP(cpu): cpu.push8(cpu.pack_p())
def _PLP(cpu): cpu.unpack_p(cpu.pop8())
def _PHA(cpu): cpu.push8(cpu.a)
def _PLA(cpu): cpu.a = cpu.set_nz(cpu.pop8())
def _TXA(cpu): cpu.a = cpu.set_nz(cpu.x)
def _TAX(cpu): cpu.x = cpu.set_nz(cpu.a)
def _TYA(cpu): cpu.a = cpu.set_nz(cpu.y)
def _TAY(cpu): cpu.y = cpu.set_nz(cpu.a)
def _INX(cpu): cpu.x = cpu.set_nz((cpu.x + 1) & 0xFF)
def _INY(cpu): cpu.y = cpu.set_nz((cpu.y + 1) & 0xFF)
def _DEX(cpu): cpu.x = cpu.set_nz((cpu.x - 1) & 0xFF)
def _DEY(cpu): cpu.y = cpu.set_nz((cpu.y - 1) & 0xFF)
def _LDA_imm(cpu): cpu.a = cpu.set_nz(cpu.read8(cpu.addr_imm()))
def _LDA_zp(cpu): cpu.a = cpu.set_nz(cpu.read8(cpu.addr_zp()))
def _LDA_zpx(cpu): cpu.a = cpu.set_nz(cpu.read8(cpu.addr_zpx()))
def _LDA_abs(cpu): cpu.a = cpu.set_nz(cpu.read8(cpu.addr_abs()))
def _LDA_absx(cpu): cpu.a = cpu.set_nz(cpu.read8(cpu.addr_absx()))
def _LDA_absy(cpu): cpu.a = cpu.set_nz(cpu.read8(cpu.addr_absy()))
def _LDA_indy(cpu): cpu.a = cpu.set_nz(cpu.read8(cpu.addr_indy()))
def _LDX_imm(cpu): cpu.x = cpu.set_nz(cpu.read8(cpu.addr_imm()))
def _LDX_abs(cpu): cpu.x = cpu.set_nz(cpu.read8(cpu.addr_abs()))
def _LDX_absy(cpu): cpu.x = cpu.set_nz(cpu.read8(cpu.addr_absy()))
def _LDX_zp(cpu): cpu.x = cpu.set_nz(cpu.read8(cpu.addr_zp()))
def _LDY_imm(cpu): cpu.y = cpu.set_nz(cpu.read8(cpu.addr_imm()))
def _LDY_abs(cpu): cpu.y = cpu.set_nz(cpu.read8(cpu.addr_abs()))
def _LDY_absx(cpu): cpu.y = cpu.set_nz(cpu.read8(cpu.addr_absx()))
def _LDY_zp(cpu): cpu.y = cpu.set_nz(cpu.read8(cpu.addr_zp()))
def _STA_zp(cpu): cpu.write8(cpu.addr_zp(), cpu.a)
def _STA_zpx(cpu): cpu.write8(cpu.addr_zpx(), cpu.a)
def _STA_abs(cpu): cpu.write8(cpu.addr_abs(), cpu.a)
def _STA_absx(cpu): cpu.write8(cpu.addr_absx(), cpu.a)
def _STA_absy(cpu): cpu.write8(cpu.addr_absy(), cpu.a)
def _STA_indy(cpu): cpu.write8(cpu.addr_indy(), cpu.a)
def _STX_zp(cpu): cpu.write8(cpu.addr_zp(), cpu.x)
def _STX_abs(cpu): cpu.write8(cpu.addr_abs(), cpu.x)
def _STY_zp(cpu): cpu.write8(cpu.addr_zp(), cpu.y)
def _STY_abs(cpu): cpu.write8(cpu.addr_abs(), cpu.y)
def _AND_imm(cpu): cpu.a = cpu.set_nz(cpu.a & cpu.read8(cpu.addr_imm()))
def _AND_zp(cpu): cpu.a = cpu.set_nz(cpu.a & cpu.read8(cpu.addr_zp()))
def _AND_abs(cpu): cpu.a = cpu.set_nz(cpu.a & cpu.read8(cpu.addr_abs()))
def _ORA_imm(cpu): cpu.a = cpu.set_nz(cpu.a | cpu.read8(cpu.addr_imm()))
def _ORA_zp(cpu): cpu.a = cpu.set_nz(cpu.a | cpu.read8(cpu.addr_zp()))
def _EOR_imm(cpu): cpu.a = cpu.set_nz(cpu.a ^ cpu.read8(cpu.addr_imm()))
def _EOR_abs(cpu): cpu.a = cpu.set_nz(cpu.a ^ cpu.read8(cpu.addr_abs()))
def _adc(cpu, m):
r = cpu.a + m + (1 if cpu.c else 0)
cpu.c = r > 0xFF
cpu.v = ((cpu.a ^ r) & (m ^ r) & 0x80) != 0
cpu.a = cpu.set_nz(r & 0xFF)
def _ADC_imm(cpu): _adc(cpu, cpu.read8(cpu.addr_imm()))
def _ADC_zp(cpu): _adc(cpu, cpu.read8(cpu.addr_zp()))
def _ADC_abs(cpu): _adc(cpu, cpu.read8(cpu.addr_abs()))
def _ADC_absy(cpu): _adc(cpu, cpu.read8(cpu.addr_absy()))
def _sbc(cpu, m):
_adc(cpu, m ^ 0xFF)
def _SBC_imm(cpu): _sbc(cpu, cpu.read8(cpu.addr_imm()))
def _SBC_zp(cpu): _sbc(cpu, cpu.read8(cpu.addr_zp()))
def _SBC_abs(cpu): _sbc(cpu, cpu.read8(cpu.addr_abs()))
def _SBC_absx(cpu): _sbc(cpu, cpu.read8(cpu.addr_absx()))
def _SBC_absy(cpu): _sbc(cpu, cpu.read8(cpu.addr_absy()))
def _AND_absx(cpu): cpu.a = cpu.set_nz(cpu.a & cpu.read8(cpu.addr_absx()))
def _AND_absy(cpu): cpu.a = cpu.set_nz(cpu.a & cpu.read8(cpu.addr_absy()))
def _AND_indy(cpu): cpu.a = cpu.set_nz(cpu.a & cpu.read8(cpu.addr_indy()))
def _ORA_absx(cpu): cpu.a = cpu.set_nz(cpu.a | cpu.read8(cpu.addr_absx()))
def _ORA_abs(cpu): cpu.a = cpu.set_nz(cpu.a | cpu.read8(cpu.addr_abs()))
def _ORA_zpx(cpu): cpu.a = cpu.set_nz(cpu.a | cpu.read8(cpu.addr_zpx()))
def _ORA_indy(cpu): cpu.a = cpu.set_nz(cpu.a | cpu.read8(cpu.addr_indy()))
def _EOR_zp(cpu): cpu.a = cpu.set_nz(cpu.a ^ cpu.read8(cpu.addr_zp()))
def _EOR_zpx(cpu): cpu.a = cpu.set_nz(cpu.a ^ cpu.read8(cpu.addr_zpx()))
def _EOR_absx(cpu): cpu.a = cpu.set_nz(cpu.a ^ cpu.read8(cpu.addr_absx()))
def _EOR_absy(cpu): cpu.a = cpu.set_nz(cpu.a ^ cpu.read8(cpu.addr_absy()))
def _ADC_zpx(cpu): _adc(cpu, cpu.read8(cpu.addr_zpx()))
def _ADC_absx(cpu): _adc(cpu, cpu.read8(cpu.addr_absx()))
def _CMP_absx(cpu): _cmp_with(cpu, cpu.a, cpu.read8(cpu.addr_absx()))
def _CMP_absy(cpu): _cmp_with(cpu, cpu.a, cpu.read8(cpu.addr_absy()))
def _CMP_zpx(cpu): _cmp_with(cpu, cpu.a, cpu.read8(cpu.addr_zpx()))
def _JMP_ind(cpu):
addr = cpu.addr_abs()
cpu.pc = cpu.read8(addr) | (cpu.read8((addr & 0xFF00) | ((addr + 1) & 0xFF)) << 8)
def _BIT_zp(cpu):
v = cpu.read8(cpu.addr_zp())
cpu.z = (cpu.a & v) == 0
cpu.n = (v & 0x80) != 0
cpu.v = (v & 0x40) != 0
def _BIT_abs(cpu):
v = cpu.read8(cpu.addr_abs())
cpu.z = (cpu.a & v) == 0
cpu.n = (v & 0x80) != 0
cpu.v = (v & 0x40) != 0
def _ASL_zp(cpu):
addr = cpu.addr_zp()
v = cpu.read8(addr)
cpu.c = (v & 0x80) != 0
cpu.write8(addr, cpu.set_nz((v << 1) & 0xFF))
def _ASL_abs(cpu):
addr = cpu.addr_abs()
v = cpu.read8(addr)
cpu.c = (v & 0x80) != 0
cpu.write8(addr, cpu.set_nz((v << 1) & 0xFF))
def _LSR_abs(cpu):
addr = cpu.addr_abs()
v = cpu.read8(addr)
cpu.c = (v & 0x01) != 0
cpu.write8(addr, cpu.set_nz(v >> 1))
def _ROL_abs(cpu):
addr = cpu.addr_abs()
v = cpu.read8(addr)
new_c = (v & 0x80) != 0
v = ((v << 1) | (1 if cpu.c else 0)) & 0xFF
cpu.write8(addr, cpu.set_nz(v))
cpu.c = new_c
def _ROR_abs(cpu):
addr = cpu.addr_abs()
v = cpu.read8(addr)
new_c = (v & 0x01) != 0
v = ((v >> 1) | (0x80 if cpu.c else 0)) & 0xFF
cpu.write8(addr, cpu.set_nz(v))
cpu.c = new_c
def _ROR_zp(cpu):
addr = cpu.addr_zp()
v = cpu.read8(addr)
new_c = (v & 0x01) != 0
v = ((v >> 1) | (0x80 if cpu.c else 0)) & 0xFF
cpu.write8(addr, cpu.set_nz(v))
cpu.c = new_c
def _STA_zpx(cpu_): cpu_.write8(cpu_.addr_zpx(), cpu_.a)
def _TSX(cpu): cpu.x = cpu.set_nz(cpu.sp)
def _TXS(cpu): cpu.sp = cpu.x
def _CLI(cpu): pass
def _CLV(cpu): cpu.v = False
def _SED(cpu): pass # decimal mode ignored
def _STX_zpy(cpu):
a = (cpu.read8(cpu.pc) + cpu.y) & 0xFF; cpu.pc = (cpu.pc + 1) & 0xFFFF
cpu.write8(a, cpu.x)
def _STY_zpx(cpu): cpu.write8(cpu.addr_zpx(), cpu.y)
def _LDX_zpy(cpu):
a = (cpu.read8(cpu.pc) + cpu.y) & 0xFF; cpu.pc = (cpu.pc + 1) & 0xFFFF
cpu.x = cpu.set_nz(cpu.read8(a))
def _LDY_zpx(cpu): cpu.y = cpu.set_nz(cpu.read8(cpu.addr_zpx()))
def _cmp_with(cpu, lhs, m):
r = (lhs - m) & 0x1FF
cpu.c = lhs >= m
cpu.set_nz(r & 0xFF)
def _CMP_imm(cpu): _cmp_with(cpu, cpu.a, cpu.read8(cpu.addr_imm()))
def _CMP_zp(cpu): _cmp_with(cpu, cpu.a, cpu.read8(cpu.addr_zp()))
def _CMP_abs(cpu): _cmp_with(cpu, cpu.a, cpu.read8(cpu.addr_abs()))
def _CPX_imm(cpu): _cmp_with(cpu, cpu.x, cpu.read8(cpu.addr_imm()))
def _CPX_abs(cpu): _cmp_with(cpu, cpu.x, cpu.read8(cpu.addr_abs()))
def _CPY_imm(cpu): _cmp_with(cpu, cpu.y, cpu.read8(cpu.addr_imm()))
def _ASL_a(cpu):
cpu.c = (cpu.a & 0x80) != 0
cpu.a = cpu.set_nz((cpu.a << 1) & 0xFF)
def _LSR_a(cpu):
cpu.c = (cpu.a & 0x01) != 0
cpu.a = cpu.set_nz(cpu.a >> 1)
def _LSR_zp(cpu):
addr = cpu.addr_zp()
v = cpu.read8(addr)
cpu.c = (v & 0x01) != 0
cpu.write8(addr, cpu.set_nz(v >> 1))
def _ROL_a(cpu):
new_c = (cpu.a & 0x80) != 0
cpu.a = cpu.set_nz(((cpu.a << 1) | (1 if cpu.c else 0)) & 0xFF)
cpu.c = new_c
def _ROL_zp(cpu):
addr = cpu.addr_zp()
v = cpu.read8(addr)
new_c = (v & 0x80) != 0
v = ((v << 1) | (1 if cpu.c else 0)) & 0xFF
cpu.write8(addr, cpu.set_nz(v))
cpu.c = new_c
def _ROR_a(cpu):
new_c = (cpu.a & 0x01) != 0
cpu.a = cpu.set_nz(((cpu.a >> 1) | (0x80 if cpu.c else 0)) & 0xFF)
cpu.c = new_c
def _INC_abs(cpu):
addr = cpu.addr_abs()
cpu.write8(addr, cpu.set_nz((cpu.read8(addr) + 1) & 0xFF))
def _INC_absx(cpu):
addr = cpu.addr_absx()
cpu.write8(addr, cpu.set_nz((cpu.read8(addr) + 1) & 0xFF))
def _INC_zp(cpu):
addr = cpu.addr_zp()
cpu.write8(addr, cpu.set_nz((cpu.read8(addr) + 1) & 0xFF))
def _INC_zpx(cpu):
addr = cpu.addr_zpx()
cpu.write8(addr, cpu.set_nz((cpu.read8(addr) + 1) & 0xFF))
def _DEC_abs(cpu):
addr = cpu.addr_abs()
cpu.write8(addr, cpu.set_nz((cpu.read8(addr) - 1) & 0xFF))
def _DEC_absx(cpu):
addr = cpu.addr_absx()
cpu.write8(addr, cpu.set_nz((cpu.read8(addr) - 1) & 0xFF))
def _DEC_zp(cpu):
addr = cpu.addr_zp()
cpu.write8(addr, cpu.set_nz((cpu.read8(addr) - 1) & 0xFF))
def _DEC_zpx(cpu):
addr = cpu.addr_zpx()
cpu.write8(addr, cpu.set_nz((cpu.read8(addr) - 1) & 0xFF))
def _ASL_zpx(cpu):
addr = cpu.addr_zpx()
v = cpu.read8(addr)
cpu.c = (v & 0x80) != 0
cpu.write8(addr, cpu.set_nz((v << 1) & 0xFF))
def _ASL_absx(cpu):
addr = cpu.addr_absx()
v = cpu.read8(addr)
cpu.c = (v & 0x80) != 0
cpu.write8(addr, cpu.set_nz((v << 1) & 0xFF))
def _LSR_zpx(cpu):
addr = cpu.addr_zpx()
v = cpu.read8(addr)
cpu.c = (v & 0x01) != 0
cpu.write8(addr, cpu.set_nz(v >> 1))
def _LSR_absx(cpu):
addr = cpu.addr_absx()
v = cpu.read8(addr)
cpu.c = (v & 0x01) != 0
cpu.write8(addr, cpu.set_nz(v >> 1))
def _ROL_zpx(cpu):
addr = cpu.addr_zpx()
v = cpu.read8(addr)
new_c = (v & 0x80) != 0
v = ((v << 1) | (1 if cpu.c else 0)) & 0xFF
cpu.write8(addr, cpu.set_nz(v))
cpu.c = new_c
def _ROL_absx(cpu):
addr = cpu.addr_absx()
v = cpu.read8(addr)
new_c = (v & 0x80) != 0
v = ((v << 1) | (1 if cpu.c else 0)) & 0xFF
cpu.write8(addr, cpu.set_nz(v))
cpu.c = new_c
def _ROR_zpx(cpu):
addr = cpu.addr_zpx()
v = cpu.read8(addr)
new_c = (v & 0x01) != 0
v = ((v >> 1) | (0x80 if cpu.c else 0)) & 0xFF
cpu.write8(addr, cpu.set_nz(v))
cpu.c = new_c
def _ROR_absx(cpu):
addr = cpu.addr_absx()
v = cpu.read8(addr)
new_c = (v & 0x01) != 0
v = ((v >> 1) | (0x80 if cpu.c else 0)) & 0xFF
cpu.write8(addr, cpu.set_nz(v))
cpu.c = new_c
def _LDA_indx(cpu):
zp = cpu.read8(cpu.pc); cpu.pc = (cpu.pc + 1) & 0xFFFF
addr_ptr = (zp + cpu.x) & 0xFF
addr = cpu.ram[addr_ptr] | (cpu.ram[(addr_ptr + 1) & 0xFF] << 8)
cpu.a = cpu.set_nz(cpu.read8(addr))
def _STA_indx(cpu):
zp = cpu.read8(cpu.pc); cpu.pc = (cpu.pc + 1) & 0xFFFF
addr_ptr = (zp + cpu.x) & 0xFF
addr = cpu.ram[addr_ptr] | (cpu.ram[(addr_ptr + 1) & 0xFF] << 8)
cpu.write8(addr, cpu.a)
def _AND_indx(cpu):
zp = cpu.read8(cpu.pc); cpu.pc = (cpu.pc + 1) & 0xFFFF
addr_ptr = (zp + cpu.x) & 0xFF
addr = cpu.ram[addr_ptr] | (cpu.ram[(addr_ptr + 1) & 0xFF] << 8)
cpu.a = cpu.set_nz(cpu.a & cpu.read8(addr))
def _ORA_indx(cpu):
zp = cpu.read8(cpu.pc); cpu.pc = (cpu.pc + 1) & 0xFFFF
addr_ptr = (zp + cpu.x) & 0xFF
addr = cpu.ram[addr_ptr] | (cpu.ram[(addr_ptr + 1) & 0xFF] << 8)
cpu.a = cpu.set_nz(cpu.a | cpu.read8(addr))
def _EOR_indx(cpu):
zp = cpu.read8(cpu.pc); cpu.pc = (cpu.pc + 1) & 0xFFFF
addr_ptr = (zp + cpu.x) & 0xFF
addr = cpu.ram[addr_ptr] | (cpu.ram[(addr_ptr + 1) & 0xFF] << 8)
cpu.a = cpu.set_nz(cpu.a ^ cpu.read8(addr))
def _ADC_indx(cpu):
zp = cpu.read8(cpu.pc); cpu.pc = (cpu.pc + 1) & 0xFFFF
addr_ptr = (zp + cpu.x) & 0xFF
addr = cpu.ram[addr_ptr] | (cpu.ram[(addr_ptr + 1) & 0xFF] << 8)
_adc(cpu, cpu.read8(addr))
def _SBC_indx(cpu):
zp = cpu.read8(cpu.pc); cpu.pc = (cpu.pc + 1) & 0xFFFF
addr_ptr = (zp + cpu.x) & 0xFF
addr = cpu.ram[addr_ptr] | (cpu.ram[(addr_ptr + 1) & 0xFF] << 8)
_sbc(cpu, cpu.read8(addr))
def _CMP_indx(cpu):
zp = cpu.read8(cpu.pc); cpu.pc = (cpu.pc + 1) & 0xFFFF
addr_ptr = (zp + cpu.x) & 0xFF
addr = cpu.ram[addr_ptr] | (cpu.ram[(addr_ptr + 1) & 0xFF] << 8)
_cmp_with(cpu, cpu.a, cpu.read8(addr))
def _CPX_zp(cpu):
_cmp_with(cpu, cpu.x, cpu.read8(cpu.addr_zp()))
def _CPY_zp(cpu):
_cmp_with(cpu, cpu.y, cpu.read8(cpu.addr_zp()))
def _BPL(cpu): cpu.branch(not cpu.n)
def _BMI(cpu): cpu.branch(cpu.n)
def _BNE(cpu): cpu.branch(not cpu.z)
def _BEQ(cpu): cpu.branch(cpu.z)
def _BCC(cpu): cpu.branch(not cpu.c)
def _BCS(cpu): cpu.branch(cpu.c)
def _BVC(cpu): cpu.branch(not cpu.v)
def _BVS(cpu): cpu.branch(cpu.v)
def _JMP_abs(cpu): cpu.pc = cpu.addr_abs()
def _JSR(cpu):
target = cpu.addr_abs()
cpu.push16((cpu.pc - 1) & 0xFFFF)
cpu.pc = target
def _RTS(cpu):
cpu.pc = (cpu.pop16() + 1) & 0xFFFF
# Build dispatch table
_DISPATCH = {
0x00: _NOP, # treat BRK as NOP (we never expect it)
0x18: _CLC, 0x38: _SEC, 0x58: _NOP, 0x78: _SEI, 0xD8: _CLD,
0x08: _PHP, 0x28: _PLP, 0x48: _PHA, 0x68: _PLA,
0x8A: _TXA, 0xAA: _TAX, 0x98: _TYA, 0xA8: _TAY,
0xE8: _INX, 0xC8: _INY, 0xCA: _DEX, 0x88: _DEY,
0xA9: _LDA_imm, 0xA5: _LDA_zp, 0xB5: _LDA_zpx, 0xAD: _LDA_abs,
0xBD: _LDA_absx, 0xB9: _LDA_absy, 0xB1: _LDA_indy,
0xA2: _LDX_imm, 0xAE: _LDX_abs, 0xBE: _LDX_absy, 0xA6: _LDX_zp,
0xA0: _LDY_imm, 0xAC: _LDY_abs, 0xBC: _LDY_absx, 0xA4: _LDY_zp,
0x85: _STA_zp, 0x95: _STA_zpx, 0x8D: _STA_abs,
0x9D: _STA_absx, 0x99: _STA_absy, 0x91: _STA_indy,
0x86: _STX_zp, 0x8E: _STX_abs,
0x84: _STY_zp, 0x8C: _STY_abs,
0x29: _AND_imm, 0x25: _AND_zp, 0x2D: _AND_abs,
0x3D: _AND_absx, 0x39: _AND_absy, 0x31: _AND_indy,
0x09: _ORA_imm, 0x05: _ORA_zp, 0x0D: _ORA_abs, 0x15: _ORA_zpx,
0x1D: _ORA_absx, 0x11: _ORA_indy,
0x49: _EOR_imm, 0x4D: _EOR_abs, 0x45: _EOR_zp, 0x55: _EOR_zpx,
0x5D: _EOR_absx, 0x59: _EOR_absy,
0x69: _ADC_imm, 0x65: _ADC_zp, 0x6D: _ADC_abs, 0x79: _ADC_absy,
0x75: _ADC_zpx, 0x7D: _ADC_absx,
0xE9: _SBC_imm, 0xE5: _SBC_zp, 0xED: _SBC_abs, 0xFD: _SBC_absx, 0xF9: _SBC_absy,
0xC9: _CMP_imm, 0xC5: _CMP_zp, 0xCD: _CMP_abs, 0xD5: _CMP_zpx,
0xDD: _CMP_absx, 0xD9: _CMP_absy,
0xE0: _CPX_imm, 0xEC: _CPX_abs,
0xC0: _CPY_imm,
0x24: _BIT_zp, 0x2C: _BIT_abs,
0x0A: _ASL_a, 0x06: _ASL_zp, 0x0E: _ASL_abs,
0x4A: _LSR_a, 0x46: _LSR_zp, 0x4E: _LSR_abs,
0x2A: _ROL_a, 0x26: _ROL_zp, 0x2E: _ROL_abs,
0x6A: _ROR_a, 0x66: _ROR_zp, 0x6E: _ROR_abs,
0xEE: _INC_abs, 0xFE: _INC_absx, 0xE6: _INC_zp, 0xF6: _INC_zpx,
0xCE: _DEC_abs, 0xDE: _DEC_absx, 0xC6: _DEC_zp, 0xD6: _DEC_zpx,
0x16: _ASL_zpx, 0x1E: _ASL_absx,
0x56: _LSR_zpx, 0x5E: _LSR_absx,
0x36: _ROL_zpx, 0x3E: _ROL_absx,
0x76: _ROR_zpx, 0x7E: _ROR_absx,
0xA1: _LDA_indx, 0x81: _STA_indx, 0x21: _AND_indx,
0x01: _ORA_indx, 0x41: _EOR_indx,
0x61: _ADC_indx, 0xE1: _SBC_indx, 0xC1: _CMP_indx,
0xE4: _CPX_zp, 0xC4: _CPY_zp,
0x6C: _JMP_ind,
0x9A: _TXS, 0xBA: _TSX,
0x58: _CLI, 0xB8: _CLV, 0xF8: _SED,
0x96: _STX_zpy, 0x94: _STY_zpx,
0xB6: _LDX_zpy, 0xB4: _LDY_zpx,
0x10: _BPL, 0x30: _BMI, 0xD0: _BNE, 0xF0: _BEQ,
0x90: _BCC, 0xB0: _BCS, 0x50: _BVC, 0x70: _BVS,
0x4C: _JMP_abs, 0x20: _JSR, 0x60: _RTS,
0xEA: _NOP,
}

78
assets/depackSpeechRam.py Executable file
View file

@ -0,0 +1,78 @@
#!/usr/bin/env python3
"""Run Space Taxi's depacker to produce a FLAT 64K RAM image.
Why this exists: the disk's "SPACE TAXI+" is a crunched single file (it
matches the running game's memory in 0.6% of bytes), so the speech
samples cannot be read out of it statically. And a VICE memory dump is
no good either -- it captures the banked-IN view, so $A000-$BFFF comes
back as BASIC ROM and 13 of the 15 utterances are missing. Every
mem0000*.bin in this tree has that problem.
The way out needs no emulator install: cpu6502.py (next to this script) is a
plain 6502 core over a flat 64K bytearray with no banking and no I/O,
which is all a depacker needs. Load the .prg, run from the BASIC SYS
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/ (gitignored research
area, same as genC64Data.py's inputs).
"""
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
STUFF = os.path.join(HERE, "..", "stuff")
# The BASIC stub is `0B08 D307 9E "2059" 00` -- SYS 2059.
ENTRY = 0x080B
MAX_STEPS = 60000000
CHECK_MASK = 0xFFFF # how often to test for completion
# Completion test: the $9B00 index table is populated and the first
# utterance's $FE lead-in is in place.
TABLE_ADDR = 0x9B00
FIRST_UTT = 0x9B40
def main():
prgPath = sys.argv[1] if len(sys.argv) > 1 else os.path.join(STUFF, "extract", "SPACE TAXI+.prg")
outPath = sys.argv[2] if len(sys.argv) > 2 else os.path.join(STUFF, "staxi-ram.bin")
sys.path.insert(0, HERE)
from cpu6502 import Cpu6502
prg = open(prgPath, "rb").read()
load = prg[0] | (prg[1] << 8)
ram = bytearray(65536)
ram[load:load + len(prg) - 2] = prg[2:]
print("loaded %s at $%04X..$%04X" % (os.path.basename(prgPath), load, load + len(prg) - 3))
cpu = Cpu6502(ram)
cpu.pc = ENTRY
steps = 0
done = False
try:
while steps < MAX_STEPS:
cpu.step()
steps += 1
if (steps & CHECK_MASK) == 0:
if ram[TABLE_ADDR] != 0 and ram[FIRST_UTT] == 0xFE:
done = True
break
except Exception as exc:
# A depacker that runs off into I/O or an undocumented opcode
# still usually gets the data down first, so report and carry on
# to the write rather than throwing the work away.
print("6502 stopped after %d steps at PC=$%04X: %s" % (steps, cpu.pc, exc))
open(outPath, "wb").write(bytes(ram))
print("%d steps, depack %s -> %s" % (steps, "detected" if done else "NOT detected", outPath))
if not done:
print("WARNING: completion check never fired; the image may be partial.")
if __name__ == "__main__":
main()

115
assets/dumpScenes.py Normal file
View file

@ -0,0 +1,115 @@
#!/usr/bin/env python3
# Drive the captured Space Taxi RAM through the 6502 emulator to
# decompress every scene the game knows how to load.
#
# Approach:
# 1. Load a VICE memory dump as flat 64KB RAM. The dump captured at
# runtime contains the decompressor at $9656, the scene pointer
# table at $9600, and (we hope) the compressed data the pointers
# reach into.
# 2. Sanity check: run scene ID 1 (the captured live level) through
# $6283 and compare the resulting screen buffer at $7530-$7929
# with the live screen RAM at $0400-$07E7 already in the dump.
# If they match, the emulator + dump together can reproduce any
# scene. If not, we know something's missing (likely hidden RAM
# under ROMs that VICE didn't capture).
# 3. If sanity passes: iterate scene IDs 0..23 (lettered levels)
# plus known special IDs (title=$24, level-complete=$19) and
# dump each scene's screen + color buffer.
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from cpu6502 import Cpu6502
def load_dump(path):
with open(path, 'rb') as f:
raw = f.read()
if len(raw) == 0x10000:
return bytearray(raw)
if len(raw) == 0x10002:
# 2-byte LE load-addr header
return bytearray(raw[2:])
raise SystemExit(f'{path}: unexpected dump size {len(raw)}')
# Memory map landmarks (from MECHANICS.md):
ADDR_DECOMP_ENTRY = 0x6283 # bank-switch wrapper around $9656
SCREEN_BUF_BASE = 0x7530 # decompressed screen RAM target
SCREEN_BUF_SIZE = 0x400 # 4 pages = 1024 bytes
COLOR_BUF_BASE = 0x7918 # decompressed color RAM target
COLOR_BUF_SIZE = 0x400
def run_scene(ram, scene_id):
"""Run $6283(X=scene_id) on a fresh copy of `ram`. Returns a tuple
(screen_buf, color_buf, final_ram) so the caller can compare or
examine state."""
work = bytearray(ram) # copy so callers can keep the original
cpu = Cpu6502(work)
cpu.a = scene_id
cpu.x = scene_id
cpu.y = 0
cpu.call(ADDR_DECOMP_ENTRY)
screen = bytes(work[SCREEN_BUF_BASE:SCREEN_BUF_BASE + SCREEN_BUF_SIZE])
color = bytes(work[COLOR_BUF_BASE:COLOR_BUF_BASE + COLOR_BUF_SIZE])
return screen, color, work
def compare_screen(decomp_screen, live_screen, label):
"""Compare 1024-byte decompressed screen vs live screen RAM.
Only the first 1000 bytes are visible (40x25). Bytes 1000..1023
are unused tail / overlap with color RAM, so we ignore them."""
a = decomp_screen[:1000]
b = live_screen[:1000]
if a == b:
print(f' {label}: MATCH')
return True
# Show how many cells differ and a sample
diffs = [(i, a[i], b[i]) for i in range(1000) if a[i] != b[i]]
print(f' {label}: {len(diffs)} / 1000 cells differ')
for i, av, bv in diffs[:6]:
row, col = divmod(i, 40)
print(f' cell ({col:2d},{row:2d}): decomp=${av:02X} live=${bv:02X}')
return False
def main():
if len(sys.argv) < 2:
print('usage: dumpScenes.py <dump-path> [scene-id]', file=sys.stderr)
sys.exit(2)
ram = load_dump(sys.argv[1])
# Figure out what the captured state is, then pick the scene to
# test against. $7215 = current level number (0..23 = A..X),
# but the title screen / specials don't update it -- the
# most-recent JSR $6283 used scene $24 (title) or $19 (other
# special). Default to the level-number scene, but allow override.
state_level = ram[0x7215]
if len(sys.argv) >= 3:
scene = int(sys.argv[2], 0)
else:
# Heuristic: if state sentinel $07A6 says we're on the title,
# the live screen matches scene 36 ($24). Otherwise we're
# mid-gameplay and the live screen matches scene = level.
# Title state in this game: top-left tile is the inner frame
# corner $5D / $5C. For now just try scene 36 then scene N.
scene = 36
print(f'=== Sanity test: scene {scene} (${scene:02X}) vs live screen RAM ===')
print(f' captured state: $7215={state_level} $07A6=${ram[0x07A6]:02X}')
try:
scr, col, _ = run_scene(ram, scene)
except Exception as e:
print(f' emulator raised: {type(e).__name__}: {e}')
return
live_scr = bytes(ram[0x0400:0x0400 + 1024])
live_col = bytes(b & 0x0F for b in ram[0xD800:0xD800 + 1024])
compare_screen(scr, live_scr, 'screen')
masked_col = bytes(b & 0x0F for b in col)
compare_screen(masked_col, live_col, 'color ')
if __name__ == '__main__':
main()

View file

@ -1,103 +0,0 @@
#!/usr/bin/env python3
# extractFlame.py -- wire the engine-flame sprite cels from the C64 ROM
# image (stuff/spacetaxi/raw.bin) into the sprite sheet's flame row.
#
# The flame is C64 sprite 2, MULTICOLOR (verified: $D01C bit2 set). Its
# per-direction cel is chosen from the pointer table at $6DB0 (indexed by
# the direction mask $716A). Reading raw.bin at $6DB0 (2-byte load-addr
# header -> +2) gives, and the port's kFlameCelByDirMask mirrors exactly:
# mask 1 UP -> block $D8 mask 8 RIGHT -> block $D7
# mask 2 DOWN -> block $D4 mask 9 UP+RIGHT -> block $D3
# mask 4 LEFT -> block $D5 mask 10 DOWN+RIGHT-> block $D6
# mask 5 UP+LEFT -> block $D2
# mask 6 DOWN+LEFT -> block $D1
# flameCels[0..7] load from .spr cels 18..25 (sheet row 2, cols 0..7), so
# the cel order below is the kFlameCelByDirMask index order.
#
# Sprite data lives at block*64 in VIC bank 0 ($3000-$37FF). Each C64
# multicolor sprite is 12 MC-pixels x 21 rows (3 bytes/row, 2 bits/pixel);
# each MC pixel is 2 hi-res pixels, so it maps 1 MC pixel -> 2 of the
# port's 24-wide 4bpp cel pixels. Colors are the live-level flame palette
# ($7D05->$D025=$02 red = MC0, $7D06->$D026=$07 yellow = MC1; sprite-2
# colour = orange), mapped to sprites.png's indexed palette.
#
# Patches ONLY the flame row (row 2) of sprites.png -- taxi (row 0),
# passenger (row 1), and font.png are left untouched. Re-run the .spr
# bake afterwards (make/<port>.mk STAXI_SPR rule).
import os
import sys
try:
from PIL import Image
except ImportError:
sys.exit("extractFlame.py: needs Pillow (pip install pillow)")
HERE = os.path.dirname(os.path.abspath(__file__))
RAW = os.path.join(HERE, "..", "..", "..", "stuff", "spacetaxi", "raw.bin")
PNG = os.path.join(HERE, "sprites", "sprites.png")
CEL = 24 # cel is 24x24 px
LOAD_HDR = 2 # raw.bin 2-byte load-address header
# flameCels index -> C64 sprite block (from $6DB0 / kFlameCelByDirMask).
FLAME_BLOCKS = [0xD8, 0xD4, 0xD5, 0xD2, 0xD1, 0xD7, 0xD3, 0xD6]
# Multicolor 2-bit pixel -> palette index. The engine renders sprite
# cels through kDefaultPalette (stRender.c), whose index IS the C64 VIC-II
# colour code -- the sprite's own baked palette is ignored (loadSpriteSheet
# passes NULL). So the C64 flame colours map DIRECTLY by code:
# 00 transparent(0), 01 MC0=$D025=$02 red(2), 11 MC1=$D026=$07 yellow(7),
# 10 sprite-2 colour -> orange(8) (barely used by these cels).
# (NOT the sprites.png preview-palette indices, which differ.)
MC_TO_INDEX = {0: 0, 1: 2, 2: 8, 3: 7}
FLAME_ROW_Y = 2 * CEL # sheet row 2 (flame row) top edge
def loadRaw():
with open(RAW, "rb") as f:
return f.read()
def celFromBlock(raw, block):
# Return a 24x24 list-of-rows of palette indices for one flame block.
base = LOAD_HDR + block * 64
cel = [[0] * CEL for _ in range(CEL)]
for row in range(21): # C64 sprite is 21 rows tall
for byteIdx in range(3): # 3 bytes/row
b = raw[base + row * 3 + byteIdx]
for pair in range(4): # 4 MC pixels per byte (MSB first)
mc = (b >> ((3 - pair) * 2)) & 3
idx = MC_TO_INDEX[mc]
px = (byteIdx * 4 + pair) * 2 # MC pixel -> 2 cel pixels
cel[row][px] = idx
cel[row][px + 1] = idx
return cel
def main():
raw = loadRaw()
im = Image.open(PNG)
if im.mode != "P":
sys.exit(f"extractFlame.py: {PNG} is {im.mode}, expected indexed 'P'")
px = im.load()
nonEmpty = 0
for celIdx, block in enumerate(FLAME_BLOCKS):
cel = celFromBlock(raw, block)
x0 = celIdx * CEL
opaque = 0
for y in range(CEL):
for x in range(CEL):
v = cel[y][x]
px[x0 + x, FLAME_ROW_Y + y] = v
if v != 0:
opaque += 1
nonEmpty += 1 if opaque else 0
print(f" flameCels[{celIdx}] block ${block:02X}: {opaque} opaque px -> cel {18 + celIdx}")
im.save(PNG)
print(f"extractFlame: patched {nonEmpty}/8 flame cels into {os.path.relpath(PNG, HERE)}")
if __name__ == "__main__":
main()

225
assets/extractSpeech.py Executable file
View file

@ -0,0 +1,225 @@
#!/usr/bin/env python3
"""Extract Space Taxi's digitised speech from a C64 memory dump.
FORMAT (reverse-engineered from bankedSfxLoad at $9802, see
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,
terminated by a zero code. `code` is the PETSCII character the game
asks for -- 'H' 'T' '!' spell "Hey Taxi!", 'P' + digit + '?' spell
"Pad two, please", 'U' is "up".
* Each utterance is a byte stream. For every byte the player busy-waits
that many passes of a fixed delay loop and then TOGGLES bit 0 of
$D417, which flips voice 1 in and out of the filter -- a 1-bit
speaker. So each byte is the length of one half-cycle.
* $FE is a raster sync (the player waits on $D011), $FF ends the
utterance.
The delay loop at $98A1..$98AA costs 27 cycles per pass and the
toggle/advance/fetch tail ($98AC..$987F) another 33, hence CYCLES().
LIMITATION: a plain VICE memory dump captures the BANKED-IN view, so
$A000-$BFFF comes back as BASIC ROM ("CBMBASIC" at $A004) and most of the
speech is missing -- the player banks BASIC out ($01 = $06) precisely
because the samples live in the RAM underneath. Dump it with the ROMs
banked out, e.g. in the VICE monitor:
bank ram
save "staxi-ram.bin" 0 0000 ffff
Usage: extractSpeech.py <dump.bin> [outDir]
The dump may have a 2-byte load address or not; both are handled.
"""
import os
import struct
import sys
import wave
NTSC_HZ = 1022727.0 # the port already uses this (ST_SID_HZ_NUM)
OUT_RATE = 22050
TABLE_ADDR = 0x9B00
END_MARK = 0xFF
SYNC_MARK = 0xFE
def cyclesFor(count):
"""Half-cycle length in CPU cycles for one stream byte."""
return 27 * count + 33
def loadDump(path):
"""Return a 64K-addressable bytes object from a dump with or without
a load-address header."""
raw = open(path, "rb").read()
if len(raw) in (65538, 65536 + 2):
return raw[2:]
if len(raw) == 65536:
return raw
# Headerless dump starting at $0801 (what mem0801.prg turned out to be).
pad = bytearray(65536)
pad[0x0801:0x0801 + len(raw)] = raw[:65536 - 0x0801]
return bytes(pad)
def readTable(mem):
"""[(code, pointer)] from the $9B00 index."""
out = []
y = 0
while y < 0x300:
code = mem[TABLE_ADDR + y]
if code == 0 or code == 0xFF:
break
out.append((code, mem[TABLE_ADDR + y + 1] | (mem[TABLE_ADDR + y + 2] << 8)))
y += 3
return out
def decode(mem, ptr, limit=0x4000):
"""Return (runs, byteCount, sawEnd). runs is [(level, cycles)]."""
runs = []
level = 0
addr = ptr
while addr - ptr < limit:
b = mem[addr]
addr += 1
if b == END_MARK:
return runs, addr - ptr, True
if b == SYNC_MARK:
runs.append((level, int(NTSC_HZ / 60.0)))
continue
runs.append((level, cyclesFor(b)))
level ^= 1
return runs, addr - ptr, False
def writeWav(runs, path):
samples = bytearray()
carry = 0.0
perOut = NTSC_HZ / OUT_RATE
for level, cyc in runs:
carry += cyc
n = int(carry / perOut)
carry -= n * perOut
samples += struct.pack("<h", 12000 if level else -12000) * n
w = wave.open(path, "wb")
w.setnchannels(1)
w.setsampwidth(2)
w.setframerate(OUT_RATE)
w.writeframes(bytes(samples))
w.close()
return len(samples) // 2 / float(OUT_RATE)
def emitC(mem, table, romLo, outDir):
"""Write stSpeechData.c/.h: the raw run-length streams plus an index.
The streams ship as-is (about 7.4 KB for all 15) and the game expands
them to PCM on the fly -- rendering them at bake time would be ~38 KB
at 5 kHz, which will not fit an IIgs bank alongside everything else.
"""
blobs = []
index = []
for code, ptr in table:
if ptr >= romLo:
continue
runs, nbytes, sawEnd = decode(mem, ptr)
nxt = next((q for _, q in sorted(table, key=lambda e: e[1]) if q > ptr), None)
if not sawEnd or (nxt is not None and nbytes > nxt - ptr):
continue
index.append((code, sum(len(b) for b in blobs), nbytes))
blobs.append(bytes(mem[ptr:ptr + nbytes]))
data = b"".join(blobs)
h = os.path.join(outDir, "stSpeechData.h")
c = os.path.join(outDir, "stSpeechData.c")
with open(h, "w") as fp:
fp.write("// Generated by assets/extractSpeech.py. Do not hand-edit.\n"
"//\n"
"// Space Taxi's digitised speech as the C64 stores it: 1-bit\n"
"// run-length streams. Each byte is one half-cycle length; the\n"
"// player toggles its output after each. $FE is a raster sync\n"
"// and $FF ends an utterance. stAudio.c expands these to PCM\n"
"// through the jlAudioPlaySfxStream fill callback.\n\n"
"#ifndef ST_SPEECH_DATA_H\n#define ST_SPEECH_DATA_H\n\n"
"#include <stdint.h>\n\n"
"typedef struct {\n"
" uint8_t code; // PETSCII: 'H' 'T' '!' 'P' '1'..'9' '?' 'U'\n"
" uint16_t offset; // into kStSpeechRle\n"
" uint16_t length;\n"
"} StSpeechEntryT;\n\n"
"#define ST_SPEECH_COUNT %du\n"
"#define ST_SPEECH_RLE_BYTES %du\n\n"
"extern const StSpeechEntryT kStSpeechIndex[ST_SPEECH_COUNT];\n"
"extern const uint8_t kStSpeechRle[ST_SPEECH_RLE_BYTES];\n\n"
"#endif\n" % (len(index), len(data)))
with open(c, "w") as fp:
fp.write('// Generated by assets/extractSpeech.py. Do not hand-edit.\n\n'
'#include "stSpeechData.h"\n\n'
'const StSpeechEntryT kStSpeechIndex[ST_SPEECH_COUNT] = {\n')
for code, off, ln in index:
fp.write(" { 0x%02Xu, %5du, %5du }, // '%s'\n"
% (code, off, ln, chr(code) if 33 <= code < 127 else "?"))
fp.write("};\n\nconst uint8_t kStSpeechRle[ST_SPEECH_RLE_BYTES] = {\n")
for i in range(0, len(data), 16):
fp.write(" " + " ".join("0x%02X," % b for b in data[i:i + 16]) + "\n")
fp.write("};\n")
print("\nemitted %s (%d entries) and %s (%d bytes)" % (h, len(index), c, len(data)))
def main():
if len(sys.argv) < 2:
sys.exit(__doc__)
emit = "--c" in sys.argv
args = [a for a in sys.argv[1:] if a != "--c"]
mem = loadDump(args[0])
outDir = args[1] if len(args) > 1 else "speech"
os.makedirs(outDir, exist_ok=True)
if mem[0xA004:0xA00C] == b"CBMBASIC":
sys.stderr.write("WARNING: $A000 holds BASIC ROM, not the RAM under it.\n"
" Utterances at $A000 and above will not decode.\n"
" Re-dump with `bank ram` (see the module docstring).\n\n")
table = readTable(mem)
romLo = 0xA000 if mem[0xA004:0xA00C] == b"CBMBASIC" else 0x10000
ordered = sorted(p for _, p in table)
good = 0
print("%-4s %-6s %7s %7s %s" % ("code", "ptr", "bytes", "dur(s)", "file"))
for code, ptr in table:
label = chr(code) if 33 <= code < 127 else "?"
# Utterances are stored back to back, so the next pointer is the
# expected end. Both checks matter: BASIC ROM is full of $FF, so
# "found a terminator" alone happily reports success on garbage --
# it claimed all 15 decoded from a ROM-shadowed dump.
nxt = next((q for q in ordered if q > ptr), None)
expected = (nxt - ptr) if nxt else None
if ptr >= romLo:
print("%-4s $%04X %7s %7s -- SKIPPED (shadowed by BASIC ROM in this dump)"
% (label, ptr, "-", "-"))
continue
runs, nbytes, sawEnd = decode(mem, ptr)
if not sawEnd:
print("%-4s $%04X %7d %7s -- SKIPPED (no $FF terminator)" % (label, ptr, nbytes, "-"))
continue
# The terminator must land BEFORE the next utterance starts.
# (Entries are back to back but may carry a pad byte, so this is
# <= rather than ==.) Overrunning means the $FF we found was not
# ours -- which is exactly what happens against a ROM shadow.
if expected is not None and nbytes > expected:
print("%-4s $%04X %7d %7s -- SKIPPED (ran %d bytes, next entry is %d away)"
% (label, ptr, nbytes, "-", nbytes, expected))
continue
path = os.path.join(outDir, "%02X_%s.wav" % (code, label if label != "?" else "x"))
dur = writeWav(runs, path)
good += 1
print("%-4s $%04X %7d %7.2f %s" % (label, ptr, nbytes, dur, os.path.basename(path)))
print("\n%d of %d utterances decoded into %s" % (good, len(table), outDir))
if emit:
emitC(mem, table, romLo, outDir)
if __name__ == "__main__":
main()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -4,19 +4,20 @@
# exact sprite-to-background collision bits), the standard sprite
# bitmaps (cab, passenger, flame, warp, death, intro star, leaving
# cels), the demo-mode RNG table ($446A), the SID SFX programs (the
# 9-byte blocks fed to $42E9), the engine-flame pointer table ($6DB0)
# and the level-intro star velocity tables ($450C/$4513).
# 9-byte blocks fed to $42E9) and the engine-flame pointer table ($6DB0).
# (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")
@ -55,12 +56,27 @@ def c_bytes(data, per_line=16, indent=" "):
return "\n".join(lines)
# The cross-segment accessors the IIgs needs (see the block this emits
# into stC64Data.c). These used to be hand-added to the GENERATED file,
# which meant re-running this script silently deleted them and broke the
# IIgs build -- the header kept declaring them and nothing defined them.
# Prototype and return expression per accessor; the header prints the
# prototypes aligned, so keep the spacing.
ACCESSORS = [
("const uint8_t *stC64Charset(void);", "&kStCharset[0][0]"),
("const uint8_t *stC64SpriteBitmap(uint8_t idx);", "kStSpriteBitmaps[idx]"),
("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]"),
]
def main():
raw = load_raw()
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")
@ -100,14 +116,16 @@ def main():
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("// 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.")
for decl in ACCESSORS:
h.append(decl[0])
h.append("")
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"')
@ -143,11 +161,25 @@ def main():
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("// Accessors. On the IIgs the program is split across banks and a 2-byte")
c.append("// data reference cannot cross a segment (only a 3-byte JSL/JML can). These")
c.append("// tiny functions live in the same segment as the tables, so their reads")
c.append("// are intra-segment; callers in other segments reach them by a supported")
c.append("// inter-segment call. On the other ports they inline away to nothing.")
c.append("// ---------------------------------------------------------------------------")
first = True
for decl, body in ACCESSORS:
c.append("")
if not first:
c.append("")
first = False
# The header pads return types to align its prototypes; the
# definitions here must not inherit that padding.
c.append(" ".join(decl[:-1].split()) + " {")
c.append(" return " + body + ";")
c.append("}")
with open(OUT_H, "w", encoding="ascii") as fp:
fp.write("\n".join(h) + "\n")

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,301 +0,0 @@
#!/usr/bin/env python3
# genPlaceholderArt.py -- generate generic geometric placeholder art
# for the Space Taxi port. Output is 320x200 indexed PNG, 16-color.
# Replace these with hand-authored art any time; the build pipeline
# picks up whichever PNG sits in assets/.
import os
from PIL import Image, ImageDraw
ROOT = os.path.dirname(os.path.abspath(__file__))
# 16-color palette matching the engine's kDefaultPalette (see stRender.c).
# Each entry is (R, G, B) in 0..255. The .jas format will store the
# 4-bit-per-channel quantization automatically via joeyasset.
PALETTE_RGB = [
(0x00, 0x00, 0x00), # 0 black
(0x00, 0x00, 0x77), # 1 dark blue
(0xCC, 0x00, 0x00), # 2 red
(0x00, 0x77, 0x00), # 3 dark green
(0x66, 0x44, 0x00), # 4 brown
(0xEE, 0xEE, 0x00), # 5 yellow
(0x88, 0x88, 0x88), # 6 mid gray
(0xBB, 0xBB, 0xBB), # 7 light gray
(0x44, 0x66, 0xFF), # 8 sky blue
(0x77, 0x88, 0xFF), # 9 light blue
(0xFF, 0x88, 0x88), # 10 light red
(0x88, 0xFF, 0x44), # 11 light green
(0xFF, 0x88, 0x00), # 12 orange
(0xFF, 0x00, 0xFF), # 13 magenta
(0x00, 0xFF, 0xFF), # 14 cyan
(0xFF, 0xFF, 0xFF), # 15 white
]
def new_canvas(w=320, h=200):
"""Returns a fresh w x h indexed PIL image with the palette installed."""
img = Image.new('P', (w, h), 0)
flat = []
for r, g, b in PALETTE_RGB:
flat.extend([r, g, b])
flat.extend([0] * (768 - len(flat))) # pad to 256 entries
img.putpalette(flat)
return img
def write_indexed_png(img, path):
"""Save with mode='P' so PNG output is indexed (joeyasset wants this
when converted through ppm; ImageMagick's `convert` preserves
indexed mode when writing PPM)."""
img.save(path, 'PNG', optimize=True)
print(f" wrote {path}")
def gen_tilebank():
"""tilebank0.png: 320x200, 40x25 cells of 8x8 each (= 1000 tile
slots). The engine convention is:
index 0 = empty (non-solid)
index 1..63 = solid walls
index 64..127 = landing-pad surfaces
index 128..255 = decorative non-solid
We fill a few representative slots; the rest stay black so they
show as "empty" if a level references them.
"""
img = new_canvas()
px = img.load()
def draw_tile(idx, painter):
bx = (idx % 40) * 8
by = (idx // 40) * 8
painter(bx, by)
def block(color):
return lambda x, y: [px.__setitem__((x+dx, y+dy), color)
for dx in range(8) for dy in range(8)]
def bricks(color_a, color_b):
def paint(x, y):
for dy in range(8):
row_offset = 4 if (y // 8 + dy // 4) % 2 == 0 else 0
for dx in range(8):
is_grout = (dx == (row_offset % 8)) or (dy == 3 or dy == 7)
px[x + dx, y + dy] = color_b if is_grout else color_a
return paint
def stripes(color_a, color_b):
def paint(x, y):
for dy in range(8):
c = color_a if (dy + (x // 8)) % 2 == 0 else color_b
for dx in range(8):
px[x + dx, y + dy] = c
return paint
def landing_pad(color_a, color_b):
# diagonal stripes signal a landing-pad surface
def paint(x, y):
for dy in range(8):
for dx in range(8):
px[x + dx, y + dy] = color_a if ((dx + dy) // 2) % 2 == 0 else color_b
return paint
# tile 0 -> already black (empty); leave as-is
# walls 1..7: a variety of brick/stripe patterns
draw_tile(1, bricks(6, 0)) # gray brick
draw_tile(2, bricks(4, 0)) # brown brick
draw_tile(3, block(6)) # solid gray
draw_tile(4, block(4)) # solid brown
draw_tile(5, stripes(6, 7)) # gray stripes (ceiling)
draw_tile(6, stripes(1, 8)) # blue stripes (sky deco)
draw_tile(7, block(7)) # solid light gray
# landing pad tiles 64..67
draw_tile(64, landing_pad(5, 12)) # yellow/orange pad
draw_tile(65, landing_pad(11, 3)) # green pad
draw_tile(66, landing_pad(9, 8)) # blue pad
draw_tile(67, landing_pad(15, 7)) # white pad
write_indexed_png(img, os.path.join(ROOT, 'tiles', 'tilebank0.png'))
def gen_sprites():
"""RETIRED 2026-07-20: sprites.png is now fully extracted from the
C64 dump by stuff/spacetaxi/extractSprites.py (taxi $C1/$C0/$DC/
$DD, passenger $C4-$CB/$D9, flame $6DB0 table, warp $E2-$E8 --
216x96, 4 rows). Regenerating placeholder art here would clobber
the real sheet, the same trap gen_font has with the hand-edited
font.png. Refuses to run; use extractSprites.py instead."""
print("gen_sprites: REFUSING -- sprites.png is real C64 art now.")
print(" regenerate via: python3 stuff/spacetaxi/extractSprites.py "
"stuff/spacetaxi/raw.bin examples/spacetaxi/assets/sprites/sprites.png")
return
def gen_sprites_retired():
img = new_canvas(9 * 24, 3 * 24) # 216x72: 9 cols x 3 rows of 24x24
d = ImageDraw.Draw(img)
# ---- Taxi cels: simple side-view "pod with thruster" ----
# Cel 0 = idle (thruster off)
# Cel 1..3 = thrust-on, alternating flame frames
for cel in range(4):
x = cel * 24
# Pod body: rounded rectangle in gray
d.rectangle([x+2, 4, x+21, 16], fill=7, outline=15)
# Cockpit window in blue
d.rectangle([x+13, 6, x+19, 10], fill=8, outline=15)
# Landing skid
d.line([(x+3, 17), (x+20, 17)], fill=6)
d.line([(x+3, 17), (x+3, 19)], fill=6)
d.line([(x+20,17), (x+20, 19)], fill=6)
# Thruster flame (cels 1..3 only, alternating shape)
if cel >= 1:
flame_color = [2, 12, 5][(cel - 1) % 3]
flame_top = 18
flame_bot = 22 if cel != 2 else 23
d.polygon([(x+8, flame_top),
(x+15, flame_top),
(x+12, flame_bot)], fill=flame_color)
# ---- Taxi shrink cels (transporter warp): the cab receding into the
# distance, 5 progressively smaller sizes, centered in the 24x24 cell.
# Placed in the free row-0 slots (cels 4..8). No flame -- it is warping
# away, not thrusting. The renderer draws one per warp step ($E2..$E9
# on the C64 = 8 shrink cels; the port approximates with 5 sizes).
for i in range(5):
cx = (4 + i) * 24 + 12
scale = 1.0 - i * 0.19 # 1.0, 0.81, 0.62, 0.43, 0.24
bw = max(3, int(round(19 * scale)))
bh = max(2, int(round(12 * scale)))
x0 = cx - bw // 2
y0 = 12 - bh // 2
# Pod body (gray, white outline)
d.rectangle([x0, y0, x0 + bw, y0 + bh], fill=7, outline=15)
# Cockpit window (blue) while big enough to show
if bw >= 8:
d.rectangle([x0 + bw - 5, y0 + 1, x0 + bw - 1, y0 + 3], fill=8)
# ---- Passenger cels: small stick figure, walk cycle ----
# Centered in 16x16, walking right.
for cel in range(4):
x = cel * 16
y = 24
# Head
d.ellipse([x+5, y+1, x+10, y+6], fill=10, outline=15)
# Body
d.line([(x+7, y+7), (x+7, y+12)], fill=15)
# Arms (swing alternately based on cel)
if cel == 0 or cel == 2:
d.line([(x+4, y+10), (x+10, y+10)], fill=15)
else:
d.line([(x+3, y+9), (x+11, y+11)], fill=15)
# Legs (alternating stride)
if cel == 0 or cel == 2:
d.line([(x+7, y+12), (x+4, y+15)], fill=15)
d.line([(x+7, y+12), (x+10,y+15)], fill=15)
else:
d.line([(x+7, y+12), (x+5, y+15)], fill=15)
d.line([(x+7, y+12), (x+9, y+15)], fill=15)
write_indexed_png(img, os.path.join(ROOT, 'sprites', 'sprites.png'))
def gen_font():
"""font.png: 320x200 (40x25 grid of 8x8 ASCII glyphs).
Authored manually for the printable subset (0x20..0x7E).
Each glyph cell is at (ascii%40, ascii/40) when ascii<128.
Unfilled cells stay black (treated as TILE_NO_GLYPH at runtime).
WARNING: this generator is UPPERCASE-ONLY (lowercase maps to the
uppercase glyphs). The COMMITTED font.png has been hand-extended with
real lowercase glyphs, so running this clobbers it -- the HUD reverts
to all-caps. Regenerate SPRITES only (gen_sprites) unless you also add
the lowercase glyphs here first; then `git checkout` font.png.
"""
# A compact 5-pixel-wide bitmap font, drawn in 7 rows with 1px
# padding on all sides (so 5x7 active inside an 8x8 cell, baseline
# at row 6). The font supports digits, uppercase letters, space,
# and basic punctuation -- enough for HUD text. Lowercase letters
# map to the same glyphs as uppercase (room to add later).
GLYPHS = {
' ': [],
'!': ['..#..', '..#..', '..#..', '..#..', '.....', '..#..', '.....'],
'"': ['.#.#.', '.#.#.', '.....', '.....', '.....', '.....', '.....'],
'0': ['.###.', '#...#', '#..##', '#.#.#', '##..#', '#...#', '.###.'],
'1': ['..#..', '.##..', '..#..', '..#..', '..#..', '..#..', '.###.'],
'2': ['.###.', '#...#', '....#', '...#.', '..#..', '.#...', '#####'],
'3': ['.###.', '#...#', '....#', '..##.', '....#', '#...#', '.###.'],
'4': ['...#.', '..##.', '.#.#.', '#..#.', '#####', '...#.', '...#.'],
'5': ['#####', '#....', '####.', '....#', '....#', '#...#', '.###.'],
'6': ['.###.', '#....', '#....', '####.', '#...#', '#...#', '.###.'],
'7': ['#####', '....#', '...#.', '..#..', '..#..', '..#..', '..#..'],
'8': ['.###.', '#...#', '#...#', '.###.', '#...#', '#...#', '.###.'],
'9': ['.###.', '#...#', '#...#', '.####', '....#', '....#', '.###.'],
':': ['.....', '..#..', '.....', '.....', '.....', '..#..', '.....'],
'.': ['.....', '.....', '.....', '.....', '.....', '..#..', '.....'],
'-': ['.....', '.....', '.....', '#####', '.....', '.....', '.....'],
'/': ['....#', '...#.', '...#.', '..#..', '.#...', '.#...', '#....'],
'A': ['.###.', '#...#', '#...#', '#####', '#...#', '#...#', '#...#'],
'B': ['####.', '#...#', '#...#', '####.', '#...#', '#...#', '####.'],
'C': ['.###.', '#...#', '#....', '#....', '#....', '#...#', '.###.'],
'D': ['####.', '#...#', '#...#', '#...#', '#...#', '#...#', '####.'],
'E': ['#####', '#....', '#....', '####.', '#....', '#....', '#####'],
'F': ['#####', '#....', '#....', '####.', '#....', '#....', '#....'],
'G': ['.###.', '#...#', '#....', '#..##', '#...#', '#...#', '.###.'],
'H': ['#...#', '#...#', '#...#', '#####', '#...#', '#...#', '#...#'],
'I': ['.###.', '..#..', '..#..', '..#..', '..#..', '..#..', '.###.'],
'J': ['..###', '...#.', '...#.', '...#.', '...#.', '#..#.', '.##..'],
'K': ['#...#', '#..#.', '#.#..', '##...', '#.#..', '#..#.', '#...#'],
'L': ['#....', '#....', '#....', '#....', '#....', '#....', '#####'],
'M': ['#...#', '##.##', '#.#.#', '#.#.#', '#...#', '#...#', '#...#'],
'N': ['#...#', '##..#', '#.#.#', '#.#.#', '#..##', '#...#', '#...#'],
'O': ['.###.', '#...#', '#...#', '#...#', '#...#', '#...#', '.###.'],
'P': ['####.', '#...#', '#...#', '####.', '#....', '#....', '#....'],
'Q': ['.###.', '#...#', '#...#', '#...#', '#.#.#', '#..#.', '.##.#'],
'R': ['####.', '#...#', '#...#', '####.', '#.#..', '#..#.', '#...#'],
'S': ['.###.', '#...#', '#....', '.###.', '....#', '#...#', '.###.'],
'T': ['#####', '..#..', '..#..', '..#..', '..#..', '..#..', '..#..'],
'U': ['#...#', '#...#', '#...#', '#...#', '#...#', '#...#', '.###.'],
'V': ['#...#', '#...#', '#...#', '#...#', '.#.#.', '.#.#.', '..#..'],
'W': ['#...#', '#...#', '#...#', '#.#.#', '#.#.#', '##.##', '#...#'],
'X': ['#...#', '#...#', '.#.#.', '..#..', '.#.#.', '#...#', '#...#'],
'Y': ['#...#', '#...#', '.#.#.', '..#..', '..#..', '..#..', '..#..'],
'Z': ['#####', '....#', '...#.', '..#..', '.#...', '#....', '#####'],
}
img = new_canvas()
px = img.load()
def paint_glyph(ascii_code, glyph_rows):
col = ascii_code % 40
row = ascii_code // 40
bx = col * 8
by = row * 8
# active 5x7 area: cell-x 1..5, cell-y 0..6 (top row, 1px left pad)
for ry, line in enumerate(glyph_rows):
for rx, ch in enumerate(line):
if ch == '#':
px[bx + 1 + rx, by + ry] = 15 # white
for ascii_code in range(32, 127):
c = chr(ascii_code)
if c in GLYPHS:
paint_glyph(ascii_code, GLYPHS[c])
elif 'a' <= c <= 'z':
paint_glyph(ascii_code, GLYPHS[c.upper()])
write_indexed_png(img, os.path.join(ROOT, 'font.png'))
def main():
os.makedirs(os.path.join(ROOT, 'tiles'), exist_ok=True)
os.makedirs(os.path.join(ROOT, 'sprites'), exist_ok=True)
print("genPlaceholderArt: emitting 320x200 indexed PNGs...")
gen_tilebank()
gen_sprites()
gen_font()
print("done.")
if __name__ == '__main__':
main()

77
assets/genSongData.py Normal file
View file

@ -0,0 +1,77 @@
#!/usr/bin/env python3
# Bake assets/songs/song*.song into stSongData.h with JoeyLib's songbake
# (JOEYLIB names the checkout, ../joeylib by default).
#
# 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
# assets/sidToSong.py turns that into songbake text.
import os
import subprocess
import sys
import tempfile
here = os.path.dirname(os.path.abspath(__file__))
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
header = """// Space Taxi (C64) tunes as JoeyLib JYM1 streams. GENERATED by
// assets/genSongData.py -- do not hand-edit. The songs were extracted by
// running the game's own music player under a 6502 emulator
// (assets/sidCapture.py logs every SID write per PAL frame;
// assets/sidToSong.py turns that into songbake text in assets/songs/).
//
// Where the C64 plays them:
// BLOCKING via $44CF (the game spins until the tune ends; the port
// parks in ST_STATE_JINGLE with the sim frozen):
// 0..3 start of every screen, rotating on $5E9B ($6906)
// 4 level 25 start ($6935) and the bonus cab ($70B1)
// 5 game over ($5C1F), after the two-second GAME OVER! wait
// NON-BLOCKING via $CB02 (started and left to the IRQ player):
// 6 the high-score name entry screen ($4E35) -- no port screen yet
// 7 the high-score table ($4C1F, joystick UP on the title)
// 8 the level intro while the cab flies in ($459C); endless, so
// the source is trimmed to 30 s and looped
#ifndef ST_SONG_DATA_H
#define ST_SONG_DATA_H
#include <stdint.h>
"""
footer = """typedef struct {
const uint8_t *data;
uint16_t length;
} StSongT;
#define ST_SONG_COUNT %du
#define ST_SONG_BONUS 4u
#define ST_SONG_GAME_OVER 5u
#define ST_SONG_NAME_ENTRY 6u
#define ST_SONG_SCORES 7u
#define ST_SONG_INTRO 8u
static const StSongT kStSongs[ST_SONG_COUNT] = {
%s};
#endif // ST_SONG_DATA_H
"""
parts = []
for n in range(count):
src = os.path.join(here, "songs", "song%d.song" % n)
with tempfile.NamedTemporaryFile(suffix=".h", delete=False) as tmp:
path = tmp.name
subprocess.run([sys.executable, songbake, "-c", "gStSong%d" % n, src, path], check=True, stderr=subprocess.DEVNULL)
body = open(path).read()
os.unlink(path)
body = "\n".join(l for l in body.split("\n") if l.startswith("static") or l.startswith(" ") or l.startswith("}"))
parts.append(body.rstrip() + "\n\n")
table = "".join(" { gStSong%d, (uint16_t)sizeof(gStSong%d) },\n" % (n, n) for n in range(count))
with open(out, "w") as fp:
fp.write(header + "".join(parts) + footer % (count, table))
print("wrote", os.path.normpath(out))

113
assets/genSpeechPcm.py Executable file
View file

@ -0,0 +1,113 @@
#!/usr/bin/env python3
"""genSpeechPcm.py - bake Space Taxi's speech to 8-bit PCM.
Reads the C64 run-length speech streams from stSpeechData.c (the output
of extractSpeech.py) and writes speech.bin: every utterance decoded to
5 kHz sample bytes for each passenger pitch the game can select, in BOTH
polarities. The decode is the exact algorithm stAudio.c used to run per
sample at play time; on the 65816 that took 2.4 s of CPU per second of
speech (three run bytes per sample, each a multiply) and stalled the ride
for half a second on every "HEY TAXI". A 1-bit intermediate format came
next, but expanding bits still needed a per-byte loop (and an asm copy of
it on the IIgs); with sample bytes the player is a block copy.
Polarity: an utterance is decoded from a +100 start. The C64 keeps the
output level running from one utterance into the next, so a phrase whose
first word ends on the low level plays its second word inverted. Both
versions are stored and the player picks by the running parity.
speech.bin (little-endian):
"STSP" u8 version(3) u8 pitchBase u8 pitchCount u8 count
u8 format (0 = signed +/-100, 1 = 0x80-biased for the IIgs raw stream)
u8 polarities (1 or 2) u8 pad[2] u32 dataBytes
entries[pitchCount * count * polarities]: u8 code, u8 parity,
u16 samples, u32 offset -- polarity is the innermost index
(0 = as decoded, 1 = inverted). parity = 1 when the utterance
ends on the opposite level from the one it started on.
data: sample bytes
With --single only polarity 0 is written, halving the file; the player
negates the samples itself when the running parity calls for it. The
X68000 needs that: its Human68k floppy cannot spare the other 60 KB.
usage: genSpeechPcm.py stSpeechData.c speech.bin [--biased] [--single]
"""
import re
import struct
import sys
SID_HZ = 1022727 # ST_SID_HZ_NUM
RATE = 5000 # ST_SPEECH_RATE
CYC_PER_SAMPLE = SID_HZ // RATE
SYNC_CYCLES = SID_HZ // 60 // 8
CYCLE_TAIL = 33
SYNC = 0xFE
END = 0xFF
PITCH_BASE = 2 # stFare.c: stAudioSpeech(rng(3) + 2)
PITCH_COUNT = 3
def parseSource(path):
text = open(path).read()
idx = re.search(r"kStSpeechIndex\[[^\]]*\]\s*=\s*\{(.*?)\};", text, re.S).group(1)
entries = [(int(c, 16), int(o), int(n)) for c, o, n in
re.findall(r"\{\s*0x([0-9A-Fa-f]+)u,\s*(\d+)u,\s*(\d+)u\s*\}", idx)]
rle = re.search(r"kStSpeechRle\[[^\]]*\]\s*=\s*\{(.*?)\};", text, re.S).group(1)
data = bytes(int(x, 16) for x in re.findall(r"0x([0-9A-Fa-f]{2})", rle))
return entries, data
def decode(rle, perOn):
"""speechDecode() for one utterance from a +100 start, remain 0."""
remain = 0
level = 100
out = []
i = 0
n = len(rle)
while True:
while remain <= 0:
if i >= n:
return out, level
b = rle[i]
i += 1
if b == END:
i = n
continue
if b == SYNC:
remain += SYNC_CYCLES
continue
remain += perOn * b + CYCLE_TAIL
level = -100 if level > 0 else 100
out.append(level)
remain -= CYC_PER_SAMPLE
def main():
args = [a for a in sys.argv[1:] if not a.startswith("--")]
biased = "--biased" in sys.argv[1:]
single = "--single" in sys.argv[1:]
polarities = 1 if single else 2
src, dst = args[0], args[1]
entries, data = parseSource(src)
records = []
blob = bytearray()
for p in range(PITCH_COUNT):
perOn = 5 * (PITCH_BASE + p) + 12
for code, off, length in entries:
samples, level = decode(data[off:off + length], perOn)
parity = 1 if level < 0 else 0
for pol in range(polarities):
pcm = bytearray()
for s in samples:
v = -s if pol else s
pcm.append((v + 0x80) & 0xFF if biased else v & 0xFF)
records.append((code, parity, len(samples), len(blob)))
blob += pcm
with open(dst, "wb") as f:
f.write(b"STSP" + struct.pack("<BBBBBBBBI", 3, PITCH_BASE, PITCH_COUNT, len(entries), 1 if biased else 0, polarities, 0, 0, len(blob)))
for code, parity, n, off in records:
f.write(struct.pack("<BBHI", code, parity, n, off))
f.write(blob)
total = sum(n for _, _, n, _ in records)
print(f"{dst}: {len(entries)} utterances x {PITCH_COUNT} pitches x {polarities} "
f"polarit{'y' if polarities == 1 else 'ies'}, {total} samples, {len(blob)} data bytes, "
f"{'biased' if biased else 'signed'}")
if __name__ == "__main__":
main()

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 `stuff/spacetaxi/romToLevel.py` directly
C64 levels are emitted by `assets/romToLevel.py` directly
from a raw VICE dump of the original .prg.
## Naming

205
assets/romToLevel.py Normal file
View file

@ -0,0 +1,205 @@
#!/usr/bin/env python3
# Convert ROM-extracted Space Taxi scenes directly into the runtime
# 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.
#
# Replaces the .txt -> mkstlevel pipeline for canonical levels. The C
# mkstlevel tool stays in tree for hand-authored .txt levels.
#
# Usage:
# romToLevel.py <raw-dump> <out-dir> # all 24 levels
# romToLevel.py <raw-dump> <out-dir> <scene_id> # one specific
#
# Writes:
# <out-dir>/level01.dat ... level24.dat (scene 0 = "A" = level01)
# <out-dir>/tbank0.png (charset extracted from $2800)
import os
import struct
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from cpu6502 import Cpu6502
from dumpScenes import load_dump, run_scene
# STL2 binary layout (see stLevel.c):
# "STL2"
# nameLen + name
# tileBank, music, bgColor, borderColor, taxiSpawnX, taxiSpawnY
# xAccel, yAccel, xGrav (int8), yGrav (int8)
# bgColor1, bgColor2, bgColor3, spriteMc0, spriteMc1, sprite0Color, sprite1Color
# padCount + padCount * (letter, tileX, tileY, tileW)
# fareCount + fareCount * (spawnPad, destPad)
# tilemap[40 * 25]
# colormap[40 * 25]
ST_TILEMAP_W = 40
ST_PLAYFIELD_ROWS = 25
ST_TILE_PX = 8
# VIC-II sprite-register origins: sprite-X $18 (24) is display column 0,
# sprite-Y $32 (50) is the first visible raster line. All in-game pad/taxi
# positions are stored in this sprite-register space, so tile conversion
# subtracts these origins. PAD_SURFACE_ORIGIN folds in the ~14px landing-
# gear offset (50-14=36) so a pad's row byte maps to its contact-surface
# tile row rather than the sprite-top row.
VIC_X_ORIGIN = 24
VIC_Y_ORIGIN = 50
PAD_SURFACE_ORIGIN = 36
LEVEL_LETTERS = [chr(ord('A') + i) for i in range(24)]
def write_level(path, level_index, after, screen, colormap, sprite_blocks):
"""Write an STL4 level .dat straight from post-decompression RAM.
Everything stays in the C64's own units so the runtime can mirror the
original routines byte for byte:
name[22] $7D78-$7D8D level name (screen codes, ASCII-compatible)
header[9] $7D00-$7D08 VIC colours: border, bg, bg1-3, mc0, mc1,
sprite-0, sprite-1
padCount $7D5A pads in the table
specialPad $7D09 fare pads (== padCount, or padCount-1 when
the last pad is the fuel station)
fuelRate $7D60 ticks of flight per half fuel cell
spawn[5] $7D5B-$7D5F X frac, X lo, X hi, Y frac, Y hi (sprite coords)
templates[8] $7D8F-$7D96 Y accel, X accel, Y gravity, X gravity
(16-bit little-endian each)
pads[10][8] $7D0A-$7D59 raw slots: x1 hi/lo, x2 hi/lo, row,
passenger X frac, passenger X col, unused
tilemap[1000] screen RAM (all 25 rows, the HUD rows included)
colormap[1000] colour RAM low nibbles
levelIndex 0..23 = A..X, 24 = title
spriteCount N, then N x (ptr, 63 bytes): the per-level sprite
bitmaps the level's extra scene loads at $2000+
hookData[616] $7D98-$7FFF: the level's hook code and tables
"""
cells = ST_TILEMAP_W * ST_PLAYFIELD_ROWS
assert len(screen) == cells, f'screen is {len(screen)}, need {cells}'
assert len(colormap) == cells, f'colormap is {len(colormap)}, need {cells}'
with open(path, 'wb') as fp:
fp.write(b'STL4')
fp.write(bytes(after[0x7D78:0x7D78 + 22]))
fp.write(bytes(after[0x7D00:0x7D09]))
fp.write(bytes([after[0x7D5A], after[0x7D09], after[0x7D60]]))
fp.write(bytes(after[0x7D5B:0x7D60]))
fp.write(bytes(after[0x7D8F:0x7D97]))
pad_count = min(after[0x7D5A], 10)
for pad_idx in range(10):
if pad_idx < pad_count:
off = 0x7D0A + pad_idx * 8
fp.write(bytes(after[off:off + 8]))
else:
fp.write(bytes(8))
fp.write(bytes(screen))
fp.write(bytes(colormap))
fp.write(bytes([level_index]))
fp.write(bytes([len(sprite_blocks)]))
for ptr, data in sprite_blocks:
fp.write(bytes([ptr]))
fp.write(bytes(data))
fp.write(bytes(after[0x7D98:0x8000]))
# Levels whose data blob flags an extra scene ($7D97 != 0) load a second
# scene from this table ($6F55, indexed by level) right after the level
# itself. Those extra scenes only write sprite bitmaps into $2000-$27FF
# (block pointers $80-$9F) for the level's hook code to animate.
_EXTRA_SCENE = [0, 0, 0, 0, 0, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0, 0x1F, 0, 0, 0,
0x20, 0x21, 0, 0, 0, 0x22, 0, 0, 0, 0x23]
def extract_one_level(ram, scene_id, out_dir):
"""Decompress `scene_id` and write out_dir/level<NN>.dat (NN = scene+1)."""
letter = LEVEL_LETTERS[scene_id]
screen_buf, color_buf, after = run_scene(ram, scene_id)
sprite_blocks = []
if after[0x7D97] != 0 and _EXTRA_SCENE[scene_id] != 0:
# Start the extra scene from a zeroed sprite window so the blocks
# it writes are exactly the ones the level ships with.
work = bytearray(after)
for i in range(0x2000, 0x2800):
work[i] = 0
_s, _c, after2 = run_scene(bytes(work), _EXTRA_SCENE[scene_id])
for ptr in range(0x80, 0xA0):
block = bytes(after2[ptr * 64:ptr * 64 + 63])
if any(block):
sprite_blocks.append((ptr, block))
# The level data itself (pads, hooks) is untouched by the extra
# scene; keep reading it from the level's own image.
cells = ST_TILEMAP_W * ST_PLAYFIELD_ROWS
screen = list(screen_buf[:cells])
colormap = [(b & 0x0F) for b in color_buf[:cells]]
level_num = scene_id + 1
out_path = os.path.join(out_dir, f'level{level_num:02d}.dat')
write_level(out_path, scene_id, after, screen, colormap, sprite_blocks)
return out_path, after[0x7D5A], len(sprite_blocks)
def extract_screen_y(ram, out_dir):
"""Screen Y, the 25th screen ("MYSTERY SCREEN !!!"), is scene 24.
$5010 dispatches on the screens-completed counter: at 24 it jumps to
$5167, which loads #$59 ('Y') as the pending screen; the shift only
ends when the counter reaches 25. So a full 24-hour shift plays A..X
and then Y -- the 25 screens the intro advertises.
"""
screen_buf, color_buf, after = run_scene(ram, 24)
cells = ST_TILEMAP_W * ST_PLAYFIELD_ROWS
screen = list(screen_buf[:cells])
colormap = [(b & 0x0F) for b in color_buf[:cells]]
out_path = os.path.join(out_dir, 'level25.dat')
write_level(out_path, 24, after, screen, colormap, [])
return out_path
def extract_title(ram, out_dir):
"""The title screen is scene 25 (what $477A decompresses).
Pads, spawn and templates are dummies."""
screen_buf, color_buf, after = run_scene(ram, 25)
cells = ST_TILEMAP_W * ST_PLAYFIELD_ROWS
screen = list(screen_buf[:cells])
colormap = [(b & 0x0F) for b in color_buf[:cells]]
out_path = os.path.join(out_dir, 'title.dat')
# The title scene writes no level name, so $7D78 still holds whatever
# the previously decompressed scene left there (PUZZLER, level H).
# The title never draws the name; ship it blank rather than stale.
work = bytearray(after)
work[0x7D78:0x7D78 + 22] = b' ' * 22
write_level(out_path, 25, work, screen, colormap, [])
return out_path
def main():
if len(sys.argv) < 3:
print('usage: romToLevel.py <raw-dump> <out-dir> [scene_id]', file=sys.stderr)
sys.exit(2)
ram = load_dump(sys.argv[1])
out_dir = sys.argv[2]
os.makedirs(out_dir, exist_ok=True)
if len(sys.argv) >= 4:
scene_ids = [int(sys.argv[3], 0)]
else:
# Levels A..X = scenes 0..23. The title comes from screen RAM
# at $0400 (the dump was taken on the title screen), not from
# a decompressed scene.
scene_ids = list(range(24))
for sid in scene_ids:
path, pad_count, nspr = extract_one_level(ram, sid, out_dir)
size = os.path.getsize(path)
print(f' scene {sid:2d} (level {LEVEL_LETTERS[sid]}): pads={pad_count} sprites={nspr} size={size} -> {path}')
# Screen Y and the title are not part of the A..X sweep.
if len(sys.argv) < 4:
y_path = extract_screen_y(ram, out_dir)
print(f' scene 24 (screen Y) : -> {y_path} ({os.path.getsize(y_path)} bytes)')
title_path = extract_title(ram, out_dir)
print(f' title (screen RAM $0400) : -> {title_path} ({os.path.getsize(title_path)} bytes)')
if __name__ == '__main__':
main()

57
assets/sidCapture.py Normal file
View file

@ -0,0 +1,57 @@
#!/usr/bin/env python3
"""Run the C64 Space Taxi music player and log every SID write per frame.
The player lives in the TITLE memory image at $CB00-$CFFF (its own relocated
copy; the $7500 copy is gameplay-side). Rather than hand-decode its byte
code, drive the real routines under cpu6502.py -- the same trick that
extracted the speech:
$CB00 select song ($CB01 = song number) and init (hooks the IRQ vector)
$CF52 the IRQ body: first frame calls start ($CC28), then ticks the three
voices via $CC6C, then JMPs to the KERNAL IRQ ($EA31) which is what
advances the jiffy clock $A2. We poke an RTS at $EA31 and bump $A2
ourselves, once per frame.
A song is over when all three voice stream pointers' high bytes are zero
(that is exactly what the game's play-and-wait loop at $44D5 spins on).
"""
import json, os, sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from cpu6502 import Cpu6502
RAM = bytearray(open(sys.argv[1], "rb").read()[:65536])
OUT = sys.argv[2]
MAX_FRAMES = 4000
class SidTrap(Cpu6502):
def __init__(self, ram):
super().__init__(ram)
self.log = []
self.frame = 0
def write8(self, addr, val):
a = addr & 0xFFFF
if 0xD400 <= a <= 0xD418:
self.log.append((self.frame, a - 0xD400, val & 0xFF))
super().write8(addr, val)
songs = {}
for song in range(0, 9):
ram = bytearray(RAM)
ram[0xEA31] = 0x60 # KERNAL IRQ tail -> RTS
cpu = SidTrap(ram)
cpu.call(0xCC00) # full reset first
ram[0xCB01] = song
cpu.call(0xCB00) # select + arm
for frame in range(MAX_FRAMES):
cpu.frame = frame
ram[0xA2] = (ram[0xA2] + 1) & 0xFF
cpu.call(0xCF52)
if ram[0xCB81] == 0 and ram[0xCB88] == 0 and ram[0xCB8F] == 0:
break
songs[song] = {"frames": frame + 1, "writes": cpu.log,
"ptr": ram[0xCB40 + song * 2] | (ram[0xCB41 + song * 2] << 8)}
print(f"song {song}: ptr=${songs[song]['ptr']:04X} frames={frame+1} "
f"({(frame+1)/50:.1f}s PAL) sidWrites={len(cpu.log)}")
json.dump(songs, open(OUT, "w"))

111
assets/sidToSong.py Normal file
View file

@ -0,0 +1,111 @@
#!/usr/bin/env python3
"""SID register log (sidCapture.py) -> JoeyLib .song text for songbake.
One row per PAL frame (tickms 20). Per voice, a note-on is a gate rising
edge or a pitch change while gated; gate falling is 'off'. SID frequency ->
Hz exactly (@Hz cells), so no equal-temperament rounding. Attenuation comes
from the sustain nibble. A voice whose waveform is NOISE goes to the noise
column instead of a tone column, with a pitch scaled from its frequency.
Loop detection: the player is deterministic, so once the (frame-relative)
event rows repeat with a fixed period, that period is the loop.
"""
import json, sys
PAL_CLOCK = 985248.0
songs = json.load(open(sys.argv[1]))
outDir = sys.argv[2]
def sidHz(lo, hi):
return ((hi << 8) | lo) * PAL_CLOCK / 16777216.0
def rowsFor(writes, nframes):
# replay register writes frame by frame, producing per-frame voice state
regs = [0] * 0x19
state = []
byFrame = {}
for f, r, v in writes:
byFrame.setdefault(f, []).append((r, v))
for f in range(nframes):
for r, v in byFrame.get(f, []):
regs[r] = v
vs = []
for vc in range(3):
b = vc * 7
vs.append(dict(hz=sidHz(regs[b], regs[b + 1]), gate=regs[b + 4] & 1,
wave=regs[b + 4] & 0xF0, sus=regs[b + 6] >> 4))
state.append(vs)
return state
def atten(sus):
# SID sustain 0..15 -> AGI attenuation 14..0 (0 loudest)
return max(0, min(14, round(14 - sus * 14 / 15)))
def noisePitch(hz):
# brighter hiss for higher SID "frequency"; 0..31, 0 = brightest
p = int(31 - min(31, hz / 400.0))
return max(0, min(31, p))
def toRows(state):
rows = []
prev = [dict(hz=None, gate=0, wave=0, sus=0) for _ in range(3)]
for vs in state:
tone = ["---", "---", "---"]
noise = "---"
for vc in range(3):
cur, was = vs[vc], prev[vc]
isNoise = (cur["wave"] & 0x80) != 0
on = cur["gate"] and (not was["gate"] or abs(cur["hz"] - was["hz"]) > 0.5
or cur["wave"] != was["wave"])
off = was["gate"] and not cur["gate"]
if on:
if isNoise:
noise = f"N{noisePitch(cur['hz'])}:{atten(cur['sus'])}"
else:
hz = max(1, min(65535, int(round(cur["hz"]))))
tone[vc] = f"@{hz}:{atten(cur['sus'])}"
elif off:
if (was["wave"] & 0x80):
noise = "off"
else:
tone[vc] = "off"
prev[vc] = dict(cur)
rows.append((tone, noise))
return rows
def findLoop(rows):
# smallest (offset, period) with rows[o+k] == rows[o+p+k] for the tail
n = len(rows)
for p in range(8, n // 2):
for o in range(0, n - 2 * p):
if rows[o:o + p] == rows[o + p:o + 2 * p] and rows[o:n - p] == rows[o + p:n]:
return o, p
return None
for num, s in sorted(songs.items(), key=lambda kv: int(kv[0])):
num = int(num)
st = rowsFor(s["writes"], s["frames"])
rows = toRows(st)
loop = findLoop(rows) if s["frames"] >= 4000 else None
if loop:
o, p = loop
rows = rows[:o + p]
lines = ["; Space Taxi (C64) song %d, extracted by running the game's own" % num,
"; player ($CB00/$CF52) under cpu6502.py and logging SID writes.",
"; One row = one PAL frame.", "tickms 20"]
for i, (tone, noise) in enumerate(rows):
if loop and i == loop[0]:
lines.append("loop")
cells = tone + ([noise] if noise != "---" or True else [])
lines.append(" ".join(cells))
open(f"{outDir}/song{num}.song", "w").write("\n".join(lines) + "\n")
events = sum(1 for t, n in rows for c in t + [n] if c != "---")
waves = sorted({hex(v["wave"]) for vs in st for v in vs if v["gate"]})
print(f"song {num}: {len(rows)} rows, {events} events, waveforms {waves}"
+ (f", LOOP at row {loop[0]} period {loop[1]}" if loop else ""))

94
assets/songs/song0.song Normal file
View file

@ -0,0 +1,94 @@
; Space Taxi (C64) song 0, extracted by running the game's own
; player ($CB00/$CF52) under cpu6502.py and logging SID writes.
; One row = one PAL frame.
tickms 20
--- --- --- ---
@336:2 @424:2 --- ---
--- --- --- ---
--- --- --- ---
off off --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@378:2 @424:2 --- ---
--- --- --- ---
--- --- --- ---
off off --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@336:2 @424:2 --- ---
--- --- --- ---
--- --- --- ---
off off --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@378:2 @476:2 --- ---
--- --- --- ---
--- --- --- ---
off off --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@318:2 @252:2 --- ---
--- --- --- ---
--- --- --- ---
--- off --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @476:2 --- ---
--- --- --- ---
--- --- --- ---
off off --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@283:2 @252:2 --- ---
--- --- --- ---
--- --- --- ---
--- off --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @476:2 --- ---
--- --- --- ---
--- --- --- ---
off off --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@252:2 @252:2 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---

94
assets/songs/song1.song Normal file
View file

@ -0,0 +1,94 @@
; Space Taxi (C64) song 1, extracted by running the game's own
; player ($CB00/$CF52) under cpu6502.py and logging SID writes.
; One row = one PAL frame.
tickms 20
--- --- --- ---
@252:7 @159:7 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off off --- ---
@283:7 @168:7 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off off --- ---
@252:7 @189:7 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off off --- ---
@283:7 @212:7 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off off --- ---
@318:7 @189:7 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off off --- ---
@336:7 @168:7 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off off --- ---
@318:7 @159:7 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off off --- ---
@336:7 @168:7 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off off --- ---
@378:7 @189:7 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off --- --- ---
--- --- --- ---

86
assets/songs/song2.song Normal file
View file

@ -0,0 +1,86 @@
; Space Taxi (C64) song 2, extracted by running the game's own
; player ($CB00/$CF52) under cpu6502.py and logging SID writes.
; One row = one PAL frame.
tickms 20
--- --- --- ---
@126:0 @159:0 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off --- ---
--- --- --- ---
off --- --- ---
@126:0 @168:0 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off --- ---
--- --- --- ---
off --- --- ---
@141:0 @189:0 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off --- ---
--- --- --- ---
off --- --- ---
@159:0 @159:0 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off --- ---
--- --- --- ---
off --- --- ---
@159:0 @168:0 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off --- ---
--- --- --- ---
off --- --- ---
@168:0 @189:0 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off --- ---
--- --- --- ---
off --- --- ---
@168:0 @159:0 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off --- ---
--- --- --- ---
off --- --- ---
@189:0 @168:0 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off --- ---
--- --- --- ---
off --- --- ---
--- @159:0 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off --- ---
--- --- --- ---
--- --- --- ---
--- @126:0 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---

86
assets/songs/song3.song Normal file
View file

@ -0,0 +1,86 @@
; Space Taxi (C64) song 3, extracted by running the game's own
; player ($CB00/$CF52) under cpu6502.py and logging SID writes.
; One row = one PAL frame.
tickms 20
--- --- --- ---
@252:0 @336:2 @336:2 ---
--- --- --- ---
--- --- --- ---
off off off ---
--- --- --- ---
@283:0 --- --- ---
--- --- --- ---
--- --- --- ---
off --- --- ---
--- --- --- ---
@318:0 @336:2 @336:2 ---
--- --- --- ---
--- --- --- ---
off off off ---
--- --- --- ---
@252:0 --- --- ---
--- --- --- ---
--- --- --- ---
off --- --- ---
--- --- --- ---
@283:0 @336:2 @336:2 ---
--- --- --- ---
--- --- --- ---
off off off ---
--- --- --- ---
@252:0 --- --- ---
--- --- --- ---
--- --- --- ---
off --- --- ---
--- --- --- ---
@283:0 @336:2 @336:2 ---
--- --- --- ---
--- --- --- ---
off off off ---
--- --- --- ---
@476:0 --- --- ---
--- --- --- ---
--- --- --- ---
off --- --- ---
--- --- --- ---
--- @318:2 @378:2 ---
--- --- --- ---
--- --- --- ---
--- off off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @336:2 @378:2 ---
--- --- --- ---
--- --- --- ---
--- off off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @283:2 @252:2 ---
--- --- --- ---
--- --- --- ---
--- off off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @252:2 @476:2 ---
--- --- --- ---
--- --- --- ---
--- off off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---

126
assets/songs/song4.song Normal file
View file

@ -0,0 +1,126 @@
; Space Taxi (C64) song 4, extracted by running the game's own
; player ($CB00/$CF52) under cpu6502.py and logging SID writes.
; One row = one PAL frame.
tickms 20
--- --- --- ---
@504:5 @449:5 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off off --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@504:5 @449:5 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off off --- ---
@673:5 @504:5 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off off --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@504:5 @424:5 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off off --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@673:5 @504:5 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off off --- ---
@848:5 @673:5 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @635:5 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @673:5 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off off --- ---
--- --- --- ---

736
assets/songs/song5.song Normal file
View file

@ -0,0 +1,736 @@
; Space Taxi (C64) song 5, extracted by running the game's own
; player ($CB00/$CF52) under cpu6502.py and logging SID writes.
; One row = one PAL frame.
tickms 20
--- --- --- ---
@504:2 @159:2 @106:10 ---
--- --- --- ---
--- --- --- ---
--- off --- ---
--- --- --- ---
--- --- @212:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @159:2 @106:10 ---
--- --- --- ---
--- --- --- ---
--- off --- ---
--- --- --- ---
--- --- @212:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @159:2 @106:10 ---
--- --- --- ---
--- --- --- ---
--- off --- ---
--- --- --- ---
--- --- @212:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @159:2 @106:10 ---
--- --- --- ---
--- --- --- ---
--- off --- ---
off --- --- ---
--- --- @212:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@504:2 @159:2 @106:10 ---
--- --- --- ---
--- --- --- ---
--- off --- ---
--- --- --- ---
--- --- @212:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @159:2 @106:10 ---
--- --- --- ---
--- --- --- ---
--- off --- ---
--- --- --- ---
--- --- @212:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @159:2 @106:10 ---
--- --- --- ---
--- --- --- ---
--- off --- ---
--- --- --- ---
--- --- @212:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @159:2 @106:10 ---
--- --- --- ---
--- --- --- ---
--- off --- ---
off --- --- ---
--- --- @212:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@504:2 @159:2 @106:10 ---
--- --- --- ---
--- --- --- ---
--- off --- ---
--- --- --- ---
--- --- @212:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @159:2 @106:10 ---
--- --- --- ---
--- --- --- ---
--- off --- ---
--- --- --- ---
--- --- @212:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @159:2 @106:10 ---
--- --- --- ---
--- --- --- ---
--- off --- ---
off --- --- ---
--- --- @212:10 ---
--- --- --- ---
--- --- --- ---
--- --- off ---
--- --- --- ---
@566:2 @141:2 @100:10 ---
--- --- --- ---
--- --- --- ---
--- off --- ---
off --- --- ---
--- --- @200:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@566:2 @141:2 @100:10 ---
--- --- --- ---
--- --- --- ---
--- off --- ---
--- --- --- ---
--- --- @200:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @141:2 @100:10 ---
--- --- --- ---
--- --- --- ---
--- off --- ---
--- --- --- ---
--- --- @200:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @141:2 @100:10 ---
--- --- --- ---
--- --- --- ---
--- off --- ---
--- --- --- ---
--- --- @200:10 ---
--- --- --- ---
--- --- --- ---
--- --- off ---
--- --- --- ---
--- @141:2 @100:10 ---
--- --- --- ---
--- --- --- ---
--- off --- ---
off --- --- ---
--- --- @200:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@635:2 @504:2 @94:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off off --- ---
--- --- @189:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- @94:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- @189:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@673:2 @566:2 @84:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off off --- ---
--- --- @168:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- @84:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- @168:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@755:2 @635:2 @79:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off off --- ---
--- --- @159:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- @79:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- @159:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@800:2 @635:2 @71:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off off --- ---
--- --- @141:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- @71:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- @141:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@848:2 @635:2 @67:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- @133:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- @67:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- @133:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- @67:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- @133:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- @67:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- @133:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@424:2 @318:2 @67:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off off --- ---
--- --- @133:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@848:2 @336:2 @252:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off off --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- off ---
--- --- --- ---
@848:2 @336:2 @252:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off off --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- off ---
--- --- --- ---
@848:2 @336:2 @252:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off off --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- off ---
--- --- --- ---
@755:2 @318:2 @225:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off off --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- off ---
--- --- --- ---
@755:2 @318:2 @225:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off off --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- off ---
--- --- --- ---
@673:2 @283:2 @212:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- off ---
--- --- --- ---
@635:2 @252:2 @189:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off off --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@566:2 @283:2 @168:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off off --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@755:2 @318:2 @126:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- @189:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- @126:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- @189:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- @126:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- @189:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- @126:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- @189:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- @126:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off --- --- ---
--- --- @189:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@635:2 --- @126:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off --- --- ---
--- --- @189:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@504:2 --- @126:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off --- --- ---
--- --- @189:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@635:2 --- @126:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off off --- ---
--- --- @189:10 ---
--- --- --- ---
--- --- --- ---
--- --- off ---
--- --- --- ---
@755:2 @283:2 @119:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- @189:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- @119:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off --- --- ---
--- --- @189:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@848:2 --- @119:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- @189:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- @119:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off --- --- ---
--- --- @189:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@755:2 --- @119:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off --- --- ---
--- --- @189:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@673:2 --- @119:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off --- --- ---
--- --- @189:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@635:2 --- @119:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off --- --- ---
--- --- @189:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@566:2 --- @119:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off off --- ---
--- --- @189:10 ---
--- --- --- ---
--- --- --- ---
--- --- off ---
--- --- --- ---
@504:2 @318:2 @126:10 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off off --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- off ---
--- --- --- ---
--- --- --- ---

840
assets/songs/song6.song Normal file
View file

@ -0,0 +1,840 @@
; Space Taxi (C64) song 6, extracted by running the game's own
; player ($CB00/$CF52) under cpu6502.py and logging SID writes.
; One row = one PAL frame.
tickms 20
--- --- --- ---
--- @378:1 @504:1 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @378:1 @504:1 ---
--- --- --- ---
--- off off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @378:1 @504:1 ---
--- --- --- ---
--- off off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @378:1 @504:1 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@94:0 --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@126:0 --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@159:0 --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@189:0 @336:1 @449:1 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @300:1 @400:1 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @336:1 @449:1 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @378:1 @504:1 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @378:1 @504:1 ---
--- --- --- ---
--- off off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off @378:1 @504:1 ---
--- --- --- ---
--- off off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @378:1 @504:1 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@212:0 --- --- ---
--- --- --- ---
--- --- --- ---
off --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@189:0 --- --- ---
--- --- --- ---
--- --- --- ---
off --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@159:0 --- --- ---
--- --- --- ---
--- --- --- ---
off --- --- ---
--- --- --- ---
--- --- --- ---
--- @378:1 @504:1 ---
--- --- --- ---
--- off off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @378:1 @504:1 ---
--- --- --- ---
--- off off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@141:0 @424:1 @566:1 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @378:1 @504:1 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @424:1 @566:1 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off --- --- ---
--- --- --- ---
--- off off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@119:0 @476:1 @635:1 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @476:1 @635:1 ---
--- --- --- ---
--- off off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @476:1 @635:1 ---
--- --- --- ---
--- off off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @476:1 @635:1 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@59:0 --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @476:1 @635:1 ---
--- --- --- ---
--- off off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @476:1 @635:1 ---
--- --- --- ---
--- off off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @476:1 @635:1 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @424:1 @566:1 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off --- --- ---
--- --- --- ---
--- off off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @476:1 @635:1 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@126:0 @504:1 @673:1 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @504:1 @673:1 ---
--- --- --- ---
--- off off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @504:1 @673:1 ---
--- --- --- ---
--- off off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @504:1 @673:1 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@63:0 --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @504:1 @673:1 ---
--- --- --- ---
--- off off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off @504:1 @673:1 ---
--- --- --- ---
--- off off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@126:0 @566:1 @755:1 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @504:1 @673:1 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off --- --- ---
--- --- --- ---
--- off off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @566:1 @755:1 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@141:0 @635:1 @848:1 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off --- --- ---
--- --- --- ---
--- --- off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@71:0 --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @635:1 @848:1 ---
--- --- --- ---
--- off off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off @635:1 @848:1 ---
--- --- --- ---
--- off off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@141:0 @566:1 @848:1 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @566:1 @848:1 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @566:1 @848:1 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off --- --- ---
--- --- --- ---
--- off off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@159:0 @534:1 @848:1 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off --- off ---
--- --- --- ---
--- off --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---

852
assets/songs/song7.song Normal file
View file

@ -0,0 +1,852 @@
; Space Taxi (C64) song 7, extracted by running the game's own
; player ($CB00/$CF52) under cpu6502.py and logging SID writes.
; One row = one PAL frame.
tickms 20
--- --- --- ---
@378:0 @1270:5 @126:7 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @1346:5 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @1270:5 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off --- ---
--- @1132:5 @119:7 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @1270:5 off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @1346:5 --- ---
--- --- --- ---
--- --- --- ---
off --- --- ---
--- off --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@252:0 @1270:5 @106:7 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @1346:5 off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @1510:5 --- ---
--- --- --- ---
--- --- --- ---
off --- --- ---
--- off --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@283:0 @1008:5 @94:7 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@318:0 --- off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@336:0 --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @1008:5 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@378:0 --- @126:7 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @952:5 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @848:5 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @755:5 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @1132:5 @119:7 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @1270:5 off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @1346:5 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @1510:5 @126:7 ---
--- --- --- ---
--- --- --- ---
off --- --- ---
--- off --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @1510:5 off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @1510:5 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@424:0 @1132:5 @94:7 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off --- ---
@378:0 --- off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@336:0 @1132:5 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@378:0 @1270:5 @126:7 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off --- ---
--- --- off ---
--- --- --- ---
--- --- --- ---
off --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@336:0 @952:5 @119:7 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off --- ---
@318:0 --- off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@283:0 @952:5 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@318:0 @1008:5 @106:7 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off --- ---
--- --- off ---
--- --- --- ---
--- --- --- ---
off --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@283:0 @755:5 @94:7 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off --- ---
@252:0 --- off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@238:0 @755:5 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off --- ---
@252:0 @1008:5 @126:7 ---
--- --- --- ---
--- --- --- ---
off --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @952:5 off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@318:0 @1008:5 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off --- ---
@189:0 @1132:5 @119:7 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @1008:5 off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @1132:5 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @1270:5 @106:7 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @1132:5 off ---
--- --- --- ---
--- --- --- ---
off --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @1270:5 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@238:0 @1346:5 @94:7 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@252:0 @1270:5 off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@283:0 @1346:5 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off --- ---
@252:0 @1270:5 @126:7 ---
--- --- --- ---
--- --- --- ---
off --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off --- ---
--- --- off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @1270:5 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@238:0 @1510:5 @119:7 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@252:0 --- off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@283:0 --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@318:0 --- @106:7 ---
--- --- --- ---
--- --- --- ---
off --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off --- ---
--- --- off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@283:0 @1510:5 @94:7 ---
--- --- --- ---
--- --- --- ---
off --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off --- ---
@283:0 --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@378:0 @1132:5 @141:7 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off --- ---
--- --- off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @1008:5 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @952:5 @119:7 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @1008:5 off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @1132:5 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @755:5 @106:7 ---
--- --- --- ---
--- --- --- ---
off --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off --- ---
--- --- off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@378:0 @1132:5 --- ---
off --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@378:0 --- --- ---
off --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@378:0 @1199:5 @94:7 ---
--- --- --- ---
--- --- --- ---
off --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@378:0 --- off ---
--- --- --- ---
--- --- --- ---
off --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off --- ---
@378:0 --- --- ---
--- --- --- ---
--- --- --- ---
off --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
@378:0 @1270:5 @126:7 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @1510:5 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @1270:5 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @1510:5 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @1270:5 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- @94:7 ---
--- @1510:5 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @1270:5 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @1510:5 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @1270:5 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @1510:5 --- ---
--- --- --- ---
--- --- --- ---
--- --- @126:7 ---
--- --- --- ---
--- @1270:5 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @1510:5 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @1270:5 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @1510:5 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @1270:5 --- ---
--- --- --- ---
--- --- @94:7 ---
--- --- --- ---
--- --- --- ---
--- @1510:5 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @1270:5 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @1510:5 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- @1270:5 --- ---
--- --- off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- @126:7 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- off --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
off --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- off ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---

1508
assets/songs/song8.song Normal file

File diff suppressed because it is too large Load diff

BIN
assets/sprites/sprites.png (Stored with Git LFS)

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 KiB

View file

@ -1,496 +0,0 @@
#!/usr/bin/env python3
# extractFromDump.py -- convert a VICE C64 memory dump into JoeyLib
# Space Taxi assets.
#
# Reads: /tmp/spacetaxi/mem0000-<labelN>.bin (VICE `save` output;
# 2-byte LE start-addr header + 64 KB RAM).
# Writes: examples/spacetaxi/assets/tiles/tilebank<N>.png
# examples/spacetaxi/assets/levels/level<NN>.txt
# examples/spacetaxi/assets/sprites/sprites.png (level1 only)
# examples/spacetaxi/assets/font.png (level1 only)
#
# Per-level:
# - Custom charset at $2800-$2FFF (2 KB, 256 chars * 8 bytes mono)
# becomes a tile bank PNG: 40 cols x 25 rows of 8x8 cells, with
# tile index N at cell (N%40, N/40). The PNG uses the EGA-ish
# 16-color palette we already install on stage.
# - Screen RAM at $0400-$07E7 (1000 bytes = 40x25 char codes) and
# color RAM at $D800-$DBE7 (1000 bytes = 40x25 palette nybbles)
# become the tilemap+colormap in the level .txt source.
#
# Sprite data at $3000-$37FF + sprite pointer table at $07F8-$07FF
# is decoded once (it's the same across levels for the active set):
# - sprite 0 / 7: the taxi cab body & shadow
# - sprite 1..6: passenger frames / animation cels
# 64 bytes per sprite, 24x21 1-bit, MSB-leftmost. Color comes from
# $D027-$D02E (one color per sprite).
#
# Usage:
# python3 extractFromDump.py stuff/spacetaxi/mem0000-level1.bin level01 1
# python3 extractFromDump.py stuff/spacetaxi/mem0000-level2.bin level02 2
# args: <dump-path> <level-name-without-extension> <tilebank-id>
#
# Dumps live in stuff/spacetaxi/ (committed-ish, repo-local) rather
# than /tmp so they survive reboots and stay with the project.
import os
import struct
import sys
from collections import Counter
from PIL import Image
# C64 standard color palette (EGA-ish 16-color JoeyLib mapping).
# Indices 0..15 match the C64 VIC-II color register order so a value
# read from $D8xx or $D02x maps directly to a JoeyLib palette slot.
# Colors below approximate Pepto's standard PAL palette in $RGB form.
C64_PALETTE_RGB = [
(0x00, 0x00, 0x00), # 0 black
(0xFF, 0xFF, 0xFF), # 1 white
(0x88, 0x39, 0x32), # 2 red
(0x67, 0xB6, 0xBD), # 3 cyan
(0x8B, 0x3F, 0x96), # 4 purple
(0x55, 0xA0, 0x49), # 5 green
(0x40, 0x31, 0x8D), # 6 blue
(0xBF, 0xCE, 0x72), # 7 yellow
(0x8B, 0x54, 0x29), # 8 orange
(0x57, 0x42, 0x00), # 9 brown
(0xB8, 0x69, 0x62), # 10 light red
(0x50, 0x50, 0x50), # 11 dark gray
(0x78, 0x78, 0x78), # 12 mid gray
(0x94, 0xE0, 0x89), # 13 light green
(0x78, 0x69, 0xC4), # 14 light blue
(0x9F, 0x9F, 0x9F), # 15 light gray
]
ROOT = os.path.dirname(os.path.abspath(__file__))
def load_dump(path):
with open(path, "rb") as f:
raw = f.read()
start = raw[0] | (raw[1] << 8)
if start != 0:
raise SystemExit(f"{path}: unexpected start ${start:04X}, want $0000")
if len(raw) - 2 != 0x10000:
raise SystemExit(f"{path}: not a 64K dump (got {len(raw) - 2} bytes)")
return raw[2:]
def new_canvas(w, h):
img = Image.new("P", (w, h), 0)
flat = bytearray()
for r, g, b in C64_PALETTE_RGB:
flat.extend((r, g, b))
flat.extend(b"\x00" * (768 - len(flat)))
img.putpalette(bytes(flat))
return img
def paint_charset_glyph(px, mem, src_code, bx, by):
"""Plot the 8x8 glyph for C64 screen code `src_code` at pixel
(bx, by) of the indexed-palette PIL image. Foreground = palette
index 1, background untouched (already index 0 from new_canvas).
"""
src = 0x2800 + src_code * 8
for r in range(8):
byte = mem[src + r]
for c in range(8):
if byte & (0x80 >> c):
px[bx + c, by + r] = 1
def extract_tilebank(mem, out_path, code_to_slot=None):
"""Render the custom charset at $2800-$2FFF as a 320x200 PNG.
When `code_to_slot` is None, lays out all 256 chars at their raw
C64 positions (40 cols x 25 rows). When `code_to_slot` is given,
writes each charset glyph at the slot the level loader expects
-- i.e. the bank tile at position N is the glyph for screen-code
`code_for_slot[N]`. That keeps level data and tilebank aligned
after the level extractor's frequency-sorted code remapping.
"""
img = new_canvas(320, 200)
px = img.load()
if code_to_slot is None:
for ch in range(256):
paint_charset_glyph(px, mem, ch, (ch % 40) * 8, (ch // 40) * 8)
else:
# Invert: slot -> code. Multiple codes can collide into slot 0
# (background); only the first one we see "owns" that slot for
# painting purposes -- the rest just fall through to the same
# blank tile. Highest priority is the explicit space mapping.
slot_to_code = {}
for code, slot in code_to_slot.items():
slot_to_code.setdefault(slot, code)
for slot, code in slot_to_code.items():
if slot >= 256:
continue
paint_charset_glyph(px, mem, code, (slot % 40) * 8, (slot // 40) * 8)
img.save(out_path, "PNG", optimize=True)
def build_code_to_slot(mem):
"""Build the screen-code -> alphabet-slot map shared between the
tilebank emission and the level emission. The slot index is what
the runtime sees in level.tilemap[]; the tilebank must mirror this
ordering so a level-data slot-N cell renders the charset glyph
that was actually used in the original screen RAM at that code.
"""
screen = mem[0x0400:0x0400 + 1000]
codes = sorted(Counter(screen).items(), key=lambda kv: (-kv[1], kv[0]))
code_to_slot = {}
next_slot = 0
for code, _count in codes:
if code == 0x20: # space -> always index 0
code_to_slot[code] = 0
continue
if code in code_to_slot:
continue
if next_slot >= len(ALPHABET):
code_to_slot[code] = 0
continue
# Reserve slot 0 for background (space). The very first
# non-space code claims slot 1, etc.
if next_slot == 0:
next_slot = 1
code_to_slot[code] = next_slot
next_slot += 1
return code_to_slot
def screencode_to_char(code):
"""Convert a C64 screen code into a printable tilemap character
using the encoding the .txt format already understands:
0 -> '.' (treated as 0)
1..9 -> '1'..'9' (digits encode the lower 10 indices)
but we shift to use up most of the index space below.
For arbitrary 0..255 we need a 256-symbol alphabet -- not feasible
in a single character per cell. So we encode as TWO chars per cell
using base-16 hex would still only cover 0..255. Decision: keep
.txt cells one-char and map screen codes via a custom 64-char
alphabet plus an @char directive table emitted into the file so
the loader knows exactly which screen code each char represents.
"""
# This function is unused as a one-char encoder; we encode via the
# generic alphabet below instead.
return code
# Generic 1-char alphabet (62 codes); the level loader's index map
# was: '.' or ' '=0, '0'..'9'=0..9, 'A'..'Z'=10..35, 'a'..'z'=36..61.
# That's only 62 codes, not 256. The original C64 levels use ~30
# distinct screen codes per level so most levels fit. We map the N
# most-frequent screen codes to those alphabet slots.
ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
def extract_level(mem, level_name, tilebank_id, out_path, code_to_alpha):
"""Render screen RAM + color RAM as an STL1-format .txt source
using the prebuilt screen-code -> alphabet-slot mapping. Caller
must pass the same mapping that was used to reorder the tilebank
PNG; otherwise the level tilemap and tilebank diverge.
"""
screen = mem[0x0400:0x0400 + 1000] # 40x25
color = mem[0xD800:0xD800 + 1000] # 40x25, low nybble = palette slot
codes = sorted(Counter(screen).items(), key=lambda kv: (-kv[1], kv[0]))
# Full 25-row C64 screen. The title screen uses all 25 rows
# (PUBLISHED ... SOFTWARE credit wraps across rows 21, 22, 24
# with frame chars interleaved). Gameplay HUD draws over the
# bottom 3 rows at runtime.
playfield_rows = 25
lines = []
lines.append(f"# Extracted from VICE memory dump. Edit at your own risk;")
lines.append(f"# regenerating will overwrite. Keep the @tile mappings in")
lines.append(f"# sync with the tilebank PNG (extracted from $2800).")
lines.append(f"")
lines.append(f"@name {level_name.upper()}")
lines.append(f"@tilebank {tilebank_id}")
lines.append(f"@music 0")
bg = mem[0xD021] & 0x0F
border = mem[0xD020] & 0x0F
lines.append(f"@bgColor {bg}")
lines.append(f"@borderColor {border}")
# Best-effort taxi spawn: middle-top of playfield. The original
# script-driven spawn position would need to be lifted out of
# game state at dump time; for now hand-edit if you want it
# to match the cracker's preferred spawn.
lines.append(f"@taxiSpawn 18 2")
lines.append(f"")
# Pads: scan the playfield for "landable surfaces" -- runs of
# >= 3 identical non-background tile cells with two or more
# empty rows directly above (so the taxi can approach from the
# top). Per-row scanning runs top-to-bottom; for the same
# surface tile across rows we only take the topmost contiguous
# platform edge.
bg_code = codes[0][0] if codes else 0x20
def is_bg(code):
return code == bg_code or code == 0x20
pads = []
seen_used = [False] * (playfield_rows * 40)
for row in range(2, playfield_rows):
line = screen[row * 40:(row + 1) * 40]
prev = screen[(row - 1) * 40:row * 40]
prev2 = screen[(row - 2) * 40:(row - 1) * 40]
col = 0
while col < 40 and len(pads) < 8:
if is_bg(line[col]):
col += 1
continue
j = col
while j < 40 and line[j] == line[col] and not seen_used[row * 40 + j]:
j += 1
width = j - col
# Approachable from above? At least 2/3 of cells in the
# 2 rows above this stretch must be background.
above_clear = 0
for k in range(col, j):
if is_bg(prev[k]):
above_clear += 1
if is_bg(prev2[k]):
above_clear += 1
if width >= 3 and above_clear >= width:
pads.append((row, col, width))
for k in range(col, j):
seen_used[row * 40 + k] = True
col = j
lines.append("# Pads auto-detected from landable surfaces (>= 3 wide,")
lines.append("# approachable from above). Verify and adjust as needed.")
pad_letter = ord("A")
for (row, col, width) in pads:
if pad_letter > ord("H"):
break
lines.append(f"@pad {chr(pad_letter)} {col} {row} {width}")
pad_letter += 1
if pad_letter == ord("A"):
lines.append("# (No landable surfaces auto-detected; emitting placeholders.)")
lines.append("@pad A 4 10 4")
lines.append("@pad B 32 10 4")
pad_letter = ord("C")
lines.append("")
# Fare list: simple round-robin between detected pads.
pad_count = pad_letter - ord("A")
if pad_count >= 2:
for k in range(pad_count):
src = chr(ord("A") + k)
dst = chr(ord("A") + ((k + 1) % pad_count))
lines.append(f"@fare {src} {dst} 45")
else:
lines.append("@fare A B 45")
lines.append("")
# Emit the @tile mapping for round-trip clarity (and so the
# author can read a level .txt and tell which screen code maps
# to which char). Format: @tile <char> <screencode> -- the
# mkstlevel parser ignores these today but they're useful
# documentation, and a future loader could honor them.
lines.append("# Screen-code -> alphabet-char mapping (one per unique code).")
inv = {v: k for k, v in code_to_alpha.items()}
for idx in sorted(inv):
ch = '.' if idx == 0 else ALPHABET[idx]
code = inv[idx]
lines.append(f"# @tile '{ch}' = $C64_screencode_{code:02X}")
lines.append("")
# Tilemap (22 rows of 40 chars each).
lines.append("@tilemap")
for r in range(playfield_rows):
row = screen[r * 40:(r + 1) * 40]
s = ""
for code in row:
idx = code_to_alpha.get(code, 0)
s += '.' if idx == 0 else ALPHABET[idx]
lines.append(s)
lines.append("")
lines.append("@colormap")
for r in range(playfield_rows):
row = color[r * 40:(r + 1) * 40]
s = "".join(
('0' if (c & 0x0F) == 0
else ALPHABET[(c & 0x0F)]
if (c & 0x0F) < len(ALPHABET) else '0')
for c in row
)
lines.append(s)
with open(out_path, "w") as fp:
fp.write("\n".join(lines) + "\n")
def extract_sprites(mem, out_path):
"""Render C64 hardware sprites into the JoeyLib-expected sprite
sheet layout (320x200, 16-color indexed PNG):
y= 0..23 : 1 taxi cel (24x24, x = 0)
sprite 0 ptr (in level1 dump: $DC -> $3700)
The adjacent slot $DD is the landing-gear-down
pose, not a thrust frame -- ignored for now.
y= 24..47 : 4 passenger cels (16x16 each, x = 0, 16, 32, 48)
Sprite 3 ptr ($07FB) is the passenger data; sprites 3-6 share it.
"""
img = new_canvas(320, 200)
px = img.load()
# Shared multicolor sprite colors from VIC registers.
mcm_color0 = mem[0xD025] & 0x0F # 2-bit value 01
mcm_color1 = mem[0xD026] & 0x0F # 2-bit value 11
mcm_mask = mem[0xD01C] # bit per sprite: 1 = multicolor
def paint_sprite_at(data_addr, sprite_color, dst_x, dst_y, dst_w, dst_h, multicolor=False):
# Mono C64 sprite is 24x21, 1 bit per pixel, 3 bytes per row.
# Multicolor C64 sprite is 12 "double-wide pixels" x 21 rows,
# 2 bits per pixel, still 3 bytes per row. Color mapping:
# 00 -> transparent
# 01 -> mcm_color0 ($D025)
# 10 -> sprite_color (sprite's own $D027+sp)
# 11 -> mcm_color1 ($D026)
if sprite_color == 0:
sprite_color = 1
sx_max = min(24, dst_w)
sy_max = min(21, dst_h)
for r in range(sy_max):
if multicolor:
for byte_idx in range(3):
byte = mem[data_addr + r * 3 + byte_idx]
# 4 multicolor pixels per byte; each spans 2
# hardware pixels wide.
for pp in range(4):
bits = (byte >> ((3 - pp) * 2)) & 0x03
if bits == 0:
continue
if bits == 1:
col = mcm_color0
elif bits == 2:
col = sprite_color
else:
col = mcm_color1
sx0 = byte_idx * 8 + pp * 2
if sx0 + 1 >= sx_max:
break
px[dst_x + sx0, dst_y + r] = col
px[dst_x + sx0 + 1, dst_y + r] = col
else:
for byte_idx in range(3):
byte = mem[data_addr + r * 3 + byte_idx]
for bit in range(8):
sx = byte_idx * 8 + bit
if sx >= sx_max:
break
if byte & (0x80 >> bit):
px[dst_x + sx, dst_y + r] = sprite_color
def paint_c64_sprite(sp_index, dst_x, dst_y, dst_w, dst_h):
ptr = mem[0x07F8 + sp_index]
if ptr == 0:
return False
sprite_color = mem[0xD027 + sp_index] & 0x0F
is_multicolor = bool(mcm_mask & (1 << sp_index))
paint_sprite_at(ptr * 64, sprite_color, dst_x, dst_y, dst_w, dst_h,
multicolor=is_multicolor)
return True
# Single taxi cel from sprite 0's actual pointer.
paint_c64_sprite(0, 0, 0, 24, 24)
# 4 passenger cels = the shared C64 sprite 3 (sprites 3..6 all
# point to the same data in the level-1 dump).
for cel in range(4):
paint_c64_sprite(3, cel * 16, 24, 16, 16)
img.save(out_path, "PNG", optimize=True)
def extract_font(mem, out_path):
"""Render the custom charset (which IS the in-game font in C64
text-mode games) as the 320x200 font.png. Same layout as
tilebank, but here we want each glyph at cell (ascii%40,
ascii/40) for jlDrawText's ASCII map.
For Space Taxi specifically, the charset doubles as the level
tile bank AND any in-screen text Sierra-style. We map the C64
screen codes used for ASCII display (codes 1..26 = A..Z,
32..63 = punctuation/digits) directly to their ASCII positions
in the JoeyLib font sheet.
"""
img = new_canvas(320, 200)
px = img.load()
def render_at(ascii_code, charset_idx):
if ascii_code < 32 or ascii_code >= 128:
return
src = 0x2800 + (charset_idx & 0xFF) * 8
col = ascii_code % 40
row = ascii_code // 40
bx = col * 8
by = row * 8
for r in range(8):
byte = mem[src + r]
for c in range(8):
if byte & (0x80 >> c):
px[bx + c, by + r] = 1
# C64 upper-only screen codes: 1..26 = A..Z, 27=[ 28=£ 29=] 30=↑ 31=←
# 32 = space, 33..63 = !"# ... ?, etc. We map ASCII directly.
for c in range(32, 64):
render_at(c, c) # symbols + digits 1:1
for c, code in enumerate("ABCDEFGHIJKLMNOPQRSTUVWXYZ"):
render_at(ord(code), 1 + c) # screen code 1..26 -> A..Z
img.save(out_path, "PNG", optimize=True)
def main():
if len(sys.argv) != 4:
print("usage: extractFromDump.py <dump-path> <level-name> <tilebank-id>", file=sys.stderr)
sys.exit(2)
dump_path = sys.argv[1]
level_name = sys.argv[2]
tilebank_id = int(sys.argv[3])
mem = load_dump(dump_path)
print(f"loaded {dump_path}: 64 KB OK")
tiles_dir = os.path.join(ROOT, "assets", "tiles")
sprites_dir = os.path.join(ROOT, "assets", "sprites")
levels_dir = os.path.join(ROOT, "assets", "levels")
os.makedirs(tiles_dir, exist_ok=True)
os.makedirs(sprites_dir, exist_ok=True)
os.makedirs(levels_dir, exist_ok=True)
# Build the screen-code -> bank-slot mapping once and feed it to
# both extractors so the tilemap and tilebank stay aligned.
code_to_slot = build_code_to_slot(mem)
# 8.3 DOS naming -- "tilebank0" is 9 chars and fopen fails under
# DOSBox strict 8.3 mode. Keep the short form across the pipeline.
tilebank_path = os.path.join(tiles_dir, f"tbank{tilebank_id}.png")
extract_tilebank(mem, tilebank_path, code_to_slot)
print(f" wrote {tilebank_path}")
level_path = os.path.join(levels_dir, f"{level_name}.txt")
extract_level(mem, level_name, tilebank_id, level_path, code_to_slot)
print(f" wrote {level_path}")
# Sprites + font come from level 1 only (sprite pointer table is
# script-driven so it differs by scene; we use the first dump's
# set as the canonical sheet, plus its charset as the font).
if tilebank_id == 1:
sprite_path = os.path.join(sprites_dir, "sprites.png")
extract_sprites(mem, sprite_path)
print(f" wrote {sprite_path}")
font_path = os.path.join(ROOT, "assets", "font.png")
extract_font(mem, font_path)
print(f" wrote {font_path}")
if __name__ == "__main__":
main()

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
generated/amiga/sprites/sprites.spc (Stored with Git LFS)

Binary file not shown.

BIN
generated/amiga/sprites/sprites.spr (Stored with Git LFS)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show more