From 10b278311f6c37fd112f28a8d412d2185d19ed7c Mon Sep 17 00:00:00 2001 From: James Dansereau <106631954+jkdansereau@users.noreply.github.com> Date: Wed, 2 Sep 2026 01:30:01 -0400 Subject: [PATCH] =?UTF-8?q?docs:=20M-35=20QoL=20run=20=E2=80=94=20review?= =?UTF-8?q?=20sheet,=20playtest=20checklist,=20options-menu=20plan?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - M-35-QOL-REVIEW.md: per-change file:area breakdown, config keys with ranges/defaults, verification evidence. - M-35-PLAYTEST-CHECKLIST.md: the human handoff — what to do, what correct feels like, which key tunes it. Item 0 (default-config regression) first. - OPTIONS-MENU-PLAN.md: WI-3 design — recommends a port-layer fast3d 2D overlay over a src/ menu edit; resume checklist. Claude-Session: https://claude.ai/code/session_01GULf6knbsqfsnW39tMr9WT --- docs/dev/M-35-PLAYTEST-CHECKLIST.md | 89 +++++++++++++++++++ docs/dev/M-35-QOL-REVIEW.md | 132 ++++++++++++++++++++++++++++ docs/dev/OPTIONS-MENU-PLAN.md | 107 ++++++++++++++++++++++ 3 files changed, 328 insertions(+) create mode 100644 docs/dev/M-35-PLAYTEST-CHECKLIST.md create mode 100644 docs/dev/M-35-QOL-REVIEW.md create mode 100644 docs/dev/OPTIONS-MENU-PLAN.md diff --git a/docs/dev/M-35-PLAYTEST-CHECKLIST.md b/docs/dev/M-35-PLAYTEST-CHECKLIST.md new file mode 100644 index 00000000..959eb150 --- /dev/null +++ b/docs/dev/M-35-PLAYTEST-CHECKLIST.md @@ -0,0 +1,89 @@ +# M-35 QoL playtest checklist + +Everything on this branch is config-gated to prior behaviour, so the +**first** thing to confirm is that nothing changed with a default +`ge007.ini`. Then flip the new keys and check the feel. + +Edit `data/ge007.ini` while the game is closed. + +--- + +## 0. Regression (default config) — do first + +- [ ] Delete `data/ge007.ini`, launch, quit. It rewrites with new keys: + `[Input] MouseCaptureMode = 0`, `[Game] ScreenShakeIntensity = 1`. + Existing keys keep their values. +- [ ] Play a level normally (mouse grabbed on focus, as before). Mouse look, + aim (RMB), menus — all exactly as the last build. **No behaviour change + expected at defaults.** +- [ ] Alt-tab away and back — cursor frees / re-grabs (unchanged). + +## 1. Click-to-lock capture — `Input.MouseCaptureMode = 1` + +- [ ] Launch. At the main menu the **OS cursor is visible and free**; moving + the mouse moves the game crosshair 1:1 (see item 3). +- [ ] Click in the window during a level → cursor hides, mouse-look engages. +- [ ] Press **ESC** during a level → cursor reappears, mouse-look stops, + you can move the pointer off the window. (ESC does *not* quit.) +- [ ] Click back in → re-locks. First click only re-locks — it must **not** + fire the weapon. +- [ ] Open the pause/watch (Start), then a front-end menu → cursor frees + automatically. Return to the stage → re-locks without a click. +- [ ] Alt-tab out mid-level → cursor frees; alt-tab back → stays free until + you click (Quake behaviour). +- **Correct feel:** exactly like Quake/Nightdive remasters — click to play, + ESC to get your cursor back. +- **Tune:** none — it's a mode toggle. + +## 2. Aim sensitivity — B3 + +- [ ] With `Input.MouseCaptureMode` either value, hold RMB (aim mode) and + make small aiming corrections. Default is now `MouseAimSpeed = 16` + (was 25). +- **Correct feel:** you can settle the sight on a distant guard's head + without overshooting; a fast flick still crosses the screen. +- **Tune:** `Input.MouseAimSpeed` (1–100+). Up = faster. If hipfire turning + now feels faster than aiming, raise `MouseAimSpeed` or lower + `Input.MouseTurnSpeed` (default 100). `Input.AimBand` (5–40, default 20) + widens the usable stick range past the aim gate — raise it if aim feels + capped/notchy at the extremes. +- [ ] Check the D166 hipfire pitch (mouse up/down without RMB): still + proportional taps, consistent with the yaw. Tune + `Input.HipfirePitchSpeed` (default 100). + +## 3. Menu pointer 1:1 — needs `Input.MouseCaptureMode = 1` + +- [ ] On the mission-select grid, move the OS cursor to each of the four + **corner** level tiles — the game crosshair must reach all of them + (this is the D169 case; absolute mapping should make it exact now). +- [ ] File-select, mode-select, difficulty-select, watch menu — crosshair + tracks the OS cursor with no drift, no lag, no acceleration. +- [ ] Keyboard nav (arrows / stick) still works on every screen. +- **Correct feel:** the crosshair sits exactly under the OS pointer, like a + normal PC menu. +- **Tune:** `Input.MenuPointerSpeed` (default 100) still scales the legacy + relative path; with capture mode the absolute path ignores it. If tracking + feels off, check the window isn't a weird aspect ratio (the map assumes the + render fills the window). +- **Note:** in legacy mode (`MouseCaptureMode = 0`) the menu still uses the + D165/D169 relative estimator — 1:1 menu tracking requires capture mode. + +## 4. Screen shake — `Game.ScreenShakeIntensity` + +- [ ] Default `1` — explosions shake the screen exactly as before. +- [ ] Set `0` — no shake on explosions / big hits. +- [ ] Set `2`–`3` — exaggerated. +- **Correct:** `1` is indistinguishable from the current build. + +--- + +## Single most important thing to test first + +**Item 0** — confirm a default `ge007.ini` produces a build that plays +identically to `def0e0ac`. If that holds, everything else is opt-in polish. + +## Known not-done (parked, see OPTIONS-MENU-PLAN.md) + +- No in-game options **menu** yet — only the config file + the expanded + `config.c` API and the `Game.ScreenShakeIntensity` hook. The menu is a + port-layer overlay design, documented, not built. diff --git a/docs/dev/M-35-QOL-REVIEW.md b/docs/dev/M-35-QOL-REVIEW.md new file mode 100644 index 00000000..9fca6e24 --- /dev/null +++ b/docs/dev/M-35-QOL-REVIEW.md @@ -0,0 +1,132 @@ +# M-35 — Native-PC input + options-menu QoL review + +Branch: `qol/native-pc-input-menu` (cut from `def0e0ac`). Port-layer only +except one documented route-(b) `#ifdef PORT` line in `src/fr.c` (D181). +Every new behaviour is config-gated with the default reproducing prior +behaviour; `tools_pc/golden/` headless captures are unaffected at defaults +(no mouse in a headless run; `Game.ScreenShakeIntensity` default 1.0 is a +no-op multiply). + +Findings: **D180** (input pass), **D181** (screen-shake hook). See +`docs/dev/findings.md` §F. + +--- + +## WI-1 — In-game mouse: Quake-style click-to-lock + +**Status: landed, feel-check owed.** + +### What changed + +| File:area | Change | +|---|---| +| `port/src/input.c` — new statics `mouseCaptureMode` / `captureArmed` / `windowFocused` | Capture-mode state. `mouseCaptureMode` registered as `Input.MouseCaptureMode` (0/1, **default 0**). | +| `port/src/input.c` `inputInit` | In capture mode, start **un**grabbed (`SDL_SetRelativeMouseMode(FALSE)`), wait for a click. Legacy mode unchanged (grab on start). | +| `port/src/input.c` `applyGrab()` / `reconcileGrab(menuMode)` | `applyGrab` is the old `inputSetMouseGrab` body (SDL relative-mode toggle + delta drain). `reconcileGrab` computes the wanted grab state: legacy → `windowFocused`; capture → `captureArmed && windowFocused && !menuMode`. Called once per controller-0 poll and from every notify hook. | +| `port/src/input.c` `inputSetMouseGrab(on)` | Now just records `windowFocused` and calls `reconcileGrab` — so focus loss/gain works in both modes without special-casing. | +| `port/src/input.c` `inputNotifyClick()` | Capture mode + focused → `captureArmed = 1` + regrab. No-op otherwise (game sees the click). | +| `port/src/input.c` `inputReleaseCapture()` | Capture mode + grabbed → `captureArmed = 0`, release, return 1 (caller swallows the key). Returns 0 otherwise (ESC falls through to the N64 B button, D145). | +| `port/src/input.c` `inputComputePad` idx 0 | Calls `reconcileGrab(menuMode)`; when capture-mode + free + in a stage, zeroes the local `mb` so mouse buttons never reach the game (no phantom fire from the re-lock click). | +| `port/src/video.c` event pump | `SDL_MOUSEBUTTONDOWN` → `inputNotifyClick()`. `SDLK_ESCAPE` keydown → `inputReleaseCapture()` (before the existing fall-through). | +| `port/include/input.h` | Prototypes for the three new hooks. | + +### Config keys + +| Key | Range | Default | Effect | +|---|---|---|---| +| `Input.MouseCaptureMode` | 0–1 | **0** | 0 = legacy (cursor grabbed whenever the window is focused). 1 = click-to-lock: free cursor until you click the window; ESC / focus-loss / opening a menu frees it; entering a stage while "armed" re-locks. | +| `Input.MouseAimSpeed` | 1–500 | **16** (was 25) | Aim-mode (RMB / LShift) sensitivity %. B3: still overshot at 25. | + +Controller input is completely independent of all of the above — the pad +branch in `inputComputePad` is untouched, and `reconcileGrab` only ever +toggles `SDL_SetRelativeMouseMode`. + +### D166 / hipfire-pitch interaction + +The D166 hipfire pitch pulse is unchanged. It only runs when the cursor is +grabbed (mouse deltas are zero while free), so click-to-lock does not +perturb it. The M-24 aim-band (`61 + gain` into the `60 + aimBand` ceiling) +is unchanged; only the `mouseAimSpeed` scalar feeding `gx`/`gy` moved. + +### Owed feel-checks (playtest checklist items 1–4) + +Headless runs cannot move the mouse, so the capture ergonomics, the new +`MouseAimSpeed = 16` default, and the D166 pulse consistency under the new +model are all inferred, not observed. + +--- + +## WI-2 — Menu pointer: absolute cursor tracking + +**Status: landed for capture mode, feel-check owed. Route (a), port-only.** + +### What changed + +`port/src/input.c`, the `menuMode` pointer branch (the D165/D169 +P-controller). Added an **absolute-position** target source: when +`mouseCaptureMode && !mouseGrabbed` (i.e. a real free OS cursor over a +front-end screen), read `SDL_GetMouseState` + `SDL_GetWindowSize` of the +mouse-focus window, normalise to `[0,1]²`, and map straight onto the live +virtual front-end rect (`loH..hiH × loV..hiV`, already derived from +`getPlayer_c_screen*` for D169). That becomes `menuTgt{H,V}` directly +(no accumulation, no drift). The P-controller/estimator below is unchanged +and simply chases the target, so the game's `cursor_h_pos/v_pos` lands +exactly where the OS pointer is. + +The relative-delta path (legacy `MenuPointerMode`, or capture mode while +grabbed) is **unchanged** — `haveAbs` gates only the new assignment. + +### Why this is enough for 1:1 + +The D169 fix already made the clamp rect track the real 440×330 field. The +only thing missing for true 1:1 was a non-drifting target: a free OS cursor +*is* an absolute device, so mapping its window fraction onto the rect is +exact. No route-(b) hook needed. + +### Keyboard nav parity + +Untouched — the menu branch only ever adds to `sx/sy`; D-pad / stick keys +still feed the same axes. The D118d `front.c` latent sibling was **not** +touched (no evidence the absolute path exposes it; the stick values emitted +are small P-controller corrections, never a slammed ±80). + +### Caveat + +Absolute tracking is only wired for `MouseCaptureMode = 1`. In legacy mode +(default) the menu still uses the D165/D169 relative estimator. If +click-to-lock becomes the default later, WI-2 is 1:1 for free; until then +menu 1:1 requires `Input.MouseCaptureMode = 1`. + +--- + +## WI-3 — In-game video/options menu + +**Status: config-system foundation landed; menu surface = design doc only.** +See `docs/dev/OPTIONS-MENU-PLAN.md`. + +### Landed + +- `configRegisterFloat` / `configRegisterUInt` + clamps (`port/src/config.c`, + `port/include/config.h`). `configSave` rewritten to emit all four option + kinds section-grouped in one pass. +- `Game.ScreenShakeIntensity` (0.0–10.0, default 1.0) — the first + route-(b) hook, D181, proving the pattern the menu will lean on. + +### Not landed (budget) — precise resume point in the design doc + +- The menu tables / handlers themselves. GE's menu system (`front.c` / + `initmenus.c` / `mainmenu.c`) differs enough from PD's `optionsmenu.c` + that it is an adaptation, not a copy. Design doc covers the entry point, + the handler shape, and the minimum option set. + +--- + +## Verification evidence + +| Check | Result | +|---|---| +| `./build-pc.sh ntsc-final` | green, 241/241 | +| `-level_09` framediff (200/320/440) | 3/3 within threshold; nonclear Δ 0.00–0.01pp (91.6%); phash 0/1/5 = documented D117 intro-pan noise | +| `GE_STARTMENU=7` boot | crash-free, no `ge007.crash.log` | +| golden dumps at defaults | unaffected (no headless mouse; shake scale 1.0 = no-op) | +| linkcheck | see PR body | diff --git a/docs/dev/OPTIONS-MENU-PLAN.md b/docs/dev/OPTIONS-MENU-PLAN.md new file mode 100644 index 00000000..be3c7eed --- /dev/null +++ b/docs/dev/OPTIONS-MENU-PLAN.md @@ -0,0 +1,107 @@ +# In-game PC options menu — design + resume point + +Status: **config-system foundation landed (M-35); menu surface not built.** +Pattern follows `docs/dev/AUDIO-PLAN.md` — a plan doc that a later session +executes. + +Findings context: D180 (input), D181 (first route-(b) hook). Config API: +`configRegister{Int,UInt,Float,String}` with clamps (`port/include/config.h`). + +--- + +## 1. What PD does, and why it doesn't port directly + +PD's `pd_port/port/src/optionsmenu.c` (~2000 lines) hooks port-side handlers +into **PD's own decomp menu tables** (`menudata`/`menuitem` arrays with +`MENUITEMTYPE_SLIDER` etc.). It works because PD's front end is a generic +data-driven list menu. + +GE is different. GE's settings UI is the **in-game watch** (`src/game/options.c`): +`draw_watch_game_options_page` / `draw_watch_control_options_page` + +`watch_screenN_navigation` — hand-drawn pages with bespoke nav functions, +not a table. The front-end menus (`front.c`, `initmenus.c`, `mpmenu.c`) are +also bespoke draw/nav pairs, not a reusable list widget. + +So there is **no table to inject rows into**. A PC options surface is either +(A) a new hand-built watch page, (B) a new hand-built front-end screen, or +(C) a port-layer overlay drawn outside the game's menu system. + +## 2. Recommended approach: (C) port-layer overlay + +A self-contained immediate-mode overlay in `port/`, toggled by a hotkey +(e.g. `F10`), drawn as fast3d 2D quads + the game's own `textRender` over +the top of whatever is on screen, with keyboard/mouse nav handled entirely +in the port layer. Reasons: + +- **Zero `src/` menu-code edits.** No new bespoke nav function to get wrong + (the watch-nav code is exactly the D118d / over-scroll family). +- Works in-level *and* in the front end (it's above the game). +- The values it edits are already port-owned `config.c` variables — the + overlay is just a view over the registered option list. +- Precedent: the port already draws port-owned UI (FPS in the window title, + F12 screenshot). This is the render-side equivalent. + +Cost: an immediate-mode widget layer (label, slider, toggle, dropdown) in +fast3d 2D. ~300–500 lines. The `config.c` registry already gives us +key/min/max/value + a type tag (after M-35) — enough to auto-generate rows. + +### Sketch + +``` +port/src/optionsoverlay.c + optionsOverlayToggle() // F10 in video.c event pump + optionsOverlayHandleInput() // called from inputComputePad idx 0 when open: + // swallow kbd/mouse from the game, drive the cursor + optionsOverlayRender(Gfx**) // called from a fast3d 2D hook after the game DL +``` + +Registry additions needed in `config.c`: +- `configForEachOption(cb)` — iterate {key, type, ptr, min, max}. +- optional per-option metadata: display label, step, enum-value names, + "apply live" vs "needs restart". Add a `configRegisterIntEx(...)` variant + or a side table keyed by dotted key. + +Live-apply: most keys already re-read their variable every frame +(`mouseAimSpeed`, `portScreenShakeScale`, `cfgVSync` via `videoSetVSync`). +Ones that need a hook (MSAA, fullscreen) get an `configOnChange` callback +or are flagged "restart". + +## 3. Minimum option set for v1 + +| Row | Key | Type | Notes | +|---|---|---|---| +| VSync | `Video.VSync` | toggle | live (`SDL_GL_SetSwapInterval`) | +| Frame cap | `Video.FpsCap` | int slider 0–360 | live | +| MSAA | `Video.MSAA` | dropdown 1/2/4/8 | needs FBO rebuild — flag "restart" for v1 | +| Texture filter | `Video.TextureFilter` | dropdown nearest/bilinear/3-point | live-ish | +| Mouse aim speed | `Input.MouseAimSpeed` | slider 1–100 | live | +| Mouse turn speed | `Input.MouseTurnSpeed` | slider 1–100 | live | +| Mouse invert Y | `Input.MouseInvertY` | toggle | live | +| Capture mode | `Input.MouseCaptureMode` | toggle | live | +| Screen shake | `Game.ScreenShakeIntensity` | slider 0–3 (×) | live (D181) | +| Screenshot key | — | (F12, documented; rebind = later) | — | + +`configSave()` on overlay close. + +## 4. Alternative if an in-fiction surface is required later + +Route (B): a new front-end screen `constructor_menuXX_pcoptions` + +`menuXX_pcoptions_navigation` in `front.c` under `#ifdef PORT`, reached from +a new "PC OPTIONS" row on the main options menu. Each row calls a +`configGet/Set` shim. This is a genuine route-(b) `src/` edit (menu content +change) → its own Dxx, N64 verbatim under `#else`, opt-in. More faithful, +more surface area, more nav bugs. Defer unless the owner wants it in-fiction. + +## 5. Resume checklist + +1. Add `configForEachOption` + a label/step/enum side-table to `config.c`. +2. Build the fast3d 2D immediate-mode widget helpers (reuse `gDPFillRectangle` + + `textRender`; see how `options.c draw_watch_game_options_page` emits + text for the font/`Gfx**` conventions). +3. `port/src/optionsoverlay.c` state machine + F10 toggle in `video.c`. +4. Route input: when open, `inputComputePad` idx 0 returns a neutral pad and + forwards nav to the overlay (same "swallow" pattern WI-1 uses for the + free-cursor click). +5. `configOnChange` hooks for MSAA/fullscreen; everything else is live. +6. Verify: overlay open/close in-level and in the front end; golden dumps + unaffected with the overlay closed (it draws nothing).