#!/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 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) -> bytes: """1 бит/пиксель SDLPoP -> 8bpp getimage: 1 = FONT_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(FONT_COLOR if on else 0xFF) return bytes(out) def pack_atlas(fonts: list[tuple[dict, list[dict]]]) -> int: entries = [] for font, glyphs in fonts: assert font["first"] <= FIRST <= LAST <= font["last"] entries.extend(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 in enumerate(entries): offset = len(out) pixels = glyph_pixels(glyph) 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 #define POP_FONT_COLOR 0x{FONT_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']} #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)) atlas_size = pack_atlas(parsed) emit_header(parsed[0][0], parsed[1][0], atlas_size) print(f"{OUT_ATL}: 2 x {LAST - FIRST + 1} glyphs, {atlas_size} Б; " f"snapshot from 0x{SNAPSHOT_OFF:04X}") if __name__ == "__main__": main()