115 lines
4.4 KiB
Python
115 lines
4.4 KiB
Python
#!/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()
|