Files
Sprinter-SDCC/applications/PoP/toolchain/pop_pack_title.py
T

138 lines
5.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()