mirror of
https://github.com/valentderah/totk-pc-recomp.git
synced 2026-11-11 18:01:03 +00:00
An IPS, IPS32 or pchtxt file for main that writes main's .rodata, .data or .bss was refused as a whole. Such writes need no lifting: tools/exefs_patches.py now puts them in build-cache/exefs.data, and runtime/mods.c stores them into the loaded main before any game code runs, as emulators and Atmosphere apply a patch file to the module before its dynamic linker runs. A DT_RELR word gets main's base added; bytes over words an explicit-addend relocation fills change nothing and are dropped; a write into the dynamic linker's tables, a lazily bound function pointer or outside main's image keeps the file unsupported. A mod whose data lines do not all fit the build keeps its code patches off too. The mods are applied from the bottom one up (mods_exefs_apply), so where two mods write the same bytes the higher one wins, as it does for files. --probe-mods checks it with two mods writing one byte. The lifted code reads some of main's bytes at lift time (the jump tables). recomp/gen.py lists those ranges in <gen>/main/folded.txt, and the setup checks the data writes against them after the lift: a file that writes them becomes unsupported, since the change would not reach the generated code. Generated C without mods is unchanged. This is the data part of #790 (ericdallo's ExeFS work), split out on its own; the plugin part follows separately. Co-Authored-By: itaskaev2 <165683571+itaskaev2@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
661 lines
27 KiB
C
661 lines
27 KiB
C
// Player mods: the scan of the mods folder, its order and switches (mods.h).
|
|
//
|
|
// One subfolder per mod, as emulators take them. The RomFS root is the first of <mod>/romfs, <mod>/romfslite (TKMM's
|
|
// name for it when it bypasses Atmosphere's RomFS build), <mod>/atmosphere/contents/<title>/romfs,
|
|
// <mod>/contents/<title>/romfs and <mod>/<title>/romfs that exists; the ExeFS and cheats folders likewise. A mod is
|
|
// listed whatever it holds; what the port cannot use is its problem, shown in the menu and logged, and never stops the
|
|
// start. romfs.c serves the RomFS files of the enabled mods, the top one winning a path.
|
|
//
|
|
// The order and the switches are totk.ini keys (cfg.c): TOTK_MODS_ORDER lists names top first, separated by '|' (a
|
|
// Windows file name cannot hold '|'; a folder whose name does on Linux is listed as unusable); a folder it does not name
|
|
// goes below the named ones, by name. TOTK_MODS_DISABLED lists the switched-off names. The menu changes the list in
|
|
// place and writes both keys; the scan's state is kept to tell the menu a restart is due.
|
|
//
|
|
// ExeFS patch files: an .ips or .pchtxt file in the exefs folder or in atmosphere/exefs_patches/<name>/ is for the game
|
|
// when its build id is main's (an IPS file's name, a pchtxt file's @nsobid line); others are another version's and
|
|
// counted. The setup builds every mod's files into patch sets and writes build-cache/exefs.status next to totk.exe
|
|
// (tools/exefs_patches.py has the format): a file whose line is missing or has another content hash waits for the
|
|
// rebuild at start (cheats.c), and its mod stays off until then. A file's writes into main's data (.rodata, .data,
|
|
// .bss) are lines of build-cache/exefs.data, stored into the loaded main before the game runs (mods_exefs_data), as
|
|
// emulators apply a patch file to the module before its dynamic linker runs.
|
|
#include <ctype.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <strings.h>
|
|
|
|
#include "cfg.h"
|
|
#include "host.h"
|
|
#include "mods.h"
|
|
#include "platform/platform.h"
|
|
|
|
#define TITLE "0100F2C0115B6000"
|
|
#define MAX_MODS 512
|
|
|
|
static ModInfo *mods;
|
|
static int nmods;
|
|
static char dir[2048];
|
|
static char start_order[MAX_MODS][256]; // names in the scan's order and their switches, for mods_changed
|
|
static int start_enabled[MAX_MODS];
|
|
|
|
// 1 when a file lies anywhere under path.
|
|
static int has_file_cb(const char *name, int kind, uint64_t size, void *ctx);
|
|
static int has_file(const char *path) {
|
|
struct { const char *path; int found; } c = {path, 0};
|
|
plat_fs_list(path, has_file_cb, &c);
|
|
return c.found;
|
|
}
|
|
static int has_file_cb(const char *name, int kind, uint64_t size, void *ctx) {
|
|
(void)size;
|
|
struct { const char *path; int found; } *c = ctx;
|
|
if (kind == PLAT_FS_FILE) {
|
|
c->found = 1;
|
|
} else if (kind == PLAT_FS_DIR) {
|
|
char sub[2100];
|
|
if (snprintf(sub, sizeof sub, "%s/%s", c->path, name) < (int)sizeof sub) c->found = has_file(sub);
|
|
}
|
|
return !c->found;
|
|
}
|
|
|
|
// The first of the emulator and Atmosphere places of <mod>/<part> that is a folder, into out ("" none).
|
|
static void find_root(const char *mod, const char *part, char *out, size_t cap) {
|
|
static const char *const forms[] = {"%s/%s", "%s/atmosphere/contents/" TITLE "/%s", "%s/contents/" TITLE "/%s",
|
|
"%s/" TITLE "/%s"};
|
|
for (size_t i = 0; i < sizeof forms / sizeof *forms; i++) {
|
|
if (snprintf(out, cap, forms[i], mod, part) < (int)cap && plat_dir_exists(out)) return;
|
|
}
|
|
out[0] = 0;
|
|
}
|
|
|
|
static int ends_with_ci(const char *s, const char *tail) {
|
|
size_t n = strlen(s), t = strlen(tail);
|
|
return n >= t && !strcasecmp(s + n - t, tail);
|
|
}
|
|
|
|
// What the ExeFS folder holds: MOD_EXEFS for patches (.ips, .pchtxt), MOD_SUBSDK for code modules (subsdk* plugins, a
|
|
// replaced main). main.npdm alone is the process's metadata, not code: it counts as neither.
|
|
static int exefs_cb(const char *name, int kind, uint64_t size, void *ctx) {
|
|
(void)size;
|
|
int *k = ctx;
|
|
if (kind != PLAT_FS_FILE) return 1;
|
|
if (ends_with_ci(name, ".ips") || ends_with_ci(name, ".pchtxt")) *k |= MOD_EXEFS;
|
|
else if (!strncasecmp(name, "subsdk", 6) || !strcasecmp(name, "main")) *k |= MOD_SUBSDK;
|
|
return 1;
|
|
}
|
|
|
|
// MOD_CHEATS when the cheats folder holds an Atmosphere cheat file (<build id>.txt).
|
|
static int cheats_cb(const char *name, int kind, uint64_t size, void *ctx) {
|
|
(void)size;
|
|
if (kind == PLAT_FS_FILE && ends_with_ci(name, ".txt")) *(int *)ctx |= MOD_CHEATS;
|
|
return 1;
|
|
}
|
|
|
|
// 1 when name is one of the '|'-separated names of list.
|
|
static int listed(const char *list, const char *name) {
|
|
size_t n = strlen(name);
|
|
for (const char *p = list; p && *p;) {
|
|
const char *bar = strchr(p, '|');
|
|
size_t len = bar ? (size_t)(bar - p) : strlen(p);
|
|
if (len == n && !memcmp(p, name, n)) return 1;
|
|
p = bar ? bar + 1 : NULL;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// The position of name in the '|'-separated list, or -1.
|
|
static int list_pos(const char *list, const char *name) {
|
|
size_t n = strlen(name);
|
|
int k = 0;
|
|
for (const char *p = list; p && *p; k++) {
|
|
const char *bar = strchr(p, '|');
|
|
size_t len = bar ? (size_t)(bar - p) : strlen(p);
|
|
if (len == n && !memcmp(p, name, n)) return k;
|
|
p = bar ? bar + 1 : NULL;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
static const char *order_list; // TOTK_MODS_ORDER while sorting
|
|
static int by_order(const void *a, const void *b) {
|
|
const ModInfo *x = a, *y = b;
|
|
int px = list_pos(order_list, x->name), py = list_pos(order_list, y->name);
|
|
if (px >= 0 && py >= 0) return px - py;
|
|
if (px >= 0 || py >= 0) return px >= 0 ? -1 : 1;
|
|
return strcmp(x->name, y->name);
|
|
}
|
|
|
|
static const char *problem_name(ModProblem p) {
|
|
switch (p) {
|
|
case MOD_EMPTY: return "empty";
|
|
case MOD_PLUGIN: return "has a code module (subsdk or main), not supported; the whole mod stays off";
|
|
case MOD_EXEFS_LATER: return "ExeFS patches wait for a rebuild of the game code; the whole mod stays off";
|
|
case MOD_EXEFS_UNSUPPORTED: return "an ExeFS patch file cannot be applied; the whole mod stays off";
|
|
case MOD_EXEFS_OTHER: return "its ExeFS patches are for another game version";
|
|
case MOD_BAD_NAME: return "its folder name holds '|', which the order list uses; rename the folder";
|
|
default: return "ok";
|
|
}
|
|
}
|
|
|
|
// ---- ExeFS patch files
|
|
|
|
static uint32_t fnv(const void *p, size_t n) {
|
|
uint32_t h = 0x811C9DC5u;
|
|
for (size_t i = 0; i < n; i++) h = (h ^ ((const uint8_t *)p)[i]) * 0x01000193u;
|
|
return h;
|
|
}
|
|
|
|
const char *mods_main_build_id(void) {
|
|
static char bid[65];
|
|
static int read;
|
|
if (read) return bid;
|
|
read = 1;
|
|
char path[2100];
|
|
snprintf(path, sizeof path, "%s/main", rt_config()->exefs);
|
|
PlatFile *f = plat_file_open(path);
|
|
uint8_t head[0x60];
|
|
int ok = f && plat_file_pread(f, head, sizeof head, 0) == sizeof head && !memcmp(head, "NSO0", 4);
|
|
if (f) plat_file_close(f);
|
|
for (int i = 0; ok && i < 32; i++) snprintf(bid + 2 * i, 3, "%02x", head[0x40 + i]);
|
|
return bid;
|
|
}
|
|
|
|
// 1 when hex (any case; trailing zeros left out or not) names main's build id; also when main's is unknown.
|
|
static int same_build(const char *hex, size_t n) {
|
|
const char *bid = mods_main_build_id();
|
|
if (!*bid) return 1;
|
|
if (!n || n > 64) return 0;
|
|
for (size_t i = 0; i < 64; i++) {
|
|
char c = i < n ? hex[i] : '0';
|
|
if (c >= 'A' && c <= 'F') c = (char)(c - 'A' + 'a');
|
|
if (c != bid[i]) return 0;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
// build-cache/<name> next to totk.exe, read once into *text; "" when there is none (mods_probe_cache sets the text).
|
|
static char *status_text, *data_text;
|
|
static const char *build_cache(const char *name, char **text) {
|
|
if (*text) return *text;
|
|
char path[2100];
|
|
uint64_t n = 0;
|
|
if (plat_exe_path(path, sizeof path)) {
|
|
char *cut = strrchr(path, '/'), *back = strrchr(path, '\\');
|
|
if (back > cut) cut = back;
|
|
if (cut && snprintf(cut + 1, sizeof path - (size_t)(cut + 1 - path), "build-cache/%s", name) <
|
|
(int)(sizeof path - (size_t)(cut + 1 - path)))
|
|
*text = (char *)plat_file_read(path, &n);
|
|
}
|
|
if (!*text) *text = calloc(1, 1);
|
|
return *text;
|
|
}
|
|
|
|
static const char *exefs_status(void) { return build_cache("exefs.status", &status_text); }
|
|
static const char *exefs_data(void) { return build_cache("exefs.data", &data_text); }
|
|
|
|
// The status line of key: its kind, built-in sets and note, NUL-terminated copies; 0 when none.
|
|
static int status_line(const char *key, const char *content, char *kind, char *over, char *note) {
|
|
char want[20];
|
|
snprintf(want, sizeof want, "%s\t%s\t", key, content);
|
|
for (const char *line = exefs_status(); *line;) {
|
|
const char *end = strchr(line, '\n');
|
|
size_t len = end ? (size_t)(end - line) : strlen(line);
|
|
if (len > 18 && !memcmp(line, want, 18)) {
|
|
char copy[600]; // the line after the two hashes, without the next line
|
|
size_t rest = len - 18;
|
|
snprintf(copy, sizeof copy, "%.*s", (int)(rest < sizeof copy ? rest : sizeof copy - 1), line + 18);
|
|
char *t1 = strchr(copy, '\t'), *t2 = t1 ? strchr(t1 + 1, '\t') : NULL;
|
|
if (!t1 || !t2) return 0;
|
|
*t1 = *t2 = 0;
|
|
char *r = strchr(t2 + 1, '\r');
|
|
if (r) *r = 0;
|
|
snprintf(kind, 16, "%s", copy);
|
|
snprintf(over, 256, "%s", strcmp(t1 + 1, "-") ? t1 + 1 : "");
|
|
snprintf(note, 256, "%s", t2 + 1);
|
|
return 1;
|
|
}
|
|
line = end ? end + 1 : line + len;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
typedef struct {
|
|
ModInfo *m;
|
|
const char *folder, *rel; // the folder listed and its path in the mod
|
|
int unsupported;
|
|
} ExefsWalk;
|
|
|
|
// One file of an exefs folder: its build, then its status line.
|
|
static int exefs_file_cb(const char *leaf, int kind, uint64_t size, void *ctx) {
|
|
(void)size;
|
|
ExefsWalk *w = ctx;
|
|
ModInfo *m = w->m;
|
|
int ips = ends_with_ci(leaf, ".ips");
|
|
if (kind != PLAT_FS_FILE || (!ips && !ends_with_ci(leaf, ".pchtxt"))) return 1;
|
|
m->kind |= MOD_EXEFS;
|
|
char path[4300], rel[600];
|
|
snprintf(path, sizeof path, "%s/%s", w->folder, leaf);
|
|
snprintf(rel, sizeof rel, "%s/%s/%s", m->name, w->rel, leaf);
|
|
uint64_t n = 0;
|
|
char *data = (char *)plat_file_read(path, &n);
|
|
if (!data) {
|
|
rt_log("mods: %s: cannot read %s", m->name, path);
|
|
return 1;
|
|
}
|
|
int for_main;
|
|
if (ips) {
|
|
for_main = same_build(leaf, strlen(leaf) - 4);
|
|
} else {
|
|
char *id = NULL; // after "@nsobid-" at the start of a line
|
|
for (char *p = data; !id && p && data + n - p > 8;) {
|
|
if (!memcmp(p, "@nsobid-", 8)) id = p + 8;
|
|
p = memchr(p, '\n', (size_t)(data + n - p));
|
|
if (p) p++;
|
|
}
|
|
size_t len = 0;
|
|
while (id && id + len < data + n && isxdigit((unsigned char)id[len])) len++;
|
|
for_main = id && same_build(id, len);
|
|
}
|
|
char key[9], content[9], kind_s[16], over[256], note[256];
|
|
snprintf(key, sizeof key, "%08X", fnv(rel, strlen(rel)));
|
|
snprintf(content, sizeof content, "%08X", fnv(data, (size_t)n));
|
|
free(data);
|
|
if (!for_main) {
|
|
m->other_version++;
|
|
return 1;
|
|
}
|
|
if (!status_line(key, content, kind_s, over, note)) {
|
|
m->pending++;
|
|
m->pending_key = (m->pending_key ^ fnv(key, 8)) * 0x01000193u;
|
|
m->pending_key = (m->pending_key ^ fnv(content, 8)) * 0x01000193u;
|
|
return 1;
|
|
}
|
|
if (!strcmp(kind_s, "unsupported")) {
|
|
if (!w->unsupported) snprintf(m->note, sizeof m->note, "%s: %s", leaf, note);
|
|
w->unsupported = 1;
|
|
} else if (!strcmp(kind_s, "code") && m->nsets < (int)(sizeof m->sets / sizeof *m->sets)) {
|
|
m->sets[m->nsets++] = (uint32_t)strtoul(key, NULL, 16);
|
|
size_t k = strlen(m->overrides);
|
|
if (*over) snprintf(m->overrides + k, sizeof m->overrides - k, "%s%s", k ? "," : "", over);
|
|
} else if (!strcmp(kind_s, "data") && m->ndata < (int)(sizeof m->data / sizeof *m->data)) {
|
|
m->data[m->ndata++] = (uint32_t)strtoul(key, NULL, 16);
|
|
} else if (!strcmp(kind_s, "other")) {
|
|
m->other_version++;
|
|
}
|
|
unsigned long bytes; // a code or data file's data writes: exefs.data must hold them all (mods_exefs_data)
|
|
if ((!strcmp(kind_s, "code") || !strcmp(kind_s, "data")) && sscanf(note, "data %lu", &bytes) == 1)
|
|
m->data_bytes += (uint32_t)bytes;
|
|
return 1;
|
|
}
|
|
|
|
// atmosphere/exefs_patches/<name>/ of a mod: each <name> folder's files.
|
|
static int exefs_patches_cb(const char *leaf, int kind, uint64_t size, void *ctx) {
|
|
(void)size;
|
|
ExefsWalk *w = ctx;
|
|
if (kind != PLAT_FS_DIR) return 1;
|
|
char folder[4300], rel[600];
|
|
snprintf(folder, sizeof folder, "%s/%s", w->folder, leaf);
|
|
snprintf(rel, sizeof rel, "atmosphere/exefs_patches/%s", leaf);
|
|
ExefsWalk sub = {w->m, folder, rel, 0};
|
|
plat_fs_list(folder, exefs_file_cb, &sub);
|
|
w->unsupported |= sub.unsupported;
|
|
return 1;
|
|
}
|
|
|
|
// The ExeFS patch files of mod m (folder path): 1 when one cannot be applied.
|
|
static int exefs_check(ModInfo *m, const char *path) {
|
|
ExefsWalk w = {m, m->exefs, m->exefs + strlen(path) + 1, 0};
|
|
if (m->exefs[0]) plat_fs_list(m->exefs, exefs_file_cb, &w);
|
|
char patches[2200];
|
|
snprintf(patches, sizeof patches, "%s/atmosphere/exefs_patches", path);
|
|
ExefsWalk at = {m, patches, "", 0};
|
|
if (plat_dir_exists(patches)) plat_fs_list(patches, exefs_patches_cb, &at);
|
|
return w.unsupported || at.unsupported;
|
|
}
|
|
|
|
// One mod folder of the scan.
|
|
static int scan_cb(const char *name, int kind, uint64_t size, void *ctx) {
|
|
(void)size;
|
|
const char *disabled = ctx;
|
|
if (kind != PLAT_FS_DIR) return 1;
|
|
if (nmods == MAX_MODS) {
|
|
rt_log("mods: more than %d folders in %s; the rest are left out", MAX_MODS, dir);
|
|
return 0;
|
|
}
|
|
ModInfo *m = &mods[nmods];
|
|
memset(m, 0, sizeof *m);
|
|
char path[2100];
|
|
if (snprintf(m->name, sizeof m->name, "%s", name) >= (int)sizeof m->name ||
|
|
snprintf(path, sizeof path, "%s/%s", dir, name) >= (int)sizeof path) {
|
|
rt_log("mods: %s: name too long, left out", name);
|
|
return 1;
|
|
}
|
|
find_root(path, "romfs", m->romfs, sizeof m->romfs);
|
|
if (!m->romfs[0]) find_root(path, "romfslite", m->romfs, sizeof m->romfs);
|
|
find_root(path, "exefs", m->exefs, sizeof m->exefs);
|
|
find_root(path, "cheats", m->cheats, sizeof m->cheats);
|
|
if (m->romfs[0] && has_file(m->romfs)) m->kind |= MOD_ROMFS;
|
|
if (m->exefs[0]) plat_fs_list(m->exefs, exefs_cb, &m->kind);
|
|
if (m->cheats[0]) plat_fs_list(m->cheats, cheats_cb, &m->kind);
|
|
int unsupported = exefs_check(m, path);
|
|
// a mod whose code part the port cannot use is off as a whole: its RomFS files may expect that code
|
|
m->problem = strchr(m->name, '|') ? MOD_BAD_NAME
|
|
: m->kind & MOD_SUBSDK ? MOD_PLUGIN
|
|
: unsupported ? MOD_EXEFS_UNSUPPORTED
|
|
: m->pending ? MOD_EXEFS_LATER
|
|
: m->nsets || m->ndata || m->kind & (MOD_ROMFS | MOD_CHEATS) ? MOD_OK
|
|
: m->kind & MOD_EXEFS ? MOD_EXEFS_OTHER
|
|
: MOD_EMPTY;
|
|
m->enabled = !listed(disabled, m->name);
|
|
nmods++;
|
|
return 1;
|
|
}
|
|
|
|
// The mods folder into out: TOTK_MODS_DIR, else <dir of totk.exe>/mods. 0 when the exe's path is unknown.
|
|
static int mods_folder(char *out, size_t cap) {
|
|
const char *d = cfg_get("TOTK_MODS_DIR");
|
|
if (d && *d) return snprintf(out, cap, "%s", d) < (int)cap;
|
|
char exe[2048];
|
|
if (!plat_exe_path(exe, sizeof exe)) return 0;
|
|
char *cut = strrchr(exe, '/'), *back = strrchr(exe, '\\');
|
|
if (back > cut) cut = back;
|
|
if (!cut) return 0;
|
|
*cut = 0;
|
|
return snprintf(out, cap, "%s/mods", exe) < (int)cap;
|
|
}
|
|
|
|
typedef struct {
|
|
const char *folder, *disabled;
|
|
void (*cb)(const char *name, const char *romfs, void *ctx);
|
|
void *ctx;
|
|
} Peek;
|
|
|
|
static int peek_cb(const char *name, int kind, uint64_t size, void *ctx) {
|
|
(void)size;
|
|
const Peek *pk = ctx;
|
|
if (kind != PLAT_FS_DIR || listed(pk->disabled, name)) return 1;
|
|
char path[2100], romfs[2048];
|
|
if (snprintf(path, sizeof path, "%s/%s", pk->folder, name) >= (int)sizeof path) return 1;
|
|
find_root(path, "romfs", romfs, sizeof romfs);
|
|
if (!romfs[0]) find_root(path, "romfslite", romfs, sizeof romfs);
|
|
if (romfs[0]) pk->cb(name, romfs, pk->ctx);
|
|
return 1;
|
|
}
|
|
|
|
void mods_peek_romfs(void (*cb)(const char *name, const char *romfs, void *ctx), void *ctx) {
|
|
char folder[2048];
|
|
if (!mods_folder(folder, sizeof folder) || !plat_dir_exists(folder)) return;
|
|
Peek pk = {folder, cfg_get("TOTK_MODS_DISABLED"), cb, ctx};
|
|
plat_fs_list(folder, peek_cb, &pk);
|
|
}
|
|
|
|
static int scanned;
|
|
|
|
void mods_scan(void) {
|
|
if (scanned) return;
|
|
scanned = 1;
|
|
if (!mods_folder(dir, sizeof dir)) return;
|
|
if (!plat_dir_exists(dir)) {
|
|
rt_log("mods: no folder %s", dir);
|
|
return;
|
|
}
|
|
mods = calloc(MAX_MODS, sizeof *mods);
|
|
if (!mods) return;
|
|
plat_fs_list(dir, scan_cb, (void *)cfg_get("TOTK_MODS_DISABLED"));
|
|
order_list = cfg_get("TOTK_MODS_ORDER");
|
|
qsort(mods, nmods, sizeof *mods, by_order);
|
|
order_list = NULL;
|
|
for (int i = 0; i < nmods; i++) {
|
|
snprintf(start_order[i], sizeof start_order[i], "%s", mods[i].name);
|
|
start_enabled[i] = mods[i].enabled;
|
|
const ModInfo *m = &mods[i];
|
|
rt_log("mods: %s: %s,%s%s%s%s%s, %s%s%s", m->name, m->enabled ? "on" : "off", m->kind & MOD_ROMFS ? " romfs" : "",
|
|
m->kind & MOD_EXEFS ? " exefs" : "", m->kind & MOD_SUBSDK ? " subsdk" : "",
|
|
m->kind & MOD_CHEATS ? " cheats" : "", m->kind ? "" : " no files", problem_name(m->problem),
|
|
m->note[0] ? ": " : "", m->note);
|
|
}
|
|
rt_log("mods: %d folders in %s", nmods, dir);
|
|
}
|
|
|
|
// ---- ExeFS data writes (build-cache/exefs.data; tools/exefs_patches.py data_line has the format)
|
|
|
|
// 1 when key names one of m's usable ExeFS patch files: one with a patch set, or one with data writes only.
|
|
static int owns(const ModInfo *m, uint32_t key) {
|
|
for (int k = 0; k < m->nsets; k++)
|
|
if (m->sets[k] == key) return 1;
|
|
for (int k = 0; k < m->ndata; k++)
|
|
if (m->data[k] == key) return 1;
|
|
return 0;
|
|
}
|
|
|
|
static int hex_digit(char c) {
|
|
return c >= '0' && c <= '9' ? c - '0'
|
|
: c >= 'a' && c <= 'f' ? c - 'a' + 10
|
|
: c >= 'A' && c <= 'F' ? c - 'A' + 10
|
|
: -1;
|
|
}
|
|
|
|
// One line: "<file hash>\t<content hash>\t<offset in main>\t<bytes>[\tbase]", all hex. 0 when malformed.
|
|
typedef struct { char key[9], content[9]; uint64_t off; uint8_t bytes[256]; int n, base; } DataRun;
|
|
static int parse_run(const char *line, size_t len, DataRun *r) {
|
|
char copy[1100], *f[6];
|
|
if (len >= sizeof copy) return 0;
|
|
memcpy(copy, line, len);
|
|
copy[len] = 0;
|
|
int nf = 0;
|
|
for (char *p = copy; p && nf < 6; nf++) {
|
|
f[nf] = p;
|
|
p = strchr(p, '\t');
|
|
if (p) *p++ = 0;
|
|
}
|
|
if (nf < 4 || nf > 5 || strlen(f[0]) != 8 || strlen(f[1]) != 8) return 0;
|
|
memcpy(r->key, f[0], 9);
|
|
memcpy(r->content, f[1], 9);
|
|
char *end;
|
|
r->off = strtoull(f[2], &end, 16);
|
|
size_t hex = strlen(f[3]);
|
|
if (!*f[2] || *end || !hex || hex % 2 || hex / 2 > sizeof r->bytes) return 0;
|
|
r->n = (int)(hex / 2);
|
|
for (int i = 0; i < r->n; i++) {
|
|
int hi = hex_digit(f[3][2 * i]), lo = hex_digit(f[3][2 * i + 1]);
|
|
if (hi < 0 || lo < 0) return 0;
|
|
r->bytes[i] = (uint8_t)(hi << 4 | lo);
|
|
}
|
|
r->base = nf == 5;
|
|
return nf == 4 || (!strcmp(f[4], "base") && r->n == 8);
|
|
}
|
|
|
|
// Stores n bytes into the loaded main at a: a read-only page (.rodata, loader.c seal) is writable for its part of the
|
|
// copy only, as cheats.c wr_main does. 0 when a page is neither readable nor writable.
|
|
static int store_main(uint64_t a, const uint8_t *from, int n) {
|
|
while (n > 0) {
|
|
uint64_t page = a & ~0xFFFull, end;
|
|
int k = (int)(page + 0x1000 - a);
|
|
if (k > n) k = n;
|
|
int prot = plat_mem_state(page, page + 0x1000, &end);
|
|
if (prot != PLAT_PROT_READ && prot != PLAT_PROT_RW) return 0;
|
|
if (prot == PLAT_PROT_READ && !plat_mem_protect(page, 0x1000, PLAT_PROT_RW)) return 0;
|
|
memcpy((void *)(uintptr_t)a, from, (size_t)k);
|
|
if (prot == PLAT_PROT_READ) plat_mem_protect(page, 0x1000, PLAT_PROT_READ);
|
|
a += (uint64_t)k;
|
|
from += k;
|
|
n -= k;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
long mods_exefs_data(const ModInfo *m, uint64_t base, uint64_t size, uint64_t code_end,
|
|
int (*store)(uint64_t addr, const uint8_t *bytes, int n)) {
|
|
if (!store) store = store_main;
|
|
long total = 0, found = 0;
|
|
for (int pass = 0; pass < 2; pass++) { // pass 0 checks every line, pass 1 stores
|
|
// exefs.data must hold every byte the status lines count: one missing or cut short would leave the code
|
|
// patches on without their data
|
|
if (pass && found != (long)m->data_bytes) {
|
|
rt_log("mods: %s: build-cache/exefs.data holds %ld of the %u bytes its ExeFS patch files write", m->name,
|
|
found, m->data_bytes);
|
|
return -1;
|
|
}
|
|
for (const char *line = exefs_data(), *next; *line; line = next) {
|
|
const char *nl = strchr(line, '\n');
|
|
size_t len = nl ? (size_t)(nl - line) : strlen(line);
|
|
next = nl ? nl + 1 : line + len;
|
|
if (len && line[len - 1] == '\r') len--;
|
|
char key[9];
|
|
snprintf(key, sizeof key, "%.*s", (int)(len < 8 ? len : 8), line);
|
|
if (!len || *line == '#' || !owns(m, (uint32_t)strtoul(key, NULL, 16))) continue;
|
|
DataRun r;
|
|
char kind[16], over[256], note[256];
|
|
// a line whose file the status does not list as built (another content) or that leaves main's data: the
|
|
// two build-cache files and this build disagree
|
|
if (!parse_run(line, len, &r) || !status_line(r.key, r.content, kind, over, note) ||
|
|
(strcmp(kind, "code") && strcmp(kind, "data")) || r.off < code_end || r.off > size ||
|
|
(uint64_t)r.n > size - r.off) {
|
|
rt_log("mods: %s: a line of build-cache/exefs.data does not fit this build: %.*s", m->name, (int)len,
|
|
line);
|
|
return -1;
|
|
}
|
|
if (!pass) {
|
|
found += r.n;
|
|
continue;
|
|
}
|
|
if (r.base) { // a DT_RELR word holds its addend; the dynamic linker adds the module's base
|
|
uint64_t w;
|
|
memcpy(&w, r.bytes, 8);
|
|
w += base;
|
|
memcpy(r.bytes, &w, 8);
|
|
}
|
|
if (!store(base + r.off, r.bytes, r.n)) {
|
|
rt_log("mods: %s: cannot write main+%#llx", m->name, (unsigned long long)r.off);
|
|
return -1;
|
|
}
|
|
total += r.n;
|
|
}
|
|
}
|
|
return total;
|
|
}
|
|
|
|
static char *dup_text(const char *s) {
|
|
if (!s) return NULL;
|
|
size_t n = strlen(s) + 1;
|
|
char *d = malloc(n);
|
|
if (d) memcpy(d, s, n);
|
|
return d;
|
|
}
|
|
|
|
void mods_probe_cache(const char *status, const char *data) {
|
|
free(status_text);
|
|
free(data_text);
|
|
status_text = dup_text(status);
|
|
data_text = dup_text(data);
|
|
}
|
|
|
|
void mods_rescan(void) {
|
|
free(mods);
|
|
mods = NULL;
|
|
nmods = 0;
|
|
scanned = 0;
|
|
}
|
|
|
|
// Switches the patch set mod_<key> on; 0 when this build lacks it (logged).
|
|
static int set_on(const ModInfo *m, uint32_t key) {
|
|
char name[32];
|
|
snprintf(name, sizeof name, "mod_%08x", key);
|
|
int set = rt_patch_set(name);
|
|
if (set >= 0) rt_patch_switch(set, 1);
|
|
else rt_log("mods: %s: patch set %s is not in this build", m->name, name);
|
|
return set >= 0;
|
|
}
|
|
|
|
void mods_exefs_apply(uint64_t base, uint64_t size, uint64_t code_end,
|
|
int (*store)(uint64_t addr, const uint8_t *bytes, int n)) {
|
|
// bottom first: the top mod's data writes land last and win, as its RomFS files do. Patch sets are only switches
|
|
// (rt_patch_switch), so their order does not matter.
|
|
for (int i = nmods - 1; i >= 0; i--) {
|
|
const ModInfo *m = &mods[i];
|
|
if (!start_enabled[i] || m->problem != MOD_OK || !(m->nsets || m->ndata)) continue;
|
|
// the data first: a mod whose data writes cannot all be stored keeps its code patches off too, as half of a
|
|
// patch file is worse than none
|
|
long stored = size ? mods_exefs_data(m, base, size, code_end, store) : 0;
|
|
if (stored < 0) {
|
|
rt_log("mods: %s: its ExeFS patch files stay off: their data writes could not all be stored", m->name);
|
|
continue;
|
|
}
|
|
for (int k = 0; k < m->nsets; k++) set_on(m, m->sets[k]);
|
|
char over[256];
|
|
snprintf(over, sizeof over, "%s", m->overrides);
|
|
for (char *s = strtok(over, ","); s; s = strtok(NULL, ",")) {
|
|
int set = rt_patch_set(s);
|
|
if (set >= 0) rt_patch_switch(set, 0);
|
|
rt_log("mods: %s replaces the port's patch set %s; it stays off", m->name, s);
|
|
}
|
|
rt_log("mods: %s: %d ExeFS patch files on, %ld bytes of main's data stored", m->name, m->nsets + m->ndata,
|
|
stored);
|
|
}
|
|
}
|
|
|
|
void mods_exefs_switch(void) {
|
|
mods_scan();
|
|
const RtLoaded *main_mod = NULL;
|
|
for (int i = 0; ldr_module(i); i++)
|
|
if (!strcmp(ldr_module(i)->name, "main")) main_mod = ldr_module(i);
|
|
if (main_mod) mods_exefs_apply(main_mod->base, main_mod->size, main_mod->text_end, NULL);
|
|
else mods_exefs_apply(0, 0, 0, NULL);
|
|
}
|
|
|
|
int mods_exefs_pending(uint32_t *key) {
|
|
int n = 0;
|
|
for (int i = 0; i < nmods; i++) {
|
|
if (!start_enabled[i] || mods[i].problem != MOD_EXEFS_LATER) continue;
|
|
n += (int)mods[i].pending;
|
|
*key = (*key ^ mods[i].pending_key) * 0x01000193u;
|
|
}
|
|
return n;
|
|
}
|
|
|
|
const char *mods_dir(void) { return dir; }
|
|
int mods_count(void) { return nmods; }
|
|
ModInfo *mods_at(int i) { return i >= 0 && i < nmods ? &mods[i] : NULL; }
|
|
|
|
// TOTK_MODS_ORDER as every listed name top first; TOTK_MODS_DISABLED as the switched-off names. A name holding '|'
|
|
// would split into two; it is left out of both (its mod is unusable anyway).
|
|
static void write_keys(void) {
|
|
size_t cap = (size_t)nmods * 257 + 1;
|
|
char *order = calloc(cap, 1), *off = calloc(cap, 1);
|
|
if (order && off) {
|
|
for (int i = 0; i < nmods; i++) {
|
|
if (mods[i].problem == MOD_BAD_NAME) continue;
|
|
if (*order) strcat(order, "|");
|
|
strcat(order, mods[i].name);
|
|
if (!mods[i].enabled) {
|
|
if (*off) strcat(off, "|");
|
|
strcat(off, mods[i].name);
|
|
}
|
|
}
|
|
cfg_set("TOTK_MODS_ORDER", order);
|
|
cfg_set("TOTK_MODS_DISABLED", off);
|
|
}
|
|
free(order);
|
|
free(off);
|
|
}
|
|
|
|
void mods_set_enabled(int i, int on) {
|
|
if (i < 0 || i >= nmods) return;
|
|
mods[i].enabled = on != 0;
|
|
write_keys();
|
|
}
|
|
|
|
void mods_move(int i, int delta) {
|
|
int j = i + delta;
|
|
if (i < 0 || i >= nmods || j < 0 || j >= nmods || i == j) return;
|
|
ModInfo t = mods[i];
|
|
mods[i] = mods[j];
|
|
mods[j] = t;
|
|
write_keys();
|
|
}
|
|
|
|
int mods_changed(void) {
|
|
for (int i = 0; i < nmods; i++)
|
|
if (strcmp(mods[i].name, start_order[i]) || mods[i].enabled != start_enabled[i]) return 1;
|
|
return 0;
|
|
}
|