#!/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. Выход: poc/res/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 # Набор спрайтов соперника выбирается ТИПОМ УРОВНЯ (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"), } SET_NAME = "GUARD" GUARD_DIR = os.path.join(HERE, "..", "SDLPoP", "data", "GUARD") PAL_BIN = os.path.join(HERE, "..", "SDLPoP", "data", "PRINCE", "res10.bin") OUT_DIR = os.path.join(HERE, "..", "poc", "res", "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): каталоги и число кадров.""" 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 = os.path.join(HERE, "..", "SDLPoP", "data", src) OUT_DIR = os.path.join(HERE, "..", "poc", "res", 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 ", "", "#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: на # уровне 3 curr_guard_color = 0, оригинал палитру не подменяет вовсе. saved = SET_NAME select_set("SKEL") skel = set_palette() select_set(saved) if skel: vals = [] for (r, g, b) in skel: vals += [B.scale6to8(b), B.scale6to8(g), B.scale6to8(r), 0] lines += [ "/* Скелет (SKEL/res750.pal): свой набор — свой цвет. */", "static const uint8_t pop_skel_pal[64] = {", " " + ",".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 print(f" {nm}: {len(pages.get(pg, {}))} спрайтов, каталог {cnt}, {sz} Б") write_pal_header(os.path.join(HERE, "..", "roomtest", "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()