Files
Cesarus85--ringfall-mjolnir-vr/tools/embed_assets.py
T

182 lines
7.5 KiB
Python

#!/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), their controller button icons (port/assets/buttons,
made by tools/button_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,
port/assets/titles/titles.json and port/assets/buttons/buttons.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"
BUTTON_ASSETS = Path("port/assets/buttons")
BUTTON_LIST = BUTTON_ASSETS / "buttons.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 the menus' (a title or a button icon, drawn with the high-res
text)."""
result = []
for folder, listing, title in ((HUD_ASSETS, LAYOUT, False), (TITLE_ASSETS, TITLE_LIST, True),
(BUTTON_ASSETS, BUTTON_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, BUTTON_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), (BUTTON_ASSETS, BUTTON_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", [])))
# (the sequences whose sprites it stands for, a bit each of 32; 0: the whole bitmap)
sequences = [sprite["sequence"] for sprite in asset.get("sprites", [])]
if any(not 0 <= sequence < 32 for sequence in sequences):
sys.exit(f"{name}: sprite sequences must be 0 to 31 (hud_hires.h: sprites)")
sprites = sum(1 << sequence for sequence in sequences)
table.append(f'\t{{ "{tag}", {asset["bitmap"]}, {width}, {height}, 0x{asset["crc"]:08x}u, {coverage}, '
f'{int(title)}, 0x{sprites:x}u, 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()