Major changes to setup. Now installable by mere mortals on Debian, Fedora, and WSL.
This commit is contained in:
parent
f6c4ad3747
commit
4c11efe83f
12 changed files with 939 additions and 24 deletions
2
PLAN.md
2
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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
560
assets/cpu6502.py
Normal file
560
assets/cpu6502.py
Normal 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,
|
||||
}
|
||||
|
|
@ -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:
|
||||
sys.path.insert(0, HERE)
|
||||
from cpu6502 import Cpu6502
|
||||
except ImportError:
|
||||
sys.exit("depackSpeechRam: need cpu6502.py in %s" % STUFF)
|
||||
|
||||
prg = open(prgPath, "rb").read()
|
||||
load = prg[0] | (prg[1] << 8)
|
||||
|
|
|
|||
115
assets/dumpScenes.py
Normal file
115
assets/dumpScenes.py
Normal 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()
|
||||
|
|
@ -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
|
||||
|
|
|
|||
205
assets/romToLevel.py
Normal file
205
assets/romToLevel.py
Normal 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()
|
||||
|
|
@ -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])
|
||||
|
|
|
|||
30
installer/components/staxiData.sh
Normal file
30
installer/components/staxiData.sh
Normal 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() {
|
||||
:
|
||||
}
|
||||
|
|
@ -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 \
|
||||
|
|
|
|||
|
|
@ -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 \
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue