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.
This commit is contained in:
James Dansereau committed 2026-09-27 14:49:08 -04:00
1 parent 95f8f5f8b5
commit fe57d73924
10 files changed
+952 -178

No files matched your search

+42 -24
View File
@@ -1,6 +1,8 @@
# D356 — Settings regroup: functional sections, save-file scoping, per-section reset
Status: **PROPOSAL — awaiting sign-off** (2026-09-27). Supersedes the "BOND FILE"
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
@@ -138,6 +140,12 @@ expected not to, see below).
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)
@@ -253,26 +261,35 @@ the §5.4 confirmation contract.
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**
— dispatches the GAMEPLAY and VIDEO section-reset handlers through the same
handler the confirm step calls (arm → confirm → dispatch, no bypass), then
logs/asserts on the following tick:
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:
1. **Selected-file scope:** the chosen file's `save_data` fields ==
BLANKSAVEDATA (auto-aim/sight/look-ahead/ammo enabled, Music/FX 0xFF —
per WATCH-SETTINGS-PLAN §6).
2. **Scope isolation:** a *second* folder's `save_data` is byte-identical
to its pre-reset state (a GAMEPLAY reset never touches other files).
3. **Hidden rows:** GRAPHICS draw/LOD slider values (hidden while the
auto-FOV toggles are on) == their table defaults after a GRAPHICS reset,
even while hidden.
4. **Mixed scopes:** after a GAMEPLAY reset, the ini rows
(SkipIntro/NoHitFlash/AllUnlocked/HudScale) == the ini default table
while the save rows == BLANKSAVEDATA — the two scopes land
independently.
- **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 `__Resolution` is 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_MAX` endpoint), not the probe's pre-dirtied 4096.
- **Scope isolation (after all dispatches):** a *second* folder's
`save_data` is byte-identical to its pre-reset snapshot (a reset of the
selected file never touches other files).
In-stage dispatch is covered by a `GE_WSPROBE`-style queue check on a
`-level_XX` run (reset commits ride the D352 queue; the probe asserts the
enqueued field count and the post-apply save state).
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
@@ -311,11 +328,12 @@ enqueued field count and the post-apply save state).
3. `GE_WSPROBE` / `GE_WSPROBE_FRONT` regression: identical pass as D355
(queue/commit paths untouched).
4. **`GE_WSPROBE_RESET` (§5.8) exercises the UI action, not just direct
commits:** confirmed-reset dispatch → (a) selected file == BLANKSAVEDATA,
(b) second file byte-identical (scope isolation), (c) hidden GRAPHICS
draw/LOD values == table defaults, (d) mixed-scope GAMEPLAY lands both
scopes independently. In-stage dispatch: `GE_WSPROBE`-style queue check
on a `-level_XX` run.
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_XX` run (enqueued command count + post-apply state).
5. **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
+5
View File
@@ -1,6 +1,11 @@
# 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.
+2 -1
View File
@@ -1,6 +1,6 @@
# Watch-backed settings in PC Options and F10 — proposal for review
Status: **watch-backed subset implemented (D350), robustness fixes landed (D352), Turok-style regrouping done (D353), front-end F10 routing + transparent default Bond-file selection landed (D354: non-stage contexts use the chooser path, default = explicit pick > `selected_folder_num` > first valid file, with a one-shot `fileBuildWriteNewSave(FOLDER1)` fallback; GE_WSPROBE/GE_WSPROBE_FRONT headless probes), and the in-stage BGM resync landed (D355: the Music commit also drives the track-1 BGM player, matching the game's own MP-path resync; by-ear confirmed).** Wider M3 remains outstanding: master volume, window-mode/display rows, per-section reset (Gate E), key rebinding. PC-only per-file persistence and explicit Bond-file chooser approved; details below. See `docs/dev/findings.md` D350–D355 for verification. This is the watch-integration part of M3, not a replacement for the native watch menu. The older tracked `docs/dev/OPTIONS-MENU-PLAN.md` and local `docs/dev/notes/OPTIONS-MENU-PLAN.md` describe the wider M3 scope (master volume, window mode/display, resets).
Status: **watch-backed subset implemented (D350), robustness fixes landed (D352), Turok-style regrouping done (D353), front-end F10 routing + transparent default Bond-file selection landed (D354: non-stage contexts use the chooser path, default = explicit pick > `selected_folder_num` > first valid file, with a one-shot `fileBuildWriteNewSave(FOLDER1)` fallback; GE_WSPROBE/GE_WSPROBE_FRONT headless probes), the in-stage BGM resync landed (D355: the Music commit also drives the track-1 BGM player, matching the game's own MP-path resync; by-ear confirmed), and the settings regroup landed (D356: functional sections INPUT/GAMEPLAY/GRAPHICS/AUDIO/VIDEO, PD-style "(File N)" scoping annotations, N64-watch rows off the menu surface, per-section reset-to-defaults = Gate E; GE_WSPROBE_RESET headless probes front + in-stage).** Wider M3 remains outstanding: master volume, key rebinding. PC-only per-file persistence and explicit Bond-file chooser approved; details below. See `docs/dev/findings.md` D350–D356 for verification. This is the watch-integration part of M3, not a replacement for the native watch menu. The older tracked `docs/dev/OPTIONS-MENU-PLAN.md` and local `docs/dev/notes/OPTIONS-MENU-PLAN.md` describe the wider M3 scope (master volume, window mode/display, resets).
## Goal and ownership
@@ -59,4 +59,5 @@ Thus a front-end row that edits only live watch state will be silently undone be
- **Landed 2026-09-27 (D352/D353, port-only):** bridge robustness fixes (D352) and the Turok-style row-table regrouping (D353: six sections — DISPLAY, GRAPHICS, BOND FILE, MOUSE / AIM, CONTROLLER, GAME; the music/FX sliders sit in BOND FILE; the page title annotates the selected file; `buildPages` overflow logs).
- **Landed 2026-09-27 (D354, port-only):** front-end F10 routing + transparent default Bond-file selection (see item 4 above); in-stage F10 verified working by the `GE_WSPROBE` probe.
- **Landed 2026-09-27 (D355, port-only):** the Music commit now also resyncs the in-stage BGM (track 1) via `musicTrack1ApplySeqpVol`, plus a once-per-activation resync on level entry; by-ear confirmed. N64 watch path unchanged.
- **Landed 2026-09-27 (D356, port-only):** the settings regroup (plan `docs/dev/D356-SETTINGS-REGROUP-PLAN.md`): Turok-style pages renamed to functional sections (INPUT/GAMEPLAY/GRAPHICS/AUDIO/VIDEO, all below MAX_PROWS), the redundant N64-watch rows (`Bond.Look`, `Bond.AimControl`) commented out of the menu surface (keys + watch paths stay live, D181/D216 style), the `__HdrBond`/`__BondFile` rows retired for the front-only top save-file row, per-file sections annotated `"(File N)"` via the `saveScoped` flag, and the Turok reset rows replaced by a real per-section Reset-to-defaults (edge-triggered two-step arm->confirm within 3 s; watch rows -> BLANKSAVEDATA through the D352 commit path, ini rows -> the port's C initializers; `__Resolution` a documented exclusion). GE_WSPROBE_RESET front + in-stage probes headless-verified.
- A standalone AUDIO section (master volume + the config-side volume keys) and per-section reset-to-defaults (Gate E) are the next settings-scope items; key rebinding stays Phase 4.
+1
View File
@@ -233,3 +233,4 @@ D352,FIXED,D350 review of 1303c0eb:,"FIXED (port-only, port/src/watchsettings.c)
D353,FIXED,Turok-style settings regrouping:,"FIXED (port-only, optionsoverlay.c/frontoptions.c):"
D354,FIXED,Front-end F10 Bond rows silently disabled:,"FIXED (port-only, port/src/watchsettings.c):"
D355,FIXED,F10/options Music slider inaudible in-stage:,"FIXED (port-only, port/src/watchsettings.c):"
D356,FIXED,"Settings regroup: Turok-style pages renamed to functional sections (INPUT/GAMEPLAY/GRAPHICS/AUDIO/VIDEO) with PD-style ""(File N)"" scoping annotations, the redu…","FIXED (port-only, port/src/optionsoverlay.{h,c} + port/src/watchsettings.{h,c} + port/src/frontoptions.c; plan docs/dev/D356-SETTINGS-REGROUP-PLAN.md):"
1 # GENERATED by tools_pc/gen_findings_index.py -- do not hand-edit.
233 D353
234 D354
235 D355
236 D356
+88
View File
@@ -641,6 +641,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
| D353 | **Turok-style settings regrouping: the D346 VIDEO mega-section (14–15 rows, at the MAX_PROWS limit) re-split into DISPLAY + GRAPHICS, the "AUDIO (BOND FILE)" hack header dropped (music/FX sliders fold into BOND FILE), all remaining rows converted to designated initializers, `buildPages` overflow now logs, and the Bond-file page title annotates the selected file (2026-09-27).** — full `## D353` entry at file tail | FIXED (port-only, `optionsoverlay.c`/`frontoptions.c`): 6 sections of 3–11 rows, all rows designated-initialized, `optionsRowIsBondChooser` accessor, build clean + headless smoke clean. Master-volume row, per-section reset (Gate E) and key rebinding remain in the M3 backlog. |
| D354 | **Front-end F10 Bond rows silently disabled: `watchSettingsAvailable()` mis-routed F10-on-file-select through the stage path (returned 0), so with the hidden chooser stuck at "none" the volume sliders read "Select file" and did nothing; maintainer-reported (2026-09-27).** — full `## D354` entry at file tail | FIXED (port-only, `port/src/watchsettings.c`): all non-stage contexts route through the front path with a lazily-resolved default folder (explicit pick > `selected_folder_num` > first valid), one-shot game-thread `fileBuildWriteNewSave(FOLDER1)` fallback, GE_WSPROBE/GE_WSPROBE_FRONT env probes headless-verified (stage: live+save changed; front: default file 1 + save changed). |
| D355 | **F10/options Music slider inaudible in-stage: the slider drives only the X-track (track 2) via `set_mTrack2Vol`, but in-stage BGM is track 1, which the game pins to `VOLUME_MAX` at level start (lv.c:364) and never rescales from `mTrack2Vol` in solo -- so neither the N64 watch nor the F10 could change the music the player was hearing; maintainer-reported (2026-09-27).** — full `## D355` entry at file tail | FIXED (port-only, `port/src/watchsettings.c`): the Music commit now also calls `musicTrack1ApplySeqpVol(v)` (the exact resync the MP path already does, mpmenu.c:354) and a once-per-activation resync runs on stage entry; by-ear confirmed by the maintainer. N64 watch path untouched. |
| D356 | **Settings regroup: Turok-style pages renamed to functional sections (INPUT/GAMEPLAY/GRAPHICS/AUDIO/VIDEO) with PD-style "(File N)" scoping annotations, the redundant N64-watch rows (`Bond.Look`, `Bond.AimControl`) off the menu surface, and a real per-section Reset-to-defaults (two-step arm->confirm, watch rows to BLANKSAVEDATA via the D352 commit path, ini rows to the port's C initializers; `__Resolution` a documented exclusion) replacing the Turok reset rows the PC build never honored (2026-09-27).** — full `## D356` entry at file tail | FIXED (port-only, `port/src/optionsoverlay.{h,c}` + `port/src/watchsettings.{h,c}` + `port/src/frontoptions.c`; plan `docs/dev/D356-SETTINGS-REGROUP-PLAN.md`): 5 sections all below MAX_PROWS, hidden rows D181/D216-style (keys + watch paths stay live), edge-triggered reset with 3 s arm window + maintain/clear disarm, GE_WSPROBE_RESET front + in-stage probes headless-verified (all sections failures=0, scope isolation OK, saved bytes 0xFF), GE_WSPROBE/GE_WSPROBE_FRONT regressions clean. |
Phase 2 replaced the Phase-1 demo loop with the real `mainproc()` on real OS
threads, compiled GE's real `src/sched.c`, and brought in PD's fast3d software
@@ -13851,3 +13852,90 @@ Maintainer report (after D354): F10 sound-FX changes apply in real time, but the
- GE_WSPROBE logs extended with the track-1 volume (`t1=`) so the resync is visible headless.
Verification: NTSC build clean; **by-ear confirmed by the maintainer** — F10 Music now changes in-stage music in real time. The front-end commit path (input thread) now also calls `musicTrack1ApplySeqpVol`, which wraps `alCSPSetVol` on the track-1 player — same thread pattern as the pre-existing track-2 call, so the D350 threading gate is unchanged in effect.
## D356 — Settings regroup: functional sections, per-section reset-to-defaults, N64-watch rows off the menu (2026-09-27)
**Problem.** The D353 regroup left the F10 overlay with a Turok-style layout
and Turok-style per-page "Reset to defaults" rows (options.c:121/140/159/173/190,
all N64-watch defaults, none of which the PC build honors -- a D330-era known
gap). The N64-watch `Bond.Look` (stacks double-negation with the PC invert
rows) and `Bond.AimControl` (collides with `Input.AimMode`) rows were
menu-surface hazards. The plan
(docs/dev/D356-SETTINGS-REGROUP-PLAN.md, signed off 2026-09-27): rename the
page model to functional sections, scope the per-file rows under a file
annotation, drop the N64-watch rows from the menu (keep the code paths), and
replace the Turok reset row with a real per-section reset-to-defaults.
**Fix (port-only; `optionsoverlay.{h,c}`, `watchsettings.{h,c}`,
`frontoptions.c`, + plan doc; zero game-code changes).**
1. **Sections (plan §4).** `INPUT` (7 rows: the old MOUSE + PAD headers
merged, `__HdrMouse`/`__HdrPad` dropped) / `GAMEPLAY` (File N; 8 rows:
surviving watch toggles `Bond.AutoAim`/`LookAhead`/`Sight`/`Ammo` + the
`Game.*` ini rows + Quit) / `GRAPHICS` (11 rows, D353's DISPLAY+GRAPHICS
merged) / `AUDIO` (File N; `Bond.Music` + `Bond.FX` watch sliders, out of
the Bond-file page) / `VIDEO` (5 rows: the D353 DISPLAY rename,
`Video.DisplayFPS` moved from GAME). All pages below the MAX_PROWS cap.
`Bond.Look`/`Bond.AimControl` are commented out of `rows[]` with a D356
note (D181/D216 style: keys + watch paths stay live; ini power users can
still set them). The `__HdrBond` header + `__BondFile` chooser row are
retired, replaced by the front-only top `Save file: File N [change]` row
(frontoptions.c; hidden in F10 via the generalized
`optionsRowIsBondChooser` mechanism).
2. **Save-file scoping.** Sections that carry per-file rows (GAMEPLAY,
AUDIO) get a PD-style header annotation `"(File N)"` / `"(no file)"`
driven by `Row.saveScoped` + `watchSettingsActiveFolder()` (in-stage: the
active file; front: the resolved front target); per-file rows inside the
mixed-scope GAMEPLAY section carry a dim `"(save)"` tag (the section
title already says which file). The D353 `optionsRowIsBondChooser`
special-case is generalized to the `saveScoped` flag (both UIs: F10
overlay + front options screen).
3. **Per-section reset-to-defaults (Gate E).** The Turok rows are deleted and
replaced by a trailing `Reset to defaults` ROW_ACTION on every section
(`__ResetInput`/`__ResetGameplay`/`__ResetGraphics`/`__ResetAudio`/
`__ResetVideo`). Activation is edge-triggered, two-step arm -> confirm
within 3 s (`RESET_ARM_US`), one commit per confirmed activation, disarm
on timeout or navigating away (`optionsResetMaintain`), disarm on
close (`optionsResetClear`); the arm state is shared by the F10 overlay
and the front screen (never open at once; both clear it). The front
screen suppresses held-key repeat for reset rows; F10's held-repeat
branches skip them -- callers guarantee edges, so hold never repeats
into confirm. Confirm clears the pending arm first, then
`rowResetSection(i)` walks the section's DECLARED row range (header to
next header, so conditionally hidden rows are reset too):
watch rows -> `watchSettingsSet(field, watchSettingsBlankValue(field),
1)` (BLANKSAVEDATA, through the D352 commit path: front direct write,
stage queue drained + persisted by the game thread); ini rows -> the
`kResetDefaults[]` table (the port's own C initializers, each entry
citing its source variable) applied via `rowSetCommit` and persisted like
any ini edit. `__Resolution` is a documented exclusion (action-backed,
not a registered config row -- a numeric default cannot express it; the
player's resolution choice survives a VIDEO reset). No new write path.
**Verification (headless, this session).**
- `GE_WSPROBE_RESET=1` front boot: all 5 sections dirty -> arm -> confirm ->
verify, `failures=0` each; `__Resolution` untouched (exclusion holds);
`file 2 unchanged (scope isolation)` -- the second-file negative test.
- `GE_WSPROBE_RESET=1 -level_33`: `stage dispatch: 6 command(s) queued`
(4 GAMEPLAY + 2 AUDIO watch rows), all 5 sections `failures=0` after the
game-thread drain, `saved bytes OK (music/sfx = 0xFF)` -- the persisted
save holds BLANKSAVEDATA raw.
- Probe phase machine (`watchSettingsGameTick`, same env gate): front tick
120 = prepare (front-only, direct commits), stage tick 120 = dispatch
(queue), stage tick 121 = verify (post-drain). A direct `-level_XX` boot
enters the stage long before the front counter would reach 120, so a
stage run starts its timer from phase 0 (the stage dispatch builds its
section list lazily).
- Regressions: `GE_WSPROBE=120 -level_33` live music/fx/t1 32767->4096 +
save 255->32 (D355 resync still fires); `GE_WSPROBE_FRONT=120` default
folder 0 commit 4096 -> save.music=32. All runs end with the orderly
`video: exiting (render parked ...)` quit.
- Build: full reconfigure + 248/248 targets + link clean
(`build-pc-cmd.bat`; the pi agent shell drops MSYS env at the
msys-2.0.dll boundary, so the build goes through a `cmd.exe` wrapper with
the MSYS2 toolchain on PATH).
**Residuals.** By-ear confirmation of the in-stage Music slider is owed
(D355 territory, unchanged by this entry). The hidden N64-watch rows
(`Bond.Look`, `Bond.AimControl`) are still reachable by hand-editing the
save file, exactly as on N64 -- only the menu surface is gone
(D181/D216/D304 pattern).
+40 -1
View File
@@ -17,10 +17,17 @@
* input.c inputComputePad : controller 0 swallowed while open;
* nav routed to optionsOverlayHandleInput()
* gfx_pc.cpp gfx_run : optionsOverlayEmit() appended after the game DL
*
* D346 (M2): the row API below is the shared row table the front MENU_PC_
* OPTIONS screen (frontoptions.c) renders from, so one option set in one
* place.
*/
#include <PR/ultratypes.h>
#include <PR/gbi.h>
#include <config.h>
#include <video.h>
#include <watchsettings.h>
#ifdef __cplusplus
extern "C" {
@@ -51,14 +58,46 @@ int optionsRowIsHeader(int i);
int optionsRowIsShown(int i);
const char *optionsRowLabel(int i);
int optionsRowIsSlider(int i);
int optionsRowIsBondChooser(int i); /* D353: the Bond-file chooser row */
int optionsRowIsBondChooser(int i); /* D353: the Bond-file chooser row
(front options screen only) */
/* D356: 1 when this row's value lives in the selected save file (content
* rows); on a header, when its section contains such rows -- the front page
* uses the header form for its "(File N)" title annotation. */
int optionsRowIsSaveScoped(int i);
int optionsRowNeedsRestart(int i);
double optionsRowFraction(int i);
double optionsRowGetValue(int i); /* D356: raw value (fraction is bar geometry) */
void optionsRowSetFraction(int i, double f);
void optionsRowCommit(int i); /* slider release; watch rows persist once */
void optionsRowValueText(int i, char *out, int n);
void optionsRowAdjust(int i, int dir);
/* D356: per-section "Reset to defaults" rows (ROW_ACTION, keys
* "__Reset*"). Activation is edge-triggered and two-step (arm -> confirm
* within 3 s, one commit per confirmed activation; timeout or navigating
* away disarms) -- optionsRowIsReset lets the front screen suppress its
* held-key repeat for them. optionsResetMaintain runs each frame from the
* open UI (selRow = its selected rows[] index, -1 = none); optionsResetClear
* when a UI closes. Identical contract in F10 and on the front options
* screen (they are never open at the same time). Front: the confirmed reset
* writes the section's file rows through watchSettingsSet (direct commit)
* and the ini rows live (persisted on screen close, like any ini edit).
* In stage: the file rows enter the D352 queue (the game thread applies +
* persists them) and the ini rows are applied live immediately. */
int optionsRowIsReset(int i);
void optionsRowActivateReset(int i);
void optionsResetMaintain(int selRow);
void optionsResetClear(void);
/* D356 GE_WSPROBE_RESET dev hook (env-gated, driven from
* watchSettingsGameTick): runs the real arm -> confirm -> dispatch for
* EVERY section's reset row and verifies each section's values after its
* own dispatch (plan §5.8). Front is one-shot; the stage pair runs on the
* tick after the stage drain. */
void optionsResetProbePrepare(void);
void optionsResetProbeDispatchStage(void);
void optionsResetProbeVerifyStage(void);
#ifdef __cplusplus
}
#endif
+21
View File
@@ -17,6 +17,12 @@ enum WatchSettingField {
int watchSettingsFieldForKey(const char *key); /* -1 for non-watch row */
int watchSettingsFolder(void); /* explicit front chooser; -1 = none */
/* D356: the chooser cycles folders only -- "none" is retired from the UI
* surface (plan §5.5): the front target is always a folder (frontFolder()
* resolves -1 to a valid folder lazily, D354; the auto-init fallback builds
* file 1 if none exists). The UI shows an unavailable state until the
* target's file actually exists instead of a number the write path cannot
* use. */
void watchSettingsChooseFile(int dir); /* game thread only */
int watchSettingsAvailable(void);
int watchSettingsRead(enum WatchSettingField field); /* front: saved file, F10: snapshot */
@@ -24,4 +30,19 @@ void watchSettingsSet(enum WatchSettingField field, int value, int commit);
void watchSettingsCommit(enum WatchSettingField field); /* slider release */
void watchSettingsGameTick(void); /* game thread; after gfxFrameMsgQ receive */
/* D356: context-aware, READ-ONLY active-file accessor (never mutates the
* stored front target): in stage -> the stage's active file; front
* contexts -> the resolved front target (frontFolder()'s lazy default).
* -1 when nothing usable (e.g. the auto-init fallback not yet applied).
* Drives the "(File N)" title annotations in the F10 overlay and the front
* options screen (plan §5.5). */
int watchSettingsActiveFolder(void);
/* D356: the BLANKSAVEDATA value of a field -- the per-file "default" the
* reset-to-defaults rows restore (plan §5.3/§5.6). */
int watchSettingsBlankValue(enum WatchSettingField field);
/* D352: queued field commands awaiting the game thread (0 in front
* contexts -- nothing is queued there). Drives the in-stage reset probe's
* dispatch check. */
int watchSettingsQueueCount(void);
#endif
+76 -24
View File
@@ -41,6 +41,7 @@
#include "config.h"
#include "envflag.h"
#include "optionsoverlay.h"
#include "watchsettings.h"
#include "frontoptions.h"
#include "input.h"
@@ -153,6 +154,9 @@ static void titleCase(const char *in, char *out, int n)
static int rowY(int k)
{
/* D356: level 1's item 0 is the save-file row; the page's content rows
* sit one line lower. */
if (s_level == 1) k++;
return ROW_Y0 + k * ROW_DY;
}
@@ -171,9 +175,12 @@ static void buildPages(void)
s_rowN = 0;
if (s_pageN > 0) {
int overflow = 0;
/* D356: the level-1 page also carries the top save-file row, so the
* content rows get one less line. */
int cap = MAX_PROWS - 1;
for (int i = s_pageHdr[s_page] + 1; i < n && !optionsRowIsHeader(i); i++) {
if (!optionsRowIsShown(i)) continue;
if (s_rowN < MAX_PROWS) {
if (s_rowN < cap) {
s_rowIdx[s_rowN++] = i;
} else {
overflow++;
@@ -183,15 +190,17 @@ static void buildPages(void)
* outgrew the page (the D346b VIDEO section did, at exactly 15) lost
* rows with no trace. */
if (overflow)
sysLogPrintf(LOG_WARNING, "frontoptions: section '%s' has %d row(s) past MAX_PROWS=%d; last visible: '%s'",
optionsRowLabel(s_pageHdr[s_page]), overflow, MAX_PROWS,
optionsRowLabel(s_rowIdx[MAX_PROWS - 1]));
sysLogPrintf(LOG_WARNING, "frontoptions: section '%s' has %d row(s) past the page cap (%d); last visible: '%s'",
optionsRowLabel(s_pageHdr[s_page]), overflow, cap,
optionsRowLabel(s_rowIdx[cap - 1]));
}
}
static int itemCount(void)
{
return s_level == 0 ? s_pageN : s_rowN;
/* D356: level 1 carries the top save-file row (item 0, not in rows[]) on
* top of the page's content rows. */
return s_level == 0 ? s_pageN : s_rowN + 1;
}
/* Put the crosshair on item k, unconditionally. */
@@ -262,6 +271,7 @@ static void goBack(void)
playSfx(DOOR_METAL_CLOSE2_SFX);
if (s_dragRow >= 0) optionsRowCommit(s_dragRow);
s_dragRow = -1;
optionsResetClear(); /* D356: navigating away (or closing) disarms a pending reset */
if (s_level == 1) {
s_level = 0;
cursorToItem(s_page);
@@ -315,6 +325,11 @@ void frontOptionsMenuInterface(void)
buildPages();
cursorToItem(0);
s_hl = -1;
} else if (s_hl == 0 && s_level == 1) {
/* D356: the top save-file row -- A steps to the next file (the
* D352 chooser; folders only, "none" retired, plan §5.5). */
playSfx(DOOR_LOCK_SFX);
watchSettingsChooseFile(1);
} else if (s_hl >= 0 && s_hl < s_rowN) {
int i = s_rowIdx[s_hl];
playSfx(DOOR_LOCK_SFX);
@@ -322,6 +337,11 @@ void frontOptionsMenuInterface(void)
cursor_h_pos <= BAR_X1 + 4) {
optionsRowSetFraction(i, ((double)cursor_h_pos - BAR_X0) / (BAR_X1 - BAR_X0));
s_dragRow = i;
} else if (optionsRowIsReset(i)) {
/* D356: reset rows are two-step arm -> confirm (edge
* activation: joyGetButtonsPressedThisFrame fires once per
* press). The value column shows "Confirm" while armed. */
optionsRowActivateReset(i);
} else {
optionsRowAdjust(i, +1); /* toggle / cycle / step (wraps) */
}
@@ -341,8 +361,10 @@ void frontOptionsMenuInterface(void)
}
/* Left/right (D-pad, C buttons; keyboard A/D) adjust the highlighted row,
* with hold-to-repeat. */
if (s_level == 1 && s_hl >= 0 && s_hl < s_rowN) {
* with hold-to-repeat. D356: on the save-file row it cycles the file;
* on a reset row a fresh press activates (arm/confirm) and the held
* repeat is suppressed (the activation contract, plan §5.4). */
if (s_level == 1 && s_hl >= 0) {
int dir = 0;
/* D345(f): stick X joins the D-pad/C-buttons, so Left/Right arrows
* (and the pad's left stick) adjust values like F10's left/right. */
@@ -359,7 +381,15 @@ void frontOptionsMenuInterface(void)
s_repeatTimer = 4;
}
if (fire) {
optionsRowAdjust(s_rowIdx[s_hl], dir);
if (s_hl == 0) {
watchSettingsChooseFile(dir); /* save-file row */
} else {
int i = s_rowIdx[s_hl];
if (optionsRowIsReset(i))
optionsRowActivateReset(i);
else
optionsRowAdjust(i, dir);
}
}
}
s_repeatDir = dir;
@@ -367,6 +397,10 @@ void frontOptionsMenuInterface(void)
s_repeatDir = 0;
}
/* D356: reset arm state -- the screen's selected rows[] index (s_hl 0 is
* the save row, not a rows[] entry); moving on disarms, goBack clears. */
optionsResetMaintain(s_level == 1 && s_hl > 0 ? s_rowIdx[s_hl] : -1);
/* The dossier: tabs + a blank CLASSIFIED page, as the cheat screen. */
disable_all_switches(walletinst[0]);
set_item_visibility_in_objinstance(walletinst[0], SW_TABS, 1);
@@ -450,24 +484,36 @@ Gfx *frontOptionsMenuDraw(Gfx *DL)
strcat(title, "\n");
DL = ink(DL, ROW_X, TITLE_Y, title, INK);
/* D353: scope annotation -- the Bond-file section's title carries
* the chooser's value ("(File 2)" / "(no file)"), so the player sees
* which save the per-file toggles and sliders touch without scrolling
* to the "Edit file" row. */
/* D356: scope annotation -- a section that carries per-file rows has
* its title carry the active file ("GAMEPLAY (File 2)"), PD-style.
* watchSettingsActiveFolder() is the context-aware accessor (front:
* the resolved front target; -1 while no file is usable, which the
* annotation renders as an unavailable state, never a number the
* write path cannot use). */
if (optionsRowIsSaveScoped(s_pageHdr[s_page])) {
int f = watchSettingsActiveFolder();
char note[16];
if (f < 0) snprintf(note, sizeof(note), "(no file)");
else snprintf(note, sizeof(note), "(File %d)", f + 1);
DL = ink(DL, ROW_X + titleW + 8, TITLE_Y, note, INK_DIM);
}
/* D356: the top save-file row (item 0, not in the row table): the
* single file control of the front screen (F10 always targets the
* active file, so it has no file row). L/R (and A) cycle folders; ""
* (no file) is the unavailable state until one is created. */
{
int bondRow = -1;
for (int k = 0; k < s_rowN; k++) {
if (optionsRowIsBondChooser(s_rowIdx[k])) { bondRow = k; break; }
}
if (bondRow >= 0) {
char vt[32] = "", note[40];
optionsRowValueText(s_rowIdx[bondRow], vt, sizeof(vt));
if (vt[0] && strcmp(vt, "Select file") != 0)
snprintf(note, sizeof(note), "(%s)", vt);
else
snprintf(note, sizeof(note), "(no file)");
DL = ink(DL, ROW_X + titleW + 8, TITLE_Y, note, INK_DIM);
int y = rowY(0);
char sv[32];
if (s_hl == 0) {
DL = microcode_constructor_related_to_menus(DL, ROW_X - 2, y - 1,
ROW_X + 140, y + 0xE, HILITE);
}
DL = ink(DL, ROW_X, y, "Save file\n", INK);
int f = watchSettingsActiveFolder();
if (f < 0) snprintf(sv, sizeof(sv), "(no file)");
else snprintf(sv, sizeof(sv), "File %d\n", f + 1);
DL = inkR(DL, VAL_R, y, sv, INK);
}
for (int k = 0; k < s_rowN; k++) {
@@ -481,6 +527,12 @@ Gfx *frontOptionsMenuDraw(Gfx *DL)
}
DL = ink(DL, ROW_X, y, label, INK);
/* D356: per-file rows in the mixed-scope section carry a dim
* "(save)" tag (the section title already says which file). */
if (optionsRowIsSaveScoped(i)) {
DL = ink(DL, ROW_X + measureW(label) + 4, y, "(save)\n", INK_DIM);
}
if (optionsRowIsSlider(i)) {
s32 fx = BAR_X0 + (s32)((BAR_X1 - BAR_X0) * optionsRowFraction(i) + 0.5);
DL = microcode_constructor_related_to_menus(DL, BAR_X0, y + 5, BAR_X1, y + 8, 0x00000040);
+574 -118
View File
@@ -47,10 +47,15 @@ extern MENU current_menu;
#include "config.h"
#include "video.h"
#include "input.h"
#include "front.h" /* selected_folder_num (D356 stage probe) */
#include "file.h" /* save_data (D356 reset probe: second-file isolation) */
#include "optionsoverlay.h"
#include "watchsettings.h"
#include "../fast3d/gfx_api.h"
/* file2.c; same extern as watchsettings.c (not in a header). */
extern save_data *fileGetSaveForFoldernum(u32 folder);
/* D324 class: -Iinclude resolves <math.h> to GE's N64 stub, which does not
* declare lround; without this the call is an implicit `int lround()`
* (GCC only rescued it via its builtin signature). */
@@ -139,86 +144,29 @@ struct Row {
* (e.g. deadzone raw 0..30000 -> % of full stick). NULL/0 = plain int. */
const char *unit;
int dispDiv;
/* D356: 1 = this row's value lives in the selected save file (per-file
* watch rows). Carries the dim "(save)" tag on the only section that
* mixes scopes (GAMEPLAY); on headers the flag is recomputed at init to
* mean "this section contains per-file rows" (drives the "(File N)"
* title annotation in both UIs). */
int saveScoped;
};
static struct Row rows[] = {
/* D353: Turok-style regrouping. The D346b single 15-row VIDEO section
* sat exactly at the front page's MAX_PROWS cap (a new row would have
* been silently truncated). Split into DISPLAY (presentation) and
* GRAPHICS (quality + distance); sections now top out at 11 rows
* (GRAPHICS with both auto-FOV toggles off). All rows use designated
* initializers (D351 class: positional shifts into hidePtr/unit). */
{ .key="__HdrDisplay", .label="DISPLAY", .kind=ROW_HEADER },
{ .key="Video.Fullscreen", .label="Fullscreen", .kind=ROW_TOGGLE, .step=1, .names=kOnOff },
{ .key="__Resolution", .label="Resolution", .kind=ROW_RES },
{ .key="Video.VSync", .label="VSync", .kind=ROW_TOGGLE, .step=1, .names=kOnOff },
{ .key="Video.FpsCap", .label="Frame rate cap", .kind=ROW_FPSCAP },
{ .key="__HdrGraphics", .label="GRAPHICS", .kind=ROW_HEADER },
{ .key="Video.MSAA", .label="Anti-aliasing", .kind=ROW_MSAA, .restart=1 },
{ .key="Video.TextureFilter", .label="Texture filter", .kind=ROW_ENUM, .step=1, .names=kTexFilter },
{ .key="Video.Anisotropy", .label="Anisotropic filtering", .kind=ROW_SLIDER, .step=1, .unit="x" },
{ .key="Video.FovScale", .label="FOV scale", .kind=ROW_SLIDER, .step=5, .unit="%" },
/* D334: native widescreen (world projected at the window aspect, Hor+).
* While on, "Widescreen auto FOV" has no effect (it was the stretch-era
* vertical-FOV compensation). */
{ .key="Video.NativeWidescreen", .label="Native widescreen", .kind=ROW_TOGGLE, .step=1, .names=kOnOff },
{ .key="Video.WidescreenAuto", .label="Widescreen auto FOV", .kind=ROW_TOGGLE, .step=1, .names=kOnOff },
{ .key="Video.SafeAreaCrop", .label="Crop overscan", .kind=ROW_TOGGLE, .step=1, .names=kOnOff },
{ .key="Video.DrawDistance", .label="Draw distance", .kind=ROW_SLIDER, .step=25, .hiddenIfOn="Video.DrawDistanceAutoFov", .unit="%" },
{ .key="Video.DrawDistanceAutoFov", .label="Draw dist. follows FOV", .kind=ROW_TOGGLE, .step=1, .names=kOnOff },
{ .key="Video.LodDistance", .label="LOD distance", .kind=ROW_SLIDER, .step=25, .hiddenIfOn="Video.LodDistanceAutoFov", .unit="%" },
{ .key="Video.LodDistanceAutoFov", .label="LOD dist. follows FOV", .kind=ROW_TOGGLE, .step=1, .names=kOnOff },
/* Aim row edits Input.AimModeSens -- the knob the default GEPD aim path
* actually uses (Input.MouseAimSpeed only feeds the legacy velocity-stick
* fallback, so it was inert here). D304: both this row and the turn-speed
* row below now use the (0,0) "inherit the registered clamp" sentinel
* instead of hardcoded uiMin/uiMax -- they were previously (1,80) and
* (0,100), two arbitrary, DIFFERENT, and (for turn speed) outright wrong
* ceilings that didn't even match either row's own backing config clamp
* (both are actually registered 1..500 in input.c) -- fixed as an earlier
* part of this same D304 pass. **Follow-up, same day, user feedback after
* trying the widened sliders live:** exposing BOTH per-mode knobs still
* let a player decouple them into a bad, hard-to-diagnose state (e.g. aim
* mode very slow, hipfire very fast, or vice versa) with no indication
* anything was wrong -- worse than a narrow range. Per the user's request,
* these two rows and the "Link aim/turn sens" toggle that tried to paper
* over the same risk are pulled from the menu entirely; only the single
* master `Input.MouseSensitivity` row below remains player-facing, exactly
* mirroring the reference GEPD/PD injector model this input was ported
* from (GEPD-Edition Mouse Injector's goldeneye.c -- source no longer
* vendored in the public repo, see reference/mouse-injector/README.md) --
* ONE user-facing
* sensitivity value, with the two per-mode constants staying fixed at
* their calibrated (38/50) defaults, never independently player-tunable.
* Config vars + SensLink logic stay in `input.c`/`rowSet()` untouched
* (ini power users can still hand-edit them; existing ini files with
* either key keep working) -- only the menu surface changes, same
* established pattern as the D181/D216 pulled rows below. */
/* { "Input.AimModeSens", "Mouse aim speed", ROW_SLIDER, 5, NULL, 0, 0, 0, 0,0,0,0,0 }, */
/* { "Input.MouseTurnSpeed", "Mouse turn speed", ROW_SLIDER, 5, NULL, 0, 0, 0, 0,0,0,0,0 }, */
/* { "Input.SensLink", "Link aim/turn sens",ROW_TOGGLE,1, kOnOff, 0, 0, 0, 0,0,0,0,0 }, */
{ .key="__HdrBond", .label="BOND FILE", .kind=ROW_HEADER, .found=1 },
{ .key="__BondFile", .label="Edit file", .kind=ROW_BOND_FILE, .found=1 },
{ .key="Bond.Look", .label="Look up/down (watch; stacks)", .kind=ROW_TOGGLE,
.names=kReverse, .found=1, .uiMax=1, .cfgMax=1 },
{ .key="Bond.AutoAim", .label="Auto-aim", .kind=ROW_TOGGLE,
.names=kOnOff, .found=1, .uiMax=1, .cfgMax=1 },
{ .key="Bond.AimControl", .label="Aim control", .kind=ROW_TOGGLE,
.names=kHold, .found=1, .uiMax=1, .cfgMax=1 },
{ .key="Bond.Sight", .label="Sight on screen", .kind=ROW_TOGGLE,
.names=kOnOff, .found=1, .uiMax=1, .cfgMax=1 },
{ .key="Bond.LookAhead", .label="Look ahead", .kind=ROW_TOGGLE,
.names=kOnOff, .found=1, .uiMax=1, .cfgMax=1 },
{ .key="Bond.Ammo", .label="Ammo on screen", .kind=ROW_TOGGLE,
.names=kOnOff, .found=1, .uiMax=1, .cfgMax=1 },
/* D353: the separate "AUDIO (BOND FILE)" header is retired -- the two
* sliders belong to the same per-file section as the watch toggles. A
* port-level master volume (M3) will start a real AUDIO section. */
{ .key="Bond.Music", .label="Music volume", .kind=ROW_SLIDER, .step=128,
.uiMax=32767, .cfgMax=32767, .found=1, .unit="%", .dispDiv=328 },
{ .key="Bond.FX", .label="FX volume", .kind=ROW_SLIDER, .step=128,
.uiMax=32767, .cfgMax=32767, .found=1, .unit="%", .dispDiv=328 },
{ .key="__HdrMouse", .label="MOUSE / AIM", .kind=ROW_HEADER },
/* D356: Turok-style functional sections in Turok's order (INPUT,
* GAMEPLAY, GRAPHICS, AUDIO, VIDEO; docs/dev/D356-SETTINGS-REGROUP-PLAN.md).
* The D353 "BOND FILE" section -- the one named after WHERE a value is
* saved -- is gone: its surviving rows split into GAMEPLAY (the per-file
* watch toggles, dim "(save)" tagged since the section mixes scopes) and
* AUDIO (the per-file volume sliders, homogeneous so title annotation
* only), and the "Edit file" chooser row is retired in favour of the
* single top save-file row on the front options screen (frontoptions.c).
* Per-section "Reset to defaults" rows (D356, plan Gate E) share
* rowResetSection(); "(File N)" title annotations come from
* watchSettingsActiveFolder(). All rows use designated initializers
* (D351 class). */
{ .key="__HdrInput", .label="INPUT", .kind=ROW_HEADER },
{ .key="Input.MouseSensitivity", .label="Mouse sensitivity", .kind=ROW_SLIDER, .step=5 },
{ .key="Input.MouseInvertY", .label="Invert look (mouse)", .kind=ROW_TOGGLE, .step=1, .names=kOnOff },
/* Input.PdMouseAim (findings D332): the Perfect Dark port's mouse-aim
@@ -237,10 +185,81 @@ static struct Row rows[] = {
* (65% of the half-width, camera turn from ~49%). Hidden while the aim
* style is CENTRED (PC), where the crosshair doesn't travel. */
{ .key="Input.AimRange", .label="Aim range", .kind=ROW_ENUM, .step=1, .names=kAimRange, .hiddenIfOn="Input.AimMode" },
{ .key="__HdrPad", .label="CONTROLLER", .kind=ROW_HEADER },
{ .key="Input.PadLookInvertY", .label="Invert look (controller)", .kind=ROW_TOGGLE, .step=1, .names=kOnOff },
{ .key="Input.PadDeadzone", .label="Stick deadzone", .kind=ROW_SLIDER, .step=500, .unit="%", .dispDiv=300 },
{ .key="Input.PadTriggerPct", .label="Trigger threshold", .kind=ROW_SLIDER, .step=1, .unit="%" },
{ .key="__ResetInput", .label="Reset to defaults", .kind=ROW_ACTION },
{ .key="__HdrGameplay", .label="GAMEPLAY", .kind=ROW_HEADER },
/* D356 exposure filter (menu surface only, the D181/D216/D304 pattern --
* config keys, the N64 watch paths and ini hand-edits all stay live):
* Bond.Look duplicates Input.MouseInvertY + Input.PadLookInvertY
* (stacking both is a double-negation trap);
* Bond.AimControl collides with Input.AimMode (two rows fighting over
* the same N64-vs-PC aim model).
* The four surviving per-file toggles move here from the D353 BOND FILE
* section and carry .saveScoped (dim "(save)" tag, the only mixed-scope
* section). */
/* { .key="Bond.Look", .label="Look up/down (watch; stacks)", .kind=ROW_TOGGLE,
.names=kReverse, .found=1, .uiMax=1, .cfgMax=1 }, */
{ .key="Bond.AutoAim", .label="Auto-aim", .kind=ROW_TOGGLE,
.names=kOnOff, .found=1, .uiMax=1, .cfgMax=1, .saveScoped=1 },
/* { .key="Bond.AimControl", .label="Aim control", .kind=ROW_TOGGLE,
.names=kHold, .found=1, .uiMax=1, .cfgMax=1 }, */
{ .key="Bond.Sight", .label="Sight on screen", .kind=ROW_TOGGLE,
.names=kOnOff, .found=1, .uiMax=1, .cfgMax=1, .saveScoped=1 },
{ .key="Bond.LookAhead", .label="Look ahead", .kind=ROW_TOGGLE,
.names=kOnOff, .found=1, .uiMax=1, .cfgMax=1, .saveScoped=1 },
{ .key="Bond.Ammo", .label="Ammo on screen", .kind=ROW_TOGGLE,
.names=kOnOff, .found=1, .uiMax=1, .cfgMax=1, .saveScoped=1 },
/* D216/Game.SkipIntro: user report (v0.2.1 testing) that it breaks audio
* -- pulled from the menu until root-caused. Not exposed to players; the
* config var + lv.c hook stay in place (dead unless an existing ini has
* it set, which no menu path can do any more). Do not re-add without
* fixing the underlying issue first. */
{ .key="Game.SkipIntro", .label="Skip intro", .kind=ROW_TOGGLE, .step=1, .names=kOnOff },
/* D232: the community "no damage flash" toggle (suppresses the red/green
* hit-flash overlay in bondview2). */
{ .key="Game.NoHitFlash", .label="No hit flash", .kind=ROW_TOGGLE, .step=1, .names=kOnOff },
/* D257: everything-unlocked goodie. The C initializers (port/src/video.c)
* start it OFF (0) -- the old "default ON" note was stale and is
* corrected by D356. Consumed at startup by main.c -- applies from the
* next launch. */
{ .key="Game.AllUnlocked", .label="All unlocked", .kind=ROW_TOGGLE, .step=1, .names=kOnOff },
/* D226: scales the ammo counter, pickup / status messages and dialogue
* about their screen anchors. 100% = original, nothing emitted. */
{ .key="Game.HudScale", .label="HUD scale", .kind=ROW_SLIDER, .step=5, .unit="%" },
{ .key="__ResetGameplay", .label="Reset to defaults", .kind=ROW_ACTION },
/* D293: only quit path used to be the OS window-close / Alt+F4 -- no
* discoverable in-game way to exit, a real gap on Deck/controller-only
* setups. Not config-backed (like __Resolution); activating it exits
* the same way video.c's SDL_QUIT/Alt+F4 handlers already do. */
{ .key="__QuitToDesktop", .label="Quit to desktop", .kind=ROW_ACTION },
{ .key="__HdrGraphics", .label="GRAPHICS", .kind=ROW_HEADER },
{ .key="Video.MSAA", .label="Anti-aliasing", .kind=ROW_MSAA, .restart=1 },
{ .key="Video.TextureFilter", .label="Texture filter", .kind=ROW_ENUM, .step=1, .names=kTexFilter },
{ .key="Video.Anisotropy", .label="Anisotropic filtering", .kind=ROW_SLIDER, .step=1, .unit="x" },
{ .key="Video.FovScale", .label="FOV scale", .kind=ROW_SLIDER, .step=5, .unit="%" },
/* D334: native widescreen (world projected at the window aspect, Hor+).
* While on, "Widescreen auto FOV" has no effect (it was the stretch-era
* vertical-FOV compensation). */
{ .key="Video.NativeWidescreen", .label="Native widescreen", .kind=ROW_TOGGLE, .step=1, .names=kOnOff },
{ .key="Video.WidescreenAuto", .label="Widescreen auto FOV", .kind=ROW_TOGGLE, .step=1, .names=kOnOff },
{ .key="Video.SafeAreaCrop", .label="Crop overscan", .kind=ROW_TOGGLE, .step=1, .names=kOnOff },
{ .key="Video.DrawDistance", .label="Draw distance", .kind=ROW_SLIDER, .step=25, .hiddenIfOn="Video.DrawDistanceAutoFov", .unit="%" },
{ .key="Video.DrawDistanceAutoFov", .label="Draw dist. follows FOV", .kind=ROW_TOGGLE, .step=1, .names=kOnOff },
{ .key="Video.LodDistance", .label="LOD distance", .kind=ROW_SLIDER, .step=25, .hiddenIfOn="Video.LodDistanceAutoFov", .unit="%" },
{ .key="Video.LodDistanceAutoFov", .label="LOD dist. follows FOV", .kind=ROW_TOGGLE, .step=1, .names=kOnOff },
/* D304: the per-mode aim/turn-sensitivity sliders and the "Link" toggle
* that papered over their decoupling risk were pulled from the menu
* (user feedback after the D304 widening); the ONE master
* Input.MouseSensitivity row above is the player-facing knob, mirroring
* the GEPD/PD injector model. Config vars + SensLink logic stay live in
* input.c/rowSetCommit() for ini hand-edits. */
/* { "Input.AimModeSens", "Mouse aim speed", ROW_SLIDER, 5, NULL, 0, 0, 0, 0,0,0,0,0 }, */
/* { "Input.MouseTurnSpeed", "Mouse turn speed", ROW_SLIDER, 5, NULL, 0, 0, 0, 0,0,0,0,0 }, */
/* { "Input.SensLink", "Link aim/turn sens",ROW_TOGGLE,1, kOnOff, 0, 0, 0, 0,0,0,0,0 }, */
/* D181/Game.ScreenShakeIntensity: user testing (v0.2.1) found the slider
* "basically useless" -- viShake() is only called from explosion.c, so it
* scales explosion shake alone; it never touches the always-on walking
@@ -248,28 +267,33 @@ static struct Row rows[] = {
* reads as to a player. Pulled from the menu until it covers all
* screen-shake/view-bob sources, not just explosions. Config var + fr.c
* hook stay in place. */
/* D232: the community "no damage flash" toggle (suppresses the red/green
* hit-flash overlay in bondview2). */
{ .key="__HdrGame", .label="GAME", .kind=ROW_HEADER },
/* D226: scales the ammo counter, pickup / status messages and dialogue
* about their screen anchors. 100% = original, nothing emitted. */
{ .key="Game.HudScale", .label="HUD scale", .kind=ROW_SLIDER, .step=5, .unit="%" },
{ .key="Game.SkipIntro", .label="Skip intro", .kind=ROW_TOGGLE, .step=1, .names=kOnOff },
{ .key="__ResetGraphics", .label="Reset to defaults", .kind=ROW_ACTION },
{ .key="__HdrAudio", .label="AUDIO", .kind=ROW_HEADER },
/* D356: the per-file volume sliders get a real AUDIO section (the D353
* "AUDIO (BOND FILE)" header was retired with the BOND FILE section).
* A port-level master volume (M3) will join here as Turok's
* Master/Sound/Music trio. */
{ .key="Bond.Music", .label="Music volume", .kind=ROW_SLIDER, .step=128,
.uiMax=32767, .cfgMax=32767, .found=1, .unit="%", .dispDiv=328 },
{ .key="Bond.FX", .label="FX volume", .kind=ROW_SLIDER, .step=128,
.uiMax=32767, .cfgMax=32767, .found=1, .unit="%", .dispDiv=328 },
{ .key="__ResetAudio", .label="Reset to defaults", .kind=ROW_ACTION },
/* D353's DISPLAY section renamed VIDEO (the presentation knobs), with
* Video.DisplayFPS moved here from the old GAME section. */
{ .key="__HdrVideo", .label="VIDEO", .kind=ROW_HEADER },
{ .key="Video.Fullscreen", .label="Fullscreen", .kind=ROW_TOGGLE, .step=1, .names=kOnOff },
{ .key="__Resolution", .label="Resolution", .kind=ROW_RES },
{ .key="Video.VSync", .label="VSync", .kind=ROW_TOGGLE, .step=1, .names=kOnOff },
{ .key="Video.FpsCap", .label="Frame rate cap", .kind=ROW_FPSCAP },
{ .key="Video.DisplayFPS", .label="Show FPS", .kind=ROW_TOGGLE, .step=1, .names=kOnOff },
{ .key="Game.NoHitFlash", .label="No hit flash", .kind=ROW_TOGGLE, .step=1, .names=kOnOff },
/* D216/Game.SkipIntro: user report (v0.2.1 testing) that it breaks audio
* -- pulled from the menu until root-caused. Not exposed to players; the
* config var + lv.c hook stay in place (dead unless an existing ini has
* it set, which no menu path can do any more). Do not re-add without
* fixing the underlying issue first. */
/* D257: everything-unlocked goodie (default ON). Consumed at startup by
* main.c -- applies from the next launch. */
{ .key="Game.AllUnlocked", .label="All unlocked", .kind=ROW_TOGGLE, .step=1, .names=kOnOff },
/* D293: only quit path used to be the OS window-close / Alt+F4 -- no
* discoverable in-game way to exit, a real gap on Deck/controller-only
* setups. Not config-backed (like __Resolution); activating it exits
* the same way video.c's SDL_QUIT/Alt+F4 handlers already do. */
{ .key="__QuitToDesktop", .label="Quit to desktop", .kind=ROW_ACTION },
{ .key="__ResetVideo", .label="Reset to defaults", .kind=ROW_ACTION },
/* D356: the D353 BOND FILE header + "Edit file" chooser row are retired --
* the front options screen's top save-file row (frontoptions.c, D356) is
* the single file control; F10 always targets the active file. */
/* { .key="__HdrBond", .label="BOND FILE", .kind=ROW_HEADER, .found=1 }, */
/* { .key="__BondFile", .label="Edit file", .kind=ROW_BOND_FILE, .found=1 }, */
};
#define NUM_ROWS ((int)(sizeof(rows) / sizeof(rows[0])))
@@ -545,6 +569,18 @@ static void overlayInit(void)
}
}
}
/* D356: on a header row, saveScoped means "this section contains
* per-file rows" (drives the "(File N)" title annotation in both UIs);
* on a content row it is the literal per-file flag from the table. */
for (int i = 0; i < NUM_ROWS; i++) {
if (rows[i].kind != ROW_HEADER) {
continue;
}
int has = 0;
for (int j = i + 1; j < NUM_ROWS && rows[j].kind != ROW_HEADER; j++)
has |= rows[j].saveScoped;
rows[i].saveScoped = has;
}
overlayUpdateVisible();
/* Build the windowed-resolution preset list: presets that fit the desktop,
@@ -670,6 +706,11 @@ static void rowSetCommit(struct Row *r, double v, int commit)
static void rowSet(struct Row *r, double v) { rowSetCommit(r, v, 1); }
/* D356 reset rows (defined below, in the reset block). */
static struct Row *rowAt(int i);
static int isResetRow(const struct Row *r);
static void rowActivateReset(struct Row *r);
static void rowAdjust(struct Row *r, int dir)
{
if (!r->found || r->kind == ROW_HEADER) { /* D237: headers have no value */
@@ -725,6 +766,11 @@ static void rowAdjust(struct Row *r, int dir)
break;
}
case ROW_ACTION:
/* D356: reset rows arm/confirm through the shared activation
* contract (edge-triggered by their callers: fresh key press / fresh
* click / A-press -- the held-repeat paths below never re-fire them).
* The quit row is the only non-reset ROW_ACTION left. */
if (isResetRow(r)) { rowActivateReset(r); break; }
/* D293: same exit path as SDL_QUIT / Alt+F4 (video.c), just reachable
* without OS window chrome or a keyboard. */
sysLogPrintf(LOG_INFO, "optionsoverlay: quit to desktop requested");
@@ -737,6 +783,163 @@ static void rowAdjust(struct Row *r, int dir)
}
}
/* ------------------------------------------------------------------------ */
/* D356: per-section "Reset to defaults" (plan Gate E, made concrete). */
/* The activation contract (plan §5.4): edge-triggered, two-step */
/* arm -> confirm within 3 s, one commit per confirmed activation, */
/* disarm on timeout or navigating away. The arm state is shared by the */
/* F10 overlay and the front options screen (they are never open at the */
/* same time; both clear it on open/close). */
/* ------------------------------------------------------------------------ */
#define RESET_ARM_US 3000000 /* arm expires after 3 s */
static int s_resetArmedRow = -1; /* rows[] index of the armed reset row */
static uint64_t s_resetArmedUs = 0;
static int isResetRow(const struct Row *r)
{
return r->kind == ROW_ACTION && strncmp(r->key, "__Reset", 7) == 0;
}
int optionsRowIsReset(int i)
{
struct Row *r = rowAt(i);
return r && isResetRow(r);
}
/* ini rows reset to the port's C initializers (verified at implementation
* time, D356 -- each entry cites its source variable): the reset table
* mirrors them; config.c itself has no central default table (first-write
* "defaults" are just the current values). __Resolution is NOT here: it is
* action-backed (s_resSel + videoRequestWindowSize), not a registered config
* row, so a numeric default cannot express it -- a documented exclusion (the
* findings D356 entry states it); the player's resolution choice survives a
* VIDEO reset. */
static const struct { const char *key; double def; } kResetDefaults[] = {
/* INPUT (port/src/input.c initializers) */
{ "Input.MouseSensitivity", 100 }, /* static int mouseSensitivity = 100 */
{ "Input.MouseInvertY", 0 }, /* = 0 */
{ "Input.AimMode", 0 }, /* = AIMMODE_N64 (0) */
{ "Input.AimRange", 0 }, /* = 0 (PC) */
{ "Input.PadLookInvertY", 0 }, /* = 0 */
{ "Input.PadDeadzone", 7000 },/* = STICK_DEADZONE (7000) */
{ "Input.PadTriggerPct", 23 }, /* = 23 */
/* GRAPHICS (port/src/video.c initializers) */
{ "Video.MSAA", 4 }, /* = 4 */
{ "Video.TextureFilter", 1 }, /* = 1 (bilinear) */
{ "Video.Anisotropy", 4 }, /* = 4 */
{ "Video.FovScale", 100 }, /* = 100 */
{ "Video.NativeWidescreen", 1 }, /* = 1 */
{ "Video.WidescreenAuto", 1 }, /* = 1 */
{ "Video.SafeAreaCrop", 1 }, /* = 1 */
{ "Video.DrawDistance", 150 }, /* = 150 (D218) */
{ "Video.DrawDistanceAutoFov", 1 }, /* = 1 (D218) */
{ "Video.LodDistance", 150 }, /* = 150 (D249) */
{ "Video.LodDistanceAutoFov", 0 }, /* = 0 (standalone perf lever) */
/* GAMEPLAY ini rows (port/src/video.c initializers) */
{ "Game.SkipIntro", 0 }, /* = 0 */
{ "Game.NoHitFlash", 0 }, /* = 0 */
{ "Game.AllUnlocked", 0 }, /* = 0 -- D257's "default ON" note was stale */
{ "Game.HudScale", 100 }, /* = 100 (D226) */
/* VIDEO (port/src/video.c initializers; DisplayFPS in overlayConfigInit) */
{ "Video.Fullscreen", 0 }, /* = 0 (windowed) */
{ "Video.VSync", 1 }, /* = 1 (on) */
{ "Video.FpsCap", 60 }, /* = 60 */
{ "Video.DisplayFPS", 0 }, /* registered 0 */
};
static double kResetDefault(const char *key)
{
for (size_t i = 0; i < sizeof(kResetDefaults) / sizeof(kResetDefaults[0]); i++)
if (strcmp(kResetDefaults[i].key, key) == 0) return kResetDefaults[i].def;
return -1.0;
}
/* The section's DECLARED row range (its header through the next header) --
* NOT the visible list, so conditionally hidden rows (the manual draw/LOD %
* sliders while their auto-FOV toggles are on) are reset too; their live
* values are written through the same ptr/rowSet path, visibility
* irrelevant. Per row, its OWN scope is reset: watch rows -> the selected
* file's BLANKSAVEDATA values through the normal commit path (front: direct
* write; stage: D352 queue, applied+persisted by the game thread); ini rows
* -> the table above (applied live now, persisted like any ini edit when the
* screen/overlay closes). No cross-scope surprise: a section reset touches
* only that section's rows, and a file reset never touches other files. */
static void rowResetSection(int iReset)
{
int hdr = iReset - 1;
while (hdr >= 0 && rows[hdr].kind != ROW_HEADER) hdr--;
if (hdr < 0) {
sysLogPrintf(LOG_WARNING, "optionsoverlay: reset row %d has no section header", iReset);
return;
}
int end = hdr + 1;
while (end < NUM_ROWS && rows[end].kind != ROW_HEADER) end++;
int nWatch = 0, nIni = 0;
for (int j = hdr + 1; j < end; j++) {
struct Row *r = &rows[j];
if (r->kind == ROW_ACTION) continue; /* reset rows, Quit */
if (strcmp(r->key, "__Resolution") == 0) continue; /* documented exclusion */
int field = watchSettingsFieldForKey(r->key);
if (field >= 0) {
watchSettingsSet((enum WatchSettingField)field,
watchSettingsBlankValue((enum WatchSettingField)field), 1);
nWatch++;
} else if (r->found && r->ptr) {
double def = kResetDefault(r->key);
if (def < 0.0) continue;
rowSetCommit(r, def, 1);
nIni++;
}
}
sysLogPrintf(LOG_INFO, "optionsoverlay: section reset '%s': %d file row(s), %d ini row(s)",
rows[hdr].label, nWatch, nIni);
}
/* Arm (first edge) or confirm (second edge within RESET_ARM_US). Callers
* guarantee edges: the front screen routes only fresh presses (its held-key
* repeat is suppressed for reset rows) and F10's fresh-press / fresh-click
* paths; the held-repeat branches skip reset rows entirely. */
static void rowActivateReset(struct Row *r)
{
if (!isResetRow(r)) return;
uint64_t now = sysGetMicroseconds();
int i = (int)(r - rows);
if (s_resetArmedRow == i && now - s_resetArmedUs <= RESET_ARM_US) {
s_resetArmedRow = -1;
rowResetSection(i); /* exactly one commit per confirmed activation */
return;
}
s_resetArmedRow = i;
s_resetArmedUs = now;
int h = i - 1;
while (h >= 0 && rows[h].kind != ROW_HEADER) h--;
sysLogPrintf(LOG_INFO, "optionsoverlay: section reset '%s' armed (confirm within 3 s)",
h >= 0 ? rows[h].label : "?");
}
void optionsRowActivateReset(int i)
{
struct Row *r = rowAt(i);
if (r) rowActivateReset(r);
}
/* Timeout / navigate-away disarm. selRow = the rows[] index the UI has
* selected (any other value, e.g. -1, disarms). */
void optionsResetMaintain(int selRow)
{
if (s_resetArmedRow < 0) return;
uint64_t now = sysGetMicroseconds();
if (s_resetArmedRow != selRow || now - s_resetArmedUs > RESET_ARM_US)
s_resetArmedRow = -1;
}
void optionsResetClear(void)
{
s_resetArmedRow = -1;
}
/* ------------------------------------------------------------------------ */
void optionsOverlayToggle(void)
@@ -749,6 +952,7 @@ void optionsOverlayToggle(void)
s_open = !s_open;
sysLogPrintf(LOG_INFO, "optionsoverlay: %s", s_open ? "opened" : "closed");
if (!s_open) {
optionsResetClear(); /* D356: closing the overlay disarms a pending reset */
configSave();
}
}
@@ -845,6 +1049,10 @@ void optionsOverlayHandleInput(void)
* visible (a stage poll would otherwise leave it locked/hidden). */
inputSuspendForOverlay();
/* D356: a pending reset arm expires after 3 s or when the selection
* leaves the armed row; the overlay's selection is s_visIdx[s_sel]. */
optionsResetMaintain(s_visIdx[s_sel]);
overlayUpdateVisible(); /* % rows may have appeared/vanished (auto toggles) */
overlayApplyWheel(); /* D314: wheel notches queued by the host thread */
overlayUpdateScroll();
@@ -919,7 +1127,11 @@ void optionsOverlayHandleInput(void)
} else if ((rt || lf) && adjDir != 0) {
if (--adjTimer <= 0) {
adjTimer = 4;
rowAdjust(&rows[s_visIdx[s_sel]], adjDir);
/* D356: reset actions are edge-triggered -- the held-repeat
* re-fires adjust rows but must never re-fire a reset (a held
* key could arm AND confirm, or commit repeatedly). */
if (!isResetRow(&rows[s_visIdx[s_sel]]))
rowAdjust(&rows[s_visIdx[s_sel]], adjDir);
}
} else {
adjDir = 0;
@@ -1013,8 +1225,9 @@ static Gfx *fillRect(Gfx *gdl, s32 x0, s32 y0, s32 x1, s32 y1,
return gdl;
}
static void valueText(const struct Row *r, char *out, int n)
static void valueText(int i, char *out, int n)
{
const struct Row *r = &rows[i];
double v = rowGet(r);
if (r->kind == ROW_BOND_FILE) {
int f = watchSettingsFolder();
@@ -1032,11 +1245,26 @@ static void valueText(const struct Row *r, char *out, int n)
} else if (s_resFitN <= 0) {
snprintf(out, n, "n/a");
} else {
int i = s_resFit[s_resSel];
snprintf(out, n, "%d x %d", kResList[i][0], kResList[i][1]);
int i2 = s_resFit[s_resSel];
snprintf(out, n, "%d x %d", kResList[i2][0], kResList[i2][1]);
}
return;
}
if (r->kind == ROW_ACTION) {
if (isResetRow(r)) {
/* D356: the label is the button; the value column shows the
* armed state so the confirm step is discoverable. */
snprintf(out, n, s_resetArmedRow == i ? "Confirm" : "");
} else {
snprintf(out, n, "[ENTER]");
}
return;
}
if (r->kind == ROW_MSAA) {
if ((int)lround(v) <= 1) snprintf(out, n, "None");
else snprintf(out, n, "%dx", (int)lround(v));
return;
}
if ((r->kind == ROW_TOGGLE || r->kind == ROW_ENUM) && r->names) {
int idx = (int)lround(v);
int cnt = 0;
@@ -1046,15 +1274,6 @@ static void valueText(const struct Row *r, char *out, int n)
return;
}
}
if (r->kind == ROW_MSAA) {
if ((int)lround(v) <= 1) snprintf(out, n, "None");
else snprintf(out, n, "%dx", (int)lround(v));
return;
}
if (r->kind == ROW_ACTION) {
snprintf(out, n, "[ENTER]");
return;
}
if (r->kind == ROW_SLIDER && r->type == CONFIG_OPT_FLOAT) {
snprintf(out, n, "%.2f", v);
return;
@@ -1203,16 +1422,32 @@ Gfx *optionsOverlayEmit(void)
if (r->kind == ROW_HEADER) { /* D237 category label, no value */
gdl = drawText(gdl, OV_X0, rowY, (char *)r->label, 0xffe040ff);
/* D356: sections that contain per-file rows carry the active
* file in the header ("GAMEPLAY (File 1)"), PD-style -- in-stage
* the active file, front end the resolved front target. */
if (r->saveScoped) {
int f = watchSettingsActiveFolder();
char note[16];
if (f < 0) snprintf(note, sizeof(note), "(no file)");
else snprintf(note, sizeof(note), "(File %d)", f + 1);
gdl = drawText(gdl, OV_X0 + measureText(r->label) + 6, rowY, note, 0x909090ff);
}
continue;
}
gdl = drawText(gdl, OV_LABEL_X, rowY, (char *)r->label,
r->found ? col : 0x808080ff);
/* D356: per-file rows in the mixed-scope section carry a dim
* "(save)" tag (the section title already says which file). */
if (r->saveScoped && r->kind != ROW_HEADER) {
gdl = drawText(gdl, OV_LABEL_X + measureText(r->label) + 5, rowY,
"(save)", 0x909090ff);
}
if (!r->found) {
gdl = drawTextR(gdl, right, rowY, "(n/a)", 0x808080ff);
continue;
}
valueText(r, val, sizeof(val));
valueText(s_visIdx[p], val, sizeof(val));
if (r->restart) {
/* value left of the bar span, "(restart)" pinned to the edge */
gdl = drawText(gdl, bx0, rowY, val, col);
@@ -1283,6 +1518,16 @@ int optionsRowIsBondChooser(int i)
return r && r->kind == ROW_BOND_FILE;
}
/* D356: content rows carry the literal per-file flag from the table; header
* rows were recomputed at init to "this section contains per-file rows".
* Both drive UI decoration (the "(save)" tag, the "(File N)" annotation).
* The F10 overlay and the front options screen are the only consumers. */
int optionsRowIsSaveScoped(int i)
{
struct Row *r = rowAt(i);
return r && r->saveScoped;
}
int optionsRowNeedsRestart(int i)
{
struct Row *r = rowAt(i);
@@ -1299,6 +1544,14 @@ double optionsRowFraction(int i)
return f < 0.0 ? 0.0 : (f > 1.0 ? 1.0 : f);
}
/* D356: raw value (the fraction accessors are bar geometry only); the
* reset probe verifies against raw defaults. */
double optionsRowGetValue(int i)
{
struct Row *r = rowAt(i);
return r ? rowGet(r) : 0.0;
}
void optionsRowSetFraction(int i, double f)
{
struct Row *r = rowAt(i);
@@ -1327,11 +1580,7 @@ void optionsRowValueText(int i, char *out, int n)
if (!r || n <= 0) {
return;
}
if (r->kind == ROW_ACTION) {
out[0] = '\0'; /* the label is the button */
return;
}
valueText(r, out, n);
valueText(i, out, n);
}
void optionsRowAdjust(int i, int dir)
@@ -1341,3 +1590,210 @@ void optionsRowAdjust(int i, int dir)
rowAdjust(r, dir);
}
}
/* ------------------------------------------------------------------------ */
/* D356 GE_WSPROBE_RESET dev hook (env-gated; driven from the game-thread */
/* hook in watchSettingsGameTick, so the dispatch runs on the right */
/* thread in both contexts). Drives the REAL arm -> confirm -> dispatch */
/* UI path for EVERY section's reset row and verifies each section's */
/* values after ITS dispatch (plan §5.8): watch rows vs BLANKSAVEDATA, */
/* ini rows vs the defaults table, __Resolution untouched, and (front) a */
/* second file's bytes unchanged (scope isolation). */
/* ------------------------------------------------------------------------ */
#define PROBE_MAX_SECTIONS 8
static int probeSectionIdxs[PROBE_MAX_SECTIONS];
static int probeSectionN = 0;
static int probeResSelBefore = -2;
static int probeOtherFolder = -1;
static save_data probeOtherBytes;
/* The header + end of the section a reset row belongs to. Written in
* plain local-variable form on purpose: the pointer-increment style
* (`while (*hdr >= 0 && ...) *hdr--;`) miscompiled under -O2 (the loop
* pointer walked the stack and the faulted on garbage rows[] indices). */
static void probeSectionRange(int iReset, int *hdr, int *end)
{
int h, e;
if (iReset <= 0 || iReset >= NUM_ROWS) {
sysLogPrintf(LOG_ERROR, "wsresetprobe: bad reset row index %d (NUM_ROWS %d)", iReset, NUM_ROWS);
*hdr = -1; *end = -1;
return;
}
h = iReset - 1;
while (h >= 0 && rows[h].kind != ROW_HEADER) h--;
e = h + 1;
while (e < NUM_ROWS && rows[e].kind != ROW_HEADER) e++;
*hdr = h;
*end = e;
}
/* Verify every row of section iReset is at its default. Returns the
* failure count. Watch rows compare against BLANKSAVEDATA via
* watchSettingsRead (front: the saved file, stage: the post-drain
* snapshot); ini rows against the defaults table. */
static int probeVerifySection(int iReset)
{
int hdr, end;
probeSectionRange(iReset, &hdr, &end);
if (hdr < 0) {
sysLogPrintf(LOG_WARNING, "wsresetprobe: section %d: no header", iReset);
return 1;
}
int bad = 0;
for (int j = hdr + 1; j < end; j++) {
struct Row *r = &rows[j];
if (r->kind == ROW_ACTION) continue;
if (strcmp(r->key, "__Resolution") == 0) continue; /* documented exclusion */
int field = watchSettingsFieldForKey(r->key);
if (field >= 0) {
int want = watchSettingsBlankValue((enum WatchSettingField)field);
int got = watchSettingsRead((enum WatchSettingField)field);
if (got != want) {
sysLogPrintf(LOG_WARNING, "wsresetprobe: section '%s': %s = %d, want %d (BLANKSAVEDATA)",
rows[hdr].label, r->key, got, want);
bad++;
}
} else if (r->found && r->ptr) {
double want = kResetDefault(r->key);
if (want < 0.0) continue; /* no default (the handler skips it too) */
double got = optionsRowGetValue(j);
if (got < want - 0.001 || got > want + 0.001) {
sysLogPrintf(LOG_WARNING, "wsresetprobe: section '%s': %s = %g, want %g (default table)",
rows[hdr].label, r->key, got, want);
bad++;
}
}
}
return bad;
}
/* Front context (tick 1): dirty one value per section, then arm + confirm
* every section's reset row in order, verifying each section right after
* its own dispatch (the front commit is direct). Finally: resolution
* untouched + a second file's bytes unchanged (scope isolation). */
void optionsResetProbePrepare(void)
{
overlayInit(); /* idempotent: resolves the ini rows (found + ptr) so
* the front probe dispatches every declared row, not
* just the watch rows */
probeSectionN = 0;
for (int i = 0; i < NUM_ROWS; i++)
if (isResetRow(&rows[i]) && probeSectionN < PROBE_MAX_SECTIONS)
probeSectionIdxs[probeSectionN++] = i;
probeResSelBefore = s_resSel;
int active = watchSettingsActiveFolder();
sysLogPrintf(LOG_INFO, "wsresetprobe: front prepare start (active folder %d, %d sections)", active, probeSectionN);
probeOtherFolder = -1;
for (int f = FOLDER1; f < MAX_FOLDER_COUNT; f++) {
if (f == active) continue;
save_data *s = fileGetSaveForFoldernum((u32)f);
if (s) {
probeOtherFolder = f;
probeOtherBytes = *s;
break;
}
}
sysLogPrintf(LOG_INFO, "wsresetprobe: other-folder snapshot done (folder %d)", probeOtherFolder);
/* 1. dirty a distinct value in each section (its first adjustable row). */
for (int s = 0; s < probeSectionN; s++) {
int iR = probeSectionIdxs[s];
int hdr, end;
probeSectionRange(iR, &hdr, &end);
if (hdr < 0) continue;
int did = 0;
for (int j = hdr + 1; j < end && !did; j++) {
struct Row *r = &rows[j];
if (r->kind == ROW_ACTION || strcmp(r->key, "__Resolution") == 0) continue;
int field = watchSettingsFieldForKey(r->key);
if (field >= 0) {
int blank = watchSettingsBlankValue((enum WatchSettingField)field);
int want = (blank > 1) ? 0 : 1; /* distinct from the blank */
if (watchSettingsRead((enum WatchSettingField)field) == want) continue;
watchSettingsSet((enum WatchSettingField)field, want, 1);
did = 1;
} else if (r->found && r->ptr && (r->kind == ROW_TOGGLE || r->kind == ROW_ENUM ||
r->kind == ROW_SLIDER || r->kind == ROW_MSAA)) {
optionsRowAdjust(j, 1); /* one step off the default */
did = 1;
}
}
sysLogPrintf(LOG_INFO, "wsresetprobe: section '%s' dirty=%d", rows[hdr].label, did);
}
/* 2. arm + confirm every section, verifying each after its dispatch. */
for (int s = 0; s < probeSectionN; s++) {
int iReset = probeSectionIdxs[s];
int hdr, end;
probeSectionRange(iReset, &hdr, &end);
if (hdr < 0) continue;
optionsRowActivateReset(iReset); /* arm */
sysLogPrintf(LOG_INFO, "wsresetprobe: section %d armed", s);
optionsRowActivateReset(iReset); /* confirm -> commit */
int bad = probeVerifySection(iReset);
sysLogPrintf(LOG_INFO, "wsresetprobe: section %d confirmed + verified", s);
if (s_resSel != probeResSelBefore) {
sysLogPrintf(LOG_WARNING, "wsresetprobe: __Resolution changed (%d -> %d)", probeResSelBefore, s_resSel);
bad++;
}
sysLogPrintf(LOG_INFO, "wsresetprobe: section '%s' verified, failures=%d", rows[hdr].label, bad);
}
if (probeOtherFolder >= 0) {
save_data *s = fileGetSaveForFoldernum((u32)probeOtherFolder);
if (!s || memcmp(&probeOtherBytes, s, sizeof(save_data)) != 0) {
sysLogPrintf(LOG_WARNING, "wsresetprobe: file %d bytes CHANGED (scope isolation broken)", probeOtherFolder + 1);
} else {
sysLogPrintf(LOG_INFO, "wsresetprobe: file %d unchanged (scope isolation)", probeOtherFolder + 1);
}
} else {
sysLogPrintf(LOG_INFO, "wsresetprobe: no second folder to compare");
}
}
/* In stage (tick 2): arm + confirm every section. The file rows enter the
* D352 command queue (applied + persisted by the game thread at the next
* drain); log the queue depth so the dispatch is visible. */
void optionsResetProbeDispatchStage(void)
{
if (probeSectionN == 0) {
for (int i = 0; i < NUM_ROWS; i++)
if (isResetRow(&rows[i]) && probeSectionN < PROBE_MAX_SECTIONS)
probeSectionIdxs[probeSectionN++] = i;
}
probeResSelBefore = s_resSel;
for (int s = 0; s < probeSectionN; s++) {
optionsRowActivateReset(probeSectionIdxs[s]); /* arm */
optionsRowActivateReset(probeSectionIdxs[s]); /* confirm */
}
int q = watchSettingsQueueCount();
if (q < 6) {
sysLogPrintf(LOG_WARNING, "wsresetprobe: stage dispatch: queue depth %d, want >= 6 (4 GAMEPLAY + 2 AUDIO watch rows)", q);
} else {
sysLogPrintf(LOG_INFO, "wsresetprobe: stage dispatch: %d command(s) queued (game thread drains next tick)", q);
}
}
/* In stage (tick 3, after the drain + persist): verify every section. */
void optionsResetProbeVerifyStage(void)
{
for (int s = 0; s < probeSectionN; s++) {
int iReset = probeSectionIdxs[s];
int hdr, end;
probeSectionRange(iReset, &hdr, &end);
int bad = probeVerifySection(iReset);
if (s_resSel != probeResSelBefore) {
sysLogPrintf(LOG_WARNING, "wsresetprobe: __Resolution changed (%d -> %d)", probeResSelBefore, s_resSel);
bad++;
}
sysLogPrintf(LOG_INFO, "wsresetprobe: stage: section '%s' verified, failures=%d", rows[hdr].label, bad);
}
/* The persisted bytes must hold the BLANKSAVEDATA raw values too. */
save_data *save = fileGetSaveForFoldernum(selected_folder_num);
if (!save) {
sysLogPrintf(LOG_WARNING, "wsresetprobe: stage: no save for folder %d", (int)selected_folder_num);
} else if (save->music_vol != 0xFF || save->sfx_vol != 0xFF) {
sysLogPrintf(LOG_WARNING, "wsresetprobe: stage: saved music=%d fx=%d, want 0xFF/0xFF",
(int)save->music_vol, (int)save->sfx_vol);
} else {
sysLogPrintf(LOG_INFO, "wsresetprobe: stage: saved bytes OK (music/sfx = 0xFF)");
}
}
+103 -10
View File
@@ -12,6 +12,7 @@
#include "player.h"
#include "system.h"
#include "watchsettings.h"
#include "optionsoverlay.h" /* D356 GE_WSPROBE_RESET probe hooks */
extern s32 g_StageNum; /* boss.c: direct -level_XX starts with MENU_INVALID */
extern save_data *fileGetSaveForFoldernum(u32 folder);
@@ -108,28 +109,86 @@ int watchSettingsFolder(void)
/* D352: game-thread only. The chooser is a front-end control (options
* screen); in-stage F10 targets the active file, so a stage-context call
* signals a broken UI invariant -- refuse it loudly rather than race the
* front screen's readers. */
* front screen's readers.
* D356: "none" retired from the cycle (plan §5.5) -- the front target is
* always a folder: frontFolder() resolves a -1 chosen lazily to a valid
* folder (D354) and the auto-init fallback builds file 1 if none exists,
* so the UI shows an unavailable state ("(no file)" annotation, via
* watchSettingsActiveFolder) instead of a number the write path cannot
* use. */
void watchSettingsChooseFile(int dir)
{
if (stageActive()) {
sysLogPrintf(LOG_WARNING, "watchsettings: choose-file in a stage context; ignored");
return;
}
/* Include 'none' so selection is always intentional; skip empty slots.
* Do not load the file here: GE's loader would change front-end audio. */
/* Skip empty slots. Do not load the file here: GE's loader would change
* front-end audio. */
SDL_AtomicLock(&uiLock);
int next = chosen;
for (int n = 0; n <= MAX_FOLDER_COUNT; n++) {
next += dir < 0 ? -1 : 1;
if (next < -1) next = MAX_FOLDER_COUNT - 1;
if (next >= MAX_FOLDER_COUNT) next = -1;
if (next == -1 || validSave(next)) break;
int next = (chosen < FOLDER1) ? FOLDER1 : chosen;
for (int n = 0; n < MAX_FOLDER_COUNT; n++) {
next += dir;
if (next < FOLDER1) next = MAX_FOLDER_COUNT - 1;
if (next >= MAX_FOLDER_COUNT) next = FOLDER1;
if (validSave(next)) break;
}
chosen = next;
stagedField = -1;
int sel = chosen;
SDL_AtomicUnlock(&uiLock);
sysLogPrintf(LOG_INFO, "watchsettings: selected Bond file %d", sel < 0 ? 0 : sel + 1);
sysLogPrintf(LOG_INFO, "watchsettings: selected save file %d", sel + 1);
}
/* D356: context-aware, read-only (never mutates the stored front target):
* in stage -> the stage's active file; front contexts -> the resolved front
* target (frontFolder()'s lazy default). -1 when nothing usable (the
* auto-init file not yet applied) -- the UIs render that as an unavailable
* state ("(no file)"), never a file number the write path cannot use.
* NOTE: frontFolder() acquires uiLock itself -- never call it (or this)
* while uiLock is held: it is a plain spinlock, not re-entrant. */
int watchSettingsActiveFolder(void)
{
int f;
if (stageActive()) {
SDL_AtomicLock(&uiLock);
f = selected_folder_num;
SDL_AtomicUnlock(&uiLock);
} else {
f = frontFolder(); /* self-locks; may default + store 'chosen' */
}
return validSave(f) ? f : -1;
}
/* D356: queued field commands awaiting the game thread (0 in front
* contexts -- nothing is queued there). Drives the in-stage reset probe's
* dispatch check (plan §5.8). */
int watchSettingsQueueCount(void)
{
SDL_AtomicLock(&lock);
int n = count;
SDL_AtomicUnlock(&lock);
return n;
}
/* D356: the per-file "default" for the reset rows -- BLANKSAVEDATA's watch
* fields (file2.c's blank save is a macro, not a global; only the music,
* sfx and options fields matter here: full 0xFF volumes, DEFAULT_OPTIONS).
* completion_bitflags is left 0 (BLANKSAVEDATA sets BOND_BROSNAN; the reset
* rows never touch it). */
static const save_data kBlankSave = {
0, 0,
0, 0, /* completion_bitflags, flag_007 */
0xFF, 0xFF, /* music_vol, sfx_vol (expanded -> VOLUME_MAX) */
DEFAULT_OPTIONS, /* options */
0, 0, 0,
};
int watchSettingsBlankValue(enum WatchSettingField field)
{
if ((unsigned)field >= WATCH_SETTING_COUNT) return 0;
if (field == WATCH_SETTING_MUSIC) return ((int)kBlankSave.music_vol << 7) | (kBlankSave.music_vol >> 1);
if (field == WATCH_SETTING_FX) return ((int)kBlankSave.sfx_vol << 7) | (kBlankSave.sfx_vol >> 1);
return (kBlankSave.options & bits[field]) ? 1 : 0;
}
static int stageActive(void)
@@ -387,6 +446,40 @@ static void applyValue(enum WatchSettingField field, int value)
void watchSettingsGameTick(void)
{
/* D356: the GE_WSPROBE_RESET env gate drives the real reset UI dispatch
* (arm -> confirm -> handler, no bypass) through optionsoverlay.c's
* probe hooks. Each phase waits for its context to be stable (~120
* ticks, a couple of seconds, after the front title has been rendering /
* after stage start) so the probe's save-file I/O never stalls the boot
* black frame. Phase 0 = the pre-stage front probe (dispatch + verify
* directly); once a stage is active, phase 1 = dispatch (arms+confirms
* all sections, queueing the file rows) and phase 2 = verify (after the
* game thread has drained + persisted them). */
{
static int wsResetProbePhase = 0;
static int wsProbeFrontTicks = 0, wsProbeStageTicks = 0;
if (getenv("GE_WSPROBE_RESET")) {
if (!stageActive()) {
if (wsResetProbePhase == 0 && ++wsProbeFrontTicks == 120) {
optionsResetProbePrepare();
wsResetProbePhase = 1;
}
} else {
/* A direct -level_XX boot enters the stage long before the
* front counter would reach 120, so a stage run starts its
* own timer from phase 0 (the front prepare is front-only;
* the stage dispatch builds its section list lazily). */
if (wsResetProbePhase == 0) wsResetProbePhase = 1;
if (wsResetProbePhase == 1 && ++wsProbeStageTicks == 120) {
optionsResetProbeDispatchStage();
wsResetProbePhase = 2;
} else if (wsResetProbePhase == 2 && ++wsProbeStageTicks == 121) {
optionsResetProbeVerifyStage();
wsResetProbePhase = 3;
}
}
}
}
/* This hook runs on the game thread after a gfxFrameMsgQ receive, even
* during load. D352: HOLD pending commands (do not drain) until a stage
* is active -- a slider-release commit must not be silently discarded