Files
Sprinter-SDCC/applications/PoP/toolchain/pop_pack_guard.py
T
snark13 5b6664ffce Зеркальные кадры персонажей — готовыми файлами, а не разворотом в рантайме
Замеры в MAME (такты Z80, кадр = 430 000) решили вопрос однозначно:
  загрузка 28 атласов Кида с HDD  — 27 160 092 такта (1.26 с), 970 003 на страницу;
  разворот ОДНОЙ страницы на месте — 12 645 582 такта (0.59 с), т.е. 771 такт/байт.
Даже идеальный вариант (копия accel'ом ~0.25 кадра + asm-реверс ~2.2 кадра)
дал бы ~1.05 млн на страницу — то есть в лучшем случае сравнялся бы с чтением
готового файла.  На 34 страницы: 1.5 с загрузки против 20 с разворота.

* Упаковщики пишут второй набор: vflip_cols/vflip_page в pop_pack_kid.py ->
  kid0_v.atl…kid27_v.atl, sword_v.atl; pop_pack_guard.py -> g0_v.atl…g4_v.atl
  ТОЛЬКО для обычного стража (на уровне 9 tbl_guard_type = 0; тень рисуется
  атласами Кида, скелет/визирь с переворотом не встречаются).
* pop_vflip.c схлопнулся до таблицы «страница -> зеркальная пара»; весь
  побайтовый разворот и аллокация EMM удалены.
* pop_vflip_load_all (банк 8) грузит набор ОДНИМ вызовом и откуда угодно —
  задел под загрузку ресурсов во время интро.  Зовётся при старте и при
  смене уровня, если это POP_UPSIDE_LEVEL; чит U на других уровнях поднимает
  набор лениво (страховка в главном цикле).
* Стартовый уровень стал параметром сборки: make LEVEL=N (-DFIRST_LEVEL).

Проверено в MAME на уровне 9: переворот мгновенный, pop_flip_screen = 897 263
такта (2.1 кадра) — это сама копия экрана, загрузки в кадре больше нет.
tests-host 6/6.
2026-08-12 23:17:22 +03:00

207 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.
Выход: 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, vflip_page
# Набор спрайтов соперника выбирается ТИПОМ УРОВНЯ (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 <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: на
# уровне 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
# Зеркальный по вертикали набор — пара к 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(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()