mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
Lay out the action prompt's face buttons as the pad layout has them.
The action prompt at the top right of the HUD draws the four face buttons as a diamond in the console's places: A right, B bottom, X top, Y left. Under pad_layout = xbox, playstation or auto the A action is the bottom button, yet the diamond kept A on the right, so the prompt pointed at the wrong button. At start the runtime now edits the prompt's layout in the shared interface archive for such a layout (the buttons, the dots and the lit crosses move, and the bubbles of X and Y flip) and serves the result from the profile folder; with nintendo, the default, nothing is changed. Every field is found and checked before anything is written, so a layout a mod changed is left as it is, and the archive is packed again with the game's own dictionary at the same unpacked size. One SARC reader and dictionary lookup now serve this, the camera pack and the menu's assets. When the layout changes, the menu's Restart counts the diamond. tests/action_prompt_check.py passes without game files and on the game's own archive (4 copies, 208 fields, 13 ms at start). Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
@@ -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 <folder with romfs.bin and index.json>` 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.
|
||||
|
||||
@@ -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 `<profile>/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:
|
||||
|
||||
406
runtime/action_prompt.c
Normal file
406
runtime/action_prompt.c
Normal file
@@ -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 <math.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <zstd.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
30
runtime/action_prompt.h
Normal file
30
runtime/action_prompt.h
Normal file
@@ -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 <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
// 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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
|
||||
#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 <profile>/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
|
||||
|
||||
71
runtime/sarc.c
Normal file
71
runtime/sarc.c
Normal file
@@ -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 <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <zstd.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
24
runtime/sarc.h
Normal file
24
runtime/sarc.h
Normal file
@@ -0,0 +1,24 @@
|
||||
// SARC archives and the zstd dictionaries of Pack/ZsDic.pack.zs (sarc.c).
|
||||
#ifndef SARC_H
|
||||
#define SARC_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
// 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
|
||||
@@ -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-<w>x<h>), which
|
||||
// the setup does not write: the shape is the reason then, not the setup.
|
||||
int off = romfs_prompts_off(want);
|
||||
|
||||
596
tests/action_prompt_check.c
Normal file
596
tests/action_prompt_check.c
Normal file
@@ -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 <math.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <zdict.h>
|
||||
#include <zstd.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
66
tests/action_prompt_check.py
Normal file
66
tests/action_prompt_check.py
Normal file
@@ -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())
|
||||
Reference in New Issue
Block a user