#!/usr/bin/env python3 """ mkatlas.py — упаковщик атласов спрайтов Sprinter (.atl, вариант II — docs/sprite-api-design.md §3.1). Формат файла (== образ EMM-страницы, файл-офсет == W0-адрес): 0x000 'S','P','A','1', count u8, резерв ×3 0x008 резерв (0x38-0x3A и 0x66-0x67 — под патч ISR-стаба загрузчиком) 0x068 каталог: count × 8 Б {offset u16 LE, fw u8, fh u8, nx u8, ny u8, резерв u16} — offset указывает на getimage-ЗАГОЛОВОК ленты 0x100 ленты подряд: u16 w=fw*nx, u16 h=fh*ny (LE) + пиксели построчно Вход — список лент: img.png:FWxFH индексированный PNG (mode P/L; байты как есть, прозрачность = индекс 0xFF); сетка nx,ny из размеров картинки img.raw:FWxFH:NXxNY сырые байты (fw*nx)×(fh*ny), построчно Пример: mkatlas.py sprites.atl hero.png:16x16 coin.png:16x16 boss.raw:24x24:2x2 Ограничения: count ≤ 19, файл ≤ 16384 Б, кадр ≤ 64×64. """ import os import sys HDR_SIZE = 0x100 DIR_OFF = 0x68 MAX_ENTRY = (HDR_SIZE - DIR_OFF) // 8 # 19 PAGE_SIZE = 16384 def die(msg): sys.exit(f"mkatlas: {msg}") def load_png(path): from PIL import Image im = Image.open(path) if im.mode not in ("P", "L"): die(f"{path}: нужен индексированный PNG (mode P/L), не {im.mode}") return im.width, im.height, bytes(im.tobytes()) def parse_spec(spec): parts = spec.split(":") if len(parts) < 2: die(f"'{spec}': нужен вид file:FWxFH[:NXxNY]") path = parts[0] fw, fh = (int(v) for v in parts[1].lower().split("x")) if not (1 <= fw <= 64 and 1 <= fh <= 64): die(f"{path}: кадр {fw}x{fh} вне 1..64 (контракт спрайтов)") if path.lower().endswith(".png"): if len(parts) != 2: die(f"{path}: для PNG сетка берётся из размеров картинки") w, h, pix = load_png(path) else: if len(parts) != 3: die(f"{path}: для raw обязательна сетка :NXxNY") nx, ny = (int(v) for v in parts[2].lower().split("x")) w, h = fw * nx, fh * ny pix = open(path, "rb").read() if len(pix) != w * h: die(f"{path}: {len(pix)} байт, ожидалось {w}x{h}={w * h}") if w % fw or h % fh: die(f"{path}: {w}x{h} не делится на кадр {fw}x{fh}") nx, ny = w // fw, h // fh if not (1 <= nx <= 255 and 1 <= ny <= 255): die(f"{path}: сетка {nx}x{ny} вне 1..255") return dict(path=path, fw=fw, fh=fh, nx=nx, ny=ny, w=w, h=h, pix=pix) def main(): if len(sys.argv) < 3: sys.exit(__doc__.strip()) out, specs = sys.argv[1], sys.argv[2:] if len(specs) > MAX_ENTRY: die(f"лент {len(specs)} > {MAX_ENTRY} (каталог 0x68-0xFF)") entries = [parse_spec(s) for s in specs] blob = bytearray(HDR_SIZE) blob[0:4] = b"SPA1" blob[4] = len(entries) off = HDR_SIZE for i, e in enumerate(entries): d = DIR_OFF + i * 8 blob[d:d + 2] = off.to_bytes(2, "little") blob[d + 2] = e["fw"] blob[d + 3] = e["fh"] blob[d + 4] = e["nx"] blob[d + 5] = e["ny"] blob += e["w"].to_bytes(2, "little") # getimage-заголовок ленты blob += e["h"].to_bytes(2, "little") blob += e["pix"] off = len(blob) if len(blob) > PAGE_SIZE: die(f"итог {len(blob)} Б > {PAGE_SIZE} (одна EMM-страница); " f"разбейте на несколько .atl") with open(out, "wb") as f: f.write(blob) print(f"mkatlas: {out}: лент {len(entries)}, " f"{len(blob)} Б (свободно {PAGE_SIZE - len(blob)})") for i, e in enumerate(entries): print(f" [{i}] {os.path.basename(e['path'])}: кадр " f"{e['fw']}x{e['fh']}, сетка {e['nx']}x{e['ny']}") if __name__ == "__main__": main()