diff --git a/docs/building.md b/docs/building.md index d41af214..67c81b2a 100644 --- a/docs/building.md +++ b/docs/building.md @@ -348,6 +348,21 @@ A change to the button prompts (`tools/make_romfs_patches.py --prompts`, `tools/ `.venv-py314\Scripts\python tests\romfs_prompts_test.py`, which runs the tool on a RomFS it makes itself (no game files); a change to the settings passes `nx2navi.exe --probe-cfg`. +A change to the action prompt's diamond of face buttons (`runtime/action_prompt.c`) or to the SARC and zstd dictionary +helpers (`runtime/sarc.c`) passes `python tests\action_prompt_check.py`. It builds them alone, with the +undefined-behaviour sanitizer, on a layout file, archives and dictionaries it makes from the format `action_prompt.c`'s +header comment describes, and checks that: + +- by label every place, cross, flip and bubble moves and no other byte, in every copy of the layout in the archive; +- a second run changes nothing, and by label then by position gives the game's file back; +- a file that is not the expected one is refused and left as it was: one value off, half laid out, one copy of four, + damaged offsets, a pane record laid over the records that lead to another field, more than 16 copies, copies that + share some of their bytes; +- the packed file names the dictionary the original named, found by its id in the pack of dictionaries. + +No game files. `--romfs ` also runs it on the game's own archive: four copies laid +out, 208 fields, the same dictionary and unpacked size, and no byte changed outside the layout's copies. + A change to what the library warm logs when the driver's cache of stage libraries is cold (`runtime/vk_warm_hint.h`, `runtime/vk_pipeline.c` `lw_main`) passes `python tests\warm_hint_check.py`, which builds the header alone and checks the added text by GPU vendor and by why the cache was cold. No game files. diff --git a/docs/controls.md b/docs/controls.md index 7e467309..3e85f537 100644 --- a/docs/controls.md +++ b/docs/controls.md @@ -21,6 +21,22 @@ The log names the layout in effect (`input: gamepad layout ...`) and each gamepa (`input: gamepad "...": first press`); the menu shows the layout under *Automatic*. The game's prompts show the Switch letters unless *Button prompts* says otherwise (below). +The action prompt (top right) shows the four face buttons as a diamond. The game draws it in the Switch places (A right, +B bottom, X top, Y left), which `nintendo` keeps. Under `xbox`, `playstation` and `auto` the diamond follows such a +pad instead (A bottom, B right, X left, Y top), whatever *Button prompts* is. + +The game reads the diamond once at start, before a gamepad is open: + +- `auto` gets the Xbox diamond, so a Switch Pro Controller under `auto` sees A at the bottom although its A is on the + right. Pick `nintendo` for that pad. +- A change of layout shows after a restart; until then *Button prompts* says *prompts change after restart*. +- A RomFS mod that changes the action prompt keeps its own diamond. A mod that replaces the same archive for other + reasons gets the diamond moved on top of its changes. + +The log says what happened in a line that starts with `action prompt:`. How the layout is changed: +`runtime/action_prompt.c` (`DoCommand_00` in `UI/Layout2Archive/Common.Product.100.Nin_Ounce_NVN2.blarc.zs`, served +from `/action-prompt/`). + ## Button prompts `NX2NAVI_PROMPTS` (`prompts`, the *Button prompts* row on the Controls tab) picks the buttons the game's prompts show: `nintendo` (default, the game's own), `xbox`, `playstation`, `steamdeck` or `pc` (*Keyboard and mouse*). It changes three things the game draws: diff --git a/runtime/action_prompt.c b/runtime/action_prompt.c new file mode 100644 index 00000000..22634380 --- /dev/null +++ b/runtime/action_prompt.c @@ -0,0 +1,406 @@ +// The action prompt's diamond of face buttons for the pad layout at start. The action prompt (top right) shows the +// four face buttons as a diamond in the Switch's places, A on the right. Under a layout by label (NX2NAVI_PAD_LAYOUT = +// xbox, playstation or auto; input_map.c input_pad_layout_pick) A is the bottom button, so romfs.c +// (action_prompt_overlay) serves the shared interface layouts with the diamond laid out as such a pad has its buttons: +// A bottom, B right, X left, Y top. The game reads the layouts once at start, so a change of layout shows after a +// restart. +// +// The layout is DoCommand_00.buil in the SARC UI/Layout2Archive/Common.Product.100.Nin_Ounce_NVN2.blarc.zs, a BFFH +// file of little-endian records that point at each other by 64-bit file offsets: +// - The header has a group of slots per table: at +0x00 the offset of the list of name offsets, at +0x08 that of the +// list of record offsets, at +0x10 that of the names' hashes, at +0x18 the count. Panes at 0x118, materials at 0x138. +// The group at 0x218 is the materials' texture blocks: no names, the list of the blocks' offsets in the third slot +// (0x228), the count at 0x230. +// - A pane record holds scale x and y at +0x10 and +0x14, translate x and y at +0x1c and +0x20, and flags at +0x38, in +// which 0x1000 puts a text pane's origin on its right edge. +// - A material record holds its block's index at +0x14. A block starts with the offset of its texture bindings and +// their count; a binding is 16 bytes, the offset of the texture's name at +0x08. +// Each face button K has a group N_K_00, shown while K's action is offered: P_BaseK_00, a cross whose lit arm points at +// K; three dots, the other buttons, each 80 units out in its own direction; and N_BtnK_00 at K's place, with K's +// letter, the bubble behind the action's name (W_BaseK_00, and W_BaseKDecide_00 under the null N_BaseKDecide_00, which +// the Decide animation scales) and the name (T_GuideK_00). The bubble opens to the right, or to the left for the left +// button, so the name stays clear of the diamond. The cross has two pictures, the lit arm right +// (DoCommandCrossBase_A_00 and DoCommandCrossBaseGrd_A_00) or down (_B_00), flipped for left and up. Each cross's +// material has texture bindings of its own; only the names' strings are shared, so a cross changes picture by taking +// the other picture's name offsets. No animation keys these positions, flips or flags, so the layout holds while the +// prompt animates. +#include +#include +#include +#include +#include + +#include "action_prompt.h" +#include "host.h" +#include "platform/platform.h" +#include "sarc.h" + +#define MEMBER "DoCommand_00.buil" +#define MAX_RAW (64u << 20) +enum { PANE_TABLE = 0x118, MATERIAL_TABLE = 0x138, BLOCKS = 0x228, BLOCK_COUNT = 0x230 }; +// The pane record's fields this reads; PANE_NEEDS: the bytes a record needs to hold all of them. The same for a +// material record. +enum { PANE_SCALE_X = 0x10, PANE_SCALE_Y = 0x14, PANE_X = 0x1c, PANE_Y = 0x20, PANE_FLAGS = 0x38, PANE_NEEDS = 0x3c }; +enum { MATERIAL_BLOCK = 0x14, MATERIAL_NEEDS = 0x18, BINDING_SIZE = 16, BINDING_NAME = 0x08, TEXT_RIGHT = 0x1000 }; + +// ---- the diamond in each layout +enum { EAST, SOUTH, NORTH, WEST }; +// A field is a float of a pane record, the text origin of a pane (F_RIGHT: 1 on its right edge, else 0) or the picture +// of a cross (F_CROSS: 0 the lit arm right, 1 down). The last two are held as floats too, so that one comparison checks +// every field. +enum { F_X, F_Y, F_SCALE_X, F_SCALE_Y, F_RIGHT, F_CROSS }; +static const unsigned field_at[] = { + [F_X] = PANE_X, [F_Y] = PANE_Y, [F_SCALE_X] = PANE_SCALE_X, [F_SCALE_Y] = PANE_SCALE_Y, [F_RIGHT] = PANE_FLAGS}; +static const char *const field_name[] = {"x", "y", "scale x", "scale y", "text origin", "cross picture"}; + +static const char buttons[4] = {'A', 'B', 'X', 'Y'}; +static const int direction[2][4] = {{EAST, SOUTH, NORTH, WEST}, // by position: the Switch's places + {SOUTH, EAST, WEST, NORTH}}; // by label: an Xbox pad's +static const float place[4][2] = {[EAST] = {80, 0}, [SOUTH] = {0, -80}, [NORTH] = {0, 80}, [WEST] = {-80, 0}}; +// The cross lit toward each direction: its picture and the flips that turn it. +static const struct { int picture; float sx, sy; } cross[4] = { + [EAST] = {0, 1, 1}, [SOUTH] = {1, 1, 1}, [NORTH] = {1, 1, -1}, [WEST] = {0, -1, 1}}; +// The panes at a face button's place: the other buttons' dots in each group, and each button (index into buttons). +static const struct { const char *pane; int button; } at_button[] = { + {"P_B_00", 1}, {"P_X_00", 2}, {"P_Y_00", 3}, {"P_A_00", 0}, {"P_X_02", 2}, {"P_Y_02", 3}, + {"P_A_02", 0}, {"P_B_03", 1}, {"P_Y_04", 3}, {"P_A_01", 0}, {"P_B_02", 1}, {"P_X_03", 2}, + {"N_BtnA_00", 0}, {"N_BtnB_00", 1}, {"N_BtnX_00", 2}, {"N_BtnY_00", 3}}; +enum { AT_BUTTON = sizeof at_button / sizeof *at_button }; + +typedef struct { + char pane[24]; // the pane; for F_CROSS the cross's pane, whose material has the same name + int field; + float value; +} Field; +// What diamond() sets: x and y of each pane at a button's place; each button's cross (picture, flips); for X and Y, +// which may be the left button, the flip and x of both bubbles and the name's x and origin. +enum { MAX_FIELDS = 2 * AT_BUTTON + 4 * 3 + 2 * 6 }; + +static int put(Field *f, int k, const char *pane, int field, float value) { + if (k >= MAX_FIELDS) return k + 1; // counted, not stored: action_prompt_lay_out refuses a count it does not expect + snprintf(f[k].pane, sizeof f[k].pane, "%s", pane); + f[k].field = field, f[k].value = value; + return k + 1; +} + +// Every field the diamond's layout sets, by label (label = 1) or by position; their count. +static int diamond(int label, Field *f) { + int k = 0; + char name[24]; + for (size_t i = 0; i < AT_BUTTON; i++) { + const float *at = place[direction[label][at_button[i].button]]; + k = put(f, k, at_button[i].pane, F_X, at[0]); + k = put(f, k, at_button[i].pane, F_Y, at[1]); + } + for (int b = 0; b < 4; b++) { + int d = direction[label][b]; + snprintf(name, sizeof name, "P_Base%c_00", buttons[b]); + k = put(f, k, name, F_CROSS, (float)cross[d].picture); + k = put(f, k, name, F_SCALE_X, cross[d].sx); + k = put(f, k, name, F_SCALE_Y, cross[d].sy); + if (b < 2) continue; // A and B are never the left button: their bubbles open to the right in both layouts + int left = d == WEST; + snprintf(name, sizeof name, "W_Base%c_00", buttons[b]); + k = put(f, k, name, F_SCALE_X, left ? -1.0f : 1.0f); + k = put(f, k, name, F_X, left ? 80.0f : -80.0f); + snprintf(name, sizeof name, "W_Base%cDecide_00", buttons[b]); + k = put(f, k, name, F_SCALE_X, left ? -1.0f : 1.0f); + k = put(f, k, name, F_X, left ? 80.0f : -80.0f); + snprintf(name, sizeof name, "T_Guide%c_00", buttons[b]); + k = put(f, k, name, F_X, left ? -68.0f : 64.0f); + k = put(f, k, name, F_RIGHT, (float)left); + } + return k; +} + +// ---- reading the layout, every offset checked against its size +static int u32_at(const uint8_t *p, size_t n, uint64_t off, uint32_t *v) { + if (off > n || n - off < 4) return 0; + memcpy(v, p + off, 4); + return 1; +} + +static int u64_at(const uint8_t *p, size_t n, uint64_t off, uint64_t *v) { + if (off > n || n - off < 8) return 0; + memcpy(v, p + off, 8); + return 1; +} + +static const char *str_at(const uint8_t *p, size_t n, uint64_t off) { + return off < n && memchr(p + off, 0, n - off) ? (const char *)p + off : NULL; +} + +// The record of the entry `name` of the table whose header slots start at slot, with at least size bytes; 0 when it +// is not there. +static uint64_t table_find(const uint8_t *p, size_t n, size_t slot, const char *name, size_t size) { + uint64_t names, records; + uint32_t count; + if (!u64_at(p, n, slot, &names) || !u64_at(p, n, slot + 8, &records) || !u32_at(p, n, slot + 24, &count) || + names > n || records > n || (n - names) / 8 < count || (n - records) / 8 < count) + return 0; + for (uint32_t i = 0; i < count; i++) { + uint64_t s, r; + const char *t; + if (!u64_at(p, n, names + 8 * (uint64_t)i, &s) || !(t = str_at(p, n, s)) || strcmp(t, name)) continue; + return u64_at(p, n, records + 8 * (uint64_t)i, &r) && r && r <= n && n - r >= size ? r : 0; + } + return 0; +} + +// The offsets of the name offsets of the cross `pane`'s picture and gradient (its material's first two texture +// bindings); 0 when they are not there. +static int cross_names(const uint8_t *p, size_t n, const char *pane, uint64_t at[2]) { + uint64_t m = table_find(p, n, MATERIAL_TABLE, pane, MATERIAL_NEEDS), blocks, block, bindings; + uint32_t index, count, bound; + if (!m || !u32_at(p, n, m + MATERIAL_BLOCK, &index) || !u64_at(p, n, BLOCKS, &blocks) || + !u32_at(p, n, BLOCK_COUNT, &count) || index >= count || !u64_at(p, n, blocks + 8 * (uint64_t)index, &block) || + !u64_at(p, n, block, &bindings) || !u32_at(p, n, block + 8, &bound) || bound < 2) + return 0; + for (int k = 0; k < 2; k++) { + uint64_t b, s; + if (!u64_at(p, n, bindings + 8 * (uint64_t)k, &b) || b > n || n - b < BINDING_SIZE || + !u64_at(p, n, b + BINDING_NAME, &s)) + return 0; + at[k] = b + BINDING_NAME; + } + return 1; +} + +// Which picture the names at at[] give the cross: 0 the lit arm right (A), 1 down (B), -1 neither. +static int cross_picture(const uint8_t *p, size_t n, const uint64_t at[2]) { + static const char *const names[2][2] = {{"DoCommandCrossBase_A_00", "DoCommandCrossBaseGrd_A_00"}, + {"DoCommandCrossBase_B_00", "DoCommandCrossBaseGrd_B_00"}}; + uint64_t s[2]; + const char *t[2]; + for (int k = 0; k < 2; k++) + if (!u64_at(p, n, at[k], &s[k]) || !(t[k] = str_at(p, n, s[k]))) return -1; + for (int c = 0; c < 2; c++) + if (!strcmp(t[0], names[c][0]) && !strcmp(t[1], names[c][1])) return c; + return -1; +} + +// Where a field is: the offset of its 4-byte word (the pane's flags for F_RIGHT), or for F_CROSS the offsets of the +// cross's two name offsets. Every offset is checked against the file's size when it is found. +typedef struct { + uint64_t at[2]; +} Where; + +static int field_find(const uint8_t *p, size_t n, const Field *f, Where *w) { + if (f->field == F_CROSS) return cross_names(p, n, f->pane, w->at); + uint64_t r = table_find(p, n, PANE_TABLE, f->pane, PANE_NEEDS); + w->at[0] = r + field_at[f->field], w->at[1] = 0; + return r != 0; +} + +// A found field's value; 0 when it is not one this knows (a cross with another picture). +static int field_get(const uint8_t *p, size_t n, const Field *f, const Where *w, float *v) { + if (f->field == F_CROSS) { + int c = cross_picture(p, n, w->at); + *v = (float)c; + return c >= 0; + } + if (f->field == F_RIGHT) { + uint32_t flags; + memcpy(&flags, p + w->at[0], 4); + *v = (flags & TEXT_RIGHT) ? 1.0f : 0.0f; + return 1; + } + memcpy(v, p + w->at[0], 4); + return 1; +} + +// The game stores some zeros as -2.8e-14. +static int same(float a, float b) { return fabsf(a - b) < 1e-3f; } + +int action_prompt_lay_out(uint8_t *p, size_t n, int label, char *why, size_t why_n) { + label = !!label; + Field from[MAX_FIELDS], to[MAX_FIELDS]; + int count = diamond(!label, from); + if (count != MAX_FIELDS || diamond(label, to) != MAX_FIELDS) { + snprintf(why, why_n, "the diamond has %d fields, not %d", count, MAX_FIELDS); + return -1; + } + if (n < 4 || memcmp(p, "BFFH", 4)) { + snprintf(why, why_n, "%s is not a BFFH layout", MEMBER); + return -1; + } + // Find and read every field before anything is written. + Where where[MAX_FIELDS]; + float now[MAX_FIELDS]; + int done = 1; + for (int k = 0; k < count; k++) { + if (!field_find(p, n, &from[k], &where[k]) || !field_get(p, n, &from[k], &where[k], &now[k])) { + snprintf(why, why_n, "%s has no %s %s", MEMBER, from[k].pane, field_name[from[k].field]); + return -1; + } + done &= same(now[k], to[k].value); + } + if (done) return 0; + for (int k = 0; k < count; k++) + if (!same(now[k], from[k].value)) { + snprintf(why, why_n, "%s %s %s is %g, not %g", MEMBER, from[k].pane, field_name[from[k].field], + (double)now[k], (double)from[k].value); + return -1; + } + // The name offsets of both pictures, from the crosses that show them now (the diamond by either layout shows both). + uint64_t names[2][2]; + for (int k = 0; k < count; k++) + if (from[k].field == F_CROSS) + for (int j = 0; j < 2; j++) memcpy(&names[(int)now[k]][j], p + where[k].at[j], 8); + // Write, keeping each word's bytes as they were. + uint8_t undo[MAX_FIELDS][2][8]; + int changed = 0; + for (int k = 0; k < count; k++) { + int words = to[k].field == F_CROSS ? 2 : 1, size = to[k].field == F_CROSS ? 8 : 4; + for (int j = 0; j < words; j++) memcpy(undo[k][j], p + where[k].at[j], size); + if (same(now[k], to[k].value)) continue; + changed++; + if (to[k].field == F_CROSS) { + for (int j = 0; j < 2; j++) memcpy(p + where[k].at[j], &names[(int)to[k].value][j], 8); + } else if (to[k].field == F_RIGHT) { + uint32_t flags; + memcpy(&flags, p + where[k].at[0], 4); + flags = to[k].value != 0 ? flags | TEXT_RIGHT : flags & ~(uint32_t)TEXT_RIGHT; + memcpy(p + where[k].at[0], &flags, 4); + } else { + memcpy(p + where[k].at[0], &to[k].value, 4); + } + } + // Read the layout again as the game will: each field where it was, with its new value. A file whose fields lie on + // the records that lead to them (or on each other) reads otherwise; it gets its bytes back, last written first. + for (int k = 0; k < count; k++) { + Where w; + float v; + if (field_find(p, n, &to[k], &w) && !memcmp(&w, &where[k], sizeof w) && field_get(p, n, &to[k], &w, &v) && + same(v, to[k].value)) + continue; + for (int i = count - 1; i >= 0; i--) { + int words = to[i].field == F_CROSS ? 2 : 1, size = to[i].field == F_CROSS ? 8 : 4; + for (int j = words - 1; j >= 0; j--) memcpy(p + where[i].at[j], undo[i][j], size); + } + snprintf(why, why_n, "%s %s %s overlaps another field or the records that lead to it", MEMBER, to[k].pane, + field_name[to[k].field]); + return -1; + } + return changed; +} + +// The game's archive holds each layout four times under the same name (628 members, 157 names; the four copies of +// DoCommand_00.buil are the same bytes, each at its own offset). Every copy is laid out, each checked first on a copy +// of its own, so either all of them change or none. +int action_prompt_archive(uint8_t *sarc, size_t n, int label, char *why, size_t why_n) { + enum { MAX_COPIES = 16 }; + size_t at[MAX_COPIES], size[MAX_COPIES], a, s; + unsigned next = 0; + int copies = 0, total = 0, found; + while ((found = sarc_next(sarc, n, MEMBER, &next, &a, &s)) == 1) { + if (copies == MAX_COPIES) { + snprintf(why, why_n, "the layout archive holds %s more than %d times", MEMBER, MAX_COPIES); + return -1; + } + at[copies] = a, size[copies++] = s; + } + if (found < 0) { + snprintf(why, why_n, "the layout archive is damaged"); + return -1; + } + if (!copies) { + snprintf(why, why_n, "the layout archive has no %s", MEMBER); + return -1; + } + // Copies that share their bytes are laid out once; copies that share only some of them cannot be laid out alone. + for (int c = 0; c < copies; c++) + for (int d = 0; d < c; d++) + if (!(at[c] == at[d] && size[c] == size[d]) && at[c] < at[d] + size[d] && at[d] < at[c] + size[c]) { + snprintf(why, why_n, "two copies of %s in the layout archive overlap", MEMBER); + return -1; + } + for (int c = 0; c < copies; c++) { + uint8_t *trial = malloc(size[c] ? size[c] : 1); + if (!trial) { + snprintf(why, why_n, "out of memory"); + return -1; + } + memcpy(trial, sarc + at[c], size[c]); + int r = action_prompt_lay_out(trial, size[c], label, why, why_n); + free(trial); + if (r < 0) return -1; + } + for (int c = 0; c < copies; c++) { + int r = action_prompt_lay_out(sarc + at[c], size[c], label, why, why_n); + if (r < 0) return -1; // not after a trial of the same bytes; kept so that a change here cannot hide one + total += r; + } + return total; +} + +int action_prompt_build(const void *in, size_t n, const void *zsdic, size_t zsdic_n, int label, void **out, + size_t *out_n) { + uint64_t t0 = plat_now_ns(); + label = !!label; + *out = NULL; + int result = -1; + uint8_t *dics = NULL, *raw = NULL; + void *z = NULL; + ZSTD_DDict *dd = NULL; + ZSTD_CDict *cd = NULL; + char why[200] = ""; + // The frame names its dictionary by id (0: none); Pack/ZsDic.pack.zs holds them. + unsigned id = ZSTD_getDictID_fromFrame(in, n); + size_t dn = 0, dic_at = 0, dic_n = 0; + if (id && (!(dics = zsdic_unpack(zsdic, zsdic_n, MAX_RAW, &dn)) || !zsdic_find(dics, dn, id, &dic_at, &dic_n))) { + snprintf(why, sizeof why, "no dictionary %u in Pack/ZsDic.pack.zs", id); + goto done; + } + if (id && (!(dd = ZSTD_createDDict(dics + dic_at, dic_n)) || !(cd = ZSTD_createCDict(dics + dic_at, dic_n, 3)))) { + snprintf(why, sizeof why, "dictionary %u of Pack/ZsDic.pack.zs cannot be loaded", id); + goto done; + } + unsigned long long rn = ZSTD_getFrameContentSize(in, n); + if (rn == ZSTD_CONTENTSIZE_UNKNOWN || rn == ZSTD_CONTENTSIZE_ERROR || !rn || rn > MAX_RAW) { + snprintf(why, sizeof why, "the layout archive is not a zstd frame with a size"); + goto done; + } + if (!(raw = malloc((size_t)rn))) { + snprintf(why, sizeof why, "out of memory"); + goto done; + } + ZSTD_DCtx *dctx = ZSTD_createDCtx(); + size_t got = !dctx ? 0 : dd ? ZSTD_decompress_usingDDict(dctx, raw, (size_t)rn, in, n, dd) + : ZSTD_decompressDCtx(dctx, raw, (size_t)rn, in, n); + ZSTD_freeDCtx(dctx); + if (ZSTD_isError(got) || got != rn) { + snprintf(why, sizeof why, "the layout archive does not decompress"); + goto done; + } + int changed = action_prompt_archive(raw, (size_t)rn, label, why, sizeof why); + if (changed <= 0) { + result = changed; + goto done; + } + // Level 3: the archive unpacks to about 10 MB and is packed again at every start (about 15 ms). + size_t cap = ZSTD_compressBound((size_t)rn), zn = 0; + ZSTD_CCtx *cctx = ZSTD_createCCtx(); + if (cctx && (z = malloc(cap))) + zn = cd ? ZSTD_compress_usingCDict(cctx, z, cap, raw, (size_t)rn, cd) + : ZSTD_compressCCtx(cctx, z, cap, raw, (size_t)rn, 3); + ZSTD_freeCCtx(cctx); + if (!z || !zn || ZSTD_isError(zn)) { + snprintf(why, sizeof why, "the layout archive cannot be compressed again"); + goto done; + } + *out = z, *out_n = zn, z = NULL; + result = 1; + rt_log("action prompt: face buttons laid out %s, %d fields (%.0f ms)", label ? "by label (A bottom)" : "by position", + changed, (double)(plat_now_ns() - t0) / 1e6); +done: + if (result == 0) + rt_log("action prompt: the layout archive has the face buttons %s already", label ? "by label" : "by position"); + if (result < 0) rt_log("action prompt: %s; the diamond keeps the Switch places", why); + free(z); + free(raw); + free(dics); + ZSTD_freeDDict(dd); + ZSTD_freeCDict(cd); + return result; +} diff --git a/runtime/action_prompt.h b/runtime/action_prompt.h new file mode 100644 index 00000000..1dc07c63 --- /dev/null +++ b/runtime/action_prompt.h @@ -0,0 +1,30 @@ +// The action prompt's diamond of face buttons laid out for a pad layout (action_prompt.c). +#ifndef ACTION_PROMPT_H +#define ACTION_PROMPT_H + +#include +#include + +// The shared interface layouts of the game, which hold the action prompt (DoCommand_00). +#define ACTION_PROMPT_ARCHIVE "UI/Layout2Archive/Common.Product.100.Nin_Ounce_NVN2.blarc.zs" + +// Lays the diamond of DoCommand_00.buil (p[0..n), in place) out for the face buttons by label (label nonzero: A +// bottom, B right, X left, Y top) or by position (label = 0: the Switch's places, A right, B bottom, X top, Y left). +// The runtime asks for label 1 only (romfs.c action_prompt_overlay); label 0 takes a layout by label back. Every field +// is found and checked before any is written, and read again after. Returns the number of fields changed; 0 when the +// diamond is laid out that way already; -1 when the file is not the layout this expects (why says which field +// differs; the file is left as it was). +int action_prompt_lay_out(uint8_t *p, size_t n, int label, char *why, size_t why_n); + +// The same on the decompressed Common layout archive (a SARC holding DoCommand_00.buil), on every copy of the layout +// it holds: all of them change or none. The count is over all copies; -1 also for a damaged archive, more than 16 +// copies, or copies that share some of their bytes. +int action_prompt_archive(uint8_t *sarc, size_t n, int label, char *why, size_t why_n); + +// The Common layout archive's zstd file in[0..n) with the diamond laid out for label, compressed again with the +// dictionary of Pack/ZsDic.pack.zs (zsdic) the original names: *out (malloc'd) and *out_n. Returns 1 when *out was +// made, 0 when the diamond is laid out that way already (nothing to serve), -1 when it cannot be (logged). +int action_prompt_build(const void *in, size_t n, const void *zsdic, size_t zsdic_n, int label, void **out, + size_t *out_n); + +#endif diff --git a/runtime/camera_speed.c b/runtime/camera_speed.c index 6a2c11af..bd330519 100644 --- a/runtime/camera_speed.c +++ b/runtime/camera_speed.c @@ -24,6 +24,7 @@ #include "cfg.h" #include "host.h" #include "platform/platform.h" +#include "sarc.h" #define MEMBER "Component/PlayerCameraParam/PlayerCamera.game__component__PlayerCameraParam.bgyml" #define MAX_RAW (64u << 20) @@ -46,32 +47,6 @@ static int u32_at(const uint8_t *p, size_t n, size_t off, uint32_t *v) { return 1; } -// SARC member `name` of the archive p[0..n): its start and size; 0 when it is not there. -static int sarc_member(const uint8_t *p, size_t n, const char *name, size_t *at, size_t *size) { - uint32_t data; - uint16_t head, count, bom; - if (n < 0x14 || memcmp(p, "SARC", 4)) return 0; - memcpy(&head, p + 4, 2); - memcpy(&bom, p + 6, 2); - if (bom != 0xfeff || !u32_at(p, n, 12, &data) || head + 12u > n || memcmp(p + head, "SFAT", 4)) return 0; - memcpy(&count, p + head + 6, 2); - size_t names = head + 12 + (size_t)count * 16; - if (names + 8 > n || memcmp(p + names, "SFNT", 4)) return 0; - names += 8; - for (unsigned i = 0; i < count; i++) { - uint32_t attr, lo, hi; - size_t e = head + 12 + (size_t)i * 16; - if (!u32_at(p, n, e + 4, &attr) || !u32_at(p, n, e + 8, &lo) || !u32_at(p, n, e + 12, &hi)) return 0; - size_t s = names + (size_t)(attr & 0xffffff) * 4, len = strlen(name); - if (!(attr >> 24) || s + len + 1 > n || memcmp(p + s, name, len + 1)) continue; - if (lo > hi || data + (size_t)hi > n) return 0; - *at = data + lo; - *size = hi - lo; - return 1; - } - return 0; -} - // The StickSensitivity floats of the BYML p[0..n), multiplied by k; their count, 0 when the file has no such array. static int scale_sensitivity(uint8_t *p, size_t n, float k, float was[8]) { uint32_t keys, root, head; @@ -117,21 +92,19 @@ int cam_pack_build(const void *in, size_t n, const void *zsdic, size_t zsdic_n, ZSTD_DDict *dd = NULL; ZSTD_CDict *cd = NULL; void *z = NULL; - // The dictionaries: Pack/ZsDic.pack.zs is a SARC compressed without one. - unsigned long long dn = ZSTD_getFrameContentSize(zsdic, zsdic_n); - size_t dic_at, dic_n; - if (dn == ZSTD_CONTENTSIZE_UNKNOWN || dn == ZSTD_CONTENTSIZE_ERROR || dn > MAX_RAW || !(dics = malloc((size_t)dn)) || - ZSTD_decompress(dics, (size_t)dn, zsdic, zsdic_n) != dn || - !sarc_member(dics, (size_t)dn, "pack.zsdic", &dic_at, &dic_n)) { - rt_log("camera speed: no pack.zsdic in Pack/ZsDic.pack.zs; the camera keeps its own speed"); + // The frame names its dictionary (pack.zsdic) by id; Pack/ZsDic.pack.zs holds them. + unsigned id = ZSTD_getDictID_fromFrame(in, n); + size_t dn, dic_at, dic_n; + if (!id || !(dics = zsdic_unpack(zsdic, zsdic_n, MAX_RAW, &dn)) || !zsdic_find(dics, dn, id, &dic_at, &dic_n)) { + rt_log("camera speed: Pack/ZsDic.pack.zs has no dictionary %u for Pack/Actor/PlayerCamera.pack.zs; the camera " + "keeps its own speed", id); goto done; } unsigned long long rn = ZSTD_getFrameContentSize(in, n); - unsigned id = ZSTD_getDictID_fromFrame(in, n); dd = ZSTD_createDDict(dics + dic_at, dic_n); - if (!dd || id != ZSTD_getDictID_fromDDict(dd) || rn == ZSTD_CONTENTSIZE_UNKNOWN || - rn == ZSTD_CONTENTSIZE_ERROR || rn > MAX_RAW || !(raw = malloc((size_t)rn))) { - rt_log("camera speed: Pack/Actor/PlayerCamera.pack.zs is not a pack.zsdic frame with a size; the camera keeps its " + if (!dd || rn == ZSTD_CONTENTSIZE_UNKNOWN || rn == ZSTD_CONTENTSIZE_ERROR || rn > MAX_RAW || + !(raw = malloc((size_t)rn))) { + rt_log("camera speed: Pack/Actor/PlayerCamera.pack.zs is not a zstd frame with a size; the camera keeps its " "own speed"); goto done; } diff --git a/runtime/fs.h b/runtime/fs.h index 01876c57..9736600f 100644 --- a/runtime/fs.h +++ b/runtime/fs.h @@ -51,6 +51,9 @@ enum { ROMFS_PROMPTS_OK, ROMFS_PROMPTS_MISSING, ROMFS_PROMPTS_FAILED, ROMFS_PROM const char *romfs_prompts(void); int romfs_prompts_ready(const char *folder); int romfs_prompts_off(const char *folder); +// The pad layout (PAD_LAYOUT_*, input_map.h) the action prompt's diamond of face buttons has from start +// (action_prompt.c); -1 when the layout by label was asked for but the layout file could not be changed. +int romfs_action_prompt(void); const char *romfs_overlay(size_t k, const char **file, uint64_t *size, int *is_added); // RomFS path of overlay k void fs_trace(const char *what, const char *path); // NX2NAVI_TRACE=fs: one line per open diff --git a/runtime/menu_assets.c b/runtime/menu_assets.c index d4036a89..ea464846 100644 --- a/runtime/menu_assets.c +++ b/runtime/menu_assets.c @@ -19,6 +19,7 @@ #include "fs.h" #include "host.h" #include "menu_assets.h" +#include "sarc.h" #include "tex.h" #define MAX_RAW (512u << 20) @@ -28,26 +29,6 @@ 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; @@ -59,13 +40,11 @@ static void dicts_load(void) { 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; + 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); diff --git a/runtime/romfs.c b/runtime/romfs.c index 5316a9c1..616efad8 100644 --- a/runtime/romfs.c +++ b/runtime/romfs.c @@ -30,6 +30,7 @@ #include #include +#include "action_prompt.h" #include "cfg.h" #include "fs.h" #include "host.h" @@ -782,6 +783,32 @@ static void camera_overlay(void) { if (ok && serve_generated(CAMPACK, base, out, out_n, "camera", "camera speed", keeps)) cam_pack_served(); } +// The action prompt's diamond of face buttons for the pad layout at start (action_prompt.c). By position (the +// Switch's places) the game's own layout is right; by label the Common layout archive the game would read (a mod's, else +// the RomFS's) is served with the diamond laid out again, written to /action-prompt/. A pad is not open yet +// this early, so auto counts as by label, as for the prompts' folders (input_map.c input_prompts_folder). +static int action_prompt_for = -1; // romfs_action_prompt +static void action_prompt_overlay(void) { + int layout = input_pad_layout_pick(cfg_value("NX2NAVI_PAD_LAYOUT"), 0); + if (layout == PAD_LAYOUT_NINTENDO) { + action_prompt_for = layout; + return; + } + static const char keeps[] = "the action prompt keeps the Switch places"; + Overlay *base, *dic_base; + size_t n, dn, out_n = 0; + void *in = read_served(ACTION_PROMPT_ARCHIVE, &base, &n, keeps), *out = NULL; + void *dic = in ? read_served("Pack/ZsDic.pack.zs", &dic_base, &dn, keeps) : NULL; + int made = in && dic ? action_prompt_build(in, n, dic, dn, 1, &out, &out_n) : -1; + free(in); + free(dic); + if (made == 0 || (made > 0 && serve_generated(ACTION_PROMPT_ARCHIVE, base, out, out_n, "action-prompt", + "face buttons by label", keeps))) + action_prompt_for = layout; +} + +int romfs_action_prompt(void) { return action_prompt_for; } + // The button prompts' folders (input_prompts_folder) and whether each had a manifest next to romfs-index.bin at start; // the one the settings asked for at start, the one overlay_init loaded (NULL: the game's own prompts) and why the first // did not load (ROMFS_PROMPTS_*). The menu compares them with the settings in effect (ui/menu.cpp prompts_note). @@ -938,6 +965,7 @@ static void overlay_init(void) { restbl_overlay(); heapdef_overlay(); camera_overlay(); + action_prompt_overlay(); added_init(); if (noverlay) qsort(overlays, noverlay, sizeof *overlays, overlay_order); for (size_t k = 1; k < noverlay; k++) // overlay_contest left one per file; the generated ones replace in place diff --git a/runtime/sarc.c b/runtime/sarc.c new file mode 100644 index 00000000..25cf5412 --- /dev/null +++ b/runtime/sarc.c @@ -0,0 +1,71 @@ +// SARC archives and the zstd dictionaries of Pack/ZsDic.pack.zs. A SARC is a little-endian header ("SARC", its +// length at +4, the byte-order mark 0xfeff at +6, the start of the members' data at +12), then SFAT: its member count +// at +6 and from +12 one 16-byte entry per member (name hash, attributes, start and end relative to the data). An +// entry's attributes hold a nonzero top byte when the member has a name, and in the low 24 bits the name's offset in +// SFNT's string table divided by 4; SFNT's 8-byte header follows the entries. The game's archives may hold several +// members under one name (UI/Layout2Archive/Common...blarc.zs holds each layout four times), so members are walked +// one by one. +#include +#include +#include + +#include "sarc.h" + +int sarc_next(const uint8_t *p, size_t n, const char *name, unsigned *next, size_t *at, size_t *size) { + uint32_t data; + uint16_t head, count, bom; + if (n < 0x14 || memcmp(p, "SARC", 4)) return -1; + memcpy(&head, p + 4, 2); + memcpy(&bom, p + 6, 2); + memcpy(&data, p + 12, 4); + if (bom != 0xfeff || (size_t)head + 12 > n || memcmp(p + head, "SFAT", 4)) return -1; + memcpy(&count, p + head + 6, 2); + size_t names = (size_t)head + 12 + (size_t)count * 16; // the entries end where SFNT starts + if (names + 8 > n || memcmp(p + names, "SFNT", 4)) return -1; + names += 8; + size_t len = name ? strlen(name) : 0; + for (unsigned i = *next; i < count; i++) { + uint32_t attr, lo, hi; + const uint8_t *e = p + head + 12 + (size_t)i * 16; + memcpy(&attr, e + 4, 4); + memcpy(&lo, e + 8, 4); + memcpy(&hi, e + 12, 4); + if (name) { + size_t s = names + (size_t)(attr & 0xffffff) * 4; + if (!(attr >> 24) || s > n || n - s < len + 1 || memcmp(p + s, name, len + 1)) continue; + } + if (lo > hi || (size_t)data + hi > n) return -1; + *at = (size_t)data + lo; + *size = hi - lo; + *next = i + 1; + return 1; + } + *next = count; + return 0; +} + +int sarc_member(const uint8_t *p, size_t n, const char *name, size_t *at, size_t *size) { + unsigned next = 0; + return sarc_next(p, n, name, &next, at, size) == 1; +} + +uint8_t *zsdic_unpack(const void *z, size_t zn, size_t max, size_t *n) { + unsigned long long rn = ZSTD_getFrameContentSize(z, zn); + if (rn == ZSTD_CONTENTSIZE_UNKNOWN || rn == ZSTD_CONTENTSIZE_ERROR || !rn || rn > max) return NULL; + uint8_t *raw = malloc((size_t)rn); + size_t got = raw ? ZSTD_decompress(raw, (size_t)rn, z, zn) : 0; + if (!raw || ZSTD_isError(got) || got != rn) { + free(raw); + return NULL; + } + *n = (size_t)rn; + return raw; +} + +int zsdic_find(const uint8_t *dics, size_t n, unsigned id, size_t *at, size_t *size) { + unsigned next = 0; + if (!id) return 0; + while (sarc_next(dics, n, NULL, &next, at, size) == 1) + if (ZSTD_getDictID_fromDict(dics + *at, *size) == id) return 1; + return 0; +} diff --git a/runtime/sarc.h b/runtime/sarc.h new file mode 100644 index 00000000..3be54481 --- /dev/null +++ b/runtime/sarc.h @@ -0,0 +1,24 @@ +// SARC archives and the zstd dictionaries of Pack/ZsDic.pack.zs (sarc.c). +#ifndef SARC_H +#define SARC_H + +#include +#include + +// The next member of the SARC p[0..n) from member *next on that is named `name` (any member when name is NULL): 1 with +// its start and size, *next moved past it; 0 when no such member is left; -1 when the archive is damaged (its header, +// or the found member's range). +int sarc_next(const uint8_t *p, size_t n, const char *name, unsigned *next, size_t *at, size_t *size); + +// The first member named `name`: 1 with its start and size; 0 when there is none or the archive is damaged. +int sarc_member(const uint8_t *p, size_t n, const char *name, size_t *at, size_t *size); + +// Pack/ZsDic.pack.zs's archive (z[0..zn), a frame compressed without a dictionary) unpacked: malloc'd, *n its size; +// NULL when it is not a zstd frame that says its size, or that size is over max. +uint8_t *zsdic_unpack(const void *z, size_t zn, size_t max, size_t *n); + +// The dictionary with id `id` among the members of that archive (dics[0..n)): 1 with its start and size; 0 when none +// has it (and always for id 0, the id of a frame without a dictionary). +int zsdic_find(const uint8_t *dics, size_t n, unsigned id, size_t *at, size_t *size); + +#endif diff --git a/runtime/ui/menu.cpp b/runtime/ui/menu.cpp index 15dd3beb..b673a304 100644 --- a/runtime/ui/menu.cpp +++ b/runtime/ui/menu.cpp @@ -1268,13 +1268,17 @@ static void row_walk_key(void) { // layout, as a layout that puts A on the right button needs the face buttons named the other way round), but the game // reads its fonts and UI archive once at start (romfs.c overlay_init). While the folder in use differs: "files // missing" when the setup did not write the one asked for, why it was left out at start (damaged, a game resolution -// that is not 16:9, a RomFS mod with the same files, NX2NAVI_ROMFS_PATCHES), else "prompts change after restart", and only then -// *restart is set: a restart would bring the prompts asked for. NULL when they match. +// that is not 16:9, a RomFS mod with the same files, NX2NAVI_ROMFS_PATCHES), else "prompts change after restart" with +// *restart set: a restart would bring the prompts asked for. The action prompt's diamond of face buttons is laid out +// for the pad layout at start too (romfs.c action_prompt_overlay), whatever the prompts: while the layout asks for the +// other places, a restart would lay it out again, so *restart is set whatever the note, and with the folder in use the +// note is "prompts change after restart". NULL when both match. static const char *prompts_note(bool *restart) { - *restart = false; const char *layout = cfg_value("NX2NAVI_PAD_LAYOUT"), *used = romfs_prompts(); const char *want = input_prompts_folder(input_prompts_pick(cfg_get("NX2NAVI_PROMPTS"), layout), layout); - if (want == used || (want && used && !strcmp(want, used))) return nullptr; + int diamond = romfs_action_prompt(); // -1 when it could not be laid out: a restart would not change it + *restart = diamond >= 0 && diamond != input_pad_layout_pick(layout, 0); + if (want == used || (want && used && !strcmp(want, used))) return *restart ? menu_text(UI_MENU_PROMPTS_RESTART) : nullptr; // A game resolution of another shape needs a folder of its own for the PC prompts (romfs-patches-pc-x), which // the setup does not write: the shape is the reason then, not the setup. int off = romfs_prompts_off(want); diff --git a/tests/action_prompt_check.c b/tests/action_prompt_check.c new file mode 100644 index 00000000..e4cae8f6 --- /dev/null +++ b/tests/action_prompt_check.c @@ -0,0 +1,596 @@ +// runtime/action_prompt.c: the action prompt's diamond of face buttons laid out by label. The layout file here is made +// by the test from the format as action_prompt.c's header comment describes it, with the values the game's 1.1.1 +// DoCommand_00.buil holds (A right, B bottom, X top, Y left), so the reader is checked against a writer of its own; +// the archives and zstd dictionaries are made here too. No game files. Given romfs.bin and the offsets and sizes of +// the layout archive and Pack/ZsDic.pack.zs in it, it also checks the game's own archive (game_archive). Built and +// run by tests/action_prompt_check.py; prints each failed check and exits 1 on any. +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "action_prompt.h" +#include "sarc.h" + +static int fails; +#define CHECK(c, ...) \ + do { \ + if (!(c)) { \ + fails++; \ + printf("FAIL line %d: ", __LINE__); \ + printf(__VA_ARGS__); \ + printf("\n"); \ + } \ + } while (0) + +// ---- what action_prompt.c reaches in the runtime +static char last_log[512]; +void rt_log(const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + vsnprintf(last_log, sizeof last_log, fmt, ap); + va_end(ap); +} +uint64_t plat_now_ns(void) { return (uint64_t)((double)clock() / CLOCKS_PER_SEC * 1e9); } + +// ---- a BFFH layout as the game's DoCommand_00.buil has it +enum { SIZE = 0x8000, PANE_REC = 0x44 }; +static uint8_t file[SIZE]; +static size_t top; // the next free byte + +static uint64_t alloc(size_t n) { + top = (top + 7) & ~(size_t)7; + uint64_t at = top; + top += n; + if (top > SIZE) { + printf("the test's layout is too small\n"); + exit(2); + } + return at; +} +static void w32(uint64_t at, uint32_t v) { memcpy(file + at, &v, 4); } +static void w64(uint64_t at, uint64_t v) { memcpy(file + at, &v, 8); } +static void wf(uint64_t at, float v) { memcpy(file + at, &v, 4); } +static void set_f(uint8_t *p, uint64_t at, float v) { memcpy(p + at, &v, 4); } +static float rf(const uint8_t *p, uint64_t at) { + float v; + memcpy(&v, p + at, 4); + return v; +} +static uint32_t r32(const uint8_t *p, uint64_t at) { + uint32_t v; + memcpy(&v, p + at, 4); + return v; +} +static uint64_t r64(const uint8_t *p, uint64_t at) { + uint64_t v; + memcpy(&v, p + at, 8); + return v; +} +static uint64_t str(const char *s) { + uint64_t at = alloc(strlen(s) + 1); + memcpy(file + at, s, strlen(s) + 1); + return at; +} + +#define Z (-2.842171e-14f) // the game stores some zeros so +static const struct { + const char *name; + float x, y, sx, sy; + uint32_t flags; +} panes[] = { + {"RootPane", 0, 0, 1, 1, 0x02555000}, + {"P_BaseA_00", 0, 0, 1, 1, 0x02555089}, {"P_B_00", 0, -80, 1, 1, 0x0255508b}, {"P_X_00", 0, 80, 1, 1, 0x0255508b}, + {"P_Y_00", -80, 0, 1, 1, 0x0255508b}, {"N_BtnA_00", 80, 0, 1, 1, 0x0255500b}, + {"P_BaseB_00", Z, 0, 1, 1, 0x02555089}, {"P_A_00", 80, 0, 1, 1, 0x0255508b}, {"P_X_02", Z, 80, 1, 1, 0x0255508b}, + {"P_Y_02", -80, 0, 1, 1, 0x0255508b}, {"N_BtnB_00", 0, -80, 1, 1, 0x0255500b}, + {"P_BaseX_00", Z, 0, 1, -1, 0x02555089}, {"P_A_02", 80, 0, 1, 1, 0x0255508b}, {"P_B_03", Z, -80, 1, 1, 0x0255508b}, + {"P_Y_04", -80, 0, 1, 1, 0x0255508b}, {"N_BtnX_00", 0, 80, 1, 1, 0x0255500b}, + {"W_BaseX_00", -80, 0, 1, 1, 0x02554889}, {"W_BaseXDecide_00", -80, 0, 1, 1, 0x02554889}, + {"T_GuideX_00", 64, 0, 1, 1, 0x02554909}, + {"P_BaseY_00", Z, 0, -1, 1, 0x02555089}, {"P_A_01", 80, 0, 1, 1, 0x0255508b}, {"P_B_02", Z, -80, 1, 1, 0x0255508b}, + {"P_X_03", Z, 80, 1, 1, 0x0255508b}, {"N_BtnY_00", -80, 0, 1, 1, 0x0255500b}, + {"W_BaseY_00", 80, 0, -1, 1, 0x02554889}, {"W_BaseYDecide_00", 80, 0, -1, 1, 0x02554889}, + {"T_GuideY_00", -68, 0, 1, 1, 0x02555909}, + // left as they are: A's and L's bubbles, A's Decide null + {"W_BaseA_00", -80, 0, 1, 1, 0x02554889}, {"N_BaseADecide_00", -80, 0, 1, 1, 0x02555002}, + {"T_GuideL_00", 66, 0, 1, 1, 0x02554909}, +}; +enum { NPANES = sizeof panes / sizeof *panes }; +static uint64_t pane_at[NPANES]; +static const char *const materials[] = {"P_BaseA_00", "P_BaseB_00", "P_BaseX_00", "P_BaseY_00", "P_B_00"}; +static const char *const textures[][2] = {{"DoCommandCrossBase_A_00", "DoCommandCrossBaseGrd_A_00"}, + {"DoCommandCrossBase_B_00", "DoCommandCrossBaseGrd_B_00"}, + {"DoCommandCrossBase_B_00", "DoCommandCrossBaseGrd_B_00"}, + {"DoCommandCrossBase_A_00", "DoCommandCrossBaseGrd_A_00"}, + {"DoCommandSubBase_00", "DoCommandSubBaseGrd_00"}}; +enum { NMATS = sizeof materials / sizeof *materials }; +static uint64_t binding_at[NMATS][2]; // where each material's first two texture name offsets are +static uint64_t name_a[2], name_b[2]; // the offsets of the A and B pictures' names + +static size_t make_layout(void) { + memset(file, 0, sizeof file); + top = 0x240; + memcpy(file, "BFFH", 4); + // the names first, A's and B's pictures shared as the game shares them + name_a[0] = str(textures[0][0]), name_a[1] = str(textures[0][1]); + name_b[0] = str(textures[1][0]), name_b[1] = str(textures[1][1]); + // panes + uint64_t names = alloc(8 * NPANES), records = alloc(8 * NPANES); + for (int i = 0; i < NPANES; i++) { + uint64_t r = alloc(PANE_REC); + for (int k = 0; k < PANE_REC; k += 4) w32(r + k, 0xa5a50000u + (uint32_t)(i << 8) + (uint32_t)k); + wf(r + 0x10, panes[i].sx), wf(r + 0x14, panes[i].sy), wf(r + 0x1c, panes[i].x), wf(r + 0x20, panes[i].y); + w32(r + 0x38, panes[i].flags); + w64(names + 8 * i, str(panes[i].name)), w64(records + 8 * i, r); + pane_at[i] = r; + } + w64(0x118, names), w64(0x120, records), w32(0x130, NPANES), w32(0x134, 8); + // materials and their texture blocks + uint64_t mnames = alloc(8 * NMATS), mrecords = alloc(8 * NMATS), blocks = alloc(8 * NMATS); + for (int i = 0; i < NMATS; i++) { + uint64_t m = alloc(0x40), block = alloc(0x40), bindings = alloc(3 * 8); + w32(m + 0x14, (uint32_t)i); + w64(block, bindings), w32(block + 8, 3), w32(block + 12, 8); + for (int k = 0; k < 3; k++) { + uint64_t b = alloc(16); + w32(b, 0x01010202); + uint64_t s = k == 2 ? str("DecoNoise_00") + : i < 4 ? ((i == 0 || i == 3) ? name_a[k] : name_b[k]) + : str(textures[i][k]); + w64(b + 8, s); + w64(bindings + 8 * k, b); + if (k < 2) binding_at[i][k] = b + 8; + } + w64(mnames + 8 * i, str(materials[i])), w64(mrecords + 8 * i, m), w64(blocks + 8 * i, block); + } + w64(0x138, mnames), w64(0x140, mrecords), w32(0x150, NMATS), w32(0x154, 8); + w64(0x228, blocks), w32(0x230, NMATS), w32(0x234, 8); + return top; +} + +static int pane(const char *name) { + for (int i = 0; i < NPANES; i++) + if (!strcmp(panes[i].name, name)) return i; + printf("no pane %s in the test's table\n", name); + exit(2); +} +static float px(const uint8_t *p, const char *name, int off) { return rf(p, pane_at[pane(name)] + off); } + +// 1 when a differing byte at `at` is one the layout by label may change. +static int may_change(uint64_t at) { + static const char *const moved[] = {"P_B_00", "P_X_00", "P_Y_00", "N_BtnA_00", "P_A_00", "P_X_02", "P_Y_02", + "N_BtnB_00", "P_A_02", "P_B_03", "P_Y_04", "N_BtnX_00", "P_A_01", "P_B_02", + "P_X_03", "N_BtnY_00", "P_BaseX_00", "P_BaseY_00", "W_BaseX_00", + "W_BaseXDecide_00", "T_GuideX_00", "W_BaseY_00", "W_BaseYDecide_00", + "T_GuideY_00"}; + for (size_t i = 0; i < sizeof moved / sizeof *moved; i++) { + uint64_t r = pane_at[pane(moved[i])]; + if ((at >= r + 0x10 && at < r + 0x18) || (at >= r + 0x1c && at < r + 0x24) || (at >= r + 0x38 && at < r + 0x3c)) + return 1; + } + for (int i = 0; i < 4; i++) + for (int k = 0; k < 2; k++) + if (at >= binding_at[i][k] && at < binding_at[i][k] + 8) return 1; + return 0; +} + +// ---- a SARC: one other member, then the given layouts in order, each named `name` +enum { MAX_COPIES = 17 }; +static size_t member_at[MAX_COPIES]; // where each layout copy starts in the last archive made + +static size_t make_sarc(uint8_t *out, size_t cap, const uint8_t *const *layouts, int copies, size_t n, + const char *name) { + static const char other[] = "Other_00.buil"; + static const uint8_t other_data[64] = {1, 2, 3, 4, 5, 6, 7, 8}; + int count = 1 + copies; + size_t name_len = (strlen(name) + 1 + 3) & ~(size_t)3, other_len = (sizeof other + 3) & ~(size_t)3; + size_t sfat = 0x14, sfnt = sfat + 0xc + 16 * (size_t)count; + size_t data = (sfnt + 8 + other_len + name_len * copies + 0xff) & ~(size_t)0xff; + size_t total = (data + sizeof other_data + 0xff) & ~(size_t)0xff; + for (int c = 0; c < copies; c++) member_at[c] = total, total = (total + n + 0xff) & ~(size_t)0xff; + if (total > cap) return 0; + memset(out, 0, total); + memcpy(out, "SARC", 4); + uint16_t hl = 0x14, bom = 0xfeff, v = 0x100, fl = 0xc, cnt = (uint16_t)count, eight = 8; + uint32_t t32 = (uint32_t)total, d32 = (uint32_t)data, key = 0x65; + memcpy(out + 4, &hl, 2), memcpy(out + 6, &bom, 2); + memcpy(out + 8, &t32, 4), memcpy(out + 12, &d32, 4), memcpy(out + 16, &v, 2); + memcpy(out + sfat, "SFAT", 4), memcpy(out + sfat + 4, &fl, 2), memcpy(out + sfat + 6, &cnt, 2); + memcpy(out + sfat + 8, &key, 4); + memcpy(out + sfnt, "SFNT", 4), memcpy(out + sfnt + 4, &eight, 2); + size_t names = sfnt + 8; + memcpy(out + names, other, sizeof other); + uint32_t node[4] = {1, 0x01000000u, 0, (uint32_t)sizeof other_data}; + memcpy(out + sfat + 0xc, node, sizeof node); + memcpy(out + data, other_data, sizeof other_data); + for (int c = 0; c < copies; c++) { + size_t s = names + other_len + name_len * c; + memcpy(out + s, name, strlen(name) + 1); + uint32_t e[4] = {2, 0x01000000u | (uint32_t)((s - names) / 4), (uint32_t)(member_at[c] - data), + (uint32_t)(member_at[c] - data + n)}; + memcpy(out + sfat + 0xc + 16 * (size_t)(c + 1), e, sizeof e); + memcpy(out + member_at[c], layouts[c], n); + } + return total; +} + +// Points copy c of an archive make_sarc made (its SFAT entry c + 1) at s[at..at + size). +static void set_range(uint8_t *s, int c, size_t at, size_t size) { + uint32_t data, lo, hi; + memcpy(&data, s + 12, 4); + lo = (uint32_t)(at - data), hi = (uint32_t)(at + size - data); + memcpy(s + 0x20 + 16 * (size_t)(c + 1) + 8, &lo, 4); + memcpy(s + 0x20 + 16 * (size_t)(c + 1) + 12, &hi, 4); +} + +// A zstd dictionary with the id `id`, its content and samples taken from src[0..n); its size, 0 when it cannot be made. +static size_t make_dict(uint8_t *out, size_t cap, unsigned id, const uint8_t *src, size_t n) { + enum { SAMPLES = 32 }; + size_t sizes[SAMPLES]; + for (int i = 0; i < SAMPLES; i++) sizes[i] = n / SAMPLES; + ZDICT_params_t params = {3, 0, id}; + size_t r = ZDICT_finalizeDictionary(out, cap, src, n < 2048 ? n : 2048, src, sizes, SAMPLES, params); + return ZDICT_isError(r) ? 0 : r; +} + +// ---- the game's own archive, read from romfs.bin +static uint8_t *read_part(const char *path, const char *off, const char *size, size_t *n) { + long long at = strtoll(off, NULL, 10); + *n = (size_t)strtoull(size, NULL, 10); + FILE *f = fopen(path, "rb"); + uint8_t *p = f && *n ? malloc(*n) : NULL; +#ifdef _WIN32 + int ok = p && !_fseeki64(f, at, SEEK_SET) && fread(p, 1, *n, f) == *n; +#else + int ok = p && !fseeko(f, (off_t)at, SEEK_SET) && fread(p, 1, *n, f) == *n; +#endif + if (f) fclose(f); + if (!ok) free(p); + return ok ? p : NULL; +} + +// The .zs file z[0..zn) unpacked with the dictionary it names from the unpacked dictionaries dics[0..dn); NULL when it +// does not unpack. +static uint8_t *unpack(const void *z, size_t zn, const uint8_t *dics, size_t dn, size_t *n) { + unsigned id = ZSTD_getDictID_fromFrame(z, zn); + unsigned long long rn = ZSTD_getFrameContentSize(z, zn); + size_t at = 0, size = 0; + if (rn == ZSTD_CONTENTSIZE_UNKNOWN || rn == ZSTD_CONTENTSIZE_ERROR || (id && !zsdic_find(dics, dn, id, &at, &size))) + return NULL; + uint8_t *raw = malloc((size_t)rn); + ZSTD_DCtx *d = ZSTD_createDCtx(); + size_t got = !raw || !d ? 0 + : id ? ZSTD_decompress_usingDict(d, raw, (size_t)rn, z, zn, dics + at, size) + : ZSTD_decompressDCtx(d, raw, (size_t)rn, z, zn); + ZSTD_freeDCtx(d); + if (ZSTD_isError(got) || got != rn) { + free(raw); + return NULL; + } + *n = (size_t)rn; + return raw; +} + +// By label: made once, with the game's dictionary and unpacked size, and byte for byte the archive laid out by label, +// which differs from the game's only inside its four copies of the layout; then nothing more to make by label, nothing +// to make by position on the game's own, and by position again on the one by label. +static void game_archive(char **argv) { + size_t an, dn, pn = 0, rn = 0, on = 0, bn = 0; + uint8_t *a = read_part(argv[1], argv[2], argv[3], &an), *d = read_part(argv[1], argv[4], argv[5], &dn); + uint8_t *dics = d ? zsdic_unpack(d, dn, 64u << 20, &pn) : NULL; + uint8_t *raw = a && dics ? unpack(a, an, dics, pn, &rn) : NULL; + CHECK(raw != NULL, "game: cannot read and unpack the layout archive from %s", argv[1]); + if (!raw) { + free(dics), free(a), free(d); + return; + } + size_t at[16], size[16]; + unsigned next = 0; + int copies = 0; + while (copies < 16 && sarc_next(raw, rn, "DoCommand_00.buil", &next, &at[copies], &size[copies]) == 1) copies++; + char why[200] = ""; + uint8_t *expect = malloc(rn), *expect_back = malloc(rn); + memcpy(expect, raw, rn); + int fields = action_prompt_archive(expect, rn, 1, why, sizeof why); + CHECK(copies == 4 && fields == 4 * 52, "game: %d copies of the layout, %d fields changed (%s)", copies, fields, why); + memcpy(expect_back, expect, rn); + action_prompt_archive(expect_back, rn, 0, why, sizeof why); + + void *out = NULL, *again = NULL; + size_t out_n = 0, again_n = 0; + int made = action_prompt_build(a, an, d, dn, 1, &out, &out_n); + CHECK(made == 1, "game: build by label %d (%s)", made, last_log); + if (made == 1) { + printf("game: %s, %zu bytes packed (the game's %zu)\n", last_log, out_n, an); + CHECK(ZSTD_getDictID_fromFrame(out, out_n) == ZSTD_getDictID_fromFrame(a, an) && + ZSTD_getFrameContentSize(out, out_n) == rn, + "game: the built frame names another dictionary or size"); + uint8_t *got = unpack(out, out_n, dics, pn, &on); + CHECK(got && on == rn && !memcmp(got, expect, rn), "game: the built archive is not the archive laid out by label"); + size_t outside = 0; + for (size_t i = 0; got && on == rn && i < rn; i++) { + if (got[i] == raw[i]) continue; + int inside = 0; + for (int c = 0; c < copies; c++) inside |= i >= at[c] && i < at[c] + size[c]; + outside += !inside; + } + CHECK(!outside, "game: %zu bytes changed outside the layout's copies", outside); + CHECK(action_prompt_build(out, out_n, d, dn, 1, &again, &again_n) == 0 && !again, "game: second build: %s", + last_log); + made = action_prompt_build(out, out_n, d, dn, 0, &again, &again_n); + uint8_t *back = made == 1 ? unpack(again, again_n, dics, pn, &bn) : NULL; + CHECK(back && bn == rn && !memcmp(back, expect_back, rn), "game: by position again: %d (%s)", made, last_log); + free(back); + free(again); + free(got); + } + again = NULL; + CHECK(action_prompt_build(a, an, d, dn, 0, &again, &again_n) == 0 && !again, + "game: build by position on the game's archive: %s", last_log); + free(out); + free(expect); + free(expect_back); + free(raw); + free(dics); + free(a); + free(d); +} + +int main(int argc, char **argv) { + char why[200]; + size_t n = make_layout(); + static uint8_t before[SIZE], p[SIZE]; + memcpy(before, file, n); + + // By label: every place, cross, flip and bubble the game's diamond needs, and nothing else. + memcpy(p, before, n); + int changed = action_prompt_lay_out(p, n, 1, why, sizeof why); + CHECK(changed == 52, "fields changed by label: %d (%s)", changed, changed < 0 ? why : ""); + static const struct { const char *pane; float x, y; } places[] = { + {"N_BtnA_00", 0, -80}, {"N_BtnB_00", 80, 0}, {"N_BtnX_00", -80, 0}, {"N_BtnY_00", 0, 80}, + {"P_B_00", 80, 0}, {"P_X_00", -80, 0}, {"P_Y_00", 0, 80}, {"P_A_00", 0, -80}, + {"P_X_02", -80, 0}, {"P_Y_02", 0, 80}, {"P_A_02", 0, -80}, {"P_B_03", 80, 0}, + {"P_Y_04", 0, 80}, {"P_A_01", 0, -80}, {"P_B_02", 80, 0}, {"P_X_03", -80, 0}}; + for (size_t i = 0; i < sizeof places / sizeof *places; i++) + CHECK(px(p, places[i].pane, 0x1c) == places[i].x && px(p, places[i].pane, 0x20) == places[i].y, + "%s at %g %g, expected %g %g", places[i].pane, px(p, places[i].pane, 0x1c), px(p, places[i].pane, 0x20), + places[i].x, places[i].y); + // The crosses: A lit down (picture B), B right (A), X left (A flipped in x), Y up (B flipped in y). + CHECK(r64(p, binding_at[0][0]) == name_b[0] && r64(p, binding_at[0][1]) == name_b[1], "A's cross is not picture B"); + CHECK(r64(p, binding_at[1][0]) == name_a[0] && r64(p, binding_at[1][1]) == name_a[1], "B's cross is not picture A"); + CHECK(r64(p, binding_at[2][0]) == name_a[0] && r64(p, binding_at[2][1]) == name_a[1], "X's cross is not picture A"); + CHECK(r64(p, binding_at[3][0]) == name_b[0] && r64(p, binding_at[3][1]) == name_b[1], "Y's cross is not picture B"); + CHECK(px(p, "P_BaseA_00", 0x10) == 1 && px(p, "P_BaseA_00", 0x14) == 1, "A's cross flipped"); + CHECK(px(p, "P_BaseX_00", 0x10) == -1 && px(p, "P_BaseX_00", 0x14) == 1, "X's cross not flipped in x only"); + CHECK(px(p, "P_BaseY_00", 0x10) == 1 && px(p, "P_BaseY_00", 0x14) == -1, "Y's cross not flipped in y only"); + // X's bubble opens to the left, Y's to the right; the text origin follows. + CHECK(px(p, "W_BaseX_00", 0x10) == -1 && px(p, "W_BaseX_00", 0x1c) == 80, "X's bubble"); + CHECK(px(p, "W_BaseXDecide_00", 0x10) == -1 && px(p, "W_BaseXDecide_00", 0x1c) == 80, "X's Decide bubble"); + CHECK(px(p, "T_GuideX_00", 0x1c) == -68 && r32(p, pane_at[pane("T_GuideX_00")] + 0x38) == 0x02555909, "X's name"); + CHECK(px(p, "W_BaseY_00", 0x10) == 1 && px(p, "W_BaseY_00", 0x1c) == -80, "Y's bubble"); + CHECK(px(p, "W_BaseYDecide_00", 0x10) == 1 && px(p, "W_BaseYDecide_00", 0x1c) == -80, "Y's Decide bubble"); + CHECK(px(p, "T_GuideY_00", 0x1c) == 64 && r32(p, pane_at[pane("T_GuideY_00")] + 0x38) == 0x02554909, "Y's name"); + int stray = 0; + for (size_t i = 0; i < n; i++) + if (p[i] != before[i] && !may_change(i)) stray++; + CHECK(!stray, "%d bytes changed outside the diamond's fields", stray); + + // Laid out by label already: nothing changes, and it says so. + uint8_t *again = malloc(n); + memcpy(again, p, n); + CHECK(action_prompt_lay_out(again, n, 1, why, sizeof why) == 0 && !memcmp(again, p, n), "a second run changed it"); + // And back by position: the game's file again, its stored -2.8e-14 zeros now plain zeros. + changed = action_prompt_lay_out(again, n, 0, why, sizeof why); + CHECK(changed == 52, "fields changed back by position: %d (%s)", changed, changed < 0 ? why : ""); + uint8_t *expect = malloc(n); + memcpy(expect, before, n); + for (int i = 0; i < NPANES; i++) + if (panes[i].x == Z && strncmp(panes[i].name, "P_Base", 6)) set_f(expect, pane_at[i] + 0x1c, 0.0f); + CHECK(!memcmp(again, expect, n), "by label and back is not the game's layout"); + + // A file that is not the expected one is refused before anything is written. + struct { const char *pane; int off; float v; } wrong[] = { + {"P_Y_04", 0x20, 1.0f}, {"N_BtnA_00", 0x20, -80.0f}, {"P_BaseY_00", 0x10, 1.0f}, {"T_GuideY_00", 0x1c, -60.0f}}; + for (size_t i = 0; i < sizeof wrong / sizeof *wrong; i++) { + memcpy(p, before, n); + memcpy(p + pane_at[pane(wrong[i].pane)] + wrong[i].off, &wrong[i].v, 4); + memcpy(again, p, n); + int r = action_prompt_lay_out(p, n, 1, why, sizeof why); + CHECK(r == -1 && !memcmp(p, again, n) && strstr(why, wrong[i].pane), "%s changed: %d, %s", wrong[i].pane, r, why); + } + // Half laid out (A moved, the rest not): refused, untouched. + memcpy(p, before, n); + set_f(p, pane_at[pane("N_BtnA_00")] + 0x1c, 0), set_f(p, pane_at[pane("N_BtnA_00")] + 0x20, -80); + memcpy(again, p, n); + CHECK(action_prompt_lay_out(p, n, 1, why, sizeof why) == -1 && !memcmp(p, again, n), "half laid out: %s", why); + // A cross with another picture, a missing pane, a missing material: refused. + memcpy(p, before, n); + memcpy(p + binding_at[0][0], &name_b[0], 8); + CHECK(action_prompt_lay_out(p, n, 1, why, sizeof why) == -1 && strstr(why, "P_BaseA_00"), "cross picture: %s", why); + memcpy(p, before, n); + uint64_t s = r64(p, r64(p, 0x118) + 8 * (uint64_t)pane("P_X_03")); + p[s] = 'Q'; + CHECK(action_prompt_lay_out(p, n, 1, why, sizeof why) == -1 && strstr(why, "P_X_03"), "missing pane: %s", why); + memcpy(p, before, n); + memset(p + 0x150, 0, 4); // no materials + CHECK(action_prompt_lay_out(p, n, 1, why, sizeof why) == -1 && strstr(why, "P_BaseA_00"), "no materials: %s", why); + + // Offsets out of the file, a cut file and a wrong magic: refused, never read past the end. + memcpy(p, before, n); + uint64_t far = (uint64_t)n + 100; + memcpy(p + 0x118, &far, 8); + CHECK(action_prompt_lay_out(p, n, 1, why, sizeof why) == -1, "pane names past the end"); + memcpy(p, before, n); + far = UINT64_MAX - 4; + memcpy(p + 0x120, &far, 8); + CHECK(action_prompt_lay_out(p, n, 1, why, sizeof why) == -1, "pane records at 2^64"); + memcpy(p, before, n); + CHECK(action_prompt_lay_out(p, 0x200, 1, why, sizeof why) == -1, "a cut file"); + CHECK(action_prompt_lay_out(p, 3, 1, why, sizeof why) == -1, "a 3-byte file"); + p[0] = 'X'; + CHECK(action_prompt_lay_out(p, n, 1, why, sizeof why) == -1 && strstr(why, "BFFH"), "wrong magic: %s", why); + // A label other than 1 counts as by label. + memcpy(p, before, n); + CHECK(action_prompt_lay_out(p, n, 2, why, sizeof why) == 52 && px(p, "N_BtnA_00", 0x20) == -80, "label 2: %s", why); + // Both bubbles of X on one record, their values the same in both layouts: laid out, the record moved once. + memcpy(p, before, n); + uint64_t shared = pane_at[pane("W_BaseX_00")]; + memcpy(p + r64(p, 0x120) + 8 * (uint64_t)pane("W_BaseXDecide_00"), &shared, 8); + changed = action_prompt_lay_out(p, n, 1, why, sizeof why); + CHECK(changed == 52 && px(p, "W_BaseX_00", 0x10) == -1 && px(p, "W_BaseX_00", 0x1c) == 80, + "bubbles on one record: %d (%s)", changed, why); + // P_B_00's record laid over P_BaseA_00's material, its x on the material's block index (0 as x is 0.0): the file + // reads as expected, but moving the dot would lose A's cross. Refused, and every byte as it was. + memcpy(p, before, n); + uint64_t mat = r64(p, r64(p, 0x140)), over = mat + 0x14 - 0x1c; + memcpy(p + r64(p, 0x120) + 8 * (uint64_t)pane("P_B_00"), &over, 8); + set_f(p, mat + 0x18, -80.0f); + memcpy(again, p, n); + changed = action_prompt_lay_out(p, n, 1, why, sizeof why); + CHECK(changed == -1 && !memcmp(p, again, n) && strstr(why, "overlaps"), "a pane on a material: %d, %s", changed, + why); + // Random damage, mostly to the header's tables: a refusal or a layout, never a crash. Each copy is an allocation + // of its own, so that AddressSanitizer, where the runner has it, sees a read past its end. + srand(1); + for (int t = 0; t < 20000; t++) { + uint8_t *q = malloc(n); + memcpy(q, before, n); + for (int k = 0; k < 1 + rand() % 4; k++) { + size_t at = rand() % 2 ? 0x110 + rand() % 0x130 : rand() % n; + q[at] ^= (uint8_t)(1 << rand() % 8); + } + action_prompt_lay_out(q, n, 1, why, sizeof why); + free(q); + } + + // The archive, holding the layout four times as the game's does: every copy changes, nothing else; one copy + // that is not the expected layout leaves all of them as they were; without the layout, refused. + static uint8_t sarc[SIZE * 20], sarc2[SIZE * 20]; + const uint8_t *four[MAX_COPIES] = {before, before, before, before}; + size_t sn = make_sarc(sarc, sizeof sarc, four, 4, n, "DoCommand_00.buil"); + memcpy(sarc2, sarc, sn); + changed = action_prompt_archive(sarc2, sn, 1, why, sizeof why); + memcpy(p, before, n); + action_prompt_lay_out(p, n, 1, why, sizeof why); + CHECK(changed == 4 * 52 && !memcmp(sarc2, sarc, member_at[0]), "archive: %d", changed); + for (int c = 0; c < 4; c++) CHECK(!memcmp(sarc2 + member_at[c], p, n), "copy %d not laid out by label", c); + // Two copies laid out already and two not (an archive half laid out by hand): the other two are laid out. + const uint8_t *mixed[MAX_COPIES] = {p, before, p, before}; + sn = make_sarc(sarc2, sizeof sarc2, mixed, 4, n, "DoCommand_00.buil"); + changed = action_prompt_archive(sarc2, sn, 1, why, sizeof why); + CHECK(changed == 2 * 52 && !memcmp(sarc2 + member_at[1], p, n) && !memcmp(sarc2 + member_at[3], p, n), + "archive half laid out: %d (%s)", changed, why); + memcpy(p, before, n); + set_f(p, pane_at[pane("P_Y_04")] + 0x20, 1.0f); + const uint8_t *one_off[MAX_COPIES] = {before, p, before, before}; + sn = make_sarc(sarc2, sizeof sarc2, one_off, 4, n, "DoCommand_00.buil"); + memcpy(sarc, sarc2, sn); + CHECK(action_prompt_archive(sarc2, sn, 1, why, sizeof why) == -1 && !memcmp(sarc, sarc2, sn) && + strstr(why, "P_Y_04"), + "archive with one copy not as expected: %s", why); + sn = make_sarc(sarc2, sizeof sarc2, four, 1, n, "DoCommand_01.buil"); + CHECK(action_prompt_archive(sarc2, sn, 1, why, sizeof why) == -1 && strstr(why, "DoCommand_00.buil"), + "archive without the layout: %s", why); + // More copies than it lays out: refused, not the first 16 laid out. + const uint8_t *many[MAX_COPIES]; + for (int c = 0; c < MAX_COPIES; c++) many[c] = before; + sn = make_sarc(sarc2, sizeof sarc2, many, MAX_COPIES, n, "DoCommand_00.buil"); + memcpy(sarc, sarc2, sn); + CHECK(sn && action_prompt_archive(sarc2, sn, 1, why, sizeof why) == -1 && !memcmp(sarc, sarc2, sn) && + strstr(why, "more than"), + "archive with %d copies: %s", MAX_COPIES, why); + // Copies that share their bytes are laid out once; copies that share some of them, refused. + sn = make_sarc(sarc2, sizeof sarc2, four, 4, n, "DoCommand_00.buil"); + set_range(sarc2, 1, member_at[0], n); + changed = action_prompt_archive(sarc2, sn, 1, why, sizeof why); + CHECK(changed == 3 * 52, "archive with two copies on the same bytes: %d (%s)", changed, why); + sn = make_sarc(sarc2, sizeof sarc2, four, 4, n, "DoCommand_00.buil"); + set_range(sarc2, 1, member_at[0] + 0x100, n); + memcpy(sarc, sarc2, sn); + CHECK(action_prompt_archive(sarc2, sn, 1, why, sizeof why) == -1 && !memcmp(sarc, sarc2, sn) && + strstr(why, "overlap"), + "archive with overlapping copies: %s", why); + // A copy that runs past the archive's end: damaged, refused. + sn = make_sarc(sarc2, sizeof sarc2, four, 4, n, "DoCommand_00.buil"); + set_range(sarc2, 2, member_at[2], sn); + memcpy(sarc, sarc2, sn); + CHECK(action_prompt_archive(sarc2, sn, 1, why, sizeof why) == -1 && !memcmp(sarc, sarc2, sn) && + strstr(why, "damaged"), + "archive with a copy past its end: %s", why); + sn = make_sarc(sarc, sizeof sarc, four, 4, n, "DoCommand_00.buil"); + + // The zstd file without a dictionary: made once, then nothing to make; a damaged frame refused. + size_t cap = ZSTD_compressBound(sn), zn; + uint8_t *z = malloc(cap); + zn = ZSTD_compress(z, cap, sarc, sn, 3); + uint8_t dic_frame[64]; + size_t dic_n = ZSTD_compress(dic_frame, sizeof dic_frame, "SARC", 4, 1); + void *out = NULL; + size_t out_n = 0; + int made = action_prompt_build(z, zn, dic_frame, dic_n, 1, &out, &out_n); + uint8_t *back = malloc(sn); + CHECK(made == 1 && out && ZSTD_decompress(back, sn, out, out_n) == sn, "build: %d (%s)", made, last_log); + memcpy(sarc2, sarc, sn); + action_prompt_archive(sarc2, sn, 1, why, sizeof why); + CHECK(made == 1 && !memcmp(back, sarc2, sn), "the built archive is not the archive laid out by label"); + void *out2 = NULL; + CHECK(action_prompt_build(out, out_n, dic_frame, dic_n, 1, &out2, &out_n) == 0 && !out2 && + strstr(last_log, "already"), + "build on a laid out archive: %s", last_log); + CHECK(action_prompt_build(z, zn / 2, dic_frame, dic_n, 1, &out2, &out_n) == -1 && !out2 && + strstr(last_log, "Switch places"), + "build on a cut frame: %s", last_log); + free(out); + + // The zstd file with a dictionary, as the game's: the pack of dictionaries holds two (ids 1234 and 77) beside a + // member that is not one; the archive's frame names 1234. Made, with that dictionary named again; a frame that + // names one the pack does not hold (4321), refused. + enum { DICT_CAP = 8192 }; + static uint8_t dicts[3][DICT_CAP]; + static const unsigned ids[3] = {1234, 77, 4321}; + for (int i = 0; i < 3; i++) + CHECK(make_dict(dicts[i], DICT_CAP, ids[i], sarc + member_at[0], n), "dictionary %u not made", ids[i]); + static uint8_t pack[SIZE * 4]; + const uint8_t *two[MAX_COPIES] = {dicts[0], dicts[1]}; + size_t pack_n = make_sarc(pack, sizeof pack, two, 2, DICT_CAP, "zs.zsdic"); + uint8_t *pack_z = malloc(ZSTD_compressBound(pack_n)); + size_t pack_zn = ZSTD_compress(pack_z, ZSTD_compressBound(pack_n), pack, pack_n, 3); + for (int i = 0; i < 3; i += 2) { + ZSTD_CDict *cd = ZSTD_createCDict(dicts[i], DICT_CAP, 3); + ZSTD_CCtx *cc = ZSTD_createCCtx(); + zn = ZSTD_compress_usingCDict(cc, z, cap, sarc, sn, cd); + ZSTD_freeCCtx(cc); + ZSTD_freeCDict(cd); + out = NULL; + made = action_prompt_build(z, zn, pack_z, pack_zn, 1, &out, &out_n); + if (i == 2) { + CHECK(made == -1 && !out && strstr(last_log, "no dictionary 4321"), "a frame of another dictionary: %s", + last_log); + break; + } + CHECK(made == 1 && ZSTD_getDictID_fromFrame(out, out_n) == 1234, "build with a dictionary: %d (%s)", made, + last_log); + ZSTD_DCtx *dc = ZSTD_createDCtx(); + size_t got = made == 1 ? ZSTD_decompress_usingDict(dc, back, sn, out, out_n, dicts[0], DICT_CAP) : 0; + ZSTD_freeDCtx(dc); + CHECK(got == sn && !memcmp(back, sarc2, sn), "the archive built with a dictionary is not the one laid out"); + free(out); + } + free(pack_z); + free(back); + free(z); + free(again); + free(expect); + + if (argc == 6) game_archive(argv); + else if (argc != 1) printf("usage: action_prompt_check [romfs.bin archive-offset size zsdic-offset size]\n"), fails++; + if (fails) printf("%d checks failed\n", fails); + else printf("action prompt: all checks passed\n"); + return fails != 0; +} diff --git a/tests/action_prompt_check.py b/tests/action_prompt_check.py new file mode 100644 index 00000000..3ee8a8d8 --- /dev/null +++ b/tests/action_prompt_check.py @@ -0,0 +1,66 @@ +"""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())