mirror of
https://github.com/Cesarus85/ringfall-mjolnir-vr.git
synced 2026-10-23 06:00:25 +02:00
With a mouse, the menus' pointer (platform_ui_pointer_set_active) was the system's cursor. The game window now has a cursor of its own: port/assets/cursor/menu_cursor.png, an 80 x 116 blue arrow with a transparent background, embedded by tools/embed_assets.py as the window icon is and made into an SDL colour cursor when the window is created. At 100% display scale the cursor is the picture at a quarter of its size (20 x 29, by two halvings); the picture itself is the alternate image SDL scales from at larger display scales (SDL_HINT_MOUSE_DPI_SCALE_CURSORS, which SDL implements on Windows). The hot spot is the arrow's tip, the picture's top-left corner. Desktop builds only: the code sits beside the window icon's, so Android and the web keep theirs. The cursor shows wherever SDL shows one over the window, which is the menus' pointer, the scoreboard's and a mouse freed with F12; in play the mouse stays captured and hidden as before. Checked on Windows: the build, and one start to the main menu with a hidden window. A check line (not in this commit) logged the cursor made and active, 20 x 29 with the 80 x 116 alternate, and saved SDL's 20 x 29 image, which looks as intended. Not built here for Linux, where SDL uses the 20 x 29 image at every display scale. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
275 lines
12 KiB
Python
275 lines
12 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), the fonts the text is drawn with
|
|
(port/assets/fonts), the menus' files (port/assets/menus, made by
|
|
tools/ce_menus.py) and SMAA's shader and lookup textures
|
|
(port/third_party/smaa) 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 checksum of its pixels, from port/assets/hud/layout.json,
|
|
port/assets/titles/titles.json and port/assets/buttons/buttons.json), the
|
|
fonts and their tags from port/assets/fonts/fonts.json, the menu files
|
|
listed in port/assets/menus/menus.json, and SMAA's shader and lookup
|
|
textures, as port/linux/src/hud_hires.h, text_hires.h, menu_files.h and
|
|
xgpu_post.c declare them. The builds generate it
|
|
(hud_assets_build, called by tools/linux_build.py, windows_build.py and
|
|
android_build.py), so the assets 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 (it keeps .ascii strings as they are, but once garbled them).
|
|
"""
|
|
|
|
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"
|
|
# the HUD textures whose bitmaps Halo Custom Edition lays out alike
|
|
CUSTOM_EDITION_HUD = HUD_ASSETS / "custom_edition.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"
|
|
MENU_ASSETS = Path("port/assets/menus")
|
|
MENU_LIST = MENU_ASSETS / "menus.json"
|
|
# SMAA's files and the names port/linux/src/xgpu_post.c declares them by
|
|
SMAA_ASSETS = Path("port/third_party/smaa")
|
|
SMAA_FILES = (("SMAA.hlsl", "xgpu_smaa_shader"), ("area_tex.zlib", "xgpu_smaa_area_texture"),
|
|
("search_tex.zlib", "xgpu_smaa_search_texture"))
|
|
# the desktop windows' icon (tools/app_icon.py; port/linux/src/sdl_platform.c)
|
|
WINDOW_ICON = Path("port/assets/icon/opence-icon-256.png")
|
|
# the pointer the menus show in place of the system's (port/linux/src/sdl_platform.c)
|
|
MENU_CURSOR = Path("port/assets/cursor/menu_cursor.png")
|
|
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 menu_files() -> List[str]:
|
|
"""The menus' files menus.json lists, relative to their folder."""
|
|
if not (ROOT / MENU_LIST).is_file():
|
|
return []
|
|
return json.loads((ROOT / MENU_LIST).read_text())["files"]
|
|
|
|
|
|
def smaa_files() -> List[tuple]:
|
|
"""SMAA's files that the checkout has, with their symbols."""
|
|
return [(name, symbol) for name, symbol in SMAA_FILES if (ROOT / SMAA_ASSETS / name).is_file()]
|
|
|
|
|
|
def hud_asset_inputs() -> List[Path]:
|
|
"""The files the generated source is made from."""
|
|
inputs = [listing for listing in (LAYOUT, CUSTOM_EDITION_HUD, TITLE_LIST, BUTTON_LIST, FONT_LIST, MENU_LIST)
|
|
if (ROOT / listing).is_file()]
|
|
return [*inputs, *(folder / f"{asset['name']}.png" for folder, asset, _ in textures()),
|
|
*(FONT_ASSETS / name for name in font_files()), *(MENU_ASSETS / name for name in menu_files()),
|
|
*(SMAA_ASSETS / name for name, _ in smaa_files()),
|
|
*([WINDOW_ICON] if (ROOT / WINDOW_ICON).is_file() else []),
|
|
*([MENU_CURSOR] if (ROOT / MENU_CURSOR).is_file() else [])]
|
|
|
|
|
|
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), (MENU_ASSETS, MENU_LIST)):
|
|
if (ROOT / listing).is_file():
|
|
inputs += [folder, listing]
|
|
if (ROOT / SMAA_ASSETS).is_dir():
|
|
inputs.append(SMAA_ASSETS)
|
|
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]. It is made
|
|
whatever assets the checkout has (with none, its tables are empty), so
|
|
that the symbols the platform layer refers to are always defined."""
|
|
inputs = hud_asset_inputs()
|
|
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 = []
|
|
custom_edition = set()
|
|
if (ROOT / CUSTOM_EDITION_HUD).is_file():
|
|
custom_edition = {(entry["tag"], entry["bitmap"])
|
|
for entry in json.loads((ROOT / CUSTOM_EDITION_HUD).read_text())["assets"]}
|
|
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", [])))
|
|
point_threshold = int(any(cell.get("thresholds") 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'{point_threshold}, {int(title)}, 0x{sprites:x}u, '
|
|
f'{int((asset["tag"], asset["bitmap"]) in custom_edition)}, asset{index}, {len(data)} }},')
|
|
lines.append("const struct hud_hires_embedded hud_hires_embedded[] =")
|
|
lines.append("{")
|
|
lines.extend(table)
|
|
if not table:
|
|
lines.append("\t{ 0 },")
|
|
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)};")
|
|
lines.append("")
|
|
# the menus' files (menu_files.h)
|
|
lines.append('#include "menu_files.h"')
|
|
lines.append("")
|
|
menus = menu_files()
|
|
for index, name in enumerate(menus):
|
|
data = (ROOT / MENU_ASSETS / name).read_bytes()
|
|
if name.endswith(".png"):
|
|
png_size(data, name)
|
|
lines.append(f"static const unsigned int menu{index}[] = {{")
|
|
lines.extend(words(data))
|
|
lines.append("};")
|
|
lines.append("")
|
|
lines.append("const struct menu_file_embedded menu_files_embedded[] =")
|
|
lines.append("{")
|
|
for index, name in enumerate(menus):
|
|
size = (ROOT / MENU_ASSETS / name).stat().st_size
|
|
lines.append(f'\t{{ "{name}", menu{index}, {size} }},')
|
|
if not menus:
|
|
lines.append("\t{ 0 },")
|
|
lines.append("};")
|
|
lines.append(f"const unsigned int menu_files_embedded_count = {len(menus)};")
|
|
lines.append("")
|
|
# SMAA's shader, as text a GLSL compiler takes (ASCII, ending in a NUL),
|
|
# and its lookup textures (xgpu_post.c); each of size 0 that the
|
|
# checkout does not have. The OpenGL ES renderer has no SMAA.
|
|
lines.append("#ifndef HALO_GLES")
|
|
present = dict(smaa_files())
|
|
for name, symbol in SMAA_FILES:
|
|
data = (ROOT / SMAA_ASSETS / name).read_bytes() if name in present else b""
|
|
if data and name.endswith(".hlsl"):
|
|
data = bytes(byte if byte < 0x80 else 0x20 for byte in data) + b"\0"
|
|
lines.append("")
|
|
lines.append(f"const unsigned int {symbol}[] = {{")
|
|
lines.extend(words(data) if data else ["\t0,"])
|
|
lines.append("};")
|
|
lines.append(f"const unsigned long {symbol}_size = {len(data)};")
|
|
# the windows' icon, a PNG (of size 0 where the checkout has none)
|
|
data = (ROOT / WINDOW_ICON).read_bytes() if (ROOT / WINDOW_ICON).is_file() else b""
|
|
lines.append("")
|
|
lines.append("const unsigned int platform_window_icon[] = {")
|
|
lines.extend(words(data) if data else ["\t0,"])
|
|
lines.append("};")
|
|
lines.append(f"const unsigned long platform_window_icon_size = {len(data)};")
|
|
# the menus' pointer, a PNG (of size 0 where the checkout has none)
|
|
data = (ROOT / MENU_CURSOR).read_bytes() if (ROOT / MENU_CURSOR).is_file() else b""
|
|
lines.append("")
|
|
lines.append("const unsigned int platform_menu_cursor[] = {")
|
|
lines.extend(words(data) if data else ["\t0,"])
|
|
lines.append("};")
|
|
lines.append(f"const unsigned long platform_menu_cursor_size = {len(data)};")
|
|
lines.append("")
|
|
lines.append("#endif")
|
|
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()
|