Major changes to setup. Now installable by mere mortals on Debian, Fedora, and WSL.

This commit is contained in:
Scott Duensing 2026-09-28 15:27:29 -05:00
parent f6c4ad3747
commit 4c11efe83f
12 changed files with 939 additions and 24 deletions

View file

@ -25,7 +25,7 @@ Same loop every time, so it stays cheap to iterate:
`stLevel.c`, `spacetaxi.c`). `stLevel.c`, `spacetaxi.c`).
6. **Build + ship** -- `make -f make/dos.mk EXAMPLE=spacetaxi` 6. **Build + ship** -- `make -f make/dos.mk EXAMPLE=spacetaxi`
first, then once stable also amiga / atarist / iigs. 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. behavior. Do not start the next phase before this.
## Phase 0 -- foundation (DONE) ## Phase 0 -- foundation (DONE)

View file

@ -82,9 +82,9 @@ plus the inline scripts run during verification sessions.
## TRACE INFRASTRUCTURE ## 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/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). - `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).
- `stuff/spacetaxi/dumpScenes.py` + `dumpAllLevels.py`: pre-existing scene-decompress drivers. - `examples/spacetaxi/assets/dumpScenes.py` + `stuff/spacetaxi/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/romToLevel.py`: emits the `.dat` files from raw.bin via the same emulator.
## METHODOLOGY (for future verification sessions) ## METHODOLOGY (for future verification sessions)

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

View file

@ -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 back as BASIC ROM and 13 of the 15 utterances are missing. Every
mem0000*.bin in this tree has that problem. 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, 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 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. 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") 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") outPath = sys.argv[2] if len(sys.argv) > 2 else os.path.join(STUFF, "staxi-ram.bin")
sys.path.insert(0, STUFF) sys.path.insert(0, HERE)
try: from cpu6502 import Cpu6502
from cpu6502 import Cpu6502
except ImportError:
sys.exit("depackSpeechRam: need cpu6502.py in %s" % STUFF)
prg = open(prgPath, "rb").read() prg = open(prgPath, "rb").read()
load = prg[0] | (prg[1] << 8) load = prg[0] | (prg[1] << 8)

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

@ -82,7 +82,7 @@ without changes.
3. Author each level layout in a text editor or grid tool, save as a 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 `examples/spacetaxi/mkstlevel`
converts from a human-readable text grid to .dat. The 24 canonical 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. from a raw VICE dump of the original .prg.
## Naming ## 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 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 <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 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<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()

View file

@ -13,8 +13,8 @@ extracted the speech:
A song is over when all three voice stream pointers' high bytes are zero 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). (that is exactly what the game's play-and-wait loop at $44D5 spins on).
""" """
import json, sys import json, os, sys
sys.path.insert(0, "/home/scott/claude/joeylib/stuff/spacetaxi") sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from cpu6502 import Cpu6502 from cpu6502 import Cpu6502
RAM = bytearray(open(sys.argv[1], "rb").read()[:65536]) RAM = bytearray(open(sys.argv[1], "rb").read()[:65536])

View file

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

View file

@ -25,12 +25,16 @@
set -euo pipefail set -euo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
: "${LLVM816_ROOT:?bench-staxi-title.sh: source toolchains/env.sh first}" source "$repo/scripts/lib/joeyEnv.sh"
CADIUS="${CADIUS:-$LLVM816_ROOT/tools/cadius/cadius}" 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 data_disk=$repo/build/iigs/bin/joey.2mg
rompath="${MAME_ROMPATH:-$HOME/.mame/roms}" rompath="$IIGS_ROMPATH"
maxFrames="${STAXI_MAX_FRAMES:-15000}" maxFrames="${STAXI_MAX_FRAMES:-15000}"
maxChanges="${STAXI_MAX_CHANGES:-80}" maxChanges="${STAXI_MAX_CHANGES:-80}"
label="${1:-run}" label="${1:-run}"
@ -148,7 +152,7 @@ LUA
echo "bench-staxi-title[$label]: running STAXI headless under MAME (cap $maxFrames frames, $maxChanges changes)..." >&2 echo "bench-staxi-title[$label]: running STAXI headless under MAME (cap $maxFrames frames, $maxChanges changes)..." >&2
cd "$work" cd "$work"
out=$(QT_QPA_PLATFORM=offscreen SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \ 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" \ -rompath "$rompath" \
-flop3 "$work/boot.po" -flop4 "$work/joey.2mg" \ -flop3 "$work/boot.po" -flop4 "$work/joey.2mg" \
-video none -sound none -nothrottle \ -video none -sound none -nothrottle \

View file

@ -7,12 +7,16 @@
set -euo pipefail set -euo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
: "${LLVM816_ROOT:?diag-staxi-screen.sh: source toolchains/env.sh first}" source "$repo/scripts/lib/joeyEnv.sh"
CADIUS="${CADIUS:-$LLVM816_ROOT/tools/cadius/cadius}" 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 data_disk=$repo/build/iigs/bin/joey.2mg
rompath="${MAME_ROMPATH:-$HOME/.mame/roms}" rompath="$IIGS_ROMPATH"
dumpFrame="${STAXI_DUMP_FRAME:-9000}" dumpFrame="${STAXI_DUMP_FRAME:-9000}"
work=$(mktemp -d -t joeylib-staxidiag.XXXXXX) work=$(mktemp -d -t joeylib-staxidiag.XXXXXX)
@ -120,7 +124,7 @@ LUA
echo "diag-staxi: dumping SHR at frame $dumpFrame..." >&2 echo "diag-staxi: dumping SHR at frame $dumpFrame..." >&2
cd "$work" cd "$work"
out=$(QT_QPA_PLATFORM=offscreen SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \ out=$(QT_QPA_PLATFORM=offscreen SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \
timeout 900 mame apple2gs \ timeout 900 "$MAME" apple2gs \
-rompath "$rompath" \ -rompath "$rompath" \
-flop3 "$work/boot.po" -flop4 "$work/joey.2mg" \ -flop3 "$work/boot.po" -flop4 "$work/joey.2mg" \
-video none -sound none -nothrottle \ -video none -sound none -nothrottle \

View file

@ -1,7 +1,7 @@
// Space Taxi -- STL4 level file parser (plain stdio, no JoeyLib), shared // Space Taxi -- STL4 level file parser (plain stdio, no JoeyLib), shared
// by the game and the host-side simulation harness. // by the game and the host-side simulation harness.
// //
// Layout (all bytes, written by stuff/spacetaxi/romToLevel.py): // Layout (all bytes, written by examples/spacetaxi/assets/romToLevel.py):
// "STL4" // "STL4"
// name[22] $7D78 level name in screen codes // name[22] $7D78 level name in screen codes
// header[9] $7D00 VIC colour block // header[9] $7D00 VIC colour block