First public commit.

This commit is contained in:
The Dust Council 2026-07-03 19:35:35 -07:00
parent 2a48f52979
commit 4bac9d83ed
288 changed files with 18417 additions and 1076 deletions

68
lenser/crt.py Normal file
View file

@ -0,0 +1,68 @@
"""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)