- 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.
20 KiB
D356 — Settings regroup: functional sections, save-file scoping, per-section reset
Status: LANDED (D356, 2026-09-27, port-only) — signed off and implemented
this session; verification in docs/dev/findings.md D356 (GE_WSPROBE_RESET
front + in-stage probes, regressions, build). Supersedes the "BOND FILE"
section introduced in D353. Baselines: Turok PC port options (Nightdive, list
provided by the maintainer), the PD port's port/src/optionsmenu.c
(C:\Users\james\Source\Repos\pd_port), and plan §6 of
docs/dev/OPTIONS-MENU-PLAN.md (the original "align with Turok" note).
1. Goal
The D353 "BOND FILE" section is the one section named after where a value is
saved — an implementation detail — while every other section is functional
(DISPLAY, GRAPHICS, MOUSE/AIM, CONTROLLER, GAME). Its contents are also a
mishmash (aim/look toggles + HUD toggles + audio), and it exposes N64-watch
rows that collide with or duplicate the PC-native rows the port added
(watch look-invert vs Input.MouseInvertY/Input.PadLookInvertY; watch aim
control vs Input.AimMode). The baseline ports (Turok, PD) never name a
section after the save and never re-expose legacy overrides.
D356 rewrites the row table so that:
- Every section is functional, using the baseline names: INPUT, GAMEPLAY, GRAPHICS, AUDIO, VIDEO.
- Save-file scope is communicated the PD way — in the section title ("GAMEPLAY (File 1)"), generalized from the D353 BOND-FILE annotation — plus a single Save file: File N [change] row at the top of the options screen (PD "Player N" pattern; D354's default resolution applies). "Bond file" appears nowhere in the UI.
- Redundant/colliding watch rows are removed from the menu surface only (the established D181/D216/D304 pattern): keys, in-game watch behavior, and ini hand-editing all remain untouched. No game-code change (rule 2).
- A Reset to defaults action row per section (Turok's per-category reset; plan §6 Gate E, made concrete) with a confirmation contract: edge-triggered activation (no auto-repeat), two-step arm→confirm, never repeatable (WATCH-SETTINGS-PLAN §6 gate: "separate confirmation and persistence policy before wiring a button" — §5.4 below is that policy).
- A context-aware file accessor (front-end target vs active stage target) with the "none" chooser position retired, so "Save file: File N" is always a true statement (§5.5).
- Section resets iterate the section's declared rows (including conditionally hidden ones, §5.6) and reset verification exercises the UI action itself, headlessly (§5.8).
2. Non-goals
- No
src/gamechanges; the N64 watch menu keeps all its options and its N64 wording (ground truth). - No key rebinding (Phase 4) — but the section layout leaves room for a "KEY BINDINGS" page exactly where Turok/PD have one.
- Master volume stays M3-backlog; the AUDIO section is shaped for it (Turok's Master/Sound/Music trio).
- N64 build untouched.
3. Exposure filter (menu surface only)
| Row (key) | Verdict | Reason |
|---|---|---|
Bond.Look ("Look up/down (watch; stacks)") |
Hide | Duplicates Input.MouseInvertY + Input.PadLookInvertY; stacking both produces the double-negation trap. |
Bond.AimControl ("Aim control") |
Hide | Collides with Input.AimMode (N64 vs PC aim model) — two rows fighting over the same domain. |
Bond.AutoAim |
Keep | Unique gameplay assist; orthogonal to aim style/range. |
Bond.LookAhead |
Keep | Unique; no PC counterpart, no conflict. |
Bond.Sight, Bond.Ammo |
Keep | Unique HUD-visibility options; compose with Game.HudScale. |
Bond.Music, Bond.FX |
Keep | Core of D350–D355. |
Hidden rows are commented out in rows[] with a D356 note (D181/D216 style:
config keys + watch path stay live; ini power users can still set them).
4. New layout (5 pages + save-file row)
MAX_PAGES=8, MAX_PROWS=15 — all pages below the caps (D353 overflow log
still guards).
Top row (drawn by frontoptions.c above the paged content, not paged):
Save file: File 1 [change] — cycles files via watchSettingsChooseFile
(D354 default resolution: explicit pick > viewed folder > first valid;
auto-init fallback). Hidden in F10 (existing optionsRowIsBondChooser
mechanism, generalized). F10 targets the active file, as today.
| Page | Rows (key · kind) | Title annotation |
|---|---|---|
| INPUT | Input.MouseSensitivity · Input.MouseInvertY · Input.AimMode · Input.AimRange · Input.PadLookInvertY · Input.PadDeadzone · Input.PadTriggerPct · Reset to defaults |
|
| GAMEPLAY (File N) | Bond.AutoAim · Bond.LookAhead · Bond.Sight · Bond.Ammo (save, dim "(save)" tag) · Game.SkipIntro · Game.NoHitFlash · Game.AllUnlocked · Game.HudScale · Reset to defaults · __QuitToDesktop |
(File 1) |
| GRAPHICS | 11 rows unchanged from D353 · Reset to defaults | |
| AUDIO (File N) | Bond.Music · Bond.FX (save; title annotation only — homogeneous section, PD-style) · (master volume, M3) · Reset to defaults |
(File 1) |
| VIDEO | Video.Fullscreen · __Resolution · Video.VSync · Video.FpsCap · Video.DisplayFPS · Reset to defaults |
Moves from D353 layout: mouse/aim + controller rows merge into INPUT
(the two old headers __HdrMouse/__HdrPad are dropped); DISPLAY → VIDEO
(rename, Video.DisplayFPS moves from GAME); GAME section is renamed
GAMEPLAY and absorbs the surviving watch toggles; the __HdrBond header
and __BondFile chooser row are retired (replaced by the top save-file row);
Bond.Look/Bond.AimControl rows are commented out (§3).
Scope annotation: the D353 "(File N)"/"(no file)" title annotation — today
special-cased to the BOND FILE page via optionsRowIsBondChooser() — is
generalized: any page containing per-file rows annotates. Sections with
per-file rows: GAMEPLAY, AUDIO.
F10: same table (F10 already shows all rows, chooser hidden). The (Gameplay/Audio) annotation in-stage shows the active file number (the context-aware accessor, §5.8 — not the raw chooser value). "Reset to defaults" rows stay visible in F10 (O2) under the same confirmation contract (§5.4).
(save) tag (O1): the four mixed-scope GAMEPLAY save rows carry a subtle
dim "(save)" suffix — the only place a section mixes scopes. AUDIO is
homogeneously per-file, so its title annotation is sufficient (no tags).
Implementation: one int saveScoped field on struct Row + dim-ink suffix in
the row-draw path (frontoptions.c already draws the dim annotation).
5. Code changes
All in port/ (D350-family); files: port/src/optionsoverlay.c,
port/src/frontoptions.c, port/include/optionsoverlay.h,
port/src/watchsettings.c (only if reset needs a new enqueue helper —
expected not to, see below).
5.1 Row table (optionsoverlay.c)
- Rename headers:
__HdrDisplay→VIDEO (last page), add__HdrInput(INPUT),__HdrGameplay(GAMEPLAY); delete__HdrBond,__HdrMouse,__HdrPad, and rename__HdrGame→GAMEPLAY. New section order in the table: INPUT, GAMEPLAY, GRAPHICS, AUDIO, VIDEO (Turok's order: Input, Gameplay, Graphics, Audio, Video). - Move the 7 mouse/aim + 3 controller rows under INPUT; move the 4 surviving
Bond.*toggles + music/FX into GAMEPLAY/AUDIO; comment outBond.Look/Bond.AimControl(§3). - New
ROW_ACTIONrows, one per section:.key="__Reset<Section>",.label="Reset to defaults". All five use one shared handler (rowResetSection(section)).
5.2 Save-file top row (frontoptions.c)
- New non-paged line above the page content: "Save file: File N" + a
"change" control (left/right or Enter cycles, reusing
watchSettingsChooseFile(dir); D354 lazy default fills N). Dim ink, same visual language as the D353 annotation.selected_folder_numunchanged — the row never touches the game's active file (plan Gate B invariant). Display value reads through the context-aware accessor (§5.5), never the raw chooser value. - Unavailable state (maintainer consistency fix): the D354 auto-init fallback creates the blank file on a later game tick, so at an empty/corrupt-save startup the accessor can return -1 for a few ticks. The row then shows the unavailable state ("(no file)"), never a file number the write path cannot use. Once creation succeeds the same tick the row shows "File 1".
- F10 context: row suppressed (extend the D353 accessor to cover it).
5.3 Reset to defaults (Gate E)
- Per-file rows (GAMEPLAY/AUDIO save rows): defaults = BLANKSAVEDATA
(
src/game/file2.c— the game's own factory save; per WATCH-SETTINGS-PLAN §6, watch rows reset to BLANKSAVEDATA/DEFAULT_OPTIONSfor the selected file, not from a global ini snapshot; new saves start Music/FX 0xFF, auto-aim/sight/look-ahead/ammo enabled — read the fields fromBLANKSAVEDATAat reset time, no hardcoded table). Reset re-applies live via the existingwatchsettingsapply path and persists field-scoped to the selected file (watchSettingsPersistField), exactly like a normal commit. In-stage: enqueued through the D352 command queue (N field commits withcommit=1) — no new threading; game thread applies + persists. - Ini rows (INPUT/GRAPHICS/VIDEO/GAMEPLAY-ini):
config.chas no central default table (first-write "defaults" = current values, config.c:280), so the section handler uses an explicitstatic const struct { const char *key; double def; }reset table inoptionsoverlay.c, mirroring the port's C initializers (each entry commented with its source variable; verified against the initializer at implementation time — e.g.Game.AllUnlocked= 0,port/src/video.c:116,348; the D257 comment in optionsoverlay.c that says "default ON" is stale and is corrected as part of D356). Front-end: write live +configSave()(existing). In-stage ini resets: applied immediately (O3 — consistent with how every other ini row already behaves when changed in F10; video.c applies most display changes on the fly). - Resolution is excluded from the VIDEO reset (maintainer item 2).
__Resolutionis an action backed bys_resSel+videoRequestWindowSize()— not a registered config row, so the numeric default table cannot reset it, and a "default window size" is a display-capability choice, not a tunable. The D356 findings entry documents the exception explicitly: VIDEO reset covers Fullscreen, VSync, FpsCap, DisplayFPS — Resolution keeps the player's current choice. - Scope-aware by construction: a section's reset touches each row by its own scope (file rows → that file's BLANKSAVEDATA values; ini rows → ini defaults). No cross-scope surprise (Gate E: "do not make a global reset wipe per-file watch settings" — a reset of the selected file never touches other files; §5.8 verifies this).
- Activation contract (§5.4) applies to every section's reset row.
5.4 Reset activation contract (maintainer item 1)
A reset is a destructive bulk write — GAMEPLAY's in particular changes both ini values and the selected save in one action. WATCH-SETTINGS-PLAN §6 gates confirmation + persistence policy; this is that policy:
- Edge-triggered: reset actions fire only on a fresh event. The
frontoptions left/right key repeat (held keys re-issue the action) is
suppressed for
ROW_ACTIONreset rows — the arming/confirm events must carry a no-repeat flag (SDL keydown withrepeat==0/ a fresh click), so a held key or held mouse button can never arm and confirm, nor re-trigger. - Two-step confirm: first activation arms the row (label swaps to "Confirm reset…", armed highlight); a second activation within 3 s commits. Timeout, or navigating to another row, disarms (label restores). Arm state is per-row, per-open; opening/closing F10 or the options screen clears all armed resets.
- One commit per confirmed activation. No auto-repeat, no re-arming on the same event. F10 and the options screen share the identical contract (O2: rows stay visible in F10; confirmation makes accidental in-game resets unlikely).
- The confirmed action then dispatches the section handler (§5.3) exactly once.
5.5 Context-aware file accessor; retire "none" (maintainer item 3)
watchSettingsFolder() returns the raw chosen, which may be -1 before
frontFolder() lazily resolves, and in-stage chosen can differ from the
active file (snapFolder). The generalized title/top-row accessor must be
context-aware, not a repurposed chooser-value read:
- New
watchSettingsActiveFolder(void)(name tentative):- Front-end contexts (options screen, F10-on-file-select, title):
the resolved front target — explicit
chosenif set, elseselected_folder_numif valid, else first valid folder (i.e. thefrontFolder()resolution; the accessor is pure-read and does not mutatechosen— lazy mutation stays infrontFolder()/the auto-init fallback). - In-stage (F10): the active stage target —
selected_folder_num(the file the stage loaded,snapFolderat apply time). - The D353 title annotation and the §5.2 top row both read through this accessor, in both surfaces.
- Front-end contexts (options screen, F10-on-file-select, title):
the resolved front target — explicit
- The chooser's "none" position is retired.
watchSettingsChooseFilecycles FOLDER1..MAX only (dropping the -1 stop). Rationale: D354's default resolution + boot-time BLANKSAVEDATA slots + the auto-init fallback guarantee a valid target, so "none" was only reachable by deliberate cycling and serves no remaining purpose — and "Save file: File N" (the §5.2 invariant) is not true whenchosen == -1.watchSettingsFolder()'s "-1 = none" contract is updated/superseded by the new accessor.
5.6 Reset iterates declared rows (maintainer item 4)
The section handler iterates the section's declared row range in
rows[] (from its header to the next header), not s_visIdx/the
currently visible list — conditionally hidden rows (GRAPHICS draw/LOD
sliders while their auto-FOV toggles are on) must be reset too, or they
silently survive a purported section reset. Hidden-row defaults come from
the same default table; their live values are written directly (the
ptr/rowSet path), visibility is irrelevant. The (save) tag, hidden-row
resets, and the reset table are all keyed off rows[] indices, never the
visible list.
5.7 F10
Unchanged machinery: shared table, save-file row suppressed (the D353 accessor pattern extended to the §5.2 top row). Annotation shows the active file via §5.5. "Reset to defaults" rows stay visible in F10 (O2) under the §5.4 confirmation contract.
5.8 Reset verification probe (maintainer item 5)
New env-gated probe GE_WSPROBE_RESET=<tick> (GE_D314/GE_WSPROBE
pattern): at tick N of a front-end boot it drives the UI action path
through the same handler the confirm step calls (arm → confirm → dispatch,
no bypass). Every section being verified is itself dispatched: the
probe dispatches all five section resets (INPUT, GAMEPLAY, GRAPHICS, AUDIO,
VIDEO) via arm → confirm, and after each dispatch verifies that
section's rows:
- INPUT / GRAPHICS / VIDEO (ini rows): values == the default table.
GRAPHICS specifically covers the hidden draw/LOD sliders (hidden
while their auto-FOV toggles are on) — a section reset must land them
even while they are not visible; VIDEO's
__Resolutionis the documented exclusion (unchanged, logged). - GAMEPLAY (mixed scope): the four save rows == BLANKSAVEDATA (auto-aim/sight/look-ahead/ammo) and the ini rows (SkipIntro/ NoHitFlash/AllUnlocked/HudScale) == the ini default table — the two scopes land independently in one action.
- AUDIO (save rows): Music/FX == BLANKSAVEDATA (0xFF raw = 32767,
the
VOLUME_MAXendpoint), not the probe's pre-dirtied 4096. - Scope isolation (after all dispatches): a second folder's
save_datais byte-identical to its pre-reset snapshot (a reset of the selected file never touches other files).
Front-end prep (tick N) first dirties live values (Music/FX → 4096, VSync/HudScale → non-defaults) and snapshots the second folder, so the verifications at tick N+1 prove the resets actually moved values back.
In-stage dispatch is covered by the same env var on a -level_XX run:
arm → confirm at tick N (file rows enqueue through the D352 queue — the
probe logs the enqueued command count), drain at N+1, then the same
per-section value verification against the post-apply state.
5.9 No changes
buildPagesinfrontoptions.c(generic; 5 pages < MAX_PAGES),watchSettingsGameTick/queue (D352/D354/D355 logic untouched), the D355 track-1 resync,BLANKSAVEDATAitself.
6. Threading / persistence gates (unchanged, restated)
- D350: no GE writes from the F10 input/render thread except the sanctioned front-end commit path; in-stage edits queue to the game thread.
- D352: queue holds across transitions; per-field coalescing.
- D354: front contexts (including F10-on-file-select) route through the
chooser path;
frontFolder()default resolution; auto-init fallback. - D355: Music commit + level-entry resync of the BGM (track 1) player.
- D356 adds no new write path: reset = N existing commits (file rows) + existing ini writes (ini rows).
7. Docs
docs/dev/findings.mdD356 entry + §F index row; regeneratefindings-index.csv.docs/dev/WATCH-SETTINGS-PLAN.md: status + D353/D354 "Bond File" section references updated; Gate E marked landed (subset: per-section rows).docs/dev/OPTIONS-MENU-PLAN.md(tracked) + local notes: §6 "align with Turok" annotated as landed via D356 (M3 master volume + key bindings remain).
8. Verification
- Clean NTSC build (
./build-pc.sh/cmake --build build-pc). - Headless smoke: boots to front end; log page count (5, under MAX_PAGES),
no
buildPagesoverflow warning, save-file row resolves to File 1 by default (D354), annotation on GAMEPLAY/AUDIO pages, no "none" reachable (chooser cycles files only, §5.5). GE_WSPROBE/GE_WSPROBE_FRONTregression: identical pass as D355 (queue/commit paths untouched).GE_WSPROBE_RESET(§5.8) exercises the UI action, not just direct commits: per-section arm → confirm → dispatch of all five sections, with each section's rows verified right after its own dispatch (ini == table, save == BLANKSAVEDATA, hidden GRAPHICS rows included, VIDEO resolution untouched), then the second-folder byte-identity check. In-stage dispatch:GE_WSPROBE-style queue check on a-level_XXrun (enqueued command count + post-apply state).- Confirmation contract check (§5.4): a held key/click arms but never confirms (edge-triggered); second activation within 3 s commits exactly once; timeout/navigate disarms. Headlessly assertable via the probe (arm-then-timeout leaves values untouched).
- Manual visual pass (owed, same list as D353): arrows through 5 pages,
save-file cycling updates the annotation live,
(save)tag on the four GAMEPLAY save rows, F10 in-stage, reset in stage vs front (including the armed/confirm visual state), rapid slider drag.
9. Open questions — RESOLVED (maintainer sign-off, 2026-09-27)
- O1: yes — subtle dim "(save)" tag on the four mixed-scope GAMEPLAY rows; AUDIO's title annotation alone is sufficient (homogeneous section).
- O2: visible in F10, provided resets require the §5.4 confirmation step (which they do).
- O3: immediate in-stage ini resets, consistent with existing ini edits in F10.
- O4:
__QuitToDesktopstays in GAMEPLAY, literally last — after the Reset row (section order: rows…, Reset to defaults, Quit to desktop; the §4 page table is corrected accordingly).
Layout itself is signed off by the maintainer (2026-09-27): five-page regroup + menu-only exposure filter, with the §5.4–§5.8 contracts as specified above.
10. Estimate
One commit. ~5 files (optionsoverlay.c, frontoptions.c,
watchsettings.c, port/include/optionsoverlay.h,
port/include/watchsettings.h), net +400/−140 (table reshuffle + reset
contract + context-aware accessor + GE_WSPROBE_RESET are the new code).
Findings/csv/plan updates ride along.