Files

399 lines
20 KiB
C

// The mouse pointer in TotK 1.4.3's own menus (the title, the inventory, the System tab, Options, shops, dialogue
// choices): while a menu screen has the game's focus box (eui's BoxCursor, the frame the d-pad moves), the Windows
// pointer is released from the mouse camera and shown, the item under it takes the focus as if the d-pad had moved
// there (highlight, sound and description included), a left click on an item is A on it and a right click is B.
// Nothing is drawn and no game code is called: at each pad read (ov_input.c GetNpadStates, the game thread) the open
// screens are read and the focus is moved with the three writes eui::BoxCursorMgr::moveBoxCursor makes, so the game's
// own BoxCursorControl moves the box at its next update.
//
// Adapted from Weeping of the Empire's engine/runtime/totk_menu_cursor.cpp @ df0586cdc5702ca19bbebfa59697e0b6980f6237
// (https://github.com/daniel-madrid-07/weeping-of-the-empire), Copyright (c) 2026 daniel-madrid-07, MIT License: the
// focus read, the hit test, the stale-hit-pane fallback and the focus writes, which it does for 1.0.0.
// Placed in 1.4.3 by hand:
// - eui::ScreenMgr::sInstance: 1.0.0 main+0x4640010, 1.4.3 main+0x3acf960 (its createInstance main+0xc613d4 and
// constructor main+0xc61450 match 1.0.0's main+0x119c5cc / +0x119c648 instruction for instruction, the object
// 0x450 bytes with the same field stores, so its layout is 1.0.0's);
// - the screen's reserved node (+0x150) and request bits (+0x175, bit 2) and its draw target byte (+0x16c): the same
// stores at 1.4.3 main+0x87903c.
// The other fields are 1.0.0's; menu_cursor logs what it reads the first time a menu opens (TOTK_MENU_CURSOR_LOG=1:
// every screen change), so a field that moved shows as a menu with no nodes or boxes off the layout.
//
// Only while keyboard and mouse are the device used last (input_kbm_last): a gamepad player sees the menus unchanged.
// TOTK_MENU_CURSOR (live, default 1): 0 leaves the menus to the keys and the pad.
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include "cfg.h"
#include "gfx.h"
#include "host.h"
#include "input.h"
#include "input_map.h"
#include "input_wsys.h"
#include "layout.h"
#include "platform/platform.h"
#define SCREEN_MGR_V143 0x3acf960ull // eui::ScreenMgr::sInstance
#define MAIN_SIZE 0x3adf000ull // main's image (code, data, bss) in 1.4.3
// eui::ScreenMgr, BoxCursorMgr, BoxCursorControl
enum { SM_BOX_MGR = 0x3f8, BM_CONTROLS = 0x28, CTL_ACTIVE = 0x58, CTL_RESERVED = 0x60 };
// eui::Screen
enum { SCR_LAYOUT = 0x28, SCR_DRAW_INFO = 0x60, SCR_NODES = 0x80, SCR_RESERVED = 0x150, SCR_STATE = 0x16e,
SCR_REQUEST = 0x175, SCR_OPENED = 2 };
// eui::BoxCursorNode, AnimButton
enum { NODE_BUTTON = 0x18, NODE_SCREEN = 0x20, BTN_LAYOUT = 0x20, BTN_FLAGS = 0x40, BTN_HIT_PANE = 0x58 };
#define BTN_USABLE 0x20010u // both set: active and enabled (ButtonGroup::Update's test)
// nn::ui2d::DrawInfo: projection (4 x 4) and view (3 x 4); nn::ui2d::Layout: root pane, size, name
enum { DI_PROJ = 0x10, DI_VIEW = 0x50, LYT_ROOT = 0x18, LYT_SIZE = 0x28, LYT_NAME = 0x30 };
// nn::ui2d::Pane
enum { PANE_PARENT = 0x18, PANE_SIZE = 0x50, PANE_FLAG = 0x58, PANE_ALPHA = 0x59, PANE_BASE = 0x5b, PANE_MTX = 0x70 };
static uint64_t main_lo, main_hi;
static int ok_at(uint64_t p, uint64_t n) {
return p >= 0x10000 && p < (1ull << 47) && (p & 7) == 0 && mem_guest_readable(p, n);
}
static uint64_t rd64(uint64_t a) { uint64_t v; memcpy(&v, (const void *)(uintptr_t)a, 8); return v; }
static uint32_t rd32(uint64_t a) { uint32_t v; memcpy(&v, (const void *)(uintptr_t)a, 4); return v; }
static uint8_t rd8(uint64_t a) { return *(const volatile uint8_t *)(uintptr_t)a; }
static float rdf(uint64_t a) { float v; memcpy(&v, (const void *)(uintptr_t)a, 4); return v; }
// An object of the game: readable, its vtable in main's image
static int game_object(uint64_t p, uint64_t n) {
if (!ok_at(p, n)) return 0;
uint64_t vt = rd64(p);
return vt >= main_lo && vt < main_hi;
}
// ---- panes (AnimButton::HitTest's rules: 1.0.0 main+0x222ea40, +0x223b090)
static int pane_shown(uint64_t pane) {
if (!ok_at(pane, PANE_MTX + 0x20)) return 0;
if (!(rd8(pane + PANE_FLAG) & 1) || !rd8(pane + PANE_ALPHA)) return 0;
int depth = 0;
for (uint64_t p = rd64(pane + PANE_PARENT); p; p = rd64(p + PANE_PARENT)) {
if (!ok_at(p, PANE_MTX) || ++depth > 64) return 0;
uint8_t f = rd8(p + PANE_FLAG);
if (!(f & 1) || (!rd8(p + PANE_ALPHA) && (f & 2))) return 0;
}
return 1;
}
// The pane's box in layout coordinates (origin at the centre, y up): centre and half size
static void pane_box(uint64_t pane, float *cx, float *cy, float *hw, float *hh) {
float w = rdf(pane + PANE_SIZE), h = rdf(pane + PANE_SIZE + 4);
float m00 = rdf(pane + PANE_MTX), m11 = rdf(pane + PANE_MTX + 0x14);
uint8_t base = rd8(pane + PANE_BASE), bh = base & 3, bv = base >> 2 & 3;
*hw = fabsf(w * m00) * 0.5f, *hh = fabsf(h * m11) * 0.5f;
*cx = rdf(pane + PANE_MTX + 0xc), *cy = rdf(pane + PANE_MTX + 0x1c);
if (bh == 1) *cx += *hw; else if (bh == 2) *cx -= *hw; // the translation is the left / right edge
if (bv == 1) *cy -= *hh; else if (bv == 2) *cy += *hh; // ... the top / bottom edge
}
// The screen's draw transform, for a screen drawn away from its centred layout space (scale and translation only)
typedef struct { int ok; float v[12], p[16]; } Xf;
static Xf screen_xf(uint64_t scr) {
Xf x = {0};
uint64_t di = rd64(scr + SCR_DRAW_INFO);
if (!ok_at(di, DI_VIEW + sizeof x.v)) return x;
memcpy(x.p, (const void *)(uintptr_t)(di + DI_PROJ), sizeof x.p);
memcpy(x.v, (const void *)(uintptr_t)(di + DI_VIEW), sizeof x.v);
for (int i = 0; i < 16; i++) if (!isfinite(x.p[i])) return x;
for (int i = 0; i < 12; i++) if (!isfinite(x.v[i])) return x;
x.ok = x.p[0] != 0 && x.p[5] != 0 && fabsf(x.p[1]) < 1e-5f && fabsf(x.p[4]) < 1e-5f && fabsf(x.v[1]) < 1e-4f &&
fabsf(x.v[4]) < 1e-4f;
return x;
}
static int to_ndc(const Xf *t, float x, float y, float *nx, float *ny) {
float vx = t->v[0] * x + t->v[1] * y + t->v[3], vy = t->v[4] * x + t->v[5] * y + t->v[7],
vz = t->v[8] * x + t->v[9] * y + t->v[11];
float cx = t->p[0] * vx + t->p[1] * vy + t->p[2] * vz + t->p[3];
float cy = t->p[4] * vx + t->p[5] * vy + t->p[6] * vz + t->p[7];
float cw = t->p[12] * vx + t->p[13] * vy + t->p[14] * vz + t->p[15];
if (!(fabsf(cw) > 1e-6f)) return 0;
*nx = cx / cw, *ny = cy / cw;
return 1;
}
// The transform maps the layout as the plain layout test does (centre to 0,0, corner to 1,1): it adds nothing
static int xf_plain(const Xf *t, float lw, float lh) {
float ax, ay, bx, by;
if (!to_ndc(t, 0, 0, &ax, &ay) || !to_ndc(t, lw / 2, lh / 2, &bx, &by)) return 1;
return fabsf(ax) < 0.01f && fabsf(ay) < 0.01f && fabsf(bx - 1) < 0.01f && fabsf(by - 1) < 0.01f;
}
// The pointer: layout coordinates (x, y) and device coordinates (nx, ny), and the transform when it is not plain
typedef struct { float x, y, nx, ny; const Xf *t; } Pt;
static int pane_hit(uint64_t pane, const Pt *p) {
if (!pane_shown(pane)) return 0;
float cx, cy, hw, hh;
pane_box(pane, &cx, &cy, &hw, &hh);
if (!p->t) return p->x >= cx - hw && p->x <= cx + hw && p->y >= cy - hh && p->y <= cy + hh;
float ax, ay, bx, by;
if (!to_ndc(p->t, cx - hw, cy - hh, &ax, &ay) || !to_ndc(p->t, cx + hw, cy + hh, &bx, &by)) return 0;
return p->nx >= fminf(ax, bx) && p->nx <= fmaxf(ax, bx) && p->ny >= fminf(ay, by) && p->ny <= fmaxf(ay, by);
}
// ---- screens and nodes
typedef struct { uint64_t control, node, screen; } Focus;
static uint64_t screen_mgr(void) {
uint64_t slot = MOD_MAIN_BASE + SCREEN_MGR_V143, sm = rd64(slot);
return game_object(sm, SM_BOX_MGR + 8) ? sm : 0;
}
// The focused node of an open screen, from the draw targets' box cursors (all 0: none, as in gameplay)
static Focus focus_now(void) {
Focus none = {0};
uint64_t sm = screen_mgr(), bm = sm ? rd64(sm + SM_BOX_MGR) : 0;
if (!ok_at(bm, BM_CONTROLS + 16)) return none;
for (int i = 0; i < 2; i++) {
uint64_t ctl = rd64(bm + BM_CONTROLS + 8ull * i);
if (!ok_at(ctl, CTL_RESERVED + 8)) continue;
uint64_t node = rd64(ctl + CTL_ACTIVE);
if (!ok_at(node, NODE_SCREEN + 8)) continue;
uint64_t scr = rd64(node + NODE_SCREEN);
if (!game_object(scr, SCR_REQUEST + 1) || rd8(scr + SCR_STATE) != SCR_OPENED) continue;
return (Focus){ctl, node, scr};
}
return none;
}
static void layout_name(uint64_t scr, char *out, size_t n) {
snprintf(out, n, "?");
uint64_t lyt = rd64(scr + SCR_LAYOUT), name = ok_at(lyt, LYT_NAME + 8) ? rd64(lyt + LYT_NAME) : 0;
if (name < 0x10000 || name >= (1ull << 47) || !mem_guest_readable(name, n)) return;
memcpy(out, (const void *)(uintptr_t)name, n - 1);
out[n - 1] = 0;
for (char *q = out; *q; q++) if (*q < 0x20 || *q > 0x7e) { *q = 0; break; }
}
// The open screens: eui::ScreenMgr's screen array, count at +0x8 and its pointers at +0x10 (1.0.0's layout, which the
// 1.4.3 constructor keeps). The map has no focus box, so it is found here: the Purah Pad's frame (AppMenuOverlay_00)
// with AppMap_00 and the menu's background AppMenuBG_00 open. The Scope opens the frame over AppMap_00 without that
// background (Weeping of the Empire's engine/runtime/totk_menu_cursor.cpp census tells them apart the same way).
// TOTK_MENU_CURSOR_LOG=1 logs the open screens whenever the set changes.
enum { SM_COUNT = 0x8, SM_SCREENS = 0x10 };
static int map_open(void) {
uint64_t sm = screen_mgr();
if (!sm) return 0;
int32_t n = (int32_t)rd32(sm + SM_COUNT);
uint64_t arr = rd64(sm + SM_SCREENS);
if (n <= 0 || n > 1024 || !ok_at(arr, 8ull * (uint64_t)n)) return 0;
const char *e = cfg_get("TOTK_MENU_CURSOR_LOG");
int verbose = e && !strcmp(e, "1"), pad = 0, map = 0, bg = 0;
char list[1024];
size_t len = 0;
uint64_t h = 1469598103934665603ull;
for (int32_t i = 0; i < n; i++) {
uint64_t scr = rd64(arr + 8ull * (uint64_t)i);
if (!game_object(scr, SCR_REQUEST + 1) || rd8(scr + SCR_STATE) != SCR_OPENED) continue;
char nm[48];
layout_name(scr, nm, sizeof nm);
pad |= !strcmp(nm, "AppMenuOverlay_00");
map |= !strcmp(nm, "AppMap_00");
bg |= !strcmp(nm, "AppMenuBG_00");
if (!verbose) continue;
for (const char *c = nm; *c; c++) h = (h ^ (uint8_t)*c) * 1099511628211ull;
if (len < sizeof list - 64) len += (size_t)snprintf(list + len, sizeof list - len, "%s%s", len ? " " : "", nm);
}
static uint64_t last;
static int lines;
if (verbose && h != last && lines < 200) last = h, lines++, rt_log("menu_cursor: open screens: %s", list);
return pad && map && bg;
}
static void layout_size(uint64_t scr, float *lw, float *lh) {
*lw = 1920, *lh = 1080;
uint64_t lyt = rd64(scr + SCR_LAYOUT);
if (!ok_at(lyt, LYT_SIZE + 8)) return;
float w = rdf(lyt + LYT_SIZE), h = rdf(lyt + LYT_SIZE + 4);
if (w >= 64 && w <= 8192 && h >= 64 && h <= 8192) *lw = w, *lh = h;
}
// The screen's focusable nodes: an sead OffsetList, head {prev, next} at +0x80, node-to-link offset at +0x94
#define MAX_NODES 512
static int nodes_of(uint64_t scr, uint64_t *node, uint64_t *btn) {
uint64_t head = scr + SCR_NODES;
int32_t off = (int32_t)rd32(head + 0x14), n = 0;
if (off < 0 || off > 0x40) return 0;
for (uint64_t link = rd64(head + 8); link != head && n < MAX_NODES; link = rd64(link + 8)) {
if (!ok_at(link, 16)) break;
uint64_t nd = link - (uint64_t)off;
if (!ok_at(nd, NODE_SCREEN + 8)) continue;
uint64_t b = rd64(nd + NODE_BUTTON);
if (!ok_at(b, BTN_HIT_PANE + 8)) continue;
node[n] = nd, btn[n] = b, n++;
}
return n;
}
static int usable(uint64_t btn) { return !(~rd32(btn + BTN_FLAGS) & BTN_USABLE); }
static uint64_t root_pane(uint64_t btn) {
uint64_t blyt = rd64(btn + BTN_LAYOUT);
return ok_at(blyt, LYT_ROOT + 8) ? rd64(blyt + LYT_ROOT) : 0;
}
// Every usable node's shown hit pane lies off the visible layout: the hit panes are stale (TotK never uses touch;
// the dialogue choices' sit far from where the choices are drawn), so the buttons' own parts panes stand for them
static int stale_hit_panes(int n, const uint64_t *btn, float lw, float lh, const Xf *t) {
int seen = 0;
for (int i = 0; i < n; i++) {
if (!usable(btn[i])) continue;
uint64_t pane = rd64(btn[i] + BTN_HIT_PANE);
if (!ok_at(pane, PANE_MTX + 0x20) || !pane_shown(pane)) continue;
float cx, cy, hw, hh, vx, vy;
pane_box(pane, &cx, &cy, &hw, &hh);
int in = t ? to_ndc(t, cx, cy, &vx, &vy) && fabsf(vx) <= 1 && fabsf(vy) <= 1
: fabsf(cx) <= lw / 2 && fabsf(cy) <= lh / 2;
seen++;
if (in) return 0;
}
return seen > 0;
}
// The usable node under the pointer, 0: none
static uint64_t node_at(int n, const uint64_t *node, const uint64_t *btn, const Pt *p, float lw, float lh) {
if (!stale_hit_panes(n, btn, lw, lh, p->t)) {
for (int i = 0; i < n; i++)
if (usable(btn[i]) && pane_hit(rd64(btn[i] + BTN_HIT_PANE), p)) return node[i];
return 0;
}
uint64_t found = 0;
float best = 1e30f;
for (int i = 0; i < n; i++) { // parts panes can be several items wide: the nearest centre
uint64_t pane = root_pane(btn[i]);
if (!usable(btn[i]) || !pane_hit(pane, p)) continue;
float cx, cy, hw, hh, d;
pane_box(pane, &cx, &cy, &hw, &hh);
d = (cx - p->x) * (cx - p->x) + (cy - p->y) * (cy - p->y);
if (d < best) best = d, found = node[i];
}
return found;
}
// The focus to node: the writes moveBoxCursor makes. The request bit is a byte eui also writes, so it is raised
// again until the focus is there.
static void move_focus(const Focus *f, uint64_t node) {
memcpy((void *)(uintptr_t)(f->control + CTL_RESERVED), &node, 8);
memcpy((void *)(uintptr_t)(f->screen + SCR_RESERVED), &node, 8);
__atomic_fetch_or((uint8_t *)(uintptr_t)(f->screen + SCR_REQUEST), (uint8_t)4, __ATOMIC_SEQ_CST);
}
// Screens with a focus box that are gameplay, not menus: the L ability wheel, the D-pad strips, Autobuild's list, the
// Purah Pad's camera and the map (the stick moves its own cursor there)
static int gameplay_screen(const char *nm) {
static const char *const names[] = {"ShortCutMainItem_00", "ShortCut_00", "AutoBuilder_00", "CameraPointer_00",
"AppMap_00"};
for (size_t i = 0; i < sizeof names / sizeof *names; i++)
if (!strcmp(nm, names[i])) return 1;
return 0;
}
static volatile int32_t g_pointer; // a menu has the pointer: input_cursor_allowed releases the capture
static volatile int32_t g_map; // the Purah Pad's map is open (not the Scope): the mouse moves its cursor
int menu_cursor_pointer(void) { return __atomic_load_n(&g_pointer, __ATOMIC_RELAXED); }
int menu_cursor_map(void) { return __atomic_load_n(&g_map, __ATOMIC_RELAXED); }
static int enabled(void) {
if (!RT_V143) return 0;
const char *v = cfg_get("TOTK_MENU_CURSOR"), *in = cfg_get("TOTK_INPUT");
return !(v && !strcmp(v, "0")) && !(in && !strcmp(in, "none"));
}
static void set_pointer(int on) {
if (plat_xchg32(&g_pointer, on) == on) return;
ikb_cursor_update();
if (on) ikb_pointer_set(0.5f, 0.5f); // the pointer appears in the middle, not where the capture left it
}
// The pointer on the game's picture, 0..1 from its top left (the present's fit, letterbox or pillarbox)
static int picture_pos(float *px, float *py) {
float nx, ny, r[4];
if (!ikb_pointer(&nx, &ny) || !gfx_picture_rect(r) || r[2] <= 0 || r[3] <= 0) return 0;
*px = (nx - r[0]) / r[2], *py = (ny - r[1]) / r[3];
return *px >= 0 && *px <= 1 && *py >= 0 && *py <= 1;
}
static void log_screen(const Focus *f, const char *nm, int n, float lw, float lh, const Xf *t, int stale) {
static uint64_t last;
static int lines;
const char *e = cfg_get("TOTK_MENU_CURSOR_LOG");
int verbose = e && !strcmp(e, "1");
if (f->screen == last || (!verbose && lines >= 6)) return;
last = f->screen, lines++;
rt_log("menu_cursor: screen %s (layout %gx%g, %d nodes, %s hit panes, draw transform %s)", nm, lw, lh, n,
stale ? "stale" : "own", t ? "scaled" : "plain");
}
// Once per pad read on the game thread: the pointer's hover and clicks, as pad buttons (A, B) for this read
void menu_cursor_poll(uint64_t *buttons) {
static int inited, pending_a, held_l, held_r;
static uint64_t pending_node, pending_until, hold_until, hold_bit, menu_seen;
static float last_px = -1, last_py = -1;
if (!inited) {
inited = 1;
main_lo = MOD_MAIN_BASE, main_hi = MOD_MAIN_BASE + MAIN_SIZE;
if (RT_V143) rt_log("menu_cursor: on for 1.4.3 (TOTK_MENU_CURSOR=0 turns it off)");
}
if (!enabled()) { set_pointer(0); __atomic_store_n(&g_map, 0, __ATOMIC_RELAXED); return; }
Focus f = focus_now();
char nm[48] = "";
if (f.screen) layout_name(f.screen, nm, sizeof nm);
// The open screens are scanned every 100 ms, not at every pad read: each screen costs memory queries
uint64_t now = plat_now_ms();
static uint64_t map_at;
static int map, map_logged;
if (!map_at || now - map_at >= 100) map_at = now, map = map_open();
if (map && !map_logged) map_logged = 1, rt_log("menu_cursor: the map is open: the mouse moves its cursor");
__atomic_store_n(&g_map, map, __ATOMIC_RELAXED);
// With the gamepad as the last device the menus stay as they are on the console: no pointer, the capture kept.
// Moving the mouse (or a key) makes keyboard and mouse the last device and the pointer comes.
int menu = f.screen && !gameplay_screen(nm) && ikb_foreground() && input_kbm_last();
if (menu) menu_seen = now;
// A menu's focus gap (a tab switch, a popup opening: no focus box for a frame or two) keeps the pointer where it
// is, for up to 300 ms: releasing it there captured it again and centred it when the next tab took the focus
int gap = !menu && !f.screen && menu_seen && now - menu_seen < 300 && ikb_foreground() && input_kbm_last();
set_pointer(menu || gap);
int l = ikb_down(KEY_MOUSE_L), r = ikb_down(KEY_MOUSE_R);
int click_l = l && !held_l, click_r = r && !held_r;
held_l = l, held_r = r;
if (now < hold_until) { // a click's button, held for 60 ms so the game sees one press whatever its read rate
*buttons |= hold_bit;
return;
}
if (gap) return; // a click's focus request waits for the next screen
if (!menu) { pending_a = 0; last_px = last_py = -1; return; }
if (pending_a) { // the focus asked for by a click: A once it is there
if (rd64(f.control + CTL_ACTIVE) == pending_node) {
pending_a = 0, hold_until = now + 60, hold_bit = 1ull << B_A;
*buttons |= hold_bit;
return;
}
if (now > pending_until) pending_a = 0;
else move_focus(&f, pending_node);
return;
}
if (click_r) { hold_until = now + 60, hold_bit = 1ull << B_B; *buttons |= hold_bit; return; }
float px, py;
if (!picture_pos(&px, &py)) return;
int moved = fabsf(px - last_px) > 0.001f || fabsf(py - last_py) > 0.001f;
if (!moved && !click_l) return;
last_px = px, last_py = py;
float lw, lh;
layout_size(f.screen, &lw, &lh);
Xf xf = screen_xf(f.screen);
Pt p = {(px - 0.5f) * lw, (0.5f - py) * lh, px * 2 - 1, 1 - py * 2, NULL};
if (xf.ok && !xf_plain(&xf, lw, lh)) p.t = &xf;
static uint64_t node[MAX_NODES], btn[MAX_NODES];
int n = nodes_of(f.screen, node, btn);
log_screen(&f, nm, n, lw, lh, p.t, n && stale_hit_panes(n, btn, lw, lh, p.t));
uint64_t at = n ? node_at(n, node, btn, &p, lw, lh) : 0;
if (!at) return;
if (at != f.node) move_focus(&f, at);
if (click_l) {
if (at == f.node) { hold_until = now + 60, hold_bit = 1ull << B_A; *buttons |= hold_bit; }
else pending_a = 1, pending_node = at, pending_until = now + 300;
}
}
// The L ability wheel or a D-pad strip (materials, weapons, shields, bows) has the focus: 1 / 0, -1 with the menu
// cursor off (the screens are not read). input.c asks while L or the D-pad is held, before it gives the mouse to their
// selectors: L also soothes the horse and turns menu tabs, the D-pad does other things too.
int menu_cursor_shortcut(void) {
if (!enabled()) return -1;
Focus f = focus_now();
char nm[48] = "";
if (f.screen) layout_name(f.screen, nm, sizeof nm);
return f.screen && (!strcmp(nm, "ShortCutMainItem_00") || !strcmp(nm, "ShortCut_00"));
}