Files
NX2navi/tools/menu_preview.py
chrg6 8daedbd806 Redraw the in-game menu with the game's own interface textures, and bring in the launcher's save import and faster builds.
In-game menu: the settings window, its tabs, rows, buttons and cursor, the performance overlay and the Preparing
shaders box are drawn with the game's interface textures where the RomFS holds them (runtime/ui/menu_skin.cpp,
perf_panel.cpp; plain shapes until they are decoded). Restart joins Continue and Quit, each behind a confirmation;
VSync is an on/off switch; DLSS is listed only on NVIDIA GPUs; the menu sounds are quieter; Picture shape is gone and
the setting descriptions are shorter; edge cases of input, layout and fonts are fixed. tools/menu_preview.py draws the
menu without the game.

Launcher: Import Saves takes a save from the game folder's saves/ folder or one the player picks; downloads fall back
to Windows' own certificate check when Python's refuses a server; updates are faster; the title bar is drawn in the
window and Navi is every icon. tools/make_game.py builds with CLANG64's clang where the launcher installed it, packs
the RomFS beside the lift and the compile, takes a prebuilt runtime, and compiles the game's resources without cmd.exe,
so a user folder with a space in its name builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 03:57:01 -05:00

102 lines
4.9 KiB
Python

"""Draw the in-game settings menu and the performance overlay without the game (tools/menu_preview.cpp).
python tools/menu_preview.py [--out build/menu-preview] [--font FILE] [--size 1920x1080] [--romfs FOLDER]
Builds tools/menu_preview.cpp with the menu's own row, shape, overlay and text code and Dear ImGui (host clang++, into
build/menu-preview), runs it for each scene and writes menu.png, overlay.png and boot.png. Without --romfs no game
files are read: the window has its plain border in place of the game's ornamented frame and the boxes are plain
shapes. With --romfs, an extracted RomFS folder, the textures listed in runtime/ui/menu_art.cpp are decoded into --out
and the boxes look as they do in the game (needs what tools/ui_textures.py needs). The font given (default: DejaVu
Sans, else the first TrueType font found) stands in for the game's either way.
"""
import argparse
import os
import re
import shutil
import struct
import subprocess
import sys
import zlib
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
RT = ROOT / 'runtime'
SOURCES = [ROOT / 'tools' / 'menu_preview.cpp', RT / 'ui' / 'option_rows.cpp', RT / 'ui' / 'menu_skin.cpp',
RT / 'ui' / 'perf_panel.cpp', RT / 'ui_text.c'] + sorted((RT / 'third_party' / 'imgui').glob('imgui*.cpp'))
FONTS = ['/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 'C:/Windows/Fonts/segoeui.ttf', 'C:/Windows/Fonts/arial.ttf']
def png(path, w, h, rgba):
"""An 8-bit RGBA PNG of the raw pixels."""
rows = b''.join(b'\0' + rgba[y * w * 4:(y + 1) * w * 4] for y in range(h))
def chunk(kind, data):
return struct.pack('>I', len(data)) + kind + data + struct.pack('>I', zlib.crc32(kind + data) & 0xffffffff)
Path(path).write_bytes(b'\x89PNG\r\n\x1a\n' + chunk(b'IHDR', struct.pack('>IIBBBBB', w, h, 8, 6, 0, 0, 0)) +
chunk(b'IDAT', zlib.compress(rows, 6)) + chunk(b'IEND', b''))
def build(out):
cxx = shutil.which('clang++') or shutil.which('g++')
if not cxx:
sys.exit('needs clang++ or g++ on PATH')
exe = out / ('menu_preview.exe' if os.name == 'nt' else 'menu_preview')
objs = []
for src in SOURCES:
obj = out / (src.stem + '.o')
if not obj.is_file() or obj.stat().st_mtime < max(src.stat().st_mtime, (RT / 'ui' / 'menu_skin.h').stat().st_mtime,
(RT / 'ui' / 'menu_art.h').stat().st_mtime):
lang = ['-x', 'c', '-std=gnu11'] if src.suffix == '.c' else ['-x', 'c++', '-std=gnu++17']
cmd = [cxx, *lang, '-c', '-O2', '-w', '-I', str(RT), '-I', str(RT / 'third_party' / 'imgui'), str(src), '-o',
str(obj)]
if subprocess.run(cmd).returncode:
sys.exit(f'{src.name} did not compile')
objs.append(str(obj))
if subprocess.run([cxx, *objs, '-o', str(exe), '-lm']).returncode:
sys.exit('the link failed')
return exe
def decode_art(romfs, out):
"""The textures of runtime/ui/menu_art.cpp kNames as out/art/NN.art for menu_preview.cpp; that folder."""
sys.path.insert(0, str(ROOT / 'tools'))
import ui_textures
source = (RT / 'ui' / 'menu_art.cpp').read_text(encoding='utf-8')
names = re.findall(r'"([^"]+)"', re.search(r'kNames\[MENU_ART_COUNT\] = \{(.*?)\};', source, re.S).group(1))
data = ui_textures.Romfs(romfs).read(f'{ui_textures.ARCHIVES}/Common.Product.100.Nin_Ounce_NVN2.bntx.zs')
textures = {t['name']: t for t in ui_textures.bntx_textures(data)}
art = out / 'art'
art.mkdir(parents=True, exist_ok=True)
lib = ui_textures.astc_library(out)
for i, name in enumerate(names):
rgba = ui_textures.decode(textures[name], data, lib)
(art / f'{i:02}.art').write_bytes(struct.pack('<II', rgba.shape[1], rgba.shape[0]) + rgba.tobytes())
return art
def main():
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
ap.add_argument('--out', type=Path, default=ROOT / 'build' / 'menu-preview')
ap.add_argument('--font', help='a TrueType font for the text')
ap.add_argument('--size', default='1920x1080')
ap.add_argument('--romfs', type=Path, help="an extracted RomFS folder: draw the game's textures")
a = ap.parse_args()
font = a.font or next((f for f in FONTS if Path(f).is_file()), None)
if not font:
sys.exit('no font found: give one with --font')
w, h = (int(x) for x in a.size.lower().split('x'))
a.out.mkdir(parents=True, exist_ok=True)
exe = build(a.out)
art = [str(decode_art(a.romfs, a.out))] if a.romfs else []
for scene in ('menu', 'overlay', 'boot'):
raw = a.out / f'{scene}.rgba'
if subprocess.run([str(exe), font, scene, str(w), str(h), str(raw), *art]).returncode:
sys.exit(f'the {scene} scene failed')
png(a.out / f'{scene}.png', w, h, raw.read_bytes())
raw.unlink()
print(a.out / f'{scene}.png')
if __name__ == '__main__':
main()