mirror of
https://github.com/jkdansereau/goldeneye-pc-port.git
synced 2026-10-23 15:52:42 +02:00
watchsettings.c: front-end F10 routing + transparent default Bond-file selection, auto-init fallback, headless probes (D354)
This commit is contained in:
1 parent
462675cf47
commit
61263ad303
4 files changed
+149
-30
No files matched your search
@@ -1,6 +1,6 @@
|
|||||||
# Watch-backed settings in PC Options and F10 — proposal for review
|
# Watch-backed settings in PC Options and F10 — proposal for review
|
||||||
|
|
||||||
Status: **watch-backed subset implemented (D350), robustness fixes landed (D352: transition hold-not-drop, chooser spinlock, queue coalescing, warning logs), and the Turok-style regrouping is done (D353: DISPLAY/GRAPHICS split, BOND FILE absorbs the music/FX sliders, designated rows, overflow guard, file-scope title annotation).** 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–D353 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), and 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).** 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–D354 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
|
## Goal and ownership
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ Thus a front-end row that edits only live watch state will be silently undone be
|
|||||||
1. Add a **watch-backed row type** to the existing shared row model, not another settings menu or fake config registration. Its read/write/availability callbacks operate on GE's real option API; existing config-backed rows continue to work unchanged. Keep enum labels, range/step and value formatting in the shared row table so F10 and the GE-styled screen cannot diverge.
|
1. Add a **watch-backed row type** to the existing shared row model, not another settings menu or fake config registration. Its read/write/availability callbacks operate on GE's real option API; existing config-backed rows continue to work unchanged. Keep enum labels, range/step and value formatting in the shared row table so F10 and the GE-styled screen cannot diverge.
|
||||||
2. Use GE's existing getters/setters for **all eight proposed entries** (`src/game/options.c:526-598`) and both volume controls (`get_mTrack2Vol`/`set_mTrack2Vol`, `call_sndGetSfxSlotFirstNaturalVolume`/`sub_GAME_7F0A91A0`). For FX use the same game volume API as the watch, not an SDL channel gain. No `src/game` bridge or Rule-2 game-code edit is expected for these rows. Preserve the watch's side effects; persistence, if approved, is a separate port-side operation.
|
2. Use GE's existing getters/setters for **all eight proposed entries** (`src/game/options.c:526-598`) and both volume controls (`get_mTrack2Vol`/`set_mTrack2Vol`, `call_sndGetSfxSlotFirstNaturalVolume`/`sub_GAME_7F0A91A0`). For FX use the same game volume API as the watch, not an SDL channel gain. No `src/game` bridge or Rule-2 game-code edit is expected for these rows. Preserve the watch's side effects; persistence, if approved, is a separate port-side operation.
|
||||||
3. **Threading is a design gate:** F10 input runs on the scheduler thread (`inputComputePad(0)`); the GE-styled screen and watch run on the game thread. Do not directly mutate game/watch state from the F10 thread merely because a getter or pointer is accessible. Route F10 writes as bounded requests to a safe game-thread point, and publish a safe, current snapshot for F10 rendering/reads, or establish an equivalently reviewed synchronization scheme. Cancel or retarget queued writes when the active folder/stage changes; never apply a write to the next folder by accident. Check audio/music initialization before **writing** Music/FX in the front end: the music getter reads a global, but the setters may touch not-yet-ready playback state.
|
3. **Threading is a design gate:** F10 input runs on the scheduler thread (`inputComputePad(0)`); the GE-styled screen and watch run on the game thread. Do not directly mutate game/watch state from the F10 thread merely because a getter or pointer is accessible. Route F10 writes as bounded requests to a safe game-thread point, and publish a safe, current snapshot for F10 rendering/reads, or establish an equivalently reviewed synchronization scheme. Cancel or retarget queued writes when the active folder/stage changes; never apply a write to the next folder by accident. Check audio/music initialization before **writing** Music/FX in the front end: the music getter reads a global, but the setters may touch not-yet-ready playback state.
|
||||||
4. Make scope visible to the player: use a dedicated **Bond file** section/page in `MENU_PC_OPTIONS` for GE watch options, with an **explicit Bond-file chooser**; never trust `selected_folder_num` on that screen (frontoptions init even assigns it a fallback). Do not silently choose folder 1. F10 in-stage targets the active file with no chooser. Switching chooser folders must not change the game's active file. D350 reads saved fields directly without calling `fileLoadSettingsForFolder` (which would audibly change front-end music). Unvalidated/absent slots are skipped. Test new/blank files in a normal boot as well as the D299 `GE_STARTMENU` shortcut.
|
4. Make scope visible to the player: use a dedicated **Bond file** section/page in `MENU_PC_OPTIONS` for GE watch options, with an **explicit Bond-file chooser**; never trust `selected_folder_num` on that screen (frontoptions init even assigns it a fallback). F10 in-stage targets the active file with no chooser. **D354 supersedes the "do not silently choose folder 1" stance:** all non-stage contexts (options screen *and* F10 on file select) lazily default to *explicit pick > `selected_folder_num` > first valid folder* so the section works transparently; an explicit "Edit file" cycle (still including "none") always overrides, and a game-thread `fileBuildWriteNewSave(FOLDER1)` one-shot builds a blank file 1 when no valid save exists. Switching chooser folders must not change the game's active file. D350 reads saved fields directly without calling `fileLoadSettingsForFolder` (which would audibly change front-end music). Unvalidated/absent slots are skipped. Test new/blank files in a normal boot as well as the D299 `GE_STARTMENU` shortcut.
|
||||||
5. **Approved port-side persistence:** resolve the actual save-slot pointer with `fileGetSaveForFoldernum(folder)`; `saves[folder]` is **not** generally the folder's physical slot (`fileWriteSave` requires a pointer into `saves[SAVESLOT1..SAVESLOTRAMROM)`). For F10, use a validated in-place path: serialize the live settings into a **temporary copy** with `fileSaveSettingsForFolder(©)`, then copy only the PC-UI-edited field/bit(s) into the real save-slot pointer and call `fileWriteSave(save)` with existing CRC/EEPROM handling. In the front end, where no GE player is active, merge only the selected field directly into the chosen saved record instead of loading it into live watch state. Retain `OPTION_CONTROLTYPE` from the real save: the port reasserts Honey/Solitaire every poll and must not overwrite stored controller style. This field-scoped merge also prevents a watch-only edit to a *different* field from piggybacking onto the next PC-UI save, preserving the approved session-only asymmetry. Validate the path against absent/free slots and ensure the live state belongs to **only** the chosen folder (do not load or copy values from a different folder). Never call `fileClearSavefileForFolder` as a generic settings save. Toggle/enum changes persist on value change; slider drags persist on release, not each detent. F10 requests must reach the correct game-thread/file target before write. The GE watch's own edits remain session-only. Test stage load, file switch, copy/erase, and restart for cross-file contamination; no N64 game-code changes. **Known erase edge:** GE's `fileClearSavefileForFolder` serializes *current* live settings into the reset file; after editing B, erasing A could copy B's live options into wiped A (also possible on N64 after watch edits). Document/test, do not change game code.
|
5. **Approved port-side persistence:** resolve the actual save-slot pointer with `fileGetSaveForFoldernum(folder)`; `saves[folder]` is **not** generally the folder's physical slot (`fileWriteSave` requires a pointer into `saves[SAVESLOT1..SAVESLOTRAMROM)`). For F10, use a validated in-place path: serialize the live settings into a **temporary copy** with `fileSaveSettingsForFolder(©)`, then copy only the PC-UI-edited field/bit(s) into the real save-slot pointer and call `fileWriteSave(save)` with existing CRC/EEPROM handling. In the front end, where no GE player is active, merge only the selected field directly into the chosen saved record instead of loading it into live watch state. Retain `OPTION_CONTROLTYPE` from the real save: the port reasserts Honey/Solitaire every poll and must not overwrite stored controller style. This field-scoped merge also prevents a watch-only edit to a *different* field from piggybacking onto the next PC-UI save, preserving the approved session-only asymmetry. Validate the path against absent/free slots and ensure the live state belongs to **only** the chosen folder (do not load or copy values from a different folder). Never call `fileClearSavefileForFolder` as a generic settings save. Toggle/enum changes persist on value change; slider drags persist on release, not each detent. F10 requests must reach the correct game-thread/file target before write. The GE watch's own edits remain session-only. Test stage load, file switch, copy/erase, and restart for cross-file contamination; no N64 game-code changes. **Known erase edge:** GE's `fileClearSavefileForFolder` serializes *current* live settings into the reset file; after editing B, erasing A could copy B's live options into wiped A (also possible on N64 after watch edits). Document/test, do not change game code.
|
||||||
6. For future per-section reset-to-defaults: ini rows reset from registered PC defaults; watch rows reset to **`BLANKSAVEDATA`/`DEFAULT_OPTIONS`** (`bondconstants.h:2151`, `file2.h:23`) for the selected file, not from a global ini snapshot. New saves start with Music/FX 0xFF (full volume) and auto-aim/sight/look-ahead/ammo enabled. Separate confirmation and persistence policy before wiring a button.
|
6. For future per-section reset-to-defaults: ini rows reset from registered PC defaults; watch rows reset to **`BLANKSAVEDATA`/`DEFAULT_OPTIONS`** (`bondconstants.h:2151`, `file2.h:23`) for the selected file, not from a global ini snapshot. New saves start with Music/FX 0xFF (full volume) and auto-aim/sight/look-ahead/ammo enabled. Separate confirmation and persistence policy before wiring a button.
|
||||||
|
|
||||||
@@ -56,4 +56,5 @@ Thus a front-end row that edits only live watch state will be silently undone be
|
|||||||
|
|
||||||
- **Approved:** PC-UI edits persist per selected Bond file; watch-only edits stay session-only. The front screen gets an explicit file chooser; F10 uses the active file. Preserve stored controller type; toggles/enums save on change, slider drags on release. No `src/game` behavior change or N64 save-format change.
|
- **Approved:** PC-UI edits persist per selected Bond file; watch-only edits stay session-only. The front screen gets an explicit file chooser; F10 uses the active file. Preserve stored controller type; toggles/enums save on change, slider drags on release. No `src/game` behavior change or N64 save-format change.
|
||||||
- **Still separate product decisions:** whether/how to expose GE Screen/Ratio alongside PC video settings; whether to expose control style at all given the port's forced mapping (deferred); whether Bond-file reset needs its own confirmation rather than being bundled with global PC defaults.
|
- **Still separate product decisions:** whether/how to expose GE Screen/Ratio alongside PC video settings; whether to expose control style at all given the port's forced mapping (deferred); whether Bond-file reset needs its own confirmation rather than being bundled with global PC defaults.
|
||||||
- **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). 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.
|
- **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. 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.
|
||||||
@@ -231,3 +231,4 @@ D350,PARTIAL,Watch-backed PC options:,"PARTIAL (implemented; user reports tests
|
|||||||
D351,FIXED,Existing D346 value/unit rows had positional initializers shifted into hidePtr:,"FIXED (port-only): designated row initializers restore unit, deadzone divisor and hide-if-auto fields; build clean, headless smoke clean."
|
D351,FIXED,Existing D346 value/unit rows had positional initializers shifted into hidePtr:,"FIXED (port-only): designated row initializers restore unit, deadzone divisor and hide-if-auto fields; build clean, headless smoke clean."
|
||||||
D352,FIXED,D350 review of 1303c0eb:,"FIXED (port-only, port/src/watchsettings.c):"
|
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):"
|
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):"
|
||||||
|
@@ -639,6 +639,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
|
|||||||
| D351 | **Existing D346 value/unit rows had positional initializers shifted into `hidePtr`: %/x rows hidden or formatted without units.** — full `## D351` entry at file tail | FIXED (port-only): designated row initializers restore unit, deadzone divisor and hide-if-auto fields; build clean, headless smoke clean. Manual settings visual check owed. |
|
| D351 | **Existing D346 value/unit rows had positional initializers shifted into `hidePtr`: %/x rows hidden or formatted without units.** — full `## D351` entry at file tail | FIXED (port-only): designated row initializers restore unit, deadzone divisor and hide-if-auto fields; build clean, headless smoke clean. Manual settings visual check owed. |
|
||||||
| D352 | **D350 review of 1303c0eb: stage transitions silently drop queued F10 setting changes (queue drained before the `stageActive` check); the UI thread's chooser state is unsynchronized; rapid slider drags push one RSP command per detent and re-persist the same value each frame; persist failures are silent (2026-09-27).** — full `## D352` entry at file tail | FIXED (port-only, `port/src/watchsettings.c`): hold-not-drop at stage transitions, spinlock on chooser state + front-screen guard, per-field coalesced queue commits, warning logs on all no-op paths, `PersistField` made static. NTSC build clean, headless menu smoke clean. |
|
| D352 | **D350 review of 1303c0eb: stage transitions silently drop queued F10 setting changes (queue drained before the `stageActive` check); the UI thread's chooser state is unsynchronized; rapid slider drags push one RSP command per detent and re-persist the same value each frame; persist failures are silent (2026-09-27).** — full `## D352` entry at file tail | FIXED (port-only, `port/src/watchsettings.c`): hold-not-drop at stage transitions, spinlock on chooser state + front-screen guard, per-field coalesced queue commits, warning logs on all no-op paths, `PersistField` made static. NTSC build clean, headless menu smoke clean. |
|
||||||
| 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. |
|
| 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). |
|
||||||
|
|
||||||
Phase 2 replaced the Phase-1 demo loop with the real `mainproc()` on real OS
|
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
|
threads, compiled GE's real `src/sched.c`, and brought in PD's fast3d software
|
||||||
@@ -13819,3 +13820,18 @@ The D346 "single Video section" compromise (14–15 rows, exactly at `MAX_PROWS=
|
|||||||
- **Overflow guard + accessor.** `buildPages` logs a `LOG_WARNING` naming the section and the last visible row if a section ever outgrows `MAX_PROWS` (the D346 VIDEO section truncated silently at exactly 15 rows before this); `optionsRowIsBondChooser()` is exported so the front-end title annotation and future F10 code can find the chooser row by role instead of index.
|
- **Overflow guard + accessor.** `buildPages` logs a `LOG_WARNING` naming the section and the last visible row if a section ever outgrows `MAX_PROWS` (the D346 VIDEO section truncated silently at exactly 15 rows before this); `optionsRowIsBondChooser()` is exported so the front-end title annotation and future F10 code can find the chooser row by role instead of index.
|
||||||
|
|
||||||
Section order: DISPLAY, GRAPHICS, BOND FILE, MOUSE / AIM, CONTROLLER, GAME (6 sections, under `MAX_PAGES=8`). Per-section "reset to defaults" (Gate E) and the §6-missing master-volume row remain outstanding. Verification: NTSC build clean, headless menu smoke boots to the front end; the regrouped page flow (arrows through 6 pages, annotation visible, no overflow log) needs a manual visual pass.
|
Section order: DISPLAY, GRAPHICS, BOND FILE, MOUSE / AIM, CONTROLLER, GAME (6 sections, under `MAX_PAGES=8`). Per-section "reset to defaults" (Gate E) and the §6-missing master-volume row remain outstanding. Verification: NTSC build clean, headless menu smoke boots to the front end; the regrouped page flow (arrows through 6 pages, annotation visible, no overflow log) needs a manual visual pass.
|
||||||
|
|
||||||
|
## D354 — Front-end F10 Bond rows disabled; default Bond-file selection (2026-09-27)
|
||||||
|
|
||||||
|
Maintainer report: the F10 volume settings "don't apply", while the watch and the main-menu options work. Diagnosis (port-only):
|
||||||
|
|
||||||
|
- **The in-stage F10 path is sound.** A new env-gated probe (`GE_WSPROBE=<tick>`, same pattern as `GE_D314`) queues an F10-style commit of Music+FX on the Nth active stage tick and logs live + saved before/after. Headless `-level_33` run: live music 15163→4096, save.music 118→32, `watchsettings: persisted field 0/1 in Bond file 1` — the queue→apply→persist bridge works end-to-end in a stage.
|
||||||
|
- **The real gap was the front end.** `watchSettingsAvailable()` special-cased only `MENU_PC_OPTIONS` (front path) and stages (snapshot path); every other context — notably **F10 over the file-select screen**, the primary F10 surface — fell through to the stage path and returned "unavailable", disabling every Bond row. With the chooser row hidden in F10 and `chosen` starting at -1 ("none"), the rows read "Select file" and could never be enabled: the user's "volume settings in F10 don't apply".
|
||||||
|
- **Fix (maintainer-suggested UX):**
|
||||||
|
- All **non-stage** contexts now route through the front path (chooser + `validSave`), so F10-on-file-select behaves like the options screen.
|
||||||
|
- New `frontFolder()`: lazy default target, resolving `chosen` on first use to **explicit pick > `selected_folder_num` (the folder the player is viewing) > first valid folder**, under the existing `uiLock`. A fresh PC eeprom is booted with a BLANKSAVEDATA slot per folder (libultra.c D259/D281 patch), so the default almost always lands on file 1 — the Bond section is usable transparently, no "Edit file" ceremony. The D353 title annotation now shows "(File 1)" by default instead of "(no file)".
|
||||||
|
- **Auto-init fallback:** one-shot, game-thread, in `watchSettingsGameTick`: if *no* folder holds a valid save (wiped/corrupt eeprom), call the game's own `fileBuildWriteNewSave(FOLDER1)` (the routine boot validation uses, `file2.c:509`) and default to it; logs the outcome. Never modifies game code.
|
||||||
|
- `watchSettingsChooseFile` guard flipped to "refuse in stage context" (front-end control only); "none" remains reachable by explicit cycling.
|
||||||
|
- `GE_WSPROBE_FRONT=<tick>` probe verifies the same front commit headlessly: default boot (menu) resolves folder 0 (file 1) with no interaction, commit 4096 → save.music 255→32.
|
||||||
|
|
||||||
|
Verification: NTSC build clean; both probes pass headless; in-stage behavior unchanged (probe-identical). The D350 threading gate is unchanged in effect: the front path's `fileWriteSave` (EEPROM) now also runs from the F10 input thread in front contexts, but only one settings surface is ever active at a time and the game thread performs no save writes on menus, so writes stay serialized; reads (`saves[]`/CRC) are stable menu state. Manual by-ear check owed: F10 music/FX on file select, and default-file behavior after erase.
|
||||||
+128
-27
@@ -20,12 +20,24 @@ extern void fileWriteSave(save_data *save);
|
|||||||
extern s32 fileGamePakProbe(void);
|
extern s32 fileGamePakProbe(void);
|
||||||
extern void fileGenerateCRC(u8 *start, u8 *end, save_data *result);
|
extern void fileGenerateCRC(u8 *start, u8 *end, save_data *result);
|
||||||
extern void sub_GAME_7F0A91A0(u16 volume);
|
extern void sub_GAME_7F0A91A0(u16 volume);
|
||||||
|
extern void fileBuildWriteNewSave(u32 folder);
|
||||||
extern void set_cur_player_look_vertical_inverted(u32 value);
|
extern void set_cur_player_look_vertical_inverted(u32 value);
|
||||||
extern void cur_player_set_aim_control(u32 value);
|
extern void cur_player_set_aim_control(u32 value);
|
||||||
extern void cur_player_set_sight_onscreen_control(u32 value);
|
extern void cur_player_set_sight_onscreen_control(u32 value);
|
||||||
extern void cur_player_set_ammo_onscreen_setting(u32 value);
|
extern void cur_player_set_ammo_onscreen_setting(u32 value);
|
||||||
|
|
||||||
static int frontScreen(void); /* defined below; the choose-file guard uses it */
|
static int frontFolder(void); /* defined below; the lazy file default */
|
||||||
|
static int stageActive(void); /* defined below; the choose-file guard uses it */
|
||||||
|
|
||||||
|
/* D354: env-gated headless probe for the F10 queue->apply->persist path
|
||||||
|
* (same pattern as optionsoverlay's GE_D314). GE_WSPROBE=<tick> makes the
|
||||||
|
* <tick>-th active stage tick queue an F10-style commit of Music+FX to a
|
||||||
|
* known value, with before/after live+saved logs so a stage run with no
|
||||||
|
* keyboard/mouse can prove the bridge applies and persists. */
|
||||||
|
static int wsProbeTick = -1;
|
||||||
|
static int wsProbeTickN = 0;
|
||||||
|
static int wsProbePhase = 0; /* 0=armed, 1=queued (log pre), 2=done (log post) */
|
||||||
|
|
||||||
|
|
||||||
static const char *const keys[WATCH_SETTING_COUNT] = {
|
static const char *const keys[WATCH_SETTING_COUNT] = {
|
||||||
"Bond.Music", "Bond.FX", "Bond.Look", "Bond.AutoAim",
|
"Bond.Music", "Bond.FX", "Bond.Look", "Bond.AutoAim",
|
||||||
@@ -37,11 +49,10 @@ static const u16 bits[WATCH_SETTING_COUNT] = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/* Explicit chooser is separate from selected_folder_num (which frontoptions
|
/* Explicit chooser is separate from selected_folder_num (which frontoptions
|
||||||
* assigns FOLDER1 merely to draw its dossier background). -1 means none.
|
* assigns FOLDER1 merely to draw its dossier background). -1 means none
|
||||||
* D352: guarded by uiLock. Today these are game-thread-only (front options
|
* (unresolved; frontFolder() lazily defaults it). D352: guarded by uiLock;
|
||||||
* screen); the lock plus the frontScreen() guard in watchSettingsChooseFile
|
* D354: also read/written by F10 front-end contexts (input thread), which
|
||||||
* keep a future scheduler-thread caller (e.g. an F10 file indicator) from
|
* is why the lock exists. */
|
||||||
* racing the front screen. */
|
|
||||||
static int chosen = -1;
|
static int chosen = -1;
|
||||||
static int staged[WATCH_SETTING_COUNT];
|
static int staged[WATCH_SETTING_COUNT];
|
||||||
static int stagedField = -1, stagedFolder = -1;
|
static int stagedField = -1, stagedFolder = -1;
|
||||||
@@ -86,13 +97,14 @@ int watchSettingsFolder(void)
|
|||||||
return f;
|
return f;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* D352: game-thread only. F10 hides the chooser row, so a call from
|
/* D352: game-thread only. The chooser is a front-end control (options
|
||||||
* elsewhere signals a broken UI invariant -- refuse it loudly rather than
|
* screen); in-stage F10 targets the active file, so a stage-context call
|
||||||
* race the front screen's unlocked readers. */
|
* signals a broken UI invariant -- refuse it loudly rather than race the
|
||||||
|
* front screen's readers. */
|
||||||
void watchSettingsChooseFile(int dir)
|
void watchSettingsChooseFile(int dir)
|
||||||
{
|
{
|
||||||
if (!frontScreen()) {
|
if (stageActive()) {
|
||||||
sysLogPrintf(LOG_WARNING, "watchsettings: choose-file outside the front options screen; ignored");
|
sysLogPrintf(LOG_WARNING, "watchsettings: choose-file in a stage context; ignored");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
/* Include 'none' so selection is always intentional; skip empty slots.
|
/* Include 'none' so selection is always intentional; skip empty slots.
|
||||||
@@ -112,7 +124,6 @@ void watchSettingsChooseFile(int dir)
|
|||||||
sysLogPrintf(LOG_INFO, "watchsettings: selected Bond file %d", sel < 0 ? 0 : sel + 1);
|
sysLogPrintf(LOG_INFO, "watchsettings: selected Bond file %d", sel < 0 ? 0 : sel + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
static int frontScreen(void) { return current_menu == MENU_PC_OPTIONS; }
|
|
||||||
static int stageActive(void)
|
static int stageActive(void)
|
||||||
{
|
{
|
||||||
/* Direct -level_XX boots never visit the front-end RUN_STAGE state. */
|
/* Direct -level_XX boots never visit the front-end RUN_STAGE state. */
|
||||||
@@ -120,15 +131,41 @@ static int stageActive(void)
|
|||||||
(current_menu == MENU_INVALID && g_StageNum != LEVELID_TITLE);
|
(current_menu == MENU_INVALID && g_StageNum != LEVELID_TITLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* D354: lazy default target for front-end (non-stage) contexts. The chooser
|
||||||
|
* used to start at -1 ("none"), which disabled every Bond row -- and in F10
|
||||||
|
* the chooser row is hidden, so on the front end (file select) the rows could
|
||||||
|
* never be enabled at all: the volume sliders read "Select file" and did
|
||||||
|
* nothing. Resolution order: an explicit "Edit file" choice > the folder the
|
||||||
|
* player is viewing (selected_folder_num, set by file select) > the first
|
||||||
|
* valid folder. The first two are pure reads of stable menu state, safe from
|
||||||
|
* the F10 input thread (D350 gate: no GE writes from it; only chosen is
|
||||||
|
* written, under uiLock). A fresh PC eeprom is booted with a BLANKSAVEDATA
|
||||||
|
* slot per folder (libultra.c D259/D281 patch), so a default almost always
|
||||||
|
* resolves; the game-thread fallback in watchSettingsGameTick builds one if
|
||||||
|
* it does not. */
|
||||||
|
static int frontFolder(void)
|
||||||
|
{
|
||||||
|
SDL_AtomicLock(&uiLock);
|
||||||
|
int f = chosen;
|
||||||
|
if (f < FOLDER1) {
|
||||||
|
if (validSave(selected_folder_num))
|
||||||
|
f = chosen = selected_folder_num;
|
||||||
|
else
|
||||||
|
for (int i = FOLDER1; i < MAX_FOLDER_COUNT; i++)
|
||||||
|
if (validSave(i)) { f = chosen = i; break; }
|
||||||
|
}
|
||||||
|
SDL_AtomicUnlock(&uiLock);
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
int watchSettingsAvailable(void)
|
int watchSettingsAvailable(void)
|
||||||
{
|
{
|
||||||
if (frontScreen()) {
|
if (!stageActive()) {
|
||||||
SDL_AtomicLock(&uiLock);
|
/* Front end: MENU_PC_OPTIONS, or F10 over file select / title.
|
||||||
int f = chosen;
|
* D354: F10 front contexts were mis-routed through the stage path
|
||||||
SDL_AtomicUnlock(&uiLock);
|
* (and returned "unavailable"), disabling every Bond row. */
|
||||||
return validSave(f) != NULL;
|
return validSave(frontFolder()) != NULL;
|
||||||
}
|
}
|
||||||
if (!stageActive()) return 0;
|
|
||||||
int valid;
|
int valid;
|
||||||
SDL_AtomicLock(&lock);
|
SDL_AtomicLock(&lock);
|
||||||
valid = snapFolder >= FOLDER1 && snapFolder < MAX_FOLDER_COUNT;
|
valid = snapFolder >= FOLDER1 && snapFolder < MAX_FOLDER_COUNT;
|
||||||
@@ -146,10 +183,9 @@ static int savedValue(const save_data *save, enum WatchSettingField field)
|
|||||||
int watchSettingsRead(enum WatchSettingField field)
|
int watchSettingsRead(enum WatchSettingField field)
|
||||||
{
|
{
|
||||||
if ((unsigned)field >= WATCH_SETTING_COUNT) return 0;
|
if ((unsigned)field >= WATCH_SETTING_COUNT) return 0;
|
||||||
if (frontScreen()) {
|
if (!stageActive()) {
|
||||||
int f = -1, stagedVal = -1;
|
int f = frontFolder(), stagedVal = -1;
|
||||||
SDL_AtomicLock(&uiLock);
|
SDL_AtomicLock(&uiLock);
|
||||||
f = chosen;
|
|
||||||
if (stagedField == (int)field && stagedFolder == f)
|
if (stagedField == (int)field && stagedFolder == f)
|
||||||
stagedVal = staged[field];
|
stagedVal = staged[field];
|
||||||
SDL_AtomicUnlock(&uiLock);
|
SDL_AtomicUnlock(&uiLock);
|
||||||
@@ -238,15 +274,15 @@ static void saveFrontField(int folder, enum WatchSettingField field, int value)
|
|||||||
void watchSettingsSet(enum WatchSettingField field, int value, int commit)
|
void watchSettingsSet(enum WatchSettingField field, int value, int commit)
|
||||||
{
|
{
|
||||||
if ((unsigned)field >= WATCH_SETTING_COUNT || !watchSettingsAvailable()) return;
|
if ((unsigned)field >= WATCH_SETTING_COUNT || !watchSettingsAvailable()) return;
|
||||||
if (frontScreen()) {
|
if (!stageActive()) {
|
||||||
|
int f = frontFolder();
|
||||||
SDL_AtomicLock(&uiLock);
|
SDL_AtomicLock(&uiLock);
|
||||||
if (commit) {
|
if (commit) {
|
||||||
int f = chosen;
|
|
||||||
stagedField = -1;
|
stagedField = -1;
|
||||||
SDL_AtomicUnlock(&uiLock);
|
SDL_AtomicUnlock(&uiLock);
|
||||||
saveFrontField(f, field, value);
|
saveFrontField(f, field, value);
|
||||||
} else {
|
} else {
|
||||||
stagedField = field; stagedFolder = chosen; staged[field] = value;
|
stagedField = field; stagedFolder = f; staged[field] = value;
|
||||||
SDL_AtomicUnlock(&uiLock);
|
SDL_AtomicUnlock(&uiLock);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -279,12 +315,12 @@ void watchSettingsSet(enum WatchSettingField field, int value, int commit)
|
|||||||
void watchSettingsCommit(enum WatchSettingField field)
|
void watchSettingsCommit(enum WatchSettingField field)
|
||||||
{
|
{
|
||||||
if ((unsigned)field >= WATCH_SETTING_COUNT) return;
|
if ((unsigned)field >= WATCH_SETTING_COUNT) return;
|
||||||
if (frontScreen()) {
|
if (!stageActive()) {
|
||||||
/* D352: a commit is meaningful only if a drag staged an edit; a
|
/* D352: a commit is meaningful only if a drag staged an edit; a
|
||||||
* plain click with no file selected stays a silent no-op. */
|
* plain click with no file selected stays a silent no-op. */
|
||||||
int pending;
|
int f = frontFolder(), pending;
|
||||||
SDL_AtomicLock(&uiLock);
|
SDL_AtomicLock(&uiLock);
|
||||||
pending = stagedField == (int)field && stagedFolder == chosen && chosen >= FOLDER1;
|
pending = stagedField == (int)field && stagedFolder == f && f >= FOLDER1;
|
||||||
SDL_AtomicUnlock(&uiLock);
|
SDL_AtomicUnlock(&uiLock);
|
||||||
if (!pending) return;
|
if (!pending) return;
|
||||||
} else if (!watchSettingsAvailable()) {
|
} else if (!watchSettingsAvailable()) {
|
||||||
@@ -338,6 +374,50 @@ void watchSettingsGameTick(void)
|
|||||||
* against selected_folder_num at apply time, so a stale cross-file edit
|
* against selected_folder_num at apply time, so a stale cross-file edit
|
||||||
* is still dropped, never misapplied. */
|
* is still dropped, never misapplied. */
|
||||||
if (!stageActive() || !g_CurrentPlayer) {
|
if (!stageActive() || !g_CurrentPlayer) {
|
||||||
|
/* D354: one-shot fallback -- if not a single folder holds a valid
|
||||||
|
* save (wiped/corrupt eeprom), build the game's own blank save for
|
||||||
|
* file 1 (game thread, so the EEPROM write is on the owning thread)
|
||||||
|
* and default to it. A fresh PC eeprom never reaches this: boot
|
||||||
|
* patches in a BLANKSAVEDATA slot per folder (libultra.c D259/D281).
|
||||||
|
* frontFolder() re-checks, so an explicit chooser pick is respected. */
|
||||||
|
if (!stageActive() && frontFolder() < FOLDER1) {
|
||||||
|
static int wsAutoInitTried = 0;
|
||||||
|
if (!wsAutoInitTried) {
|
||||||
|
wsAutoInitTried = 1;
|
||||||
|
sysLogPrintf(LOG_INFO, "watchsettings: no valid Bond save in any folder; building a blank file 1 (fileBuildWriteNewSave)");
|
||||||
|
fileBuildWriteNewSave(FOLDER1);
|
||||||
|
SDL_AtomicLock(&uiLock);
|
||||||
|
if (validSave(FOLDER1))
|
||||||
|
chosen = FOLDER1;
|
||||||
|
SDL_AtomicUnlock(&uiLock);
|
||||||
|
sysLogPrintf(validSave(FOLDER1) ? LOG_INFO : LOG_WARNING,
|
||||||
|
"watchsettings: blank file 1 %s", validSave(FOLDER1) ? "created" : "unavailable (no free slot?)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* D354 probe: GE_WSPROBE_FRONT=<tick> exercises the front-end
|
||||||
|
* (chooser-default) write path the same way F10-on-file-select
|
||||||
|
* would, headlessly. */
|
||||||
|
{
|
||||||
|
static int wsFrontProbe = -1, wsFrontTickN = 0, wsFrontPhase = 0;
|
||||||
|
if (wsFrontProbe < 0) {
|
||||||
|
const char *e = getenv("GE_WSPROBE_FRONT");
|
||||||
|
wsFrontProbe = (e && atoi(e) > 0) ? atoi(e) : 2147483647;
|
||||||
|
}
|
||||||
|
if (wsFrontPhase == 0 && wsFrontTickN++ == (size_t)wsFrontProbe) {
|
||||||
|
wsFrontPhase = 1;
|
||||||
|
int f = frontFolder();
|
||||||
|
save_data *save = validSave(f);
|
||||||
|
sysLogPrintf(LOG_INFO, "wsfront: folder=%d save.music=%d; committing front-end Music=4096",
|
||||||
|
f, save ? (int)save->music_vol : -1);
|
||||||
|
watchSettingsSet(WATCH_SETTING_MUSIC, 4096, 1);
|
||||||
|
} else if (wsFrontPhase == 1 && wsFrontTickN++ == (size_t)wsFrontProbe + 1) {
|
||||||
|
wsFrontPhase = 2;
|
||||||
|
int f = frontFolder();
|
||||||
|
save_data *s2 = validSave(f);
|
||||||
|
sysLogPrintf(LOG_INFO, "wsfront: post save.music=%d (folder %d)",
|
||||||
|
s2 ? (int)s2->music_vol : -1, f);
|
||||||
|
}
|
||||||
|
}
|
||||||
SDL_AtomicLock(&lock);
|
SDL_AtomicLock(&lock);
|
||||||
snapFolder = -1;
|
snapFolder = -1;
|
||||||
SDL_AtomicUnlock(&lock);
|
SDL_AtomicUnlock(&lock);
|
||||||
@@ -359,6 +439,27 @@ void watchSettingsGameTick(void)
|
|||||||
applyValue((enum WatchSettingField)c->field, c->value);
|
applyValue((enum WatchSettingField)c->field, c->value);
|
||||||
if (c->commit) watchSettingsPersistField(c->folder, (enum WatchSettingField)c->field);
|
if (c->commit) watchSettingsPersistField(c->folder, (enum WatchSettingField)c->field);
|
||||||
}
|
}
|
||||||
|
/* D354 probe: log before/after the queued commit, one tick apart. */
|
||||||
|
if (wsProbeTick < 0) {
|
||||||
|
const char *e = getenv("GE_WSPROBE");
|
||||||
|
wsProbeTick = (e && atoi(e) > 0) ? atoi(e) : 2147483647;
|
||||||
|
}
|
||||||
|
if (wsProbePhase == 0 && wsProbeTickN++ == (size_t)wsProbeTick) {
|
||||||
|
wsProbePhase = 1;
|
||||||
|
save_data *save = validSave(selected_folder_num);
|
||||||
|
sysLogPrintf(LOG_INFO, "wsprobe: pre music=%d fx=%d save.music=%d save.fx=%d (folder %d, %d queued)",
|
||||||
|
get_mTrack2Vol(), call_sndGetSfxSlotFirstNaturalVolume(),
|
||||||
|
save ? (int)save->music_vol : -1, save ? (int)save->sfx_vol : -1,
|
||||||
|
(int)selected_folder_num, n);
|
||||||
|
watchSettingsSet(WATCH_SETTING_MUSIC, 4096, 1);
|
||||||
|
watchSettingsSet(WATCH_SETTING_FX, 4096, 1);
|
||||||
|
} else if (wsProbePhase == 1 && wsProbeTickN++ == (size_t)wsProbeTick + 1) {
|
||||||
|
wsProbePhase = 2;
|
||||||
|
save_data *save = validSave(selected_folder_num);
|
||||||
|
sysLogPrintf(LOG_INFO, "wsprobe: post music=%d fx=%d save.music=%d save.fx=%d",
|
||||||
|
get_mTrack2Vol(), call_sndGetSfxSlotFirstNaturalVolume(),
|
||||||
|
save ? (int)save->music_vol : -1, save ? (int)save->sfx_vol : -1);
|
||||||
|
}
|
||||||
int values[WATCH_SETTING_COUNT];
|
int values[WATCH_SETTING_COUNT];
|
||||||
for (int f = 0; f < WATCH_SETTING_COUNT; f++)
|
for (int f = 0; f < WATCH_SETTING_COUNT; f++)
|
||||||
values[f] = liveValue((enum WatchSettingField)f);
|
values[f] = liveValue((enum WatchSettingField)f);
|
||||||
|
|||||||
Reference in new issue
Block a user