mirror of
https://github.com/valentderah/totk-pc-recomp.git
synced 2026-11-11 01:41:02 +00:00
The VM refused C0 (register conditional), so cheats like C046B400 FFFFFFFF were greyed out as unsupported. C0 now runs with dmnt's operand types 0-5, widths and conditions; C1 and C2 save, restore and clear registers, C3 moves registers to and from the static registers. tools/cheats.py gives the same opcode lengths and accepts the same opcodes. skip() took the end of a nested block at depth 1 for the else of the failed outer block; it now counts depth the way dmnt does, and 2x000000 is an else only for x = 1. tests/cheats_vm_check.py runs tools/cheats.py and runtime/cheats.c on cheats of its own, the two from the issue included. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
742 lines
31 KiB
C
742 lines
31 KiB
C
// Cheats of the mods' Atmosphere cheat files (cheats.h). Each enabled mod the port can use (mods.h) may hold
|
|
// <build id>.txt in its cheats folder, the build id the first 8 bytes of main's NSO build id, as Atmosphere and the
|
|
// emulators name it; the file name's case does not matter. Each cheat's switch is the setting TOTK_CHEAT_<hash>
|
|
// (cheat_<hash> under [cheats] in totk.ini), the hash FNV-1a 32 of "<mod folder name>/<cheat name>", so two mods'
|
|
// cheats of one name keep their own switches.
|
|
//
|
|
// Code cheats (opcode 0 writes into main's code) cannot be applied here: the game's code is lifted C. tools/cheats.py
|
|
// turns the code cheats of every mod into patch sets cheat_<hash> when the game is built and writes
|
|
// build-cache\cheats.status next to totk.exe, one line per cheat: name hash, content hash, kind, buttons, note
|
|
// (tools/cheats.py has the format). A cheat whose status line is missing or has another content hash was added or
|
|
// edited after the build: a data cheat runs anyway, a code cheat waits for the setup to run again.
|
|
//
|
|
// Data cheats run in a VM after Atmosphere's dmnt cheat VM (opcodes 0-A and C0-C3; C4 and the 0xF.. extensions are not
|
|
// supported), every presented frame while their switch is on, with registers and saved registers cleared before each
|
|
// cheat; the static registers (C3) stay, shared by all cheats, as dmnt keeps them. Memory is the
|
|
// guest's, mapped at its own addresses: main's image is written with its pages made writable for the write (read-only
|
|
// data included, as dmnt writes through the debugger), everything else after mem_query says the guest may read or write
|
|
// it. Buttons are input_poll's HidNpadButton bits, the same as the cheat format's key masks; they read 0 while the menu
|
|
// is open.
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <strings.h>
|
|
|
|
#include "cfg.h"
|
|
#include "cheats.h"
|
|
#include "host.h"
|
|
#include "input.h"
|
|
#include "mods.h"
|
|
#include "platform/platform.h"
|
|
|
|
#ifdef _WIN32
|
|
#define SEP "\\"
|
|
#else
|
|
#define SEP "/"
|
|
#endif
|
|
|
|
enum { STATUS_MAX = 160, OPS_PER_RUN = 1 << 20 };
|
|
|
|
typedef struct {
|
|
CheatInfo info;
|
|
uint32_t *words;
|
|
int n;
|
|
int dup; // 1 for the first cheat of its name in its file, 2 for the next ...
|
|
uint32_t content; // FNV-1a 32 of its words as tools/cheats.py writes them
|
|
int set; // patch set, -1 none
|
|
volatile int32_t on;
|
|
char note[STATUS_MAX];
|
|
} Cheat;
|
|
|
|
static Cheat *g_cheats;
|
|
static int g_n, g_cap, g_vm; // g_vm: some cheat needs cheats_frame
|
|
static char g_file[64]; // "<build id>.txt"
|
|
static char g_dir[PLAT_PATH_MAX]; // the folder of totk.exe, with a trailing separator
|
|
static int g_files; // mods whose file was read
|
|
static uint64_t g_static[256]; // C3's static registers: 0x00-0x7F loaded by cheats, 0x80-0xFF stored by them
|
|
static uint64_t g_main, g_main_size, g_code_end; // main's image; its code with the padding up to .rodata
|
|
|
|
static uint32_t fnv(const char *s, size_t n) {
|
|
uint32_t h = 0x811C9DC5u;
|
|
for (size_t i = 0; i < n; i++) h = (h ^ (uint8_t)s[i]) * 0x01000193u;
|
|
return h;
|
|
}
|
|
|
|
// The folder of the executable, with a trailing separator.
|
|
static int exe_dir(char *out, size_t n) {
|
|
if (!plat_exe_path(out, n)) return 0;
|
|
char *cut = plat_last_sep(out);
|
|
if (!cut) return 0;
|
|
cut[1] = 0;
|
|
return 1;
|
|
}
|
|
|
|
static int build_id(char out[17]) {
|
|
char path[PLAT_PATH_MAX];
|
|
snprintf(path, sizeof path, "%s/main", rt_config()->exefs);
|
|
PlatFile *f = plat_file_open(path);
|
|
if (!f) return 0;
|
|
uint8_t head[0x48];
|
|
int ok = plat_file_pread(f, head, sizeof head, 0) == sizeof head && !memcmp(head, "NSO0", 4);
|
|
plat_file_close(f);
|
|
for (int i = 0; ok && i < 8; i++) snprintf(out + 2 * i, 3, "%02X", head[0x40 + i]);
|
|
return ok;
|
|
}
|
|
|
|
// ---- opcodes
|
|
|
|
// Words of the opcode at w (left words remain), 0 when it is cut short.
|
|
static int op_len(const uint32_t *w, int left) {
|
|
uint32_t f = w[0];
|
|
int t = f >> 28, width = (f >> 24) & 0xF, n;
|
|
switch (t) {
|
|
case 0: case 1: n = width == 8 ? 4 : 3; break;
|
|
case 2: case 8: n = 1; break;
|
|
case 3: n = width == 0 ? 2 : 1; break;
|
|
case 4: case 6: n = 3; break;
|
|
case 5: case 7: n = 2; break;
|
|
case 9: n = 1 + (((f >> 8) & 0xF) ? (width == 8 ? 2 : 1) : 0); break;
|
|
case 0xA: n = ((f >> 8) & 0xF) == 2 || ((f >> 8) & 0xF) == 4 || ((f >> 8) & 0xF) == 5 ? 2 : 1; break;
|
|
case 0xC: // C0TcSX..: an address word for operand types 0 and 2, the value (two words at width 8) for 4
|
|
if (((f >> 24) & 0xF) != 0) n = 1;
|
|
else if (((f >> 8) & 0xF) == 0 || ((f >> 8) & 0xF) == 2) n = 2;
|
|
else n = ((f >> 8) & 0xF) == 4 ? (((f >> 20) & 0xF) == 8 ? 3 : 2) : 1;
|
|
break;
|
|
default: n = 1; break;
|
|
}
|
|
return n <= left ? n : 0;
|
|
}
|
|
|
|
// NULL when the VM runs the opcode, else why not (tools/cheats.py vm_ok).
|
|
static const char *vm_unsupported(const uint32_t *w, char *buf, int n) {
|
|
uint32_t f = w[0];
|
|
int t = f >> 28, width = (f >> 24) & 0xF;
|
|
int ext = f >> 24; // 0xC0-0xC3 run; C0's width is at bits 20-23 and its operand type (at most 5) at bits 8-11
|
|
if (ext == 0xC0) t = -1, width = (f >> 20) & 0xF;
|
|
if ((t > 0xA && (ext < 0xC1 || ext > 0xC3)) || (t == -1 && ((f >> 8) & 0xF) > 5) ||
|
|
(t == 5 && ((f >> 12) & 0xF) > 1) || (t == 0xA && ((f >> 8) & 0xF) > 5)) {
|
|
snprintf(buf, n, "opcode %08X is not supported", f);
|
|
return buf;
|
|
}
|
|
if ((t <= 1 || t == 5 || t == 6 || t == 7 || t == 9 || t == 0xA) && width != 1 && width != 2 && width != 4 &&
|
|
width != 8) { // C0 too (t -1)
|
|
snprintf(buf, n, "opcode %08X has a bad width", f);
|
|
return buf;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
// The kind of a cheat the build did not see: heading, data, rebuild (it writes main's code) or unsupported.
|
|
static CheatKind classify(Cheat *c) {
|
|
int ops = 0, ends = 0;
|
|
for (int i = 0; i < c->n;) {
|
|
int len = op_len(c->words + i, c->n - i);
|
|
if (!len) {
|
|
snprintf(c->note, sizeof c->note, "opcode %08X is cut short", c->words[i]);
|
|
return CHEAT_UNSUPPORTED;
|
|
}
|
|
uint32_t f = c->words[i];
|
|
ops++;
|
|
ends += f >> 28 == 2;
|
|
if (f >> 28 == 0 && ((f >> 20) & 0xF) == 0) {
|
|
uint64_t off = ((uint64_t)(f & 0xFF) << 32) | c->words[i + 1];
|
|
if (off < g_code_end - g_main) return CHEAT_REBUILD;
|
|
}
|
|
if (vm_unsupported(c->words + i, c->note, sizeof c->note)) return CHEAT_UNSUPPORTED;
|
|
i += len;
|
|
}
|
|
return ops == ends && ops <= 1 ? CHEAT_HEADING : CHEAT_DATA;
|
|
}
|
|
|
|
// ---- guest memory
|
|
|
|
static uint64_t region(int m) {
|
|
const RtGuestLayout *l = mem_layout();
|
|
switch (m) {
|
|
case 0: return g_main;
|
|
case 1: return l->heap.base;
|
|
case 2: return l->alias.base;
|
|
case 3: return l->aslr.base;
|
|
default: return 0;
|
|
}
|
|
}
|
|
|
|
static int in_main(uint64_t a, unsigned n) { return a >= g_main && a + n <= g_main + g_main_size; }
|
|
|
|
static int guest_ok(uint64_t a, unsigned n, uint32_t perm) {
|
|
if (in_main(a, n)) return 1;
|
|
RtMemoryInfo mi;
|
|
if (mem_query(a, &mi) || !mi.state || (mi.perm & perm) != perm) return 0;
|
|
return a + n <= mi.base + mi.size;
|
|
}
|
|
|
|
static int rd(uint64_t a, int width, uint64_t *v) {
|
|
if (!a || !guest_ok(a, (unsigned)width, 1)) return 0;
|
|
uint64_t x = 0;
|
|
memcpy(&x, (const void *)(uintptr_t)a, (size_t)width);
|
|
*v = x;
|
|
return 1;
|
|
}
|
|
|
|
// Writes n bytes into main's image a page at a time: a read-only page is writable for its part of the copy only.
|
|
static void wr_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;
|
|
if (prot == PLAT_PROT_READ && !plat_mem_protect(page, 0x1000, PLAT_PROT_RW)) return;
|
|
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;
|
|
}
|
|
}
|
|
|
|
static void wr(uint64_t a, int width, uint64_t v) {
|
|
uint64_t now;
|
|
if (!rd(a, width, &now) || !memcmp(&now, &v, (size_t)width)) return; // unchanged: no write, no protection change
|
|
void *p = (void *)(uintptr_t)a;
|
|
if (in_main(a, (unsigned)width)) {
|
|
wr_main(a, (const uint8_t *)&v, width);
|
|
} else if (guest_ok(a, (unsigned)width, 2)) {
|
|
memcpy(p, &v, (size_t)width); // a frozen region thaws on the write (memory.c)
|
|
}
|
|
}
|
|
|
|
static uint64_t masked(int width, uint64_t v) { return width >= 8 ? v : v & ((1ull << (8 * width)) - 1); }
|
|
|
|
static uint64_t arith(int op, uint64_t a, uint64_t b) {
|
|
switch (op) {
|
|
case 0: return a + b;
|
|
case 1: return a - b;
|
|
case 2: return a * b;
|
|
case 3: return b < 64 ? a << b : 0;
|
|
case 4: return b < 64 ? a >> b : 0;
|
|
case 5: return a & b;
|
|
case 6: return a | b;
|
|
case 7: return ~a;
|
|
case 8: return a ^ b;
|
|
default: return a; // 9: move
|
|
}
|
|
}
|
|
|
|
static int compare(int cond, uint64_t a, uint64_t b) {
|
|
switch (cond) {
|
|
case 1: return a > b;
|
|
case 2: return a >= b;
|
|
case 3: return a < b;
|
|
case 4: return a <= b;
|
|
case 5: return a == b;
|
|
case 6: return a != b;
|
|
default: return 0;
|
|
}
|
|
}
|
|
|
|
// From i (after a failed condition, or an else reached while running), the opcode to continue at: after the
|
|
// matching else (else_ok) or end. Conditions 1, 8 and C0 open a block, as in Atmosphere's dmnt
|
|
// (stratosphere/dmnt/source/cheat/impl/dmnt_cheat_vm.cpp, CheatVirtualMachine::SkipConditionalBlock), which steps
|
|
// over whole opcodes so that an operand word reading 20000000 ends nothing.
|
|
static int skip(const uint32_t *w, int n, int i, int else_ok) {
|
|
int depth = 0;
|
|
while (i < n) {
|
|
int len = op_len(w + i, n - i);
|
|
if (!len) return n;
|
|
uint32_t f = w[i];
|
|
int t = f >> 28;
|
|
i += len;
|
|
if (t == 1 || t == 8 || f >> 24 == 0xC0) depth++;
|
|
else if (t == 2 && ((f >> 24) & 0xF) == 1) { // else; any other 2x000000 ends, as dmnt decodes is_else
|
|
if (depth == 0 && else_ok) return i;
|
|
} else if (t == 2 && depth-- == 0) {
|
|
return i;
|
|
}
|
|
}
|
|
return n;
|
|
}
|
|
|
|
static uint64_t imm64(const uint32_t *w) { return (uint64_t)w[0] << 32 | w[1]; }
|
|
|
|
// C0TcSX..: if register S <cond c> operand X, all cut to width T; the operand as Atmosphere's dmnt reads it
|
|
// (stratosphere/dmnt/source/cheat/impl/dmnt_cheat_vm.cpp, CheatVirtualMachine::DecodeNextOpcode and Execute,
|
|
// BeginRegisterConditionalBlock). X: 0 region M + a, 1 region M + register r, 2 register R + a, 3 register R +
|
|
// register r, 4 the value V, 5 register R. M, R at bits 4-7, r and the top bits of a at bits 0-3. A read the guest
|
|
// cannot make compares as 0: dmnt ignores the read's result and compares the zero it started with.
|
|
static int reg_cond(const uint32_t *w, const uint64_t *r) {
|
|
uint32_t f = w[0];
|
|
int width = (f >> 20) & 0xF, cond = (f >> 16) & 0xF, a4 = (f >> 4) & 0xF, a0 = f & 0xF;
|
|
uint64_t src = masked(width, r[(f >> 12) & 0xF]), b = 0, addr;
|
|
switch ((f >> 8) & 0xF) { // w[1] exists for types 0, 2 and 4 only (op_len)
|
|
case 0: addr = region(a4) + (((uint64_t)a0 << 32) | w[1]); break;
|
|
case 1: addr = region(a4) + r[a0]; break;
|
|
case 2: addr = r[a4] + (((uint64_t)a0 << 32) | w[1]); break;
|
|
case 3: addr = r[a4] + r[a0]; break;
|
|
case 4: return compare(cond, src, width == 8 ? imm64(w + 1) : masked(width, w[1]));
|
|
default: return compare(cond, src, masked(width, r[a4]));
|
|
}
|
|
if (!rd(addr, width, &b)) b = 0;
|
|
return compare(cond, src, b);
|
|
}
|
|
|
|
static void run(const Cheat *c, uint64_t keys) {
|
|
uint64_t r[16] = {0}, saved[16] = {0};
|
|
int loop[16] = {0};
|
|
const uint32_t *w = c->words;
|
|
int n = c->n, budget = OPS_PER_RUN;
|
|
for (int i = 0; i < n && budget-- > 0;) {
|
|
int len = op_len(w + i, n - i);
|
|
if (!len) return;
|
|
uint32_t f = w[i];
|
|
int t = f >> 28, width = (f >> 24) & 0xF, x = (f >> 20) & 0xF, y = (f >> 16) & 0xF, z = (f >> 12) & 0xF;
|
|
uint64_t rel = ((uint64_t)(f & 0xFF) << 32) | (len > 1 ? w[i + 1] : 0), v;
|
|
switch (t) {
|
|
case 0: // 0TMR00AA AAAAAAAA V..: store a value at region M + A + register R
|
|
wr(region(x) + rel + r[y], width, width == 8 ? imm64(w + i + 2) : w[i + 2]);
|
|
break;
|
|
case 1: // 1TMC00AA AAAAAAAA V..: if (region M + A) <cond C> V
|
|
if (!rd(region(x) + rel, width, &v) || !compare(y, v, width == 8 ? imm64(w + i + 2) : w[i + 2])) {
|
|
i = skip(w, n, i + len, 1);
|
|
continue;
|
|
}
|
|
break;
|
|
case 2: // 20000000 end, 21000000 else: an else reached while running skips to the end
|
|
if (width == 1) {
|
|
i = skip(w, n, i + len, 0);
|
|
continue;
|
|
}
|
|
break;
|
|
case 3: // 300R0000 VVVVVVVV loop start, 310R0000 loop end
|
|
if (width == 0) {
|
|
r[y] = w[i + 1];
|
|
loop[y] = i + len;
|
|
} else if (r[y] && --r[y]) {
|
|
i = loop[y];
|
|
continue;
|
|
}
|
|
break;
|
|
case 4: // 400R0000 V V: register R = V
|
|
r[y] = imm64(w + i + 1);
|
|
break;
|
|
case 5: // 5TMRI0AA AAAAAAAA: R = (I ? R : region M) + A, read
|
|
if (!rd(z ? r[y] + rel : region(x) + rel, width, &v)) return; // a broken pointer chain: nothing more
|
|
r[y] = v;
|
|
break;
|
|
case 6: { // 6T0RIor0 V V: store V at R (+ register r); I: R += width
|
|
uint64_t a = r[y] + (((f >> 8) & 0xF) ? r[(f >> 4) & 0xF] : 0);
|
|
wr(a, width, imm64(w + i + 1));
|
|
if (z) r[y] += (uint64_t)width;
|
|
break;
|
|
}
|
|
case 7: // 7T0RC000 VVVVVVVV: R = R <op C> V
|
|
r[y] = masked(width, arith(z, r[y], w[i + 1]));
|
|
break;
|
|
case 8: // 8kkkkkkk: if the buttons are held
|
|
if ((keys & (f & 0x0FFFFFFF)) != (f & 0x0FFFFFFF)) {
|
|
i = skip(w, n, i + len, 1);
|
|
continue;
|
|
}
|
|
break;
|
|
case 9: { // 9TCRS0s0 / 9TCRS100 V..: R = S <op C> (s or V)
|
|
uint64_t b = (f >> 8) & 0xF ? (width == 8 ? imm64(w + i + 1) : w[i + 1]) : r[(f >> 4) & 0xF];
|
|
r[y] = masked(width, arith(x, r[z], b));
|
|
break;
|
|
}
|
|
case 0xA: { // ATSRIOxa (aaaaaaaa): store register S at an address of type O
|
|
int o = (f >> 8) & 0xF, m = (f >> 4) & 0xF;
|
|
uint64_t imm = ((uint64_t)(f & 0xF) << 32) | (len > 1 ? w[i + 1] : 0), a;
|
|
switch (o) {
|
|
case 0: a = r[y]; break;
|
|
case 1: a = r[y] + r[m]; break;
|
|
case 2: a = r[y] + imm; break;
|
|
case 3: a = region(m) + r[y]; break;
|
|
case 4: a = region(m) + imm; break;
|
|
default: a = region(m) + r[y] + imm; break;
|
|
}
|
|
wr(a, width, r[x]);
|
|
if (z) r[y] += (uint64_t)width;
|
|
break;
|
|
}
|
|
case 0xC: // C1-C3 as dmnt's Execute runs SaveRestoreRegister(Mask) and ReadWriteStaticRegister
|
|
switch (f >> 24) {
|
|
case 0xC0:
|
|
if (!reg_cond(w + i, r)) {
|
|
i = skip(w, n, i + len, 1);
|
|
continue;
|
|
}
|
|
break;
|
|
case 0xC1: { // C10D0Sx0: x 0 register D = saved S, 1 saved D = register S, 2 saved D = 0,
|
|
// 3 register D = 0
|
|
int d = (f >> 16) & 0xF, s = (f >> 8) & 0xF, op = (f >> 4) & 0xF;
|
|
if (op == 1) saved[d] = r[s];
|
|
else if (op == 2) saved[d] = 0;
|
|
else if (op == 3) r[d] = 0;
|
|
else r[d] = saved[s]; // dmnt restores for any other x too
|
|
break;
|
|
}
|
|
case 0xC2: { // C2x0XXXX: for each register in the mask X, x as C1 with D = S = that register
|
|
int op = (f >> 20) & 0xF;
|
|
for (int k = 0; k < 16; k++) {
|
|
if (!(f >> k & 1)) continue;
|
|
if (op == 1) saved[k] = r[k];
|
|
else if (op == 2) saved[k] = 0;
|
|
else if (op == 3) r[k] = 0;
|
|
else r[k] = saved[k];
|
|
}
|
|
break;
|
|
}
|
|
case 0xC3: { // C3000XXx: static register XX < 0x80 into register x, else register x into it
|
|
int s = (f >> 4) & 0xFF;
|
|
if (s < 0x80) r[f & 0xF] = g_static[s];
|
|
else g_static[s] = r[f & 0xF];
|
|
break;
|
|
}
|
|
default:
|
|
return;
|
|
}
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
i += len;
|
|
}
|
|
}
|
|
|
|
// ---- the file
|
|
|
|
static char *read_text(const char *path) {
|
|
uint64_t n = 0;
|
|
char *s = (char *)plat_file_read(path, &n);
|
|
if (s && n >= 3 && !memcmp(s, "\xEF\xBB\xBF", 3)) memmove(s, s + 3, (size_t)n - 2); // a UTF-8 BOM
|
|
return s;
|
|
}
|
|
|
|
static void add_word(Cheat *c, uint32_t v, int *cap) {
|
|
if (c->n == *cap) {
|
|
*cap = *cap ? *cap * 2 : 16;
|
|
c->words = (uint32_t *)realloc(c->words, sizeof *c->words * (size_t)*cap);
|
|
if (!c->words) rt_fatal("cheats: out of memory");
|
|
}
|
|
c->words[c->n++] = v;
|
|
}
|
|
|
|
// Appends the cheats of one mod's file; mod: the mod's folder name (kept).
|
|
static void parse(char *s, const char *mod) {
|
|
int first = g_n, wcap = 0;
|
|
for (char *line = s; line && *line;) {
|
|
char *end = strchr(line, '\n'), *next = end ? end + 1 : NULL;
|
|
if (end) *end = 0;
|
|
while (*line == ' ' || *line == '\t') line++;
|
|
size_t len = strlen(line);
|
|
while (len && (line[len - 1] == '\r' || line[len - 1] == ' ' || line[len - 1] == '\t')) line[--len] = 0;
|
|
if (len >= 2 && (line[0] == '[' || line[0] == '{') && (line[len - 1] == ']' || line[len - 1] == '}')) {
|
|
if (g_n == g_cap) {
|
|
g_cap = g_cap ? g_cap * 2 : 32;
|
|
g_cheats = (Cheat *)realloc(g_cheats, sizeof *g_cheats * (size_t)g_cap);
|
|
if (!g_cheats) rt_fatal("cheats: out of memory");
|
|
}
|
|
Cheat *c = &g_cheats[g_n++];
|
|
memset(c, 0, sizeof *c);
|
|
line[len - 1] = 0;
|
|
char *name = line + 1;
|
|
while (*name == ' ' || *name == '\t') name++;
|
|
size_t k = strlen(name);
|
|
while (k && (name[k - 1] == ' ' || name[k - 1] == '\t')) name[--k] = 0;
|
|
c->info.name = strdup(name);
|
|
c->info.mod = mod;
|
|
c->info.kind = line[0] == '{' ? CHEAT_MASTER : CHEAT_HEADING;
|
|
c->set = -1;
|
|
c->dup = 1; // a repeated name gets "#2", "#3" ... (tools/cheats.py parse)
|
|
for (int j = first; j < g_n - 1; j++) c->dup += !strcmp(g_cheats[j].info.name, c->info.name);
|
|
wcap = 0;
|
|
} else if (g_n > first) {
|
|
for (char *tok = strtok(line, " \t"); tok; tok = strtok(NULL, " \t")) {
|
|
char *e;
|
|
unsigned long v = strtoul(tok, &e, 16);
|
|
if (e - tok == 8 && !*e) add_word(&g_cheats[g_n - 1], (uint32_t)v, &wcap);
|
|
}
|
|
}
|
|
line = next;
|
|
}
|
|
}
|
|
|
|
// The file g_file in folder, its name in any case, into out; 0 when there is none.
|
|
typedef struct {
|
|
const char *dir;
|
|
char *out;
|
|
size_t cap;
|
|
int found;
|
|
} FindFile;
|
|
static int find_cb(const char *name, int kind, uint64_t size, void *ctx) {
|
|
(void)size;
|
|
FindFile *f = (FindFile *)ctx;
|
|
if (kind == PLAT_FS_FILE && !strcasecmp(name, g_file) &&
|
|
snprintf(f->out, f->cap, "%s/%s", f->dir, name) < (int)f->cap)
|
|
f->found = 1;
|
|
return !f->found;
|
|
}
|
|
static int find_file(const char *folder, char *out, size_t cap) {
|
|
FindFile f = {folder, out, cap, 0};
|
|
plat_fs_list(folder, find_cb, &f);
|
|
return f.found;
|
|
}
|
|
|
|
// The status line of key, its fields split in place; 0 when none.
|
|
static int status_of(char *status, const char *key, char **fields) {
|
|
for (char *line = status; line && *line;) {
|
|
char *end = strchr(line, '\n'), *next = end ? end + 1 : NULL;
|
|
if (!strncmp(line, key, 8) && line[8] == '\t') {
|
|
static char copy[512];
|
|
size_t n = end ? (size_t)(end - line) : strlen(line);
|
|
if (n >= sizeof copy) n = sizeof copy - 1;
|
|
memcpy(copy, line, n);
|
|
copy[n] = 0;
|
|
if (n && copy[n - 1] == '\r') copy[n - 1] = 0;
|
|
int k = 0;
|
|
fields[k++] = copy;
|
|
for (char *p = copy; *p && k < 5; p++)
|
|
if (*p == '\t') *p = 0, fields[k++] = p + 1;
|
|
while (k < 5) fields[k++] = (char *)"";
|
|
return 1;
|
|
}
|
|
line = next;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// The menu's font has no numero sign (U+2116), which cheat names often start with: they show "No" instead.
|
|
static void plain_name(char *s) {
|
|
for (char *p; (p = strstr(s, "\xE2\x84\x96")) != NULL;) {
|
|
p[0] = 'N', p[1] = 'o';
|
|
memmove(p + 2, p + 3, strlen(p + 3) + 1);
|
|
}
|
|
}
|
|
|
|
// ---- rebuild: code cheats and ExeFS patches the build lacks
|
|
// The setup writes build-cache\rebuild.txt (installer/steps.py write_rebuild_command), the command that starts its
|
|
// window in rebuild mode on this install folder, one argument per line. When an enabled mod has code cheats this build
|
|
// lacks (CHEAT_REBUILD) or ExeFS patch files it lacks (mods_exefs_pending), the game runs it and exits; the setup lifts
|
|
// and compiles them in and starts the game again.
|
|
// build-cache\rebuild.skip holds the key of the cheats a rebuild was started for: a rebuild that failed or was
|
|
// cancelled leaves them unbuilt, and the same set does not start it again. TOTK_MODS_REBUILD=off never starts it.
|
|
// Scripted runs (--seconds, --boot) never start it.
|
|
static void rebuild_if_needed(const char *dir) {
|
|
uint32_t key = 0x811C9DC5u;
|
|
int need = 0;
|
|
for (int i = 0; i < g_n; i++) {
|
|
const Cheat *c = &g_cheats[i];
|
|
if (c->info.kind != CHEAT_REBUILD) continue;
|
|
need++;
|
|
key = (key ^ fnv(c->info.key, strlen(c->info.key))) * 0x01000193u;
|
|
key = (key ^ c->content) * 0x01000193u;
|
|
}
|
|
int patches = mods_exefs_pending(&key);
|
|
if (!need && !patches) return;
|
|
char cmd_path[PLAT_PATH_MAX + 64], skip_path[PLAT_PATH_MAX + 64], skip[16] = "", want[16];
|
|
snprintf(cmd_path, sizeof cmd_path, "%sbuild-cache" SEP "rebuild.txt", dir);
|
|
snprintf(skip_path, sizeof skip_path, "%sbuild-cache" SEP "rebuild.skip", dir);
|
|
snprintf(want, sizeof want, "%08X", key);
|
|
char *old = read_text(skip_path);
|
|
if (old) snprintf(skip, sizeof skip, "%.8s", old);
|
|
free(old);
|
|
const char *why = !strcmp(cfg_value("TOTK_MODS_REBUILD"), "off") ? "mods_rebuild = off"
|
|
: rt_config()->seconds || rt_config()->boot ? "a scripted run"
|
|
: !strcmp(skip, want) ? "the last rebuild for them failed or was cancelled"
|
|
: NULL;
|
|
char *cmd = why ? NULL : read_text(cmd_path);
|
|
if (!why && !cmd) why = "no build-cache/rebuild.txt (a build without the setup)";
|
|
if (why) {
|
|
rt_log("cheats: %d code cheats and %d ExeFS patch files are not built into the game; no rebuild: %s", need,
|
|
patches, why);
|
|
free(cmd);
|
|
return;
|
|
}
|
|
const char *argv[32];
|
|
int argc = 0;
|
|
for (char *line = cmd; line && *line && argc < 31;) {
|
|
char *end = strchr(line, '\n'), *next = end ? end + 1 : NULL;
|
|
if (end) *end = 0;
|
|
size_t n = strlen(line);
|
|
if (n && line[n - 1] == '\r') line[n - 1] = 0;
|
|
if (*line) argv[argc++] = line;
|
|
line = next;
|
|
}
|
|
argv[argc] = NULL;
|
|
if (argc < 2 || !plat_file_write(skip_path, want, strlen(want))) {
|
|
rt_log("cheats: %d code cheats and %d ExeFS patch files are not built into the game; cannot start the "
|
|
"rebuild (%s)", need, patches, cmd_path);
|
|
free(cmd);
|
|
return;
|
|
}
|
|
rt_log("cheats: %d code cheats and %d ExeFS patch files are not built into the game; starting the rebuild: %s",
|
|
need, patches, argv[0]);
|
|
int ok = plat_spawn(argv, dir);
|
|
free(cmd);
|
|
if (!ok) {
|
|
rt_log("cheats: the rebuild did not start; the game starts with the code it has");
|
|
return;
|
|
}
|
|
prof_write(); // plat_exit runs no atexit handlers
|
|
plat_exit(0);
|
|
}
|
|
|
|
void cheats_init(void) {
|
|
const RtLoaded *m = ldr_count() ? ldr_module(0) : NULL;
|
|
char dir[PLAT_PATH_MAX], bid[17], path[2400]; // path: a mod's cheats folder (ModInfo.cheats) + the file
|
|
if (!m || !exe_dir(dir, sizeof dir) || !build_id(bid)) return;
|
|
snprintf(g_dir, sizeof g_dir, "%s", dir);
|
|
g_main = m->base;
|
|
g_main_size = m->size;
|
|
g_code_end = m->base + ((m->text_end + 0xFFF) & ~0xFFFull);
|
|
snprintf(g_file, sizeof g_file, "%s.txt", bid);
|
|
mods_scan();
|
|
for (int k = 0; k < mods_count(); k++) { // the Mods tab's order, top first
|
|
const ModInfo *mod = mods_at(k);
|
|
if (!mod->enabled || mod->problem != MOD_OK || !mod->cheats[0] || !find_file(mod->cheats, path, sizeof path))
|
|
continue;
|
|
char *text = read_text(path);
|
|
if (!text) {
|
|
rt_log("cheats: %s: cannot read %s", mod->name, path);
|
|
continue;
|
|
}
|
|
parse(text, strdup(mod->name));
|
|
free(text);
|
|
g_files++;
|
|
}
|
|
if (!g_files) {
|
|
rt_log("cheats: no enabled mod holds cheats/%s", g_file);
|
|
rebuild_if_needed(dir); // an enabled mod's ExeFS patches may still wait for it
|
|
return;
|
|
}
|
|
snprintf(path, sizeof path, "%sbuild-cache" SEP "cheats.status", dir);
|
|
char *status = read_text(path);
|
|
int counts[CHEAT_UNSUPPORTED + 1] = {0}, seen_dups = 0;
|
|
for (int i = 0; i < g_n; i++) {
|
|
Cheat *c = &g_cheats[i];
|
|
char named[900];
|
|
snprintf(named, sizeof named, c->dup > 1 ? "%s/%s#%d" : "%s/%s", c->info.mod, c->info.name, c->dup);
|
|
seen_dups += c->dup > 1;
|
|
char key[9], content[9];
|
|
snprintf(key, sizeof key, "%08X", fnv(named, strlen(named)));
|
|
size_t tn = (size_t)c->n * 9 + 1, tl = 0;
|
|
char *t = (char *)malloc(tn);
|
|
if (!t) rt_fatal("cheats: out of memory");
|
|
t[0] = 0;
|
|
for (int k = 0; k < c->n; k++) tl += (size_t)snprintf(t + tl, tn - tl, k ? " %08X" : "%08X", c->words[k]);
|
|
c->content = fnv(t, tl);
|
|
snprintf(content, sizeof content, "%08X", c->content);
|
|
free(t);
|
|
snprintf(c->info.key, sizeof c->info.key, "TOTK_CHEAT_%s", key);
|
|
int master = c->info.kind == CHEAT_MASTER;
|
|
char *f[5];
|
|
CheatKind kind;
|
|
if (status && status_of(status, key, f) && !strcmp(f[1], content)) {
|
|
const char *k = f[2];
|
|
kind = !strcmp(k, "code") ? CHEAT_CODE : !strcmp(k, "key") ? CHEAT_KEY : !strcmp(k, "master") ? CHEAT_MASTER
|
|
: !strcmp(k, "restore") ? CHEAT_RESTORE : !strcmp(k, "heading") ? CHEAT_HEADING
|
|
: !strcmp(k, "data") ? CHEAT_DATA : CHEAT_UNSUPPORTED;
|
|
c->info.buttons = (uint32_t)strtoul(f[3], NULL, 16);
|
|
snprintf(c->note, sizeof c->note, "%s", f[4]);
|
|
if (kind == CHEAT_CODE || kind == CHEAT_KEY || kind == CHEAT_MASTER) {
|
|
char set[32];
|
|
snprintf(set, sizeof set, "cheat_%08x", fnv(named, strlen(named)));
|
|
c->set = rt_patch_set(set);
|
|
if (c->set < 0 && kind != CHEAT_MASTER) kind = CHEAT_REBUILD;
|
|
}
|
|
} else {
|
|
kind = classify(c);
|
|
}
|
|
if (master && kind == CHEAT_DATA) kind = CHEAT_MASTER; // a data master code: the VM, always on
|
|
c->info.kind = kind;
|
|
c->info.note = c->note;
|
|
const char *v = cfg_get(c->info.key);
|
|
c->on = kind == CHEAT_MASTER || (v && *v && strcmp(v, "0") != 0);
|
|
if (kind == CHEAT_KEY && c->set >= 0) rt_patch_switch(c->set, 0); // on while the buttons are held
|
|
if (kind == CHEAT_KEY || kind == CHEAT_DATA || (kind == CHEAT_MASTER && c->set < 0)) g_vm = 1;
|
|
counts[kind]++;
|
|
}
|
|
free(status);
|
|
rebuild_if_needed(dir);
|
|
for (int i = 0; i < g_n; i++) plain_name((char *)g_cheats[i].info.name); // the hashes above read the file's bytes
|
|
int on = 0;
|
|
for (int i = 0; i < g_n; i++) on += g_cheats[i].on && g_cheats[i].info.kind != CHEAT_HEADING;
|
|
rt_log("cheats: %s in %d mods: %d cheats, %d on; %d code, %d while buttons are held, %d data, %d master, "
|
|
"%d restore, %d headings, %d wait for a rebuild, %d not supported%s",
|
|
g_file, g_files, g_n, on, counts[CHEAT_CODE], counts[CHEAT_KEY], counts[CHEAT_DATA], counts[CHEAT_MASTER],
|
|
counts[CHEAT_RESTORE], counts[CHEAT_HEADING], counts[CHEAT_REBUILD], counts[CHEAT_UNSUPPORTED],
|
|
seen_dups ? " (repeated names numbered)" : "");
|
|
}
|
|
|
|
int cheats_rebuild_pending(void) {
|
|
char path[PLAT_PATH_MAX + 64];
|
|
snprintf(path, sizeof path, "%sbuild-cache" SEP "rebuild.txt", g_dir);
|
|
uint32_t key = 0;
|
|
int need = mods_exefs_pending(&key);
|
|
for (int i = 0; i < g_n && !need; i++) need = g_cheats[i].info.kind == CHEAT_REBUILD;
|
|
return need && g_dir[0] && plat_file_exists(path);
|
|
}
|
|
|
|
void cheats_rebuild_allow(void) {
|
|
char path[PLAT_PATH_MAX + 64];
|
|
snprintf(path, sizeof path, "%sbuild-cache" SEP "rebuild.skip", g_dir);
|
|
plat_fs_unlink(path);
|
|
}
|
|
|
|
int cheats_count(void) { return g_n; }
|
|
|
|
const CheatInfo *cheats_info(int i) { return i >= 0 && i < g_n ? &g_cheats[i].info : NULL; }
|
|
|
|
int cheats_on(int i) { return i >= 0 && i < g_n && g_cheats[i].on; }
|
|
|
|
const char *cheats_file(void) { return g_file; }
|
|
|
|
void cheats_set(int i, int on) {
|
|
if (i < 0 || i >= g_n) return;
|
|
Cheat *c = &g_cheats[i];
|
|
if (c->info.kind != CHEAT_CODE && c->info.kind != CHEAT_KEY && c->info.kind != CHEAT_DATA) return;
|
|
plat_xchg32(&c->on, on != 0);
|
|
if (c->info.kind == CHEAT_CODE) rt_patch_switch(c->set, on); // CHEAT_KEY: cheats_frame follows the buttons
|
|
cfg_set(c->info.key, on ? "1" : "0");
|
|
rt_log("cheats: %s/%s %s", c->info.mod, c->info.name, on ? "on" : "off");
|
|
}
|
|
|
|
void cheats_frame(uint64_t frame) {
|
|
if (!g_vm || frame < 60) return; // the game polls its input (and starts SDL) before this
|
|
uint64_t keys = 0;
|
|
int32_t lx, ly, rx, ry;
|
|
input_poll(&keys, &lx, &ly, &rx, &ry); // 0 while the menu is open
|
|
for (int i = 0; i < g_n; i++) {
|
|
Cheat *c = &g_cheats[i];
|
|
switch (c->info.kind) {
|
|
case CHEAT_KEY:
|
|
if (c->set >= 0) {
|
|
int want = c->on && (keys & c->info.buttons) == c->info.buttons;
|
|
if (rt_patch_enabled(c->set) != want) rt_patch_switch(c->set, want);
|
|
}
|
|
break;
|
|
case CHEAT_DATA:
|
|
if (c->on) run(c, keys);
|
|
break;
|
|
case CHEAT_MASTER:
|
|
if (c->set < 0) run(c, keys);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
const char *cheats_buttons(uint32_t mask, char *buf, int n) {
|
|
static const char *const names[] = {"A", "B", "X", "Y", "Left stick press", "Right stick press", "L", "R", "ZL",
|
|
"ZR", "+", "-", "D-pad left", "D-pad up", "D-pad right", "D-pad down",
|
|
"Left stick left", "Left stick up", "Left stick right", "Left stick down",
|
|
"Right stick left", "Right stick up", "Right stick right", "Right stick down"};
|
|
int k = 0;
|
|
buf[0] = 0;
|
|
for (int b = 0; b < 24; b++)
|
|
if (mask & (1u << b)) k += snprintf(buf + k, (size_t)(n > k ? n - k : 0), "%s%s", k ? " + " : "", names[b]);
|
|
return buf;
|
|
}
|