#!/usr/bin/env python3 # testTwoMachines.py - run two copies of the patched game, each with its own SwiftLink, and cross # connect their serial lines so whatever one transmits the other receives. # # python3 testTwoMachines.py # # VICE's -rsdev1 "host:port" makes the emulator the TCP *client*, so the two instances cannot be # pointed at each other directly. A relay in this script accepts both connections and forwards bytes # between them - a null modem cable made of sockets. It also logs every byte in both directions, # which is what makes the exchange observable. import os import select import socket import sys import threading import time sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from viceHarness import ViceSession, aciaArgs, pickFreePort, readByte, SCRATCH SHOTS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "shots") class NullModemRelay: def __init__(self): self.port = pickFreePort() self.listener = socket.socket() self.listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.listener.bind(("127.0.0.1", self.port)) self.listener.listen(4) self.conns = [] self.log = [bytearray(), bytearray()] self.running = True threading.Thread(target=self.serve, daemon=True).start() def serve(self): self.listener.settimeout(0.5) while self.running: readable = list(self.conns) if len(self.conns) < 2: try: conn, _ = self.listener.accept() conn.setblocking(False) self.conns.append(conn) print(f"[relay] connection {len(self.conns)}", flush=True) continue except socket.timeout: pass except OSError: return if not readable: time.sleep(0.1) continue ready, _, _ = select.select(readable, [], [], 0.2) for conn in ready: try: chunk = conn.recv(4096) except OSError: chunk = b"" if not chunk: continue index = self.conns.index(conn) self.log[index] += chunk for other in self.conns: if other is not conn: try: other.sendall(chunk) except OSError: pass def stop(self): self.running = False try: self.listener.close() except OSError: pass for conn in self.conns: try: conn.close() except OSError: pass def pickModemOpponent(session, tag, answerKey): # Options menu: the highlighted row lives in $91D5, row 0 = COMPETE WITH MODEM OPPONENT. Fire # picks it; the game then asks A/O (answer or originate) and then for space before it opens the # link, so keep offering both until the ACIA command register stops reading back zero. for _ in range(4): session.enterMonitor() row = readByte(session, 0x91D5) session.sock.sendall(b"x\n") session.recv(3) print(f"[{tag}] menu row = {row}", flush=True) if row == 0: break session.hold("KP_8", 250) time.sleep(0.8) session.hold("KP_0", 300) time.sleep(2) return answerKey def answerModemPrompts(session, tag, answerKey): # aciaCommandShadow ($E5BE) is zero until configureUserPortLines has programmed the 6551, so it # is the cheapest "is the link open yet" test there is. for attempt in range(24): session.hold(answerKey, 250) session.hold("space", 250) time.sleep(1.5) session.enterMonitor() shadow = readByte(session, 0xE5BE) session.sock.sendall(b"x\n") session.recv(2) if shadow: print(f"[{tag}] link opened after {attempt + 1} attempt(s), " f"command register = ${shadow:02X}", flush=True) return True print(f"[{tag}] link never opened", flush=True) return False def forceCarrier(session, tag): # C= + C toggles carrierOverrideFlags ($E04A) bit 6, which makes the driver believe DCD is up. # Two machines on a direct cable have no carrier signal, so without this they sit in the Hayes # dialogue for ever. VICE maps the Commodore key to left Ctrl. for _ in range(4): session.key("ctrl+c") time.sleep(1) session.enterMonitor() flags = readByte(session, 0xE04A) phase = readByte(session, 0xE040) session.sock.sendall(b"x\n") session.recv(2) print(f"[{tag}] carrierOverrideFlags = {flags}, connectionPhase = {phase}", flush=True) if flags: return True return False def main(): disk = os.path.abspath(sys.argv[1]) os.makedirs(SHOTS, exist_ok=True) relay = NullModemRelay() print(f"relay on 127.0.0.1:{relay.port}", flush=True) args = aciaArgs(rsDevAddress=f"127.0.0.1:{relay.port}", baud=2400) a = ViceSession(disk, f"{SCRATCH}/two.a.vice.log", args, label="A") b = ViceSession(disk, f"{SCRATCH}/two.b.vice.log", args, label="B") try: a.connect() b.connect() a.bootPastLoader() b.bootPastLoader() a.findWindow() b.findWindow() a.focus() b.focus() time.sleep(8) a.shot(f"{SHOTS}/two01aMenu.png") b.shot(f"{SHOTS}/two01bMenu.png") # A answers, B originates - the two halves of the Hayes dialogue the driver expects. pickModemOpponent(a, "A", "a") pickModemOpponent(b, "B", "o") answerModemPrompts(a, "A", "a") answerModemPrompts(b, "B", "o") time.sleep(10) a.shot(f"{SHOTS}/two02aLinkOpen.png") b.shot(f"{SHOTS}/two02bLinkOpen.png") report(relay) # There is no carrier on a null modem cable, so tell both drivers to pretend there is one. forceCarrier(a, "A") forceCarrier(b, "B") time.sleep(20) a.shot(f"{SHOTS}/two03aCarrierForced.png") b.shot(f"{SHOTS}/two03bCarrierForced.png") report(relay) for session, tag in ((a, "A"), (b, "B")): session.enterMonitor() session.mon("m e030 e047") session.mon("m e5b8 e5bf") session.mon("m de00 de03") session.sock.sendall(b"x\n") session.recv(3) time.sleep(30) a.shot(f"{SHOTS}/two04aLater.png") b.shot(f"{SHOTS}/two04bLater.png") report(relay) finally: a.close() b.close() time.sleep(1) relay.stop() report(relay) def report(relay): for i, buf in enumerate(relay.log): name = "AB"[i] if i < 2 else str(i) print(f"[relay] from {name}: {len(buf)} bytes: {bytes(buf[-120:])!r}", flush=True) if __name__ == "__main__": main()