#!/usr/bin/env python3 """pop_pack_title.py — титульные композиции SDLPoP для Sprinter. Исходник — PNG-ресурсы ``data/TITLE/res51..55`` из локального SDLPoP. Runtime не должен держать 320x200 картинку в W2 и не разбирает PNG: каждая готовая композиция разрезана на пять getimage-лент высотой не более 49 строк. Лента вместе с заголовком атласа занимает одну EMM-страницу; экран рисуется лента за лентой, и страница сразу освобождается. Композиции в точности повторяют ``show_title()`` до story/intro: t0 — основной рисунок; t1 — рисунок + PRESENTS; t2 — рисунок + название игры; t3 — рисунок + логотип PRINCE OF PERSIA и Jordan Mechner. """ from pathlib import Path from PIL import Image ROOT = Path(__file__).resolve().parent.parent SRC = ROOT / "SDLPoP" / "data" / "TITLE" OUT = ROOT / "poc" / "res" / "title" ATL_MAGIC = b"SPA1" ATL_DIR_OFF = 0x68 ATL_MIN_SIZE = 0x100 PAGE_SIZE = 16384 STRIP_H = 49 def atlas_blob(pixels: bytes, width: int, height: int) -> bytes: """Один непрозрачный getimage в минимальном SPA1-атласе.""" data_off = ATL_DIR_OFF + 8 out = bytearray(data_off) out[:4] = ATL_MAGIC out[4] = 1 out[ATL_DIR_OFF:ATL_DIR_OFF + 2] = data_off.to_bytes(2, "little") # Каталог хранит preview-габарит в uint8, но atlas_image читает только # offset; реальный getimage-width — u16 в блобе. Так уже пакуются # широкие 320-пиксельные полосы фона. out[ATL_DIR_OFF + 2] = min(width, 255) out[ATL_DIR_OFF + 3] = height out[ATL_DIR_OFF + 4] = out[ATL_DIR_OFF + 5] = 1 out += width.to_bytes(2, "little") + height.to_bytes(2, "little") out += pixels if len(out) < ATL_MIN_SIZE: out += bytes(ATL_MIN_SIZE - len(out)) if len(out) > PAGE_SIZE: raise SystemExit(f"title atlas {len(out)} B > {PAGE_SIZE} B") return bytes(out) def image(n: int) -> Image.Image: im = Image.open(SRC / f"res{n}.png") if im.mode != "P": raise SystemExit(f"res{n}.png: expected indexed PNG, got {im.mode}") return im.copy() def assert_palette(base: Image.Image, overlay: Image.Image, n: int) -> None: if base.getpalette()[:48] != overlay.getpalette()[:48]: raise SystemExit(f"res{n}.png uses a palette different from res51.png") def paste_opaque(dst: Image.Image, src: Image.Image, x: int, y: int, n: int) -> None: assert_palette(dst, src, n) dst.paste(src, (x, y)) def paste_transparent(dst: Image.Image, src: Image.Image, x: int, y: int, n: int) -> None: assert_palette(dst, src, n) # TITLE_POP — единственный из пяти элементов, который SDLPoP рисует # blitters_10h_transp. В PNG тот же факт записан colour-key index 0. # Image.point() у P-изображения возвращает снова P, а paste() принимает # маску только L/1/RGBA. Строим L явно, не меняя индексы самой картинки. mask = Image.frombytes("L", src.size, bytes(0 if p == 0 else 255 for p in src.tobytes())) dst.paste(src, (x, y), mask) # Семнадцатая запись — фон текстовой рамки story (STORY_BG_INDEX в # pop_pack_intro.py). Она нужна здесь потому, что «In the absence…» # проявляется полосами ПОВЕРХ последнего кадра титров, то есть при ещё # загруженной title.pal: без этой записи фон рамки был бы чёрным. STORY_BG_INDEX = 16 STORY_BG_COLOR = (0x10, 0x00, 0x60) def write_palette(base: Image.Image) -> None: rgb = base.getpalette()[:48] pal = bytearray() for i in range(16): r, g, b = rgb[i * 3:i * 3 + 3] pal += bytes((b, g, r, 0)) # родной формат gfx_pal_fload assert len(pal) == STORY_BG_INDEX * 4 r, g, b = STORY_BG_COLOR pal += bytes((b, g, r, 0)) (OUT / "title.pal").write_bytes(pal) def write_scene(scene: int, canvas: Image.Image) -> int: count = 0 for part, top in enumerate(range(0, canvas.height, STRIP_H)): band = canvas.crop((0, top, canvas.width, min(top + STRIP_H, canvas.height))) path = OUT / f"t{scene}_{part}.atl" path.write_bytes(atlas_blob(band.tobytes(), band.width, band.height)) count += 1 return count def main() -> None: OUT.mkdir(parents=True, exist_ok=True) main_image = image(51) if main_image.size != (320, 200): raise SystemExit(f"res51.png: expected 320x200, got {main_image.size}") presents = main_image.copy() paste_opaque(presents, image(52), 96, 106, 52) game = main_image.copy() paste_opaque(game, image(53), 96, 122, 53) logo = main_image.copy() paste_transparent(logo, image(54), 24, 107, 54) paste_opaque(logo, image(55), 48, 184, 55) write_palette(main_image) scenes = (main_image, presents, game, logo) strips = [write_scene(i, scene) for i, scene in enumerate(scenes)] assert strips == [5, 5, 5, 5] print(f"{OUT}: {len(scenes)} title compositions x {strips[0]} strips, " f"one strip <= {STRIP_H} rows / one EMM page") if __name__ == "__main__": main()