''' Shared decode library for Kellogg's Tony & Friends asset formats. Ported (not copied) from the C# reference project https://github.com/movAX13h/tony-and-friends-in-kelloggs-land (no LICENSE file, therefore reimplemented from scratch in Python using their format docs as a guide). Container: PCKELL.DAT holds all 178 assets, footer-indexed (see DATContainer). ''' import struct import os from collections import namedtuple # --------------------------------------------------------------------------- # Container (PCKELL.DAT) # --------------------------------------------------------------------------- class DATContainer: def __init__(self, path): with open(path, 'rb') as f: self.data = f.read() self.entries = {} # filename -> bytes self._parse() def _parse(self): data = self.data num_entries = struct.unpack_from('= n: return None op = data[pc] if op == 0x03: # add si, cx (0x03 0xF1) if data[pc+1] != 0xF1: return None pc += 2 elif op == 0xCB: # retf pc += 1 break elif op in (0x56, 0x58, 0x5E, 0x50): # push si / push ax / pop si / push ax pc += 1 elif op == 0xEE: # out dx, al -> advance EGA page (cycles 0..3) ega_page = (ega_page + 1) & 3 pc += 1 elif op == 0xD0: # rol al, 1 (0xD0 0xC0) if data[pc+1] != 0xC0: return None pc += 2 elif op == 0x8A: # mov cl, ah/bl/bh (no-op for our purposes) if data[pc+1] not in (0xCC, 0xCB, 0xCF): return None pc += 2 elif op == 0xC6: sub = data[pc+1] if sub == 0x84: # mov byte ptr [si+AAAA], BB offset = data[pc+2] | (data[pc+3] << 8) const = data[pc+4] pc += 5 instrs.append(CopyInstr(ega_page, offset, bytes([const]))) elif sub == 0x44: # mov byte ptr [si+AA], BB offset = data[pc+2] const = data[pc+3] pc += 4 instrs.append(CopyInstr(ega_page, offset, bytes([const]))) else: return None elif op == 0xC7: sub = data[pc+1] if sub == 0x44: # mov word ptr [si+AA], BBBB offset = data[pc+2] const = data[pc+3] | (data[pc+4] << 8) pc += 5 instrs.append(CopyInstr(ega_page, offset, bytes([const & 0xFF, (const >> 8) & 0xFF]))) elif sub == 0x84: # mov word ptr [si+AAAA], BBBB offset = data[pc+2] | (data[pc+3] << 8) const = data[pc+4] | (data[pc+5] << 8) pc += 6 instrs.append(CopyInstr(ega_page, offset, bytes([const & 0xFF, (const >> 8) & 0xFF]))) else: return None else: return None return instrs BOB_STRIDE = 84 class BobFrame: # pixels: list of palette indices, width*height, -1 = transparent. # slot/anchor_x/anchor_y kommen aus dem 14-Byte-Frame-Header (Felder # u0/u1/u2, Fund 26.07.2026): slot = Engine-Sprite-Index (0-49 rechts, # +50 = Blickrichtung links), anchor = Fusspunkt-Anker relativ zur # linken oberen Ecke (anchor_y > height z.B. bei Cocos Hangel-Frames). __slots__ = ('width', 'height', 'pixels', 'slot', 'anchor_x', 'anchor_y') def __init__(self, width, height, slot=0, anchor_x=0, anchor_y=0): self.width = width self.height = height self.slot = slot self.anchor_x = anchor_x self.anchor_y = anchor_y self.pixels = [-1] * (width * height) def set(self, x, y, idx): if 0 <= x < self.width and 0 <= y < self.height: self.pixels[y * self.width + x] = idx def parse_bob(data): '''Returns list of BobFrame (palette-index pixel grids, -1 = transparent/unset).''' frames = [] pos = 0 n = len(data) while pos < n: header = data[pos:pos+14] if len(header) < 14: raise ValueError(f'truncated header at {pos}') slot = struct.unpack_from('h', data, 4)[0] height = struct.unpack_from('>h', data, 6)[0] unknown = struct.unpack_from('>h', data, 8)[0] if unknown != 9: raise ValueError(f'unexpected constant {unknown} (expected 9)') pos = 10 cells = [] # row-major, len width*height, each (tile, type) for _ in range(width * height): value = struct.unpack_from('>H', data, pos)[0] pos += 2 tile = value & 0x1FF ctype = value >> 9 cells.append((tile, ctype)) if pos != len(data): raise ValueError(f'missed {len(data) - pos} extra bytes at end of MAP') return width, height, cells