Уровень 4: второй тайлсет (дворец) — ассеты и переключение (шаги A и B)

Порт tbl_envir_ki[tbl_level_type[level]] (seg000:1108): оригинал под один и
тот же набор id грузит РАЗНЫЙ .DAT — VDUNGEON или VPALACE.

Упаковщик (toolchain/pop_pack_bg.py):
- аргумент набора: `pop_pack_bg.py dungeon|palace`.  Каскад каталогов —
  сначала свой набор, потом чужой фолбэком (в распакованном data/ res230/
  231/348 есть только в VDUNGEON, два десятка — только в VPALACE).
- ОБА набора пакуются по одному объединению id, поэтому раскладка
  id -> (страница, idx) общая и заголовок один: коду достаточно подменить
  имена файлов.
- PALACE_ENV_IDS: 78/80/82 (doortop_fram_bot), 81/83 (doortop_fram_top),
  84 (полоска стены), 145 (stripe_id) — их рисует только палас, render_room
  про них не знает.
- ENV_SHIFT 5 -> 4: с паласными кусками страница 2 переваливала за 16 КБ
  (16 996).  Цена — 10 страниц EMM на набор вместо 5, при 215 свободных.
- *tile.pal: 32 записи (env 0x50..0x5F + wall 0x60..0x6F) на набор.  Полная
  kid.pal не трогается — Кид, страж, меч и зелья в других слотах.

Движок:
- pop_level_type() (tbl_level_type, SDLPoP data.h:840): дворцовые уровни
  4, 5, 6, 10, 11, 14.  Живёт в pop_level.c, потому что по типу расходятся
  не только атласы, но и ветки отрисовки seg008, кладка стены и модификатор
  пустой клетки от упавшей плиты (remove_loose, seg007:0EB8).
- pop_bg_load(set): no-op при том же наборе, при смене выгружает старый
  (иначе текут 12 EMM-страниц) и правит 32 записи палитры в ОБЕ страницы
  дабл-буфера.  Зовётся на старте и на границе уровня, не в кадре.
- Путь к атласу склеивается на месте (bg_path): двадцать строк-имён в банке
  — лишние полкилобайта.

Проверено в MAME: уровень 1 (подземелье) рисуется как прежде; уровень 4
(-DFIRST_LEVEL=4) — дворцовые арки, окна, пол, решётчатая дверь уровня,
Кид не перекрашен.  Стены пока чёрные: паласный wall_pattern — шаг C.

tests-host: 5/5.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-10 11:28:45 +03:00
parent c8fe0bd37a
commit 97929b55d3
34 changed files with 250 additions and 65 deletions
+101 -23
View File
@@ -129,7 +129,37 @@ CHOMPER_BLOOD_COLOR = VGA16_PAL_BASE + 12 # тот же mono-цвет 12 (seg0
# рисуется chtab_6 id 146). В статический render_room не попадает.
TORCH_ENV_IDS = {146}
ENV_SHIFT = 5 # env фон: страница = id>>5, idx = id&31
# ---- Два тайлсета: подземелье и дворец --------------------------------
# Оригинал грузит РАЗНЫЙ .DAT под один и тот же набор id:
# tbl_envir_ki[tbl_level_type[level]] -> VDUNGEON / VPALACE (seg000:1108),
# то есть id спрайта от набора НЕ зависит — меняется только картинка и
# палитра (env 0x50..0x5F, wall 0x60..0x6F). Поэтому оба набора пакуются
# по ОДНОМУ И ТОМУ ЖЕ объединению id: раскладка id -> (страница, idx)
# остаётся одна, и коду достаточно подменить ИМЕНА файлов.
#
# Порядок каталогов — каскад: сначала «свой» набор, потом чужой как
# фолбэк (в распакованном data/ несколько id есть только в одном из них:
# res230/231/348 только в VDUNGEON, два десятка — только в VPALACE).
SETS = {
"dungeon": ("pop_", lambda: (R.VDUNGEON_DIR, R.VPALACE_DIR)),
"palace": ("pal_", lambda: (R.VPALACE_DIR, R.VDUNGEON_DIR)),
}
SRC_DIRS = None # ставит main() по выбранному набору
# Спрайты, которые рисуются ТОЛЬКО на паласных уровнях (seg008):
# 78, 80, 82 doortop_fram_bot (низ верха двери, draw_tile_right)
# 81, 83 doortop_fram_top (draw_tile_topright)
# 84 полоска на стене (draw_tile_right, tiles_20_wall)
# 145 stripe_id соседнего тайла (draw_tile_right)
# render_room про них не знает (он рисует по подземельной ветке), поэтому
# добавляем явно — иначе при переключении набора в этих местах будет дыра.
PALACE_ENV_IDS = {78, 80, 81, 82, 83, 84, 145}
ENV_SHIFT = 4 # env фон: страница = id>>SHIFT, idx = id&MASK.
# Было 5: паласный набор добавляет куски верха
# двери (78..84), и страница 2 переваливала за
# 16 КБ. При 4 обе раскладки укладываются, цена
# — 10 страниц EMM на набор вместо 5.
ENV_MASK = (1 << ENV_SHIFT) - 1
# ---- .atl формат (== libbgi, но каталог не ограничен 19) --------------
@@ -194,7 +224,8 @@ NO_TRANSP_ENV_IDS = {36, 43, 73, 74, 96, 149} | LEVELDOOR_ENV_IDS
def load_indexed(chtab, id, opaque=False, mono=None):
"""PNG (mode P, 16 цветов) -> (w, h, bytes 8bpp). Каскад VDUNGEON->VPALACE.
"""PNG (mode P, 16 цветов) -> (w, h, bytes 8bpp). Каскад по SRC_DIRS
(порядок задаёт выбранный тайлсет, см. SETS).
Пиксель 0 -> 0xFF (прозрач); i -> base+i (env 0x50 / wall 0x60).
opaque=True — пиксель 0 остаётся ЦВЕТОМ (base+0 = чёрный), а не ключом
@@ -203,7 +234,7 @@ def load_indexed(chtab, id, opaque=False, mono=None):
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):
for d in SRC_DIRS:
p = os.path.join(d, fname)
if os.path.exists(p):
path = p
@@ -306,11 +337,29 @@ def scale6to8(v):
return (v * 255) // 63
def read_pal16(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)]
def build_tileset_palette(out_path):
"""Палитра ТОЛЬКО тайлсета: 32 записи B,G,R,0 — env (16) затем wall (16).
Ею подменяются слоты 0x50..0x6F при смене типа уровня; полная игровая
палитра (kid.pal) при этом не трогается, потому что Кид, страж, меч и
зелья живут в других слотах (levels_plan §3). Порядок каталогов —
SRC_DIRS, то есть файл соответствует выбранному набору."""
pal = bytearray(32 * 4)
src = read_pal16("res200.pal", SRC_DIRS[0]) + read_pal16("res360.pal", SRC_DIRS[0])
for i, (r, g, b) in enumerate(src):
pal[i * 4:i * 4 + 4] = bytes((scale6to8(b), scale6to8(g), scale6to8(r), 0))
with open(out_path, "wb") as f:
f.write(pal)
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)]
return read_pal16(res, d)
env = read_pal("res200.pal", R.VDUNGEON_DIR)
wall = read_pal("res360.pal", R.VDUNGEON_DIR)
@@ -334,7 +383,10 @@ def build_palette(out_path):
# ---- 5. C-заголовок --------------------------------------------------
def write_header(out_path, env_pages, wall_name, fore_name):
def write_header(out_path, env_pages):
"""Заголовок ОДИН на оба тайлсета: раскладка id -> (страница, idx) у них
общая (пакуются по одному объединению id), различаются только имена
файлов и палитра. Поэтому здесь два массива имён и один индекс."""
lines = [
"/* pop_bg_atlas.h — раскладка атласов статического фона PoP.",
" * Сгенерировано toolchain/pop_pack_bg.py — НЕ править вручную.",
@@ -343,6 +395,11 @@ def write_header(out_path, env_pages, wall_name, fore_name):
" * 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",
" *",
" * ДВА ТАЙЛСЕТА (порт tbl_envir_ki[tbl_level_type], seg000:1108):",
" * 0 = подземелье (pop_*), 1 = дворец (pal_*). Набор id и раскладка",
" * у них ОДНИ И ТЕ ЖЕ — меняются только файлы и 32 записи палитры",
" * (env 0x50..0x5F, wall 0x60..0x6F), см. *tile.pal.",
" */",
"#ifndef POP_BG_ATLAS_H",
"#define POP_BG_ATLAS_H",
@@ -350,22 +407,32 @@ def write_header(out_path, env_pages, wall_name, fore_name):
"#define POP_ENV_SHIFT %d" % ENV_SHIFT,
"#define POP_ENV_MASK %d" % ENV_MASK,
"#define POP_ENV_PAGES %d" % len(env_pages),
"#define POP_TILESETS 2",
"",
"/* Палитра: env-слоты, wall-слоты (сприйт-пиксель i -> база+i). */",
"/* Палитра: 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] = {",
"/* Имена файлов атласов по тайлсету (грузятся atlas_load). Таблицы",
" * видны только тому, кто попросил POP_BG_ATLAS_NAMES: иначе копия",
" * строк уедет в каждый включивший заголовок модуль. */",
"#ifdef POP_BG_ATLAS_NAMES",
"static const char *const pop_env_atl[POP_TILESETS][POP_ENV_PAGES] = {",
]
for pg in range(len(env_pages)):
nm = env_pages[pg]
lines.append(' %s,' % ('"%s"' % nm if nm else "0 /* пусто */"))
for prefix, _ in SETS.values():
row = ", ".join(('"%s%s"' % (prefix, nm.split("_", 1)[1]) if nm else "0")
for nm in env_pages)
lines.append(" { %s }," % row)
lines += [
"};",
'#define POP_WALL_ATL "%s"' % wall_name,
'#define POP_FORE_ATL "%s"' % fore_name,
'#define POP_POT_ATL "pop_pot.atl" /* chtab_1: зелья */',
'static const char *const pop_wall_atl[POP_TILESETS] = { %s };'
% ", ".join('"%swall.atl"' % p for p, _ in SETS.values()),
'static const char *const pop_fore_atl[POP_TILESETS] = { %s };'
% ", ".join('"%sfore.atl"' % p for p, _ in SETS.values()),
'static const char *const pop_tile_pal[POP_TILESETS] = { %s };'
% ", ".join('"%stile.pal"' % p for p, _ in SETS.values()),
"#endif /* POP_BG_ATLAS_NAMES */",
'#define POP_POT_ATL "pop_pot.atl" /* chtab_1: зелья, от набора не зависит */',
"#define POP_PAL_POT 0x%02X" % POT_PAL_BASE,
'#define POP_BG_PAL "pop_bg.pal"',
"",
@@ -377,6 +444,14 @@ def write_header(out_path, env_pages, wall_name, fore_name):
def main():
global SRC_DIRS
set_name = sys.argv[1] if len(sys.argv) > 1 else "dungeon"
if set_name not in SETS:
die(f"набор '{set_name}': ожидался один из {', '.join(SETS)}")
prefix, dirs = SETS[set_name]
SRC_DIRS = dirs()
print(f"набор: {set_name} (источник {os.path.basename(SRC_DIRS[0])}, "
f"фолбэк {os.path.basename(SRC_DIRS[1])}), префикс {prefix}")
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)
@@ -392,7 +467,8 @@ def main():
| CHOMPER_BLOOD_IDS # кровь (mono)
| CHOMPER_BLOOD_FORE_IDS
| OPENER_NOFLOOR_ENV_IDS # + opener без пола слева
| LEVELDOOR_ENV_IDS) # + дверь уровня (створка/лестница)
| LEVELDOOR_ENV_IDS # + дверь уровня (створка/лестница)
| PALACE_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]
@@ -410,7 +486,7 @@ def main():
total_bytes = 0
for pg in range(max_pg + 1):
if pg in pages:
nm = f"pop_env{pg}.atl"
nm = f"{prefix}env{pg}.atl"
cnt, sz = pack_atlas(os.path.join(OUT_DIR, nm), pages[pg])
total_bytes += sz
env_page_names.append(nm)
@@ -421,18 +497,18 @@ def main():
# 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)
cnt, sz = pack_atlas(os.path.join(OUT_DIR, prefix + "wall.atl"), wall_entries)
total_bytes += sz
print(f" pop_wall.atl: {len(wall_ids)} спрайтов, каталог {cnt}, {sz} Б")
print(f" {prefix}wall.atl: {len(wall_ids)} спрайтов, каталог {cnt}, {sz} Б")
# FORE -> idx=id
fore_entries = {id: load_indexed(R.CHTAB_ENV, id, False,
CHOMPER_BLOOD_COLOR
if id in CHOMPER_BLOOD_FORE_IDS else None)
for id in env_fore}
cnt, sz = pack_atlas(os.path.join(OUT_DIR, "pop_fore.atl"), fore_entries)
cnt, sz = pack_atlas(os.path.join(OUT_DIR, prefix + "fore.atl"), fore_entries)
total_bytes += sz
print(f" pop_fore.atl: {len(env_fore)} спрайтов, каталог {cnt}, {sz} Б")
print(f" {prefix}fore.atl: {len(env_fore)} спрайтов, каталог {cnt}, {sz} Б")
# POTION (chtab_1) -> idx=id: склянки + кадры пузырька + маска
# chtab_1 целиком (res151..res173 = id 1..23): пламя факелов, меч-предмет,
@@ -452,13 +528,15 @@ def main():
pot_entries[id] = load_potion(id, opaque=True)
else:
pot_entries[id] = load_potion(id)
# chtab_1 (пламя/меч/зелья) от тайлсета НЕ зависит — пакуем один раз.
cnt, sz = pack_atlas(os.path.join(OUT_DIR, "pop_pot.atl"), pot_entries)
total_bytes += sz
print(f" pop_pot.atl (chtab_1: факелы+зелья): {len(pot_entries)} спрайтов, каталог {cnt}, {sz} Б")
build_tileset_palette(os.path.join(OUT_DIR, prefix + "tile.pal"))
print(f" {prefix}tile.pal: 32 записи (env 0x50..0x5F + wall 0x60..0x6F)")
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")
write_header(os.path.join(OUT_DIR, "pop_bg_atlas.h"), env_page_names)
n_pages = sum(1 for n in env_page_names if n) + 2
print(f"ИТОГО: {len(env_bg)} env-фон + {len(wall_ids)} wall + "