mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
With debug_menu = 1 (off by default; the Game tab has the switch) F9 opens a panel on the left edge in the performance overlay's look, with the game in view beside it. ui/menu.cpp lays it out in the settings menu's place, so its input and the game's blocked pad are the menu's own. Typed characters now reach the menu for its text fields. Flags: the game's 11,292 on/off flags are read from its game data manager, and a click changes one through the game's own function on the game's thread at its pad read (runtime/debug_tools.c dbg_tick), so the game applies it at its next update as it does its own changes. The game knows a flag by the hash of its name only; the names shown are found in the player's own event flow files, hashed and matched when the panel first opens (2,250 of them from 915 files in 0.2 s), and none is kept in the code. World: Link's position, and a speed factor from 0.05 to 8 that multiplies the step the runtime gives the game each frame (measured 0.50 at 0.5x). Memory: a search of the heap for a value of 1, 2 or 4 bytes or a float (about 2 s), narrowed by equals, changed, unchanged, went up or went down, with a value to set at a place or to hold there every frame. Tested through the window's own messages: F9 opened it, a click on a flag set that flag in the game, text typed into the filter narrowed the list, and F9 closed it; a scripted run set and held a value and put it back. Teleporting, a free camera and starting cutscenes are not in it: writing Link's position, or every copy of it in the heap, moved him back within three frames, so they need the game's own warp, scene-change and event functions. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
311 lines
14 KiB
C++
311 lines
14 KiB
C++
// The debug menu's panel (F9 with debug_menu = 1; ui/menu.cpp lays it out in the settings menu's place while it is
|
|
// open, so its input, its sounds and the game's blocked pad are the menu's own): on the left edge, in the performance
|
|
// overlay's look, with the game in view beside it. What it does to the game is in
|
|
// debug_tools.c: the game's on/off flags by name, the game's speed, Link's position, and a search of the game's memory
|
|
// with values to set and to hold. Plain Dear ImGui with English text: a tool for testing, not part of the game's
|
|
// interface. Present thread only.
|
|
#include <algorithm>
|
|
#include <ctype.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "imgui.h"
|
|
|
|
#include "debug_tools.h"
|
|
extern "C" void rt_log(const char *fmt, ...);
|
|
#include "ui/debug_menu.h"
|
|
#include "ui/menu_skin.h"
|
|
|
|
namespace {
|
|
|
|
int g_tab = -1; // a tab asked for by the test hook (debug_menu_tab), -1 none
|
|
|
|
// ---- flags
|
|
char f_filter[64];
|
|
bool f_only_on, f_only_named;
|
|
int *f_rows; // the flags that pass the filter
|
|
int f_rows_n, f_count;
|
|
int f_age = 1000; // frames since the values were read
|
|
bool f_dirty = true;
|
|
struct Change { uint32_t hash; int on; char name[72]; } f_log[6];
|
|
int f_log_n;
|
|
|
|
bool contains(const char *hay, const char *needle) {
|
|
size_t n = strlen(needle);
|
|
for (; *hay; hay++) {
|
|
size_t i = 0;
|
|
while (i < n && hay[i] && tolower((unsigned char)hay[i]) == tolower((unsigned char)needle[i])) i++;
|
|
if (i == n) return true;
|
|
}
|
|
return !n;
|
|
}
|
|
|
|
void flags_filter(void) {
|
|
if (!f_rows) f_rows = (int *)malloc(sizeof(int) * 200000);
|
|
f_rows_n = 0;
|
|
if (!f_rows) return;
|
|
char hex[12];
|
|
for (int i = 0; i < f_count && i < 200000; i++) {
|
|
const char *name = dbg_flag_name(i);
|
|
if (f_only_on && !dbg_flag_value(i)) continue;
|
|
if (f_only_named && !name) continue;
|
|
if (f_filter[0]) {
|
|
snprintf(hex, sizeof hex, "%08x", dbg_flag_hash(i));
|
|
if (!(name && contains(name, f_filter)) && !contains(hex, f_filter)) continue;
|
|
}
|
|
f_rows[f_rows_n++] = i;
|
|
}
|
|
// the named ones first, by name; the rest by hash, as the back end lists them
|
|
std::stable_sort(f_rows, f_rows + f_rows_n, [](int a, int b) {
|
|
const char *x = dbg_flag_name(a), *y = dbg_flag_name(b);
|
|
if (!x || !y) return x && !y;
|
|
return strcmp(x, y) < 0;
|
|
});
|
|
f_dirty = false;
|
|
if (getenv("NX2NAVI_DBG_TEST")) rt_log("debug: filter \"%s\": %d flags shown", f_filter, f_rows_n); // test runs
|
|
}
|
|
|
|
void tab_flags(void) {
|
|
if (++f_age > 20) {
|
|
int n = dbg_flags_refresh();
|
|
if (n != f_count) f_count = n, f_dirty = true;
|
|
f_age = 0;
|
|
if (f_only_on) f_dirty = true; // values moved
|
|
}
|
|
if (!f_count) {
|
|
ImGui::TextWrapped("The game's flags appear once a save is loaded.");
|
|
return;
|
|
}
|
|
static int names_was;
|
|
int found = 0, names = dbg_names(&found);
|
|
if (names != names_was) names_was = names, f_dirty = true;
|
|
if (names == 2) ImGui::TextWrapped("%d on/off flags of the game, %d with a name found in its event files.", f_count, found);
|
|
else ImGui::TextWrapped("%d on/off flags of the game. Reading their names from the game's event files...", f_count);
|
|
ImGui::SetNextItemWidth(-1);
|
|
if (ImGui::InputTextWithHint("##filter", "part of a name, or of the hash", f_filter, sizeof f_filter)) f_dirty = true;
|
|
if (ImGui::Checkbox("only on", &f_only_on)) f_dirty = true;
|
|
ImGui::SameLine();
|
|
if (ImGui::Checkbox("only named", &f_only_named)) f_dirty = true;
|
|
if (f_dirty) flags_filter();
|
|
ImGui::SameLine();
|
|
ImGui::TextDisabled("%d shown", f_rows_n);
|
|
float log_h = f_log_n ? ImGui::GetTextLineHeightWithSpacing() * (float)(f_log_n + 1) + 8 : 0;
|
|
if (ImGui::BeginChild("##flags", ImVec2(0, -log_h), ImGuiChildFlags_Borders)) {
|
|
ImGuiListClipper clip;
|
|
clip.Begin(f_rows_n);
|
|
while (clip.Step())
|
|
for (int r = clip.DisplayStart; r < clip.DisplayEnd; r++) {
|
|
int i = f_rows[r];
|
|
const char *name = dbg_flag_name(i);
|
|
uint32_t hash = dbg_flag_hash(i);
|
|
bool on = dbg_flag_value(i) != 0;
|
|
char label[128];
|
|
if (name) snprintf(label, sizeof label, "%s##%08x", name, hash);
|
|
else snprintf(label, sizeof label, "(no name) %08x##%08x", hash, hash);
|
|
if (ImGui::Checkbox(label, &on)) {
|
|
dbg_flag_set(hash, on);
|
|
f_age = 15; // read again in a few frames: the game applies the change at its next update
|
|
for (int k = f_log_n < 6 ? f_log_n : 5; k > 0; k--) f_log[k] = f_log[k - 1];
|
|
f_log[0].hash = hash, f_log[0].on = on;
|
|
snprintf(f_log[0].name, sizeof f_log[0].name, "%s", name ? name : "(no name)");
|
|
if (f_log_n < 6) f_log_n++;
|
|
}
|
|
}
|
|
}
|
|
ImGui::EndChild();
|
|
if (f_log_n) {
|
|
ImGui::TextDisabled("Changed in this session (click to put back):");
|
|
for (int k = 0; k < f_log_n; k++) {
|
|
char label[128];
|
|
snprintf(label, sizeof label, "%s %08x -> %s##log%d", f_log[k].name, f_log[k].hash, f_log[k].on ? "on" : "off", k);
|
|
if (ImGui::SmallButton(label)) {
|
|
dbg_flag_set(f_log[k].hash, !f_log[k].on);
|
|
f_log[k].on = !f_log[k].on;
|
|
f_age = 15;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- world
|
|
void tab_world(void) {
|
|
float p[3];
|
|
if (dbg_player_pos(p)) ImGui::Text("Link is at x %.2f y %.2f z %.2f (metres; y is up)", p[0], p[1], p[2]);
|
|
else ImGui::TextDisabled("Link's position appears once a save is loaded.");
|
|
ImGui::Separator();
|
|
float speed = dbg_speed_factor();
|
|
ImGui::Text("Game speed");
|
|
static const float steps[] = {0.1f, 0.25f, 0.5f, 1.0f, 2.0f, 4.0f};
|
|
for (float s : steps) {
|
|
char label[16];
|
|
snprintf(label, sizeof label, s < 1 ? "%.2gx" : "%.0fx", s);
|
|
if (s != steps[0]) ImGui::SameLine();
|
|
bool is = speed == s;
|
|
if (is) ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive));
|
|
if (ImGui::Button(label)) dbg_speed_set(s);
|
|
if (is) ImGui::PopStyleColor();
|
|
}
|
|
ImGui::SetNextItemWidth(-1);
|
|
if (ImGui::SliderFloat("##speed", &speed, 0.05f, 8.0f, "%.2fx", ImGuiSliderFlags_Logarithmic)) dbg_speed_set(speed);
|
|
ImGui::TextWrapped("The whole game runs at this speed, its interface and its music's timing included. It goes back to 1x when "
|
|
"the game is closed.");
|
|
ImGui::Separator();
|
|
ImGui::TextWrapped("Not in this menu yet: teleporting, the free camera and starting cutscenes. They need the game's own "
|
|
"scene-change, warp and event functions, which are not connected yet; writing Link's position into "
|
|
"memory does not move him (the physics puts him back).");
|
|
}
|
|
|
|
// ---- memory
|
|
int m_type; // 0: 4 bytes, 1: 2 bytes, 2: 1 byte, 3: float
|
|
char m_value[32] = "0";
|
|
char m_set[32];
|
|
char m_note[160];
|
|
const int m_sizes[] = {4, 2, 1, 4};
|
|
|
|
bool parse_value(const char *s, uint32_t *out) {
|
|
char *end = NULL;
|
|
if (m_type == 3) {
|
|
float f = strtof(s, &end);
|
|
memcpy(out, &f, 4);
|
|
} else {
|
|
unsigned long long v = strtoull(s, &end, 0);
|
|
if (v >> (8 * m_sizes[m_type])) return false;
|
|
*out = (uint32_t)v;
|
|
}
|
|
return end && end != s;
|
|
}
|
|
|
|
void show_value(char *out, size_t n, uint32_t v) {
|
|
if (m_type == 3) {
|
|
float f;
|
|
memcpy(&f, &v, 4);
|
|
snprintf(out, n, "%g", (double)f);
|
|
} else {
|
|
snprintf(out, n, "%u", v);
|
|
}
|
|
}
|
|
|
|
void tab_memory(void) {
|
|
ImGui::TextWrapped("Finds a value in the game's memory: search for what a number is now, change it in the game, then narrow "
|
|
"the places down until few are left. Setting or holding a wrong place can break the running game; a save "
|
|
"made before is the way back.");
|
|
const float field = ImGui::GetFontSize() * 12;
|
|
ImGui::SetNextItemWidth(field);
|
|
int over = 0, count = dbg_search_count(&over);
|
|
if (count) ImGui::BeginDisabled();
|
|
ImGui::Combo("##type", &m_type, "4 bytes (whole number)\0002 bytes\0001 byte\000float (4 bytes)\000");
|
|
if (count) ImGui::EndDisabled();
|
|
ImGui::SameLine();
|
|
ImGui::SetNextItemWidth(field * 0.6f);
|
|
ImGui::InputText("value", m_value, sizeof m_value, ImGuiInputTextFlags_CharsScientific);
|
|
uint32_t v = 0;
|
|
bool ok = parse_value(m_value, &v);
|
|
if (!count) {
|
|
if (!ok) ImGui::BeginDisabled();
|
|
if (ImGui::Button("Search all memory")) {
|
|
int n = dbg_search_new(v, m_sizes[m_type]);
|
|
dbg_search_count(&over);
|
|
snprintf(m_note, sizeof m_note, over ? "More than %d places hold it: only these are kept. Search for a rarer value, or narrow down."
|
|
: "%d places hold it.", n);
|
|
}
|
|
if (!ok) ImGui::EndDisabled();
|
|
ImGui::SameLine();
|
|
ImGui::TextDisabled("(takes a few seconds; the game waits)");
|
|
} else {
|
|
static const char *const modes[] = {"equals the value", "changed", "unchanged", "went up", "went down"};
|
|
for (int m = 0; m < 5; m++) {
|
|
if (m && m != 3) ImGui::SameLine(); // two rows: the panel is narrow
|
|
if (m == 0 && !ok) ImGui::BeginDisabled();
|
|
if (ImGui::Button(modes[m])) snprintf(m_note, sizeof m_note, "%d places left.", dbg_search_next(m, v));
|
|
if (m == 0 && !ok) ImGui::EndDisabled();
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("New search")) dbg_search_clear(), m_note[0] = 0;
|
|
}
|
|
if (m_note[0]) ImGui::TextUnformatted(m_note);
|
|
int frozen = dbg_frozen_count();
|
|
if (frozen) {
|
|
char label[48];
|
|
snprintf(label, sizeof label, "Stop holding %d value%s", frozen, frozen == 1 ? "" : "s");
|
|
if (ImGui::Button(label)) dbg_unfreeze_all();
|
|
}
|
|
count = dbg_search_count(&over);
|
|
if (!count) return;
|
|
ImGui::SetNextItemWidth(field * 0.6f);
|
|
ImGui::InputText("value to set", m_set, sizeof m_set, ImGuiInputTextFlags_CharsScientific);
|
|
uint32_t setv = 0;
|
|
bool set_ok = parse_value(m_set, &setv);
|
|
int size = dbg_search_size();
|
|
if (ImGui::BeginChild("##results", ImVec2(0, 0), ImGuiChildFlags_Borders)) {
|
|
int shown = count < 300 ? count : 300;
|
|
if (count > shown) ImGui::TextDisabled("The first %d of %d places:", shown, count);
|
|
for (int i = 0; i < shown; i++) {
|
|
uint64_t a = dbg_search_addr(i);
|
|
uint32_t now = 0;
|
|
char text[32] = "?";
|
|
if (dbg_mem_read(a, size, &now)) show_value(text, sizeof text, now);
|
|
ImGui::PushID(i);
|
|
ImGui::Text("%012llx %-14s", (unsigned long long)a, text);
|
|
ImGui::SameLine();
|
|
if (!set_ok) ImGui::BeginDisabled();
|
|
if (ImGui::SmallButton("Set")) dbg_mem_write(a, setv, size);
|
|
if (!set_ok) ImGui::EndDisabled();
|
|
ImGui::SameLine();
|
|
bool hold = dbg_frozen(a) != 0;
|
|
if (ImGui::Checkbox("hold", &hold)) {
|
|
if (!dbg_freeze(a, set_ok ? setv : now, size, hold)) snprintf(m_note, sizeof m_note, "No more than 64 values can be held.");
|
|
}
|
|
ImGui::PopID();
|
|
}
|
|
}
|
|
ImGui::EndChild();
|
|
}
|
|
|
|
} // namespace
|
|
|
|
extern "C" void debug_menu_tab(int tab) { g_tab = tab; }
|
|
|
|
// A panel on the left edge in the performance overlay's dress (the game's panel art, gold labels, the smaller text),
|
|
// as tall as the window: the game stays in view beside it while a flag or a value is changed.
|
|
extern "C" void debug_menu_layout(uint32_t w, uint32_t h, float scale) {
|
|
const float m = 12 * scale;
|
|
const ImVec4 gold(236 / 255.0f, 196 / 255.0f, 104 / 255.0f, 1), dim(0.72f, 0.69f, 0.62f, 1);
|
|
float width = (float)w * 0.27f, min_width = 560 * scale;
|
|
if (width < min_width) width = min_width < (float)w - 2 * m ? min_width : (float)w - 2 * m;
|
|
ImGui::SetNextWindowPos(ImVec2(m, m), ImGuiCond_Always);
|
|
ImGui::SetNextWindowSize(ImVec2(width, (float)h - 2 * m), ImGuiCond_Always);
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0, 0, 0, 0)); // the panel below is the background
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0);
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(24 * scale, 20 * scale));
|
|
bool shown = ImGui::Begin("##debug", NULL, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings);
|
|
ImGui::PopStyleVar(2);
|
|
ImGui::PopStyleColor();
|
|
if (shown) {
|
|
ImDrawList *dl = ImGui::GetWindowDrawList();
|
|
ImVec2 a = ImGui::GetWindowPos(), sz = ImGui::GetWindowSize();
|
|
skin_panel(dl, a, ImVec2(a.x + sz.x, a.y + sz.y), scale * 0.8f, 0.92f);
|
|
ImGui::SetWindowFontScale(0.72f);
|
|
ImGui::TextColored(gold, "Debug menu");
|
|
ImGui::SameLine();
|
|
ImGui::TextColored(dim, " F9 or Esc closes");
|
|
ImGui::Dummy(ImVec2(0, 2 * scale));
|
|
if (!dbg_supported()) {
|
|
ImGui::TextWrapped("The debug menu knows the structures of Ocarina of Time 1.1.1 only.");
|
|
} else if (ImGui::BeginTabBar("##tabs")) {
|
|
static const char *const names[] = {"Flags", "World", "Memory"};
|
|
for (int t = 0; t < 3; t++) {
|
|
if (ImGui::BeginTabItem(names[t], NULL, g_tab == t ? ImGuiTabItemFlags_SetSelected : 0)) {
|
|
if (t == 0) tab_flags();
|
|
else if (t == 1) tab_world();
|
|
else tab_memory();
|
|
ImGui::EndTabItem();
|
|
}
|
|
}
|
|
g_tab = -1;
|
|
ImGui::EndTabBar();
|
|
}
|
|
}
|
|
ImGui::End();
|
|
}
|