#!/usr/bin/env python3 """Embeds the high-res HUD textures (port/assets/hud, made by tools/hud_assets.py), the menus' titles (port/assets/titles, made by tools/title_assets.py) and the fonts the text is drawn with (port/assets/fonts) in the game as C data: python tools/embed_assets.py OUTPUT.c writes OUTPUT.c with each PNG and the bitmap it stands for (its tag, index and the checksum of its pixels, from port/assets/hud/layout.json and port/assets/titles/titles.json), as port/linux/src/hud_hires.h declares them. The builds generate it (hud_assets_build, called by tools/linux_build.py, windows_build.py and android_build.py), so the PNGs are the committed source and Android needs no files beside its guest image. The data are 32-bit words, not bytes: the Android build passes the guest's assembly through tools/android_asm_convert.py, which rewrites identifiers in operands and would garble a long .ascii string of binary data. """ import json import struct import sys from pathlib import Path from typing import Any, List ROOT = Path(__file__).resolve().parent.parent HUD_ASSETS = Path("port/assets/hud") LAYOUT = HUD_ASSETS / "layout.json" TITLE_ASSETS = Path("port/assets/titles") TITLE_LIST = TITLE_ASSETS / "titles.json" FONT_ASSETS = Path("port/assets/fonts") FONT_LIST = FONT_ASSETS / "fonts.json" PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n" def font_files() -> List[str]: """The font files fonts.json uses, each once.""" if not (ROOT / FONT_LIST).is_file(): return [] fonts = json.loads((ROOT / FONT_LIST).read_text())["fonts"] return sorted({font["file"] for font in fonts}) def textures() -> List[tuple]: """The textures: each one's folder, its entry in its list, and whether it is a title.""" result = [] for folder, listing, title in ((HUD_ASSETS, LAYOUT, False), (TITLE_ASSETS, TITLE_LIST, True)): if (ROOT / listing).is_file(): result += [(folder, asset, title) for asset in json.loads((ROOT / listing).read_text())["assets"]] return result def hud_asset_inputs() -> List[Path]: """The files the generated source is made from.""" inputs = [listing for listing in (LAYOUT, TITLE_LIST, FONT_LIST) if (ROOT / listing).is_file()] if not inputs: return [] return [*inputs, *(folder / f"{asset['name']}.png" for folder, asset, _ in textures()), *(FONT_ASSETS / name for name in font_files())] def hud_configure_inputs() -> List[Path]: """What configure.py is run again for: the lists of assets (and the folders, for files added or removed), not each file, which a change of a list may rename or remove.""" inputs = [] for folder, listing in ((HUD_ASSETS, LAYOUT), (TITLE_ASSETS, TITLE_LIST), (FONT_ASSETS, FONT_LIST)): if (ROOT / listing).is_file(): inputs += [folder, listing] return inputs def words(data: bytes) -> List[str]: """data as lines of 32-bit words (padded with zeros).""" padded = data + b"\0" * (-len(data) % 4) values = struct.unpack(f"<{len(padded) // 4}I", padded) return ["\t" + ", ".join(f"0x{word:08x}" for word in values[start:start + 8]) + "," for start in range(0, len(values), 8)] def hud_assets_build(n: Any, prefix: str, output: Path) -> List[Path]: """Emits the rule that generates output; returns [output], or nothing when there are no assets.""" inputs = hud_asset_inputs() if not inputs: return [] n.rule( name=f"{prefix}_embed_assets", command="$python tools/embed_assets.py $out", description=f"{prefix.upper()} EMBED $out", ) n.build(outputs=output, rule=f"{prefix}_embed_assets", implicit=[Path("tools/embed_assets.py"), *inputs]) return [output] def png_size(data: bytes, name: str) -> tuple: """The width and height of an 8-bit RGBA, non-interlaced PNG (the only kind port/linux/src/hud_hires.c reads).""" if data[:8] != PNG_SIGNATURE or data[12:16] != b"IHDR": sys.exit(f"{name}: not a PNG") width, height, depth, colour, _, _, interlace = struct.unpack(">IIBBBBB", data[16:29]) if depth != 8 or colour != 6 or interlace != 0: sys.exit(f"{name}: must be 8-bit RGBA and not interlaced") return width, height def main() -> None: if len(sys.argv) != 2: sys.exit("usage: embed_assets.py OUTPUT.c") lines = [ "/* generated by tools/embed_assets.py from port/assets: do not edit */", "", '#include "hud_hires.h"', "", ] table = [] for index, (folder, asset, title) in enumerate(textures()): name = f"{asset['name']}.png" data = (ROOT / folder / name).read_bytes() width, height = png_size(data, name) scale = asset["scale"] if (width, height) != (asset["width"] * scale, asset["height"] * scale): sys.exit(f"{name}: {width}x{height}, not {scale}x its bitmap's {asset['width']}x{asset['height']}") lines.append(f"static const unsigned int asset{index}[] = {{") lines.extend(words(data)) lines.append("};") lines.append("") tag = asset["tag"].replace("\\", "\\\\") coverage = int(any(cell["kind"] == "meter" for cell in asset.get("cells", []))) table.append(f'\t{{ "{tag}", {asset["bitmap"]}, {width}, {height}, 0x{asset["crc"]:08x}u, {coverage}, ' f'{int(title)}, asset{index}, {len(data)} }},') lines.append("const struct hud_hires_embedded hud_hires_embedded[] =") lines.append("{") lines.extend(table) lines.append("};") lines.append(f"const unsigned int hud_hires_embedded_count = {len(table)};") lines.append("") # the fonts, and which draws each font tag (text_hires.h) lines.append('#include "text_hires.h"') lines.append("") files = font_files() for index, name in enumerate(files): lines.append(f"static const unsigned int font{index}[] = {{") lines.extend(words((ROOT / FONT_ASSETS / name).read_bytes())) lines.append("};") lines.append("") fonts = json.loads((ROOT / FONT_LIST).read_text())["fonts"] if files else [] lines.append("const struct text_hires_embedded text_hires_embedded[] =") lines.append("{") for font in fonts: index = files.index(font["file"]) tag = font["tag"].replace("\\", "\\\\") size = (ROOT / FONT_ASSETS / font["file"]).stat().st_size lines.append(f'\t{{ "{tag}", "{font["file"]}", font{index}, {size} }},') if not fonts: lines.append("\t{ 0 },") lines.append("};") lines.append(f"const unsigned int text_hires_embedded_count = {len(fonts)};") output = Path(sys.argv[1]) output.parent.mkdir(parents=True, exist_ok=True) text = "\n".join(lines) + "\n" if not output.is_file() or output.read_text() != text: output.write_text(text) if __name__ == "__main__": main()