From fe3126e9a7120f664f5259ddb42564bd9c112357 Mon Sep 17 00:00:00 2001 From: epicmeme546 <178368684+epicmeme546@users.noreply.github.com> Date: Wed, 11 Nov 2026 07:29:17 -0500 Subject: [PATCH] Add teleporting and cutscene starting to the debug menu. The Teleport tab lists every area of the game with its named start positions, read from the player's own game files: the warp songs' pads, both sides of the loading zones and the boss rooms among them. A click changes area the way the game's own doors do: during a short event of the game that only waits, the menu reserves the next area and start position in the game's data and asks the game's sequence for the change, so the area unloads and loads as in play, a first visit's cutscene plays and a boss room starts its fight. The Cutscenes tab lists the game's event scripts by name and starts one through the game's event manager. Tested from Hyrule Field to Kakariko Village, the Forest Temple, Kokiri Forest in the child's time, the bottom of Lake Hylia, Volvagia's room, and the pads of two warp songs, four of them in one run, with Link walking afterwards; the Kakariko arrival cutscene started by name. Co-Authored-By: Claude Fable 5.2 --- docs/releases/v0.7.2.md | 18 ++- nx2navi.ini.example | 8 +- runtime/debug_tools.c | 257 +++++++++++++++++++++++++++++++++++++- runtime/debug_tools.h | 14 +++ runtime/ui/debug_menu.cpp | 153 +++++++++++++++++++++-- runtime/ui/debug_menu.h | 4 +- runtime/ui/menu.cpp | 7 +- 7 files changed, 434 insertions(+), 27 deletions(-) diff --git a/docs/releases/v0.7.2.md b/docs/releases/v0.7.2.md index 5e55b64e..9994a5a3 100644 --- a/docs/releases/v0.7.2.md +++ b/docs/releases/v0.7.2.md @@ -37,14 +37,20 @@ Draft notes for the release after 0.7.1. The version number is not decided yet. 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 + opens a panel on the left edge, in the performance overlay's look, with the game in view beside it. It has five 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. + with the last changes listed to put back. *Teleport*: every area of the game with its named start positions, 415 + places read from your own game's files, with a search box and buttons for the warp songs' pads, both sides of the + loading zones and the boss rooms. A click takes Link there through the game's own change of area, as a door or a + warp song does, so the area loads as in play, a first visit's cutscene plays and a boss room starts its fight. + *Cutscenes*: the game's 653 event scripts by name (cutscenes, conversations, scripted moments), each started with a + click as if its trigger had fired. The game takes a teleport or an event only while Link stands free: not during a + cutscene or a conversation, and not while he swims. *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. A free camera is not in + it yet. Changing flags or memory, teleporting and starting events can break a save, and the game saves by itself + after a change of area, 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 08ea3c76..e992cc33 100644 --- a/nx2navi.ini.example +++ b/nx2navi.ini.example @@ -412,9 +412,11 @@ # 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. +# by name (the names found in your own game's event files), a teleport to every area's start positions (the warp +# songs' pads and both sides of the loading zones among them), the game's cutscenes and other events to start by name, +# 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, teleporting and starting events can break a save, and the game saves by itself after a change of area: +# 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 diff --git a/runtime/debug_tools.c b/runtime/debug_tools.c index 2c7743cd..0a47ce58 100644 --- a/runtime/debug_tools.c +++ b/runtime/debug_tools.c @@ -17,6 +17,17 @@ // // Link's position is shown but not written: it is the actor's copy ([[main+0x4e371d0]+0x480]+0x31c), which the physics // writes every frame. Writing it, or every copy of it in the heap, moved Link back within three frames. +// +// Teleporting is the game's own change of area, as a loading zone, a warp song or a cutscene makes it: the game's +// event script reserves the next area and the name of a start position in it (game data texts), and asks the +// sequence that runs during events to change area (the enum Sequence_ControlInEvent = ChangeBancOnEvent). warp_tick +// does the same during a short event of the game that only waits (DefaultParentEvent, 30 frames), so the game unloads +// the area, loads the next one and places Link exactly as it does for its own doors. The places offered are the ones +// in the player's game files (Banc/BancBasedResource/StartPos.byml.zs: each area with its named start positions, +// the warp songs' pads and both sides of every loading zone among them); none of that list is kept in this code. +// +// An event (a cutscene, a conversation, a scripted moment) is started by its file's name through the game's event +// manager, as the game's own actors start them. #include #include #include @@ -264,6 +275,49 @@ static int sarc_member(const uint8_t *p, size_t n, const char *name, size_t *at, return 0; } +// ---- the places to teleport to and the events, from the player's game files (names_thread) +typedef struct { const char *area, *pos; } Place; +static Place *places; // by area, as the game's file lists them; the texts stay in the file's buffer +static int place_n; +static char **event_names; // the event files that hold a script, by name +static int event_n; + +static uint32_t u24(const uint8_t *p) { return p[0] | (uint32_t)p[1] << 8 | (uint32_t)p[2] << 16; } + +// The i-th key of a BYML file's key table (a node 0xc2: its count, then the offsets of its texts). +static const char *byml_key(const uint8_t *p, size_t n, uint32_t keys, uint32_t i) { + uint32_t off; + if ((size_t)keys + 8 > n || p[keys] != 0xc2 || i >= u24(p + keys + 1) || (size_t)keys + 8 + 4 * (size_t)i > n) return NULL; + memcpy(&off, p + keys + 4 + 4 * (size_t)i, 4); + if ((size_t)keys + off >= n || !memchr(p + keys + off, 0, n - keys - off)) return NULL; + return (const char *)p + keys + off; +} + +// The start positions file: a dictionary (node 0xc1: its count, then entries of a key's number in 24 bits, the +// value's kind and the value's offset) of areas, each a dictionary of its start positions. +static void places_in(const uint8_t *p, size_t n) { + uint32_t keys, root; + if (n < 16 || memcmp(p, "YB", 2)) return; + memcpy(&keys, p + 4, 4), memcpy(&root, p + 12, 4); + if ((size_t)root + 4 > n || p[root] != 0xc1) return; + uint32_t areas = u24(p + root + 1); + Place *list = malloc(sizeof(Place) * 4096); + int count = 0; + for (uint32_t i = 0; list && i < areas && (size_t)root + 12 + 8 * (size_t)i <= n; i++) { + const uint8_t *e = p + root + 4 + 8 * (size_t)i; + const char *area = byml_key(p, n, keys, u24(e)); + uint32_t at; + memcpy(&at, e + 4, 4); + if (!area || e[3] != 0xc1 || (size_t)at + 4 > n || p[at] != 0xc1 || strlen(area) > 63) continue; + uint32_t spots = u24(p + at + 1); + for (uint32_t k = 0; k < spots && count < 4096 && (size_t)at + 12 + 8 * (size_t)k <= n; k++) { + const char *pos = byml_key(p, n, keys, u24(p + at + 4 + 8 * (size_t)k)); + if (pos && strlen(pos) <= 63) list[count++] = (Place){area, pos}; + } + } + places = list, place_n = count; +} + static void names_thread(void *arg) { (void)arg; uint64_t t0 = plat_now_ns(), dn = 0; @@ -289,17 +343,31 @@ static void names_thread(void *arg) { if (raw && ZSTD_decompress_usingDDict(dctx, raw, (size_t)rn, z, (size_t)fsize, dd) == rn) { names_in(raw, (size_t)rn); files++; + // an event file that holds a script (the count at 0x20) can be started by its name + uint16_t scripts = 0; + if (rn > 0x22 && !memcmp(raw, "BFEVFL", 6)) memcpy(&scripts, raw + 0x20, 2); + const char *dot = strchr(path + 16, '.'); + if (scripts && dot && dot - (path + 16) < 80 && (event_names || (event_names = calloc((size_t)count, sizeof *event_names)))) { + char *name = calloc(1, (size_t)(dot - (path + 16)) + 1); + if (name) memcpy(name, path + 16, (size_t)(dot - (path + 16))), event_names[event_n++] = name; + } } free(raw); free(z); } + uint64_t zn = 0; + uint8_t *z = romfs_load("Banc/BancBasedResource/StartPos.byml.zs", &zn); + unsigned long long rn = z ? ZSTD_getFrameContentSize(z, (size_t)zn) : 0; + uint8_t *raw = z && rn && rn < (16u << 20) ? malloc((size_t)rn) : NULL; // kept: the places' texts are in it + if (raw && ZSTD_decompress_usingDDict(dctx, raw, (size_t)rn, z, (size_t)zn, dd) == rn) places_in(raw, (size_t)rn); + free(z); } ZSTD_freeDCtx(dctx); ZSTD_freeDDict(dd); free(dics); free(dz); - rt_log("debug: names for %d of %d flags, from %d of the game's event files, in %.0f ms", names_found, flag_n, files, - (double)(plat_now_ns() - t0) / 1e6); + rt_log("debug: names for %d of %d flags, from %d of the game's event files, in %.0f ms; %d events, %d places to teleport to", + names_found, flag_n, files, (double)(plat_now_ns() - t0) / 1e6, event_n, place_n); __atomic_store_n(&names_state, 2, __ATOMIC_RELEASE); } @@ -317,6 +385,169 @@ int dbg_names(int *found) { return s; } +int dbg_places(void) { return __atomic_load_n(&names_state, __ATOMIC_ACQUIRE) == 2 ? place_n : 0; } +const char *dbg_place_area(int i) { return i >= 0 && i < dbg_places() ? places[i].area : ""; } +const char *dbg_place_pos(int i) { return i >= 0 && i < dbg_places() ? places[i].pos : ""; } +int dbg_events(void) { return __atomic_load_n(&names_state, __ATOMIC_ACQUIRE) == 2 ? event_n : 0; } +const char *dbg_event_name(int i) { return i >= 0 && i < dbg_events() ? event_names[i] : ""; } + +// ---- the game data's other kinds: the manager holds a section of 0x60 bytes for each kind (the kind's number times +// 0x60: +0x10 the entries' count, +0x18 the entries, +0x20 the {hash, index} table, +0x2c its size). An entry of a +// 64-character text is 0x108 bytes, the text's pointer at +0x68 (a second one at +0x18) and its room at +0x70. +// main+0x3e0980 (manager, value, hash) queues the change of an enum, whose value is the hash of the value's name. +enum { GD_TEXT64 = 16 }; +static uint32_t hash_of(const char *text) { return murmur3((const uint8_t *)text, strlen(text)); } + +static uint64_t gmd_entry(int kind, uint32_t stride, uint32_t hash) { + uint64_t m = gmd(), sec = m + (uint64_t)kind * 0x60, table = m ? rd_ptr(sec + 0x20) : 0, entries = m ? rd_ptr(sec + 0x18) : 0; + uint32_t count = 0, size = 0; + if (!table || !entries || !rd(sec + 0x10, &count, 4) || !rd(sec + 0x2c, &size, 4) || !size || size > 100000) return 0; + for (uint32_t i = hash % size, n = 0; n < size; i = i + 1 < size ? i + 1 : 0, n++) { + Pair pr; + if (!rd(table + (uint64_t)i * 8, &pr, 8) || !pr.hash) return 0; + if (pr.hash == hash) return pr.index < count ? entries + (uint64_t)pr.index * stride : 0; + } + return 0; +} + +static int text_get(const char *name, char out[64]) { + uint64_t e = gmd_entry(GD_TEXT64, 0x108, hash_of(name)), text = e ? rd_ptr(e + 0x68) : 0; + out[0] = 0; + if (!text || !rd(text, out, 63)) return 0; + out[63] = 0; + return 1; +} + +// A text written into the entry's own buffers (game thread). 0: the game has no such text. +static int text_set(const char *name, const char *value) { + uint64_t e = gmd_entry(GD_TEXT64, 0x108, hash_of(name)), text = e ? rd_ptr(e + 0x68) : 0, other = e ? rd_ptr(e + 0x18) : 0; + int32_t room = 0; + char buf[64] = {0}; + if (!text || !rd(e + 0x70, &room, 4) || room < 64) return 0; + snprintf(buf, sizeof buf, "%s", value); + if (other && other != text) wr(other, buf, 64); + return wr(text, buf, 64); +} + +static void enum_set(Cpu *c, const char *name, const char *value) { + rt_call(c, MOD_MAIN_BASE + 0x3e0980, 3, gmd(), (uint64_t)hash_of(value), (uint64_t)hash_of(name)); +} + +// ---- events. main+0xd79f10 builds an event request in a block of 0x2000 bytes: the event's name goes into the buffer +// its +0x1a90 points to (the room at +0x1a98). main+0xd7a820 (the event manager **[main+0x4c7e090], request) hands it +// in and answers whether the event starts; the request's two lists at +0x1968 and +0x1950 are then destroyed +// (main+0x20b6f0, their table main+0x4974b20), as the game's own request node does (main+0x32db1a0). +static int event_request(Cpu *c, const char *name) { + static _Alignas(16) uint8_t arg[0x2000]; + uint64_t a = (uint64_t)(uintptr_t)arg, slot = rd_ptr(MOD_MAIN_BASE + 0x4c7e090), mgr = slot ? rd_ptr(slot) : 0; + if (!mgr) return 0; + memset(arg, 0, sizeof arg); + rt_call(c, MOD_MAIN_BASE + 0xd79f10, 1, a); + uint64_t buf = rd_ptr(a + 0x1a90), took = 0; + int32_t room = 0; + rd(a + 0x1a98, &room, 4); + if (buf >= a && buf < a + sizeof arg && room > 1 && strlen(name) < (size_t)room) { + memcpy((void *)(uintptr_t)buf, name, strlen(name) + 1); + took = rt_call(c, MOD_MAIN_BASE + 0xd7a820, 2, mgr, a) & 0xff; + } + uint64_t vt = MOD_MAIN_BASE + 0x4974b20; + memcpy(arg + 0x1968, &vt, 8); + rt_call(c, MOD_MAIN_BASE + 0x20b6f0, 1, a + 0x1968); + memcpy(arg + 0x1950, &vt, 8); + rt_call(c, MOD_MAIN_BASE + 0x20b6f0, 1, a + 0x1950); + return took != 0; +} + +// ---- the teleport and the event start: asked for by the menu, done by warp_tick on the game's thread +enum { W_IDLE, W_ASKED, W_EVENT, W_FLIGHT }; +static struct { + char area[64], pos[64], event[80]; // q_lock: what the menu asked for + int state; // W_* + uint64_t at; // the tick of the next step, or the one at which the wait for the arrival ends + float from[3]; // where Link stood +} warp; +static char status[160]; // q_lock: the last thing that happened, for the menu + +static void status_set(const char *fmt, const char *a, const char *b) { + plat_mutex_lock(&q_lock); + snprintf(status, sizeof status, fmt, a, b); + plat_mutex_unlock(&q_lock); + rt_log("debug: %s", status); +} + +void dbg_status(char out[160]) { + plat_mutex_lock(&q_lock); + memcpy(out, status, sizeof status); + plat_mutex_unlock(&q_lock); +} + +int dbg_busy(void) { return warp.state != W_IDLE || warp.event[0]; } + +void dbg_warp(const char *area, const char *pos) { + plat_mutex_lock(&q_lock); + if (warp.state == W_IDLE && !warp.event[0] && area[0]) { + snprintf(warp.area, sizeof warp.area, "%s", area); + snprintf(warp.pos, sizeof warp.pos, "%s", pos); + warp.state = W_ASKED; + } + plat_mutex_unlock(&q_lock); +} + +void dbg_event_start(const char *name) { + plat_mutex_lock(&q_lock); + if (warp.state == W_IDLE && !warp.event[0]) snprintf(warp.event, sizeof warp.event, "%s", name); + plat_mutex_unlock(&q_lock); +} + +static const char REFUSED[] = "The game refused %s: it is in a cutscene, a conversation or a menu, or Link cannot take part " + "now (swimming, in the air). Try again once he stands free."; + +static void warp_tick(Cpu *c, uint64_t tick) { + if (warp.event[0]) { + char name[80]; + snprintf(name, sizeof name, "%s", warp.event); + status_set(player() && event_request(c, name) ? "Started the event %s." : REFUSED, name, ""); + plat_mutex_lock(&q_lock); + warp.event[0] = 0; + plat_mutex_unlock(&q_lock); + } + if (warp.state == W_ASKED) { + // the game's event that only waits: the change of area is asked for while it runs + if (player() && gmd() && dbg_player_pos(warp.from) && event_request(c, "DefaultParentEvent")) { + warp.at = tick + 4, warp.state = W_EVENT; + } else { + status_set(REFUSED, "the teleport", ""); + warp.state = W_IDLE; + } + } else if (warp.state == W_EVENT && tick >= warp.at) { + // what the game's own script reserves (its actor's ReserveBancChange): the next area and the start position's + // name, no event at the arrival, the event held during the change, a black fade, and the request itself + int ok = text_set("Sequence_ReservedNextBanc", warp.area) && text_set("Sequence_ReservedNextStartPosName", warp.pos); + text_set("Sequence_ReservedBootEventName", ""); + text_set("Sequence_ReservedEventStarterName", ""); + if (ok) { + rt_call(c, MOD_MAIN_BASE + 0x682b20, 3, gmd(), (uint64_t)1, (uint64_t)hash_of("Sequence_IsEventBancChangePause")); + enum_set(c, "Sequence_BancChangeFadePattern", "BlackFade"); + enum_set(c, "Sequence_ControlInEvent", "ChangeBancOnEvent"); + status_set("Teleporting to %s, %s...", warp.area, warp.pos[0] ? warp.pos : "its default start"); + warp.at = tick + 3600, warp.state = W_FLIGHT; + } else { + status_set("The teleport to %s could not be asked for: the game's texts for it are not there.", warp.area, ""); + warp.state = W_IDLE; + } + } else if (warp.state == W_FLIGHT) { + char now[64]; + float p[3]; + int there = text_get("Sequence_CurrentBanc", now) && !strcmp(now, warp.area) && dbg_player_pos(p) && + fabsf(p[0] - warp.from[0]) + fabsf(p[1] - warp.from[1]) + fabsf(p[2] - warp.from[2]) > 0.5f; + if (there || tick >= warp.at) { + status_set(there ? "Arrived in %s, %s." : "The teleport to %s, %s was asked for; no arrival was seen.", warp.area, + warp.pos[0] ? warp.pos : "its default start"); + warp.state = W_IDLE; + } + } +} + // ---- the memory search: values of 1, 2 or 4 bytes in the game's heap enum { RESULT_MAX = 2000000 }; static uint64_t *res_addr; @@ -396,10 +627,11 @@ int dbg_search_next(int mode, uint32_t value) { } // ---- 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. +// :next ;:setfirst ;:holdfirst ;:show []; +// :warp [];:event ;:text ": 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 char buf[2048]; static const char *next; static int state; if (!state) { @@ -417,6 +649,20 @@ static void test_script(uint64_t tick) { dbg_flag_set(hash, (int)on); } else if (sscanf(colon + 1, "speed %f", &f) == 1) { dbg_speed_set(f); + } else if (!strncmp(colon + 1, "warp ", 5)) { + char area[64] = "", pos[64] = ""; + if (sscanf(colon + 6, "%63[^ ;] %63[^ ;]", area, pos) >= 1) dbg_warp(area, pos); + } else if (!strncmp(colon + 1, "event ", 6)) { + char name[80] = ""; + if (sscanf(colon + 7, "%79[^ ;]", name) == 1) dbg_event_start(name); + } else if (!strncmp(colon + 1, "text ", 5)) { + char name[80] = "", now[64]; + float p[3] = {0, 0, 0}; + sscanf(colon + 6, "%79[^ ;]", name); + int got = text_get(name, now); + dbg_player_pos(p); + rt_log("debug: tick %llu: %s = \"%s\"%s; player %.1f %.1f %.1f", (unsigned long long)tick, name, now, + got ? "" : " (not found)", p[0], p[1], p[2]); } else if (sscanf(colon + 1, "setfirst %llu", &value) == 1) { dbg_mem_write(dbg_search_addr(0), value, dbg_search_size()); } else if (sscanf(colon + 1, "holdfirst %llu", &value) == 1) { @@ -458,6 +704,7 @@ void dbg_tick(Cpu *c) { if (!dbg_supported() || (!tick && !player())) return; tick++; test_script(tick); + warp_tick(c, tick); static Req todo[Q_MAX]; int n; plat_mutex_lock(&q_lock); diff --git a/runtime/debug_tools.h b/runtime/debug_tools.h index 98dc9b92..853543e0 100644 --- a/runtime/debug_tools.h +++ b/runtime/debug_tools.h @@ -25,6 +25,20 @@ 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) +// Teleporting and events. The places are the game's areas with their named start positions, and the events its event +// files that hold a script: both lists come from the player's game files with the flag names (0 until dbg_names +// answers 2). dbg_warp changes area as the game's own doors do; dbg_event_start starts an event by its name. One at a +// time: nothing is taken while dbg_busy. dbg_status is the last thing that happened, in words. +int dbg_places(void); +const char *dbg_place_area(int i); +const char *dbg_place_pos(int i); // "": the area's default start +void dbg_warp(const char *area, const char *pos); +int dbg_events(void); +const char *dbg_event_name(int i); +void dbg_event_start(const char *name); +int dbg_busy(void); +void dbg_status(char out[160]); + // 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 diff --git a/runtime/ui/debug_menu.cpp b/runtime/ui/debug_menu.cpp index f82a6b6a..54aca42e 100644 --- a/runtime/ui/debug_menu.cpp +++ b/runtime/ui/debug_menu.cpp @@ -1,8 +1,8 @@ // 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 +// debug_tools.c: the game's on/off flags by name, teleporting to the game's own start positions, starting its events, +// 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 @@ -128,6 +128,143 @@ void tab_flags(void) { } } +// ---- teleport and cutscenes +char t_filter[64], e_filter[64]; +int t_group; // 0 all, 1 the warp songs' pads, 2 the two sides of the loading zones, 3 boss rooms +int *t_rows, t_rows_n, t_count = -1, t_group_was = -1; +int *e_rows, e_rows_n, e_count = -1; +char t_filter_was[64] = "\x01", e_filter_was[64] = "\x01"; + +bool ends_with(const char *s, const char *end) { + size_t n = strlen(s), m = strlen(end); + return n >= m && !strcmp(s + n - m, end); +} + +// The groups go by the words the game's own names use: a warp song's pad is named after its song, a loading zone's +// far side after where it leads, and a boss's room is an area of its own whose name ends in Stage. +bool in_group(int group, const char *area, const char *pos) { + static const char *const songs[] = {"Minuet", "Bolero", "Serenade", "Requiem", "Nocturne", "Prelude"}; + static const char *const doors[] = {"From", "Side", "Entrance", "Front"}; + if (group == 1) { + for (const char *w : songs) + if (ends_with(pos, w)) return true; + return false; + } + if (group == 2) { + for (const char *w : doors) + if (strstr(pos, w)) return true; + return false; + } + if (group == 3) return ends_with(area, "Stage"); + return true; +} + +// The last thing the teleport or an event start did, and whether one is under way. +void status_line(void) { + char status[160]; + dbg_status(status); + if (status[0]) ImGui::TextWrapped("%s", status); +} + +// The lists come with the flag names, from the game's files: reading them starts once the game's flags are there. +bool lists_ready(const char *what) { + dbg_flags_refresh(); + if (dbg_names(NULL) == 2) return true; + ImGui::TextWrapped("Reading %s from the game's files... They appear once a save is loaded.", what); + return false; +} + +void tab_teleport(void) { + if (!lists_ready("the game's areas")) return; + int count = dbg_places(); + if (!count) { + ImGui::TextWrapped("The game's list of start positions was not found in its files."); + return; + } + ImGui::TextWrapped("Every area of the game with its start positions, from the game's own list. Link arrives through the " + "game's own change of area, as after a door or a warp song."); + static const char *const groups[] = {"All", "Warp song pads", "Loading zones", "Boss rooms"}; + for (int g = 0; g < 4; g++) { + if (g) ImGui::SameLine(); + bool is = t_group == g; + if (is) ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive)); + if (ImGui::Button(groups[g])) t_group = g; + if (is) ImGui::PopStyleColor(); + } + ImGui::SetNextItemWidth(-1); + ImGui::InputTextWithHint("##tfilter", "part of an area's or a position's name", t_filter, sizeof t_filter); + if (count != t_count || t_group != t_group_was || strcmp(t_filter, t_filter_was)) { + if (!t_rows) t_rows = (int *)malloc(sizeof(int) * 4096); + t_rows_n = 0; + for (int i = 0; t_rows && i < count && i < 4096; i++) { + const char *area = dbg_place_area(i), *pos = dbg_place_pos(i); + if (!in_group(t_group, area, pos)) continue; + if (t_filter[0] && !contains(area, t_filter) && !contains(pos, t_filter)) continue; + t_rows[t_rows_n++] = i; + } + t_count = count, t_group_was = t_group; + snprintf(t_filter_was, sizeof t_filter_was, "%s", t_filter); + if (getenv("NX2NAVI_DBG_TEST")) rt_log("debug: teleport list: group %d, filter \"%s\": %d of %d places", t_group, t_filter, t_rows_n, count); + } + status_line(); + bool busy = dbg_busy() != 0; + const float foot = ImGui::GetTextLineHeightWithSpacing() * 3.2f; + if (ImGui::BeginChild("##places", ImVec2(0, -foot), ImGuiChildFlags_Borders)) { + if (busy) ImGui::BeginDisabled(); + ImGuiListClipper clip; + clip.Begin(t_rows_n); + while (clip.Step()) + for (int r = clip.DisplayStart; r < clip.DisplayEnd; r++) { + int i = t_rows[r]; + const char *area = dbg_place_area(i), *pos = dbg_place_pos(i); + char label[160]; + snprintf(label, sizeof label, "%s > %s##%d", area, pos[0] ? pos : "(default start)", i); + if (ImGui::Selectable(label)) dbg_warp(area, pos); + } + if (busy) ImGui::EndDisabled(); + } + ImGui::EndChild(); + ImGui::TextWrapped("Areas ending in _Child or _Adult are the two times of the same place: Link arrives at the age he has. " + "The game may save by itself in the new area, as it does after its own doors: keep a copy of the save."); +} + +void tab_events(void) { + if (!lists_ready("the game's events")) return; + int count = dbg_events(); + ImGui::TextWrapped("The game's event scripts by name: cutscenes (most start with Dm_), conversations and scripted moments. " + "One starts as if its trigger had fired, in the area Link is in."); + ImGui::SetNextItemWidth(-1); + ImGui::InputTextWithHint("##efilter", "part of a name", e_filter, sizeof e_filter); + if (count != e_count || strcmp(e_filter, e_filter_was)) { + if (!e_rows) e_rows = (int *)malloc(sizeof(int) * 4096); + e_rows_n = 0; + for (int i = 0; e_rows && i < count && i < 4096; i++) + if (!e_filter[0] || contains(dbg_event_name(i), e_filter)) e_rows[e_rows_n++] = i; + e_count = count; + snprintf(e_filter_was, sizeof e_filter_was, "%s", e_filter); + if (getenv("NX2NAVI_DBG_TEST")) rt_log("debug: event list: filter \"%s\": %d of %d events", e_filter, e_rows_n, count); + } + status_line(); + bool busy = dbg_busy() != 0; + const float foot = ImGui::GetTextLineHeightWithSpacing() * 4.2f; + if (ImGui::BeginChild("##events", ImVec2(0, -foot), ImGuiChildFlags_Borders)) { + if (busy) ImGui::BeginDisabled(); + ImGuiListClipper clip; + clip.Begin(e_rows_n); + while (clip.Step()) + for (int r = clip.DisplayStart; r < clip.DisplayEnd; r++) { + char label[112]; + snprintf(label, sizeof label, "%s##%d", dbg_event_name(e_rows[r]), e_rows[r]); + if (ImGui::Selectable(label)) dbg_event_start(dbg_event_name(e_rows[r])); + } + if (busy) ImGui::EndDisabled(); + } + ImGui::EndChild(); + ImGui::TextWrapped("An event made for another area, or for actors that are not here, may show nothing, end at once or leave " + "the game waiting: teleport to its area first. An event can set the story's flags and can move Link to " + "another area, as it does in the game: keep a copy of the save."); +} + // ---- world void tab_world(void) { float p[3]; @@ -151,9 +288,7 @@ void tab_world(void) { 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)."); + ImGui::TextWrapped("Not in this menu yet: the free camera."); } // ---- memory @@ -293,11 +428,13 @@ extern "C" void debug_menu_layout(uint32_t w, uint32_t h, float 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++) { + static const char *const names[DEBUG_MENU_TABS] = {"Flags", "Teleport", "Cutscenes", "World", "Memory"}; + for (int t = 0; t < DEBUG_MENU_TABS; 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 if (t == 1) tab_teleport(); + else if (t == 2) tab_events(); + else if (t == 3) tab_world(); else tab_memory(); ImGui::EndTabItem(); } diff --git a/runtime/ui/debug_menu.h b/runtime/ui/debug_menu.h index 7df76a0e..ce121f13 100644 --- a/runtime/ui/debug_menu.h +++ b/runtime/ui/debug_menu.h @@ -6,8 +6,8 @@ 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 +void debug_menu_tab(int tab); // the tab to show at the next layout (test hook): 0 to 4 +#define DEBUG_MENU_TABS 5 #ifdef __cplusplus } #endif diff --git a/runtime/ui/menu.cpp b/runtime/ui/menu.cpp index 6a041960..67b0c98d 100644 --- a/runtime/ui/menu.cpp +++ b/runtime/ui/menu.cpp @@ -1264,9 +1264,10 @@ static void tab_game(float scale) { 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)) + "F9 opens a window for testing: the game's flags by name, a teleport to every area's start " + "positions, its cutscenes to start, the game's speed, and a search of its memory with values " + "to set and hold. All of these can break a save, and the game saves by itself after a change " + "of area: keep a copy of the save first.", on)) opt_apply("NX2NAVI_DEBUG_MENU", on ? "0" : "1"); } opt_end();