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>
This commit is contained in:
@@ -0,0 +1,288 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
pop_pack_bg.py — упаковщик спрайтов СТАТИЧЕСКОГО ФОНА PoP (Шаг 2 порта).
|
||||
|
||||
Что делает:
|
||||
1. Определяет РЕАЛЬНО используемый набор спрайтов фона (прогоняет
|
||||
render_room.py по всем комнатам всех уровней, собирает (chtab,id)).
|
||||
2. Грузит исходные PNG (VDUNGEON→VPALACE каскад, как в игре), переводит
|
||||
16-цветный индексированный пиксель в 8bpp getimage-блоб:
|
||||
индекс 0 -> 0xFF (прозрачность getimage)
|
||||
индекс i -> 0x50+i (env, chtab_6) / 0x60+i (wall, chtab_7)
|
||||
3. Пакует в атласы .atl с ПРЯМОЙ адресацией (idx = id), каталог любого
|
||||
размера (блобы кладутся ПОСЛЕ каталога — загрузчик atlas_image читает
|
||||
offset явно, лимит 19 из libbgi/mkatlas.py не действует). libbgi НЕ
|
||||
трогаем. Дырки в каталоге (неиспользуемые id) = фейковые записи,
|
||||
ссылающиеся на общий пустой 0×0-блоб (осознанная трата места в EMM
|
||||
ради нулевых remap-таблиц в W2 — требование пользователя).
|
||||
4. Собирает палитру Sprinter (.pal, записи B,G,R,0) из res200.pal (env
|
||||
-> слоты 0x50..0x5F) и res360.pal (wall -> 0x60..0x6F). VGA 6-бит
|
||||
каналы масштабируются в 8-бит.
|
||||
5. Пишет C-заголовок с раскладкой (имена файлов, SHIFT, число страниц,
|
||||
набор fore-id).
|
||||
|
||||
Раскладка (прямая адресация, SHIFT=5):
|
||||
ENV фон : 5 страниц, страница = id>>5, idx = id&31
|
||||
WALL : 1 страница, idx = id
|
||||
FORE : 1 страница (спрайты «перед персонажем»: фронты столбов/
|
||||
ворот/дебриса), idx = id — отдельный атлас (жизненный цикл
|
||||
другой: рисуются каждый кадр как sprite_t, не запекаются).
|
||||
W2-цена: 7 хэндлов atlas_t (~21 Б), ноль таблиц id->позиция.
|
||||
|
||||
Выход: в applications/PoP/poc/res/bg/ — *.atl + pop_bg.pal + pop_bg_atlas.h
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
from PIL import Image
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, HERE)
|
||||
import render_room as R # noqa: E402 (экстрактор + каскад загрузки)
|
||||
|
||||
OUT_DIR = os.path.join(HERE, "..", "poc", "res", "bg")
|
||||
|
||||
# Спрайты FORE-слоя (add_foretable в seg008 — «перед персонажем»): фронты
|
||||
# столбов, ворот, дебриса. См. tile_table.fore_id + анализ pop_background_strategy.
|
||||
FORE_ENV_IDS = {9, 12, 49, 88, 91, 95, 100}
|
||||
|
||||
ENV_SHIFT = 5 # env фон: страница = id>>5, idx = id&31
|
||||
ENV_MASK = (1 << ENV_SHIFT) - 1
|
||||
|
||||
# ---- .atl формат (== libbgi, но каталог не ограничен 19) --------------
|
||||
ATL_MAGIC = b"SPA1"
|
||||
ATL_DIR_OFF = 0x68 # _ATL_DIR_OFF
|
||||
ATL_MIN_SIZE = 0x100 # atlas_load требует n >= _ATL_DATA_OFF
|
||||
PAGE_SIZE = 16384
|
||||
|
||||
|
||||
def die(msg):
|
||||
sys.exit(f"pop_pack_bg: {msg}")
|
||||
|
||||
|
||||
# ---- 1. Набор используемых спрайтов ----------------------------------
|
||||
def collect_used():
|
||||
"""Прогон render_room по всем комнатам -> множество (chtab, id)."""
|
||||
used = set()
|
||||
orig = R.load_sprite
|
||||
|
||||
def spy(id, chtab=R.CHTAB_ENV, reason=""):
|
||||
img = orig(id, chtab, reason)
|
||||
if img is not None and id > 0:
|
||||
used.add((chtab, id))
|
||||
return img
|
||||
|
||||
R.load_sprite = spy
|
||||
for lvl in range(1, 15):
|
||||
fn = os.path.join(R.LEVEL_DIR, f"res{2000 + lvl}.bin")
|
||||
if not os.path.exists(fn):
|
||||
continue
|
||||
lv = R.Level(open(fn, "rb").read())
|
||||
for room in range(1, 25):
|
||||
rm = R.Room(lv, room)
|
||||
L = lv.links[room][0]
|
||||
if L:
|
||||
rm.left_room = R.Room(lv, L)
|
||||
canvas = Image.new("RGBA", (320, 192), (0, 0, 0, 0))
|
||||
for dr in (2, 1, 0):
|
||||
for c in range(10):
|
||||
R.draw_tile(canvas, rm, dr, c)
|
||||
R.load_sprite = orig
|
||||
return used
|
||||
|
||||
|
||||
# ---- 2. Загрузка спрайта в 8bpp-блоб ---------------------------------
|
||||
def load_indexed(chtab, id):
|
||||
"""PNG (mode P, 16 цветов) -> (w, h, bytes 8bpp). Каскад VDUNGEON->VPALACE.
|
||||
Пиксель 0 -> 0xFF (прозрач); i -> base+i (env 0x50 / wall 0x60)."""
|
||||
base_res = 360 if chtab == R.CHTAB_WALL else 200
|
||||
pal_base = 0x60 if chtab == R.CHTAB_WALL else 0x50
|
||||
fname = f"res{base_res + id}.png"
|
||||
path = None
|
||||
for d in (R.VDUNGEON_DIR, R.VPALACE_DIR):
|
||||
p = os.path.join(d, fname)
|
||||
if os.path.exists(p):
|
||||
path = p
|
||||
break
|
||||
if path is None:
|
||||
die(f"нет файла {fname}")
|
||||
im = Image.open(path)
|
||||
if im.mode != "P":
|
||||
die(f"{fname}: mode {im.mode}, ожидался P (16-цветный)")
|
||||
src = im.tobytes()
|
||||
out = bytes((0xFF if v == 0 else pal_base + v) for v in src)
|
||||
return im.width, im.height, out
|
||||
|
||||
|
||||
def blob(w, h, pix):
|
||||
"""getimage-блоб: u16 w, u16 h (LE) + пиксели."""
|
||||
return w.to_bytes(2, "little") + h.to_bytes(2, "little") + pix
|
||||
|
||||
|
||||
# ---- 3. Пакер .atl (каталог любого размера) --------------------------
|
||||
def pack_atlas(path, entries):
|
||||
"""entries: dict idx -> (w, h, pix). Пустые idx (0..maxidx) заполняются
|
||||
фейковой ссылкой на общий 0×0-блоб. Каталог count записей с 0x68,
|
||||
блобы — сразу после каталога."""
|
||||
if not entries:
|
||||
die(f"{path}: пустой атлас")
|
||||
count = max(entries) + 1
|
||||
if count > 255:
|
||||
die(f"{path}: count {count} > 255")
|
||||
data_off = ATL_DIR_OFF + count * 8
|
||||
|
||||
blobs = bytearray()
|
||||
empty_off = data_off + len(blobs) # общий 0×0-блоб для дырок
|
||||
blobs += blob(0, 0, b"")
|
||||
dir_bytes = bytearray(count * 8)
|
||||
for idx in range(count):
|
||||
e = entries.get(idx)
|
||||
if e is None:
|
||||
off = empty_off
|
||||
fw = fh = 0
|
||||
else:
|
||||
w, h, pix = e
|
||||
off = data_off + len(blobs)
|
||||
blobs += blob(w, h, pix)
|
||||
fw = min(w, 255)
|
||||
fh = min(h, 255)
|
||||
d = idx * 8
|
||||
dir_bytes[d:d + 2] = off.to_bytes(2, "little")
|
||||
dir_bytes[d + 2] = fw
|
||||
dir_bytes[d + 3] = fh
|
||||
dir_bytes[d + 4] = 1 # nx
|
||||
dir_bytes[d + 5] = 1 # ny
|
||||
# d+6,d+7 резерв
|
||||
|
||||
out = bytearray(data_off)
|
||||
out[0:4] = ATL_MAGIC
|
||||
out[4] = count
|
||||
out[ATL_DIR_OFF:ATL_DIR_OFF + len(dir_bytes)] = dir_bytes
|
||||
out += blobs
|
||||
if len(out) < ATL_MIN_SIZE:
|
||||
out += bytes(ATL_MIN_SIZE - len(out)) # atlas_load: n >= 0x100
|
||||
if len(out) > PAGE_SIZE:
|
||||
die(f"{path}: {len(out)} Б > {PAGE_SIZE} (одна EMM-страница)")
|
||||
with open(path, "wb") as f:
|
||||
f.write(out)
|
||||
return count, len(out)
|
||||
|
||||
|
||||
# ---- 4. Палитра ------------------------------------------------------
|
||||
def scale6to8(v):
|
||||
return (v * 255) // 63
|
||||
|
||||
|
||||
def build_palette(out_path):
|
||||
"""256-записная палитра B,G,R,0: env@0x50..0x5F, wall@0x60..0x6F."""
|
||||
def read_pal(res, d):
|
||||
b = open(os.path.join(d, res), "rb").read()
|
||||
return [(b[4 + i * 3], b[4 + i * 3 + 1], b[4 + i * 3 + 2]) for i in range(16)]
|
||||
|
||||
env = read_pal("res200.pal", R.VDUNGEON_DIR)
|
||||
wall = read_pal("res360.pal", R.VDUNGEON_DIR)
|
||||
pal = bytearray(256 * 4)
|
||||
for i, (r, g, b) in enumerate(env):
|
||||
o = (0x50 + i) * 4
|
||||
pal[o:o + 4] = bytes((scale6to8(b), scale6to8(g), scale6to8(r), 0))
|
||||
for i, (r, g, b) in enumerate(wall):
|
||||
o = (0x60 + i) * 4
|
||||
pal[o:o + 4] = bytes((scale6to8(b), scale6to8(g), scale6to8(r), 0))
|
||||
with open(out_path, "wb") as f:
|
||||
f.write(pal)
|
||||
|
||||
|
||||
# ---- 5. C-заголовок --------------------------------------------------
|
||||
def write_header(out_path, env_pages, wall_name, fore_name):
|
||||
lines = [
|
||||
"/* pop_bg_atlas.h — раскладка атласов статического фона PoP.",
|
||||
" * Сгенерировано toolchain/pop_pack_bg.py — НЕ править вручную.",
|
||||
" *",
|
||||
" * Прямая адресация (ноль remap-таблиц в W2):",
|
||||
" * ENV фон id N -> atlas env_bg[N>>%d], idx N&%d" % (ENV_SHIFT, ENV_MASK),
|
||||
" * WALL id N -> atlas wall, idx N",
|
||||
" * FORE id N -> atlas fore, idx N",
|
||||
" */",
|
||||
"#ifndef POP_BG_ATLAS_H",
|
||||
"#define POP_BG_ATLAS_H",
|
||||
"",
|
||||
"#define POP_ENV_SHIFT %d" % ENV_SHIFT,
|
||||
"#define POP_ENV_MASK %d" % ENV_MASK,
|
||||
"#define POP_ENV_PAGES %d" % len(env_pages),
|
||||
"",
|
||||
"/* Палитра: env-слоты, wall-слоты (сприйт-пиксель i -> база+i). */",
|
||||
"#define POP_PAL_ENV 0x50",
|
||||
"#define POP_PAL_WALL 0x60",
|
||||
"",
|
||||
"/* Имена файлов атласов (грузятся atlas_load). */",
|
||||
"static const char *const pop_env_atl[POP_ENV_PAGES] = {",
|
||||
]
|
||||
for pg in range(len(env_pages)):
|
||||
nm = env_pages[pg]
|
||||
lines.append(' %s,' % ('"%s"' % nm if nm else "0 /* пусто */"))
|
||||
lines += [
|
||||
"};",
|
||||
'#define POP_WALL_ATL "%s"' % wall_name,
|
||||
'#define POP_FORE_ATL "%s"' % fore_name,
|
||||
'#define POP_BG_PAL "pop_bg.pal"',
|
||||
"",
|
||||
"#endif",
|
||||
"",
|
||||
]
|
||||
with open(out_path, "w") as f:
|
||||
f.write("\n".join(lines))
|
||||
|
||||
|
||||
def main():
|
||||
os.makedirs(OUT_DIR, exist_ok=True)
|
||||
used = collect_used()
|
||||
env_ids = sorted(id for ch, id in used if ch != R.CHTAB_WALL)
|
||||
wall_ids = sorted(id for ch, id in used if ch == R.CHTAB_WALL)
|
||||
|
||||
env_bg = [i for i in env_ids if i not in FORE_ENV_IDS]
|
||||
env_fore = [i for i in env_ids if i in FORE_ENV_IDS]
|
||||
|
||||
# ENV фон -> страницы по id>>SHIFT
|
||||
pages = {}
|
||||
for id in env_bg:
|
||||
w, h, pix = load_indexed(R.CHTAB_ENV, id)
|
||||
pages.setdefault(id >> ENV_SHIFT, {})[id & ENV_MASK] = (w, h, pix)
|
||||
max_pg = max(pages) if pages else -1
|
||||
env_page_names = []
|
||||
total_bytes = 0
|
||||
for pg in range(max_pg + 1):
|
||||
if pg in pages:
|
||||
nm = f"pop_env{pg}.atl"
|
||||
cnt, sz = pack_atlas(os.path.join(OUT_DIR, nm), pages[pg])
|
||||
total_bytes += sz
|
||||
env_page_names.append(nm)
|
||||
print(f" {nm}: {len(pages[pg])} спрайтов, каталог {cnt}, {sz} Б")
|
||||
else:
|
||||
env_page_names.append(None)
|
||||
print(f" (env стр {pg}: пусто)")
|
||||
|
||||
# WALL -> idx=id
|
||||
wall_entries = {id: load_indexed(R.CHTAB_WALL, id) for id in wall_ids}
|
||||
cnt, sz = pack_atlas(os.path.join(OUT_DIR, "pop_wall.atl"), wall_entries)
|
||||
total_bytes += sz
|
||||
print(f" pop_wall.atl: {len(wall_ids)} спрайтов, каталог {cnt}, {sz} Б")
|
||||
|
||||
# FORE -> idx=id
|
||||
fore_entries = {id: load_indexed(R.CHTAB_ENV, id) for id in env_fore}
|
||||
cnt, sz = pack_atlas(os.path.join(OUT_DIR, "pop_fore.atl"), fore_entries)
|
||||
total_bytes += sz
|
||||
print(f" pop_fore.atl: {len(env_fore)} спрайтов, каталог {cnt}, {sz} Б")
|
||||
|
||||
build_palette(os.path.join(OUT_DIR, "pop_bg.pal"))
|
||||
write_header(os.path.join(OUT_DIR, "pop_bg_atlas.h"),
|
||||
env_page_names, "pop_wall.atl", "pop_fore.atl")
|
||||
|
||||
n_pages = sum(1 for n in env_page_names if n) + 2
|
||||
print(f"ИТОГО: {len(env_bg)} env-фон + {len(wall_ids)} wall + "
|
||||
f"{len(env_fore)} fore = {len(env_bg)+len(wall_ids)+len(env_fore)} спрайтов")
|
||||
print(f" {n_pages} EMM-страниц, {total_bytes} Б на диске "
|
||||
f"(+1КБ палитра), W2: {n_pages}×atlas_t = {n_pages*3} Б хэндлов")
|
||||
print(f" -> {OUT_DIR}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user