mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
The Teleport tab lists every area of the game with its named start positions, read from the player's own game files: the warp songs' pads, both sides of the loading zones and the boss rooms among them. A click changes area the way the game's own doors do: during a short event of the game that only waits, the menu reserves the next area and start position in the game's data and asks the game's sequence for the change, so the area unloads and loads as in play, a first visit's cutscene plays and a boss room starts its fight. The Cutscenes tab lists the game's event scripts by name and starts one through the game's event manager. Tested from Hyrule Field to Kakariko Village, the Forest Temple, Kokiri Forest in the child's time, the bottom of Lake Hylia, Volvagia's room, and the pads of two warp songs, four of them in one run, with Link walking afterwards; the Kakariko arrival cutscene started by name. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
728 lines
34 KiB
C
728 lines
34 KiB
C
// The debug menu's hands in the game (ui/debug_menu.cpp is its window, F9 with debug_menu = 1). Ocarina of Time 1.1.1.
|
|
//
|
|
// Reads go straight to the game's memory from the menu's thread, through plat_mem_read, which answers 0 instead of
|
|
// faulting. Changes are queued and made on the game's own thread at its pad read (dbg_tick, from ov_input.c), between
|
|
// two of the game's frames: a flag through the game's own function, so everything that follows a flag in the game
|
|
// follows this change too; a memory value by a plain write into memory the game may read and write.
|
|
//
|
|
// The game's on/off flags ("game data"): the manager is **[main+0x4c7e180]; +0x10 the number of flags, +0x18 their
|
|
// entries (0x18 bytes each, the value's byte at +0x16), +0x20 a table of {hash, index} pairs probed from hash % size
|
|
// (+0x2c the size). main+0x20d9c0 (manager, u8 *out, hash) reads a flag and main+0x682b20 (manager, value, hash)
|
|
// queues a change, which the manager applies at its next update. A flag is known to the game by the MurmurHash3 of
|
|
// its name only; the names shown are those found in the player's own game files (the event flows), hashed here and
|
|
// matched (dbg_names), so nothing of them is kept in this code.
|
|
//
|
|
// The game's speed: the runtime gives the game its step every frame (frame_rate.c ocarina_step_write, the display
|
|
// interval); the menu's factor multiplies that step, so 0.5 is half speed for the whole game and its interface.
|
|
//
|
|
// Link's position is shown but not written: it is the actor's copy ([[main+0x4e371d0]+0x480]+0x31c), which the physics
|
|
// writes every frame. Writing it, or every copy of it in the heap, moved Link back within three frames.
|
|
//
|
|
// Teleporting is the game's own change of area, as a loading zone, a warp song or a cutscene makes it: the game's
|
|
// event script reserves the next area and the name of a start position in it (game data texts), and asks the
|
|
// sequence that runs during events to change area (the enum Sequence_ControlInEvent = ChangeBancOnEvent). warp_tick
|
|
// does the same during a short event of the game that only waits (DefaultParentEvent, 30 frames), so the game unloads
|
|
// the area, loads the next one and places Link exactly as it does for its own doors. The places offered are the ones
|
|
// in the player's game files (Banc/BancBasedResource/StartPos.byml.zs: each area with its named start positions,
|
|
// the warp songs' pads and both sides of every loading zone among them); none of that list is kept in this code.
|
|
//
|
|
// An event (a cutscene, a conversation, a scripted moment) is started by its file's name through the game's event
|
|
// manager, as the game's own actors start them.
|
|
#include <math.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <zstd.h>
|
|
|
|
#include "cfg.h"
|
|
#include "cpu.h"
|
|
#include "debug_tools.h"
|
|
#include "fs.h"
|
|
#include "host.h"
|
|
#include "layout.h"
|
|
#include "platform/platform.h"
|
|
|
|
int dbg_supported(void) { return RT_V111; }
|
|
|
|
static int rd(uint64_t a, void *out, size_t n) { return a && plat_mem_read(a, out, n); }
|
|
static uint64_t rd_ptr(uint64_t a) {
|
|
uint64_t p = 0;
|
|
return rd(a, &p, 8) ? p : 0;
|
|
}
|
|
// A write into memory the game may read and write: 0 when the place is not such memory.
|
|
static int wr(uint64_t a, const void *from, size_t n) {
|
|
RtMemoryInfo mi;
|
|
if (!a || mem_query(a, &mi) || !mi.state || (mi.perm & 3) != 3 || a + n > mi.base + mi.size) return 0;
|
|
mem_thaw(a, n);
|
|
memcpy((void *)(uintptr_t)a, from, n);
|
|
return 1;
|
|
}
|
|
|
|
// ---- the player and the game's speed
|
|
static uint64_t player(void) {
|
|
uint64_t manager = rd_ptr(MOD_MAIN_BASE + 0x4e371d0), actor = manager ? rd_ptr(manager + 0x480) : 0;
|
|
char name[7];
|
|
uint64_t np = actor ? rd_ptr(actor + 0x20) : 0;
|
|
return np && rd(np, name, 7) && !memcmp(name, "Player", 7) ? actor : 0;
|
|
}
|
|
|
|
int dbg_player_pos(float xyz[3]) {
|
|
uint64_t a = dbg_supported() ? player() : 0;
|
|
return a && rd(a + 0x31c, xyz, 12) && isfinite(xyz[0]) && isfinite(xyz[1]) && isfinite(xyz[2]);
|
|
}
|
|
|
|
static float speed_factor = 1.0f;
|
|
float dbg_speed_factor(void) { return speed_factor; }
|
|
void dbg_speed_set(float factor) { speed_factor = factor >= 0.05f && factor <= 8.0f ? factor : 1.0f; }
|
|
|
|
// ---- what the menu asks for, done by dbg_tick
|
|
enum { Q_FLAG, Q_WRITE, Q_MAX = 256, FREEZE_MAX = 64 };
|
|
typedef struct { int kind; uint32_t hash; uint64_t addr, value; int size; } Req;
|
|
static PlatMutex q_lock = PLAT_MUTEX_INIT;
|
|
static Req q[Q_MAX];
|
|
static int q_n;
|
|
static struct { uint64_t addr, value; int size; } frozen[FREEZE_MAX];
|
|
static int frozen_n;
|
|
static uint64_t stat_flags, stat_writes, stat_refused;
|
|
|
|
static void ask(Req r) {
|
|
plat_mutex_lock(&q_lock);
|
|
if (q_n < Q_MAX) q[q_n++] = r;
|
|
plat_mutex_unlock(&q_lock);
|
|
}
|
|
|
|
void dbg_flag_set(uint32_t hash, int on) { ask((Req){Q_FLAG, hash, 0, (uint64_t)(on != 0), 0}); }
|
|
void dbg_mem_write(uint64_t addr, uint64_t value, int size) {
|
|
if (addr) ask((Req){Q_WRITE, 0, addr, value, size});
|
|
}
|
|
|
|
int dbg_freeze(uint64_t addr, uint64_t value, int size, int on) {
|
|
if (!addr) return 0;
|
|
int ok = 1;
|
|
plat_mutex_lock(&q_lock);
|
|
int at = -1;
|
|
for (int i = 0; i < frozen_n; i++)
|
|
if (frozen[i].addr == addr) at = i;
|
|
if (on && at < 0 && frozen_n < FREEZE_MAX) at = frozen_n++;
|
|
if (on && at >= 0) frozen[at].addr = addr, frozen[at].value = value, frozen[at].size = size;
|
|
else if (on) ok = 0;
|
|
else if (at >= 0) frozen[at] = frozen[--frozen_n];
|
|
plat_mutex_unlock(&q_lock);
|
|
return ok;
|
|
}
|
|
|
|
int dbg_frozen(uint64_t addr) {
|
|
int is = 0;
|
|
plat_mutex_lock(&q_lock);
|
|
for (int i = 0; i < frozen_n; i++) is |= frozen[i].addr == addr;
|
|
plat_mutex_unlock(&q_lock);
|
|
return is;
|
|
}
|
|
|
|
int dbg_frozen_count(void) { return frozen_n; }
|
|
|
|
void dbg_unfreeze_all(void) {
|
|
plat_mutex_lock(&q_lock);
|
|
frozen_n = 0;
|
|
plat_mutex_unlock(&q_lock);
|
|
}
|
|
|
|
// ---- the flags
|
|
static uint64_t gmd(void) {
|
|
uint64_t slot = rd_ptr(MOD_MAIN_BASE + 0x4c7e180);
|
|
return slot ? rd_ptr(slot) : 0;
|
|
}
|
|
|
|
typedef struct { uint32_t hash, index; } Pair;
|
|
static Pair *flag_pairs; // the flags by hash, sorted; made once the game's table is there
|
|
static const char **flag_name; // parallel: the name found for the hash, or NULL
|
|
static uint8_t *flag_value; // parallel: the value at the last dbg_flags_refresh
|
|
static int flag_n;
|
|
static volatile int32_t names_state; // 0 not asked, 1 being read, 2 read
|
|
static int names_found;
|
|
|
|
static int by_hash(const void *a, const void *b) {
|
|
uint32_t x = ((const Pair *)a)->hash, y = ((const Pair *)b)->hash;
|
|
return x < y ? -1 : x > y;
|
|
}
|
|
|
|
// The list of the game's flags from its hash table. 0 until the manager exists (a save is loaded).
|
|
static int flags_list(void) {
|
|
if (flag_pairs) return 1;
|
|
uint64_t m = gmd(), table = m ? rd_ptr(m + 0x20) : 0;
|
|
uint32_t count = 0, size = 0;
|
|
if (!table || !rd(m + 0x10, &count, 4) || !rd(m + 0x2c, &size, 4) || !count || count > 200000 || size < count ||
|
|
size > 1000000)
|
|
return 0;
|
|
Pair *slots = malloc(sizeof(Pair) * size), *list = malloc(sizeof(Pair) * count);
|
|
int n = 0;
|
|
if (slots && list && rd(table, slots, sizeof(Pair) * size))
|
|
for (uint32_t i = 0; i < size && n < (int)count; i++)
|
|
if (slots[i].hash && slots[i].index < count) list[n++] = slots[i];
|
|
free(slots);
|
|
if (!list || !n) {
|
|
free(list);
|
|
return 0;
|
|
}
|
|
qsort(list, (size_t)n, sizeof(Pair), by_hash);
|
|
flag_name = calloc((size_t)n, sizeof *flag_name);
|
|
flag_value = calloc((size_t)n, 1);
|
|
if (!flag_name || !flag_value) {
|
|
free(list);
|
|
return 0;
|
|
}
|
|
flag_n = n;
|
|
flag_pairs = list;
|
|
rt_log("debug: %d of the game's %u on/off flags listed", n, count);
|
|
return 1;
|
|
}
|
|
|
|
int dbg_flags_refresh(void) {
|
|
if (!dbg_supported() || !flags_list()) return 0;
|
|
uint64_t m = gmd(), entries = m ? rd_ptr(m + 0x18) : 0;
|
|
uint32_t count = 0;
|
|
if (!entries || !rd(m + 0x10, &count, 4) || !count || count > 200000) return flag_n;
|
|
uint8_t *raw = malloc((size_t)count * 0x18);
|
|
if (raw && rd(entries, raw, (size_t)count * 0x18))
|
|
for (int i = 0; i < flag_n; i++)
|
|
if (flag_pairs[i].index < count) flag_value[i] = raw[(size_t)flag_pairs[i].index * 0x18 + 0x16] & 1;
|
|
free(raw);
|
|
return flag_n;
|
|
}
|
|
|
|
uint32_t dbg_flag_hash(int i) { return i >= 0 && i < flag_n ? flag_pairs[i].hash : 0; }
|
|
int dbg_flag_value(int i) { return i >= 0 && i < flag_n ? flag_value[i] : 0; }
|
|
const char *dbg_flag_name(int i) {
|
|
return i >= 0 && i < flag_n && __atomic_load_n(&names_state, __ATOMIC_ACQUIRE) == 2 ? flag_name[i] : NULL;
|
|
}
|
|
|
|
// ---- the flags' names, from the player's game files
|
|
static uint32_t murmur3(const uint8_t *p, size_t len) {
|
|
uint32_t h = 0;
|
|
size_t n = len & ~(size_t)3;
|
|
for (size_t i = 0; i < n; i += 4) {
|
|
uint32_t k;
|
|
memcpy(&k, p + i, 4);
|
|
k *= 0xcc9e2d51u, k = (k << 15) | (k >> 17), k *= 0x1b873593u;
|
|
h ^= k, h = (h << 13) | (h >> 19), h = h * 5 + 0xe6546b64u;
|
|
}
|
|
uint32_t k = 0;
|
|
switch (len & 3) {
|
|
case 3: k ^= (uint32_t)p[n + 2] << 16; // fall through
|
|
case 2: k ^= (uint32_t)p[n + 1] << 8; // fall through
|
|
case 1:
|
|
k ^= p[n];
|
|
k *= 0xcc9e2d51u, k = (k << 15) | (k >> 17), k *= 0x1b873593u;
|
|
h ^= k;
|
|
}
|
|
h ^= (uint32_t)len;
|
|
h ^= h >> 16, h *= 0x85ebca6bu, h ^= h >> 13, h *= 0xc2b2ae35u, h ^= h >> 16;
|
|
return h;
|
|
}
|
|
|
|
static int name_char(uint8_t c) {
|
|
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_' || c == '.';
|
|
}
|
|
|
|
// Every run of name characters in a file is hashed and looked up among the flags.
|
|
static void names_in(const uint8_t *p, size_t n) {
|
|
for (size_t i = 0; i < n;) {
|
|
if (!name_char(p[i])) {
|
|
i++;
|
|
continue;
|
|
}
|
|
size_t e = i;
|
|
while (e < n && name_char(p[e])) e++;
|
|
if (e - i >= 4 && e - i <= 96) {
|
|
Pair key = {murmur3(p + i, e - i), 0};
|
|
Pair *hit = bsearch(&key, flag_pairs, (size_t)flag_n, sizeof(Pair), by_hash);
|
|
if (hit && !flag_name[hit - flag_pairs]) {
|
|
char *s = malloc(e - i + 1);
|
|
if (s) {
|
|
memcpy(s, p + i, e - i);
|
|
s[e - i] = 0;
|
|
flag_name[hit - flag_pairs] = s;
|
|
names_found++;
|
|
}
|
|
}
|
|
}
|
|
i = e;
|
|
}
|
|
}
|
|
|
|
// A member of a SARC archive: its place and size, 0 when it is not there.
|
|
static int sarc_member(const uint8_t *p, size_t n, const char *name, size_t *at, size_t *size) {
|
|
if (n < 0x20 || memcmp(p, "SARC", 4)) return 0;
|
|
uint16_t head, count;
|
|
uint32_t data;
|
|
memcpy(&head, p + 4, 2);
|
|
memcpy(&data, p + 12, 4);
|
|
if ((size_t)head + 12 > n || memcmp(p + head, "SFAT", 4)) return 0;
|
|
memcpy(&count, p + head + 6, 2);
|
|
size_t names = (size_t)head + 12 + (size_t)count * 16 + 8, len = strlen(name);
|
|
for (unsigned i = 0; i < count; i++) {
|
|
uint32_t attr, lo, hi;
|
|
size_t e = (size_t)head + 12 + (size_t)i * 16;
|
|
if (e + 16 > n) return 0;
|
|
memcpy(&attr, p + e + 4, 4), memcpy(&lo, p + e + 8, 4), memcpy(&hi, p + e + 12, 4);
|
|
size_t s = names + (size_t)(attr & 0xffffff) * 4;
|
|
if (s + len + 1 > n || memcmp(p + s, name, len + 1)) continue;
|
|
if (lo > hi || (size_t)data + hi > n) return 0;
|
|
*at = data + lo, *size = hi - lo;
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// ---- the places to teleport to and the events, from the player's game files (names_thread)
|
|
typedef struct { const char *area, *pos; } Place;
|
|
static Place *places; // by area, as the game's file lists them; the texts stay in the file's buffer
|
|
static int place_n;
|
|
static char **event_names; // the event files that hold a script, by name
|
|
static int event_n;
|
|
|
|
static uint32_t u24(const uint8_t *p) { return p[0] | (uint32_t)p[1] << 8 | (uint32_t)p[2] << 16; }
|
|
|
|
// The i-th key of a BYML file's key table (a node 0xc2: its count, then the offsets of its texts).
|
|
static const char *byml_key(const uint8_t *p, size_t n, uint32_t keys, uint32_t i) {
|
|
uint32_t off;
|
|
if ((size_t)keys + 8 > n || p[keys] != 0xc2 || i >= u24(p + keys + 1) || (size_t)keys + 8 + 4 * (size_t)i > n) return NULL;
|
|
memcpy(&off, p + keys + 4 + 4 * (size_t)i, 4);
|
|
if ((size_t)keys + off >= n || !memchr(p + keys + off, 0, n - keys - off)) return NULL;
|
|
return (const char *)p + keys + off;
|
|
}
|
|
|
|
// The start positions file: a dictionary (node 0xc1: its count, then entries of a key's number in 24 bits, the
|
|
// value's kind and the value's offset) of areas, each a dictionary of its start positions.
|
|
static void places_in(const uint8_t *p, size_t n) {
|
|
uint32_t keys, root;
|
|
if (n < 16 || memcmp(p, "YB", 2)) return;
|
|
memcpy(&keys, p + 4, 4), memcpy(&root, p + 12, 4);
|
|
if ((size_t)root + 4 > n || p[root] != 0xc1) return;
|
|
uint32_t areas = u24(p + root + 1);
|
|
Place *list = malloc(sizeof(Place) * 4096);
|
|
int count = 0;
|
|
for (uint32_t i = 0; list && i < areas && (size_t)root + 12 + 8 * (size_t)i <= n; i++) {
|
|
const uint8_t *e = p + root + 4 + 8 * (size_t)i;
|
|
const char *area = byml_key(p, n, keys, u24(e));
|
|
uint32_t at;
|
|
memcpy(&at, e + 4, 4);
|
|
if (!area || e[3] != 0xc1 || (size_t)at + 4 > n || p[at] != 0xc1 || strlen(area) > 63) continue;
|
|
uint32_t spots = u24(p + at + 1);
|
|
for (uint32_t k = 0; k < spots && count < 4096 && (size_t)at + 12 + 8 * (size_t)k <= n; k++) {
|
|
const char *pos = byml_key(p, n, keys, u24(p + at + 4 + 8 * (size_t)k));
|
|
if (pos && strlen(pos) <= 63) list[count++] = (Place){area, pos};
|
|
}
|
|
}
|
|
places = list, place_n = count;
|
|
}
|
|
|
|
static void names_thread(void *arg) {
|
|
(void)arg;
|
|
uint64_t t0 = plat_now_ns(), dn = 0;
|
|
uint8_t *dz = romfs_load("Pack/ZsDic.pack.zs", &dn), *dics = NULL;
|
|
ZSTD_DDict *dd = NULL;
|
|
ZSTD_DCtx *dctx = ZSTD_createDCtx();
|
|
unsigned long long dsize = dz ? ZSTD_getFrameContentSize(dz, (size_t)dn) : 0;
|
|
size_t at = 0, size = 0;
|
|
int files = 0;
|
|
if (dz && dsize && dsize < (64u << 20) && (dics = malloc((size_t)dsize)) &&
|
|
ZSTD_decompress(dics, (size_t)dsize, dz, (size_t)dn) == dsize && sarc_member(dics, (size_t)dsize, "zs.zsdic", &at, &size))
|
|
dd = ZSTD_createDDict(dics + at, size);
|
|
if (dd && dctx) {
|
|
uint64_t count = romfs_file_count();
|
|
for (uint64_t i = 0; i < count; i++) {
|
|
uint64_t off, fsize;
|
|
const char *path = romfs_entry(i, &off, &fsize);
|
|
size_t len = path ? strlen(path) : 0;
|
|
if (!path || strncmp(path, "Event/EventFlow/", 16) || len < 4 || strcmp(path + len - 3, ".zs")) continue;
|
|
uint8_t *z = romfs_load(path, &fsize);
|
|
unsigned long long rn = z ? ZSTD_getFrameContentSize(z, (size_t)fsize) : 0;
|
|
uint8_t *raw = z && rn && rn < (64u << 20) ? malloc((size_t)rn) : NULL;
|
|
if (raw && ZSTD_decompress_usingDDict(dctx, raw, (size_t)rn, z, (size_t)fsize, dd) == rn) {
|
|
names_in(raw, (size_t)rn);
|
|
files++;
|
|
// an event file that holds a script (the count at 0x20) can be started by its name
|
|
uint16_t scripts = 0;
|
|
if (rn > 0x22 && !memcmp(raw, "BFEVFL", 6)) memcpy(&scripts, raw + 0x20, 2);
|
|
const char *dot = strchr(path + 16, '.');
|
|
if (scripts && dot && dot - (path + 16) < 80 && (event_names || (event_names = calloc((size_t)count, sizeof *event_names)))) {
|
|
char *name = calloc(1, (size_t)(dot - (path + 16)) + 1);
|
|
if (name) memcpy(name, path + 16, (size_t)(dot - (path + 16))), event_names[event_n++] = name;
|
|
}
|
|
}
|
|
free(raw);
|
|
free(z);
|
|
}
|
|
uint64_t zn = 0;
|
|
uint8_t *z = romfs_load("Banc/BancBasedResource/StartPos.byml.zs", &zn);
|
|
unsigned long long rn = z ? ZSTD_getFrameContentSize(z, (size_t)zn) : 0;
|
|
uint8_t *raw = z && rn && rn < (16u << 20) ? malloc((size_t)rn) : NULL; // kept: the places' texts are in it
|
|
if (raw && ZSTD_decompress_usingDDict(dctx, raw, (size_t)rn, z, (size_t)zn, dd) == rn) places_in(raw, (size_t)rn);
|
|
free(z);
|
|
}
|
|
ZSTD_freeDCtx(dctx);
|
|
ZSTD_freeDDict(dd);
|
|
free(dics);
|
|
free(dz);
|
|
rt_log("debug: names for %d of %d flags, from %d of the game's event files, in %.0f ms; %d events, %d places to teleport to",
|
|
names_found, flag_n, files, (double)(plat_now_ns() - t0) / 1e6, event_n, place_n);
|
|
__atomic_store_n(&names_state, 2, __ATOMIC_RELEASE);
|
|
}
|
|
|
|
// 0 not started (no flags yet), 1 being read, 2 there. Starts the reading once the flags are listed.
|
|
int dbg_names(int *found) {
|
|
int s = __atomic_load_n(&names_state, __ATOMIC_ACQUIRE);
|
|
if (!s && flag_pairs) {
|
|
int32_t zero = 0;
|
|
if (__atomic_compare_exchange_n(&names_state, &zero, 1, 0, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE)) {
|
|
if (!plat_thread_spawn(names_thread, NULL, "debug names", 0)) __atomic_store_n(&names_state, 2, __ATOMIC_RELEASE);
|
|
s = 1;
|
|
}
|
|
}
|
|
if (found) *found = s == 2 ? names_found : 0;
|
|
return s;
|
|
}
|
|
|
|
int dbg_places(void) { return __atomic_load_n(&names_state, __ATOMIC_ACQUIRE) == 2 ? place_n : 0; }
|
|
const char *dbg_place_area(int i) { return i >= 0 && i < dbg_places() ? places[i].area : ""; }
|
|
const char *dbg_place_pos(int i) { return i >= 0 && i < dbg_places() ? places[i].pos : ""; }
|
|
int dbg_events(void) { return __atomic_load_n(&names_state, __ATOMIC_ACQUIRE) == 2 ? event_n : 0; }
|
|
const char *dbg_event_name(int i) { return i >= 0 && i < dbg_events() ? event_names[i] : ""; }
|
|
|
|
// ---- the game data's other kinds: the manager holds a section of 0x60 bytes for each kind (the kind's number times
|
|
// 0x60: +0x10 the entries' count, +0x18 the entries, +0x20 the {hash, index} table, +0x2c its size). An entry of a
|
|
// 64-character text is 0x108 bytes, the text's pointer at +0x68 (a second one at +0x18) and its room at +0x70.
|
|
// main+0x3e0980 (manager, value, hash) queues the change of an enum, whose value is the hash of the value's name.
|
|
enum { GD_TEXT64 = 16 };
|
|
static uint32_t hash_of(const char *text) { return murmur3((const uint8_t *)text, strlen(text)); }
|
|
|
|
static uint64_t gmd_entry(int kind, uint32_t stride, uint32_t hash) {
|
|
uint64_t m = gmd(), sec = m + (uint64_t)kind * 0x60, table = m ? rd_ptr(sec + 0x20) : 0, entries = m ? rd_ptr(sec + 0x18) : 0;
|
|
uint32_t count = 0, size = 0;
|
|
if (!table || !entries || !rd(sec + 0x10, &count, 4) || !rd(sec + 0x2c, &size, 4) || !size || size > 100000) return 0;
|
|
for (uint32_t i = hash % size, n = 0; n < size; i = i + 1 < size ? i + 1 : 0, n++) {
|
|
Pair pr;
|
|
if (!rd(table + (uint64_t)i * 8, &pr, 8) || !pr.hash) return 0;
|
|
if (pr.hash == hash) return pr.index < count ? entries + (uint64_t)pr.index * stride : 0;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int text_get(const char *name, char out[64]) {
|
|
uint64_t e = gmd_entry(GD_TEXT64, 0x108, hash_of(name)), text = e ? rd_ptr(e + 0x68) : 0;
|
|
out[0] = 0;
|
|
if (!text || !rd(text, out, 63)) return 0;
|
|
out[63] = 0;
|
|
return 1;
|
|
}
|
|
|
|
// A text written into the entry's own buffers (game thread). 0: the game has no such text.
|
|
static int text_set(const char *name, const char *value) {
|
|
uint64_t e = gmd_entry(GD_TEXT64, 0x108, hash_of(name)), text = e ? rd_ptr(e + 0x68) : 0, other = e ? rd_ptr(e + 0x18) : 0;
|
|
int32_t room = 0;
|
|
char buf[64] = {0};
|
|
if (!text || !rd(e + 0x70, &room, 4) || room < 64) return 0;
|
|
snprintf(buf, sizeof buf, "%s", value);
|
|
if (other && other != text) wr(other, buf, 64);
|
|
return wr(text, buf, 64);
|
|
}
|
|
|
|
static void enum_set(Cpu *c, const char *name, const char *value) {
|
|
rt_call(c, MOD_MAIN_BASE + 0x3e0980, 3, gmd(), (uint64_t)hash_of(value), (uint64_t)hash_of(name));
|
|
}
|
|
|
|
// ---- events. main+0xd79f10 builds an event request in a block of 0x2000 bytes: the event's name goes into the buffer
|
|
// its +0x1a90 points to (the room at +0x1a98). main+0xd7a820 (the event manager **[main+0x4c7e090], request) hands it
|
|
// in and answers whether the event starts; the request's two lists at +0x1968 and +0x1950 are then destroyed
|
|
// (main+0x20b6f0, their table main+0x4974b20), as the game's own request node does (main+0x32db1a0).
|
|
static int event_request(Cpu *c, const char *name) {
|
|
static _Alignas(16) uint8_t arg[0x2000];
|
|
uint64_t a = (uint64_t)(uintptr_t)arg, slot = rd_ptr(MOD_MAIN_BASE + 0x4c7e090), mgr = slot ? rd_ptr(slot) : 0;
|
|
if (!mgr) return 0;
|
|
memset(arg, 0, sizeof arg);
|
|
rt_call(c, MOD_MAIN_BASE + 0xd79f10, 1, a);
|
|
uint64_t buf = rd_ptr(a + 0x1a90), took = 0;
|
|
int32_t room = 0;
|
|
rd(a + 0x1a98, &room, 4);
|
|
if (buf >= a && buf < a + sizeof arg && room > 1 && strlen(name) < (size_t)room) {
|
|
memcpy((void *)(uintptr_t)buf, name, strlen(name) + 1);
|
|
took = rt_call(c, MOD_MAIN_BASE + 0xd7a820, 2, mgr, a) & 0xff;
|
|
}
|
|
uint64_t vt = MOD_MAIN_BASE + 0x4974b20;
|
|
memcpy(arg + 0x1968, &vt, 8);
|
|
rt_call(c, MOD_MAIN_BASE + 0x20b6f0, 1, a + 0x1968);
|
|
memcpy(arg + 0x1950, &vt, 8);
|
|
rt_call(c, MOD_MAIN_BASE + 0x20b6f0, 1, a + 0x1950);
|
|
return took != 0;
|
|
}
|
|
|
|
// ---- the teleport and the event start: asked for by the menu, done by warp_tick on the game's thread
|
|
enum { W_IDLE, W_ASKED, W_EVENT, W_FLIGHT };
|
|
static struct {
|
|
char area[64], pos[64], event[80]; // q_lock: what the menu asked for
|
|
int state; // W_*
|
|
uint64_t at; // the tick of the next step, or the one at which the wait for the arrival ends
|
|
float from[3]; // where Link stood
|
|
} warp;
|
|
static char status[160]; // q_lock: the last thing that happened, for the menu
|
|
|
|
static void status_set(const char *fmt, const char *a, const char *b) {
|
|
plat_mutex_lock(&q_lock);
|
|
snprintf(status, sizeof status, fmt, a, b);
|
|
plat_mutex_unlock(&q_lock);
|
|
rt_log("debug: %s", status);
|
|
}
|
|
|
|
void dbg_status(char out[160]) {
|
|
plat_mutex_lock(&q_lock);
|
|
memcpy(out, status, sizeof status);
|
|
plat_mutex_unlock(&q_lock);
|
|
}
|
|
|
|
int dbg_busy(void) { return warp.state != W_IDLE || warp.event[0]; }
|
|
|
|
void dbg_warp(const char *area, const char *pos) {
|
|
plat_mutex_lock(&q_lock);
|
|
if (warp.state == W_IDLE && !warp.event[0] && area[0]) {
|
|
snprintf(warp.area, sizeof warp.area, "%s", area);
|
|
snprintf(warp.pos, sizeof warp.pos, "%s", pos);
|
|
warp.state = W_ASKED;
|
|
}
|
|
plat_mutex_unlock(&q_lock);
|
|
}
|
|
|
|
void dbg_event_start(const char *name) {
|
|
plat_mutex_lock(&q_lock);
|
|
if (warp.state == W_IDLE && !warp.event[0]) snprintf(warp.event, sizeof warp.event, "%s", name);
|
|
plat_mutex_unlock(&q_lock);
|
|
}
|
|
|
|
static const char REFUSED[] = "The game refused %s: it is in a cutscene, a conversation or a menu, or Link cannot take part "
|
|
"now (swimming, in the air). Try again once he stands free.";
|
|
|
|
static void warp_tick(Cpu *c, uint64_t tick) {
|
|
if (warp.event[0]) {
|
|
char name[80];
|
|
snprintf(name, sizeof name, "%s", warp.event);
|
|
status_set(player() && event_request(c, name) ? "Started the event %s." : REFUSED, name, "");
|
|
plat_mutex_lock(&q_lock);
|
|
warp.event[0] = 0;
|
|
plat_mutex_unlock(&q_lock);
|
|
}
|
|
if (warp.state == W_ASKED) {
|
|
// the game's event that only waits: the change of area is asked for while it runs
|
|
if (player() && gmd() && dbg_player_pos(warp.from) && event_request(c, "DefaultParentEvent")) {
|
|
warp.at = tick + 4, warp.state = W_EVENT;
|
|
} else {
|
|
status_set(REFUSED, "the teleport", "");
|
|
warp.state = W_IDLE;
|
|
}
|
|
} else if (warp.state == W_EVENT && tick >= warp.at) {
|
|
// what the game's own script reserves (its actor's ReserveBancChange): the next area and the start position's
|
|
// name, no event at the arrival, the event held during the change, a black fade, and the request itself
|
|
int ok = text_set("Sequence_ReservedNextBanc", warp.area) && text_set("Sequence_ReservedNextStartPosName", warp.pos);
|
|
text_set("Sequence_ReservedBootEventName", "");
|
|
text_set("Sequence_ReservedEventStarterName", "");
|
|
if (ok) {
|
|
rt_call(c, MOD_MAIN_BASE + 0x682b20, 3, gmd(), (uint64_t)1, (uint64_t)hash_of("Sequence_IsEventBancChangePause"));
|
|
enum_set(c, "Sequence_BancChangeFadePattern", "BlackFade");
|
|
enum_set(c, "Sequence_ControlInEvent", "ChangeBancOnEvent");
|
|
status_set("Teleporting to %s, %s...", warp.area, warp.pos[0] ? warp.pos : "its default start");
|
|
warp.at = tick + 3600, warp.state = W_FLIGHT;
|
|
} else {
|
|
status_set("The teleport to %s could not be asked for: the game's texts for it are not there.", warp.area, "");
|
|
warp.state = W_IDLE;
|
|
}
|
|
} else if (warp.state == W_FLIGHT) {
|
|
char now[64];
|
|
float p[3];
|
|
int there = text_get("Sequence_CurrentBanc", now) && !strcmp(now, warp.area) && dbg_player_pos(p) &&
|
|
fabsf(p[0] - warp.from[0]) + fabsf(p[1] - warp.from[1]) + fabsf(p[2] - warp.from[2]) > 0.5f;
|
|
if (there || tick >= warp.at) {
|
|
status_set(there ? "Arrived in %s, %s." : "The teleport to %s, %s was asked for; no arrival was seen.", warp.area,
|
|
warp.pos[0] ? warp.pos : "its default start");
|
|
warp.state = W_IDLE;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- the memory search: values of 1, 2 or 4 bytes in the game's heap
|
|
enum { RESULT_MAX = 2000000 };
|
|
static uint64_t *res_addr;
|
|
static uint32_t *res_last;
|
|
static int res_n, res_size, res_over;
|
|
|
|
int dbg_mem_read(uint64_t addr, int size, uint32_t *v) {
|
|
*v = 0;
|
|
return (size == 1 || size == 2 || size == 4) && rd(addr, v, (size_t)size);
|
|
}
|
|
|
|
int dbg_search_count(int *over) {
|
|
if (over) *over = res_over;
|
|
return res_n;
|
|
}
|
|
uint64_t dbg_search_addr(int i) { return i >= 0 && i < res_n ? res_addr[i] : 0; }
|
|
int dbg_search_size(void) { return res_size; }
|
|
void dbg_search_clear(void) { res_n = res_over = 0; }
|
|
|
|
// A new search: every place in the heap, aligned to the size, that holds the value. Returns how many (at most
|
|
// RESULT_MAX are kept; dbg_search_count's over says there were more).
|
|
int dbg_search_new(uint32_t value, int size) {
|
|
enum { PIECE = 16 << 20 };
|
|
if (!res_addr) res_addr = malloc(sizeof(uint64_t) * RESULT_MAX), res_last = malloc(sizeof(uint32_t) * RESULT_MAX);
|
|
res_n = res_over = 0;
|
|
res_size = size;
|
|
if (!res_addr || !res_last || !dbg_supported()) return 0;
|
|
const RtGuestLayout *l = mem_layout();
|
|
const RtRange ranges[2] = {l->heap, l->alias};
|
|
for (int r = 0; r < 2; r++) {
|
|
uint64_t a = ranges[r].base, end = ranges[r].base + ranges[r].size;
|
|
while (a < end) {
|
|
RtMemoryInfo mi;
|
|
if (mem_query(a, &mi) || !mi.size) break;
|
|
uint64_t stop = mi.base + mi.size < end ? mi.base + mi.size : end;
|
|
if (!mi.state || (mi.perm & 3) != 3) { // free, or not the game's to read and write
|
|
a = stop;
|
|
continue;
|
|
}
|
|
while (a < stop) {
|
|
uint64_t n = stop - a < PIECE ? stop - a : PIECE;
|
|
if (mem_read_hold(a, n)) {
|
|
const uint8_t *p = (const uint8_t *)(uintptr_t)a;
|
|
#define FIND(T) \
|
|
for (uint64_t i = 0; i + sizeof(T) <= n; i += sizeof(T)) { \
|
|
T x; \
|
|
memcpy(&x, p + i, sizeof(T)); \
|
|
if (x == (T)value) { \
|
|
if (res_n < RESULT_MAX) res_addr[res_n] = a + i, res_last[res_n++] = value; \
|
|
else res_over = 1; \
|
|
} \
|
|
}
|
|
if (size == 4) { FIND(uint32_t) } else if (size == 2) { FIND(uint16_t) } else { FIND(uint8_t) }
|
|
#undef FIND
|
|
mem_read_release();
|
|
}
|
|
a += n;
|
|
}
|
|
}
|
|
}
|
|
return res_n;
|
|
}
|
|
|
|
// Keeps the results whose value now passes: mode 0 equals value, 1 changed, 2 unchanged, 3 greater, 4 less than at the
|
|
// search before (unsigned; a float is searched by its bits with mode 0).
|
|
int dbg_search_next(int mode, uint32_t value) {
|
|
int kept = 0;
|
|
for (int i = 0; i < res_n; i++) {
|
|
uint32_t v, was = res_last[i];
|
|
if (!dbg_mem_read(res_addr[i], res_size, &v)) continue;
|
|
int pass = mode == 0 ? v == value : mode == 1 ? v != was : mode == 2 ? v == was : mode == 3 ? v > was : v < was;
|
|
if (pass) res_addr[kept] = res_addr[i], res_last[kept++] = v;
|
|
}
|
|
res_n = kept;
|
|
res_over = 0;
|
|
return kept;
|
|
}
|
|
|
|
// ---- NX2NAVI_DBG_TEST="<tick>:flag <hash hex> <0|1>;<tick>:speed <factor>;<tick>:find <value> <size>;
|
|
// <tick>:next <mode> <value>;<tick>:setfirst <value>;<tick>:holdfirst <value>;<tick>:show [<hash hex>];
|
|
// <tick>:warp <area> [<start position>];<tick>:event <name>;<tick>:text <game data text's name>": the menu's requests
|
|
// without the menu, for test runs. Ticks count the game's pad reads from the player's first appearance.
|
|
static void test_script(uint64_t tick) {
|
|
static char buf[2048];
|
|
static const char *next;
|
|
static int state;
|
|
if (!state) {
|
|
const char *e = cfg_get("NX2NAVI_DBG_TEST");
|
|
state = e && *e ? 1 : -1;
|
|
if (state > 0) snprintf(buf, sizeof buf, "%s", e), next = buf;
|
|
}
|
|
while (state > 0 && next && *next && strtoull(next, NULL, 10) <= tick) {
|
|
const char *colon = strchr(next, ':'), *end = strchr(next, ';');
|
|
if (!colon) break;
|
|
unsigned hash = 0, on = 0, size = 4;
|
|
unsigned long long value = 0;
|
|
float f = 0;
|
|
if (sscanf(colon + 1, "flag %x %u", &hash, &on) == 2) {
|
|
dbg_flag_set(hash, (int)on);
|
|
} else if (sscanf(colon + 1, "speed %f", &f) == 1) {
|
|
dbg_speed_set(f);
|
|
} else if (!strncmp(colon + 1, "warp ", 5)) {
|
|
char area[64] = "", pos[64] = "";
|
|
if (sscanf(colon + 6, "%63[^ ;] %63[^ ;]", area, pos) >= 1) dbg_warp(area, pos);
|
|
} else if (!strncmp(colon + 1, "event ", 6)) {
|
|
char name[80] = "";
|
|
if (sscanf(colon + 7, "%79[^ ;]", name) == 1) dbg_event_start(name);
|
|
} else if (!strncmp(colon + 1, "text ", 5)) {
|
|
char name[80] = "", now[64];
|
|
float p[3] = {0, 0, 0};
|
|
sscanf(colon + 6, "%79[^ ;]", name);
|
|
int got = text_get(name, now);
|
|
dbg_player_pos(p);
|
|
rt_log("debug: tick %llu: %s = \"%s\"%s; player %.1f %.1f %.1f", (unsigned long long)tick, name, now,
|
|
got ? "" : " (not found)", p[0], p[1], p[2]);
|
|
} else if (sscanf(colon + 1, "setfirst %llu", &value) == 1) {
|
|
dbg_mem_write(dbg_search_addr(0), value, dbg_search_size());
|
|
} else if (sscanf(colon + 1, "holdfirst %llu", &value) == 1) {
|
|
dbg_freeze(dbg_search_addr(0), value, dbg_search_size(), value != 0);
|
|
} else if (sscanf(colon + 1, "next %u %llu", &on, &value) == 2) {
|
|
int n = dbg_search_next((int)on, (uint32_t)value);
|
|
uint32_t now = 0;
|
|
dbg_mem_read(dbg_search_addr(0), dbg_search_size(), &now);
|
|
rt_log("debug: tick %llu: %d places left (mode %u, value %llu); the first holds %u; %d held", (unsigned long long)tick, n,
|
|
on, value, now, dbg_frozen_count());
|
|
} else if (sscanf(colon + 1, "find %llu %u", &value, &size) >= 1) {
|
|
uint64_t t0 = plat_now_ns();
|
|
int n = dbg_search_new((uint32_t)value, (int)size), over = 0;
|
|
dbg_search_count(&over);
|
|
rt_log("debug: tick %llu: %d places hold %llu as %u bytes%s, searched in %.0f ms; first %#llx", (unsigned long long)tick,
|
|
n, value, size, over ? " (and more)" : "", (double)(plat_now_ns() - t0) / 1e6,
|
|
(unsigned long long)dbg_search_addr(0));
|
|
} else if (!strncmp(colon + 1, "show", 4)) {
|
|
float p[3] = {0, 0, 0}, sp = dbg_speed_factor();
|
|
int n = dbg_flags_refresh(), set = 0, found = 0;
|
|
for (int i = 0; i < n; i++) set += flag_value[i];
|
|
int names = dbg_names(&found);
|
|
dbg_player_pos(p);
|
|
rt_log("debug: tick %llu: %d flags, %d on; names state %d, %d found; player %.2f %.2f %.2f; speed %.3f; done: %llu flag "
|
|
"changes, %llu writes, %llu refused", (unsigned long long)tick, n, set, names, found, p[0], p[1], p[2], sp,
|
|
(unsigned long long)stat_flags, (unsigned long long)stat_writes, (unsigned long long)stat_refused);
|
|
if (sscanf(colon + 1, "show %x", &hash) == 1)
|
|
for (int i = 0; i < n; i++)
|
|
if (flag_pairs[i].hash == hash)
|
|
rt_log("debug: flag %08x (%s) is %d", hash, names == 2 && flag_name[i] ? flag_name[i] : "no name", flag_value[i]);
|
|
}
|
|
next = end ? end + 1 : NULL;
|
|
}
|
|
}
|
|
|
|
// The game's thread, at its pad read.
|
|
void dbg_tick(Cpu *c) {
|
|
static uint64_t tick;
|
|
if (!dbg_supported() || (!tick && !player())) return;
|
|
tick++;
|
|
test_script(tick);
|
|
warp_tick(c, tick);
|
|
static Req todo[Q_MAX];
|
|
int n;
|
|
plat_mutex_lock(&q_lock);
|
|
n = q_n, q_n = 0;
|
|
memcpy(todo, q, sizeof(Req) * (size_t)n);
|
|
for (int i = 0; i < frozen_n && n < Q_MAX; i++) todo[n++] = (Req){Q_WRITE, 0, frozen[i].addr, frozen[i].value, frozen[i].size};
|
|
plat_mutex_unlock(&q_lock);
|
|
uint64_t m = gmd();
|
|
for (int i = 0; i < n; i++) {
|
|
if (todo[i].kind == Q_FLAG) {
|
|
if (!m) continue;
|
|
rt_call(c, MOD_MAIN_BASE + 0x682b20, 3, m, todo[i].value, (uint64_t)todo[i].hash);
|
|
stat_flags++;
|
|
rt_log("debug: flag %08x set to %s", todo[i].hash, todo[i].value ? "on" : "off"); // what a tester changed, for the log
|
|
} else if (todo[i].size == 1 || todo[i].size == 2 || todo[i].size == 4) {
|
|
if (wr(todo[i].addr, &todo[i].value, (size_t)todo[i].size)) stat_writes++;
|
|
else stat_refused++;
|
|
}
|
|
}
|
|
}
|