Files
NX2navi/runtime/mods_probe.c
chrg6 e7f6a00407 Translate every shader, keep atomic loops from spinning on stale values, harden pool teardown, and update the README.
Shaders: a switch whose index is clamped against a register that a MOV of an immediate set before it now finds its
jump table (AmpereLowering.cs BrxTargets), so all 36,031 of the game's shader variants translate and no draw is skipped
for a failed one. The compare-and-swap loop behind storage-buffer atomics (patch 0033) takes the value a failed swap
returns as its next expected value instead of loading the word again, which on some drivers kept returning a stale
copy.

Memory: an unused physical pool outlives the submission being recorded, as virtual pages may still reach it through
device addresses; an image whose creation failed is not tried again for 2 s.

The installer and Linux texts name this game.
The README has the project icon, the current status, features, requirements and known issues.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 18:03:20 -05:00

131 lines
6.4 KiB
C

// --probe-mods: the mods folder scan, its order and switches (mods.c). Builds a mods folder of its own next to nx2navi.exe,
// scans it and removes it. Needs no game files.
#include <stdio.h>
#include <string.h>
#include "cfg.h"
#include "host.h"
#include "mods.h"
#include "platform/platform.h"
static int fails;
#define CHECK(c, what) do { if (!(c)) { fails++; fprintf(stderr, "FAIL mods: %s\n", what); } } while (0)
#define EQ(a, b) ((a) && !strcmp((a), (b)))
// root/rel as a file of a few bytes (rel ends in '/': a folder), with every folder above it.
static void put(const char *root, const char *rel) {
char p[2048];
snprintf(p, sizeof p, "%s/%s", root, rel);
char *slash = strrchr(p, '/');
*slash = 0;
plat_file_mkdir_p(p);
*slash = '/';
if (slash[1]) plat_file_write(p, "probe", 5);
}
static int remove_cb(const char *name, int kind, uint64_t size, void *ctx);
static void remove_tree(const char *dir) {
plat_fs_list(dir, remove_cb, (void *)dir);
plat_fs_rmdir(dir);
}
static int remove_cb(const char *name, int kind, uint64_t size, void *ctx) {
(void)size;
char sub[2048];
snprintf(sub, sizeof sub, "%s/%s", (const char *)ctx, name);
if (kind == PLAT_FS_DIR) remove_tree(sub);
else plat_fs_unlink(sub);
return 1;
}
// s ends in tail, either path separator matching either.
static int ends_with(const char *s, const char *tail) {
size_t n = strlen(s), t = strlen(tail);
if (n < t) return 0;
for (size_t i = 0; i < t; i++) {
char a = s[n - t + i], b = tail[i];
if (a == '\\') a = '/';
if (b == '\\') b = '/';
if (a != b) return 0;
}
return 1;
}
int probe_mods(void) {
char root[1024];
if (!plat_exe_path(root, sizeof root)) return 1;
char *cut = strrchr(root, '/'), *back = strrchr(root, '\\');
if (back > cut) cut = back;
if (!cut) return 1;
strcpy(cut + 1, "mods-probe");
remove_tree(root);
plat_file_mkdir_p(root);
static const char *const files[] = {
"A/romfs/x.bin", "B/atmosphere/contents/0100F2C0115B6000/romfs/x.bin",
"C/contents/0100F2C0115B6000/romfs/y.bin", "D/exefs/main.pchtxt", "E/exefs/subsdk9", "F/romfs/",
"G/romfs/z.bin", "G/exefs/1234.ips", "Ultra \xD0\x96/romfs/u.bin",
"H/romfs/h.bin", "H/exefs/subsdk9", "I/romfs/i.bin", "I/exefs/main", "J/cheats/0123456789ABCDEF.txt",
"K/romfslite/k.bin", "L/romfs/l.bin", "L/cheats/0123456789ABCDEF.txt", "L/exefs/main.npdm",
};
for (size_t i = 0; i < sizeof files / sizeof *files; i++) put(root, files[i]);
// an IPS file for this game's main (any file counts as one when main cannot be read), in Atmosphere's layout
char ips[160];
snprintf(ips, sizeof ips, "M/atmosphere/exefs_patches/x/%s.ips", *mods_main_build_id() ? mods_main_build_id() : "0");
put(root, ips);
cfg_set("NX2NAVI_MODS_DIR", root);
cfg_set("NX2NAVI_MODS_ORDER", "B|A|Gone");
cfg_set("NX2NAVI_MODS_DISABLED", "C");
mods_scan();
static const char *const order[] = {"B", "A", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
"Ultra \xD0\x96"};
enum { N = sizeof order / sizeof *order };
CHECK(mods_count() == N, "fourteen mod folders");
int in_order = mods_count() == N;
for (int i = 0; in_order && i < N; i++) in_order = EQ(mods_at(i)->name, order[i]);
CHECK(in_order, "order: mods_order first, the rest by name");
if (mods_count() == N && in_order) {
ModInfo *a = mods_at(1), *b = mods_at(0), *c = mods_at(2), *d = mods_at(3), *e = mods_at(4), *f = mods_at(5),
*g = mods_at(6), *hm = mods_at(7), *im = mods_at(8), *jm = mods_at(9), *km = mods_at(10),
*lm = mods_at(11), *mm = mods_at(12), *u = mods_at(13);
CHECK(hm->kind == (MOD_ROMFS | MOD_SUBSDK) && hm->problem == MOD_PLUGIN, "RomFS + subsdk plugin: unusable");
CHECK(im->kind == (MOD_ROMFS | MOD_SUBSDK) && im->problem == MOD_PLUGIN, "RomFS + replaced exefs/main");
CHECK(ends_with(b->romfs, "B/atmosphere/contents/0100F2C0115B6000/romfs"), "Atmosphere RomFS root");
CHECK(ends_with(c->romfs, "C/contents/0100F2C0115B6000/romfs"), "contents RomFS root");
CHECK(ends_with(a->romfs, "A/romfs") && a->kind == MOD_ROMFS && a->problem == MOD_OK, "plain RomFS mod");
CHECK(!c->enabled && a->enabled && b->enabled && g->enabled && u->enabled, "mods_disabled switches C off");
CHECK(d->kind == MOD_EXEFS && d->problem == MOD_EXEFS_OTHER && d->other_version == 1,
"ExeFS patch mod without a build id: another version's");
CHECK(e->problem == MOD_PLUGIN, "subsdk-only mod is a plugin mod");
CHECK(f->problem == MOD_EMPTY, "empty RomFS folder");
CHECK(g->kind == (MOD_ROMFS | MOD_EXEFS) && g->problem == MOD_OK && g->other_version == 1,
"RomFS + another version's ExeFS patch: the RomFS is used");
CHECK(mm->kind == MOD_EXEFS && mm->problem == MOD_EXEFS_LATER && mm->pending == 1,
"an ExeFS patch for main the build lacks waits for the rebuild");
CHECK(jm->kind == MOD_CHEATS && jm->problem == MOD_OK && ends_with(jm->cheats, "J/cheats"),
"cheats-only mod");
CHECK(km->kind == MOD_ROMFS && km->problem == MOD_OK && ends_with(km->romfs, "K/romfslite"), "merged romfslite");
CHECK(lm->kind == (MOD_ROMFS | MOD_CHEATS) && lm->problem == MOD_OK, "RomFS + cheats, main.npdm is no code");
CHECK(u->kind == MOD_ROMFS && ends_with(u->romfs, "Ultra \xD0\x96/romfs"), "Unicode folder name");
CHECK(!mods_changed(), "nothing changed after the scan");
mods_move(1, -1);
CHECK(EQ(mods_at(0)->name, "A") && EQ(mods_at(1)->name, "B"), "move up");
CHECK(EQ(cfg_get("NX2NAVI_MODS_ORDER"), "A|B|C|D|E|F|G|H|I|J|K|L|M|Ultra \xD0\x96"),
"order written without the gone name");
mods_move(0, -1);
CHECK(EQ(mods_at(0)->name, "A"), "move up at the top does nothing");
mods_move(N - 1, 1);
CHECK(EQ(mods_at(N - 1)->name, "Ultra \xD0\x96"), "move down at the bottom does nothing");
mods_set_enabled(2, 1);
const char *dis = cfg_get("NX2NAVI_MODS_DISABLED");
CHECK(mods_at(2)->enabled && (!dis || !*dis), "switching C on empties mods_disabled");
mods_set_enabled(0, 0);
CHECK(EQ(cfg_get("NX2NAVI_MODS_DISABLED"), "A"), "switching A off writes it");
CHECK(mods_changed(), "changes seen");
}
remove_tree(root);
printf("mods: %s\n", fails ? "FAIL" : "ok");
return fails ? 1 : 0;
}