mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
- A pchtxt file's build id is read as tools/exefs_patches.py reads it: after a UTF-8 BOM and blanks, the last @nsobid line before @stop, a // comment after it left out. A file saved with a BOM or an indented line was taken for another game version although the setup had built it into the game. - A symbolic link or junction to a folder in mods/ is a mod, as the setup's tools see it; it was left out, so the patch sets built for it were never switched on. - A relative mods_dir is under the game's folder, not the folder the game was started from. - A mods folder that cannot be listed is logged instead of showing no mods without a word. - The start state of a mod's switch moves with the mod when the list is reordered, so the ExeFS patches waiting for the rebuild are counted for the right mods. --probe-mods checks each of these. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
487 lines
20 KiB
C
487 lines
20 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 (a mod
|
|
// manager'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 nx2navi.ini keys (cfg.c): NX2NAVI_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. NX2NAVI_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 nx2navi.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.
|
|
#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 "0400486024DEC000" // the game's title id, which names a mod's Atmosphere folders
|
|
#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, for mods_changed
|
|
|
|
// 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; // NX2NAVI_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;
|
|
}
|
|
|
|
// The build id of a pchtxt file as tools/exefs_patches.py parse_pchtxt reads it: the last "@nsobid-<hex>" line before
|
|
// an "@stop" line, a UTF-8 BOM, blanks around it and a "//" comment after it left out. 1 when the file names one; *len
|
|
// is 0 when that line holds anything but hex digits (no build id, so for no main).
|
|
static int pchtxt_build(const char *data, size_t n, const char **id, size_t *len) {
|
|
int found = 0;
|
|
const char *p = data, *end = data + n;
|
|
if (n >= 3 && !memcmp(p, "\xEF\xBB\xBF", 3)) p += 3;
|
|
while (p < end) {
|
|
const char *eol = memchr(p, '\n', (size_t)(end - p));
|
|
if (!eol) eol = end;
|
|
while (p < eol && isspace((unsigned char)*p)) p++;
|
|
if (eol - p >= 5 && !memcmp(p, "@stop", 5)) break;
|
|
if (eol - p >= 8 && !memcmp(p, "@nsobid-", 8)) {
|
|
const char *h = p + 8, *t = h;
|
|
while (t < eol && isxdigit((unsigned char)*t)) t++;
|
|
const char *rest = t;
|
|
while (rest < eol && isspace((unsigned char)*rest)) rest++;
|
|
int clean = rest == eol || (eol - rest >= 2 && rest[0] == '/' && rest[1] == '/');
|
|
found = 1;
|
|
*id = h;
|
|
*len = clean ? (size_t)(t - h) : 0;
|
|
}
|
|
p = eol + 1;
|
|
}
|
|
return found;
|
|
}
|
|
|
|
// build-cache/exefs.status next to nx2navi.exe, read once; "" when there is none.
|
|
static const char *exefs_status(void) {
|
|
static 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/exefs.status");
|
|
text = (char *)plat_file_read(path, &n);
|
|
}
|
|
if (!text) text = calloc(1, 1);
|
|
return 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];
|
|
snprintf(copy, sizeof copy, "%.*s", (int)(len < sizeof copy ? len : 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 {
|
|
const char *id = NULL;
|
|
size_t len = 0;
|
|
for_main = pchtxt_build(data, (size_t)n, &id, &len) && len && 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, "other")) {
|
|
m->other_version++;
|
|
}
|
|
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 && kind != PLAT_FS_LINK) 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;
|
|
}
|
|
// a symbolic link or junction to a folder is a mod, as the setup's tools see it (tools/cheats.py cheat_files)
|
|
if (kind == PLAT_FS_LINK && !plat_dir_exists(path)) 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->kind & (MOD_ROMFS | MOD_CHEATS) ? MOD_OK
|
|
: m->kind & MOD_EXEFS ? MOD_EXEFS_OTHER
|
|
: MOD_EMPTY;
|
|
m->enabled = m->start_enabled = !listed(disabled, m->name);
|
|
nmods++;
|
|
return 1;
|
|
}
|
|
|
|
void mods_scan(void) {
|
|
static int done;
|
|
if (done) return;
|
|
done = 1;
|
|
// a relative mods_dir is under the folder of nx2navi.exe, as the default is, whatever folder the game started in
|
|
const char *d = cfg_get("NX2NAVI_MODS_DIR");
|
|
if (!d || !*d) d = "mods";
|
|
int absolute = d[0] == '/' || d[0] == '\\' || (d[0] && d[1] == ':');
|
|
char exe[2048];
|
|
char *cut = NULL;
|
|
if (!absolute && plat_exe_path(exe, sizeof exe)) {
|
|
cut = strrchr(exe, '/');
|
|
char *back = strrchr(exe, '\\');
|
|
if (back > cut) cut = back;
|
|
}
|
|
if (absolute) {
|
|
snprintf(dir, sizeof dir, "%s", d);
|
|
} else if (cut) {
|
|
*cut = 0;
|
|
snprintf(dir, sizeof dir, "%s/%s", exe, d);
|
|
} else {
|
|
return;
|
|
}
|
|
if (!plat_dir_exists(dir)) {
|
|
rt_log("mods: no folder %s", dir);
|
|
return;
|
|
}
|
|
mods = calloc(MAX_MODS, sizeof *mods);
|
|
if (!mods) return;
|
|
if (plat_fs_list(dir, scan_cb, (void *)cfg_get("NX2NAVI_MODS_DISABLED")) != PLAT_FS_OK)
|
|
rt_log("mods: cannot list %s (error %d); no mods", dir, plat_fs_os_error());
|
|
order_list = cfg_get("NX2NAVI_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);
|
|
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);
|
|
}
|
|
|
|
void mods_exefs_switch(void) {
|
|
mods_scan();
|
|
for (int i = 0; i < nmods; i++) {
|
|
const ModInfo *m = &mods[i];
|
|
if (!m->start_enabled || m->problem != MOD_OK || !m->nsets) continue;
|
|
for (int k = 0; k < m->nsets; k++) {
|
|
char name[32];
|
|
snprintf(name, sizeof name, "mod_%08x", m->sets[k]);
|
|
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);
|
|
}
|
|
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", m->name, m->nsets);
|
|
}
|
|
}
|
|
|
|
int mods_exefs_pending(uint32_t *key) {
|
|
int n = 0;
|
|
for (int i = 0; i < nmods; i++) {
|
|
if (!mods[i].start_enabled || 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; }
|
|
|
|
// NX2NAVI_MODS_ORDER as every listed name top first; NX2NAVI_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("NX2NAVI_MODS_ORDER", order);
|
|
cfg_set("NX2NAVI_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 != mods[i].start_enabled) return 1;
|
|
return 0;
|
|
}
|