"""Read/write C64 .crt cartridge images (CCS64/VICE format) for a 16K ROM at $8000. VICE and MAME disagree on how a 16K Generic cart must be described: * VICE attaches it only as ONE 16K CHIP packet at $8000 (two 8K packets are rejected with "Failed to attach image"). * MAME maps each CHIP at its own load address and treats a single 16K CHIP as 8K -- it needs TWO 8K packets (ROML $8000 + ROMH $A000) to map $A000-$BFFF. So `split=False` (default) targets VICE; the MAME preview path uses `split=True`. """ from __future__ import annotations import struct CART_SIZE = 0x4000 def _chip(load_addr: int, payload: bytes) -> bytes: c = bytearray(16) c[0:4] = b"CHIP" struct.pack_into(">I", c, 4, 16 + len(payload)) # packet length struct.pack_into(">H", c, 8, 0x0000) # chip type: ROM struct.pack_into(">H", c, 10, 0x0000) # bank struct.pack_into(">H", c, 12, load_addr) struct.pack_into(">H", c, 14, len(payload)) return bytes(c) + payload def write_crt(rom: bytes, path: str, name: str = "8bitlenser", split: bool = False) -> str: """Wrap a 16K cart `rom` ($8000-$BFFF) in a .crt (16K config: EXROM & GAME both 0). ``split`` False = one 16K CHIP (VICE); True = two 8K CHIPs (MAME).""" if len(rom) != CART_SIZE: raise ValueError(f"expected a 16K cart ROM, got {len(rom)} bytes") header = bytearray(64) header[0:16] = b"C64 CARTRIDGE " struct.pack_into(">I", header, 16, 64) # header length struct.pack_into(">H", header, 20, 0x0100) # version 1.0 struct.pack_into(">H", header, 22, 0x0000) # hardware type: normal/generic header[24] = 0 # EXROM line (asserted) header[25] = 0 # GAME line (asserted) -> 16K nm = name.encode("ascii", "replace")[:32] header[32:32 + len(nm)] = nm with open(path, "wb") as f: f.write(header) if split: f.write(_chip(0x8000, rom[:0x2000])) # ROML f.write(_chip(0xA000, rom[0x2000:])) # ROMH else: f.write(_chip(0x8000, rom)) # one 16K CHIP return path def read_rom(path: str) -> bytes: """Extract the raw 16K cart ROM from a .crt (handles one- or two-CHIP).""" with open(path, "rb") as f: data = f.read() hdr_len = struct.unpack_from(">I", data, 16)[0] rom = bytearray() pos = hdr_len while pos + 16 <= len(data) and data[pos:pos + 4] == b"CHIP": plen = struct.unpack_from(">I", data, pos + 4)[0] size = struct.unpack_from(">H", data, pos + 14)[0] rom += data[pos + 16:pos + 16 + size] pos += plen return bytes(rom)