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();