"""Assemble the BBC 6502 viewer with `xa` and patch in per-image parameters.""" from __future__ import annotations import os import shutil import subprocess import tempfile VIEWER_DIR = os.path.dirname(os.path.abspath(__file__)) LOAD_ADDR = 0x1900 # DFS PAGE WAIT_MODES = {"forever": 0, "key": 1, "seconds": 2} class AssemblerError(RuntimeError): pass def have_xa() -> bool: return shutil.which("xa") is not None def _xa(wrapper: str) -> bytes: if not have_xa(): raise AssemblerError("The 'xa' (xa65) assembler was not found on PATH.\n" "Install it with: sudo apt install xa65") with tempfile.TemporaryDirectory() as td: out = os.path.join(td, "v.bin") fd, wrap = tempfile.mkstemp(suffix=".s", prefix="_wrap_", dir=VIEWER_DIR) try: with os.fdopen(fd, "w") as f: f.write(wrapper) proc = subprocess.run(["xa", "-o", out, os.path.basename(wrap)], capture_output=True, text=True, cwd=VIEWER_DIR) if proc.returncode != 0: raise AssemblerError(f"xa failed:\n{proc.stdout}{proc.stderr}") with open(out, "rb") as f: return f.read() finally: os.unlink(wrap) SS_WAITMODE = {"key": 1, "seconds": 2, "both": 3} def build_slideshow_viewer(bbc_mode: int, ncol: int, base: int, palettes, advance: str = "both", seconds: int = 10, loop: bool = True, video: str = "pal") -> bytes: """Return the multi-image BBC loader (origin $1900). ``palettes`` is one physical-colour list per image (each truncated/padded to ncol) -- emitted as the ss_pal table the loader indexes by slide; the screen base hex is patched into the OSCLI *LOAD string. """ if not palettes: raise AssemblerError("a slideshow needs at least one image") rate = 60 if video == "ntsc" else 50 flat = [] for p in palettes: row = list(p[:ncol]) flat += row + [0] * (ncol - len(row)) table = ",".join(str(b & 0xFF) for b in flat) wrapper = (f"#define WAITMODE {SS_WAITMODE[advance]}\n" f"#define WAITSECS {max(0, int(seconds))}\n" f"#define RATE {rate}\n" f"#define NIMAGES {len(palettes)}\n" f"#define LOOPFLAG {1 if loop else 0}\n" f"#define MODE {bbc_mode}\n" f"#define NCOL {ncol}\n" '#include "slideshow.s"\n' "ss_pal:\n" f" .byte {table}\n") raw = bytearray(_xa(wrapper)) off = raw.find(b"LOAD 00 ") if off < 0: raise AssemblerError("could not locate the OSCLI string to patch") raw[off + 8:off + 12] = f"{base:04X}".encode("ascii") # screen-base hex return bytes(raw) def build_viewer(bbc_mode: int, ncol: int, physicals, base: int, display: str = "forever", seconds: int = 0, video: str = "pal") -> bytes: """Return the assembled loader (origin $1900) with the mode/palette/screen-base parameters patched in.""" waitmode = WAIT_MODES.get(display, 0) rate = 60 if video == "ntsc" else 50 # bbcb is PAL (50 Hz) wrapper = (f"#define WAITMODE {waitmode}\n" f"#define WAITSECS {max(0, int(seconds))}\n" f"#define RATE {rate}\n" '#include "bbc.s"\n') raw = bytearray(_xa(wrapper)) off = raw.find(b"LOAD IMG ") if off < 0: raise AssemblerError("could not locate the OSCLI string to patch") raw[off + 9:off + 13] = f"{base:04X}".encode("ascii") # screen-base hex raw[off + 14] = bbc_mode & 0xFF # p_mode raw[off + 15] = ncol & 0xFF # p_ncol for i, p in enumerate(physicals[:8]): # p_pal raw[off + 16 + i] = p & 0xFF return bytes(raw)