mirror of
https://github.com/jkdansereau/goldeneye-pc-port.git
synced 2026-10-23 15:52:42 +02:00
D350: expose watch-backed Bond settings in PC options and F10
Add a game-thread bridge and field-scoped per-file persistence for PC UI edits, with an explicit file chooser; keep watch-only edits session-only. Fix D351 row initializer offsets for units and auto-hide. Document verification and review follow-ups.
This commit is contained in:
1 parent
98a24a07f4
commit
1303c0ebe1
9 files changed
+525
-30
No files matched your search
@@ -0,0 +1,58 @@
|
|||||||
|
# Watch-backed settings in PC Options and F10 — proposal for review
|
||||||
|
|
||||||
|
Status: **watch-backed subset implemented (D350), headless-verified; maintainer reports tests good, detailed review requested next session.** Wider M3 remains outstanding. PC-only per-file persistence and explicit Bond-file chooser approved; details below. See `docs/dev/findings.md` D350 for verification and D351 for a pre-existing row initializer fix. 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
|
||||||
|
|
||||||
|
Show GE's existing watch settings in both PC settings surfaces (`MENU_PC_OPTIONS` and F10) without maintaining a second copy of each value. Both surfaces already consume one row table in `port/src/optionsoverlay.c`; the watch remains an independent UI editing the **same underlying GE value**. Changes made in either direction must be visible on the next read. Preserve the N64 watch, its behavior, and the N64 build.
|
||||||
|
|
||||||
|
**Approved ownership:** per-file GE settings belong in the Bond save; machine-wide PC settings belong in `ge007.ini`. GE's original watch edits are *not* persisted on normal play (D349 below): **watch-made changes remain session-only**, while **PC-UI-made changes persist to the selected Bond file**. This is a deliberate port-side QoL divergence, not existing GE behavior. Do not register watch-backed settings as ini variables, copy watch values into config globals, or overwrite a newly loaded Bond file with stale ini values. A proposed PC master volume is separate: it multiplies the final output and does not replace the watch's Music/FX volumes.
|
||||||
|
|
||||||
|
## Inventory and proposed rows
|
||||||
|
|
||||||
|
Source of truth: `src/game/options.c` (`game_options_entries`, `cur_player_get/set_*`, `get_mTrack2Vol`/`set_mTrack2Vol`, `sub_GAME_7F0A91A0`), `src/game/file2.c:1246-1350` (per-folder serialization), and `port/src/input.c:~939` (forced control style). Audit exact getter/setter availability and initialization before implementing each row.
|
||||||
|
|
||||||
|
| Watch control | PC presentation | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| Music / FX volume | Audio section, separate Music and FX sliders | GE range 0..32767, stored in the Bond save at 8-bit precision; use actual GE APIs, not an SDL channel gain in place of either. Master volume stays a distinct global row. |
|
||||||
|
| Look up/down (reverse/upright), auto-aim, aim control (hold/toggle), sight on screen, look ahead, ammo on screen | Bond file / gameplay subsection, with GE-specific labels | These are the watch's `game_options_entries` and save bits. Watch look-up/down is distinct from PC MouseInvertY and PadLookInvertY: the independent inversions compose (XOR). Label/help text must explain the stacking; do not combine their stored values. |
|
||||||
|
| Screen (Full/Wide/Cinema), Ratio (Normal/16:9) | Separate clearly marked GE presentation controls, if useful | Not PC window resolution/mode or `Video.NativeWidescreen`. Verify interactions with native widescreen/crop before exposing. |
|
||||||
|
| Controller style / controller diagram | Defer pending input design | The diagram is not a setting. `inputComputePad(0)` currently reasserts Honey or Solitaire from `Input.NaturalPitch` every poll, including after save load or watch changes; `bondview.c` and multiplayer load also force Honey in some contexts. A naive style row would be immediately undone. Decide whether the watch selector should be informational, limited to supported PC mappings, or integrated with the port mapping; no row until this is reconciled. Dual-controller styles need their own decision. |
|
||||||
|
|
||||||
|
This inventory is not permission to expose every field automatically. Especially do not silently merge identically named PC and GE toggles that have different input paths or persistence.
|
||||||
|
|
||||||
|
### Persistence ground truth (D349)
|
||||||
|
|
||||||
|
`fileSaveSettingsForFolder` (`src/game/file2.c:1246`) has one caller, `fileClearSavefileForFolder` (`:1379`), reached from the erase path. Normal watch edits change live values but do **not** write the file's `options`/`music_vol`/`sfx_vol` fields or EEPROM. `init_watch_at_start_of_stage` (`src/game/options.c:415`) calls `fileLoadSaveSettingsForSelectedFolder`, so the next stage reloads those fields and discards unsaved watch edits. This is a GE/N64 quirk, not a PC regression; Gate A should re-audit indirect writes before implementation and record any correction to D349.
|
||||||
|
|
||||||
|
Thus a front-end row that edits only live watch state will be silently undone before gameplay. The maintainer approved **port-side per-file persistence of PC-UI changes**, without changing the N64 save format or the N64 watch's session-only behavior. Do not auto-persist edits made directly in the watch.
|
||||||
|
|
||||||
|
## UI / data design
|
||||||
|
|
||||||
|
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.
|
||||||
|
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.
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Sequence / review gates
|
||||||
|
|
||||||
|
- **Gate A — behavior audit + D349:** re-verify the erase-only write and stage-start reload (including indirect save paths); record the ground-truth quirk, defaults, stored control-type preservation, erase cross-file edge and exact API for each row. Trace folder-to-physical-slot mapping, FX/music setter readiness, watch-to-gameplay consumers, composed inversion and the F10/game-thread boundary. Approved policy: explicit chooser + PC-UI edits persist; watch edits remain session-only.
|
||||||
|
- **Gate B — foundation: IMPLEMENTED in D350**, including explicit file chooser, CRC guard, stage/file identity and thread-safe F10 command/snapshot path. Manual gamepad/mouse playtest still owed.
|
||||||
|
- **Gate C — audio: ROWS IMPLEMENTED in D350**, Music/FX save path headless-verified. Watch↔F10 live and audible checks still owed. Global master gain is a separate, not-yet-implemented M3 item.
|
||||||
|
- **Gate D — watch gameplay: ROWS IMPLEMENTED in D350** for six GE watch booleans; auto-aim and watch look-bit per-file saves headless-verified. Each watch/in-game effect and two-file isolation still need hands-on checks. Screen/Ratio remain a separate product decision.
|
||||||
|
- **Gate E — control style and resets:** design decisions first, then separate work. Avoid exposing a control-style row that the port immediately overwrites. Do not make a global reset wipe per-file watch settings.
|
||||||
|
|
||||||
|
## Acceptance checks
|
||||||
|
|
||||||
|
- Watch changes show in both PC surfaces without reopening/restarting, and PC changes show in the watch; no extra watch-key values in `ge007.ini`.
|
||||||
|
- With two Bond files, change Music/FX and a gameplay toggle in A via PC UI; switch to B, verify B retains its own values; return to A, start a stage, quit/restart and reload, verify A's values. Repeat an edit via F10 while in-stage. Drag sliders: only one save on release, not per detent. PC-UI persistence is a *port QoL test*, not a claim about N64; watch-only edits should still revert on stage reload. Check that stored controller-style bits survive PC saves and that file copy and erase retain GE behavior (including the erase cross-file edge above).
|
||||||
|
- F10 changes are not lost or applied to another file across a stage/menu transition. Check full-range sliders, watch's save quantization, no unexpected audio spikes, and no race/crash on quit.
|
||||||
|
- Menu layouts: use a dedicated Bond file section/page plus Audio; `MAX_PROWS=15` is a **per-page** cap in `frontoptions.c`, with `MAX_PAGES=8`; F10 scrolls. Check the 440x330 front-end page, F10 visible rows, mouse, keyboard, and real pad navigation; ASCII-only new text for region safety. Check NTSC headless captures, widescreen, golden gate, and PAL/JP build where possible (only NTSC ROM locally).
|
||||||
|
- Build/link checks and the normal verification ritual after implementation. Document any game-side change as its own Rule-2 request before editing it.
|
||||||
|
|
||||||
|
## Decisions and remaining scope
|
||||||
|
|
||||||
|
- **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.
|
||||||
@@ -220,9 +220,12 @@ D338,FIXED,"Input.AimRange (F10 ""Aim range""):",FIXED (2026-09-26 bookkeeping:
|
|||||||
D339,OPEN,"#92 (low-end ~30 fps, Celeron N3060 / HD 400) —","OPEN (2026-09-26, user:"
|
D339,OPEN,"#92 (low-end ~30 fps, Celeron N3060 / HD 400) —","OPEN (2026-09-26, user:"
|
||||||
D341,FIXED,"Intro gun-barrel blood drip draws as static blocks/lines instead of dripping (user, v0.4.0, 2026-09-26; death blood fine).","FIXED (M-201, user-verified 2026-09-27):"
|
D341,FIXED,"Intro gun-barrel blood drip draws as static blocks/lines instead of dripping (user, v0.4.0, 2026-09-26; death blood fine).","FIXED (M-201, user-verified 2026-09-27):"
|
||||||
D342,FIXED,"File select: folder bodies and Bond photos vanish after selecting a file and backing out (user, v0.4.0, 2026-09-26).","FIXED (M-201, user-verified):"
|
D342,FIXED,"File select: folder bodies and Bond photos vanish after selecting a file and backing out (user, v0.4.0, 2026-09-26).","FIXED (M-201, user-verified):"
|
||||||
D343,PARTIAL,"File select: PC ""Options"" label opens the settings overlay (v0.4.0 feature, M1 of the route-B options screen, 2026-09-27); also fixes F10 on file select jumpin…","LANDED (M1): one Rule-2 #ifdef PORT draw hook (user sign-off 2026-09-27); headless + user-verified (mouse, F10, >30 s, widescreen); physical controller check owed."
|
D343,FIXED,"File select: PC ""Options"" label opens the settings overlay (v0.4.0 feature, M1 of the route-B options screen, 2026-09-27); also fixes F10 on file select jumpin…","FIXED (M1, closed): one Rule-2 #ifdef PORT draw hook (user sign-off 2026-09-27); headless + user-verified (mouse, F10, >30 s, widescreen, physical controller on the bottom-bar label and options scree…"
|
||||||
D344,FIXED,Host PC bugchecks (BSOD 0x119 VIDEO_SCHEDULER_INTERNAL_ERROR) during game test runs; probable cause:,"FIXED (probable cause; orderly quit + GE_QUITFRAME), headless + user-verified; no dump, watch for recurrence."
|
D344,FIXED,Host PC bugchecks (BSOD 0x119 VIDEO_SCHEDULER_INTERNAL_ERROR) during game test runs; probable cause:,"FIXED (probable cause; orderly quit + GE_QUITFRAME), headless + user-verified; no dump, watch for recurrence."
|
||||||
D345,FIXED,"Front-end crosshair snaps away from the pointer on screen/page changes (file select → mode select setCursorPOSforMode(0), front.c:2516; every MENU_PC_OPTIONS p…","Port-only, user-verified (parts a–g):"
|
D345,FIXED,"Front-end crosshair snaps away from the pointer on screen/page changes (file select → mode select setCursorPOSforMode(0), front.c:2516; every MENU_PC_OPTIONS p…","Port-only, user-verified (parts a–g):"
|
||||||
D346,PARTIAL,PC-settings wording pass:,Port-only display change landed (optionsoverlay.c rows[]/valueText + one colour match in frontoptions.c); both UIs (F10 overlay + options screen) share the table.
|
D346,PARTIAL,PC-settings wording pass:,Port-only display change landed (optionsoverlay.c rows[]/valueText + one colour match in frontoptions.c); both UIs (F10 overlay + options screen) share the table.
|
||||||
D347,PARTIAL,F10 overlay controller:,Port-only fix landed (optionsoverlay.c):
|
D347,FIXED,F10 overlay controller:,"FIXED (port-only, optionsoverlay.c):"
|
||||||
D348,OPEN,"File-select ""PC Options"" label:",Port-only (frontoptions.c):
|
D348,OPEN,"File-select ""PC Options"" label:",Port-only (frontoptions.c):
|
||||||
|
D349,PARTIAL,GE watch settings are not normally saved to the Bond file:,ANALYZED (static decomp audit):
|
||||||
|
D350,PARTIAL,Watch-backed PC options:,"PARTIAL (implemented; user reports tests good, review pending):"
|
||||||
|
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."
|
||||||
|
+43
-8
@@ -628,12 +628,15 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
|
|||||||
| D339 | **#92 (low-end ~30 fps, Celeron N3060 / HD 400) — measured CPU budget: game thread ~0.2–0.35 ms/frame, render-thread display-list pass 0.4–1.8 ms/frame on this box; CPU very likely not the bottleneck. New `GE_PERFSTAT=1` probe (render phases, tris/batches, game-thread busy, GPU timer query).** — full `## D339` entry at file tail | OPEN (2026-09-26, user: review toward the end of v0.4.0; needs a test machine with similar HD 400-class hardware). Earlier: INVESTIGATING — probe landed; target-hardware numbers needed before any fix. |
|
| D339 | **#92 (low-end ~30 fps, Celeron N3060 / HD 400) — measured CPU budget: game thread ~0.2–0.35 ms/frame, render-thread display-list pass 0.4–1.8 ms/frame on this box; CPU very likely not the bottleneck. New `GE_PERFSTAT=1` probe (render phases, tris/batches, game-thread busy, GPU timer query).** — full `## D339` entry at file tail | OPEN (2026-09-26, user: review toward the end of v0.4.0; needs a test machine with similar HD 400-class hardware). Earlier: INVESTIGATING — probe landed; target-hardware numbers needed before any fix. |
|
||||||
| D341 | **Intro gun-barrel blood drip draws as static blocks/lines instead of dripping (user, v0.4.0, 2026-09-26; death blood fine).** — full `## D341` entry at file tail | FIXED (M-201, user-verified 2026-09-27): two fast3d defects — the 1-cycle blood texrect sampled a stale tile-1 declaration via the D236 LOD rule, and the in-place-regenerated dyn-pool texture hit a stale address-keyed cache entry. |
|
| D341 | **Intro gun-barrel blood drip draws as static blocks/lines instead of dripping (user, v0.4.0, 2026-09-26; death blood fine).** — full `## D341` entry at file tail | FIXED (M-201, user-verified 2026-09-27): two fast3d defects — the 1-cycle blood texrect sampled a stale tile-1 declaration via the D236 LOD rule, and the in-place-regenerated dyn-pool texture hit a stale address-keyed cache entry. |
|
||||||
| D342 | **File select: folder bodies and Bond photos vanish after selecting a file and backing out (user, v0.4.0, 2026-09-26).** — full `## D342` entry at file tail | FIXED (M-201, user-verified): scratch placed at the N64 offset +0xA000 of `ptr_logo_and_walletbond_DL` landed inside the larger PC wallet model; moved past its 0x17000 reservation. |
|
| D342 | **File select: folder bodies and Bond photos vanish after selecting a file and backing out (user, v0.4.0, 2026-09-26).** — full `## D342` entry at file tail | FIXED (M-201, user-verified): scratch placed at the N64 offset +0xA000 of `ptr_logo_and_walletbond_DL` landed inside the larger PC wallet model; moved past its 0x17000 reservation. |
|
||||||
| D343 | **File select: PC "Options" label opens the settings overlay (v0.4.0 feature, M1 of the route-B options screen, 2026-09-27); also fixes F10 on file select jumping to the legal screen after 30 s.** — full `## D343` entry at file tail | LANDED (M1): one Rule-2 `#ifdef PORT` draw hook (user sign-off 2026-09-27); headless + user-verified (mouse, F10, >30 s, widescreen); physical controller check owed. |
|
| D343 | **File select: PC "Options" label opens the settings overlay (v0.4.0 feature, M1 of the route-B options screen, 2026-09-27); also fixes F10 on file select jumping to the legal screen after 30 s.** — full `## D343` entry at file tail | FIXED (M1, closed): one Rule-2 `#ifdef PORT` draw hook (user sign-off 2026-09-27); headless + user-verified (mouse, F10, >30 s, widescreen, physical controller on the bottom-bar label and options screen). |
|
||||||
| D344 | **Host PC bugchecks (BSOD 0x119 VIDEO_SCHEDULER_INTERNAL_ERROR) during game test runs; probable cause: quit called exit() while the render thread could be inside the GL driver (2026-09-27).** — full `## D344` entry at file tail | FIXED (probable cause; orderly quit + GE_QUITFRAME), headless + user-verified; no dump, watch for recurrence. |
|
| D344 | **Host PC bugchecks (BSOD 0x119 VIDEO_SCHEDULER_INTERNAL_ERROR) during game test runs; probable cause: quit called exit() while the render thread could be inside the GL driver (2026-09-27).** — full `## D344` entry at file tail | FIXED (probable cause; orderly quit + GE_QUITFRAME), headless + user-verified; no dump, watch for recurrence. |
|
||||||
| D345 | **Front-end crosshair snaps away from the pointer on screen/page changes (file select → mode select `setCursorPOSforMode(0)`, front.c:2516; every `MENU_PC_OPTIONS` page hop), and the options screen's keyboard navigation was missing/mis-modelled (2026-09-27).** — full `## D345` entry at file tail | Port-only, user-verified (parts a–g): (a) `input.c` re-assert window on menu change; (b) `cursorToItem` skips its snap while the pointer is live; (d) W/S emit stick in menus under NaturalPitch; (e/f) F10-style discrete row stepping + arrow left/right value adjust replace raw drift (first pass shipped broken, fixed); (g) screen-placed cursor parks in the left gutter, off the row text. Game-side one-liner (pad users) parked pending Rule-2 sign-off. |
|
| D345 | **Front-end crosshair snaps away from the pointer on screen/page changes (file select → mode select `setCursorPOSforMode(0)`, front.c:2516; every `MENU_PC_OPTIONS` page hop), and the options screen's keyboard navigation was missing/mis-modelled (2026-09-27).** — full `## D345` entry at file tail | Port-only, user-verified (parts a–g): (a) `input.c` re-assert window on menu change; (b) `cursorToItem` skips its snap while the pointer is live; (d) W/S emit stick in menus under NaturalPitch; (e/f) F10-style discrete row stepping + arrow left/right value adjust replace raw drift (first pass shipped broken, fixed); (g) screen-placed cursor parks in the left gutter, off the row text. Game-side one-liner deferred: physical-controller pass was good, no Rule-2 sign-off sought. |
|
||||||
| D346 | **PC-settings wording pass: option labels/values aligned to Nightdive/Turok + PD-port conventions (title-case On/Off, "Frame rate cap", "Anti-aliasing"/"None", units in values not labels, self-explanatory auto-FOV toggles); VIEW section merged into VIDEO (2026-09-27).** — full `## D346` entry at file tail | Port-only display change landed (`optionsoverlay.c` rows[]/valueText + one colour match in `frontoptions.c`); both UIs (F10 overlay + options screen) share the table. Config keys/values untouched. Deadzone % display uses raw-step grid (cosmetic follow-up noted). |
|
| D346 | **PC-settings wording pass: option labels/values aligned to Nightdive/Turok + PD-port conventions (title-case On/Off, "Frame rate cap", "Anti-aliasing"/"None", units in values not labels, self-explanatory auto-FOV toggles); VIEW section merged into VIDEO (2026-09-27).** — full `## D346` entry at file tail | Port-only display change landed (`optionsoverlay.c` rows[]/valueText + one colour match in `frontoptions.c`); both UIs (F10 overlay + options screen) share the table. Config keys/values untouched. Deadzone % display uses raw-step grid (cosmetic follow-up noted). |
|
||||||
| D347 | **F10 overlay controller: left/right did nothing (only A/Y adjusted values) and no input had hold-to-repeat (2026-09-27).** — full `## D347` entry at file tail | Port-only fix landed (`optionsoverlay.c`): D-pad left/right + stick-X wired into value adjust (A/X/B/Y kept); 18/4-frame hold-to-repeat on adjust and up/down nav, matching the options screen's D345(e)/(f) cadence. Real-device controller retest owed. |
|
| D347 | **F10 overlay controller: left/right did nothing (only A/Y adjusted values) and no input had hold-to-repeat (2026-09-27).** — full `## D347` entry at file tail | FIXED (port-only, `optionsoverlay.c`): D-pad left/right + stick-X wired into value adjust (A/X/B/Y kept); 18/4-frame hold-to-repeat on adjust and up/down nav, matching the options screen's D345(e)/(f) cadence. User validated physical-controller pass. |
|
||||||
| D348 | **File-select "PC Options" label: no settings icon exists in the game art (mainfolderimages = copy/del/select-arrow/X/check/dot only) -- a mini slider glyph is drawn instead (2026-09-27).** — full `## D348` entry at file tail | Port-only (`frontoptions.c`): three menu-box tracks + offset knobs in the LABEL_GAP left of the label, INK_DIM/label-ink colours, hot region extended. PCDUMP pixel-verified (135 knob px vs 9 paper); visual feel-check owed. |
|
| D348 | **File-select "PC Options" label: no settings icon exists in the game art (mainfolderimages = copy/del/select-arrow/X/check/dot only) -- a mini slider glyph is drawn instead (2026-09-27).** — full `## D348` entry at file tail | Port-only (`frontoptions.c`): three menu-box tracks + offset knobs in the LABEL_GAP left of the label, INK_DIM/label-ink colours, hot region extended. PCDUMP pixel-verified (135 knob px vs 9 paper); visual feel-check owed. |
|
||||||
|
| D349 | **GE watch settings are not normally saved to the Bond file: erase is the only caller of the settings serializer, and stage start reloads the file's old values.** — full `## D349` entry at file tail | ANALYZED (static decomp audit): maintainer approved PC-UI-only per-file persistence with explicit chooser, stored controller-type preservation and release-only slider writes; watch UI stays session-only. Erase cross-file edge noted. See `docs/dev/WATCH-SETTINGS-PLAN.md`. |
|
||||||
|
| D350 | **Watch-backed PC options: shared front/F10 Bond-file rows, explicit chooser, game-thread F10 bridge and field-scoped PC-only persistence.** — full `## D350` entry at file tail | PARTIAL (implemented; user reports tests good, review pending): headless front + F10 write/reload probes, NTSC build/golden 3/3, save CRC/control-style preserved; two-file targeting checked. Detailed edge-case coverage not specified by user. |
|
||||||
|
| 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. |
|
||||||
|
|
||||||
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
|
||||||
@@ -13584,7 +13587,7 @@ Implemented as `Input.PdMouseAim` (**default off**, because it changes aim feel)
|
|||||||
- **Not verifiable here:** PAL/JP captures, because only the NTSC ROM is present. PAL shares the canvas, strings and constants; JP relies on the dynamic label x.
|
- **Not verifiable here:** PAL/JP captures, because only the NTSC ROM is present. PAL shares the canvas, strings and constants; JP relies on the dynamic label x.
|
||||||
- **Owed:** user playtest (mouse, keyboard, pad; music/ambience on entering and leaving; "PC Options" sits close to the right edge, ending about x 428 of 440); a physical controller.
|
- **Owed:** user playtest (mouse, keyboard, pad; music/ambience on entering and leaving; "PC Options" sits close to the right edge, ending about x 428 of 440); a physical controller.
|
||||||
|
|
||||||
**Closed 2026-09-27.** User visual pass done: label placement accepted, mini slider glyph added beside it (D348), screen navigation fixed and verified (D345), wording aligned to Turok/Nightdive (D346/D346b); music on enter/leave unremarked. NTSC reference frames captured + pixel-verified headless (PCDUMP, incl. the D348 glyph: 135 knob px vs 9 paper). PAL/JP visual verification not capturable — only an NTSC ROM is present locally; layout is font-width-driven and region-independent except label width, so risk is low. Physical-controller pass on the bottom-row label + row-stepping still owed to the user (tracked in HANDOFF).
|
**Closed 2026-09-27.** User visual pass done: label placement accepted, mini slider glyph added beside it (D348), screen navigation fixed and verified (D345), wording aligned to Turok/Nightdive (D346/D346b); music on enter/leave unremarked. NTSC reference frames captured + pixel-verified headless (PCDUMP, incl. the D348 glyph: 135 knob px vs 9 paper). PAL/JP visual verification not capturable — only an NTSC ROM is present locally; layout is font-width-driven and region-independent except label width, so risk is low. Subsequent user physical-controller pass on the bottom-bar label and options-screen navigation: all good.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -13636,7 +13639,7 @@ Implemented as `Input.PdMouseAim` (**default off**, because it changes aim feel)
|
|||||||
|
|
||||||
**D345(f) — D345(e) shipped broken (user: "W/S still just move the crosshair").** The D345(e) edit added the stepper but its two *removals* never landed: the D345(c) `frontUpdateControlStickPosition()` call (after `buildPages()`) and its extern stayed in `frontoptions.c`, so W/S ran raw cursor drift on top of the row stepper. The build didn't catch it: with the extern gone from my intended edit but present in the file, compilation only produced an implicit-declaration warning, which this project compiles through. Fix: removed the call + extern (verified by grep, not just the edit tool's success message), and added stick-X to the left/right value block so Left/Right arrows adjust values like F10 (A/D letters already worked via C-buttons). Lesson recorded: after any edit that both adds and removes, grep-verify the removals against the file.
|
**D345(f) — D345(e) shipped broken (user: "W/S still just move the crosshair").** The D345(e) edit added the stepper but its two *removals* never landed: the D345(c) `frontUpdateControlStickPosition()` call (after `buildPages()`) and its extern stayed in `frontoptions.c`, so W/S ran raw cursor drift on top of the row stepper. The build didn't catch it: with the extern gone from my intended edit but present in the file, compilation only produced an implicit-declaration warning, which this project compiles through. Fix: removed the call + extern (verified by grep, not just the edit tool's success message), and added stick-X to the left/right value block so Left/Right arrows adjust values like F10 (A/D letters already worked via C-buttons). Lesson recorded: after any edit that both adds and removes, grep-verify the removals against the file.
|
||||||
|
|
||||||
**Not changed:** the game-side snap itself (`front.c:2516`) is untouched — no Rule-2 sign-off needed for this fix. The optional one-liner `setCursorPOSforMode(gamemode >= 0 ? gamemode : 0)` (matching its siblings) remains a separate parked item; its main beneficiary is pad users, who have no re-assert at all.
|
**Not changed:** the game-side snap itself (`front.c:2516`) is untouched — no Rule-2 sign-off needed for this fix. The optional one-liner `setCursorPOSforMode(gamemode >= 0 ? gamemode : 0)` (matching its siblings) was considered for pad users, who have no re-assert at all. Following a successful physical-controller pass, the maintainer chose to defer it; do not change game code without fresh Rule-2 sign-off.
|
||||||
|
|
||||||
**User-verified (2026-09-27):** folder-page snap gone (mouse); options screen entry/section hops keep the pointer under the mouse; sliders/toggles fine; F10-style W/S + arrow navigation with hold-to-repeat works; left/right arrows adjust values; crosshair parks in the gutter off the row text. Resolved for time being.
|
**User-verified (2026-09-27):** folder-page snap gone (mouse); options screen entry/section hops keep the pointer under the mouse; sliders/toggles fine; F10-style W/S + arrow navigation with hold-to-repeat works; left/right arrows adjust values; crosshair parks in the gutter off the row text. Resolved for time being.
|
||||||
|
|
||||||
@@ -13732,9 +13735,8 @@ Toggles flip on either adjust direction (existing `rowAdjust` behaviour);
|
|||||||
sliders step by their registered step; MSAA/enum wrap as before.
|
sliders step by their registered step; MSAA/enum wrap as before.
|
||||||
|
|
||||||
**Verification:** `build-pc.sh ntsc-final` clean; headless boot smoke clean.
|
**Verification:** `build-pc.sh ntsc-final` clean; headless boot smoke clean.
|
||||||
Controller feel is owed a real-device retest (headless can't drive the pad):
|
Subsequent user physical-controller pass validated the F10 overlay, options
|
||||||
hold ←/→ on a slider, hold ↑/↓ to traverse, confirm A/X/B/Y still work and
|
screen and bottom-bar label; all good. No new automated controller test was run.
|
||||||
nothing reaches the game while the overlay is open.
|
|
||||||
|
|
||||||
## D348 — File-select "PC Options" label gets a mini slider glyph (2026-09-27)
|
## D348 — File-select "PC Options" label gets a mini slider glyph (2026-09-27)
|
||||||
|
|
||||||
@@ -13759,3 +13761,36 @@ extends left to include the glyph. No new strings (issue #87).
|
|||||||
glyph box (logical x 339–351, y 278–292) contains 135 pure-white pixels — the
|
glyph box (logical x 339–351, y 278–292) contains 135 pure-white pixels — the
|
||||||
three knobs — vs 9 on an equivalent empty-paper strip. Visual feel-check owed
|
three knobs — vs 9 on an equivalent empty-paper strip. Visual feel-check owed
|
||||||
to the user (proportions/knob offsets are one-constant tweaks if it looks off).
|
to the user (proportions/knob offsets are one-constant tweaks if it looks off).
|
||||||
|
|
||||||
|
## D349 — Watch options are session-only in normal GE play (static audit)
|
||||||
|
|
||||||
|
**Ground truth:** `fileSaveSettingsForFolder` (`src/game/file2.c:1246`) serializes live Music/FX and the eight watch options into a supplied `save_data`; its only call site is `fileClearSavefileForFolder` (`:1379`), reached by `deleteCurrentSelectedFolder` (`src/game/file.c:84`, the Erase path). The normal watch controls (`options.c`) update live state, not the save. At stage start `init_watch_at_start_of_stage` (`options.c:415`) calls `fileLoadSaveSettingsForSelectedFolder`, which reloads the file values (`file2.c:1308-1350`) and discards unsaved watch adjustments. A repository search found no other assignments to `save->music_vol`, `save->sfx_vol` or `save->options` from live watch state. This is a decomp/N64 quirk, not a PC regression; recheck any indirect write path before implementing persistence.
|
||||||
|
|
||||||
|
**Consequences for PC options:** editable watch-backed rows on file select would revert on stage entry without an extra save path. The maintainer approved a deliberate PC-only QoL divergence: **edits in the PC settings/F10 UIs persist to the explicitly chosen/active Bond file; edits made directly in GE's watch stay session-only (faithful to N64)**. Preserve the N64 game/save format and verify per-file isolation. The agreed design is `docs/dev/WATCH-SETTINGS-PLAN.md`.
|
||||||
|
|
||||||
|
**Save hazards to cover in implementation:** `fileSaveSettingsForFolder` serializes *all* live options, including `OPTION_CONTROLTYPE`. Since the port forces Honey/Solitaire in `input.c`, keep the original stored control-type bits. Serialize into a temporary copy, merge **only the PC-UI-edited field/bit(s)** into the actual slot pointer returned by `fileGetSaveForFoldernum` (folder number is not a `saves[]` slot index), then call `fileWriteSave`. This also keeps unrelated watch-only edits session-only when a PC-UI value is later changed. Toggle/enum edits commit on change; slider drags commit on release. GE's folder loader would change front-end Music/FX volume live; D350's chooser reads the saved fields directly instead, avoiding this side effect. Erase (`fileClearSavefileForFolder`) serializes *current live* settings into the wiped save: editing B, then erasing A may put B's live watch settings into wiped A. The same edge exists on N64 after changing watch settings; acknowledge it in tests, do not change game logic. D350's front-end chooser avoids loading B into live state, so editing B there does not itself trigger this edge.
|
||||||
|
|
||||||
|
**Defaults:** `BLANKSAVEDATA` (`src/bondconstants.h:2151`) starts `music_vol`/`sfx_vol` at 0xFF and `options` at `DEFAULT_OPTIONS` (`src/game/file2.h:23`: auto-aim, sight, look-ahead, ammo on). No code change or runtime verification in this finding.
|
||||||
|
|
||||||
|
## D350 — Watch-backed PC settings: shared rows, explicit chooser and per-file save (2026-09-27)
|
||||||
|
|
||||||
|
**Port-only, no `src/game` changes.** `port/src/optionsoverlay.c` adds an Audio (Bond File) section (Music/FX sliders) and a Bond File page (explicit file chooser + watch look direction, auto-aim, aim control, sight, look-ahead, ammo). These rows are shared by F10 and `MENU_PC_OPTIONS`; control style, GE Screen/Ratio and unrelated M3 items remain deferred. `watchSettingsFieldForKey` routes them to `port/src/watchsettings.c` rather than `config.c`/ge007.ini. Empty/unvalidated files cannot be edited; the chooser is independent of `selected_folder_num`, skips absent saves, and reads save fields directly rather than loading a folder and changing front-end audio. In a running level, F10 targets the active file and reads a game-thread-published snapshot so direct watch changes appear next frame. The watch's own edits remain session-only.
|
||||||
|
|
||||||
|
**Thread handoff:** F10's scheduler-thread input queues bounded `(folder, field, value, commit)` requests under an SDL spinlock (not a GE state mutation). `port/src/libultra.c` drains these on successful `gfxFrameMsgQ` receives, *after* unlocking the OS queue mutex, on `boss.c`'s game thread; the only consumer of that queue is the boss main loop. The game thread checks stage and target file identity before using GE's setters or EEPROM, updates the snapshot, and discards stale cross-file requests. It also supports direct `-level_XX` boots, where `current_menu` stays `MENU_INVALID` despite an active level. F10's file chooser is hidden; the front screen has a separate explicit chooser.
|
||||||
|
|
||||||
|
**Save safety:** `fileGetSaveForFoldernum` alone may return a zero-filled phantom slot if `GE_STARTMENU` skipped `fileValidateSaves` (D299). `validSave` verifies GE's CRC before showing/editing a file. For F10, `fileSaveSettingsForFolder` serializes into a temporary copy and only the PC-edited Music/FX byte or watch bit is merged into the real physical save slot, then `fileWriteSave` updates CRC/EEPROM. In the front screen (no active player), the same single field is edited directly on the explicitly chosen save without invoking the folder loader; front-end audio levels do not preview the selected file's new Music/FX value until the stage loads it. The stored controller-type bits and unrelated watch-only changes stay untouched. Toggles and discrete value steps persist on change; mouse-drag sliders update their displayed value (and apply GE audio live in-stage through F10) but persist once on release (or F10 close/back). F10 preview and commit use the game-thread queue; no save per drag detent.
|
||||||
|
|
||||||
|
**Verification (native Windows, backed-up ini and EEPROM restored after each probe; temporary test hooks removed):**
|
||||||
|
- Build/link clean; `bash tools_pc/golden_gate.sh` 3/3, orderly self-quit. `GE_STARTMENU=26 GE_QUITFRAME=320` shows the new screen and quits cleanly; `-level_09 GE_OPTIONSOVERLAY=1 GE_QUITFRAME=500` opens F10 cleanly.
|
||||||
|
- A `GE_STARTMENU=26` scripted chooser attempted to edit file 1 without prior save validation: CRC guard refused, **EEPROM byte-identical**. This prevents D299-class writes to phantom slot 0.
|
||||||
|
- With test-only save validation injected at options init, `GE_INPUTSCRIPT` navigated to Bond File, selected file 1, changed Watch look: EEPROM slot 0 options `0x013a→0x013b`, only option byte + CRC changed. Separately selected file 1, entered Audio, decremented Music once: `music_vol 255→254`, FX unchanged, only music byte + CRC changed. Neither temporary hook ships.
|
||||||
|
- A second front-end script chose Bond file 1 then switched to Bond file 2 and changed Look. Only physical EEPROM **slot 4** changed (the file-2 save lives there; file number is not slot index), while slot 0/file 1's options stayed `0x013a` untouched. This confirms basic two-file targeting; copy/erase and rapid transitions remain manual checks.
|
||||||
|
- Game-thread `-level_09` temporary probe toggled Auto-aim and persisted: options `0x013a→0x0138`, original `0x0100` control-style bits unchanged, only option byte + eight CRC bytes changed. A second process loaded Auto-aim=0 from the same test EEPROM. A temporary scheduler-thread/F10 queue probe produced the same targeted EEPROM change using the game-thread handoff. All test EEPROM copies were restored; neither probe ships.
|
||||||
|
|
||||||
|
**User feedback:** maintainer reports "it tests good" and requested a commit before a new-session review. No per-case playtest notes were supplied, so do not infer that every edge case below was exercised.
|
||||||
|
|
||||||
|
**For the review:** confirm mouse/pad chooser and both settings surfaces, watch↔F10 live changes, slider drag/release and rapid close/stage transitions; front-end audio slider preview feel (edits persist but do not apply to current menu audio); two-file switch/copy/erase. PAL/JP build/visual checks remain uncapturable locally (only NTSC ROM).
|
||||||
|
|
||||||
|
## D351 — D346 unit-row initializers shifted into `hidePtr` (2026-09-27)
|
||||||
|
|
||||||
|
The existing positional `struct Row` initializers for FOV, anisotropy, draw/LOD distance, HUD scale, deadzone and trigger threshold omitted the `hidePtr` placeholder. GCC warned about assigning a `char *` unit ("%"/"x") to `int *hidePtr` and assigning deadzone's numeric display divisor to `unit`. The visible-list builder dereferences non-null hidePtr, so these rows were hidden or misformatted despite D346's intended unit labels. Corrected *only* these rows to designated initializers, preserving their key, label, clamp, step and intended `hiddenIfOn`/unit/divisor. New watch rows also use designated initializers to prevent repeats. Build has no new optionsoverlay warnings; headless menu smoke/golden gate clean. Maintainer reports the implementation tests good, but did not separately report a unit-label/auto-hide visual check; carry that into the new-session review.
|
||||||
@@ -54,6 +54,7 @@ int optionsRowIsSlider(int i);
|
|||||||
int optionsRowNeedsRestart(int i);
|
int optionsRowNeedsRestart(int i);
|
||||||
double optionsRowFraction(int i);
|
double optionsRowFraction(int i);
|
||||||
void optionsRowSetFraction(int i, double f);
|
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 optionsRowValueText(int i, char *out, int n);
|
||||||
void optionsRowAdjust(int i, int dir);
|
void optionsRowAdjust(int i, int dir);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#ifndef PORT_WATCHSETTINGS_H
|
||||||
|
#define PORT_WATCHSETTINGS_H
|
||||||
|
|
||||||
|
/* D349: one source of truth for Bond-file-backed GE settings. All GE state and
|
||||||
|
* EEPROM access runs on the game thread. F10 sends requests to that thread. */
|
||||||
|
enum WatchSettingField {
|
||||||
|
WATCH_SETTING_MUSIC,
|
||||||
|
WATCH_SETTING_FX,
|
||||||
|
WATCH_SETTING_LOOK,
|
||||||
|
WATCH_SETTING_AUTOAIM,
|
||||||
|
WATCH_SETTING_AIMCONTROL,
|
||||||
|
WATCH_SETTING_SIGHT,
|
||||||
|
WATCH_SETTING_LOOKAHEAD,
|
||||||
|
WATCH_SETTING_AMMO,
|
||||||
|
WATCH_SETTING_COUNT
|
||||||
|
};
|
||||||
|
|
||||||
|
int watchSettingsFieldForKey(const char *key); /* -1 for non-watch row */
|
||||||
|
int watchSettingsFolder(void); /* explicit front chooser; -1 = none */
|
||||||
|
void watchSettingsChooseFile(int dir); /* game thread only */
|
||||||
|
int watchSettingsAvailable(void);
|
||||||
|
int watchSettingsRead(enum WatchSettingField field); /* front: saved file, F10: snapshot */
|
||||||
|
void watchSettingsSet(enum WatchSettingField field, int value, int commit);
|
||||||
|
void watchSettingsCommit(enum WatchSettingField field); /* slider release */
|
||||||
|
void watchSettingsGameTick(void); /* game thread; after gfxFrameMsgQ receive */
|
||||||
|
int watchSettingsPersistField(int folder, enum WatchSettingField field);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -248,6 +248,7 @@ void frontOptionsMenuUpdate(void)
|
|||||||
static void goBack(void)
|
static void goBack(void)
|
||||||
{
|
{
|
||||||
playSfx(DOOR_METAL_CLOSE2_SFX);
|
playSfx(DOOR_METAL_CLOSE2_SFX);
|
||||||
|
if (s_dragRow >= 0) optionsRowCommit(s_dragRow);
|
||||||
s_dragRow = -1;
|
s_dragRow = -1;
|
||||||
if (s_level == 1) {
|
if (s_level == 1) {
|
||||||
s_level = 0;
|
s_level = 0;
|
||||||
@@ -322,6 +323,7 @@ void frontOptionsMenuInterface(void)
|
|||||||
if (joyGetButtons(PLAYER_1, A_BUTTON | Z_TRIG)) {
|
if (joyGetButtons(PLAYER_1, A_BUTTON | Z_TRIG)) {
|
||||||
optionsRowSetFraction(s_dragRow, ((double)cursor_h_pos - BAR_X0) / (BAR_X1 - BAR_X0));
|
optionsRowSetFraction(s_dragRow, ((double)cursor_h_pos - BAR_X0) / (BAR_X1 - BAR_X0));
|
||||||
} else {
|
} else {
|
||||||
|
optionsRowCommit(s_dragRow);
|
||||||
s_dragRow = -1;
|
s_dragRow = -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,7 @@
|
|||||||
#include "input.h"
|
#include "input.h"
|
||||||
#include "fs.h"
|
#include "fs.h"
|
||||||
#include "romdata.h"
|
#include "romdata.h"
|
||||||
|
#include "watchsettings.h"
|
||||||
#include "crash.h"
|
#include "crash.h"
|
||||||
|
|
||||||
#if defined(PLATFORM_WINDOWS)
|
#if defined(PLATFORM_WINDOWS)
|
||||||
@@ -706,6 +707,10 @@ s32 osRecvMesg(OSMesgQueue *mq, OSMesg *msg, s32 flag)
|
|||||||
d60logRecv(mq, m); /* TEMP D60 */
|
d60logRecv(mq, m); /* TEMP D60 */
|
||||||
pthread_cond_signal(&pq->cond);
|
pthread_cond_signal(&pq->cond);
|
||||||
pthread_mutex_unlock(&pq->lock);
|
pthread_mutex_unlock(&pq->lock);
|
||||||
|
/* gfxFrameMsgQ is consumed only by boss.c's game thread. Apply queued
|
||||||
|
* F10 watch edits there, after releasing the OS queue lock; the SDL
|
||||||
|
* input/scheduler and render threads never touch GE watch/save state. */
|
||||||
|
if (mq == &gfxFrameMsgQ) watchSettingsGameTick();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+98
-20
@@ -38,6 +38,9 @@
|
|||||||
#define _SHIFTR(v, s, w) ((u32)(((u32)(v) >> (s)) & ((0x01 << (w)) - 1)))
|
#define _SHIFTR(v, s, w) ((u32)(((u32)(v) >> (s)) & ((0x01 << (w)) - 1)))
|
||||||
#endif
|
#endif
|
||||||
#include <PR/gbi.h>
|
#include <PR/gbi.h>
|
||||||
|
#include <bondconstants.h>
|
||||||
|
|
||||||
|
extern MENU current_menu;
|
||||||
|
|
||||||
#include "platform.h"
|
#include "platform.h"
|
||||||
#include "system.h"
|
#include "system.h"
|
||||||
@@ -45,6 +48,7 @@
|
|||||||
#include "video.h"
|
#include "video.h"
|
||||||
#include "input.h"
|
#include "input.h"
|
||||||
#include "optionsoverlay.h"
|
#include "optionsoverlay.h"
|
||||||
|
#include "watchsettings.h"
|
||||||
#include "../fast3d/gfx_api.h"
|
#include "../fast3d/gfx_api.h"
|
||||||
|
|
||||||
/* D324 class: -Iinclude resolves <math.h> to GE's N64 stub, which does not
|
/* D324 class: -Iinclude resolves <math.h> to GE's N64 stub, which does not
|
||||||
@@ -69,11 +73,13 @@ extern s16 viGetY(void);
|
|||||||
/* ------------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------------ */
|
||||||
|
|
||||||
enum { ROW_TOGGLE, ROW_SLIDER, ROW_ENUM, ROW_MSAA, ROW_RES, ROW_ACTION, ROW_FPSCAP,
|
enum { ROW_TOGGLE, ROW_SLIDER, ROW_ENUM, ROW_MSAA, ROW_RES, ROW_ACTION, ROW_FPSCAP,
|
||||||
ROW_HEADER /* D237: non-selectable category label */ };
|
ROW_HEADER, ROW_BOND_FILE /* explicit chooser (front end only) */ };
|
||||||
|
|
||||||
/* D346: wording pass -- Nightdive/Turok + PD-port conventions: title-case
|
/* D346: wording pass -- Nightdive/Turok + PD-port conventions: title-case
|
||||||
* On/Off, no all-caps value strings. Display-only; config stores 0/1 either way. */
|
* On/Off, no all-caps value strings. Display-only; config stores 0/1 either way. */
|
||||||
static const char *const kOnOff[] = { "Off", "On", NULL };
|
static const char *const kOnOff[] = { "Off", "On", NULL };
|
||||||
|
static const char *const kReverse[] = { "Reverse", "Upright", NULL };
|
||||||
|
static const char *const kHold[] = { "Hold", "Toggle", NULL };
|
||||||
static const char *const kTexFilter[] = { "Nearest", "Bilinear", "3-Point", NULL };
|
static const char *const kTexFilter[] = { "Nearest", "Bilinear", "3-Point", NULL };
|
||||||
static const char *const kAimMode[] = { "N64", "Centred (PC)", NULL }; /* D337 */
|
static const char *const kAimMode[] = { "N64", "Centred (PC)", NULL }; /* D337 */
|
||||||
static const char *const kAimRange[] = { "PC", "N64", NULL }; /* D338 */
|
static const char *const kAimRange[] = { "PC", "N64", NULL }; /* D338 */
|
||||||
@@ -105,6 +111,8 @@ static const int kResList[][2] = {
|
|||||||
static int s_resFit[NUM_RES]; /* indices into kResList that fit the desktop */
|
static int s_resFit[NUM_RES]; /* indices into kResList that fit the desktop */
|
||||||
static int s_resFitN = 0;
|
static int s_resFitN = 0;
|
||||||
static int s_resSel = 0; /* index into s_resFit */
|
static int s_resSel = 0; /* index into s_resFit */
|
||||||
|
static int s_dragRow = -1; /* scheduler-thread mouse drag */
|
||||||
|
static SDL_atomic_t s_dragWatchField; /* field+1, host may close F10 mid-drag */
|
||||||
|
|
||||||
struct Row {
|
struct Row {
|
||||||
const char *key;
|
const char *key;
|
||||||
@@ -141,19 +149,19 @@ static struct Row rows[] = {
|
|||||||
{ "Video.FpsCap", "Frame rate cap", ROW_FPSCAP, 0, NULL, 0, 0, 0, 0,0,0,0,0 },
|
{ "Video.FpsCap", "Frame rate cap", ROW_FPSCAP, 0, NULL, 0, 0, 0, 0,0,0,0,0 },
|
||||||
{ "Video.MSAA", "Anti-aliasing", ROW_MSAA, 0, NULL, 1, 0, 0, 0,0,0,0,0 },
|
{ "Video.MSAA", "Anti-aliasing", ROW_MSAA, 0, NULL, 1, 0, 0, 0,0,0,0,0 },
|
||||||
{ "Video.TextureFilter", "Texture filter", ROW_ENUM, 1, kTexFilter, 0, 0, 0, 0,0,0,0,0 },
|
{ "Video.TextureFilter", "Texture filter", ROW_ENUM, 1, kTexFilter, 0, 0, 0, 0,0,0,0,0 },
|
||||||
{ "Video.Anisotropy", "Anisotropic filtering",ROW_SLIDER, 1, NULL, 0, 0, 0, 0,0,0,0,0, NULL, "x" },
|
{ .key="Video.Anisotropy", .label="Anisotropic filtering", .kind=ROW_SLIDER, .step=1, .unit="x" },
|
||||||
/* D346b: the old VIEW section (FOV / widescreen / draw distance) is merged
|
/* D346b: the old VIEW section (FOV / widescreen / draw distance) is merged
|
||||||
* into VIDEO -- one Nightdive-style "video" section; rows keep order. */
|
* into VIDEO -- one Nightdive-style "video" section; rows keep order. */
|
||||||
{ "Video.FovScale", "FOV scale", ROW_SLIDER, 5, NULL, 0, 0, 0, 0,0,0,0,0, NULL, "%" },
|
{ .key="Video.FovScale", .label="FOV scale", .kind=ROW_SLIDER, .step=5, .unit="%" },
|
||||||
/* D334: native widescreen (world projected at the window aspect, Hor+).
|
/* D334: native widescreen (world projected at the window aspect, Hor+).
|
||||||
* While on, "Widescreen auto FOV" has no effect (it was the stretch-era
|
* While on, "Widescreen auto FOV" has no effect (it was the stretch-era
|
||||||
* vertical-FOV compensation). */
|
* vertical-FOV compensation). */
|
||||||
{ "Video.NativeWidescreen", "Native widescreen",ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 },
|
{ "Video.NativeWidescreen", "Native widescreen",ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 },
|
||||||
{ "Video.WidescreenAuto", "Widescreen auto FOV",ROW_TOGGLE,1, kOnOff, 0, 0, 0, 0,0,0,0,0 },
|
{ "Video.WidescreenAuto", "Widescreen auto FOV",ROW_TOGGLE,1, kOnOff, 0, 0, 0, 0,0,0,0,0 },
|
||||||
{ "Video.SafeAreaCrop", "Crop overscan", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 },
|
{ "Video.SafeAreaCrop", "Crop overscan", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 },
|
||||||
{ "Video.DrawDistance", "Draw distance", ROW_SLIDER, 25, NULL, 0, 0, 0, 0,0,0,0,0, "Video.DrawDistanceAutoFov", "%" },
|
{ .key="Video.DrawDistance", .label="Draw distance", .kind=ROW_SLIDER, .step=25, .hiddenIfOn="Video.DrawDistanceAutoFov", .unit="%" },
|
||||||
{ "Video.DrawDistanceAutoFov","Draw dist. follows FOV",ROW_TOGGLE,1,kOnOff, 0, 0, 0, 0,0,0,0,0 },
|
{ "Video.DrawDistanceAutoFov","Draw dist. follows FOV",ROW_TOGGLE,1,kOnOff, 0, 0, 0, 0,0,0,0,0 },
|
||||||
{ "Video.LodDistance", "LOD distance", ROW_SLIDER, 25, NULL, 0, 0, 0, 0,0,0,0,0, "Video.LodDistanceAutoFov", "%" },
|
{ .key="Video.LodDistance", .label="LOD distance", .kind=ROW_SLIDER, .step=25, .hiddenIfOn="Video.LodDistanceAutoFov", .unit="%" },
|
||||||
{ "Video.LodDistanceAutoFov", "LOD dist. follows FOV",ROW_TOGGLE,1,kOnOff, 0, 0, 0, 0,0,0,0,0 },
|
{ "Video.LodDistanceAutoFov", "LOD dist. follows FOV",ROW_TOGGLE,1,kOnOff, 0, 0, 0, 0,0,0,0,0 },
|
||||||
/* Aim row edits Input.AimModeSens -- the knob the default GEPD aim path
|
/* Aim row edits Input.AimModeSens -- the knob the default GEPD aim path
|
||||||
* actually uses (Input.MouseAimSpeed only feeds the legacy velocity-stick
|
* actually uses (Input.MouseAimSpeed only feeds the legacy velocity-stick
|
||||||
@@ -184,6 +192,25 @@ static struct Row rows[] = {
|
|||||||
/* { "Input.AimModeSens", "Mouse aim speed", ROW_SLIDER, 5, NULL, 0, 0, 0, 0,0,0,0,0 }, */
|
/* { "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.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 }, */
|
/* { "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 },
|
||||||
|
{ .key="__HdrBondAudio", .label="AUDIO (BOND FILE)", .kind=ROW_HEADER, .found=1 },
|
||||||
|
{ .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 },
|
||||||
{ "__HdrMouse", "MOUSE / AIM", ROW_HEADER, 0, NULL, 0, 0, 0, 0,0,0,0,0 },
|
{ "__HdrMouse", "MOUSE / AIM", ROW_HEADER, 0, NULL, 0, 0, 0, 0,0,0,0,0 },
|
||||||
{ "Input.MouseSensitivity", "Mouse sensitivity",ROW_SLIDER, 5, NULL, 0, 0, 0, 0,0,0,0,0 },
|
{ "Input.MouseSensitivity", "Mouse sensitivity",ROW_SLIDER, 5, NULL, 0, 0, 0, 0,0,0,0,0 },
|
||||||
{ "Input.MouseInvertY", "Invert look (mouse)",ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 },
|
{ "Input.MouseInvertY", "Invert look (mouse)",ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 },
|
||||||
@@ -205,8 +232,8 @@ static struct Row rows[] = {
|
|||||||
{ "Input.AimRange", "Aim range", ROW_ENUM, 1, kAimRange, 0, 0, 0, 0,0,0,0,0, "Input.AimMode" },
|
{ "Input.AimRange", "Aim range", ROW_ENUM, 1, kAimRange, 0, 0, 0, 0,0,0,0,0, "Input.AimMode" },
|
||||||
{ "__HdrPad", "CONTROLLER", ROW_HEADER, 0, NULL, 0, 0, 0, 0,0,0,0,0 },
|
{ "__HdrPad", "CONTROLLER", ROW_HEADER, 0, NULL, 0, 0, 0, 0,0,0,0,0 },
|
||||||
{ "Input.PadLookInvertY", "Invert look (controller)",ROW_TOGGLE,1,kOnOff, 0, 0, 0, 0,0,0,0,0 },
|
{ "Input.PadLookInvertY", "Invert look (controller)",ROW_TOGGLE,1,kOnOff, 0, 0, 0, 0,0,0,0,0 },
|
||||||
{ "Input.PadDeadzone", "Stick deadzone", ROW_SLIDER, 500, NULL, 0, 0, 0, 0,0,0,0,0, NULL, "%", 300 },
|
{ .key="Input.PadDeadzone", .label="Stick deadzone", .kind=ROW_SLIDER, .step=500, .unit="%", .dispDiv=300 },
|
||||||
{ "Input.PadTriggerPct", "Trigger threshold",ROW_SLIDER, 1, NULL, 0, 0, 0, 0,0,0,0,0, NULL, "%" },
|
{ .key="Input.PadTriggerPct", .label="Trigger threshold", .kind=ROW_SLIDER, .step=1, .unit="%" },
|
||||||
/* D181/Game.ScreenShakeIntensity: user testing (v0.2.1) found the slider
|
/* D181/Game.ScreenShakeIntensity: user testing (v0.2.1) found the slider
|
||||||
* "basically useless" -- viShake() is only called from explosion.c, so it
|
* "basically useless" -- viShake() is only called from explosion.c, so it
|
||||||
* scales explosion shake alone; it never touches the always-on walking
|
* scales explosion shake alone; it never touches the always-on walking
|
||||||
@@ -219,7 +246,7 @@ static struct Row rows[] = {
|
|||||||
{ "__HdrGame", "GAME", ROW_HEADER, 0, NULL, 0, 0, 0, 0,0,0,0,0 },
|
{ "__HdrGame", "GAME", ROW_HEADER, 0, NULL, 0, 0, 0, 0,0,0,0,0 },
|
||||||
/* D226: scales the ammo counter, pickup / status messages and dialogue
|
/* D226: scales the ammo counter, pickup / status messages and dialogue
|
||||||
* about their screen anchors. 100% = original, nothing emitted. */
|
* about their screen anchors. 100% = original, nothing emitted. */
|
||||||
{ "Game.HudScale", "HUD scale", ROW_SLIDER, 5, NULL, 0, 0, 0, 0,0,0,0,0, NULL, "%" },
|
{ .key="Game.HudScale", .label="HUD scale", .kind=ROW_SLIDER, .step=5, .unit="%" },
|
||||||
{ "Game.SkipIntro", "Skip intro", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 },
|
{ "Game.SkipIntro", "Skip intro", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 },
|
||||||
{ "Video.DisplayFPS", "Show FPS", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 },
|
{ "Video.DisplayFPS", "Show FPS", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 },
|
||||||
{ "Game.NoHitFlash", "No hit flash", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 },
|
{ "Game.NoHitFlash", "No hit flash", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 },
|
||||||
@@ -384,7 +411,10 @@ static void overlayUpdateVisible(void)
|
|||||||
{
|
{
|
||||||
s_visN = 0;
|
s_visN = 0;
|
||||||
for (int i = 0; i < NUM_ROWS; i++) {
|
for (int i = 0; i < NUM_ROWS; i++) {
|
||||||
if (!(rows[i].hidePtr && *rows[i].hidePtr != 0)) {
|
/* The file chooser belongs to the GE front-end screen, not F10.
|
||||||
|
* F10 in-stage always targets the active Bond file. */
|
||||||
|
if (rows[i].kind != ROW_BOND_FILE &&
|
||||||
|
!(rows[i].hidePtr && *rows[i].hidePtr != 0)) {
|
||||||
s_visIdx[s_visN++] = i;
|
s_visIdx[s_visN++] = i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -484,7 +514,8 @@ static void overlayInit(void)
|
|||||||
|
|
||||||
for (int i = 0; i < NUM_ROWS; i++) {
|
for (int i = 0; i < NUM_ROWS; i++) {
|
||||||
if (rows[i].kind == ROW_RES || rows[i].kind == ROW_ACTION ||
|
if (rows[i].kind == ROW_RES || rows[i].kind == ROW_ACTION ||
|
||||||
rows[i].kind == ROW_HEADER) {
|
rows[i].kind == ROW_HEADER || rows[i].kind == ROW_BOND_FILE ||
|
||||||
|
watchSettingsFieldForKey(rows[i].key) >= 0) {
|
||||||
rows[i].found = 1; /* not config-backed */
|
rows[i].found = 1; /* not config-backed */
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -543,6 +574,8 @@ static void overlayInit(void)
|
|||||||
|
|
||||||
static double rowGet(const struct Row *r)
|
static double rowGet(const struct Row *r)
|
||||||
{
|
{
|
||||||
|
int field = watchSettingsFieldForKey(r->key);
|
||||||
|
if (field >= 0) return (double)watchSettingsRead(field);
|
||||||
if (!r->found || !r->ptr) {
|
if (!r->found || !r->ptr) {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
@@ -575,13 +608,20 @@ static struct Row *rowByKey(const char *key)
|
|||||||
|
|
||||||
static int s_linkDepth = 0; /* re-entrancy guard for the sens link below */
|
static int s_linkDepth = 0; /* re-entrancy guard for the sens link below */
|
||||||
|
|
||||||
static void rowSet(struct Row *r, double v)
|
static void rowSet(struct Row *r, double v);
|
||||||
|
|
||||||
|
static void rowSetCommit(struct Row *r, double v, int commit)
|
||||||
{
|
{
|
||||||
double lo = rowLo(r), hi = rowHi(r);
|
double lo = rowLo(r), hi = rowHi(r);
|
||||||
if (lo != hi) {
|
if (lo != hi) {
|
||||||
if (v < lo) v = lo;
|
if (v < lo) v = lo;
|
||||||
if (v > hi) v = hi;
|
if (v > hi) v = hi;
|
||||||
}
|
}
|
||||||
|
int field = watchSettingsFieldForKey(r->key);
|
||||||
|
if (field >= 0) {
|
||||||
|
watchSettingsSet(field, (int)lround(v), commit);
|
||||||
|
return;
|
||||||
|
}
|
||||||
switch (r->type) {
|
switch (r->type) {
|
||||||
case CONFIG_OPT_INT: *(int *)r->ptr = (int)lround(v); break;
|
case CONFIG_OPT_INT: *(int *)r->ptr = (int)lround(v); break;
|
||||||
case CONFIG_OPT_UINT: *(unsigned int *)r->ptr = (unsigned int)(v < 0 ? 0 : lround(v)); break;
|
case CONFIG_OPT_UINT: *(unsigned int *)r->ptr = (unsigned int)(v < 0 ? 0 : lround(v)); break;
|
||||||
@@ -621,11 +661,18 @@ static void rowSet(struct Row *r, double v)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void rowSet(struct Row *r, double v) { rowSetCommit(r, v, 1); }
|
||||||
|
|
||||||
static void rowAdjust(struct Row *r, int dir)
|
static void rowAdjust(struct Row *r, int dir)
|
||||||
{
|
{
|
||||||
if (!r->found || r->kind == ROW_HEADER) { /* D237: headers have no value */
|
if (!r->found || r->kind == ROW_HEADER) { /* D237: headers have no value */
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (r->kind == ROW_BOND_FILE) {
|
||||||
|
if (current_menu == MENU_PC_OPTIONS) watchSettingsChooseFile(dir);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (watchSettingsFieldForKey(r->key) >= 0 && !watchSettingsAvailable()) return;
|
||||||
double v = rowGet(r);
|
double v = rowGet(r);
|
||||||
switch (r->kind) {
|
switch (r->kind) {
|
||||||
case ROW_TOGGLE:
|
case ROW_TOGGLE:
|
||||||
@@ -688,6 +735,10 @@ static void rowAdjust(struct Row *r, int dir)
|
|||||||
void optionsOverlayToggle(void)
|
void optionsOverlayToggle(void)
|
||||||
{
|
{
|
||||||
overlayInit();
|
overlayInit();
|
||||||
|
if (s_open) {
|
||||||
|
int pending = SDL_AtomicSet(&s_dragWatchField, 0);
|
||||||
|
if (pending > 0) watchSettingsCommit(pending - 1);
|
||||||
|
}
|
||||||
s_open = !s_open;
|
s_open = !s_open;
|
||||||
sysLogPrintf(LOG_INFO, "optionsoverlay: %s", s_open ? "opened" : "closed");
|
sysLogPrintf(LOG_INFO, "optionsoverlay: %s", s_open ? "opened" : "closed");
|
||||||
if (!s_open) {
|
if (!s_open) {
|
||||||
@@ -747,7 +798,7 @@ static void sliderSetFromX(struct Row *r, double ox)
|
|||||||
double v = lo + f * (hi - lo);
|
double v = lo + f * (hi - lo);
|
||||||
double step = (r->step > 0.0) ? r->step : 1.0;
|
double step = (r->step > 0.0) ? r->step : 1.0;
|
||||||
v = lround(v / step) * step;
|
v = lround(v / step) * step;
|
||||||
rowSet(r, v);
|
rowSetCommit(r, v, 0); /* slider drag: save only on release */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* D314 (findings.md): F10 menu items flicker/mis-land on the file-select
|
/* D314 (findings.md): F10 menu items flicker/mis-land on the file-select
|
||||||
@@ -762,14 +813,14 @@ static int s_d314Enabled = -1; /* -1 = not yet resolved */
|
|||||||
void optionsOverlayHandleInput(void)
|
void optionsOverlayHandleInput(void)
|
||||||
{
|
{
|
||||||
static int prevUp, prevDn, prevLf, prevRt, prevLmb, prevRmb;
|
static int prevUp, prevDn, prevLf, prevRt, prevLmb, prevRmb;
|
||||||
static int dragRow = -1;
|
|
||||||
/* D347: hold-to-repeat state (18/4-frame cadence, same as the options
|
/* D347: hold-to-repeat state (18/4-frame cadence, same as the options
|
||||||
* screen's D345(e)/(f) blocks). */
|
* screen's D345(e)/(f) blocks). */
|
||||||
static int navDir = 0, navTimer = 0, adjDir = 0, adjTimer = 0;
|
static int navDir = 0, navTimer = 0, adjDir = 0, adjTimer = 0;
|
||||||
|
|
||||||
if (!s_open) {
|
if (!s_open) {
|
||||||
prevUp = prevDn = prevLf = prevRt = prevLmb = prevRmb = 0;
|
prevUp = prevDn = prevLf = prevRt = prevLmb = prevRmb = 0;
|
||||||
dragRow = -1;
|
s_dragRow = -1;
|
||||||
|
SDL_AtomicSet(&s_dragWatchField, 0);
|
||||||
navDir = adjDir = 0;
|
navDir = adjDir = 0;
|
||||||
SDL_AtomicSet(&s_wheelPending, 0);
|
SDL_AtomicSet(&s_wheelPending, 0);
|
||||||
return;
|
return;
|
||||||
@@ -906,7 +957,9 @@ void optionsOverlayHandleInput(void)
|
|||||||
struct Row *r = &rows[s_visIdx[hoverVis]];
|
struct Row *r = &rows[s_visIdx[hoverVis]];
|
||||||
if (r->kind == ROW_SLIDER && r->found) {
|
if (r->kind == ROW_SLIDER && r->found) {
|
||||||
sliderSetFromX(r, ox);
|
sliderSetFromX(r, ox);
|
||||||
dragRow = s_visIdx[hoverVis];
|
s_dragRow = s_visIdx[hoverVis];
|
||||||
|
int field = watchSettingsFieldForKey(r->key);
|
||||||
|
SDL_AtomicSet(&s_dragWatchField, field + 1);
|
||||||
} else {
|
} else {
|
||||||
rowAdjust(r, +1); /* toggle / cycle forward (wraps) */
|
rowAdjust(r, +1); /* toggle / cycle forward (wraps) */
|
||||||
}
|
}
|
||||||
@@ -914,11 +967,16 @@ void optionsOverlayHandleInput(void)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* drag a slider */
|
/* drag a slider */
|
||||||
if (lmb && dragRow >= 0 && rows[dragRow].kind == ROW_SLIDER) {
|
if (lmb && s_dragRow >= 0 && rows[s_dragRow].kind == ROW_SLIDER) {
|
||||||
sliderSetFromX(&rows[dragRow], ox);
|
sliderSetFromX(&rows[s_dragRow], ox);
|
||||||
}
|
}
|
||||||
if (!lmb) {
|
if (!lmb) {
|
||||||
dragRow = -1;
|
if (s_dragRow >= 0) {
|
||||||
|
int field = watchSettingsFieldForKey(rows[s_dragRow].key);
|
||||||
|
if (field >= 0) watchSettingsCommit(field);
|
||||||
|
}
|
||||||
|
s_dragRow = -1;
|
||||||
|
SDL_AtomicSet(&s_dragWatchField, 0);
|
||||||
}
|
}
|
||||||
/* right press in the value column: cycle back / decrement */
|
/* right press in the value column: cycle back / decrement */
|
||||||
if (rmb && !prevRmb && hoverVis >= 0 && !onClose && ox >= bx0 &&
|
if (rmb && !prevRmb && hoverVis >= 0 && !onClose && ox >= bx0 &&
|
||||||
@@ -951,6 +1009,16 @@ static Gfx *fillRect(Gfx *gdl, s32 x0, s32 y0, s32 x1, s32 y1,
|
|||||||
static void valueText(const struct Row *r, char *out, int n)
|
static void valueText(const struct Row *r, char *out, int n)
|
||||||
{
|
{
|
||||||
double v = rowGet(r);
|
double v = rowGet(r);
|
||||||
|
if (r->kind == ROW_BOND_FILE) {
|
||||||
|
int f = watchSettingsFolder();
|
||||||
|
if (f < 0) snprintf(out, n, "Select file");
|
||||||
|
else snprintf(out, n, "File %d", f + 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (watchSettingsFieldForKey(r->key) >= 0 && !watchSettingsAvailable()) {
|
||||||
|
snprintf(out, n, "Select file");
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (r->kind == ROW_RES) {
|
if (r->kind == ROW_RES) {
|
||||||
if (videoIsFullscreen()) {
|
if (videoIsFullscreen()) {
|
||||||
snprintf(out, n, "(fullscreen)");
|
snprintf(out, n, "(fullscreen)");
|
||||||
@@ -1185,7 +1253,8 @@ int optionsRowIsHeader(int i)
|
|||||||
int optionsRowIsShown(int i)
|
int optionsRowIsShown(int i)
|
||||||
{
|
{
|
||||||
struct Row *r = rowAt(i);
|
struct Row *r = rowAt(i);
|
||||||
return r && r->found && !(r->hidePtr && *r->hidePtr);
|
return r && r->found && !(r->hidePtr && *r->hidePtr) &&
|
||||||
|
(r->kind != ROW_BOND_FILE || current_menu == MENU_PC_OPTIONS);
|
||||||
}
|
}
|
||||||
|
|
||||||
const char *optionsRowLabel(int i)
|
const char *optionsRowLabel(int i)
|
||||||
@@ -1226,7 +1295,16 @@ void optionsRowSetFraction(int i, double f)
|
|||||||
if (f > 1.0) f = 1.0;
|
if (f > 1.0) f = 1.0;
|
||||||
double step = (r->step > 0.0) ? r->step : 1.0;
|
double step = (r->step > 0.0) ? r->step : 1.0;
|
||||||
double v = rowLo(r) + f * (rowHi(r) - rowLo(r));
|
double v = rowLo(r) + f * (rowHi(r) - rowLo(r));
|
||||||
rowSet(r, lround(v / step) * step);
|
rowSetCommit(r, lround(v / step) * step, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void optionsRowCommit(int i)
|
||||||
|
{
|
||||||
|
struct Row *r = rowAt(i);
|
||||||
|
if (r) {
|
||||||
|
int field = watchSettingsFieldForKey(r->key);
|
||||||
|
if (field >= 0) watchSettingsCommit(field);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void optionsRowValueText(int i, char *out, int n)
|
void optionsRowValueText(int i, char *out, int n)
|
||||||
|
|||||||
@@ -0,0 +1,285 @@
|
|||||||
|
/* D349: PC UI reads GE watch settings; PC-UI edits persist per Bond file.
|
||||||
|
* The original watch remains session-only. Never call GE setters/save routines
|
||||||
|
* from the F10 input/render thread. This file is port-only. */
|
||||||
|
#include <string.h>
|
||||||
|
#include <SDL.h>
|
||||||
|
#include <ultra64.h>
|
||||||
|
#include <bondconstants.h>
|
||||||
|
#include "file.h"
|
||||||
|
#include "file2.h"
|
||||||
|
#include "front.h"
|
||||||
|
#include "options.h"
|
||||||
|
#include "player.h"
|
||||||
|
#include "system.h"
|
||||||
|
#include "watchsettings.h"
|
||||||
|
|
||||||
|
extern s32 g_StageNum; /* boss.c: direct -level_XX starts with MENU_INVALID */
|
||||||
|
extern save_data *fileGetSaveForFoldernum(u32 folder);
|
||||||
|
extern void fileSaveSettingsForFolder(save_data *save);
|
||||||
|
extern void fileWriteSave(save_data *save);
|
||||||
|
extern s32 fileGamePakProbe(void);
|
||||||
|
extern void fileGenerateCRC(u8 *start, u8 *end, save_data *result);
|
||||||
|
extern void sub_GAME_7F0A91A0(u16 volume);
|
||||||
|
extern void set_cur_player_look_vertical_inverted(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_ammo_onscreen_setting(u32 value);
|
||||||
|
|
||||||
|
static const char *const keys[WATCH_SETTING_COUNT] = {
|
||||||
|
"Bond.Music", "Bond.FX", "Bond.Look", "Bond.AutoAim",
|
||||||
|
"Bond.AimControl", "Bond.Sight", "Bond.LookAhead", "Bond.Ammo"
|
||||||
|
};
|
||||||
|
static const u16 bits[WATCH_SETTING_COUNT] = {
|
||||||
|
0, 0, OPTION_INVERTLOOK, OPTION_AUTOAIM, OPTION_AIMCONTROL,
|
||||||
|
OPTION_SIGHTONSCREEN, OPTION_LOOKAHEAD, OPTION_DISPLAYAMMO
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Explicit chooser is separate from selected_folder_num (which frontoptions
|
||||||
|
* assigns FOLDER1 merely to draw its dossier background). -1 means none. */
|
||||||
|
static int chosen = -1;
|
||||||
|
static int staged[WATCH_SETTING_COUNT];
|
||||||
|
static int stagedField = -1, stagedFolder = -1;
|
||||||
|
|
||||||
|
/* Bounded producer (F10 scheduler) -> consumer (game thread) queue. Snapshot
|
||||||
|
* is also guarded; no game/watch reads from F10 render or input paths. */
|
||||||
|
struct Command { int folder, field, value, commit; };
|
||||||
|
#define CMD_CAP 128
|
||||||
|
static struct Command cmds[CMD_CAP];
|
||||||
|
static int head, count;
|
||||||
|
static int snapshot[WATCH_SETTING_COUNT];
|
||||||
|
static int snapFolder = -1;
|
||||||
|
static SDL_SpinLock lock;
|
||||||
|
|
||||||
|
int watchSettingsFieldForKey(const char *key)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < WATCH_SETTING_COUNT; i++)
|
||||||
|
if (strcmp(keys[i], key) == 0) return i;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* GE_STARTMENU skips fileValidateSaves: zeroed saves[] looks like a valid
|
||||||
|
* folder-0 save to fileGetSaveForFoldernum. Never let a PC UI write that
|
||||||
|
* phantom slot (D299). Check the actual CRC before treating it as a file. */
|
||||||
|
static save_data *validSave(int folder)
|
||||||
|
{
|
||||||
|
if (folder < FOLDER1 || folder >= MAX_FOLDER_COUNT) return NULL;
|
||||||
|
save_data *save = fileGetSaveForFoldernum((u32)folder);
|
||||||
|
if (!save) return NULL;
|
||||||
|
save_data crc = {0};
|
||||||
|
fileGenerateCRC((u8 *)&save->completion_bitflags, (u8 *)(save + 1), &crc);
|
||||||
|
return crc.chksum1 == save->chksum1 && crc.chksum2 == save->chksum2
|
||||||
|
? save : NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static save_data *chosenSave(void) { return validSave(chosen); }
|
||||||
|
|
||||||
|
int watchSettingsFolder(void) { return chosen; }
|
||||||
|
|
||||||
|
void watchSettingsChooseFile(int dir)
|
||||||
|
{
|
||||||
|
/* Include 'none' so selection is always intentional; skip empty slots.
|
||||||
|
* Do not load the file here: GE's loader would change front-end audio. */
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
chosen = next;
|
||||||
|
stagedField = -1;
|
||||||
|
sysLogPrintf(LOG_INFO, "watchsettings: selected Bond file %d", chosen < 0 ? 0 : chosen + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int frontScreen(void) { return current_menu == MENU_PC_OPTIONS; }
|
||||||
|
static int stageActive(void)
|
||||||
|
{
|
||||||
|
/* Direct -level_XX boots never visit the front-end RUN_STAGE state. */
|
||||||
|
return current_menu == MENU_RUN_STAGE ||
|
||||||
|
(current_menu == MENU_INVALID && g_StageNum != LEVELID_TITLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
int watchSettingsAvailable(void)
|
||||||
|
{
|
||||||
|
if (frontScreen()) return chosenSave() != NULL;
|
||||||
|
if (!stageActive()) return 0;
|
||||||
|
int valid;
|
||||||
|
SDL_AtomicLock(&lock);
|
||||||
|
valid = snapFolder >= FOLDER1 && snapFolder < MAX_FOLDER_COUNT;
|
||||||
|
SDL_AtomicUnlock(&lock);
|
||||||
|
return valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int savedValue(const save_data *save, enum WatchSettingField field)
|
||||||
|
{
|
||||||
|
if (field == WATCH_SETTING_MUSIC) return ((int)save->music_vol << 7) | (save->music_vol >> 1);
|
||||||
|
if (field == WATCH_SETTING_FX) return ((int)save->sfx_vol << 7) | (save->sfx_vol >> 1);
|
||||||
|
return (save->options & bits[field]) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int watchSettingsRead(enum WatchSettingField field)
|
||||||
|
{
|
||||||
|
if ((unsigned)field >= WATCH_SETTING_COUNT) return 0;
|
||||||
|
if (frontScreen()) {
|
||||||
|
save_data *save = chosenSave();
|
||||||
|
if (!save) return 0;
|
||||||
|
if (stagedField == (int)field && stagedFolder == chosen) return staged[field];
|
||||||
|
return savedValue(save, field);
|
||||||
|
}
|
||||||
|
int value = 0;
|
||||||
|
SDL_AtomicLock(&lock);
|
||||||
|
if (snapFolder >= FOLDER1 && snapFolder < MAX_FOLDER_COUNT)
|
||||||
|
value = snapshot[field];
|
||||||
|
SDL_AtomicUnlock(&lock);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* F10 game thread calls this after applying the requested GE setter.
|
||||||
|
* temp serialization must NOT overwrite unrelated, watch-only edits or the
|
||||||
|
* port-forced controller type in the Bond file. */
|
||||||
|
int watchSettingsPersistField(int folder, enum WatchSettingField field)
|
||||||
|
{
|
||||||
|
if (folder < FOLDER1 || folder >= MAX_FOLDER_COUNT ||
|
||||||
|
(unsigned)field >= WATCH_SETTING_COUNT || !g_CurrentPlayer) return 0;
|
||||||
|
save_data *save = validSave(folder);
|
||||||
|
if (!save || !fileGamePakProbe()) return 0;
|
||||||
|
save_data serialized = *save;
|
||||||
|
fileSaveSettingsForFolder(&serialized);
|
||||||
|
if (field == WATCH_SETTING_MUSIC) {
|
||||||
|
if (save->music_vol == serialized.music_vol) return 1;
|
||||||
|
save->music_vol = serialized.music_vol;
|
||||||
|
} else if (field == WATCH_SETTING_FX) {
|
||||||
|
if (save->sfx_vol == serialized.sfx_vol) return 1;
|
||||||
|
save->sfx_vol = serialized.sfx_vol;
|
||||||
|
} else {
|
||||||
|
u16 mask = bits[field];
|
||||||
|
u16 merged = (save->options & (u16)~mask) | (serialized.options & mask);
|
||||||
|
if (save->options == merged) return 1;
|
||||||
|
save->options = merged;
|
||||||
|
}
|
||||||
|
fileWriteSave(save); /* real saves[] slot; writes CRC + EEPROM */
|
||||||
|
sysLogPrintf(LOG_INFO, "watchsettings: persisted field %d in Bond file %d", field, folder + 1);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Front end has no active GE watch/player settings. Edit the explicitly
|
||||||
|
* chosen save field directly instead of calling fileLoadSettingsForFolder
|
||||||
|
* (which also applies the chosen file's audio volume to the front end). */
|
||||||
|
static void saveFront(enum WatchSettingField field, int value)
|
||||||
|
{
|
||||||
|
save_data *save = chosenSave();
|
||||||
|
if (!save || !fileGamePakProbe()) return;
|
||||||
|
if (value < 0) value = 0;
|
||||||
|
if (field == WATCH_SETTING_MUSIC || field == WATCH_SETTING_FX) {
|
||||||
|
if (value > 32767) value = 32767;
|
||||||
|
u8 raw = (u8)(value >> 7);
|
||||||
|
u8 *dest = field == WATCH_SETTING_MUSIC ? &save->music_vol : &save->sfx_vol;
|
||||||
|
if (*dest == raw) return;
|
||||||
|
*dest = raw;
|
||||||
|
} else {
|
||||||
|
u16 mask = bits[field];
|
||||||
|
u16 merged = (save->options & (u16)~mask) | (value ? mask : 0);
|
||||||
|
if (save->options == merged) return;
|
||||||
|
save->options = merged;
|
||||||
|
}
|
||||||
|
fileWriteSave(save);
|
||||||
|
sysLogPrintf(LOG_INFO, "watchsettings: persisted field %d in Bond file %d", field, chosen + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void watchSettingsSet(enum WatchSettingField field, int value, int commit)
|
||||||
|
{
|
||||||
|
if ((unsigned)field >= WATCH_SETTING_COUNT || !watchSettingsAvailable()) return;
|
||||||
|
if (frontScreen()) {
|
||||||
|
if (commit) {
|
||||||
|
saveFront(field, value);
|
||||||
|
stagedField = -1;
|
||||||
|
} else {
|
||||||
|
stagedField = field; stagedFolder = chosen; staged[field] = value;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SDL_AtomicLock(&lock);
|
||||||
|
struct Command cmd = { snapFolder, field, value, commit };
|
||||||
|
if (count < CMD_CAP) {
|
||||||
|
cmds[(head + count++) % CMD_CAP] = cmd;
|
||||||
|
snapshot[field] = value; /* immediate UI feedback for held adjustments */
|
||||||
|
} else {
|
||||||
|
sysLogPrintf(LOG_WARNING, "watchsettings: command queue full; edit not applied");
|
||||||
|
}
|
||||||
|
SDL_AtomicUnlock(&lock);
|
||||||
|
}
|
||||||
|
|
||||||
|
void watchSettingsCommit(enum WatchSettingField field)
|
||||||
|
{
|
||||||
|
if ((unsigned)field >= WATCH_SETTING_COUNT || !watchSettingsAvailable()) return;
|
||||||
|
watchSettingsSet(field, watchSettingsRead(field), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int liveValue(enum WatchSettingField field)
|
||||||
|
{
|
||||||
|
switch (field) {
|
||||||
|
case WATCH_SETTING_MUSIC: return get_mTrack2Vol();
|
||||||
|
case WATCH_SETTING_FX: return call_sndGetSfxSlotFirstNaturalVolume();
|
||||||
|
case WATCH_SETTING_LOOK: return get_cur_player_look_vertical_inverted();
|
||||||
|
case WATCH_SETTING_AUTOAIM: return cur_player_get_autoaim();
|
||||||
|
case WATCH_SETTING_AIMCONTROL: return cur_player_get_aim_control();
|
||||||
|
case WATCH_SETTING_SIGHT: return cur_player_get_sight_onscreen_control();
|
||||||
|
case WATCH_SETTING_LOOKAHEAD: return cur_player_get_lookahead();
|
||||||
|
case WATCH_SETTING_AMMO: return cur_player_get_ammo_onscreen_setting();
|
||||||
|
default: return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void applyValue(enum WatchSettingField field, int value)
|
||||||
|
{
|
||||||
|
if (value < 0) value = 0;
|
||||||
|
if (field <= WATCH_SETTING_FX && value > 32767) value = 32767;
|
||||||
|
else if (field > WATCH_SETTING_FX) value = !!value;
|
||||||
|
switch (field) {
|
||||||
|
case WATCH_SETTING_MUSIC: set_mTrack2Vol((u16)value); break;
|
||||||
|
case WATCH_SETTING_FX: sub_GAME_7F0A91A0((u16)value); break;
|
||||||
|
case WATCH_SETTING_LOOK: set_cur_player_look_vertical_inverted(value); break;
|
||||||
|
case WATCH_SETTING_AUTOAIM: cur_player_set_autoaim(value); break;
|
||||||
|
case WATCH_SETTING_AIMCONTROL: cur_player_set_aim_control(value); break;
|
||||||
|
case WATCH_SETTING_SIGHT: cur_player_set_sight_onscreen_control(value); break;
|
||||||
|
case WATCH_SETTING_LOOKAHEAD: cur_player_set_lookahead(value); break;
|
||||||
|
case WATCH_SETTING_AMMO: cur_player_set_ammo_onscreen_setting(value); break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void watchSettingsGameTick(void)
|
||||||
|
{
|
||||||
|
struct Command batch[CMD_CAP];
|
||||||
|
int n;
|
||||||
|
SDL_AtomicLock(&lock);
|
||||||
|
n = count;
|
||||||
|
for (int i = 0; i < n; i++) batch[i] = cmds[(head + i) % CMD_CAP];
|
||||||
|
head = (head + n) % CMD_CAP;
|
||||||
|
count = 0;
|
||||||
|
SDL_AtomicUnlock(&lock);
|
||||||
|
|
||||||
|
/* This hook runs on the game thread after a gfxFrameMsgQ receive, even
|
||||||
|
* during load; do not call GE audio/player APIs until a stage is active. */
|
||||||
|
if (!stageActive() || !g_CurrentPlayer) {
|
||||||
|
SDL_AtomicLock(&lock);
|
||||||
|
snapFolder = -1;
|
||||||
|
SDL_AtomicUnlock(&lock);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
struct Command *c = &batch[i];
|
||||||
|
if (c->folder != selected_folder_num ||
|
||||||
|
(unsigned)c->field >= WATCH_SETTING_COUNT ||
|
||||||
|
!validSave(c->folder)) continue;
|
||||||
|
applyValue((enum WatchSettingField)c->field, c->value);
|
||||||
|
if (c->commit) watchSettingsPersistField(c->folder, (enum WatchSettingField)c->field);
|
||||||
|
}
|
||||||
|
int values[WATCH_SETTING_COUNT];
|
||||||
|
for (int f = 0; f < WATCH_SETTING_COUNT; f++)
|
||||||
|
values[f] = liveValue((enum WatchSettingField)f);
|
||||||
|
SDL_AtomicLock(&lock);
|
||||||
|
for (int f = 0; f < WATCH_SETTING_COUNT; f++) snapshot[f] = values[f];
|
||||||
|
snapFolder = selected_folder_num;
|
||||||
|
SDL_AtomicUnlock(&lock);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user