forked from Ports/goldeneye-pc-port
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:
1 parent
95f8f5f8b5
commit
fe57d73924
10 files changed
+952
-178
No files matched your search
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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):"
|
||||
|
@@ -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).
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in new issue
Block a user