Files
NX2navi/runtime/menu_assets.c
chrg6 29fe2b4396 Restyle the settings menu with the game's own interface fonts and textures, and give the window a gold pointer.
runtime/menu_assets.c reads them from the user's RomFS at runtime (nothing is shipped): .zs files with the dictionaries of Pack/ZsDic.pack.zs (streamed when a frame does not say its size), the fonts of Font2/Scalable behind their 8-byte header and per-word XOR key, and BNTX textures (Switch 2 archives say "Ounc" where Switch ones say "NX  ") through the runtime's block-linear copy and BC4 and ASTC decoders. ui/menu_art.cpp decodes the shared interface archive on a worker at the first present and uploads each texture for Dear ImGui at its first use. The menu now uses ClarimoUDPE for its text (with a sixth more line height) and CetraDisplayPro for its title and tabs, Roboto merged behind both; umber, brown and gold colours; the game's ornamented frame along the window's edge (the art holds two sides and is drawn again turned for the other two); its cursor glow and line around the selected row (edges only, so the label stays readable); and its control-guide bar under Continue and Quit. Without the files it keeps Roboto and plain shapes. The window's pointer is an original gold arrowhead drawn at start for the system DPI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 09:26:51 -05:00

236 lines
11 KiB
C

// The settings menu's look from the game's own files in the user's RomFS, read at the first use (menu_assets.h): its
// interface fonts (Font2/Scalable) and the shared interface textures (UI/Layout2Archive/Common...bntx). Nothing is
// shipped; a RomFS without them leaves the menu as it was.
//
// - .zs files are zstd frames, most with one of the dictionaries of Pack/ZsDic.pack.zs (a SARC compressed without one;
// the frame names its dictionary by id).
// - A .bfotf / .bfttf font is an OpenType / TrueType file behind an 8-byte header, every big-endian 32-bit word XORed
// with one key: the first word is 0x7f9a0218 under it, the second the payload size.
// - BNTX: the platform block at 0x20 ("NX ", or "Ounc" on Switch 2) counts the textures (+4) and points at their BRTI records (+8); a BRTI holds the
// format (+0x1c), width and height (+0x24, +0x28), the block height of the block-linear layout as log2 of GOBs
// (+0x34), the level size (+0x50), the name (+0x60, a u16-length string) and the level offsets (+0x70). The levels
// are block linear in the Switch 2 GOB layout (tex_deswizzle.c tex_bl_copy). BC4 masks become white with the mask as
// alpha (the menu tints them), ASTC 4x4 goes through the runtime's decoder.
#include <stdlib.h>
#include <string.h>
#include <zstd.h>
#include "fs.h"
#include "host.h"
#include "menu_assets.h"
#include "tex.h"
#define MAX_RAW (512u << 20)
static uint32_t le32(const uint8_t *p) { return (uint32_t)p[0] | (uint32_t)p[1] << 8 | (uint32_t)p[2] << 16 | (uint32_t)p[3] << 24; }
static uint16_t le16(const uint8_t *p) { return (uint16_t)(p[0] | p[1] << 8); }
static uint64_t le64(const uint8_t *p) { return (uint64_t)le32(p) | (uint64_t)le32(p + 4) << 32; }
static uint32_t be32(const uint8_t *p) { return (uint32_t)p[0] << 24 | (uint32_t)p[1] << 16 | (uint32_t)p[2] << 8 | p[3]; }
// SARC member `name`: start and size inside p[0..n); 0 when absent or out of range.
static int sarc_member(const uint8_t *p, size_t n, const char *name, size_t *at, size_t *size) {
if (n < 0x14 || memcmp(p, "SARC", 4)) return 0;
size_t hl = le16(p + 4), data = le32(p + 0xc);
if (hl + 0xc > n || memcmp(p + hl, "SFAT", 4)) return 0;
size_t count = le16(p + hl + 6), ents = hl + 0xc, names = ents + 16 * count + 8;
if (names > n || memcmp(p + ents + 16 * count, "SFNT", 4)) return 0;
for (size_t i = 0; i < count; i++) {
const uint8_t *e = p + ents + 16 * i;
size_t no = names + (size_t)(le32(e + 4) & 0xffff) * 4, s = le32(e + 8), end = le32(e + 12);
if (no >= n || data + end > n || s > end) continue;
size_t len = strnlen((const char *)p + no, n - no);
if (len == strlen(name) && !memcmp(p + no, name, len)) {
*at = data + s, *size = end - s;
return 1;
}
}
return 0;
}
static ZSTD_DDict *dicts[8];
static unsigned dict_ids[8];
static int ndicts = -1;
static void dicts_load(void) {
ndicts = 0;
size_t zn = 0;
uint64_t zn64 = 0;
uint8_t *z = romfs_load("Pack/ZsDic.pack.zs", &zn64);
zn = (size_t)zn64;
if (!z) return;
unsigned long long rn = ZSTD_getFrameContentSize(z, zn);
uint8_t *raw = rn && rn <= MAX_RAW ? malloc((size_t)rn) : NULL;
if (raw && ZSTD_decompress(raw, (size_t)rn, z, zn) == rn) {
static const char *const names[] = {"zs.zsdic", "pack.zsdic", "bcett.byml.zsdic", "bkprb.zsdic"};
for (int i = 0; i < 4 && ndicts < 8; i++) {
size_t at, size;
if (!sarc_member(raw, (size_t)rn, names[i], &at, &size)) continue;
ZSTD_DDict *d = ZSTD_createDDict(raw + at, size);
if (!d) continue;
dict_ids[ndicts] = ZSTD_getDictID_fromDDict(d);
dicts[ndicts++] = d;
}
}
free(raw);
free(z);
}
uint8_t *menu_asset_load(const char *rel, size_t *n_out) {
uint64_t n = 0;
uint8_t *b = romfs_load(rel, &n);
size_t len = strlen(rel);
if (!b || len < 3 || strcmp(rel + len - 3, ".zs")) {
if (b && n_out) *n_out = (size_t)n;
return b;
}
unsigned long long rn = ZSTD_getFrameContentSize(b, (size_t)n);
unsigned id = ZSTD_getDictID_fromFrame(b, (size_t)n);
if (rn == ZSTD_CONTENTSIZE_UNKNOWN) { // a frame that does not say its size: streamed into a growing buffer
ZSTD_DDict *d = NULL;
if (id) {
if (ndicts < 0) dicts_load();
for (int i = 0; i < ndicts; i++)
if (dict_ids[i] == id) d = dicts[i];
if (!d) { free(b); return NULL; }
}
ZSTD_DCtx *dc = ZSTD_createDCtx();
if (d && dc) ZSTD_DCtx_refDDict(dc, d);
size_t cap = (size_t)n * 4 + (1u << 20), len = 0;
uint8_t *out = dc ? malloc(cap) : NULL;
ZSTD_inBuffer in = {b, (size_t)n, 0};
size_t r = 1;
while (out && r != 0) {
if (len == cap) {
if (cap >= MAX_RAW) { free(out); out = NULL; break; }
cap = cap * 2 > MAX_RAW ? MAX_RAW : cap * 2;
uint8_t *grown = realloc(out, cap);
if (!grown) { free(out); out = NULL; break; }
out = grown;
}
ZSTD_outBuffer o = {out, cap, len};
r = ZSTD_decompressStream(dc, &o, &in);
len = o.pos;
if (ZSTD_isError(r) || (r && in.pos == in.size && o.pos < o.size)) { free(out); out = NULL; break; }
}
ZSTD_freeDCtx(dc);
free(b);
if (out && n_out) *n_out = len;
return out;
}
uint8_t *raw = rn && rn != ZSTD_CONTENTSIZE_ERROR && rn <= MAX_RAW ? malloc((size_t)rn) : NULL;
size_t got = 0;
if (raw) {
if (id) {
if (ndicts < 0) dicts_load();
ZSTD_DDict *d = NULL;
for (int i = 0; i < ndicts; i++)
if (dict_ids[i] == id) d = dicts[i];
ZSTD_DCtx *dc = d ? ZSTD_createDCtx() : NULL;
got = dc ? ZSTD_decompress_usingDDict(dc, raw, (size_t)rn, b, (size_t)n, d) : 0;
ZSTD_freeDCtx(dc);
} else {
got = ZSTD_decompress(raw, (size_t)rn, b, (size_t)n);
}
}
free(b);
if (!raw || ZSTD_isError(got) || got != rn) {
free(raw);
return NULL;
}
if (n_out) *n_out = (size_t)rn;
return raw;
}
uint8_t *menu_asset_font(const char *rel, size_t *n_out) {
size_t n = 0;
uint8_t *b = menu_asset_load(rel, &n);
if (!b || n < 12) { free(b); return NULL; }
uint32_t key = be32(b) ^ 0x7f9a0218u, size = be32(b + 4) ^ key;
if (size > n - 8 || size < 12) { free(b); return NULL; }
uint8_t *out = malloc(size);
if (!out) { free(b); return NULL; }
for (uint32_t i = 0; i < size; i += 4) {
uint8_t w[4] = {0};
uint32_t k = size - i < 4 ? size - i : 4;
memcpy(w, b + 8 + i, k);
uint32_t v = be32(w) ^ key;
uint8_t o[4] = {(uint8_t)(v >> 24), (uint8_t)(v >> 16), (uint8_t)(v >> 8), (uint8_t)v};
memcpy(out + i, o, k);
}
free(b);
// OpenType with CFF ("OTTO") or TrueType outlines (0x00010000)
if (memcmp(out, "OTTO", 4) && be32(out) != 0x00010000u) { free(out); return NULL; }
if (n_out) *n_out = size;
return out;
}
static void bc4_decode(const uint8_t *blocks, uint32_t wb, uint32_t hb, uint32_t w, uint32_t h, uint8_t *rgba) {
for (uint32_t by = 0; by < hb; by++)
for (uint32_t bx = 0; bx < wb; bx++) {
const uint8_t *b = blocks + ((size_t)by * wb + bx) * 8;
uint8_t pal[8] = {b[0], b[1]};
if (b[0] > b[1])
for (int i = 0; i < 6; i++) pal[2 + i] = (uint8_t)(((6 - i) * b[0] + (i + 1) * b[1]) / 7);
else {
for (int i = 0; i < 4; i++) pal[2 + i] = (uint8_t)(((4 - i) * b[0] + (i + 1) * b[1]) / 5);
pal[6] = 0, pal[7] = 255;
}
uint64_t bits = 0;
for (int i = 0; i < 6; i++) bits |= (uint64_t)b[2 + i] << (8 * i);
for (int i = 0; i < 16; i++) {
uint32_t x = bx * 4 + i % 4, y = by * 4 + i / 4;
if (x >= w || y >= h) continue;
uint8_t *p = rgba + ((size_t)y * w + x) * 4;
p[0] = p[1] = p[2] = 255;
p[3] = pal[bits >> (3 * i) & 7];
}
}
}
int menu_asset_texture(const uint8_t *bntx, size_t n, const char *name, uint8_t **rgba_out, uint32_t *w_out, uint32_t *h_out) {
// the platform block at 0x20 is "NX " on Switch, "Ounc" in Switch 2 archives; its layout is the same
if (!bntx || n < 0x40 || memcmp(bntx, "BNTX", 4)) return 0;
uint32_t count = le32(bntx + 0x24);
if (!count || count > 65536) return 0;
uint64_t arr = le64(bntx + 0x28);
if (arr + 8ull * count > n) return 0;
for (uint32_t i = 0; i < count; i++) {
uint64_t p = le64(bntx + arr + 8ull * i);
if (p + 0x78 > n || memcmp(bntx + p, "BRTI", 4)) continue;
uint64_t no = le64(bntx + p + 0x60);
if (no + 2 > n) continue;
uint16_t nl = le16(bntx + no);
if (no + 2 + nl > n || nl != strlen(name) || memcmp(bntx + no + 2, name, nl)) continue;
uint32_t fmt = le32(bntx + p + 0x1c), w = le32(bntx + p + 0x24), h = le32(bntx + p + 0x28);
uint32_t th = le32(bntx + p + 0x34), size = le32(bntx + p + 0x50);
uint64_t mips = le64(bntx + p + 0x70);
if (mips + 8 > n || !w || !h || w > 8192 || h > 8192 || th > 5) return 0;
uint64_t data = le64(bntx + mips);
if (data + size > n) return 0;
uint32_t kind = fmt >> 8, bpb, bw = 4, bh = 4;
if (kind == 0x1d) bpb = 8; // BC4
else if (kind == 0x2d) bpb = 16; // ASTC 4x4
else if (kind == 0x0b) bpb = 4, bw = bh = 1; // R8G8B8A8
else {
rt_log("menu: interface texture %s has format %#x, not read", name, fmt);
return 0;
}
uint32_t wb = (w + bw - 1) / bw, hb = (h + bh - 1) / bh, row = wb * bpb;
// The block-linear source padded out to whole GOB columns and block-height units (tex_bl_copy reads them).
uint64_t gob_cols = (row + 63) / 64, unit_rows = 8ull << th, units = (hb + unit_rows - 1) / unit_rows;
uint64_t need = gob_cols * units * (512ull << th);
uint8_t *bl = calloc(1, need > size ? need : size), *lin = malloc((size_t)row * hb), *rgba = malloc((size_t)w * h * 4);
if (!bl || !lin || !rgba) { free(bl), free(lin), free(rgba); return 0; }
memcpy(bl, bntx + data, size);
tex_bl_copy(bl, lin, row, row, hb, 1, th, 0, 1);
if (kind == 0x1d) bc4_decode(lin, wb, hb, w, h, rgba);
else if (kind == 0x2d) astc_decode_ldr(lin, w, h, 4, 4, (fmt & 0xff) == 6, rgba);
else memcpy(rgba, lin, (size_t)w * h * 4);
free(bl), free(lin);
*rgba_out = rgba, *w_out = w, *h_out = h;
return 1;
}
return 0;
}