mirror of
https://github.com/Cesarus85/ringfall-mjolnir-vr.git
synced 2026-10-17 13:00:25 +02:00
182 lines
7.5 KiB
Python
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()
|