- 5 sections (INPUT/GAMEPLAY/GRAPHICS/AUDIO/VIDEO, all below MAX_PROWS): DISPLAY+GRAPHICS merged into GRAPHICS; AUDIO carries the watch music/FX sliders out of the Bond-file page; VIDEO keeps the pure video rows; __HdrMouse/__HdrPad merged into INPUT; __HdrBond/__BondFile retired for the front-only top save-file row (frontoptions.c) - save-file scoping: per-file sections (GAMEPLAY, AUDIO) annotated '(File N)'/'(no file)' via the new Row.saveScoped flag + watchSettingsActiveFolder(); per-file rows in mixed sections carry a dim '(save)' tag; the D353 optionsRowIsBondChooser special-case is generalized (both UIs) - N64-watch rows Bond.Look / Bond.AimControl commented out of the menu surface (D181/D216 style: keys + watch paths stay live) - per-section 'Reset to defaults' replaces the Turok rows (plan Gate E): edge-triggered two-step arm->confirm within 3 s, one commit, disarm on timeout / navigate-away / close (optionsResetMaintain/Clear); rowResetSection walks the section's declared range: watch rows -> BLANKSAVEDATA through the D352 commit path (front direct, stage queue), ini rows -> kResetDefaults[] (the port's C initializers); __Resolution is a documented exclusion (action-backed) - GE_WSPROBE_RESET probe: front tick-120 prepare/dispatch/verify, stage tick-120 dispatch / tick-121 verify; a direct -level_XX boot starts its timer from phase 0 (front prepare is front-only) - docs: findings.md D356 + index + CSV, WATCH-SETTINGS-PLAN status, OPTIONS-MENU-PLAN pointer, plan doc status -> LANDED Verified headless: GE_WSPROBE_RESET front (all 5 sections failures=0, __Resolution untouched, file-2 scope isolation OK) and -level_33 (6 commands queued, all sections verified post-drain, saved music/sfx = 0xFF); GE_WSPROBE / GE_WSPROBE_FRONT regressions clean; 248/248 build + link.
5.7 KiB
In-game PC options menu — design + resume point
Status: config-system foundation landed (M-35); menu surface not built.
(Updated 2026-09-27: the menu surface has since been built and iterated
as D346 -> D356; the current layout and per-section reset live in
docs/dev/D356-SETTINGS-REGROUP-PLAN.md + docs/dev/WATCH-SETTINGS-PLAN.md
and docs/dev/findings.md D346–D356. This doc remains the historical
M3-scope record.)
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.cvariables — 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
2026-09-27: route B chosen (v0.4.0 intake). The entry point is on the file select screen, not a main options menu (GE's front end has none). M1 landed as D343: an "Options" label that opens the F10 overlay, via one Rule-2
#ifdef PORTdraw hook. M2 (a GE-styled screen that reuses the overlay's rows) follows.
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
- Add
configForEachOption+ a label/step/enum side-table toconfig.c. - Build the fast3d 2D immediate-mode widget helpers (reuse
gDPFillRectangletextRender; see howoptions.c draw_watch_game_options_pageemits text for the font/Gfx**conventions).
port/src/optionsoverlay.cstate machine + F10 toggle invideo.c.- Route input: when open,
inputComputePadidx 0 returns a neutral pad and forwards nav to the overlay (same "swallow" pattern WI-1 uses for the free-cursor click). configOnChangehooks for MSAA/fullscreen; everything else is live.- Verify: overlay open/close in-level and in the front end; golden dumps unaffected with the overlay closed (it draws nothing).