import struct from typing import Sequence # Chunk types: # user types: 0 - 239 # reserved: 240 - 248 CHUNK_DUMMY = 249 CHUNK_BONKRAM = 250 CHUNK_SYSTEMRAM = 251 CHUNK_VIDEORAM = 252 CHUNK_PAUSE = 253 CHUNK_EOF = 254 CHUNK_IGNORE = 255 ChunksWithData = {CHUNK_DUMMY, CHUNK_SYSTEMRAM, CHUNK_VIDEORAM, CHUNK_BONKRAM} class LoadList: def __init__(self): self.data = bytearray([CHUNK_IGNORE] * 256) self.data[0:4] = struct.pack("ccBB", b'L', b'L', 1, 0) self.index = 4 def __eq__(self, other) -> bool: return isinstance(other, LoadList) and self.data == other.data def add_chunk(self, chunktype: int, size: int, bank: int = 0, address: int = 0) -> None: if chunktype < 0 or chunktype > 255: raise ValueError("chunktype must be 0 - 255") if size < 0 or size > 65535: raise ValueError(f"size must be 0 - 65535 bytes") if bank < 0 or bank > 255: raise ValueError("bank must be 0 - 255") if address < 0 or address > 65535: raise ValueError("address must be 0 - 65535") data = struct.pack(" 255: raise IndexError("too many chunks") self.data[self.index: self.index + len(data)] = data self.index += len(data) def read(self, data: bytes) -> None: if len(data) != 256: raise ValueError("data must be 256 bytes long") self.data[0:256] = data self.index = 256 def parse(self) -> Sequence[tuple]: if self.data[0] != ord('L') or self.data[1] != ord('L') or self.data[2] != 1: raise ValueError("invalid loadlist identifier", self.data[:4]) index = 4 chunks = [] while index < 256: chunktype = self.data[index] if chunktype == CHUNK_IGNORE: index += 1 # pad byte elif chunktype == CHUNK_EOF: chunks.append((CHUNK_EOF, 0, 0, 0)) return chunks else: size, bank, address = struct.unpack(" bool: return self.index <= 4 class MultiChunkFile: def __init__(self): self.chunks = [] self.loadlist = LoadList() self.datachunks = [] def read(self, filename: str) -> None: num_loadlist = 0 num_data = 0 data_total = 0 file_total = 0 with open(filename, "rb") as inf: while True: data = inf.read(256) if len(data) == 0: break loadlist = LoadList() loadlist.read(data) self.chunks.append(loadlist) num_loadlist += 1 file_total += len(data) for chunk in loadlist.parse(): if chunk[0] == CHUNK_EOF: break elif chunk[0] in ChunksWithData or chunk[0] < 240: data = inf.read(chunk[1]) self.chunks.append(data) num_data += 1 data_total += len(data) file_total += len(data) self.validate() print("Read", filename) print(f" {num_loadlist} loadlists, {num_data} data chunks") print(f" total data size: {data_total} (${data_total:x})") print(f" total file size: {file_total} (${file_total:x})") def write(self, filename: str) -> None: self.flush_ll() self.validate() num_loadlist = 0 num_data = 0 data_total = 0 file_total = 0 with open(filename, "wb") as out: for chunk in self.chunks: if isinstance(chunk, LoadList): num_loadlist += 1 out.write(chunk.data) file_total += len(chunk.data) elif isinstance(chunk, bytes): num_data += 1 out.write(chunk) file_total += len(chunk) data_total += len(chunk) else: raise TypeError("invalid chunk type") print("Written", filename) print(f" {num_loadlist} loadlists, {num_data} data chunks") print(f" total data size: {data_total} (${data_total:x})") print(f" total file size: {file_total} (${file_total:x})") def validate(self) -> None: if len(self.chunks) < 2: raise ValueError("must be at least 2 chunks", self.chunks) chunk_iter = iter(self.chunks) eof_found = False while not eof_found: try: chunk = next(chunk_iter) except StopIteration: raise ValueError("missing EOF chunk") else: if isinstance(chunk, LoadList): for lc in chunk.parse(): if lc[0] == CHUNK_EOF: eof_found = True break elif lc[0] in ChunksWithData or lc[0] < 240: size, bank, address = lc[1:] data = next(chunk_iter) if isinstance(data, bytes): if len(data) != size: raise ValueError("data chunk size mismatch") else: raise TypeError("expected data chunk") else: raise TypeError("expected LoadList chunk") try: next(chunk_iter) except StopIteration: pass else: raise ValueError("trailing chunks") def add_Dummy(self, size: int) -> None: self.add_chunk(CHUNK_DUMMY, data=bytearray(size)) def add_SystemRam(self, bank: int, address: int, data: bytes, chunksize: int = 0xfe00) -> None: if address >= 0xa000 and address < 0xc000: raise ValueError("use add_BankedRam instead to load chunks into banked ram $a000-$c000") if bank < 0 or bank > 31: raise ValueError("bank must be 0 - 31") while data: if address >= 65536: raise ValueError("data too large for system ram") self.add_chunk(CHUNK_SYSTEMRAM, bank, address, data[:chunksize]) data = data[chunksize:] address += chunksize def add_BankedRam(self, bank: int, address: int, data: bytes, chunksize: int = 0x2000) -> None: if address < 0xa000 or address >= 0xc000: raise ValueError("use add_SystemRam instead to load chunks into normal system ram") if chunksize > 0x2000: raise ValueError("chunksize too large for banked ram, max 8K") while data: if address >= 0xc000: address -= 0x2000 bank += 1 if bank >= 32: raise ValueError("data too large for banked ram") self.add_chunk(CHUNK_SYSTEMRAM, bank, address, data[:chunksize]) data = data[chunksize:] address += chunksize def add_BonkRam(self, bonk: int, address: int, data: bytes, chunksize: int = 0x4000) -> None: if bonk < 32 or bonk > 255: raise ValueError("bank for bonk ram (cartridge ram) must be 32 - 255") if chunksize > 0x4000: raise ValueError("chunksize too large for bonk ram (cartridge ram), max 16K") if address < 0xc000 or address > 0xffff: raise ValueError("use add_SystemRam instead to load chunks into normal system ram") while data: if address > 0xffff: address -= 0x4000 bonk += 1 if bonk > 255: raise ValueError("data too large for bonk ram (cartridge ram)") self.add_chunk(CHUNK_BONKRAM, bonk, address, data[:chunksize]) data = data[chunksize:] address += chunksize def add_VideoRam(self, bank: int, address: int, data: bytes, chunksize: int = 0xfe00) -> None: if bank < 0 or bank > 1: raise ValueError("bank for videoram must be 0 or 1") while data: if address >= 65536: address -= 65536 bank += 1 if bank >= 2: raise ValueError("data too large for video ram") self.add_chunk(CHUNK_VIDEORAM, bank, address, data[:chunksize]) data = data[chunksize:] address += chunksize def add_User(self, chunktype: int, data: bytes) -> None: if chunktype < 0 or chunktype > 239: raise ValueError("user chunk type must be 0 - 239") if len(data) > 65535: raise ValueError("data too large to fit in a single chunk") self.add_chunk(chunktype, data=data) def add_Pause(self, code: int) -> None: self.add_chunk_nodata(CHUNK_PAUSE, code) def add_EOF(self) -> None: self.add_chunk_nodata(CHUNK_EOF, 0) def add_chunk(self, chunktype: int, bank: int = 0, address: int = 0, data: bytes = b"") -> None: try: self.loadlist.add_chunk(chunktype, len(data), bank, address) except IndexError: # loadlist is full, flush everything out and create a new one and put it in there. self.flush_ll() self.loadlist.add_chunk(chunktype, len(data), bank, address) if data: self.datachunks.append(bytes(data)) def add_chunk_nodata(self, chunktype: int, code: int = 0) -> None: try: self.loadlist.add_chunk(chunktype, code, 0, 0) except IndexError: # loadlist is full, flush everything out and create a new one and put it in there. self.flush_ll() self.loadlist.add_chunk(chunktype, code, 0, 0) def flush_ll(self) -> None: if not self.loadlist.is_empty(): self.chunks.append(self.loadlist) self.chunks.extend(self.datachunks) self.loadlist = LoadList() self.datachunks.clear() if __name__ == "__main__": try: bitmap1 = open("testdata/ME-TITLESCREEN.BIN", "rb").read() bitmap2 = open("testdata/DS-TITLESCREEN.BIN", "rb").read() palette1 = open("testdata/ME-TITLESCREEN.PAL", "rb").read() palette2 = open("testdata/DS-TITLESCREEN.PAL", "rb").read() except IOError: print("""ERROR: cannot load the demo data files. You'll need to put the titlescreen data files from the 'musicdemo' project into the testdata directory. The musicdemo is on github: https://github.com/irmen/cx16musicdemo The four files are the two ME- and the two DS- TITLESCREEN.BIN and .PAL files, and are generated by running the makefile in that project.""") raise SystemExit program = bytearray(3333) textdata = b"hello this is a demo text.... \x00" mcf = MultiChunkFile() for _ in range(4): mcf.add_User(42, bytearray(999)) mcf.add_SystemRam(0, 0x4000, program) mcf.add_BankedRam(10, 0xa000, textdata) mcf.add_Pause(444) for _ in range(4): mcf.add_User(42, bytearray(999)) mcf.add_VideoRam(1, 0xfa00, palette1) mcf.add_VideoRam(0, 0, bitmap1) mcf.add_Pause(333) mcf.add_VideoRam(1, 0xfa00, palette2) mcf.add_VideoRam(0, 0, bitmap2) mcf.add_Pause(222) mcf.add_BonkRam(32, 0xc000, bytearray(32768)) mcf.add_Pause(111) mcf.add_EOF() mcf.write("demo.mcf") print("Verifying file...") mcf2 = MultiChunkFile() mcf2.read("demo.mcf") assert mcf2.chunks == mcf.chunks