diff --git a/docs/releases/v0.7.2.md b/docs/releases/v0.7.2.md index 2b6a46c7..5e55b64e 100644 --- a/docs/releases/v0.7.2.md +++ b/docs/releases/v0.7.2.md @@ -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. diff --git a/nx2navi.ini.example b/nx2navi.ini.example index 3d218cc1..08ea3c76 100644 --- a/nx2navi.ini.example +++ b/nx2navi.ini.example @@ -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 diff --git a/runtime/cfg.c b/runtime/cfg.c index c14fd101..e7cb199f 100644 --- a/runtime/cfg.c +++ b/runtime/cfg.c @@ -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}, diff --git a/runtime/debug_tools.c b/runtime/debug_tools.c new file mode 100644 index 00000000..2c7743cd --- /dev/null +++ b/runtime/debug_tools.c @@ -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 +#include +#include +#include +#include +#include + +#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=":flag <0|1>;:speed ;:find ; +// :next ;:setfirst ;:holdfirst ;:show []": 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++; + } + } +} diff --git a/runtime/debug_tools.h b/runtime/debug_tools.h new file mode 100644 index 00000000..98dc9b92 --- /dev/null +++ b/runtime/debug_tools.h @@ -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 + +#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 diff --git a/runtime/frame_rate.c b/runtime/frame_rate.c index 25838b33..6126b7eb 100644 --- a/runtime/frame_rate.c +++ b/runtime/frame_rate.c @@ -47,6 +47,7 @@ #include #include +#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; diff --git a/runtime/ov_input.c b/runtime/ov_input.c index 58f684e1..1620fb98 100644 --- a/runtime/ov_input.c +++ b/runtime/ov_input.c @@ -10,6 +10,7 @@ #include #include +#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); } diff --git a/runtime/ui/debug_menu.cpp b/runtime/ui/debug_menu.cpp new file mode 100644 index 00000000..f82a6b6a --- /dev/null +++ b/runtime/ui/debug_menu.cpp @@ -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 +#include +#include +#include +#include + +#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(); +} diff --git a/runtime/ui/debug_menu.h b/runtime/ui/debug_menu.h new file mode 100644 index 00000000..7df76a0e --- /dev/null +++ b/runtime/ui/debug_menu.h @@ -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 + +#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 diff --git a/runtime/ui/menu.cpp b/runtime/ui/menu.cpp index 7097d4aa..6a041960 100644 --- a/runtime/ui/menu.cpp +++ b/runtime/ui/menu.cpp @@ -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 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 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; diff --git a/runtime/ui/menu.h b/runtime/ui/menu.h index bcf2fd89..e9c216fd 100644 --- a/runtime/ui/menu.h +++ b/runtime/ui/menu.h @@ -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); diff --git a/runtime/ui/menu_sdl.cpp b/runtime/ui/menu_sdl.cpp index 79b6f2ec..392ec6c2 100644 --- a/runtime/ui/menu_sdl.cpp +++ b/runtime/ui/menu_sdl.cpp @@ -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; diff --git a/runtime/ui/menu_win.cpp b/runtime/ui/menu_win.cpp index bfca9ed3..e4754fb5 100644 --- a/runtime/ui/menu_win.cpp +++ b/runtime/ui/menu_win.cpp @@ -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;