Files
NX2navi/tests/action_prompt_check.py
chrg6 b1d08b06a8 Lay out the action prompt's face buttons as the pad layout has them.
The action prompt at the top right of the HUD draws the four face buttons as a diamond in the console's places: A right, B bottom, X top, Y left. Under pad_layout = xbox, playstation or auto the A action is the bottom button, yet the diamond kept A on the right, so the prompt pointed at the wrong button. At start the runtime now edits the prompt's layout in the shared interface archive for such a layout (the buttons, the dots and the lit crosses move, and the bubbles of X and Y flip) and serves the result from the profile folder; with nintendo, the default, nothing is changed. Every field is found and checked before anything is written, so a layout a mod changed is left as it is, and the archive is packed again with the game's own dictionary at the same unpacked size. One SARC reader and dictionary lookup now serve this, the camera pack and the menu's assets. When the layout changes, the menu's Restart counts the diamond.

tests/action_prompt_check.py passes without game files and on the game's own archive (4 copies, 208 fields, 13 ms at start).

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 23:16:17 -05:00

67 lines
2.9 KiB
Python

"""Build and run tests/action_prompt_check.c: runtime/action_prompt.c, the action prompt's diamond of face buttons laid
out by label, on a layout file and archives the test makes itself. No game files.
With --romfs, also on the game's own archive: the folder holds romfs.bin and its index.json, and the program reads the
archive and Pack/ZsDic.pack.zs from romfs.bin at the offsets the index gives. Nothing is written.
The program is built with the undefined-behaviour sanitizer (traps), with locals that start as a pattern rather than
whatever the stack held, and with AddressSanitizer where the toolchain has it (not MSYS2's).
Usage: action_prompt_check.py [--romfs DIR]
"""
import argparse
import json
import os
import shutil
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
MSYS_BIN = Path(r'C:\msys64\ucrt64\bin')
ARCHIVE = 'UI/Layout2Archive/Common.Product.100.Nin_Ounce_NVN2.blarc.zs'
DICTIONARIES = 'Pack/ZsDic.pack.zs'
def compiler():
if os.environ.get('CC'):
return os.environ['CC']
if os.name == 'nt' and (MSYS_BIN / 'clang.exe').exists():
return str(MSYS_BIN / 'clang.exe')
return shutil.which('clang') or 'clang'
def game_args(folder):
"""romfs.bin and the offset and size of the archive and the dictionaries in it."""
files = {f['path']: f for f in json.loads((folder / 'index.json').read_text(encoding='utf-8'))['files']}
for rel in (ARCHIVE, DICTIONARIES):
if rel not in files:
raise SystemExit(f'{folder / "index.json"} has no {rel}')
return [str(folder / 'romfs.bin')] + [str(files[rel][k]) for rel in (ARCHIVE, DICTIONARIES)
for k in ('offset', 'size')]
def main():
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
ap.add_argument('--romfs', type=Path, help='a folder with romfs.bin and index.json: also check the game\'s archive')
args = ap.parse_args()
work = ROOT / 'build' / 'test' / 'action_prompt'
work.mkdir(parents=True, exist_ok=True)
exe = work / ('action_prompt_check.exe' if os.name == 'nt' else 'action_prompt_check')
env = dict(os.environ)
if os.name == 'nt' and MSYS_BIN.exists():
env['PATH'] = str(MSYS_BIN) + os.pathsep + env.get('PATH', '')
sanitize = ['-fsanitize=undefined', '-fsanitize-trap=all', '-ftrivial-auto-var-init=pattern']
if os.name != 'nt':
sanitize.append('-fsanitize=address')
rt = ROOT / 'runtime'
subprocess.check_call([compiler(), '-O1', '-g', '-Wall', '-Wextra', '-Werror', *sanitize, '-I', str(rt),
str(ROOT / 'tests' / 'action_prompt_check.c'), str(rt / 'action_prompt.c'),
str(rt / 'sarc.c'), '-o', str(exe), '-lzstd', '-lm'], env=env)
run = [str(exe)] + (game_args(args.romfs) if args.romfs else [])
return subprocess.run(run, env=env, timeout=300).returncode
if __name__ == '__main__':
sys.exit(main())