From 4c11efe83f3899b4855f07e9544a71a069fe4bc0 Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Mon, 28 Sep 2026 15:27:29 -0500 Subject: [PATCH] Major changes to setup. Now installable by mere mortals on Debian, Fedora, and WSL. --- PLAN.md | 2 +- VERIFIED.md | 6 +- assets/cpu6502.py | 560 ++++++++++++++++++++++++++++++ assets/depackSpeechRam.py | 9 +- assets/dumpScenes.py | 115 ++++++ assets/levels/format.md | 2 +- assets/romToLevel.py | 205 +++++++++++ assets/sidCapture.py | 4 +- installer/components/staxiData.sh | 30 ++ scripts/bench-staxi-title.sh | 14 +- scripts/diag-staxi-screen.sh | 14 +- stLevelFile.c | 2 +- 12 files changed, 939 insertions(+), 24 deletions(-) create mode 100644 assets/cpu6502.py create mode 100644 assets/dumpScenes.py create mode 100644 assets/romToLevel.py create mode 100644 installer/components/staxiData.sh diff --git a/PLAN.md b/PLAN.md index 0bbd953..250346f 100644 --- a/PLAN.md +++ b/PLAN.md @@ -25,7 +25,7 @@ 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) diff --git a/VERIFIED.md b/VERIFIED.md index aaa7ed4..274f1a1 100644 --- a/VERIFIED.md +++ b/VERIFIED.md @@ -82,9 +82,9 @@ 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. +- `examples/spacetaxi/assets/cpu6502.py`: minimal 6502 emulator. Sufficient for all routines tested above; throws `NotImplementedError` for unsupported opcodes (none hit so far in gameplay paths). +- `examples/spacetaxi/assets/dumpScenes.py` + `stuff/spacetaxi/dumpAllLevels.py`: pre-existing scene-decompress drivers. +- `examples/spacetaxi/assets/romToLevel.py`: emits the `.dat` files from raw.bin via the same emulator. ## METHODOLOGY (for future verification sessions) diff --git a/assets/cpu6502.py b/assets/cpu6502.py new file mode 100644 index 0000000..2ab6af5 --- /dev/null +++ b/assets/cpu6502.py @@ -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, +} diff --git a/assets/depackSpeechRam.py b/assets/depackSpeechRam.py index 765fac1..22752b0 100755 --- a/assets/depackSpeechRam.py +++ b/assets/depackSpeechRam.py @@ -8,7 +8,7 @@ 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: stuff/spacetaxi/cpu6502.py is a +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. @@ -41,11 +41,8 @@ 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, STUFF) - try: - from cpu6502 import Cpu6502 - except ImportError: - sys.exit("depackSpeechRam: need cpu6502.py in %s" % STUFF) + sys.path.insert(0, HERE) + from cpu6502 import Cpu6502 prg = open(prgPath, "rb").read() load = prg[0] | (prg[1] << 8) diff --git a/assets/dumpScenes.py b/assets/dumpScenes.py new file mode 100644 index 0000000..77a32be --- /dev/null +++ b/assets/dumpScenes.py @@ -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 [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() diff --git a/assets/levels/format.md b/assets/levels/format.md index a883d27..ea60e44 100644 --- a/assets/levels/format.md +++ b/assets/levels/format.md @@ -82,7 +82,7 @@ without changes. 3. Author each level layout in a text editor or grid tool, save as a .dat per this spec. The helper `examples/spacetaxi/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 `examples/spacetaxi/assets/romToLevel.py` directly from a raw VICE dump of the original .prg. ## Naming diff --git a/assets/romToLevel.py b/assets/romToLevel.py new file mode 100644 index 0000000..7c101c9 --- /dev/null +++ b/assets/romToLevel.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python3 +# Convert ROM-extracted Space Taxi scenes directly into the runtime +# STL1 binary format (see examples/spacetaxi/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 # all 24 levels +# romToLevel.py # one specific +# +# Writes: +# /level01.dat ... level24.dat (scene 0 = "A" = level01) +# /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 examples/spacetaxi/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.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 [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() diff --git a/assets/sidCapture.py b/assets/sidCapture.py index 8c028e4..76befb8 100644 --- a/assets/sidCapture.py +++ b/assets/sidCapture.py @@ -13,8 +13,8 @@ extracted the speech: 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, sys -sys.path.insert(0, "/home/scott/claude/joeylib/stuff/spacetaxi") +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]) diff --git a/installer/components/staxiData.sh b/installer/components/staxiData.sh new file mode 100644 index 0000000..a80ee0c --- /dev/null +++ b/installer/components/staxiData.sh @@ -0,0 +1,30 @@ +# Space Taxi's level source: a memory image of the original Commodore 64 +# game. Optional -- without it every port still builds, skipping only the +# Space Taxi data -- so it is not part of `all`; install it by name. + +compDescription="Space Taxi C64 memory image (private, optional: bundle or import)" +compGroups="staxi optional" +compRequires="" +compHostDeps="" +compPrivateFile="stuff/spacetaxi/raw.bin" +compPin="$(privateSha1For "$compPrivateFile")" + + +compInstall() { + privateEnsure "$compPrivateFile" +} + + +compInstalled() { + privatePresent "$compPrivateFile" +} + + +compVerify() { + privatePresent "$compPrivateFile" +} + + +compEnv() { + : +} diff --git a/scripts/bench-staxi-title.sh b/scripts/bench-staxi-title.sh index 0bdc138..d1ba47f 100755 --- a/scripts/bench-staxi-title.sh +++ b/scripts/bench-staxi-title.sh @@ -25,12 +25,16 @@ set -euo pipefail repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) -: "${LLVM816_ROOT:?bench-staxi-title.sh: source toolchains/env.sh first}" -CADIUS="${CADIUS:-$LLVM816_ROOT/tools/cadius/cadius}" +source "$repo/scripts/lib/joeyEnv.sh" +joeyNeed LLVM816_ROOT toolchain:iigs +joeyNeed CADIUS cadius +joeyNeed MAME mame +joeyNeed IIGS_ROMPATH iigsFirmware +joeyNeed GSOS_SYSTEM_DISK iigsFirmware -sys_disk=$repo/toolchains/emulators/support/gsos-system.po +sys_disk=$GSOS_SYSTEM_DISK data_disk=$repo/build/iigs/bin/joey.2mg -rompath="${MAME_ROMPATH:-$HOME/.mame/roms}" +rompath="$IIGS_ROMPATH" maxFrames="${STAXI_MAX_FRAMES:-15000}" maxChanges="${STAXI_MAX_CHANGES:-80}" label="${1:-run}" @@ -148,7 +152,7 @@ LUA echo "bench-staxi-title[$label]: running STAXI headless under MAME (cap $maxFrames frames, $maxChanges changes)..." >&2 cd "$work" out=$(QT_QPA_PLATFORM=offscreen SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \ - timeout "${STAXI_WALL_TIMEOUT:-900}" mame apple2gs \ + timeout "${STAXI_WALL_TIMEOUT:-900}" "$MAME" apple2gs \ -rompath "$rompath" \ -flop3 "$work/boot.po" -flop4 "$work/joey.2mg" \ -video none -sound none -nothrottle \ diff --git a/scripts/diag-staxi-screen.sh b/scripts/diag-staxi-screen.sh index 0e63232..2d2f16b 100755 --- a/scripts/diag-staxi-screen.sh +++ b/scripts/diag-staxi-screen.sh @@ -7,12 +7,16 @@ set -euo pipefail repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) -: "${LLVM816_ROOT:?diag-staxi-screen.sh: source toolchains/env.sh first}" -CADIUS="${CADIUS:-$LLVM816_ROOT/tools/cadius/cadius}" +source "$repo/scripts/lib/joeyEnv.sh" +joeyNeed LLVM816_ROOT toolchain:iigs +joeyNeed CADIUS cadius +joeyNeed MAME mame +joeyNeed IIGS_ROMPATH iigsFirmware +joeyNeed GSOS_SYSTEM_DISK iigsFirmware -sys_disk=$repo/toolchains/emulators/support/gsos-system.po +sys_disk=$GSOS_SYSTEM_DISK data_disk=$repo/build/iigs/bin/joey.2mg -rompath="${MAME_ROMPATH:-$HOME/.mame/roms}" +rompath="$IIGS_ROMPATH" dumpFrame="${STAXI_DUMP_FRAME:-9000}" work=$(mktemp -d -t joeylib-staxidiag.XXXXXX) @@ -120,7 +124,7 @@ LUA echo "diag-staxi: dumping SHR at frame $dumpFrame..." >&2 cd "$work" out=$(QT_QPA_PLATFORM=offscreen SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \ - timeout 900 mame apple2gs \ + timeout 900 "$MAME" apple2gs \ -rompath "$rompath" \ -flop3 "$work/boot.po" -flop4 "$work/joey.2mg" \ -video none -sound none -nothrottle \ diff --git a/stLevelFile.c b/stLevelFile.c index dc59814..5ca896e 100644 --- a/stLevelFile.c +++ b/stLevelFile.c @@ -1,7 +1,7 @@ // Space Taxi -- STL4 level file parser (plain stdio, no JoeyLib), shared // by the game and the host-side simulation harness. // -// Layout (all bytes, written by stuff/spacetaxi/romToLevel.py): +// Layout (all bytes, written by examples/spacetaxi/assets/romToLevel.py): // "STL4" // name[22] $7D78 level name in screen codes // header[9] $7D00 VIC colour block