From 971c2d9f62105da9b39912534f9a9c8f2e5b5b89 Mon Sep 17 00:00:00 2001 From: James Dansereau <106631954+jkdansereau@users.noreply.github.com> Date: Wed, 23 Sep 2026 13:27:28 -0400 Subject: [PATCH] D281: AllUnlocked on a fresh install now yields the game's own fresh-slot layout D259 seeded only the volumes on all-zero slots. Because the AllUnlocked read patch re-CRCs every slot, a fresh eep survived validation as five folder-1 slots with options 0: sight-on-screen off (no crosshair while aiming = the "RMB aim broken" report) plus auto-aim/look-ahead/ammo off, and the game reset/rebuilt the duplicates mid-validate. All-zero slots now become what fileValidateSaves + fileBuildWriteNewSave would build (folder saves from BLANKSAVEDATA via the game's own setters, leftovers free). Verified: fresh eep + AllUnlocked + SAVELOG -> 00/21/42/63/80, all CRC OK, no repair writes. Golden 3/3 (x2 reruns); Windows + Linux builds clean. --- docs/dev/findings-index.csv | 2 +- docs/dev/findings.md | 4 ++- port/src/libultra.c | 61 +++++++++++++++++++++++++++---------- 3 files changed, 49 insertions(+), 18 deletions(-) diff --git a/docs/dev/findings-index.csv b/docs/dev/findings-index.csv index a28c057c..56cd8395 100644 --- a/docs/dev/findings-index.csv +++ b/docs/dev/findings-index.csv @@ -96,7 +96,7 @@ D270,?,Backfilled (M-132) — this label was never given its own row; only code D271,FIXED,"D236 wall, third pass —",FIXED-but-insufficient (src/game/bg.c #ifdef PORT) — D276,OPEN,"D236 wall, eighth pass —","REFRAMED — oml=0xc81049d8/fmt=3 is the primary suspect now, not D269's CI8 pair; 0x0c184b50 unattributed; wall still open, see D236" D279,?,Backfilled — this label was consumed by an undocumented partial session (2026-09-14 ~23:00) that left artifacts in scratch/ (d279_run.log… —,undocumented — artifacts only -D281,OPEN,"Game.AllUnlocked (D257) still breaks audio, and can also break right-mouse aim, when enabled before any save has ever been written (v0.2.1 user testing… —","OPEN — user-reported regression, not root-caused; mitigation is user-facing guidance only (release notes:" +D281,FIXED,"Game.AllUnlocked (D257) still breaks audio, and can also break right-mouse aim, when enabled before any save has ever been written (v0.2.1 user testing… —","FIXED (2026-09-23, port/src/libultra.c):" D282,FIXED,"QoL ask (v0.2.1 user testing, 2026-09-15):","FIXED (port/src/input.c, #ifdef not needed —" D283,OPEN,"Steam Deck first-run preset (D-unnumbered in port/src/video.c/main.c, videoApplySteamOSDefaults()) did not apply for a user who added the game via Steam… —","OPEN — mechanism understood, root cause (Desktop-vs-Game-Mode STEAMOS env var difference) unverified on real hardware; user-facing workaround documented in README/release notes." D280,?,"D236 wall, ninth pass —","REFRAMED again — no fix; next steps in detail section, see D236" diff --git a/docs/dev/findings.md b/docs/dev/findings.md index ef0341a4..6a206833 100644 --- a/docs/dev/findings.md +++ b/docs/dev/findings.md @@ -495,7 +495,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D271 | **D236 wall, third pass — suspect #3 (draw density/culling) CONFIRMED as a real over-draw; the D106 guard was the cause (M-129).** — full `## D271` entry at file tail | FIXED-but-insufficient (`src/game/bg.c` `#ifdef PORT`) — **landed** `532092c7` (M-139 R2 extraction, probes stripped); wall still open, see D236 | | D276 | **D236 wall, eighth pass — the true painter of the visible weave is a fully-opaque IA8 blocky-noise texture, not either of D269's named CI8 classes (M-134).** — full `## D276` entry at file tail | REFRAMED — `oml=0xc81049d8`/`fmt=3` is the primary suspect now, not D269's CI8 pair; `0x0c184b50` unattributed; wall still open, see D236 | | D279 | **Backfilled — this label was consumed by an undocumented partial session (2026-09-14 ~23:00) that left artifacts in `scratch/` (`d279_run.log`…** — full `## D279` entry at file tail | undocumented — artifacts only | -| D281 | **`Game.AllUnlocked` (D257) still breaks audio, and can also break right-mouse aim, when enabled before any save has ever been written (v0.2.1 user testing…** — full `## D281` entry at file tail | OPEN — user-reported regression, not root-caused; mitigation is user-facing guidance only (release notes: don't enable until a save exists, treat as experimental). | +| D281 | **`Game.AllUnlocked` (D257) still breaks audio, and can also break right-mouse aim, when enabled before any save has ever been written (v0.2.1 user testing…** — full `## D281` entry at file tail | FIXED (2026-09-23, port/src/libultra.c): the AllUnlocked patch now reproduces the game's fresh-slot result (folder saves with DEFAULT_OPTIONS + one free slot) instead of five folder-1 slots with options 0 (no aim sight). Verified by fresh-eep SAVELOG; audio half owed a user check. | | D282 | **QoL ask (v0.2.1 user testing, 2026-09-15): front-end menu navigation (main menu, file select, mission-select map) currently requires the RIGHT analog stick —…** — full `## D282` entry at file tail | FIXED (`port/src/input.c`, `#ifdef` not needed — port-layer-only file), feel unconfirmed. | | D283 | **Steam Deck first-run preset (D-unnumbered in `port/src/video.c`/`main.c`, `videoApplySteamOSDefaults()`) did not apply for a user who added the game via Steam…** — full `## D283` entry at file tail | OPEN — mechanism understood, root cause (Desktop-vs-Game-Mode `STEAMOS` env var difference) unverified on real hardware; user-facing workaround documented in README/release notes. | | D280 | **D236 wall, ninth pass — fog math VERIFIED N64-faithful; the IA8 noise class VISUALLY confirmed as the weave's painter (M-135).** No quick win for trees. — full `## D280` entry at file tail | REFRAMED again — no fix; next steps in detail section, see D236 | @@ -11695,6 +11695,8 @@ Superseded history below (the original OPEN investigation, root cause not found **Status (from §F table):** OPEN — user-reported regression, not root-caused; mitigation is user-facing guidance only (release notes: don't enable until a save exists, treat as experimental). +**Root cause + fix (2026-09-23, v0.4.0):** D259 emulated only part of the fresh-slot path. Without AllUnlocked, an all-zero slot fails `fileValidateSaves`' CRC → `fileResetSave` → a *free* `BLANKSAVEDATA` slot (DORESET, vol 0xFF, `options = DEFAULT_OPTIONS` 0x3A), and then `fileBuildWriteNewSave` fills the first free slot for each folder without a save. The AllUnlocked read patch re-CRCs every slot, so all-zero slots instead **survived** as five non-free slots all claiming folder 1, with `options = 0` (sight-on-screen OFF = no crosshair when aiming, which is the "RMB aim broken" symptom; auto-aim, look-ahead and ammo display also off). The game then reset/rebuilt the duplicate folder slots mid-validate. **Fix (`port/src/libultra.c` `geEepromPatchAllCheats`, port-only):** all-zero slots take the folders no real slot holds, in order, built exactly as `fileBuildWriteNewSave` does (`BLANKSAVEDATA` + `fileSetSaveFoldernum`/`fileSetSaveFlagDoReset(FALSE)`/`fileSetSelectedBond`, the game's own setters); leftovers become free `BLANKSAVEDATA`. Real saves keep their bytes. **Verified (native Windows, fresh zero eep, `AllUnlocked=1`, `SkipIntro=1`, `GE_SAVELOG=1`):** `fileValidateSaves` now sees `00/21/42/63/80` (folders 1–4 + one free slot, the layout a normal fresh install produces), all CRC OK and no repair writes; the eep on disk stays pristine (read-time patch). Before: five `00` slots. Golden 3/3 (AllUnlocked off by default, so the change is inert there); Windows + Linux builds clean. **Owed:** the audio half of the report was never mechanised. The volumes were already right after D259, and the folder-collision churn this removes is the most plausible remaining cause. User check: fresh install (move `ge007.eep` aside) → enable All unlocked → play a level; audio and RMB-aim crosshair should both work. + ## D282 — QoL ask (v0.2.1 user testing, 2026-09-15): front-end menu navigation (main menu, file select, mission-select map) currently requires the RIGHT analog stick —… **QoL ask (v0.2.1 user testing, 2026-09-15): front-end menu navigation (main menu, file select, mission-select map) currently requires the RIGHT analog stick — matches N64 default control schemes but is unintuitive on a modern controller/Steam Deck, where the left stick is the expected primary-navigation input.** Distinct from the F10 options overlay, which is already left-stick/D-pad navigable (`port/src/optionsoverlay.c`). Scope: front-end menu input in `src/game/front.c` (or wherever N64 stick reads are gated by the active `controldef`) — likely wants a port-layer input remap for these menu contexts specifically, not a general control-scheme change. **M-141: root cause found + fixed, port-only.** `front.c`'s menu-nav code reads `joyGetStickX/Y` — the same emulated N64 "Control Stick" channel (`stick_x`/`stick_y`) that in-level gameplay uses. `port/src/input.c`'s gamepad poll routes that channel to the LEFT physical stick by default, but `naturalPitchMode` (D194/D238's SOLITARE-style scheme, **default-on**) reroutes it to the RIGHT stick for continuous look — and since menus share the channel, they silently inherited that reroute too. The poll already computed an unused `padMenuMode` flag (`current_menu != GE_MENU_RUN_STAGE/GE_MENU_INVALID`, previously only gating shoulder-button weapon-cycle edges); moved it earlier and added a `padMenuMode` branch that always sources `stick_x`/`stick_y` from the left stick outside a running stage, regardless of `naturalPitchMode` — matching the F10 overlay's existing left-stick convention (`optionsoverlay.c`). Zero effect on in-level control feel (the branch is inert whenever `current_menu == GE_MENU_RUN_STAGE`). Verified: build green; `-level_09`/`-level_20` unregressed (1100+ frames each, no crash) — bare `-level_XX` launches skip the front end entirely, so this only proves the gameplay path (unaffected) stays clean. **In-menu feel not interactively verified this session** (no physical gamepad available in this environment) — needs a human playtest with a controller to confirm the menu now navigates on the left stick as expected. diff --git a/port/src/libultra.c b/port/src/libultra.c index 3b48d744..786d137c 100644 --- a/port/src/libultra.c +++ b/port/src/libultra.c @@ -1279,26 +1279,55 @@ static void geEepromPatchAllCheats(u8 *buf) memset(&slots[5], 0, sizeof(ge_save_slot)); int changed = 0; - for (int i = 0; i < 5; i++) { - /* D259: a fresh ge007.eep is zero-filled, so every slot reads as - * all-zero. Normally such a slot fails fileValidateSaves' CRC and is - * reset to BLANKSAVEDATA (music_vol/sfx_vol = 0xFF); but this patch - * gives the slot a valid CRC below, so it survives with volume 0 - * -> silence. Emulate fileResetSave: seed max volume on all-zero - * slots only -- real saves (incl. a deliberately muted one) keep - * their bytes. */ - { + + /* D259 + D281: a fresh ge007.eep is zero-filled, so every slot reads as + * all-zero. Without this patch such a slot fails fileValidateSaves' CRC + * and becomes a free BLANKSAVEDATA slot (fileResetSave), after which + * fileBuildWriteNewSave fills the first free slot for each folder with + * no save, in folder order. This patch gives every slot a valid CRC + * below, so an all-zero slot would instead SURVIVE as-is: five slots all + * claiming folder 1 with volume 0 (D259: silence) and options 0 (D281: + * sight-on-screen, auto-aim, look-ahead and ammo display all off, which + * reads as "RMB aim broken"). D259 seeded only the volumes. Reproduce + * the game's full result instead: all-zero slots take the folders that + * no real slot holds, in order, built exactly as fileBuildWriteNewSave + * builds them (BLANKSAVEDATA + folder + not-free + bond); any left over + * become free BLANKSAVEDATA slots. Real saves keep their bytes. */ + { + extern void fileSetSaveFoldernum(void *save, u32 folder); + extern void fileSetSaveFlagDoReset(void *save, s32 enable); + extern void fileSetSelectedBond(void *save, s32 bond); + static const ge_save_slot blank = { + 0, 0, 0x80 /* SAVEFLAGS_SET(0,0,BOND_BROSNAN,1): free */, 0x00, + 0xFF, 0xFF, 0x3A /* DEFAULT_OPTIONS */, 0, 0, 0, 0, {0} + }; + int allzero[5], present[4] = {0, 0, 0, 0}; + for (int i = 0; i < 5; i++) { const u8 *raw = (const u8 *)&slots[i]; - int allzero = 1; + allzero[i] = 1; for (int b = 0; b < (int)sizeof(ge_save_slot); b++) - if (raw[b]) { allzero = 0; break; } - if (allzero && (slots[i].music_vol != 0xFF || - slots[i].sfx_vol != 0xFF)) { - slots[i].music_vol = 0xFF; - slots[i].sfx_vol = 0xFF; - changed = 1; + if (raw[b]) { allzero[i] = 0; break; } + if (!allzero[i] && !(slots[i].completion_bitflags & 0x80 /* DORESET */)) { + int f = slots[i].completion_bitflags & 0x7; /* SAVEFLAG_FOLDER */ + if (f < 4) present[f] = 1; /* MAX_FOLDER_COUNT */ } } + int folder = 0; + for (int i = 0; i < 5; i++) { + if (!allzero[i]) continue; + slots[i] = blank; + while (folder < 4 && present[folder]) folder++; + if (folder < 4) { + fileSetSaveFoldernum(&slots[i], (u32)folder); + fileSetSaveFlagDoReset(&slots[i], 0); + fileSetSelectedBond(&slots[i], folder); + present[folder] = 1; + } + changed = 1; + } + } + + for (int i = 0; i < 5; i++) { /* Cheat ids are level ids 0..19 (CHEAT_INPUT_BUFFER_SIZE == 20): * bits 0-7 in _1, 8-15 in _2, 16-19 in the low nibble of _3. */ if (slots[i].unlocked_cheats_1 != 0xFF ||