"""Write a bootable Apple II .dsk (DOS 3.3 sector order, 143360 bytes). The .dsk stores sectors in DOS *logical* order, but the Disk II boot ROM reads *physical* sectors, so we place each chunk of the bitmap at the logical slot that maps to the physical sector our loader will read -- making the loader's physical-sequential reads come out contiguous in memory. """ from __future__ import annotations # DOS 3.3 physical-sector -> logical-sector (the .dsk read interleave). PHYS2LOG = [0, 7, 14, 6, 13, 5, 12, 4, 11, 3, 10, 2, 9, 1, 8, 15] SECTOR = 256 SPT = 16 TRACKS = 35 DISK_SIZE = TRACKS * SPT * SECTOR # 143360 class DskError(RuntimeError): pass def _offset(track: int, phys_sector: int) -> int: return (track * SPT + PHYS2LOG[phys_sector]) * SECTOR def build_boot_dsk(boot: bytes, payload: bytes) -> bytes: """boot = assembled boot0 (origin $0800); payload = data pages the loader reads in order: track 0 sectors 1-15, then whole tracks 1, 2, ... (physical order).""" if len(payload) % SECTOR: raise DskError("payload must be a whole number of 256-byte sectors") npages = len(payload) // SECTOR disk = bytearray(DISK_SIZE) disk[0:SECTOR] = (bytes(boot) + bytes(SECTOR))[:SECTOR] # boot0 at T0 phys 0 assigns = [(0, p) for p in range(1, 16)] # track 0 (skip boot) track = 1 while len(assigns) < npages: assigns += [(track, p) for p in range(16)] track += 1 if track > TRACKS: raise DskError("payload exceeds disk capacity") for page, (trk, phys) in enumerate(assigns[:npages]): off = _offset(trk, phys) disk[off:off + SECTOR] = payload[page * SECTOR:(page + 1) * SECTOR] return bytes(disk) def write_dsk(path: str, data: bytes) -> str: with open(path, "wb") as f: f.write(data) return path