#!/usr/bin/env python3 # c64trace.py -- ride a gate stream on the real game in VICE and print # the trace stsim prints, line for line, so the two can be compared byte # for byte (gate.sh c64). # # c64trace.py [rawOut] # c64trace.py --prepare make the cached snapshot (below) and exit # # demobuf.json is stsim's STSIM_DEMOBUF line: the whole 640-byte playback # buffer ($0902) as the sim starts the ride with it. rawOut, if given, # receives each tick's screen RAM, color RAM and charset in stsim's # STSIM_RAW layout. # # How the ride is set up on the C64, all from the game's own attract demo: # * The original disk boots once (the crack's intro is skipped at its # key test, the trainer menu answered with START GAME, every cheat # off) to the point the first attract demo picks its screen ($50C6); # VICE's snapshot of that moment is cached in build/c64trace/. # * Each ride restores the snapshot, makes every demo slot ($50C1.. # $50C4) the wanted screen, and when the screen and its recording are # loaded ($625F, the disk load is skipped) writes the sim's playback # buffer over $0902, zeroes the flame parity, wave index and walk # parity ($716C/$716E/$7161) that the title leaves behind -- the sim # starts its rides from a fresh game -- and sets the screens-played # count so that $6211's INC makes it the screen's place in the 24-hour # shift (A = 1 .. Y = 25), as stsim's rides have it. # * A finished wreck takes the real game's path ($6B52 -> $6B64: the cab # count drops and the cab respawns at $5F27) instead of the demo's # return to the title, as stsim's rides do. A ride ends when the cab # leaves the screen ($6BFB, end 1), the cabs run out ($6BD0, end 4), # the playback reaches the buffer's last pair (end "stream"), or after # ticks. # * Every $5F40 (the main loop's top) the state is read and printed. # # Needs VICE's x64sc (VICE=path, else on PATH; VICE_DATA=its ROM folder # when x64sc cannot find its own), Xvfb, and the original disk image # (STAXI_D64, default stuff/SPACETAX.D64). Exits 2 when one is missing. # The disk and everything derived from it stay out of the repository. import json import os import shutil import signal import socket import struct import subprocess import sys import time HERE = os.path.dirname(os.path.abspath(__file__)) REPO = os.path.normpath(os.path.join(HERE, '..', '..')) CACHE = os.path.join(REPO, 'build', 'c64trace') D64 = os.environ.get('STAXI_D64', os.path.join(REPO, 'stuff', 'SPACETAX.D64')) LIFE_S = 900 # an emulator outlives no ride by more than this # VICE binary monitor protocol (src/monitor/monitor_binary.c). STX = 0x02 API_VERSION = 0x02 CMD_MEM_GET = 0x01 CMD_MEM_SET = 0x02 CMD_CP_SET = 0x12 CMD_CP_DELETE = 0x13 CMD_REGS_SET = 0x32 CMD_REGS_GET = 0x31 CMD_DUMP = 0x41 CMD_UNDUMP = 0x42 CMD_KEYS = 0x72 CMD_PING = 0x81 CMD_BANKS = 0x82 CMD_EXIT = 0xAA CMD_QUIT = 0xBB EV_CHECKPOINT = 0x11 EV_JAM = 0x61 EV_STOPPED = 0x62 REG_PC = 0x03 OP_EXEC = 0x04 # The game's addresses. INTRO_KEY_TEST = 0x0994 # the crack's "space pressed?" test INTRO_GO = 0x099E # ...and where it goes when it is DEMO_PICK = 0x50C6 # $5010's demo branch: a demo picks its screen DEMO_SLOTS = 0x50C1 # "HWTX", the demo rotation DEMO_LOAD = 0x625F # JSR $98C4: load the screen's recording DEMO_LOADED = 0x6262 # ...returned DEMO_BUFFER = 0x0902 CARRIED = (0x716C, 0x716E, 0x7161) SCREENS_PLAYED = 0x7215 TICK_TOP = 0x5F40 CAB_LEFT = 0x6BFB WRECK_DONE = 0x6B52 LOSE_CAB = 0x6B64 PLAYER_OUT = 0x6BD0 LAST_PAIR = 638 # the playback buffer's last (mask, count) pair END_EXIT = 1 # StTickResultE: ST_TICK_LEVEL_EXIT END_OUT = 4 # ST_TICK_PLAYER_OUT TRAINER_ROWS = 12 # cursor-downs from the first option to START GAME KEY_DOWN = '\x11' KEY_RETURN = '\x0d' KEY_WAIT_S = 0.3 INTRO_WAIT_S = 6 BOOT_WAIT_S = 300 STOP_WAIT_S = 120 class Monitor: """A VICE binary-monitor connection.""" def __init__(self, port): self.sock = None for _ in range(200): try: self.sock = socket.create_connection(('127.0.0.1', port), timeout=2) break except OSError: time.sleep(0.1) if self.sock is None: raise RuntimeError('no VICE monitor on port %d' % port) self.sock.settimeout(None) self.reqId = 1 self.events = [] self.buf = b'' def recvExact(self, n): while len(self.buf) < n: chunk = self.sock.recv(1 << 20) if not chunk: raise EOFError('VICE closed the monitor') self.buf += chunk out, self.buf = self.buf[:n], self.buf[n:] return out def packet(self): hdr = self.recvExact(12) length = struct.unpack_from('