diff --git a/docs/dev/findings-index.csv b/docs/dev/findings-index.csv index 16f5a938..76469084 100644 --- a/docs/dev/findings-index.csv +++ b/docs/dev/findings-index.csv @@ -222,3 +222,5 @@ D341,FIXED,"Intro gun-barrel blood drip draws as static blocks/lines instead of D342,FIXED,"File select: folder bodies and Bond photos vanish after selecting a file and backing out (user, v0.4.0, 2026-09-26).","FIXED (M-201, user-verified):" D343,PARTIAL,"File select: PC ""Options"" label opens the settings overlay (v0.4.0 feature, M1 of the route-B options screen, 2026-09-27); also fixes F10 on file select jumpin…","LANDED (M1): one Rule-2 #ifdef PORT draw hook (user sign-off 2026-09-27); headless + user-verified (mouse, F10, >30 s, widescreen); physical controller check owed." D344,FIXED,Host PC bugchecks (BSOD 0x119 VIDEO_SCHEDULER_INTERNAL_ERROR) during game test runs; probable cause:,"FIXED (probable cause; orderly quit + GE_QUITFRAME), headless + user-verified; no dump, watch for recurrence." +D345,FIXED,"Front-end crosshair snaps away from the pointer on screen/page changes (file select → mode select setCursorPOSforMode(0), front.c:2516; every MENU_PC_OPTIONS p…","Port-only, user-verified (parts a–g):" +D346,PARTIAL,PC-settings wording pass:,Port-only display change landed (optionsoverlay.c rows[]/valueText + one colour match in frontoptions.c); both UIs (F10 overlay + options screen) share the table. diff --git a/docs/dev/findings.md b/docs/dev/findings.md index 11e16483..5045a3bd 100644 --- a/docs/dev/findings.md +++ b/docs/dev/findings.md @@ -630,6 +630,8 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D342 | **File select: folder bodies and Bond photos vanish after selecting a file and backing out (user, v0.4.0, 2026-09-26).** — full `## D342` entry at file tail | FIXED (M-201, user-verified): scratch placed at the N64 offset +0xA000 of `ptr_logo_and_walletbond_DL` landed inside the larger PC wallet model; moved past its 0x17000 reservation. | | D343 | **File select: PC "Options" label opens the settings overlay (v0.4.0 feature, M1 of the route-B options screen, 2026-09-27); also fixes F10 on file select jumping to the legal screen after 30 s.** — full `## D343` entry at file tail | LANDED (M1): one Rule-2 `#ifdef PORT` draw hook (user sign-off 2026-09-27); headless + user-verified (mouse, F10, >30 s, widescreen); physical controller check owed. | | D344 | **Host PC bugchecks (BSOD 0x119 VIDEO_SCHEDULER_INTERNAL_ERROR) during game test runs; probable cause: quit called exit() while the render thread could be inside the GL driver (2026-09-27).** — full `## D344` entry at file tail | FIXED (probable cause; orderly quit + GE_QUITFRAME), headless + user-verified; no dump, watch for recurrence. | +| D345 | **Front-end crosshair snaps away from the pointer on screen/page changes (file select → mode select `setCursorPOSforMode(0)`, front.c:2516; every `MENU_PC_OPTIONS` page hop), and the options screen's keyboard navigation was missing/mis-modelled (2026-09-27).** — full `## D345` entry at file tail | Port-only, user-verified (parts a–g): (a) `input.c` re-assert window on menu change; (b) `cursorToItem` skips its snap while the pointer is live; (d) W/S emit stick in menus under NaturalPitch; (e/f) F10-style discrete row stepping + arrow left/right value adjust replace raw drift (first pass shipped broken, fixed); (g) screen-placed cursor parks in the left gutter, off the row text. Game-side one-liner (pad users) parked pending Rule-2 sign-off. | +| D346 | **PC-settings wording pass: option labels/values aligned to Nightdive/Turok + PD-port conventions (title-case On/Off, "Frame rate cap", "Anti-aliasing"/"None", units in values not labels, self-explanatory auto-FOV toggles); VIEW section merged into VIDEO (2026-09-27).** — full `## D346` entry at file tail | Port-only display change landed (`optionsoverlay.c` rows[]/valueText + one colour match in `frontoptions.c`); both UIs (F10 overlay + options screen) share the table. Config keys/values untouched. Deadzone % display uses raw-step grid (cosmetic follow-up noted). | Phase 2 replaced the Phase-1 demo loop with the real `mainproc()` on real OS threads, compiled GE's real `src/sched.c`, and brought in PD's fast3d software @@ -13607,3 +13609,96 @@ Implemented as `Input.PdMouseAim` (**default off**, because it changes aim feel) **Status: FIXED (probable cause), user-verified.** + +## D345 — File select → mode select: crosshair snaps to the Agent row and stays there until the mouse moves (2026-09-27) + +**Report:** with mouse input, entering a folder page (file select → mode select) resets the crosshair away from the pointer; it stays at the snapped position until the next mouse movement. + +**Evidence:** `front.c:2516` — the file-select→mode-select entry path calls `setCursorPOSforMode(0)` unconditionally, teleporting `cursor_h/v_pos` to (126, 226), the first row. Its two sibling re-entry paths (`front.c:4502`, `:4646`) already use `setCursorPOSforMode(gamemode)`. The port's 1:1 menu pointer (`port/src/input.c`, D165/D169) writes `cursor_h/v_pos` from the absolute mouse position **only on frames where the OS mouse moved** (idle-mouse cede so WASD/arrow navigation can drive the cursor through the game's integrator). A click-to-enter leaves the mouse idle, so the game's snap persists until the next movement. `menuPrevActive` (`input.c:1359`) tracks menu-vs-not only, so the "just entered a menu" baseline-adopt branch does not fire on menu-to-menu transitions. Poll order is input → interface (D343), so the same-frame snap wins over any same-frame re-assert. + +**Fix (port-only, `port/src/input.c`):** carry pointer ownership across front-end screen changes. +- New statics: `menuPointerLastMenuId`, `menuPointerTransitionFrames`, `menuPointerLastAbsWrite` (perf-counter stamp of the last absolute write). +- Every pad-0 poll: if `current_menu` changed, open a 3-poll re-assert window (the transition poll's write loses to the game's same-frame snap; poll+1 sticks). +- In the 1:1 pointer block, force the absolute write when `(mouse moved) OR (window open AND mouse used within MENU_POINTER_REASSERT_MS = 1500 ms)`. Click-to-enter → cursor lands back under the pointer on the new page. Stale idle mouse (WASD navigation) → no yank; the cede-to-keyboard behaviour is preserved. +- The F10 overlay does not change `current_menu`, so opening/closing it opens no window (correct: same screen, pointer already under the mouse). Entering `MENU_PC_OPTIONS` does change it and re-asserts — desirable, flagged for playtest. + +**D345(b) — the new PC options screen (user report: "doesn't work on our new PC settings menu").** `MENU_PC_OPTIONS` has its own cursor teleporter: `cursorToItem()` (`port/src/frontoptions.c:178`) snaps the crosshair to a page's first item, called from screen-entry init, section entry, and back. The section/back hops happen *inside* frontoptions.c (no `current_menu` change), so the D345(a) window never sees them; every page hop re-snapped the crosshair away from the pointer. Fix (port-only): input.c now exports `inputMenuPointerLive()` (in a menu + abs pointer available + mouse used within `MENU_POINTER_REASSERT_MS`; refreshed every pad-0 poll, forced 0 while the F10 overlay owns the mouse), and `cursorToItem()` skips its snap when it is set — the cursor is already under the OS pointer. Keyboard/D-pad users (stale mouse) keep the snap. Also added `#include ` to `port/include/input.h` (its API uses SDL types; previously only includers that had SDL already compiled — cf. video.h). + +**D345(c) — options screen: W/S did not move through the lists (user playtest).** Cheat-screen-style menus integrate stick input into `cursor_h/v_pos` by calling `frontUpdateControlStickPosition()` from their interface (`interface_menu15_cheat`, front.c:7700); `frontOptionsMenuInterface()` never did, so keyboard/D-pad vertical input had nothing to drive the cursor (A/D worked because frontoptions.c reads those as buttons directly). Fix (port-only): `frontOptionsMenuInterface()` now calls `frontUpdateControlStickPosition()` after `buildPages()`, before the highlight reads the cursor — mirroring the cheat screen. No-op while the 1:1 pointer owns the cursor (it emits a zero stick). + +**D345(d) — W/S still dead after (c): NaturalPitch mode starves the stick (user playtest via playtest_v040.bat, confirmed running the fixed binary by exe/log timestamps).** With `NaturalPitch = 1` (default; in the user's ini), the keyboard forward/back mapping emits **C-up/C-down buttons** instead of stick (`input.c:1037-1042`, the D194/D238 in-stage aim scheme) -- and no front-end screen navigates on C-buttons, so W/S (and Up/Down arrows) produced zero stick in every menu. Fix (port-only): the natural-pitch branch now requires `!menuMode`, so menus always get the stick mapping (N64 front end is stick-driven). Watch/pause is unaffected: it runs at `current_menu == RUN_STAGE` (menuMode 0) and keeps the C-button mapping, N64-style. This also restores W/S vertical navigation on file select / cheat screen for NaturalPitch users (mouse users never noticed). + +**D345(e) — user preference: F10-style discrete keyboard navigation on the options screen ("behave like the F10 menu but keep the mouse option").** Replaced the D345(c) raw stick drift with one-row-at-a-time stepping in `frontOptionsMenuInterface()`: W/S / Up-Down arrows (stick, via D345(d)) or pad stick/D-pad (`U_JPAD`/`D_JPAD`) move the highlight ±1 row, clamped at the ends, hold-to-repeat on the screen's existing 18/4-frame cadence (separate `s_vrepeatDir/Timer` state so it can run alongside left/right value adjust). Each step snaps the crosshair via new ungated `cursorToItemRaw()` (explicit keyboard nav owns the cursor even right after mouse use); `cursorToItem()` keeps the D345(b) pointer-live gate for page transitions. Mouse is untouched: any movement re-owns the cursor and the highlight hit-test follows it next frame. Stepper is skipped while a slider drag holds A. The D345(c) `frontUpdateControlStickPosition()` call is removed (it fought the pointer over `cursor_v_pos`). + +**D345(f) — D345(e) shipped broken (user: "W/S still just move the crosshair").** The D345(e) edit added the stepper but its two *removals* never landed: the D345(c) `frontUpdateControlStickPosition()` call (after `buildPages()`) and its extern stayed in `frontoptions.c`, so W/S ran raw cursor drift on top of the row stepper. The build didn't catch it: with the extern gone from my intended edit but present in the file, compilation only produced an implicit-declaration warning, which this project compiles through. Fix: removed the call + extern (verified by grep, not just the edit tool's success message), and added stick-X to the left/right value block so Left/Right arrows adjust values like F10 (A/D letters already worked via C-buttons). Lesson recorded: after any edit that both adds and removes, grep-verify the removals against the file. + +**Not changed:** the game-side snap itself (`front.c:2516`) is untouched — no Rule-2 sign-off needed for this fix. The optional one-liner `setCursorPOSforMode(gamemode >= 0 ? gamemode : 0)` (matching its siblings) remains a separate parked item; its main beneficiary is pad users, who have no re-assert at all. + +**User-verified (2026-09-27):** folder-page snap gone (mouse); options screen entry/section hops keep the pointer under the mouse; sliders/toggles fine; F10-style W/S + arrow navigation with hold-to-repeat works; left/right arrows adjust values; crosshair parks in the gutter off the row text. Resolved for time being. + +**Verification:** builds clean (`build-pc.sh ntsc-final`; only pre-existing warnings). Headless smoke: `GE_STARTMENU=5 GE_INPUTLOG=1 GE_QUITFRAME=400` — 412 `menuptr` lines, cursor stable, orderly self-quit (`render parked after 3 ms`). The playtest items listed here were subsequently covered by the user verification above. + +## D346 — PC-settings wording pass: Nightdive/Turok + PD-port conventions (2026-09-27) + +User request: make all PC setting labels/values consistent with the PD port and +Nightdive remasters (Turok) where possible. Both settings UIs (the F10 overlay +and `MENU_PC_OPTIONS`) share one table — `rows[]` in `port/src/optionsoverlay.c` +— so a single pass covers both. Display-only: config keys, stored values, and +adjustment behavior are untouched (existing inis read/write identically). + +**Label changes** (`rows[]`, optionsoverlay.c): + +| Before | After | Rationale | +|---|---|---| +| `DISPLAY` (section) | `VIDEO` | Nightdive section naming ("Video"); pairs with the existing `VIEW` section | +| `Frame cap` | `Frame rate cap` | Turok 3's exact wording; values already Nightdive-style (`Uncapped` / `120 FPS`) | +| `MSAA` | `Anti-aliasing` | Nightdive standard; jargon-free | +| `Anisotropic` | `Anisotropic filtering` | Nightdive standard | +| `FOV scale %` / `Draw distance %` / `LOD distance %` / `HUD scale %` / `Trigger threshold %` | `%` dropped from labels, moved to the value (`100%`, `150%`, …) | Nightdive shows units in the value column, not the label | +| `Crop overscan bars` | `Crop overscan` | trimmed; "bars" was internal phrasing | +| `Draw dist. auto` / `LOD dist. auto` | `Draw dist. follows FOV` / `LOD dist. follows FOV` | the old labels didn't say what follows what (they couple the distance to `Video.FovScale`, video.c:203-231) | +| `Mouse invert Y` / `Pad invert look Y` | `Invert look (mouse)` / `Invert look (controller)` | Nightdive per-device convention; now a consistent pair | + +**Value changes:** `OFF/ON` → `Off/On` (kOnOff); `NEAREST/BILINEAR/3-POINT` → +`Nearest/Bilinear/3-Point`; `CENTRED (PC)` → `Centred (PC)`; MSAA `OFF` → +`None` (Nightdive AA vocabulary). The options screen's ON-colour match in +`frontoptions.c:454` updated `"ON"` → `"On"` to follow. + +**Mechanism:** two display-only fields appended to `struct Row` (`unit`, +`dispDiv`; C zero-fill keeps every existing positional initializer valid) and a +small branch at the integer-slider tail of `valueText()`: optional raw→display +divide, then unit suffix. Used by: FovScale/DrawDistance/LodDistance/HudScale/ +PadTriggerPct (`%`), Anisotropy (`x`), PadDeadzone (`%`, dispDiv 300 — raw +0..30000 stick units → % of full travel; default 7000 now reads "23%" instead +of "7000"). + +**Kept deliberately:** `VSync`, `Fullscreen`, `Resolution`, `Texture filter` +(universal); `Aim style`/`Aim range` + their `N64`/`PC` values (GE-specific, +already clear); `No hit flash` (community-mod name; inverting the polarity to +"Hit flash: Off" would change what On means — not a wording fix); `Skip intro`, +`Show FPS`, `All unlocked`, `Quit to desktop` (plain English, no reference +counterpart worth matching). + +**Known cosmetic follow-up (not done):** the deadzone slider's raw step is 500 +stick units, so its % display advances ~1.7% per press (23→24→25→27…). A clean +1%-per-press grid would mean changing `step` to 300 — a behavior change to the +adjustment feel, out of scope for a wording pass; flagged for a future input-QoL +item. + +**D346b — VIEW section merged into VIDEO (user request, same day).** The old +`VIEW` header row was deleted from `rows[]`; its rows (FOV scale, Native +widescreen, Widescreen auto FOV, Crop overscan, draw/LOD distance pairs) now +follow the display/quality rows in one Nightdive-style "Video" section. The +options screen's page model is one-header-one-page with a row cap, so the cap +moved 12→15 (`MAX_PROWS`) and the row pitch 20→18 px (`ROW_DY`, frontoptions.c) +to keep the worst case (15 rows: "Draw dist. follows FOV" off reveals both its +toggle and the slider) on one page — last row text at y=317, inside the 330 +paper; its 22px highlight box overflows by 1px in that state only (clipped, +invisible). Hit bands stay contiguous at the tighter pitch (band = [y-3, y+15), +next starts y+15). F10 overlay unaffected (own pitch, scrolls). Section list is +now 4 entries: Video / Mouse / Aim / Controller / Game. + +**Verification:** `build-pc.sh ntsc-final` clean (no new warnings); headless +boot into the options screen (`GE_STARTMENU=26 GE_QUITFRAME=300`) orderly +self-quit, zero crash lines. All strings plain ASCII (JP-region safe, #87). +Port-only: no `src/game` edits, N64 build untouched. diff --git a/port/include/input.h b/port/include/input.h index 3104c4bc..48ef1be0 100644 --- a/port/include/input.h +++ b/port/include/input.h @@ -12,6 +12,7 @@ */ #include +#include /* API types (SDL_GameControllerButton/Axis); cf. video.h */ #ifdef __cplusplus extern "C" { @@ -67,6 +68,11 @@ short inputPadAxis(int idx, SDL_GameControllerAxis a); * ignored -- GE only cycles forward on a bare A edge. */ void inputPostWheel(int notches); +/* D345(b): 1 while the 1:1 menu pointer owns cursor_h/v_pos (in a menu, + * abs pointer available, mouse used within the re-assert window). Port + * screens that teleport the crosshair should skip their snap when set. */ +int inputMenuPointerLive(void); + /* Re-enumerate gamepads after a hotplug (SDL_CONTROLLERDEVICEADDED/REMOVED). */ void inputRescanPads(void); diff --git a/port/src/frontoptions.c b/port/src/frontoptions.c index 1b4816d8..a2be9a4a 100644 --- a/port/src/frontoptions.c +++ b/port/src/frontoptions.c @@ -42,13 +42,13 @@ #include "envflag.h" #include "optionsoverlay.h" #include "frontoptions.h" +#include "input.h" /* front.c functions this screen shares with the cheat screen (not all are in * front.h). */ extern Gfx *frontSetupMenuBackground(Gfx *DL); extern Gfx *frontAddPreviousTabText(Gfx *DL); extern s32 frontCheckCursorOnPreviousTab(void); -extern void frontUpdateControlStickPosition(void); extern Gfx *frontDrawCursor(Gfx *DL); extern Gfx *frontPrintText(Gfx *gdl, s32 *x, s32 *y, s8 *text, s32 second_font_table, s32 first_font_table, s32 arg6, s32 view_x, s32 view_y, @@ -69,7 +69,11 @@ extern struct rectbbox folder_option_ERASE_bound; /* front.c:439 */ #define TITLE_Y 0x2B #define ROW_X 0x37 /* the cheat list's x */ #define ROW_Y0 0x41 /* cheat list starts 0x35; +12 for the title line */ -#define ROW_DY 0x14 /* the cheat list's row pitch */ +/* D346b: 20 -> 18 px so the merged Video section (14 rows default, 15 with + * "Draw dist. follows FOV" off) fits the paper on one page. Hit bands stay + * contiguous at any pitch >= 6; the 22px highlight box still clears the next + * row (y+14 < y+17). */ +#define ROW_DY 18 #define NUM_X 0x37 /* "1." on the section list */ #define SEC_X 0x4B #define VAL_R 372 /* values right-aligned here */ @@ -85,7 +89,7 @@ extern struct rectbbox folder_option_ERASE_bound; /* front.c:439 */ #define HIT_PAD 4 #define MAX_PAGES 8 -#define MAX_PROWS 12 /* the paper fits 12 rows, like the cheat list */ +#define MAX_PROWS 15 /* D346b: 12 -> 15 for the merged Video section (see ROW_DY) */ static const char kLabel[] = "PC Options"; /* ASCII only: issue #87 / D295 */ static const char kLabelNL[] = "PC Options\n"; @@ -96,6 +100,9 @@ static int s_page = 0; /* current section */ static int s_hl = -1; /* highlighted row / section, -1 = none */ static int s_dragRow = -1; /* global row index being dragged */ static int s_repeatDir = 0, s_repeatTimer = 0; +/* D345(e): hold-to-repeat state for vertical row stepping (separate from + * the left/right value-adjust repeat; both can be held at once). */ +static int s_vrepeatDir = 0, s_vrepeatTimer = 0; static int s_pageHdr[MAX_PAGES]; static int s_pageN = 0; @@ -174,11 +181,29 @@ static int itemCount(void) return s_level == 0 ? s_pageN : s_rowN; } +/* Put the crosshair on item k, unconditionally. */ +static void cursorToItemRaw(int k) +{ + /* D345(g): snap into the left gutter (x=44), not onto the row text + * (the old ROW_X+20 centred the sprite over the label/highlight box). + * 44 stays inside [ROW_HIT_X0..1] so the highlight hit-test still + * resolves row k, and clears the number column (NUM_X=55) / highlight + * boxes (from x=53/73). The PREVIOUS-tab hitbox (x>390 && y>223) is + * far away. Mouse users are unaffected: the pointer sits under the + * real mouse, never at this snap position. */ + cursor_h_pos = 44.0f; + cursor_v_pos = (f32)(rowY(k) + 6); +} + /* Put the crosshair on item k (entering a page / going back). */ static void cursorToItem(int k) { - cursor_h_pos = (f32)(ROW_X + 20); - cursor_v_pos = (f32)(rowY(k) + 6); + /* D345(b): while the 1:1 pointer owns the crosshair (mouse recently + * used), it is already under the OS pointer -- snapping would yank it + * to the top-left item and it would stay there until the next mouse + * movement. Keyboard/D-pad users (stale mouse) keep the snap. */ + if (inputMenuPointerLive()) return; + cursorToItemRaw(k); } /* ------------------------------------------------------------------------ */ @@ -209,6 +234,7 @@ void frontOptionsMenuInit(void) s_hl = -1; s_dragRow = -1; s_repeatDir = 0; + s_vrepeatDir = 0; buildPages(); cursorToItem(0); sysLogPrintf(LOG_INFO, "frontoptions: opened"); @@ -242,6 +268,11 @@ void frontOptionsMenuInterface(void) buildPages(); /* an auto toggle may have hidden or shown % rows */ + /* D345(f): NO raw stick integration on this screen (the D345(c) call to + * frontUpdateControlStickPosition() was removed): vertical input is the + * discrete row stepper below, horizontal is the left/right block -- F10 + * model. Raw drift fought the pointer over cursor_h/v_pos. */ + /* Highlight: as the cheat screen, recomputed while A is not held. */ if (joyGetButtons(PLAYER_1, A_BUTTON | Z_TRIG) == 0) { tab_prev_highlight = FALSE; @@ -298,8 +329,11 @@ void frontOptionsMenuInterface(void) * with hold-to-repeat. */ if (s_level == 1 && s_hl >= 0 && s_hl < s_rowN) { int dir = 0; - if (joyGetButtons(PLAYER_1, L_JPAD | L_CBUTTONS)) dir = -1; - if (joyGetButtons(PLAYER_1, R_JPAD | R_CBUTTONS)) dir = +1; + /* D345(f): stick X joins the D-pad/C-buttons, so Left/Right arrows + * (and the pad's left stick) adjust values like F10's left/right. */ + s8 vsx = joyGetStickX(PLAYER_1); + if (joyGetButtons(PLAYER_1, L_JPAD | L_CBUTTONS) || vsx < -5) dir = -1; + if (joyGetButtons(PLAYER_1, R_JPAD | R_CBUTTONS) || vsx > 5) dir = +1; if (dir != 0) { int fire = 0; if (dir != s_repeatDir) { @@ -323,7 +357,47 @@ void frontOptionsMenuInterface(void) set_item_visibility_in_objinstance(walletinst[0], SW_TABS, 1); set_item_visibility_in_objinstance(walletinst[0], SW_BLANK, 1); set_item_visibility_in_objinstance(walletinst[0], SW_CLASSIFIED, 1); - frontUpdateControlStickPosition(); + + /* D345(e): F10-style discrete vertical navigation. W/S / Up-Down arrows + * (stick via D345(d)) or the pad's stick/D-pad step the highlight one + * row at a time -- clamped at the ends, hold-to-repeat with the same + * 18/4-frame cadence as left/right -- and snap the crosshair onto the + * new row (ungated: explicit keyboard navigation owns the cursor even + * right after mouse use). Replaces the raw stick drift of D345(c), + * which fought the pointer over cursor_v_pos. The mouse is untouched: + * any movement re-owns the cursor and the highlight hit-test above + * follows it next frame. Skipped while a slider drag holds A. */ + if (s_dragRow < 0) { + int vdir = 0; + s8 vsy = joyGetStickY(PLAYER_1); + if (joyGetButtons(PLAYER_1, U_JPAD) || vsy > 5) vdir = -1; /* up */ + else if (joyGetButtons(PLAYER_1, D_JPAD) || vsy < -5) vdir = +1; /* down */ + if (vdir != 0) { + int fire = 0; + if (vdir != s_vrepeatDir) { + fire = 1; + s_vrepeatTimer = 18; + } else if (--s_vrepeatTimer <= 0) { + fire = 1; + s_vrepeatTimer = 4; + } + if (fire) { + int n = itemCount(); + if (n > 0) { + int q = (s_hl < 0 ? 0 : s_hl) + vdir; + if (q < 0) q = 0; + if (q >= n) q = n - 1; + if (q != s_hl) { + s_hl = q; + cursorToItemRaw(q); + } + } + } + } + s_vrepeatDir = vdir; + } else { + s_vrepeatDir = 0; + } } Gfx *frontOptionsMenuDraw(Gfx *DL) @@ -381,7 +455,7 @@ Gfx *frontOptionsMenuDraw(Gfx *DL) optionsRowValueText(i, buf, sizeof(buf) - 1); if (buf[0]) { - u32 col = (strcmp(buf, "ON") == 0) ? INK_ON : INK; + u32 col = (strcmp(buf, "On") == 0) ? INK_ON : INK; /* D346: kOnOff is now title-case */ strcat(buf, "\n"); DL = inkR(DL, VAL_R, y, buf, col); } diff --git a/port/src/input.c b/port/src/input.c index 40be7da6..ea58a662 100644 --- a/port/src/input.c +++ b/port/src/input.c @@ -265,6 +265,9 @@ extern s32 lvlGetCurrentStageToLoad(void); #define MENU_CURSOR_LO 20.0 #define MENU_CURSOR_HI_H 300.0 #define MENU_CURSOR_HI_V 220.0 +/* D345: after a front-end screen change, re-assert the 1:1 pointer for a few + * frames if the mouse was used within this many ms (see inputComputePad). */ +#define MENU_POINTER_REASSERT_MS 1500.0 #define MENU_CURSOR_MID_H 160.0 #define MENU_CURSOR_MID_V 120.0 @@ -538,6 +541,20 @@ static int hipfirePitchSpeed = 100; /* D166: hipfire pitch pulse rate, percen static int menuPrevActive = 0; static double hipPitchPhase = 0.0; /* D166: hipfire pitch pulse phase 0..1 */ static int lastMenuMouseX = -1, lastMenuMouseY = -1; /* WI-2: last abs cursor seen in a menu */ +/* D345: carry pointer ownership across front-end screen changes. The game + * teleports cursor_h/v_pos on some transitions (file select -> mode select + * calls setCursorPOSforMode(0), front.c:2516); the absolute write below only + * fires while the OS mouse moves, so with an idle mouse the snap persists + * until the next movement. lastMenuId detects the change; transitionFrames is + * a short countdown during which a recent-mouse re-assert is allowed; + * lastAbsWrite is the perf-counter stamp of the last absolute write (mouse + * recency). */ +static int menuPointerLastMenuId = -1; +static int menuPointerTransitionFrames = 0; +static Uint64 menuPointerLastAbsWrite = 0; +/* D345(b): 1 while the 1:1 pointer can own cursor_h/v_pos this poll + * (menu, mouse enabled + grabbed). Refreshed every pad-0 poll. */ +static int s_menuPointerLive = 0; /* ------------------------------------------------------------------------ */ @@ -949,6 +966,7 @@ unsigned inputComputePad(int idx, signed char *stick_x, signed char *stick_y) optionsOverlayHandleInput(); if (stick_x) *stick_x = 0; if (stick_y) *stick_y = 0; + s_menuPointerLive = 0; /* D345(b): the overlay owns the mouse */ return 0; } @@ -959,6 +977,17 @@ unsigned inputComputePad(int idx, signed char *stick_x, signed char *stick_y) int menuMode = (current_menu != GE_MENU_RUN_STAGE && current_menu != GE_MENU_INVALID); + /* D345: detect front-end screen changes (any current_menu flip, + * including into MENU_PC_OPTIONS / the F10 overlay's parent state) + * and open a short re-assert window. Runs every poll so the + * countdown drains even when the pointer block below is skipped + * (mouse disabled, unfocused window, legacy velocity mode). */ + if ((int)current_menu != menuPointerLastMenuId) { + menuPointerLastMenuId = (int)current_menu; + menuPointerTransitionFrames = 3; /* this poll + two more */ + } + if (menuPointerTransitionFrames > 0) menuPointerTransitionFrames--; + /* D194: aim mode drives the view from GRABBED relative deltas (see * aimAbsCompute) -- the cursor stays hidden and clipped to the window * for the whole hold. The earlier free-cursor experiment (suspend the @@ -1005,7 +1034,13 @@ unsigned inputComputePad(int idx, signed char *stick_x, signed char *stick_y) * because the stick's Y axis is what carries continuous analog * pitch there instead. Strafe and turn are unchanged -- both * schemes read them the same way. */ - if (naturalPitchMode) { + /* D345(d): natural pitch is an in-stage aim scheme -- the N64 front + * end navigates on the analog stick only, so in menus W/S (and the + * Up/Down arrows) must emit stick even with NaturalPitch=1, or no + * front-end screen (file select, cheat, MENU_PC_OPTIONS) can be + * driven vertically from the keyboard. Watch/pause keeps the C-button + * mapping: it is current_menu==RUN_STAGE (menuMode 0), N64-style. */ + if (naturalPitchMode && !menuMode) { if (actHeld(ks, IA_FORWARD)) button |= GE_CONT_E; /* C-up = forward */ if (actHeld(ks, IA_BACK)) button |= GE_CONT_D; /* C-down = back */ } else { @@ -1205,7 +1240,17 @@ unsigned inputComputePad(int idx, signed char *stick_x, signed char *stick_y) lastMenuMouseX = mx; lastMenuMouseY = my; } - if (mx != lastMenuMouseX || my != lastMenuMouseY) { + /* D345: re-assert right after a screen change when the + * mouse was the recent navigation device (click-to- + * enter). A stale idle mouse (WASD/arrow navigation) + * keeps the cede-to-keyboard behaviour: no yank. */ + Uint64 pcNow = SDL_GetPerformanceCounter(); + int mouseRecent = (menuPointerLastAbsWrite != 0) && + ((double)(pcNow - menuPointerLastAbsWrite) / + (double)SDL_GetPerformanceFrequency() * 1000.0) < + MENU_POINTER_REASSERT_MS; + if ((mx != lastMenuMouseX || my != lastMenuMouseY) || + (menuPointerTransitionFrames > 0 && mouseRecent)) { double fx = (double)mx / (double)ww; /* D335: under native widescreen the front end is * pillarboxed to a centred 4:3 region @@ -1226,6 +1271,7 @@ unsigned inputComputePad(int idx, signed char *stick_x, signed char *stick_y) cursor_v_pos = (float)(loV + fy * (hiV - loV)); sx = 0; /* pointer owns the cursor this poll */ sy = 0; + menuPointerLastAbsWrite = pcNow; } lastMenuMouseX = mx; lastMenuMouseY = my; @@ -1357,6 +1403,7 @@ unsigned inputComputePad(int idx, signed char *stick_x, signed char *stick_y) if (mb & SDL_BUTTON(SDL_BUTTON_RIGHT)) button |= GE_CONT_B; } menuPrevActive = menuMode; + s_menuPointerLive = (menuMode && mouseEnabled && !mouseGrabbed) ? 1 : 0; mouseDX = 0.0; mouseDY = 0.0; @@ -1506,6 +1553,20 @@ unsigned inputComputePad(int idx, signed char *stick_x, signed char *stick_y) return button; } +int inputMenuPointerLive(void) +{ + /* D345(b): true while the 1:1 menu pointer owns cursor_h/v_pos -- in a + * menu, the abs pointer is available, and the mouse was used recently. + * Port screens that teleport the crosshair (frontoptions.c cursorToItem) + * skip their snap when this is set: the cursor is already under the OS + * pointer. Keyboard/D-pad users (stale mouse) still get the snap. */ + if (!s_menuPointerLive || menuPointerLastAbsWrite == 0) return 0; + Uint64 now = SDL_GetPerformanceCounter(); + return ((double)(now - menuPointerLastAbsWrite) / + (double)SDL_GetPerformanceFrequency() * 1000.0) < + MENU_POINTER_REASSERT_MS; +} + static void applyGrab(int want) { want = want && mouseEnabled; diff --git a/port/src/optionsoverlay.c b/port/src/optionsoverlay.c index 64c2e3ed..b34d0516 100644 --- a/port/src/optionsoverlay.c +++ b/port/src/optionsoverlay.c @@ -71,9 +71,11 @@ extern s16 viGetY(void); enum { ROW_TOGGLE, ROW_SLIDER, ROW_ENUM, ROW_MSAA, ROW_RES, ROW_ACTION, ROW_FPSCAP, ROW_HEADER /* D237: non-selectable category label */ }; -static const char *const kOnOff[] = { "OFF", "ON", NULL }; -static const char *const kTexFilter[] = { "NEAREST", "BILINEAR", "3-POINT", NULL }; -static const char *const kAimMode[] = { "N64", "CENTRED (PC)", NULL }; /* D337 */ +/* D346: wording pass -- Nightdive/Turok + PD-port conventions: title-case + * On/Off, no all-caps value strings. Display-only; config stores 0/1 either way. */ +static const char *const kOnOff[] = { "Off", "On", NULL }; +static const char *const kTexFilter[] = { "Nearest", "Bilinear", "3-Point", NULL }; +static const char *const kAimMode[] = { "N64", "Centred (PC)", NULL }; /* D337 */ static const char *const kAimRange[] = { "PC", "N64", NULL }; /* D338 */ static const int kMsaaSeq[] = { 1, 2, 4, 8 }; /* D186: the sim's own tick pacemaker is hardcoded to the console's native VI @@ -123,29 +125,36 @@ struct Row { * disappears while its "auto" toggle is on). Resolved to hidePtr at init. */ const char *hiddenIfOn; int *hidePtr; + + /* D346: value-text decoration (display only). unit is appended to integer + * slider values ("%"/"x"); dispDiv>0 divides the raw value before display + * (e.g. deadzone raw 0..30000 -> % of full stick). NULL/0 = plain int. */ + const char *unit; + int dispDiv; }; static struct Row rows[] = { - { "__HdrDisplay", "DISPLAY", ROW_HEADER, 0, NULL, 0, 0, 0, 0,0,0,0,0 }, + { "__HdrDisplay", "VIDEO", ROW_HEADER, 0, NULL, 0, 0, 0, 0,0,0,0,0 }, { "Video.Fullscreen", "Fullscreen", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 }, { "__Resolution", "Resolution", ROW_RES, 0, NULL, 0, 0, 0, 0,0,0,0,0 }, { "Video.VSync", "VSync", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 }, - { "Video.FpsCap", "Frame cap", ROW_FPSCAP, 0, NULL, 0, 0, 0, 0,0,0,0,0 }, - { "Video.MSAA", "MSAA", ROW_MSAA, 0, NULL, 1, 0, 0, 0,0,0,0,0 }, + { "Video.FpsCap", "Frame rate cap", ROW_FPSCAP, 0, NULL, 0, 0, 0, 0,0,0,0,0 }, + { "Video.MSAA", "Anti-aliasing", ROW_MSAA, 0, NULL, 1, 0, 0, 0,0,0,0,0 }, { "Video.TextureFilter", "Texture filter", ROW_ENUM, 1, kTexFilter, 0, 0, 0, 0,0,0,0,0 }, - { "Video.Anisotropy", "Anisotropic", ROW_SLIDER, 1, NULL, 0, 0, 0, 0,0,0,0,0 }, - { "__HdrView", "VIEW", ROW_HEADER, 0, NULL, 0, 0, 0, 0,0,0,0,0 }, - { "Video.FovScale", "FOV scale %", ROW_SLIDER, 5, NULL, 0, 0, 0, 0,0,0,0,0 }, + { "Video.Anisotropy", "Anisotropic filtering",ROW_SLIDER, 1, NULL, 0, 0, 0, 0,0,0,0,0, NULL, "x" }, + /* D346b: the old VIEW section (FOV / widescreen / draw distance) is merged + * into VIDEO -- one Nightdive-style "video" section; rows keep order. */ + { "Video.FovScale", "FOV scale", ROW_SLIDER, 5, NULL, 0, 0, 0, 0,0,0,0,0, NULL, "%" }, /* D334: native widescreen (world projected at the window aspect, Hor+). * While on, "Widescreen auto FOV" has no effect (it was the stretch-era * vertical-FOV compensation). */ { "Video.NativeWidescreen", "Native widescreen",ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 }, { "Video.WidescreenAuto", "Widescreen auto FOV",ROW_TOGGLE,1, kOnOff, 0, 0, 0, 0,0,0,0,0 }, - { "Video.SafeAreaCrop", "Crop overscan bars", ROW_TOGGLE,1, kOnOff, 0, 0, 0, 0,0,0,0,0 }, - { "Video.DrawDistance", "Draw distance %", ROW_SLIDER, 25, NULL, 0, 0, 0, 0,0,0,0,0, "Video.DrawDistanceAutoFov" }, - { "Video.DrawDistanceAutoFov","Draw dist. auto", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 }, - { "Video.LodDistance", "LOD distance %", ROW_SLIDER, 25, NULL, 0, 0, 0, 0,0,0,0,0, "Video.LodDistanceAutoFov" }, - { "Video.LodDistanceAutoFov", "LOD dist. auto", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 }, + { "Video.SafeAreaCrop", "Crop overscan", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 }, + { "Video.DrawDistance", "Draw distance", ROW_SLIDER, 25, NULL, 0, 0, 0, 0,0,0,0,0, "Video.DrawDistanceAutoFov", "%" }, + { "Video.DrawDistanceAutoFov","Draw dist. follows FOV",ROW_TOGGLE,1,kOnOff, 0, 0, 0, 0,0,0,0,0 }, + { "Video.LodDistance", "LOD distance", ROW_SLIDER, 25, NULL, 0, 0, 0, 0,0,0,0,0, "Video.LodDistanceAutoFov", "%" }, + { "Video.LodDistanceAutoFov", "LOD dist. follows FOV",ROW_TOGGLE,1,kOnOff, 0, 0, 0, 0,0,0,0,0 }, /* Aim row edits Input.AimModeSens -- the knob the default GEPD aim path * actually uses (Input.MouseAimSpeed only feeds the legacy velocity-stick * fallback, so it was inert here). D304: both this row and the turn-speed @@ -177,7 +186,7 @@ static struct Row rows[] = { /* { "Input.SensLink", "Link aim/turn sens",ROW_TOGGLE,1, kOnOff, 0, 0, 0, 0,0,0,0,0 }, */ { "__HdrMouse", "MOUSE / AIM", ROW_HEADER, 0, NULL, 0, 0, 0, 0,0,0,0,0 }, { "Input.MouseSensitivity", "Mouse sensitivity",ROW_SLIDER, 5, NULL, 0, 0, 0, 0,0,0,0,0 }, - { "Input.MouseInvertY", "Mouse invert Y", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 }, + { "Input.MouseInvertY", "Invert look (mouse)",ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 }, /* Input.PdMouseAim (findings D332): the Perfect Dark port's mouse-aim * model. The port only accumulates the mouse; the game's own crosshair * integrator is driven by the port-supplied turn with PD's near-zero damp @@ -195,9 +204,9 @@ static struct Row rows[] = { * style is CENTRED (PC), where the crosshair doesn't travel. */ { "Input.AimRange", "Aim range", ROW_ENUM, 1, kAimRange, 0, 0, 0, 0,0,0,0,0, "Input.AimMode" }, { "__HdrPad", "CONTROLLER", ROW_HEADER, 0, NULL, 0, 0, 0, 0,0,0,0,0 }, - { "Input.PadLookInvertY", "Pad invert look Y",ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 }, - { "Input.PadDeadzone", "Stick deadzone", ROW_SLIDER, 500, NULL, 0, 0, 0, 0,0,0,0,0 }, - { "Input.PadTriggerPct", "Trigger threshold %",ROW_SLIDER,1, NULL, 0, 0, 0, 0,0,0,0,0 }, + { "Input.PadLookInvertY", "Invert look (controller)",ROW_TOGGLE,1,kOnOff, 0, 0, 0, 0,0,0,0,0 }, + { "Input.PadDeadzone", "Stick deadzone", ROW_SLIDER, 500, NULL, 0, 0, 0, 0,0,0,0,0, NULL, "%", 300 }, + { "Input.PadTriggerPct", "Trigger threshold",ROW_SLIDER, 1, NULL, 0, 0, 0, 0,0,0,0,0, NULL, "%" }, /* D181/Game.ScreenShakeIntensity: user testing (v0.2.1) found the slider * "basically useless" -- viShake() is only called from explosion.c, so it * scales explosion shake alone; it never touches the always-on walking @@ -210,7 +219,7 @@ static struct Row rows[] = { { "__HdrGame", "GAME", ROW_HEADER, 0, NULL, 0, 0, 0, 0,0,0,0,0 }, /* D226: scales the ammo counter, pickup / status messages and dialogue * about their screen anchors. 100% = original, nothing emitted. */ - { "Game.HudScale", "HUD scale %", ROW_SLIDER, 5, NULL, 0, 0, 0, 0,0,0,0,0 }, + { "Game.HudScale", "HUD scale", ROW_SLIDER, 5, NULL, 0, 0, 0, 0,0,0,0,0, NULL, "%" }, { "Game.SkipIntro", "Skip intro", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 }, { "Video.DisplayFPS", "Show FPS", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 }, { "Game.NoHitFlash", "No hit flash", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 }, @@ -922,7 +931,7 @@ static void valueText(const struct Row *r, char *out, int n) } } if (r->kind == ROW_MSAA) { - if ((int)lround(v) <= 1) snprintf(out, n, "OFF"); + if ((int)lround(v) <= 1) snprintf(out, n, "None"); else snprintf(out, n, "%dx", (int)lround(v)); return; } @@ -940,7 +949,15 @@ static void valueText(const struct Row *r, char *out, int n) else snprintf(out, n, "%d FPS", fps); return; } - snprintf(out, n, "%d", (int)lround(v)); + /* D346: integer sliders -- optional raw->display divide + unit suffix. */ + if (r->dispDiv > 0) { + v = (double)(int)lround(v / (double)r->dispDiv); + } + if (r->unit) { + snprintf(out, n, "%d%s", (int)lround(v), r->unit); + } else { + snprintf(out, n, "%d", (int)lround(v)); + } } static Gfx *drawText(Gfx *gdl, s32 x, s32 y, const char *str, u32 colour)