mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
Add a debug menu for testing Ocarina of Time: the game's flags by name, the game's speed, and a memory search, on F9.
With debug_menu = 1 (off by default; the Game tab has the switch) F9 opens a panel on the left edge in the performance overlay's look, with the game in view beside it. ui/menu.cpp lays it out in the settings menu's place, so its input and the game's blocked pad are the menu's own. Typed characters now reach the menu for its text fields. Flags: the game's 11,292 on/off flags are read from its game data manager, and a click changes one through the game's own function on the game's thread at its pad read (runtime/debug_tools.c dbg_tick), so the game applies it at its next update as it does its own changes. The game knows a flag by the hash of its name only; the names shown are found in the player's own event flow files, hashed and matched when the panel first opens (2,250 of them from 915 files in 0.2 s), and none is kept in the code. World: Link's position, and a speed factor from 0.05 to 8 that multiplies the step the runtime gives the game each frame (measured 0.50 at 0.5x). Memory: a search of the heap for a value of 1, 2 or 4 bytes or a float (about 2 s), narrowed by equals, changed, unchanged, went up or went down, with a value to set at a place or to hold there every frame. Tested through the window's own messages: F9 opened it, a click on a flag set that flag in the game, text typed into the filter narrowed the list, and F9 closed it; a scripted run set and held a value and put it back. Teleporting, a free camera and starting cutscenes are not in it: writing Link's position, or every copy of it in the heap, moved him back within three frames, so they need the game's own warp, scene-change and event functions. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
@@ -36,6 +36,15 @@ Draft notes for the release after 0.7.1. The version number is not decided yet.
|
||||
tested in this game yet**; on every other card it is version 3.1.6, which on an RX 7900 XT runs at the same frame
|
||||
rate as before and showed clean frames in a fast camera turn. It is off unless you set it, and when it fails the FSR
|
||||
3.1 library takes over. Please report how it looks on an RX 9000.
|
||||
- **A debug menu for testing** (off by default: Game tab, *Debug menu (F9)*, or `debug_menu = 1` in `nx2navi.ini`). F9
|
||||
opens a panel on the left edge, in the performance overlay's look, with the game in view beside it. It has three
|
||||
tabs. *Flags*: the game's 11,292 on/off flags with a search box, about 2,250 of them by name (the names are found in
|
||||
your own game's event files; the rest show their hash), each switched with a click through the game's own function,
|
||||
with the last changes listed to put back. *World*: Link's position and the game's speed, from 0.05x to 8x. *Memory*:
|
||||
a search of the game's memory for a number (1, 2 or 4 bytes, or a float), narrowed down by equals, changed,
|
||||
unchanged, went up or went down, with a value to set at a place or to hold there. Teleporting, a free camera and
|
||||
starting cutscenes are not in it yet: they need the game's own scene-change, warp and event functions. Changing
|
||||
flags or memory can break a save, so keep a copy first.
|
||||
- **Add to Steam** in Customize Launcher (Windows): adds the game to your Steam library as *The Legend of Zelda
|
||||
Ocarina of Time*, with Navi as its icon and the title screen's artwork and logo. Starting it from Steam starts the
|
||||
game. Steam restarts once while it is added.
|
||||
|
||||
@@ -411,6 +411,11 @@
|
||||
# has not been tested, and 3.1.6 on every other, where it runs at the same frame rate as the FSR 3.1 library's. When
|
||||
# it fails, the FSR 3.1 library takes over and the log says why (fg4:). Changes while the game runs take effect at once.
|
||||
# fsr4_fg = 0
|
||||
# The debug menu, a tool for testing (Ocarina of Time 1.1.1): with 1, F9 opens a window with the game's on/off flags
|
||||
# by name (the names found in your own game's event files), the game's speed, Link's position, and a search of the
|
||||
# game's memory with values to set and to hold. Changing flags or memory can break a save: keep a copy of the save
|
||||
# first. Do not bind F9 to a game button while it is on. The menu's Game tab has the switch. Takes effect at once.
|
||||
# debug_menu = 0
|
||||
# The DLSS model: default (NVIDIA's advice per mode: K for dlaa, quality and balanced, M for performance, L for
|
||||
# ultra_performance), k (K in every mode) or j (the other transformer model: slightly less ghosting behind moving
|
||||
# characters, a little more flicker). Any other model by its letter, a to m: on slower RTX cards the older model e is
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// nx2navi.ini: file-backed defaults for the NX2NAVI_* environment variables (docs/controls.md, nx2navi.ini.example).
|
||||
// nx2navi.ini: file-backed defaults for the NX2NAVI_* environment variables (docs/controls.md, nx2navi.ini.example).
|
||||
// Format: "key = value" lines, '#' or ';' at the start of a line begins a comment, [sections] only group. A key is a
|
||||
// variable name without "NX2NAVI_" (an optional NX2NAVI_ prefix is accepted), case-insensitive. A value is the rest of the
|
||||
// line, trimmed, optionally in double quotes; an empty value counts as unset. The environment wins over the file.
|
||||
@@ -62,7 +62,7 @@ static const char *const known[] = {
|
||||
"SVC_SLOW_MS",
|
||||
"SHOTS_FULL", "SPEED_PROBE", "SPIN", "SPIN_HANDOFF_US", "SPLIT_DRAWS", "SPLIT_IDLE", "SPLIT_IN_PASS", "SPLIT_US", "SPV_MARK_TEST", "SPV_OVERRIDE", "STICK_DEADZONE", "STICK_SENS",
|
||||
"SWKBD_ANSWER",
|
||||
"SET_AT", "SWKBD_TEST", "TAA", "FSR4", "FSR4_FG", "FG4_VERSION", "FG4_SWAPCHAIN", "TEST_PAD", "TEST_PAD_AT", "TEST_PREPARE_SHADERS", "TEX_ALIAS", "TEX_DISUSE", "TEX_GEN_UNDO", "TEX_MEMO", "TEX_VRAM_PRESSURE", "TEX_RELEASED_MB", "TEX_REUSE", "TEX_COMMIT_FREE_MB", "TEX_PROC_COMMIT_MB", "TEX_EVICT", "TEX_COMMIT_EVICT", "MIRROR_EVICT", "VRAM_BUDGET_MB", "TIMER_1MS", "TRACE", "TXD_TRANSPOSE", "UBO_SNAPSHOT", "EVO_TRACE", "EVENT_AT_SYNC", "UPSCALER", "VK_DYN_A2C", "VK_DYN_VERTEX", "VK_NAN_PRESERVE", "VK_NOOPT", "VK_NO_FLOAT64",
|
||||
"SET_AT", "SWKBD_TEST", "TAA", "DBG_TEST", "DEBUG_MENU", "MENU_TEST_DEBUG", "FSR4", "FSR4_FG", "FG4_VERSION", "FG4_SWAPCHAIN", "TEST_PAD", "TEST_PAD_AT", "TEST_PREPARE_SHADERS", "TEX_ALIAS", "TEX_DISUSE", "TEX_GEN_UNDO", "TEX_MEMO", "TEX_VRAM_PRESSURE", "TEX_RELEASED_MB", "TEX_REUSE", "TEX_COMMIT_FREE_MB", "TEX_PROC_COMMIT_MB", "TEX_EVICT", "TEX_COMMIT_EVICT", "MIRROR_EVICT", "VRAM_BUDGET_MB", "TIMER_1MS", "TRACE", "TXD_TRANSPOSE", "UBO_SNAPSHOT", "EVO_TRACE", "EVENT_AT_SYNC", "UPSCALER", "VK_DYN_A2C", "VK_DYN_VERTEX", "VK_NAN_PRESERVE", "VK_NOOPT", "VK_NO_FLOAT64",
|
||||
"VK_PRESENT", "VK_RECONVERGE", "VK_NAN_BITS", "VK_RECORDER", "VK_SSBO_ALIGN", "VK_SUBGROUP", "VK_SUBGROUP_PIN", "VK_VALIDATION", "VOLUME", "VRAM_HEADROOM", "VS_OUTS",
|
||||
"WALK_KEY", "WALK_SCALE", "WB_EXACT", "WB_FENCE_LIMIT", "WB_MERGE", "WB_NO_REUP", "WB_MERGE_MAX", "WB_PROBE", "WB_SPLIT", "WB_LAZY", "WB_LAZY_TOUCH", "WINDOW_SIZE", "XLATE", "XLATE_ID", "XLATE_KEEP", "XLATE_PROCS",
|
||||
"ZSTD_CHECK", "ZSTD_NATIVE",
|
||||
@@ -99,7 +99,7 @@ static const struct { const char *key, *section; } sections[] = {
|
||||
{"VOLUME", "audio"}, {"AUDIO_CHANNELS", "audio"}, {"AUDIO_DEVICE", "audio"}, {"MICROPHONE", "audio"}, {"MENU_SOUND_VOLUME", "audio"},
|
||||
{"MOTION", "input"}, {"RUMBLE_STRENGTH", "input"}, {"RUMBLE_CURVE", "input"}, {"RUMBLE_TRIGGERS", "input"}, {"DS3", "input"},
|
||||
{"PAD_LAYOUT", "input"}, {"PROMPTS", "input"}, {"STICK_DEADZONE", "input"}, {"LSTICK_DEADZONE", "input"}, {"RSTICK_DEADZONE", "input"},
|
||||
{"STICK_SENS", "input"},
|
||||
{"STICK_SENS", "input"}, {"DEBUG_MENU", "input"},
|
||||
{"MENU_CURSOR", "input"}, {"MOUSE_MODE", "input"}, {"MOUSE_SENS", "input"}, {"WALK_KEY", "input"}, {"WALK_SCALE", "input"}, {"INPUT_RECORD", "input"},
|
||||
{"BIND_A", "input"}, {"BIND_B", "input"}, {"BIND_X", "input"}, {"BIND_Y", "input"}, {"BIND_LCLICK", "input"},
|
||||
{"BIND_RCLICK", "input"}, {"BIND_L", "input"}, {"BIND_R", "input"}, {"BIND_ZL", "input"}, {"BIND_ZR", "input"},
|
||||
@@ -205,6 +205,7 @@ static const CfgMeta metas[] = {
|
||||
{"DS3", "0", "0|1", LIVE}, // input_ds3_win.c ds3_read: read a DualShock 3 directly; off: no HID scan
|
||||
{"PROMPTS", "nintendo", "nintendo|xbox|playstation|steamdeck|pc", RESTART},
|
||||
{"STICK_DEADZONE", "8", "0..50", LIVE}, {"STICK_SENS", "1", "0.1..3", LIVE}, // camera_speed.c cam_speed_tick
|
||||
{"DEBUG_MENU", "0", "0|1", LIVE}, // ui/menu.cpp menu_debug_toggle
|
||||
{"LSTICK_DEADZONE", "", "0..50", LIVE}, {"RSTICK_DEADZONE", "", "0..50", LIVE},
|
||||
{"MOUSE_MODE", "direct", "stick|direct", LIVE},
|
||||
{"MOUSE_SENS", "1", "0..10", LIVE}, {"MENU_CURSOR", "1", "0|1", LIVE},
|
||||
|
||||
480
runtime/debug_tools.c
Normal file
480
runtime/debug_tools.c
Normal file
@@ -0,0 +1,480 @@
|
||||
// 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.
|
||||
#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;
|
||||
}
|
||||
|
||||
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++;
|
||||
}
|
||||
free(raw);
|
||||
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", names_found, flag_n, files,
|
||||
(double)(plat_now_ns() - t0) / 1e6);
|
||||
__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;
|
||||
}
|
||||
|
||||
// ---- 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>]": 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[512];
|
||||
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 (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);
|
||||
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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
43
runtime/debug_tools.h
Normal file
43
runtime/debug_tools.h
Normal file
@@ -0,0 +1,43 @@
|
||||
// The debug menu's access to the game (debug_tools.c). Everything but dbg_tick is for the menu's thread.
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
|
||||
#ifndef __cplusplus
|
||||
#include "cpu.h"
|
||||
void dbg_tick(Cpu *c); // the game's thread, at its pad read (ov_input.c): does what the menu asked for
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
int dbg_supported(void); // this game build's structures are known (Ocarina of Time 1.1.1)
|
||||
|
||||
int dbg_player_pos(float xyz[3]); // Link's position in metres; 0 while there is no player
|
||||
float dbg_speed_factor(void); // the game's speed: 1 normal, 0.05 to 8 (frame_rate.c multiplies the game's step)
|
||||
void dbg_speed_set(float factor);
|
||||
|
||||
// The game's on/off flags, sorted by hash. dbg_flags_refresh reads their values and returns their number (0 until a
|
||||
// save is loaded). A change is made by the game's own function at the next frame.
|
||||
int dbg_flags_refresh(void);
|
||||
uint32_t dbg_flag_hash(int i);
|
||||
int dbg_flag_value(int i);
|
||||
const char *dbg_flag_name(int i); // NULL: no name found for it
|
||||
void dbg_flag_set(uint32_t hash, int on);
|
||||
int dbg_names(int *found); // 0 not started, 1 being read from the game's files, 2 read (*found of them)
|
||||
|
||||
// Memory: values of 1, 2 or 4 bytes in the game's heap.
|
||||
int dbg_search_new(uint32_t value, int size); // returns the number of places
|
||||
int dbg_search_next(int mode, uint32_t value); // 0 equals value, 1 changed, 2 unchanged, 3 greater, 4 less
|
||||
int dbg_search_count(int *over); // *over: the first search found more than are kept
|
||||
uint64_t dbg_search_addr(int i);
|
||||
int dbg_search_size(void);
|
||||
void dbg_search_clear(void);
|
||||
int dbg_mem_read(uint64_t addr, int size, uint32_t *v);
|
||||
void dbg_mem_write(uint64_t addr, uint64_t value, int size);
|
||||
int dbg_freeze(uint64_t addr, uint64_t value, int size, int on); // written every frame while on; 0: the list is full
|
||||
int dbg_frozen(uint64_t addr);
|
||||
int dbg_frozen_count(void);
|
||||
void dbg_unfreeze_all(void);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -47,6 +47,7 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "debug_tools.h"
|
||||
#include "cfg.h"
|
||||
#include "gfx.h"
|
||||
#include "host.h"
|
||||
@@ -582,7 +583,8 @@ void frame_step_present(uint64_t display_ns, int interval) {
|
||||
}
|
||||
}
|
||||
if (frame_free_running() && last_ns) {
|
||||
if (dt > 0 && dt < STEP_RESET_S) ocarina_step_write(ocarina_step(dt));
|
||||
// the debug menu's speed factor (debug_tools.c) multiplies the step the game is given
|
||||
if (dt > 0 && dt < STEP_RESET_S) ocarina_step_write(ocarina_step(dt) * (double)dbg_speed_factor());
|
||||
else ocarina_step_reset(); // a load or a debugger stop: the intervals before it say nothing about the next
|
||||
}
|
||||
last_ns = display_ns;
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "debug_tools.h"
|
||||
#include "cfg.h"
|
||||
#include "host.h"
|
||||
#include "input.h"
|
||||
@@ -93,6 +94,7 @@ static void npad_states(Cpu *c, uint32_t want) {
|
||||
s.attributes = ATTR_CONNECTED;
|
||||
input_poll(&s.buttons, &s.lx, &s.ly, &s.rx, &s.ry);
|
||||
}
|
||||
dbg_tick(c); // the debug menu's changes to the game, between two of its frames (debug_tools.c)
|
||||
*out = s;
|
||||
ret_x(c, 1);
|
||||
}
|
||||
|
||||
310
runtime/ui/debug_menu.cpp
Normal file
310
runtime/ui/debug_menu.cpp
Normal file
@@ -0,0 +1,310 @@
|
||||
// The debug menu's panel (F9 with debug_menu = 1; ui/menu.cpp lays it out in the settings menu's place while it is
|
||||
// open, so its input, its sounds and the game's blocked pad are the menu's own): on the left edge, in the performance
|
||||
// overlay's look, with the game in view beside it. What it does to the game is in
|
||||
// debug_tools.c: the game's on/off flags by name, the game's speed, Link's position, and a search of the game's memory
|
||||
// with values to set and to hold. Plain Dear ImGui with English text: a tool for testing, not part of the game's
|
||||
// interface. Present thread only.
|
||||
#include <algorithm>
|
||||
#include <ctype.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "imgui.h"
|
||||
|
||||
#include "debug_tools.h"
|
||||
extern "C" void rt_log(const char *fmt, ...);
|
||||
#include "ui/debug_menu.h"
|
||||
#include "ui/menu_skin.h"
|
||||
|
||||
namespace {
|
||||
|
||||
int g_tab = -1; // a tab asked for by the test hook (debug_menu_tab), -1 none
|
||||
|
||||
// ---- flags
|
||||
char f_filter[64];
|
||||
bool f_only_on, f_only_named;
|
||||
int *f_rows; // the flags that pass the filter
|
||||
int f_rows_n, f_count;
|
||||
int f_age = 1000; // frames since the values were read
|
||||
bool f_dirty = true;
|
||||
struct Change { uint32_t hash; int on; char name[72]; } f_log[6];
|
||||
int f_log_n;
|
||||
|
||||
bool contains(const char *hay, const char *needle) {
|
||||
size_t n = strlen(needle);
|
||||
for (; *hay; hay++) {
|
||||
size_t i = 0;
|
||||
while (i < n && hay[i] && tolower((unsigned char)hay[i]) == tolower((unsigned char)needle[i])) i++;
|
||||
if (i == n) return true;
|
||||
}
|
||||
return !n;
|
||||
}
|
||||
|
||||
void flags_filter(void) {
|
||||
if (!f_rows) f_rows = (int *)malloc(sizeof(int) * 200000);
|
||||
f_rows_n = 0;
|
||||
if (!f_rows) return;
|
||||
char hex[12];
|
||||
for (int i = 0; i < f_count && i < 200000; i++) {
|
||||
const char *name = dbg_flag_name(i);
|
||||
if (f_only_on && !dbg_flag_value(i)) continue;
|
||||
if (f_only_named && !name) continue;
|
||||
if (f_filter[0]) {
|
||||
snprintf(hex, sizeof hex, "%08x", dbg_flag_hash(i));
|
||||
if (!(name && contains(name, f_filter)) && !contains(hex, f_filter)) continue;
|
||||
}
|
||||
f_rows[f_rows_n++] = i;
|
||||
}
|
||||
// the named ones first, by name; the rest by hash, as the back end lists them
|
||||
std::stable_sort(f_rows, f_rows + f_rows_n, [](int a, int b) {
|
||||
const char *x = dbg_flag_name(a), *y = dbg_flag_name(b);
|
||||
if (!x || !y) return x && !y;
|
||||
return strcmp(x, y) < 0;
|
||||
});
|
||||
f_dirty = false;
|
||||
if (getenv("NX2NAVI_DBG_TEST")) rt_log("debug: filter \"%s\": %d flags shown", f_filter, f_rows_n); // test runs
|
||||
}
|
||||
|
||||
void tab_flags(void) {
|
||||
if (++f_age > 20) {
|
||||
int n = dbg_flags_refresh();
|
||||
if (n != f_count) f_count = n, f_dirty = true;
|
||||
f_age = 0;
|
||||
if (f_only_on) f_dirty = true; // values moved
|
||||
}
|
||||
if (!f_count) {
|
||||
ImGui::TextWrapped("The game's flags appear once a save is loaded.");
|
||||
return;
|
||||
}
|
||||
static int names_was;
|
||||
int found = 0, names = dbg_names(&found);
|
||||
if (names != names_was) names_was = names, f_dirty = true;
|
||||
if (names == 2) ImGui::TextWrapped("%d on/off flags of the game, %d with a name found in its event files.", f_count, found);
|
||||
else ImGui::TextWrapped("%d on/off flags of the game. Reading their names from the game's event files...", f_count);
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
if (ImGui::InputTextWithHint("##filter", "part of a name, or of the hash", f_filter, sizeof f_filter)) f_dirty = true;
|
||||
if (ImGui::Checkbox("only on", &f_only_on)) f_dirty = true;
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Checkbox("only named", &f_only_named)) f_dirty = true;
|
||||
if (f_dirty) flags_filter();
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("%d shown", f_rows_n);
|
||||
float log_h = f_log_n ? ImGui::GetTextLineHeightWithSpacing() * (float)(f_log_n + 1) + 8 : 0;
|
||||
if (ImGui::BeginChild("##flags", ImVec2(0, -log_h), ImGuiChildFlags_Borders)) {
|
||||
ImGuiListClipper clip;
|
||||
clip.Begin(f_rows_n);
|
||||
while (clip.Step())
|
||||
for (int r = clip.DisplayStart; r < clip.DisplayEnd; r++) {
|
||||
int i = f_rows[r];
|
||||
const char *name = dbg_flag_name(i);
|
||||
uint32_t hash = dbg_flag_hash(i);
|
||||
bool on = dbg_flag_value(i) != 0;
|
||||
char label[128];
|
||||
if (name) snprintf(label, sizeof label, "%s##%08x", name, hash);
|
||||
else snprintf(label, sizeof label, "(no name) %08x##%08x", hash, hash);
|
||||
if (ImGui::Checkbox(label, &on)) {
|
||||
dbg_flag_set(hash, on);
|
||||
f_age = 15; // read again in a few frames: the game applies the change at its next update
|
||||
for (int k = f_log_n < 6 ? f_log_n : 5; k > 0; k--) f_log[k] = f_log[k - 1];
|
||||
f_log[0].hash = hash, f_log[0].on = on;
|
||||
snprintf(f_log[0].name, sizeof f_log[0].name, "%s", name ? name : "(no name)");
|
||||
if (f_log_n < 6) f_log_n++;
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
if (f_log_n) {
|
||||
ImGui::TextDisabled("Changed in this session (click to put back):");
|
||||
for (int k = 0; k < f_log_n; k++) {
|
||||
char label[128];
|
||||
snprintf(label, sizeof label, "%s %08x -> %s##log%d", f_log[k].name, f_log[k].hash, f_log[k].on ? "on" : "off", k);
|
||||
if (ImGui::SmallButton(label)) {
|
||||
dbg_flag_set(f_log[k].hash, !f_log[k].on);
|
||||
f_log[k].on = !f_log[k].on;
|
||||
f_age = 15;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- world
|
||||
void tab_world(void) {
|
||||
float p[3];
|
||||
if (dbg_player_pos(p)) ImGui::Text("Link is at x %.2f y %.2f z %.2f (metres; y is up)", p[0], p[1], p[2]);
|
||||
else ImGui::TextDisabled("Link's position appears once a save is loaded.");
|
||||
ImGui::Separator();
|
||||
float speed = dbg_speed_factor();
|
||||
ImGui::Text("Game speed");
|
||||
static const float steps[] = {0.1f, 0.25f, 0.5f, 1.0f, 2.0f, 4.0f};
|
||||
for (float s : steps) {
|
||||
char label[16];
|
||||
snprintf(label, sizeof label, s < 1 ? "%.2gx" : "%.0fx", s);
|
||||
if (s != steps[0]) ImGui::SameLine();
|
||||
bool is = speed == s;
|
||||
if (is) ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive));
|
||||
if (ImGui::Button(label)) dbg_speed_set(s);
|
||||
if (is) ImGui::PopStyleColor();
|
||||
}
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
if (ImGui::SliderFloat("##speed", &speed, 0.05f, 8.0f, "%.2fx", ImGuiSliderFlags_Logarithmic)) dbg_speed_set(speed);
|
||||
ImGui::TextWrapped("The whole game runs at this speed, its interface and its music's timing included. It goes back to 1x when "
|
||||
"the game is closed.");
|
||||
ImGui::Separator();
|
||||
ImGui::TextWrapped("Not in this menu yet: teleporting, the free camera and starting cutscenes. They need the game's own "
|
||||
"scene-change, warp and event functions, which are not connected yet; writing Link's position into "
|
||||
"memory does not move him (the physics puts him back).");
|
||||
}
|
||||
|
||||
// ---- memory
|
||||
int m_type; // 0: 4 bytes, 1: 2 bytes, 2: 1 byte, 3: float
|
||||
char m_value[32] = "0";
|
||||
char m_set[32];
|
||||
char m_note[160];
|
||||
const int m_sizes[] = {4, 2, 1, 4};
|
||||
|
||||
bool parse_value(const char *s, uint32_t *out) {
|
||||
char *end = NULL;
|
||||
if (m_type == 3) {
|
||||
float f = strtof(s, &end);
|
||||
memcpy(out, &f, 4);
|
||||
} else {
|
||||
unsigned long long v = strtoull(s, &end, 0);
|
||||
if (v >> (8 * m_sizes[m_type])) return false;
|
||||
*out = (uint32_t)v;
|
||||
}
|
||||
return end && end != s;
|
||||
}
|
||||
|
||||
void show_value(char *out, size_t n, uint32_t v) {
|
||||
if (m_type == 3) {
|
||||
float f;
|
||||
memcpy(&f, &v, 4);
|
||||
snprintf(out, n, "%g", (double)f);
|
||||
} else {
|
||||
snprintf(out, n, "%u", v);
|
||||
}
|
||||
}
|
||||
|
||||
void tab_memory(void) {
|
||||
ImGui::TextWrapped("Finds a value in the game's memory: search for what a number is now, change it in the game, then narrow "
|
||||
"the places down until few are left. Setting or holding a wrong place can break the running game; a save "
|
||||
"made before is the way back.");
|
||||
const float field = ImGui::GetFontSize() * 12;
|
||||
ImGui::SetNextItemWidth(field);
|
||||
int over = 0, count = dbg_search_count(&over);
|
||||
if (count) ImGui::BeginDisabled();
|
||||
ImGui::Combo("##type", &m_type, "4 bytes (whole number)\0002 bytes\0001 byte\000float (4 bytes)\000");
|
||||
if (count) ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(field * 0.6f);
|
||||
ImGui::InputText("value", m_value, sizeof m_value, ImGuiInputTextFlags_CharsScientific);
|
||||
uint32_t v = 0;
|
||||
bool ok = parse_value(m_value, &v);
|
||||
if (!count) {
|
||||
if (!ok) ImGui::BeginDisabled();
|
||||
if (ImGui::Button("Search all memory")) {
|
||||
int n = dbg_search_new(v, m_sizes[m_type]);
|
||||
dbg_search_count(&over);
|
||||
snprintf(m_note, sizeof m_note, over ? "More than %d places hold it: only these are kept. Search for a rarer value, or narrow down."
|
||||
: "%d places hold it.", n);
|
||||
}
|
||||
if (!ok) ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("(takes a few seconds; the game waits)");
|
||||
} else {
|
||||
static const char *const modes[] = {"equals the value", "changed", "unchanged", "went up", "went down"};
|
||||
for (int m = 0; m < 5; m++) {
|
||||
if (m && m != 3) ImGui::SameLine(); // two rows: the panel is narrow
|
||||
if (m == 0 && !ok) ImGui::BeginDisabled();
|
||||
if (ImGui::Button(modes[m])) snprintf(m_note, sizeof m_note, "%d places left.", dbg_search_next(m, v));
|
||||
if (m == 0 && !ok) ImGui::EndDisabled();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("New search")) dbg_search_clear(), m_note[0] = 0;
|
||||
}
|
||||
if (m_note[0]) ImGui::TextUnformatted(m_note);
|
||||
int frozen = dbg_frozen_count();
|
||||
if (frozen) {
|
||||
char label[48];
|
||||
snprintf(label, sizeof label, "Stop holding %d value%s", frozen, frozen == 1 ? "" : "s");
|
||||
if (ImGui::Button(label)) dbg_unfreeze_all();
|
||||
}
|
||||
count = dbg_search_count(&over);
|
||||
if (!count) return;
|
||||
ImGui::SetNextItemWidth(field * 0.6f);
|
||||
ImGui::InputText("value to set", m_set, sizeof m_set, ImGuiInputTextFlags_CharsScientific);
|
||||
uint32_t setv = 0;
|
||||
bool set_ok = parse_value(m_set, &setv);
|
||||
int size = dbg_search_size();
|
||||
if (ImGui::BeginChild("##results", ImVec2(0, 0), ImGuiChildFlags_Borders)) {
|
||||
int shown = count < 300 ? count : 300;
|
||||
if (count > shown) ImGui::TextDisabled("The first %d of %d places:", shown, count);
|
||||
for (int i = 0; i < shown; i++) {
|
||||
uint64_t a = dbg_search_addr(i);
|
||||
uint32_t now = 0;
|
||||
char text[32] = "?";
|
||||
if (dbg_mem_read(a, size, &now)) show_value(text, sizeof text, now);
|
||||
ImGui::PushID(i);
|
||||
ImGui::Text("%012llx %-14s", (unsigned long long)a, text);
|
||||
ImGui::SameLine();
|
||||
if (!set_ok) ImGui::BeginDisabled();
|
||||
if (ImGui::SmallButton("Set")) dbg_mem_write(a, setv, size);
|
||||
if (!set_ok) ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
bool hold = dbg_frozen(a) != 0;
|
||||
if (ImGui::Checkbox("hold", &hold)) {
|
||||
if (!dbg_freeze(a, set_ok ? setv : now, size, hold)) snprintf(m_note, sizeof m_note, "No more than 64 values can be held.");
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" void debug_menu_tab(int tab) { g_tab = tab; }
|
||||
|
||||
// A panel on the left edge in the performance overlay's dress (the game's panel art, gold labels, the smaller text),
|
||||
// as tall as the window: the game stays in view beside it while a flag or a value is changed.
|
||||
extern "C" void debug_menu_layout(uint32_t w, uint32_t h, float scale) {
|
||||
const float m = 12 * scale;
|
||||
const ImVec4 gold(236 / 255.0f, 196 / 255.0f, 104 / 255.0f, 1), dim(0.72f, 0.69f, 0.62f, 1);
|
||||
float width = (float)w * 0.27f, min_width = 560 * scale;
|
||||
if (width < min_width) width = min_width < (float)w - 2 * m ? min_width : (float)w - 2 * m;
|
||||
ImGui::SetNextWindowPos(ImVec2(m, m), ImGuiCond_Always);
|
||||
ImGui::SetNextWindowSize(ImVec2(width, (float)h - 2 * m), ImGuiCond_Always);
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0, 0, 0, 0)); // the panel below is the background
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(24 * scale, 20 * scale));
|
||||
bool shown = ImGui::Begin("##debug", NULL, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings);
|
||||
ImGui::PopStyleVar(2);
|
||||
ImGui::PopStyleColor();
|
||||
if (shown) {
|
||||
ImDrawList *dl = ImGui::GetWindowDrawList();
|
||||
ImVec2 a = ImGui::GetWindowPos(), sz = ImGui::GetWindowSize();
|
||||
skin_panel(dl, a, ImVec2(a.x + sz.x, a.y + sz.y), scale * 0.8f, 0.92f);
|
||||
ImGui::SetWindowFontScale(0.72f);
|
||||
ImGui::TextColored(gold, "Debug menu");
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(dim, " F9 or Esc closes");
|
||||
ImGui::Dummy(ImVec2(0, 2 * scale));
|
||||
if (!dbg_supported()) {
|
||||
ImGui::TextWrapped("The debug menu knows the structures of Ocarina of Time 1.1.1 only.");
|
||||
} else if (ImGui::BeginTabBar("##tabs")) {
|
||||
static const char *const names[] = {"Flags", "World", "Memory"};
|
||||
for (int t = 0; t < 3; t++) {
|
||||
if (ImGui::BeginTabItem(names[t], NULL, g_tab == t ? ImGuiTabItemFlags_SetSelected : 0)) {
|
||||
if (t == 0) tab_flags();
|
||||
else if (t == 1) tab_world();
|
||||
else tab_memory();
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
}
|
||||
g_tab = -1;
|
||||
ImGui::EndTabBar();
|
||||
}
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
13
runtime/ui/debug_menu.h
Normal file
13
runtime/ui/debug_menu.h
Normal file
@@ -0,0 +1,13 @@
|
||||
// The debug menu's window (ui/debug_menu.cpp), laid out by ui/menu.cpp in the settings menu's place.
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
void debug_menu_layout(uint32_t w, uint32_t h, float scale); // between ImGui::NewFrame and Render, present thread
|
||||
void debug_menu_tab(int tab); // the tab to show at the next layout (test hook): 0 to 2
|
||||
#define DEBUG_MENU_TABS 3
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -36,12 +36,14 @@ static int g_font_bad;
|
||||
#pragma clang diagnostic pop
|
||||
#include "ui/amiibo_page.h"
|
||||
#include "ui/binds_page.h"
|
||||
#include "ui/debug_menu.h"
|
||||
#include "ui/option_rows.h"
|
||||
#include "ui/perf_panel.h"
|
||||
|
||||
extern "C" {
|
||||
#include "amiibo.h"
|
||||
#include "cheats.h"
|
||||
#include "debug_tools.h"
|
||||
#include "cfg.h"
|
||||
#include "folder_pick.h"
|
||||
#include "fs.h"
|
||||
@@ -129,17 +131,34 @@ static int change_open(int want) {
|
||||
|
||||
static void set_open(int open) { change_open(open); }
|
||||
|
||||
// The debug menu (ui/debug_menu.cpp; F9 with NX2NAVI_DEBUG_MENU = 1) is laid out in this menu's place: open is the one
|
||||
// state for both, with its input, its sounds and the game's blocked pad, and g_debug says which window it shows.
|
||||
static std::atomic<int> g_debug{0};
|
||||
extern "C" int menu_debug_enabled(void) { return cfg_bool("NX2NAVI_DEBUG_MENU"); }
|
||||
|
||||
static int g_failed_flag();
|
||||
// Until the game's first frame (menu_overlay_frame), boot screen included: the menu does not open, as it would not be
|
||||
// drawn while its keys queued up and changed settings once the game ran.
|
||||
static std::atomic<int> g_booting{1};
|
||||
extern "C" int menu_is_open(void) { return g_open.load(std::memory_order_relaxed); }
|
||||
extern "C" void menu_toggle(void) {
|
||||
if (!g_failed_flag() && !g_booting.load(std::memory_order_acquire)) change_open(-1);
|
||||
if (g_failed_flag() || g_booting.load(std::memory_order_acquire)) return;
|
||||
if (!menu_is_open()) g_debug.store(0); // F1 opens the settings; with either window open it closes that one
|
||||
change_open(-1);
|
||||
}
|
||||
// F9: opens the debug menu, and closes it again; with the settings menu open it does nothing.
|
||||
extern "C" void menu_debug_toggle(void) {
|
||||
if (!menu_debug_enabled() || g_failed_flag() || g_booting.load(std::memory_order_acquire)) return;
|
||||
if (menu_is_open()) {
|
||||
if (g_debug.load()) change_open(0);
|
||||
return;
|
||||
}
|
||||
g_debug.store(1);
|
||||
change_open(1);
|
||||
}
|
||||
|
||||
// ---- events from the window adapter, queued while the menu is open; the present path hands them to ImGui
|
||||
enum { EV_KEY, EV_POS, EV_BUTTON, EV_WHEEL, EV_FOCUS, EV_MAX = 256 };
|
||||
enum { EV_KEY, EV_POS, EV_BUTTON, EV_WHEEL, EV_FOCUS, EV_CHAR, EV_MAX = 256 };
|
||||
struct Ev { int type, a, b; float x, y; };
|
||||
static Ev ev_q[EV_MAX];
|
||||
|
||||
@@ -158,6 +177,8 @@ static void push(Ev e) {
|
||||
}
|
||||
|
||||
extern "C" void menu_event_key(int key, int down) { push({EV_KEY, key, down, 0, 0}); }
|
||||
// a typed character, for the debug menu's text fields (the settings menu has none)
|
||||
extern "C" void menu_event_char(unsigned c) { push({EV_CHAR, (int)c, 0, 0, 0}); }
|
||||
extern "C" void menu_event_mouse_pos(float x, float y) { push({EV_POS, 0, 0, x, y}); }
|
||||
extern "C" void menu_event_mouse_button(int button, int down) { push({EV_BUTTON, button, down, 0, 0}); }
|
||||
extern "C" void menu_event_wheel(float dx, float dy) { push({EV_WHEEL, 0, 0, dx, dy}); }
|
||||
@@ -183,6 +204,7 @@ static void drain_events(ImGuiIO &io) {
|
||||
case EV_BUTTON: io.AddMouseButtonEvent(e.a, e.b != 0); break;
|
||||
case EV_WHEEL: io.AddMouseWheelEvent(e.x, e.y); break;
|
||||
case EV_FOCUS: io.AddFocusEvent(e.a != 0); break;
|
||||
case EV_CHAR: io.AddInputCharacter((unsigned)e.a); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1237,6 +1259,16 @@ static void tab_game(float scale) {
|
||||
// discord_rpc.c reads the setting on its own thread and connects or clears the status within half a second
|
||||
opt_toggle("NX2NAVI_DISCORD_RPC", UI_MENU_DISCORD_RPC, UI_MENU_DISCORD_RPC_DESC);
|
||||
rows_saves();
|
||||
// The debug menu, a tool for testing with English text of its own: off unless switched on here
|
||||
if (dbg_supported()) {
|
||||
opt_group_text("Testing");
|
||||
const bool on = cfg_bool("NX2NAVI_DEBUG_MENU");
|
||||
if (opt_toggle_text("debug_menu", "Debug menu (F9)",
|
||||
"F9 opens a window for testing: the game's flags by name, the game's speed, and a search of "
|
||||
"its memory with values to set and hold. Changing flags or memory can break a save: keep a "
|
||||
"copy of the save first.", on))
|
||||
opt_apply("NX2NAVI_DEBUG_MENU", on ? "0" : "1");
|
||||
}
|
||||
opt_end();
|
||||
}
|
||||
|
||||
@@ -1760,6 +1792,8 @@ extern "C" int menu_overlay_frame(uint64_t frame, const MenuTarget *t, const cha
|
||||
g_test.opened = frame;
|
||||
g_test.page = 0;
|
||||
g_tab_request = 0;
|
||||
g_debug.store(cfg_get("NX2NAVI_MENU_TEST_DEBUG") != NULL); // the test shoots the debug menu's tabs instead
|
||||
if (g_debug.load()) debug_menu_tab(0);
|
||||
set_open(1);
|
||||
open = true;
|
||||
} else if (!g_perf_mode) {
|
||||
@@ -1818,7 +1852,14 @@ extern "C" int menu_overlay_frame(uint64_t frame, const MenuTarget *t, const cha
|
||||
// no ImGui_ImplVulkan_NewFrame: all it does is upload Dear ImGui's own font atlas when the back end holds none,
|
||||
// which it never does here (fonts_up uploads it, ui/menu_art.cpp menu_font_upload)
|
||||
ImGui::NewFrame();
|
||||
if (open) layout_menu(t->width, t->height, scale);
|
||||
if (open && g_debug.load(std::memory_order_relaxed)) {
|
||||
debug_menu_layout(t->width, t->height, scale);
|
||||
// Esc, or B on the pad when no field is being typed in, closes it
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_Escape, false) || (!io.WantTextInput && ImGui::IsKeyPressed(ImGuiKey_GamepadFaceRight, false)))
|
||||
set_open(0);
|
||||
} else if (open) {
|
||||
layout_menu(t->width, t->height, scale);
|
||||
}
|
||||
perf_draw(t, scale);
|
||||
ImGui::Render();
|
||||
if (open) g_save_pending = opt_save_tick(0);
|
||||
@@ -1828,11 +1869,12 @@ extern "C" int menu_overlay_frame(uint64_t frame, const MenuTarget *t, const cha
|
||||
const char *dir = cfg_get("NX2NAVI_MENU_TEST_DIR");
|
||||
snprintf(path, sizeof path, "%s/menu-test-%llu-%d.png", dir ? dir : ".", (unsigned long long)frame, g_test.page);
|
||||
*shot = path;
|
||||
if (++g_test.page == TAB_COUNT) {
|
||||
if (++g_test.page == (g_debug.load() ? DEBUG_MENU_TABS : TAB_COUNT)) {
|
||||
g_test.page = -1;
|
||||
set_open(0); // the next frame lays out nothing
|
||||
} else {
|
||||
g_tab_request = g_test.page;
|
||||
if (g_debug.load()) debug_menu_tab(g_test.page);
|
||||
}
|
||||
}
|
||||
return ImGui::GetDrawData()->TotalVtxCount > 0;
|
||||
|
||||
@@ -30,6 +30,9 @@ void menu_perf_cycle(void); // F10: the performance overlay off, f
|
||||
// ---- events from the window adapter, used only while the menu is open. key: an ImGuiKey; coordinates in pixels of the
|
||||
// window's client area, which is the swapchain image.
|
||||
void menu_event_key(int key, int down);
|
||||
void menu_event_char(unsigned c); // a typed character (the debug menu's text fields)
|
||||
int menu_debug_enabled(void); // NX2NAVI_DEBUG_MENU: F9 belongs to the debug menu (ui/debug_menu.cpp)
|
||||
void menu_debug_toggle(void); // F9
|
||||
void menu_event_mouse_pos(float x, float y);
|
||||
void menu_event_mouse_button(int button, int down); // 0 left, 1 right, 2 middle
|
||||
void menu_event_wheel(float dx, float dy);
|
||||
|
||||
@@ -49,6 +49,10 @@ extern "C" void menu_sdl_event(const SDL_Event *e) {
|
||||
menu_toggle();
|
||||
return;
|
||||
}
|
||||
if (e->key.scancode == SDL_SCANCODE_F9 && menu_debug_enabled()) { // the debug menu, while its setting is on
|
||||
if (!e->key.repeat) menu_debug_toggle();
|
||||
return;
|
||||
}
|
||||
if (e->key.scancode == SDL_SCANCODE_F10) {
|
||||
if (!e->key.repeat) menu_perf_cycle();
|
||||
return;
|
||||
|
||||
@@ -56,6 +56,11 @@ extern "C" int menu_window_msg(HWND h, UINT m, WPARAM w, LPARAM l, LRESULT *res)
|
||||
*res = 0;
|
||||
return 1;
|
||||
}
|
||||
if (w == VK_F9 && menu_debug_enabled()) { // the debug menu, while its setting is on
|
||||
if (!(l & (1 << 30))) menu_debug_toggle();
|
||||
*res = 0;
|
||||
return 1;
|
||||
}
|
||||
if (w == VK_F10) { // also keeps F10 from opening the window menu
|
||||
if (!(l & (1 << 30))) menu_perf_cycle();
|
||||
*res = 0;
|
||||
@@ -69,6 +74,9 @@ extern "C" int menu_window_msg(HWND h, UINT m, WPARAM w, LPARAM l, LRESULT *res)
|
||||
if (!menu_is_open()) return 0;
|
||||
if (ImGuiKey k = key_of(w)) menu_event_key(k, m == WM_KEYDOWN || m == WM_SYSKEYDOWN);
|
||||
return 0; // F11 and the system keys keep working
|
||||
case WM_CHAR: // typed text, for the debug menu's fields
|
||||
if (menu_is_open() && w >= 0x20 && w < 0xd800) menu_event_char((unsigned)w);
|
||||
return 0;
|
||||
case WM_MOUSEMOVE:
|
||||
if (menu_is_open()) menu_event_mouse_pos((float)GET_X_LPARAM(l), (float)GET_Y_LPARAM(l));
|
||||
return 0;
|
||||
|
||||
Reference in New Issue
Block a user