Files
Sprinter-SDCC/toolchain/png_strip.py
T
snark13 cd8d566d82 applications/PoP: порт Prince of Persia — PoC (roomtest) + пайплайн
Порт PoP на Sprinter.  Текущий PoC — applications/PoP/roomtest/:
комната 1 (фон-композиция тайлов) + Kid с управлением на raw-клавиатуре
и коллизией с картой.

- roomtest — pop_bg (фон), pop_kid (спрайты Kid, column-major флип,
  seqtbl-анимация), pop_ctrl (порт control() PoP на held-state
  kbd_raw), pop_map (коллизия seg004/005: бег/стоп у стены,
  падение/приземление, отскок seq_47, вертикальный прыжок K4.1).
  MEMORY=small (DATA сразу за CODE, ~23КБ кода не лезет в huge).
- toolchain (PoP) — pop_pack_kid/pop_pack_bg/render_room/extract —
  распаковка res-графики MSDOS в атласы + композиция комнат.
- toolchain/ (корень) — make_hdd.sh (быстрый HDD-тест вместо FDD),
  png_strip.py / room_compose.py (ассет-пайплайн).
- docs — PORT_PLAN, KID_PLAN, форматы ресурсов (Apple II / MSDOS / DAT).
- bgtest/coltest/poc — ранние PoC (фон, коллизия, первый прототип).

.gitignore: build-артефакты applications/*/*/*; исключены внешние
референс-репозитории (SDLPoP/mininim/PR/Apple-II — свои git-клоны) и
оригинальные game-данные MSDOS/ (копирайт, только для реверса форматов).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 17:51:08 +03:00

142 lines
6.4 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
"""
png_strip.py — генерик-конвертер произвольного набора PNG-кадров (РАЗНОГО
размера каждый) в одну raw-ленту + палитру для sprite-движка Sprinter
(формат getimage/atlas — см. libbgi/include/sprite.h, toolchain/mkatlas.py).
Не завязан ни на какой конкретный проект/игру — просто берёт список
картинок, которые вы сами укажете, и паковает их в ленту с общим
канвасом (кадр движка обязан быть фиксированного размера).
Использование:
png_strip.py OUT.raw OUT.pal img1.png img2.png ... [опции]
Опции:
--canvas WxH размер канваса кадра (по умолчанию — максимум
ширины/высоты среди входных картинок)
--anchor MODE bottom-center (по умолчанию) | top-left | center
--base-index N с какого индекса палитры начинать свои цвета
(по умолчанию 16 — под 16 EGA-слотов initgraph)
--ega добавить в палитру 16 EGA-цветов BGI перед своими
(по умолчанию включено; --no-ega выключает)
--seed-pal FILE переиспользовать цвета уже существующей палитры
(EGA16 + N своих) — свои индексы этой ленты
продолжаются ПОСЛЕ них (не с --base-index), а
OUT.pal получится ПОЛНОЙ merged-палитрой
(ega+seed+свои), готовой как --seed-pal для
следующего вызова. Так несколько независимо
собранных лент (напр. разные типы тайлов) можно
свести в ОДНУ палитру для одновременного показа
на экране (см. applications/PoP/poc/tools/
build_room_palette.py).
Каждая картинка — источник ОДНОГО кадра ленты (не атлас-сетка;
для сетки — см. mkatlas.py raw:FWxFH:NXxNY после этого шага, если
нужно). Прозрачность — альфа < 128 → индекс 0xFF (сентинел движка).
Дальше: toolchain/mkatlas.py OUT.atl OUT.raw:CW x CH:1xN
"""
import argparse
import sys
from PIL import Image
EGA = [
(0, 0, 0), (168, 0, 0), (0, 168, 0), (168, 168, 0),
(0, 0, 168), (168, 0, 168), (0, 84, 168), (168, 168, 168),
(84, 84, 84), (255, 84, 84), (84, 255, 84), (255, 255, 84),
(84, 84, 255), (255, 84, 255), (84, 255, 255), (255, 255, 255),
]
def die(msg):
sys.exit(f"png_strip: {msg}")
def main():
ap = argparse.ArgumentParser(add_help=False)
ap.add_argument("out_raw")
ap.add_argument("out_pal")
ap.add_argument("images", nargs="+")
ap.add_argument("--canvas", default=None)
ap.add_argument("--anchor", default="bottom-center",
choices=["bottom-center", "top-left", "center"])
ap.add_argument("--base-index", type=int, default=16)
ap.add_argument("--ega", dest="ega", action="store_true", default=True)
ap.add_argument("--no-ega", dest="ega", action="store_false")
ap.add_argument("--seed-pal", default=None)
args = ap.parse_args()
imgs = [Image.open(p).convert("RGBA") for p in args.images]
seed_records = []
if args.seed_pal:
raw = open(args.seed_pal, "rb").read()
if len(raw) % 4:
die(f"--seed-pal {args.seed_pal}: длина не кратна 4")
n_ega = 16 if args.ega else 0
for i in range(n_ega, len(raw) // 4):
b, g, r, _ = raw[i * 4:i * 4 + 4]
seed_records.append((r, g, b))
args.base_index = 16 + len(seed_records)
if args.canvas:
cw, ch = (int(v) for v in args.canvas.lower().split("x"))
else:
cw = max(im.width for im in imgs)
ch = max(im.height for im in imgs)
if cw > 64 or ch > 64:
die(f"канвас {cw}x{ch} превышает лимит движка 64x64")
for p, im in zip(args.images, imgs):
if im.width > cw or im.height > ch:
die(f"{p}: {im.width}x{im.height} не влезает в канвас {cw}x{ch}")
colors = {}
for i, rgb in enumerate(seed_records):
colors[rgb] = 16 + i
def color_index(r, g, b):
key = (r, g, b)
if key not in colors:
idx = args.base_index + (len(colors) - len(seed_records))
if idx >= 0xFF:
die("палитра переполнена (>= 0xFF своих цветов)")
colors[key] = idx
return colors[key]
data = bytearray()
for im in imgs:
if args.anchor == "top-left":
ox, oy = 0, 0
elif args.anchor == "center":
ox, oy = (cw - im.width) // 2, (ch - im.height) // 2
else: # bottom-center
ox, oy = (cw - im.width) // 2, ch - im.height
canvas = [0xFF] * (cw * ch)
for y in range(im.height):
for x in range(im.width):
r, g, b, a = im.getpixel((x, y))
if a < 128:
continue
canvas[(oy + y) * cw + (ox + x)] = color_index(r, g, b)
data.extend(canvas)
open(args.out_raw, "wb").write(data)
pal = bytearray()
if args.ega:
for b, g, r in EGA:
pal += bytes((b, g, r, 0))
new_colors = [rgb for rgb in colors if rgb not in seed_records]
for (r, g, b) in (*seed_records, *new_colors):
pal += bytes((b, g, r, 0))
open(args.out_pal, "wb").write(pal)
print(f"png_strip: {len(imgs)} кадров, канвас {cw}x{ch}, "
f"{len(data)} Б -> {args.out_raw}")
print(f" палитра {len(seed_records)} seed + {len(new_colors)} новых "
f"своих цветов ({len(pal)} Б) -> {args.out_pal}")
print(f"дальше: mkatlas.py OUT.atl {args.out_raw}:{cw}x{ch}:1x{len(imgs)}")
if __name__ == "__main__":
main()