mirror of
https://github.com/valentderah/totk-pc-recomp.git
synced 2026-11-11 09:51:02 +00:00
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
399 lines
20 KiB
C
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"));
|
|
}
|