Files
goldeneye-pc-port/docs/dev/OPTIONS-MENU-PLAN.md
T
James Dansereau fe57d73924 optionsoverlay/watchsettings/frontoptions: D356 settings regroup — functional sections, save-file scoping, per-section reset-to-defaults (Gate E)
- 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.
2026-09-27 14:49:08 -04:00

5.7 KiB
Raw Blame History

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.

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

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 PORT draw 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

  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).