Files
fa94e36549 Store the data writes of mods' ExeFS patch files into main before the game runs
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>
2026-11-06 15:51:42 +01:00

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;
}