a3c5c600da
pop_hof.c был написан, но никуда не вызывался: после финала автомат уходил мимо него в title, а на титрах экран получался чёрным. Теперь это хвост end_sequence и шаг show_title между credits и attract-demo, как в оригинале. Два корня пустого экрана: - story.pal — 256 записей, и запись 0x3F (цвет глифов шрифта) в ней чёрная: текст рисовался, но был не виден. Генератор кладёт туда золотой 0xB7; - полосовой переход копирует страницу акселератором, а тот читает ОЗУ-копию, куда прозрачный блит текста не пишет. Строки рисуются после перехода, прямо в видимую страницу. Попутно: - s5 в архиве PV — фон таблицы (рамка story + логотип на y=24, HOF_POP); - отдельный индекс палитры под фон текстовой рамки: финал красит его в #800000, титры оставляют #100060 (load_title_images(bgcolor)). Прежний ремап в индекс 9 подменить было нельзя — им нарисована сама титульная картинка; - второй набор глифов в font.atl цветом 0x3E: у SDLPoP шрифт маска и show_hof_text рисует текст дважды разным цветом, у нас цвет запечён в пиксели. Каталог SPA1 держит счётчик в байте, поэтому набор обрезан по '_'; - общий pop_screen_present_ltr(): им теперь пользуются story-переход интро, титры, таблица и титульная картинка финала. Заодно вернулся пропущенный шаг show_title — экран с логотипом и Jordan Mechner между «свадьбой» и титрами (BUGS_CLOSED#cutscene-ltr). Формат POP.HOF — 6 записей, имя до 15 символов, версия 2. Проверено в MAME на сборке LEVEL=14: HAIL → титульная картинка → таблица с полосой ввода и временем справа → ввод имени → титульная картинка; на следующем запуске таблица показывается между credits и demo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
209 lines
9.4 KiB
Python
209 lines
9.4 KiB
Python
#!/usr/bin/env python3
|
||
"""pop_extract_font.py — SDLPoP-шрифты в один Sprinter .atl-ресурс.
|
||
|
||
Источник истины — оригинальные встроенные шрифты SDLPoP:
|
||
* hc_small_font_data[] из menu.c — пункты меню;
|
||
* hc_font_data[] из seg009.c — крупные сообщения/заголовки.
|
||
|
||
В исходнике глиф — IMAGE_DATA(height, width, flags) и 1-битные строки.
|
||
Здесь его силуэт переводится в getimage (0xFF = прозрачность) и пакуется в
|
||
FONT\\FONT.ATL. Это именно шрифтовой атлас, а не заранее нарисованные
|
||
строки: номер ленты = код ASCII, поэтому runtime сохраняет произвольный
|
||
текст и пропорциональную ширину оригинала.
|
||
|
||
Хвост страницы [0x3C00..0x3FFF] намеренно оставлен свободным: pop_menu.c
|
||
кладёт туда снимок игровой палитры на время паузы. Тогда 1 КБ не занимает
|
||
дефицитную W2.
|
||
"""
|
||
import re
|
||
from pathlib import Path
|
||
|
||
ROOT = Path(__file__).resolve().parent.parent
|
||
SRC_SMALL = ROOT / "applications" / "PoP" / "SDLPoP" / "src" / "menu.c"
|
||
SRC_BIG = ROOT / "applications" / "PoP" / "SDLPoP" / "src" / "seg009.c"
|
||
OUT_DIR = ROOT / "applications" / "PoP" / "poc" / "res" / "font"
|
||
OUT_ATL = OUT_DIR / "font.atl"
|
||
OUT_H = ROOT / "applications" / "PoP" / "roomtest" / "pop_font.h"
|
||
|
||
FIRST, LAST = 32, 126
|
||
FONT_COLOR = 0x3F
|
||
# ВТОРОЙ ЦВЕТ КРУПНОГО ШРИФТА. У SDLPoP шрифт — маска, и цвет задаётся
|
||
# параметром рисования (show_hof_text рисует текст дважды: тень цветом 0 со
|
||
# сдвигом (1,1) и сам текст цветом 0xB7). У нас цвет запечён в пиксели
|
||
# глифа, поэтому второй цвет — это второй набор глифов. Он нужен таблице
|
||
# рекордов: чёрная тень строк и тёмное имя на золотой полосе ввода.
|
||
# Набор крупный, потому что draw_text() рисует hc_font (seg009:1594).
|
||
FONT_DARK_COLOR = 0x3E
|
||
# Каталог SPA1 хранит число картинок одним байтом, а три полных набора по
|
||
# 95 глифов — это 285. Тёмному набору полный ASCII и не нужен: им пишется
|
||
# только таблица рекордов (имена A..Z, время вида 59:43 и курсор ввода
|
||
# '_'), поэтому он обрезан по '_'. 190 + 64 = 254 картинки — потолок.
|
||
DARK_FIRST, DARK_LAST = 32, 95
|
||
ATL_MAGIC = b"SPA1"
|
||
ATL_DIR_OFF = 0x68
|
||
SNAPSHOT_OFF = 0x3C00
|
||
PAGE_SIZE = 16384
|
||
|
||
|
||
def extract_array(path: Path, name: str) -> list[int]:
|
||
text = path.read_text()
|
||
match = re.search(rf"byte {name}\[\] = \{{(.*?)\n\}};", text, re.S)
|
||
assert match, f"{name} not found in {path}"
|
||
body = re.sub(r"//[^\n]*", "", match.group(1))
|
||
body = re.sub(r"/\*.*?\*/", "", body, flags=re.S)
|
||
|
||
def word(mm: re.Match) -> str:
|
||
value = int(mm.group(1), 0)
|
||
return f"{value & 0xFF}, {(value >> 8) & 0xFF}"
|
||
|
||
def bin4(mm: re.Match) -> str:
|
||
b7, b6, b5, b4 = (int(v, 0) if v != "_" else 0 for v in mm.groups())
|
||
return str((b4 << 4) | (b5 << 5) | (b6 << 6) | (b7 << 7))
|
||
|
||
def bin8(mm: re.Match) -> str:
|
||
bits = [int(v, 0) if v != "_" else 0 for v in mm.groups()]
|
||
return str(sum(bit << (7 - i) for i, bit in enumerate(bits)))
|
||
|
||
def image_data(mm: re.Match) -> str:
|
||
h, w, flags = (int(v, 0) for v in mm.groups())
|
||
return (f"{h & 0xFF}, {(h >> 8) & 0xFF}, "
|
||
f"{w & 0xFF}, {(w >> 8) & 0xFF}, "
|
||
f"{flags & 0xFF}, {(flags >> 8) & 0xFF}")
|
||
|
||
body = re.sub(r"\bWORD\s*\(\s*(\w+)\s*\)", word, body)
|
||
body = re.sub(r"\bIMAGE_DATA\s*\(\s*(\w+)\s*,\s*(\w+)\s*,\s*(\w+)\s*\)",
|
||
image_data, body)
|
||
body = re.sub(r"\bBINARY_4\s*\(\s*([_\dxXa-fA-F]+)\s*,\s*([_\dxXa-fA-F]+)\s*,\s*"
|
||
r"([_\dxXa-fA-F]+)\s*,\s*([_\dxXa-fA-F]+)\s*\)", bin4, body)
|
||
body = re.sub(r"\bBINARY_8\s*\(\s*([_\dxXa-fA-F]+)\s*,\s*([_\dxXa-fA-F]+)\s*,\s*"
|
||
r"([_\dxXa-fA-F]+)\s*,\s*([_\dxXa-fA-F]+)\s*,\s*([_\dxXa-fA-F]+)\s*,\s*"
|
||
r"([_\dxXa-fA-F]+)\s*,\s*([_\dxXa-fA-F]+)\s*,\s*([_\dxXa-fA-F]+)\s*\)", bin8, body)
|
||
body = re.sub(r"\b_\b", "0", body)
|
||
# Hex обязан идти первым: иначе regexp съедает в `0x20` только `0`.
|
||
return [int(v, 0) for v in re.findall(r"0[xX][0-9a-fA-F]+|\d+", body)]
|
||
|
||
|
||
def parse_glyphs(vals: list[int]) -> tuple[dict, list[dict]]:
|
||
first, last = vals[0], vals[1]
|
||
font = {
|
||
"first": first,
|
||
"last": last,
|
||
"ascent": vals[2] | (vals[3] << 8),
|
||
"below": vals[4] | (vals[5] << 8),
|
||
"space_lines": vals[6] | (vals[7] << 8),
|
||
"space_chars": vals[8] | (vals[9] << 8),
|
||
}
|
||
pos = 10 + (last - first + 1) * 2 # offsets у SDLPoP вычисляются в runtime
|
||
glyphs = []
|
||
while pos < len(vals):
|
||
h = vals[pos] | (vals[pos + 1] << 8)
|
||
w = vals[pos + 2] | (vals[pos + 3] << 8)
|
||
pos += 6 # height, width, flags
|
||
stride = (w + 7) // 8
|
||
rows = []
|
||
for _ in range(h):
|
||
rows.append(vals[pos:pos + stride])
|
||
pos += stride
|
||
glyphs.append({"h": h, "w": w, "rows": rows})
|
||
assert len(glyphs) == last - first + 1, f"glyph count {len(glyphs)}"
|
||
return font, glyphs
|
||
|
||
|
||
def glyph_pixels(glyph: dict, color: int) -> bytes:
|
||
"""1 бит/пиксель SDLPoP -> 8bpp getimage: 1 = color."""
|
||
out = bytearray()
|
||
for row in glyph["rows"]:
|
||
for x in range(glyph["w"]):
|
||
byte = row[x // 8]
|
||
on = byte & (1 << (7 - (x & 7)))
|
||
out.append(color if on else 0xFF)
|
||
return bytes(out)
|
||
|
||
|
||
def pack_atlas(fonts: list[tuple[dict, list[dict], int]]) -> int:
|
||
entries = []
|
||
for font, glyphs, color, first, last in fonts:
|
||
assert font["first"] <= first <= last <= font["last"]
|
||
entries.extend((glyph, color) for glyph in
|
||
glyphs[first - font["first"]:last - font["first"] + 1])
|
||
assert len(entries) <= 255
|
||
|
||
data_off = ATL_DIR_OFF + len(entries) * 8
|
||
out = bytearray(data_off)
|
||
out[0:4] = ATL_MAGIC
|
||
out[4] = len(entries)
|
||
for index, (glyph, color) in enumerate(entries):
|
||
offset = len(out)
|
||
pixels = glyph_pixels(glyph, color)
|
||
out += glyph["w"].to_bytes(2, "little")
|
||
out += glyph["h"].to_bytes(2, "little")
|
||
out += pixels
|
||
directory = ATL_DIR_OFF + index * 8
|
||
out[directory:directory + 2] = offset.to_bytes(2, "little")
|
||
out[directory + 2] = glyph["w"]
|
||
out[directory + 3] = glyph["h"]
|
||
out[directory + 4] = 1
|
||
out[directory + 5] = 1
|
||
|
||
if len(out) > SNAPSHOT_OFF:
|
||
raise SystemExit(f"font atlas: {len(out)} Б, нет места для palette snapshot @ {SNAPSHOT_OFF:#x}")
|
||
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
||
OUT_ATL.write_bytes(out)
|
||
return len(out)
|
||
|
||
|
||
def emit_header(small: dict, big: dict, atlas_size: int) -> None:
|
||
small_n = LAST - FIRST + 1
|
||
text = f"""/*
|
||
* pop_font.h — контракт FONT\\FONT.ATL, СГЕНЕРИРОВАН.
|
||
* Источник: SDLPoP hc_small_font_data[] и hc_font_data[].
|
||
* Генератор: toolchain/pop_extract_font.py. Руками не править.
|
||
*/
|
||
#ifndef POP_FONT_H
|
||
#define POP_FONT_H
|
||
|
||
#include <stdint.h>
|
||
|
||
#define POP_FONT_COLOR 0x{FONT_COLOR:02X}
|
||
#define POP_FONT_DARK_COLOR 0x{FONT_DARK_COLOR:02X}
|
||
#define POP_FONT_FIRST {FIRST}
|
||
#define POP_FONT_LAST {LAST}
|
||
#define POP_FONT_SMALL_BASE 0
|
||
#define POP_FONT_SMALL_ASCENT {small['ascent']}
|
||
#define POP_FONT_SMALL_SPACE {small['space_chars']}
|
||
#define POP_FONT_BIG_BASE {small_n}
|
||
#define POP_FONT_BIG_ASCENT {big['ascent']}
|
||
#define POP_FONT_BIG_SPACE {big['space_chars']}
|
||
/* Тот же КРУПНЫЙ шрифт цветом POP_FONT_DARK_COLOR: тень и текст на светлом.
|
||
* Набор обрезан по '_' — им пишется только таблица рекордов. */
|
||
#define POP_FONT_DARK_BASE {2 * small_n}
|
||
#define POP_FONT_DARK_FIRST {DARK_FIRST}
|
||
#define POP_FONT_DARK_LAST {DARK_LAST}
|
||
#define POP_FONT_SNAPSHOT_OFF 0x{SNAPSHOT_OFF:04X}
|
||
#define POP_FONT_ATLAS_SIZE {atlas_size}
|
||
|
||
#endif
|
||
"""
|
||
OUT_H.write_text(text)
|
||
|
||
|
||
def main() -> None:
|
||
parsed = []
|
||
for path, name in ((SRC_SMALL, "hc_small_font_data"), (SRC_BIG, "hc_font_data")):
|
||
font, glyphs = parse_glyphs(extract_array(path, name))
|
||
print(f"{name}: chars={font['first']}..{font['last']} ascent={font['ascent']} "
|
||
f"space={font['space_chars']}")
|
||
parsed.append((font, glyphs))
|
||
sets = [(parsed[0][0], parsed[0][1], FONT_COLOR, FIRST, LAST),
|
||
(parsed[1][0], parsed[1][1], FONT_COLOR, FIRST, LAST),
|
||
(parsed[1][0], parsed[1][1], FONT_DARK_COLOR, DARK_FIRST, DARK_LAST)]
|
||
atlas_size = pack_atlas(sets)
|
||
emit_header(parsed[0][0], parsed[1][0], atlas_size)
|
||
print(f"{OUT_ATL}: 2 x {LAST - FIRST + 1} + {DARK_LAST - DARK_FIRST + 1} glyphs, "
|
||
f"{atlas_size} Б; "
|
||
f"snapshot from 0x{SNAPSHOT_OFF:04X}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|