Files
Sprinter-SDCC/applications/SprPoP/tools/pop_pack_guard.py
T
snark13 31b82661eb SprPoP: автономное приложение, выделенное из roomtest
Порт PoP переехал в applications/SprPoP — приложение, которое собирается
само: код, оригинальные данные, конверторы ресурсов и сборка внутри одной
папки.  Наружу знает единственный путь — корень тулчейна (SPRINTER_ROOT,
по умолчанию ../..).  applications/PoP/roomtest ЗАМОРОЖЕНА и остаётся
архивом закрытых задач, багов и исполненных планов.

Скопировано из applications/PoP/roomtest@4b74478.  Перенос проверен
побайтово: собранный sprpop.exe совпал с roomtest.exe того же коммита,
все 39 дисковых ресурсов и все 16 генерируемых заголовков — тоже, host-
тесты зелёные (15/15).

Раскладка:
  src/           рукописный C (roomtest.c -> sprpop.c)
  gen/           генерируемые заголовки, в репозитории
  assets/orig/   оригинальные данные игры, вне репозитория (копирайт)
  assets/packed/ то, что ложится на диск, в раскладке диска
  tools/         конверторы; все пути — в одном tools/paths.py
  build/         выход: exe, каталоги ресурсов, hdd/, промежуточные atl/

Сборка ресурсов: assets/packed и gen — версионируемые ВХОДЫ, а не то, что
пересчитывается каждым make.  Автоматика построена на ОТСУТСТВИИ файла, а
не на таймстемпах: git не хранит времена, и в свежем клоне сравнение по
времени превращалось бы в лотерею.  Недостающий ресурс или заголовок
чинится сам, рекурсивным вызовом в ветку генерации.

Музыка собирается из любого из четырёх наборов записей (make music-mp3,
music-mt32, ...); набор входит в имя stamp'а, поэтому смена набора сама
делает музыку устаревшей.  Длины реплик больше не захардкожены: упаковщик
печатает их в gen/pop_music_ticks.h, и шкала сцены выражена через них —
иначе mt32 (реплики на 6% длиннее) молча ломал катсцену.

Тулчейн: в app.mk два обратносовместимых крючка (SRC_DIR/BUILD_DIR),
HDD_IMG стал ?=; команда сборки roomtest не изменилась.  Корневой
make host-tests переключён на SprPoP.

Подгонка тайминга катсцены с принцессой (PV_MAGIC_LEAD): сцена
render-bound и идёт ~49 тиков/с вместо 60, из-за чего кода реплики
приходила раньше молнии.  Это обход, а не лечение; разбор с замерами —
docs/BUGS_OPEN.md, записи SND-PACE-DEAD, PV-RENDER-BOUND, MUS-LEFT-TEAR.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 12:12:28 +03:00

217 lines
10 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_guard.py — упаковщик спрайтов СТРАЖА (chtab_5_guard, база 750).
Оригинал грузит стражей из отдельного .DAT по типу уровня
(GUARD/FAT/SKEL/VIZIER/SHADOW, seg000:1092); уровень 1 — обычный «guard»,
то есть data/GUARD/res(750+id).png. Палитра — НЕ из PNG: оригинал
подменяет её из res10.bin (guard_palettes, 7 палитр по 16 цветов, 6-бит) по
level.guards_color; на уровне 1 у обоих стражей color = 2.
Выход: build/atl/guard/g0..gN.atl (по 8 спрайтов на страницу, адресация
id>>3 / id&7 — как у Kid) + группа палитры 0x90..0x9F в общий kid.pal
(его собирает pop_pack_kid.py, см. GUARD_PAL_BASE там).
"""
import os
import sys
from PIL import Image
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import pop_pack_bg as B
from pop_pack_kid import transpose_cols, vflip_page
import sys as _sys, os as _os
_sys.path.insert(0, _os.path.dirname(_os.path.abspath(__file__)))
import paths as P # раскладка каталогов SprPoP (tools/paths.py)
# Набор спрайтов соперника выбирается ТИПОМ УРОВНЯ (load_lev_spr, seg000:1092):
# tbl_guard_type = {0 guard, 1 fat, 2 skeleton, 3 vizier, 4 shadow, 1 нет}.
# У обычного стража палитра подменяется из res10.bin по guards_color, у
# остальных наборов curr_guard_color = 0 (seg002:186) — значит цвет берётся
# из ФАЙЛА набора (res750.pal), и подменять его не надо.
SETS = {
"GUARD": ("GUARD", "guard", None),
"SKEL": ("SKEL", "skel", "res750.pal"),
"VIZIER": ("VIZIER", "vizier", "res750.pal"),
}
SET_NAME = "GUARD"
GUARD_DIR = str(P.SDLPOP_DATA / "GUARD")
PAL_BIN = str(P.SDLPOP_DATA / "PRINCE" / "res10.bin")
OUT_DIR = str(P.atl("guard"))
PAL_BASE = 0x90 # слоты палитры Sprinter под стража
COLOR = 2 # level.guards_color обоих стражей уровня 1
SHIFT = 3 # g[id>>3], idx id&7 (спрайты крупные)
MASK = (1 << SHIFT) - 1
MAX_ID = 33 # атлас-индексы 0..33 = res751..res784
def select_set(name):
"""Переключить упаковщик на набор (GUARD/SKEL/VIZIER): каталоги и число
кадров."""
global SET_NAME, GUARD_DIR, OUT_DIR, MAX_ID
if name not in SETS:
B.die("неизвестный набор %s (есть: %s)" % (name, ", ".join(SETS)))
SET_NAME = name
src, out, _ = SETS[name]
GUARD_DIR = str(P.SDLPOP_DATA / src)
OUT_DIR = str(P.atl(out))
# Сколько кадров реально есть в наборе (у скелета их меньше, чем у стража).
n = 0
while os.path.exists(os.path.join(GUARD_DIR, f"res{751 + n}.png")):
n += 1
MAX_ID = n - 1
def set_palette():
"""16 цветов набора из его res750.pal (для наборов, где цвет НЕ
подменяется из res10.bin). None — у набора своей палитры нет."""
fname = SETS[SET_NAME][2]
if fname is None:
return None
b = open(os.path.join(GUARD_DIR, fname), "rb").read()
return [(b[4 + i * 3], b[4 + i * 3 + 1], b[4 + i * 3 + 2]) for i in range(16)]
def load_guard(id):
"""Атлас-индекс id (== frame.image) -> res(751+id).png -> (w, h, 8bpp
COLUMN-MAJOR); 0 -> 0xFF (прозрачность).
Off-by-one как у Kid (см. pop_pack_kid.load_kid): load_chtab_from_file
зовётся с ресурсом 750, а images[i-1] = res(750+i), то есть images[0] =
res751. Рисование берёт get_image(chtab, obj_id+1 1) = images[image],
значит frame.image=N -> res(751+N). С res(750+N) страж стоял в позе
предыдущего кадра (замечено на frame_166_stand_inactive: image 17
рисовался как res767 — выпад — вместо res768). """
path = os.path.join(GUARD_DIR, f"res{751 + id}.png")
if not os.path.exists(path):
return None
im = Image.open(path)
if im.mode != "P":
B.die(f"res{750+id}: mode {im.mode}, ожидался P")
src = im.tobytes()
rowmajor = bytes((0xFF if v == 0 else PAL_BASE + v) for v in src)
return im.width, im.height, transpose_cols(im.width, im.height, rowmajor)
NPAL = 7 # guard_palettes: 7 палитр по 16 цветов (res10.bin)
def guard_palette(color=None):
"""16 цветов (r,g,b 0..63) из res10.bin для палитры color (1..7).
Индексация как в оригинале (seg003:257): &guard_palettes[0x30*color-0x30].
color=None — палитра COLOR (та, что запекается в kid.pal по умолчанию)."""
if color is None:
color = COLOR
b = open(PAL_BIN, "rb").read()
off = 0x30 * color - 0x30
return [(b[off + i * 3], b[off + i * 3 + 1], b[off + i * 3 + 2]) for i in range(16)]
def write_pal_header(out_path):
"""Таблица ВСЕХ палитр стражей -> заголовок для приложения.
Оригинал держит спрайты стража в одной палитре, а цвет подменяет на лету:
redraw_screen (seg003:255) зовёт set_chtab_palette(chtab_5_guard,
&guard_palettes[0x30*curr_guard_color - 0x30], 16) ПЕРЕД отрисовкой
комнаты, где curr_guard_color = level.guards_color[room-1] & 0x0F
(enter_guard, seg002:184). У нас спрайты уже лежат в индексах
PAL_BASE + v, поэтому достаточно залить 16 слотов палитры.
Формат записи — родной для gfx_pal_load: 4 байта (B, G, R, 0)."""
rows = []
for color in range(1, NPAL + 1):
vals = []
for (r, g, b) in guard_palette(color):
# res10.bin хранит 6-битные каналы, палитра Sprinter — 8-битные
# (так же делает pop_pack_kid при записи kid.pal).
vals += [B.scale6to8(b), B.scale6to8(g), B.scale6to8(r), 0]
rows.append(" { " + ",".join("0x%02X" % v for v in vals) + " },")
lines = [
"/* pop_guard_pal.h — палитры стражей (guard_palettes из res10.bin).",
" * СГЕНЕРЕНО toolchain/pop_pack_guard.py — НЕ править вручную.",
" *",
" * Индексация как в оригинале: цвет из level.guards_color[room-1]",
" * (младшие 4 бита), 1..%d -> строка [color-1]; 0 = палитру НЕ менять" % NPAL,
" * (seg003:255 при curr_guard_color == 0 set_chtab_palette не зовёт).",
" * Запись — 4 байта (B, G, R, 0), формат gfx_pal_load. */",
"#ifndef POP_GUARD_PAL_H",
"#define POP_GUARD_PAL_H",
"",
"#include <stdint.h>",
"",
"#define POP_GUARD_PAL_BASE 0x%02X /* слоты Sprinter под chtab_5 */" % PAL_BASE,
"#define POP_GUARD_NPAL %d" % NPAL,
"",
"static const uint8_t pop_guard_pal[POP_GUARD_NPAL][64] = {",
] + rows + [
"};",
"",
]
# Палитры наборов со СВОИМ цветом (скелет, визирь) — из их res750.pal, а
# не из res10.bin: на таких уровнях curr_guard_color = 0 (seg002:186), и
# оригинал палитру не подменяет вовсе.
saved = SET_NAME
for name, cname, human in (("SKEL", "pop_skel_pal", "Скелет"),
("VIZIER", "pop_vizier_pal", "Джафар")):
select_set(name)
pal = set_palette()
select_set(saved)
if not pal:
continue
vals = []
for (r, g, b) in pal:
vals += [B.scale6to8(b), B.scale6to8(g), B.scale6to8(r), 0]
lines += [
"/* %s (%s/res750.pal): свой набор — свой цвет. */" % (human, name),
"static const uint8_t %s[64] = {" % cname,
" " + ",".join("0x%02X" % v for v in vals) + ",",
"};",
"",
]
lines += [
"#endif",
"",
]
with open(out_path, "w") as f:
f.write("\n".join(lines))
print(f" pop_guard_pal.h: {NPAL} палитр x 16 цветов = {NPAL*64} Б -> {out_path}")
def main():
os.makedirs(OUT_DIR, exist_ok=True)
pages = {}
n = 0
for id in range(0, MAX_ID + 1):
g = load_guard(id)
if g is None:
continue
pages.setdefault(id >> SHIFT, {})[id & MASK] = g
n += 1
if not pages:
B.die("нет спрайтов стража")
total = 0
for pg in range(max(pages) + 1):
nm = f"g{pg}.atl"
cnt, sz = B.pack_atlas(os.path.join(OUT_DIR, nm), pages.get(pg, {}))
total += sz
# Зеркальный по вертикали набор — пара к kid*_v.atl (см. vflip_cols).
# ТОЛЬКО для обычного стража: зелье инверсии есть на уровне 9, а там
# tbl_guard_type = 0, то есть страж; скелет (уровень 3), визирь и
# жирный с переворотом не встречаются. Если чит U всё-таки включат
# на таком уровне, соперник нарисуется неперевёрнутым кадром —
# pop_vflip_page вернёт 0 (pop_vflip.h).
if SET_NAME == "GUARD":
_, szv = B.pack_atlas(os.path.join(OUT_DIR, f"g{pg}_v.atl"),
vflip_page(pages.get(pg, {})))
total += szv
print(f" {nm}: {len(pages.get(pg, {}))} спрайтов, каталог {cnt}, {sz} Б")
write_pal_header(str(P.GEN / "pop_guard_pal.h"))
print(f"ИТОГО: {n} спрайтов стража, {max(pages)+1} EMM-страниц, {total} Б -> {OUT_DIR}")
if __name__ == "__main__":
if len(sys.argv) > 1:
select_set(sys.argv[1].upper())
main()