mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
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>
215 lines
9.4 KiB
C
215 lines
9.4 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 "sarc.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]; }
|
|
|
|
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;
|
|
size_t rn = 0, at, size;
|
|
unsigned next = 0;
|
|
uint8_t *raw = zsdic_unpack(z, zn, MAX_RAW, &rn);
|
|
if (raw) {
|
|
while (ndicts < 8 && sarc_next(raw, rn, NULL, &next, &at, &size) == 1) {
|
|
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;
|
|
}
|