Files
Sprinter-SDCC/applications/PoP/toolchain/pop_pack_bg.py
T
snark13 cbae48dbc4 applications/PoP/roomtest: пол-оверлей при подъёме (draw_floor_overlay)
Проблема из динамики: при подъёме [1,2]→[0,3] нижняя часть Kid, которая
физически за полом назначения, просвечивала.

Порт SDLPoP draw_floor_overlay (seg008:1457): на кадрах подъёма 137..144,
если тайл-назначения floor-подобный (floor/pillar/stuck/torch) И тайл СЛЕВА
пуст (кромка), передний край пола (floor_left_overlay[frame-137] =
{32,151,151,150,150,151,32,32}) + низ пола рисуются ПОВЕРХ нижней части Kid.

- pop_bg: climb_overlay_tile + доп-проход в pop_fore_over_kid (новый параметр
  frame) на кадрах 137..144.
- pop_pack_bg.py: CLIMB_OVERLAY_ENV_IDS={32,150,151} явно добавлены в env-фон
  (не попадают в used render_room — рантайм-анимация); env4 count 16→24.
- roomtest: передача Kid.frame в pop_fore_over_kid.

Проверено в MAME (кадр 137): при подтягивании видна только голова/плечи Kid
над кромкой [0,3], нижняя часть скрыта за полом.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 11:37:56 +03:00

295 lines
12 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_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}
# Пол-оверлей подъёма (floor_left_overlay, draw_floor_overlay seg008): рисуется
# рантаймом поверх нижней части Kid при подтягивании на пол у кромки — НЕ
# попадает в used render_room, добавляем явно в env-фон. (32 обычно уже есть.)
CLIMB_OVERLAY_ENV_IDS = {32, 150, 151}
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(set(id for ch, id in used if ch != R.CHTAB_WALL)
| CLIMB_OVERLAY_ENV_IDS) # + пол-оверлей подъёма
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()