Merge task/cheatc0: run cheat opcodes C0-C3 in the data cheat VM (#973)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Valiantsin DzerakhandClaude Opus 5.5 committed 2026-11-10 08:07:24 +03:00
commit 32f86a3296
5 files changed
+407 -14

No files matched your search

+3 -1
View File
@@ -246,7 +246,9 @@ files); a change to the settings passes `totk.exe --probe-cfg` and `python tests
when a setting is described neither in `totk.ini.example` nor in [settings.md](settings.md). A change to the mods' ExeFS patches
(`tools/exefs_patches.py`, `runtime/mods.c`, the `folded.txt` of `recomp/gen.py`) passes
`.venv\Scripts\python tests\exefs_patches_test.py`, which makes its own modules and runs `gen.py` on one with a jump
table (no game files), and `totk.exe --probe-mods`.
table (no game files), and `totk.exe --probe-mods`. A change to the cheat VM or the cheat opcode tables
(`runtime/cheats.c`, `tools/cheats.py`) passes `.venv\Scripts\python tests\cheats_vm_check.py`, which runs both on
cheats of its own with clang from PATH (`--cc` names another).
The lifter (`recomp/`) and the CPU headers (`runtime/cpu.h`, `runtime/simd.h`) are checked against Unicorn. Every lifter
change passes these, all run with `.venv`:
+2 -2
View File
@@ -58,7 +58,7 @@ The game's code is compiled ahead of time, so a cheat cannot write new instructi
|---|---|---|
| code | Opcode 0 writes into main's code | Built into the game at setup as a patch set (`recomp/patches.py`); switches at once. |
| key | Code writes inside a button condition (`8` mask, writes, `21` else, the original words, `20`) | A patch set that is on while the cheat is on and the buttons are held. |
| data | Writes data only: heap, main's data, pointer chains, button conditions | Runs in a cheat VM after dmnt's, opcodes 0 to A, every presented frame while on. |
| data | Writes data only: heap, main's data, pointer chains, button conditions | Runs in a cheat VM after dmnt's, opcodes 0 to A and C0 to C3, every presented frame while on. |
| master | `{name}` with code writes | A patch set that is always on. |
| restore | Writes back only the game's original instructions | Not needed: switching a cheat off restores them. Greyed out. |
@@ -66,7 +66,7 @@ A cheat whose code writes call or jump into a code cave the cheat writes itself
Not supported, and shown greyed out with the reason:
- the 0xC and 0xF opcode extensions;
- opcode C4 (extended button condition) and the 0xF extensions (FF0 pause, FF1 resume, FFF debug log);
- opcode 5 with a register offset;
- code writes mixed with other opcodes, except the button pattern above;
- code writes that change the CPU flags differently from the game's instruction, or code caves with branches or pc-relative instructions.
+83 -9
View File
@@ -10,8 +10,9 @@
// (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; the 0xC.. and 0xF.. extensions are not
// supported), every presented frame while their switch is on, with registers cleared before each cheat. Memory is the
// 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
@@ -53,6 +54,7 @@ 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) {
@@ -96,6 +98,11 @@ static int op_len(const uint32_t *w, int left) {
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;
@@ -105,12 +112,15 @@ static int op_len(const uint32_t *w, int left) {
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;
if (t > 0xA || (t == 5 && ((f >> 12) & 0xF) > 1) || (t == 0xA && ((f >> 8) & 0xF) > 5)) {
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) {
width != 8) { // C0 too (t -1)
snprintf(buf, n, "opcode %08X has a bad width", f);
return buf;
}
@@ -227,7 +237,9 @@ static int compare(int cond, uint64_t a, uint64_t b) {
}
// 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.
// 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) {
@@ -236,17 +248,41 @@ static int skip(const uint32_t *w, int n, int i, int else_ok) {
uint32_t f = w[i];
int t = f >> 28;
i += len;
if (t == 1 || t == 8) depth++;
else if (t == 2 && ((f >> 24) & 0xF) == 0 && depth-- == 0) return i;
else if (t == 2 && depth == 0 && else_ok) return i;
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};
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;
@@ -323,6 +359,44 @@ static void run(const Cheat *c, uint64_t keys) {
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;
}
+312
View File
@@ -0,0 +1,312 @@
"""The data cheat VM of runtime/cheats.c and the opcode tables of tools/cheats.py, without game files.
tools/cheats.py: opcode lengths and which opcodes the VM runs (vm_ok), and how a whole cheat is classified.
runtime/cheats.c, compiled into a harness with stubs for the rest of the runtime: the same cheats are classified and
measured opcode by opcode (op_len must give the lengths tools/cheats.py gives), then run on a fake main image and heap:
opcode C0 with every operand type, width and condition, its else and the skip over a failed block (nested, and with
operand words that look like 20000000), and C1-C3.
Run with .venv: python tests/cheats_vm_check.py [--cc <clang.exe>]
"""
import argparse
import os
import subprocess
import sys
import tempfile
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / 'tools'))
import cheats # noqa: E402
# Issue #973: two cheats for 1.1.0 whose only opcode the VM lacked was C046B400 FFFFFFFF (if r[B] as u32 != -1).
REPORTED = '''
[Zonai Devices 98]
580F0000 04646C60
580F1000 00000138
580F1000 000011A8
300C0000 0000003C
989BF000
540B1000 00000000
C046B400 FFFFFFFF
640F1000 00000000 00000062
21000000
780F0000 00000008
20000000
310C0000
[All Materials Collected 98]
580F0000 04646C60
580F1000 00000138
580F1000 00000F28
300C0000 000001FE
989BF000
540B1000 00000000
C046B400 FFFFFFFF
640F1000 00000000 00000062
21000000
780F0000 00000008
20000000
310C0000
'''
# (words, kind, note substring): every C0 operand type and width, C1-C3, and what stays unsupported.
CLASSIFY = [
('C0150010 00000010', 'data', ''), # X=0 memory + offset, 1 byte
('C025011B', 'data', ''), # X=1 memory + offset register, 2 bytes
('C04512B0 00000010', 'data', ''), # X=2 register + offset
('C08513BC', 'data', ''), # X=3 register + offset register
('C0151400 00000005', 'data', ''), # X=4 static, 1 byte: one value word
('C0851400 00000001 00000002', 'data', ''), # X=4 static, 8 bytes: two value words
('C04515B0', 'data', ''), # X=5 other register
('C1030A10', 'data', ''),
('C2100C00', 'data', ''),
('C3000801', 'data', ''),
('C0451600', 'unsupported', 'not supported'), # X=6 is no operand type
('C0351400 00000005', 'unsupported', 'bad width'),
('C4000000', 'unsupported', 'not supported'),
('FF000000', 'unsupported', 'not supported'),
('FF100000', 'unsupported', 'not supported'),
]
class StubMain:
"""No code in main: tools/cheats.py code_writes sees no instruction writes."""
def in_code(self, off):
return False
def py_classify(words):
c = cheats.Cheat('t', False, 1)
c.words = words
cheats.classify(StubMain(), c)
return c.kind, c.note
def words_of(text):
return [int(w, 16) for w in text.split()]
def check_python():
for c in cheats.parse(REPORTED, 'mod'):
kind, note = py_classify(c.words)
assert (kind, note) == ('data', ''), f'tools/cheats.py: {c.name}: {kind} {note}'
for text, kind, note in CLASSIFY:
got = py_classify(words_of(text))
assert got[0] == kind and note in got[1], f'tools/cheats.py: {text}: {got}, want {kind} {note}'
if kind == 'data':
assert [len(w) for _, w in cheats.ops(words_of(text))] == [len(words_of(text))], text
print('cheats_vm_check: tools/cheats.py ok', flush=True)
HARNESS = r'''
#include <assert.h>
#include "cheats.c"
// ---- the rest of the runtime, as far as cheats.c reaches it
static uint8_t heap[0x1000] __attribute__((aligned(4096)));
static uint8_t image[0x1000] __attribute__((aligned(4096)));
const char *cfg_get(const char *name) { (void)name; return NULL; }
const char *cfg_value(const char *name) { (void)name; return ""; }
void cfg_set(const char *name, const char *value) { (void)name; (void)value; }
const RtConfig *rt_config(void) { static RtConfig c; return &c; }
const RtGuestLayout *mem_layout(void) { static RtGuestLayout l; l.heap.base = (uintptr_t)heap; return &l; }
uint32_t mem_query(uint64_t a, RtMemoryInfo *out) {
memset(out, 0, sizeof *out);
if (a < (uintptr_t)heap || a >= (uintptr_t)heap + sizeof heap) return 1;
out->base = (uintptr_t)heap, out->size = sizeof heap, out->state = 1, out->perm = 3;
return 0;
}
void rt_fatal(const char *fmt, ...) { (void)fmt; abort(); }
void rt_log(const char *fmt, ...) { (void)fmt; }
void prof_write(void) {}
int ldr_count(void) { return 0; }
const RtLoaded *ldr_module(int i) { (void)i; return NULL; }
int rt_patch_set(const char *name) { (void)name; return -1; }
int rt_patch_enabled(int set) { (void)set; return 0; }
void rt_patch_switch(int set, int on) { (void)set; (void)on; }
void input_poll(uint64_t *b, int32_t *lx, int32_t *ly, int32_t *rx, int32_t *ry) { *b = 0, *lx = *ly = *rx = *ry = 0; }
void mods_scan(void) {}
int mods_count(void) { return 0; }
ModInfo *mods_at(int i) { (void)i; return NULL; }
int mods_exefs_pending(uint32_t *key) { (void)key; return 0; }
int plat_exe_path(char *out, size_t cap) { (void)out; (void)cap; return 0; }
PlatFile *plat_file_open(const char *path) { (void)path; return NULL; }
void plat_file_close(PlatFile *f) { (void)f; }
int64_t plat_file_pread(PlatFile *f, void *buf, uint32_t n, uint64_t off) { (void)f; (void)buf; (void)n; (void)off; return -1; }
uint8_t *plat_file_read(const char *path, uint64_t *size) { (void)path; (void)size; return NULL; }
int plat_file_write(const char *path, const void *data, size_t n) { (void)path; (void)data; (void)n; return 0; }
int plat_file_exists(const char *path) { (void)path; return 0; }
int plat_fs_unlink(const char *path) { (void)path; return 0; }
int plat_fs_list(const char *dir, int (*cb)(const char *, int, uint64_t, void *), void *ctx) { (void)dir; (void)cb; (void)ctx; return 0; }
int plat_spawn(const char *const *argv, const char *cwd) { (void)argv; (void)cwd; return 0; }
_Noreturn void plat_exit(int code) { exit(code); }
int plat_mem_state(uint64_t addr, uint64_t limit, uint64_t *end) { (void)addr; *end = limit; return PLAT_PROT_RW; }
int plat_mem_protect(uint64_t addr, uint64_t n, int prot) { (void)addr; (void)n; (void)prot; return 1; }
#include "cases.h"
// The last cheat of text, parsed as cheats_init parses a file.
static Cheat *load(const char *text) {
char *s = strdup(text);
parse(s, "t");
free(s);
return &g_cheats[g_n - 1];
}
static uint32_t u32(const uint8_t *p) { uint32_t v; memcpy(&v, p, 4); return v; }
static uint64_t u64(const uint8_t *p) { uint64_t v; memcpy(&v, p, 8); return v; }
// Marks after the condition: 1 at heap+0x100 when it held, else 2 at heap+0x104; 3 at heap+0x108 after the end.
#define MARKS " 04100000 00000100 00000001 21000000 04100000 00000104 00000002 20000000 04100000 00000108 00000003"
// Runs setup + cond + MARKS; want: 1 the condition holds, 0 it fails.
static void cond(const char *setup, const char *op, int want) {
char text[1024];
snprintf(text, sizeof text, "[c]\n%s %s%s\n", setup, op, MARKS);
memset(heap + 0x100, 0, 0x10);
run(load(text), 0);
uint32_t held = u32(heap + 0x100), other = u32(heap + 0x104), end = u32(heap + 0x108);
if (held != (want ? 1u : 0u) || other != (want ? 0u : 2u) || end != 3) {
fprintf(stderr, "%s %s: marks %u %u %u, want %d\n", setup, op, held, other, end, want);
abort();
}
}
int main(void) {
g_main = (uintptr_t)image, g_main_size = sizeof image, g_code_end = g_main; // no code: nothing is a code cheat
for (int k = 0; k < (int)(sizeof cases / sizeof *cases); k++) {
Cheat *c = load(cases[k].text);
CheatKind kind = classify(c);
if (kind != cases[k].kind || !strstr(c->note, cases[k].note)) {
fprintf(stderr, "classify %s: %d (%s), want %d (%s)\n", cases[k].text, kind, c->note, cases[k].kind, cases[k].note);
abort();
}
for (int i = 0, j = 0; kind == CHEAT_DATA && i < c->n; j++) {
int len = op_len(c->words + i, c->n - i);
if (len != cases[k].lens[j]) {
fprintf(stderr, "op_len %s, opcode %d: %d, tools/cheats.py %d\n", cases[k].text, j, len, cases[k].lens[j]);
abort();
}
i += len;
}
}
char hb[64], hb10[64]; // r[B] = the heap's address, r[B] = the heap + 0x10
snprintf(hb, sizeof hb, "400B0000 %08X %08X", (unsigned)((uintptr_t)heap >> 32), (unsigned)(uintptr_t)heap);
snprintf(hb10, sizeof hb10, "400B0000 %08X %08X", (unsigned)((uintptr_t)heap >> 32), (unsigned)((uintptr_t)heap + 0x10));
uint64_t v = 0x1122334455667705ull;
memcpy(heap + 0x10, &v, 8); // heap+0x10: 05 77 66 55 44 33 22 11
memcpy(image + 0x20, &v, 8);
// X=0 memory + offset: heap (M=1) and main (M=0), each width
cond("400A0000 00000000 00000005", "C015A010 00000010", 1);
cond("400A0000 00000000 00000005", "C016A010 00000010", 0);
cond("400A0000 00000000 00007705", "C025A010 00000010", 1);
cond("400A0000 00000000 55667705", "C045A000 00000020", 1);
cond("400A0000 11223344 55667705", "C085A000 00000020", 1);
cond("400A0000 11223344 55667706", "C085A000 00000020", 0);
// X=1 memory + offset register
cond("400A0000 00000000 55667705 400C0000 00000000 00000010", "C045A11C", 1);
cond("400A0000 00000000 55667705 400C0000 00000000 00000014", "C045A11C", 0);
// X=2 register + offset, X=3 register + offset register
char setup[256];
snprintf(setup, sizeof setup, "400A0000 00000000 55667705 %s", hb);
cond(setup, "C045A2B0 00000010", 1);
cond(setup, "C045A2B0 00000011", 0);
snprintf(setup, sizeof setup, "400A0000 00000000 55667705 %s 400C0000 00000000 00000010", hb);
cond(setup, "C045A3BC", 1);
snprintf(setup, sizeof setup, "400A0000 00000000 00000005 %s 400C0000 00000000 00000010", hb10);
cond(setup, "C015A3BC", 0); // heap+0x20 holds 0
// an address the guest cannot read compares as 0, as dmnt's read leaves its zeroed value
cond("400A0000 00000000 00000000 400B0000 00000000 00000000", "C045A2B0 00000000", 1);
cond("400A0000 00000000 00000001 400B0000 00000000 00000000", "C045A2B0 00000000", 0);
// X=4 static: the register and the value cut to the width
cond("400A0000 FFFFFFFF FFFFFF05", "C015A400 00000005", 1);
cond("400A0000 FFFFFFFF FFFFFF05", "C015A400 12345605", 1);
cond("400A0000 FFFFFFFF FFFF7705", "C025A400 00007705", 1);
cond("400A0000 FFFFFFFF 55667705", "C045A400 55667705", 1);
cond("400A0000 00000001 00000002", "C085A400 00000001 00000002", 1);
cond("400A0000 00000001 00000002", "C085A400 00000000 00000002", 0);
// X=5 other register, cut to the width
cond("400A0000 00000000 00000005 400B0000 00000001 00000005", "C045A5B0", 1);
cond("400A0000 00000000 00000005 400B0000 00000001 00000005", "C085A5B0", 0);
// the six conditions, register 5 against 4, 5 and 6; condition 0 or 7 never holds
static const int want[8][3] = {{0, 0, 0}, {1, 0, 0}, {1, 1, 0}, {0, 0, 1}, {0, 1, 1}, {0, 1, 0}, {1, 0, 1}, {0, 0, 0}};
for (int c = 0; c < 8; c++)
for (int k = 0; k < 3; k++) {
char op[32];
snprintf(op, sizeof op, "C04%XA400 %08X", c, 4 + k);
cond("400A0000 00000000 00000005", op, want[c][k]);
}
// a failed block skips a nested C0 with its else and end, and C0 operand words that read 20000000 / 21000000
cond("400A0000 00000000 00000005",
"C046A400 00000005 C045A400 00000005 04100000 00000100 00000009 21000000 20000000"
" C085A400 21000000 20000000 20000000 C045A010 20000000 20000000 C045A2B0 20000000 20000000", 0);
cond("400A0000 00000000 00000005", "C045A400 00000005 C046A400 00000005 21000000 20000000", 1);
// C1: save r[A] in slot 3, clear r[A], restore slot 3 into r[B]; clear slot 3 and restore it into r[C]
load("[s]\n400A0000 00000000 0000002A C1030A10 C10A0030 C10B0300 C1030020 C10C0300"
" A4A00410 00000110 A4B00410 00000114 A4C00410 00000118\n");
memcpy(heap + 0x110, "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF", 12);
run(&g_cheats[g_n - 1], 0);
assert(u32(heap + 0x110) == 0 && u32(heap + 0x114) == 0x2A && u32(heap + 0x118) == 0);
// C2: save A and B, clear A, restore A only; clear the saved B, restore B
load("[m]\n400A0000 00000000 00000007 400B0000 00000000 00000008 C2100C00 C2300C00 C2000400 C2200800 C2000800"
" A4A00410 00000110 A4B00410 00000114\n");
memset(heap + 0x110, 0xFF, 8);
run(&g_cheats[g_n - 1], 0);
assert(u32(heap + 0x110) == 7 && u32(heap + 0x114) == 0);
// C3: r[1] into static register 0x80, which stays across runs; static register 0x05 (none sets it) into r[2]
load("[st]\n40010000 00000000 00000033 40020000 00000000 00000044 C3000801 C3000052 A8200410 00000120\n");
memset(heap + 0x120, 0xFF, 8);
run(&g_cheats[g_n - 1], 0);
assert(g_static[0x80] == 0x33 && u64(heap + 0x120) == 0);
// saved registers start at 0 in each run, like the registers
load("[z]\nC10A0300 A8A00410 00000128\n");
memset(heap + 0x128, 0xFF, 8);
run(&g_cheats[g_n - 1], 0);
assert(u64(heap + 0x128) == 0);
puts("cheats_vm_check: runtime/cheats.c ok");
return 0;
}
'''
KIND = {'data': 'CHEAT_DATA', 'unsupported': 'CHEAT_UNSUPPORTED'}
def cases_h():
"""The C table: every classify case and each reported cheat, with tools/cheats.py's kind and opcode lengths."""
rows = [(' '.join(f'{w:08X}' for w in c.words), 'data', '') for c in cheats.parse(REPORTED, 'mod')] + CLASSIFY
out = ['static const struct { const char *text; CheatKind kind; const char *note; int lens[64]; } cases[] = {']
for text, kind, note in rows:
try:
lens = [len(w) for _, w in cheats.ops(words_of(text))]
except ValueError:
lens = []
out.append(f' {{"[t]\\n{text}\\n", {KIND[kind]}, "{note}", {{{", ".join(map(str, lens)) or "0"}}}}},')
return '\n'.join(out + ['};', ''])
def check_c(cc):
with tempfile.TemporaryDirectory(prefix='totk-cheats-') as tmp:
folder = Path(tmp)
(folder / 'check.c').write_text(HARNESS)
(folder / 'cases.h').write_text(cases_h())
exe = folder / ('check.exe' if os.name == 'nt' else 'check')
subprocess.run([cc, '-O1', '-g', '-Wall', '-Wextra', '-Wno-unused-function', '-I', str(folder),
'-I', str(ROOT / 'runtime'), str(folder / 'check.c'), '-o', str(exe)], check=True)
subprocess.run([str(exe)], check=True)
def main():
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
ap.add_argument('--cc', default='clang')
args = ap.parse_args()
check_python()
check_c(args.cc)
if __name__ == '__main__':
main()
+7 -2
View File
@@ -134,6 +134,9 @@ def ops(words):
else:
t = (w >> 24) & 0xFF if t == 0xC else (w >> 20) & 0xFFF
n = 1
if t == 0xC0: # C0TcSX..: an address word for operand types 0 and 2, the value (two at width 8) for 4
x = (w >> 8) & 0xF
n = 2 if x in (0, 2) else (3 if (w >> 20) & 0xF == 8 else 2) if x == 4 else 1
if i + n > len(words):
raise ValueError(f'opcode {w:08X} is cut short')
out.append((t, words[i:i + n]))
@@ -141,7 +144,7 @@ def ops(words):
return out
VM_TYPES = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xA}
VM_TYPES = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xA, 0xC0, 0xC1, 0xC2, 0xC3}
def vm_ok(o):
@@ -151,10 +154,12 @@ def vm_ok(o):
return f'opcode {w[0]:08X} is not supported'
if t == 5 and (w[0] >> 12) & 0xF > 1:
return f'opcode {w[0]:08X} (load from a register offset) is not supported'
if t == 0xA and (w[0] >> 8) & 0xF > 5:
if t in (0xA, 0xC0) and (w[0] >> 8) & 0xF > 5:
return f'opcode {w[0]:08X} is not supported'
if t in (0, 1, 5, 6, 7, 9, 0xA) and (w[0] >> 24) & 0xF not in (1, 2, 4, 8):
return f'opcode {w[0]:08X} has a bad width'
if t == 0xC0 and (w[0] >> 20) & 0xF not in (1, 2, 4, 8):
return f'opcode {w[0]:08X} has a bad width'
return None