One Rule-2 #ifdef PORT draw hook in constructor_menu05_fileselect (user sign-off 2026-09-27). The label's hit test and open request live in optionsoverlay.c; the open is applied on the input thread. While the overlay is open on file select, g_MenuTimer is held at 0 so the screen no longer drops to the legal screen after 30 s.
5.4 KiB
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.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).