#!/usr/bin/env python3 """pop_pack_arc.py — сложить несколько ресурсов в один архив PBA1. ЗАЧЕМ. На диске игры лежит больше трёхсот отдельных файлов, и дело не в беспорядке: замер в MAME (2026-08-25) показал, что ОДИН `open` стоит 51,4 мс, а чтение 16 КБ из уже открытого файла — 32,6 мс. То есть 61 % времени загрузки уходит на открытие, а не на данные. Одна только стартовая пачка (kid + shadow + bg + sound + guard, ~142 файла) — это 7,3 секунды чистого `open`. Сложив группу в архив и открыв его ОДИН раз, мы эти секунды возвращаем. ФОРМАТ (512-байтовый заголовок, дальше данные): 0..3 'PBA1' магия и версия — как SPA1 у атласов, PCFG, POPQ 4 count число записей, 1..126 5..7 резерв (нули) 8.. записи по 4 байта: +0..1 offset в СЕКТОРАХ по 512 от начала файла (uint16 LE) +2..3 size в байтах (uint16 LE) Оффсет именно в секторах: архив `pv` — мегабайт, в байтах uint16 не хватило бы. В секторах uint16 покрывает 32 МБ, то есть весь наш образ. Размер при этом спокойно живёт в байтах — ни один ресурс не крупнее 16 КБ, потому что каждый обязан помещаться в EMM-страницу. Данные каждого элемента выровнены на 512, поэтому его смещение всегда целое число секторов, а чтение попадает в границу сектора DSS. """ import re import sys from pathlib import Path MAGIC = b"PBA1" HEADER = 512 SECTOR = 512 MAX_ENTRIES = (HEADER - 8) // 4 # 126 def build(out_path: Path, files: list[Path]) -> None: if len(files) > MAX_ENTRIES: raise SystemExit(f"{out_path.name}: {len(files)} файлов, " f"в заголовок влезает {MAX_ENTRIES}") header = bytearray(HEADER) header[0:4] = MAGIC header[4] = len(files) blob = bytearray() for i, f in enumerate(files): data = f.read_bytes() if len(data) > 0xFFFF: raise SystemExit(f"{f}: {len(data)} Б — размер не влезает в uint16") offset = HEADER + len(blob) assert offset % SECTOR == 0 rec = 8 + i * 4 header[rec:rec + 2] = (offset // SECTOR).to_bytes(2, "little") header[rec + 2:rec + 4] = len(data).to_bytes(2, "little") blob += data pad = -len(blob) % SECTOR blob += bytes(pad) out_path.parent.mkdir(parents=True, exist_ok=True) out_path.write_bytes(bytes(header) + bytes(blob)) def const_name(prefix: str, path: Path) -> str: """kid12.atl -> ARC_KID_KID12; kid.pal -> ARC_KID_KID_PAL.""" stem = path.name if stem.lower().endswith(".atl"): stem = stem[:-4] return f"ARC_{prefix}_" + re.sub(r"[^A-Za-z0-9]", "_", stem).upper() def write_header(path: Path, prefix: str, files: list[Path]) -> None: """Индексы элементов — константами, чтобы код не считал их на память. Порядок задаёт Makefile, и он же единственный источник истины: сдвинулся список — сдвинулись константы, а серии вроде kid0..kid27 код проверяет статически (`#if ARC_KID_KID27 - ARC_KID_KID0 != 27`). """ lines = [f"/* {path.name} — индексы в архиве {prefix}. СГЕНЕРИРОВАН", " * (toolchain/pop_pack_arc.py), руками не править. */", f"#ifndef {path.stem.upper()}_H", f"#define {path.stem.upper()}_H", "", f"#define ARC_{prefix}_COUNT {len(files)}", ""] width = max(len(const_name(prefix, f)) for f in files) for i, f in enumerate(files): lines.append(f"#define {const_name(prefix, f):<{width}} {i}") lines += ["", "#endif", ""] path.write_text("\n".join(lines)) def main() -> None: args = sys.argv[1:] header = prefix = None while args and args[0].startswith("--"): opt = args.pop(0) if opt == "--header": header = Path(args.pop(0)) elif opt == "--prefix": prefix = args.pop(0) else: raise SystemExit(f"неизвестный ключ {opt}") if len(args) < 2: raise SystemExit("usage: pop_pack_arc.py [--header H.h --prefix NAME] " "OUT.ARC file [file ...]") out = Path(args[0]) files = [Path(a) for a in args[1:]] for f in files: if not f.exists(): raise SystemExit(f"нет файла: {f}") build(out, files) if header: if not prefix: raise SystemExit("--header требует --prefix") write_header(header, prefix, files) total = out.stat().st_size print(f"{out.name}: {len(files)} записей, {total / 1024:.0f} КБ") if __name__ == "__main__": main()