v0.2.1 hotfix batch: D259 audio silence, D260 wheel direction, D261 watch-nav repeat, D263 script order, D264 watch preview model

User playtest reports against v0.2.0:
- D259: geEepromPatchAllCheats left an all-zero fresh save at volume 0
  (silence) instead of seeding max volume like fileResetSave would.
- D260: swap wheel-up/wheel-down to match the PC wheel-menu's on-screen
  order (was inverted vs. user expectation).
- D261: watch-menu stick-held navigation had no repeat past the smooth-scroll
  band, so holding W/S or a pegged stick crawled one item per tap; added a
  bounded auto-repeat.
- D263: GE_INPUTSCRIPT stick tokens were applied after the stick was already
  written out and silently discarded (test-harness fix, kept from the D263
  Facility/Ourumov investigation, which itself found no port bug).
- D264: watch item-preview ModelRenderData template read across two globals
  that no longer sit adjacent on PC, zeroing renderdata.flags and gating all
  geometry emission in the preview's subdraw pass.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
This commit is contained in:
James Dansereau committed 2026-09-15 08:21:32 -04:00
1 parent b0dc9d1257
commit ffb786bad8
5 files changed
+121 -11

No files matched your search

+22
View File
@@ -12,6 +12,25 @@
> missions load and run crash-free, but the known issues below are real; > missions load and run crash-free, but the known issues below are real;
> feedback is very welcome. > feedback is very welcome.
### What's new since v0.2.0
Small hotfix batch from user playtest feedback on v0.2.0:
- **Fixed: `All unlocked` + a fresh save produced silent music/SFX** (D259).
A brand-new save now seeds max volume the same way the N64 does, whether
or not `All unlocked` is on.
- **Mouse wheel weapon cycling now matches the on-screen wheel-menu order**:
scroll up = previous weapon, scroll down = next (D260).
- **Watch menu: holding a direction now auto-repeats** instead of crawling
one item per key press/notch (D261).
- **Fixed: the watch menu's item-preview 3D model wasn't rendering** —
weapons now show their model when selected on the watch inventory page
(D264).
- Investigated the Facility report of Ourumov never executing Trevelyan:
confirmed it's proximity/line-of-sight-triggered exactly as on the N64
(walk toward the room), not a port bug — the full scripted beat, including
the execution shot, fires correctly (D263).
### What's new since v0.1.0 ### What's new since v0.1.0
- **Drop-in ROM, no tooling**: unpack the bundle, drop your NTSC-U (US) ROM - **Drop-in ROM, no tooling**: unpack the bundle, drop your NTSC-U (US) ROM
@@ -77,6 +96,9 @@
selected** (e.g. the MSAA value appears both on its own row and again at selected** (e.g. the MSAA value appears both on its own row and again at
the panel bottom); earlier builds showed it as an intermittent 4K- the panel bottom); earlier builds showed it as an intermittent 4K-
fullscreen ghost of the top row. fullscreen ghost of the top row.
- **Surface 1: the 2D billboard trees near the start render as a solid wall
of tree texture** instead of discrete sprites (D236). Under active
investigation; no fix in this release.
- **Steam Deck: one intermittent crash remains**: the v0.1.0-era Facility - **Steam Deck: one intermittent crash remains**: the v0.1.0-era Facility
crash is confirmed fixed on Deck hardware, but a SIGSEGV can occasionally crash is confirmed fixed on Deck hardware, but a SIGSEGV can occasionally
occur during heavy firefights (D255; seen on Bunker and Frigate). The occur during heavy firefights (D255; seen on Bunker and Frigate). The
+15 -11
View File
@@ -1177,6 +1177,14 @@ unsigned inputComputePad(int idx, signed char *stick_x, signed char *stick_y)
} }
} }
if (idx == 0 && scriptIsActive()) {
/* D263: apply BEFORE the stick is written out -- scripted stick
* tokens (SUP/SDOWN/SLEFT/SRIGHT) used to be discarded. */
button = scriptApply(button);
sx = scriptCurSX;
sy = scriptCurSY;
}
if (sx > STICK_MAX) sx = STICK_MAX; if (sx > STICK_MAX) sx = STICK_MAX;
if (sx < -STICK_MAX) sx = -STICK_MAX; if (sx < -STICK_MAX) sx = -STICK_MAX;
if (sy > STICK_MAX) sy = STICK_MAX; if (sy > STICK_MAX) sy = STICK_MAX;
@@ -1185,12 +1193,6 @@ unsigned inputComputePad(int idx, signed char *stick_x, signed char *stick_y)
if (stick_x) *stick_x = (signed char)sx; if (stick_x) *stick_x = (signed char)sx;
if (stick_y) *stick_y = (signed char)sy; if (stick_y) *stick_y = (signed char)sy;
if (idx == 0 && scriptIsActive()) {
button = scriptApply(button);
sx = scriptCurSX;
sy = scriptCurSY;
}
if (configGetInputLog() && (button || sx || sy)) { if (configGetInputLog() && (button || sx || sy)) {
sysLogPrintf(LOG_NOTE, "GE_INPUTLOG cont%d: btn=%04x stick=(%d,%d)", sysLogPrintf(LOG_NOTE, "GE_INPUTLOG cont%d: btn=%04x stick=(%d,%d)",
idx, button, sx, sy); idx, button, sx, sy);
@@ -1297,11 +1299,13 @@ void inputSuspendForOverlay(void)
void inputPostWheel(int notches) void inputPostWheel(int notches)
{ {
/* D223: keep the direction -- up cycles to the next weapon, down to the /* D223: keep the direction. v0.2.1: swapped per user request -- up now
* previous one. A burst of same-direction notches just re-arms the same * cycles to the PREVIOUS weapon, down to the NEXT one (PC convention
* pulse; a direction change mid-sequence restarts it (last wins). */ * "scroll down = advance list"). A burst of same-direction notches just
if (notches > 0) { wheelFwd = WHEEL_FWD_POLLS; wheelBack = 0; } * re-arms the same pulse; a direction change mid-sequence restarts it
else if (notches < 0) { wheelBack = 2; wheelFwd = 0; } * (last wins). */
if (notches > 0) { wheelBack = 2; wheelFwd = 0; }
else if (notches < 0) { wheelFwd = WHEEL_FWD_POLLS; wheelBack = 0; }
} }
/* Called from the host event pump on SDL_CONTROLLERDEVICEADDED/REMOVED. /* Called from the host event pump on SDL_CONTROLLERDEVICEADDED/REMOVED.
+19
View File
@@ -1278,6 +1278,25 @@ static void geEepromPatchAllCheats(u8 *buf)
int changed = 0; int changed = 0;
for (int i = 0; i < 5; i++) { 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. */
{
const u8 *raw = (const u8 *)&slots[i];
int allzero = 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;
}
}
/* Cheat ids are level ids 0..19 (CHEAT_INPUT_BUFFER_SIZE == 20): /* 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. */ * bits 0-7 in _1, 8-15 in _2, 16-19 in the low nibble of _3. */
if (slots[i].unlocked_cheats_1 != 0xFF || if (slots[i].unlocked_cheats_1 != 0xFF ||
+14
View File
@@ -1721,7 +1721,21 @@ void gunRenderFirstPersonGunModels(Gfx **gdlptr)
Gfx *set_enviro_fog_for_items_in_solo_watch_menu(Gfx *gdl, ITEM_IDS itemid, Mtxf *mtx, s32 arg3, s32 arg4) Gfx *set_enviro_fog_for_items_in_solo_watch_menu(Gfx *gdl, ITEM_IDS itemid, Mtxf *mtx, s32 arg3, s32 arg4)
{ {
#ifdef PORT
/* D264 (ABI/layout class, cf. D140): on N64 this 64-byte copy spans two
* adjacent globals — D_80035D00 (zero) and D_80035D04 {1, 3, 0..} — i.e.
* the ModelRenderData template documented above D_80035D00 in gun.c
* (zbufferenabled=TRUE, flags=3, rest zero; CULLMODE_BOTH is 0). The PC
* link puts them in separate .bss/.data sections, so the read was all
* zeros: flags==0 gates every geometry node in subdraw() and the watch
* item preview emitted nothing. Use the explicit template (identical
* values to the N64 ROM bytes). */
ModelRenderData renderdata = {0};
renderdata.zbufferenabled = TRUE;
renderdata.flags = 3;
#else
ModelRenderData renderdata = *((ModelRenderData *) (&D_80035D00)); ModelRenderData renderdata = *((ModelRenderData *) (&D_80035D00));
#endif
ModelHeader model; ModelHeader model;
u8 spb8[0x80]; u8 spb8[0x80];
s32 padb4; s32 padb4;
+51
View File
@@ -67,6 +67,44 @@
#define GE_WATCH_STICK_FASTDOWN(pad) (joyGetStickY(pad) < -0x46) #define GE_WATCH_STICK_FASTDOWN(pad) (joyGetStickY(pad) < -0x46)
#endif #endif
#ifdef PORT
/* v0.2.1 (user report: "watch menu sensitivity for wasd/controller"): D118d
* removed the N64 slam-the-stick fast scroll because digital sources peg
* past its threshold -- but that left |stickY| >= 0x46 with NO repeat at all
* (the latched path steps once per press), so holding W/S or a full stick
* press crawled one item per tap through the list. Restore fast navigation
* as a bounded auto-repeat: while the stick is held past the smooth-scroll
* band (0x1f..0x45, untouched), step one extra item every GE_WATCH_NAV_RATE
* frames after an initial GE_WATCH_NAV_DELAY hold. The first press still goes
* through the latched single-step path, so a tap behaves exactly as before.
* Returns -1 (up) / +1 (down) on repeat frames, 0 otherwise. */
#define GE_WATCH_NAV_DELAY 15
#define GE_WATCH_NAV_RATE 6
static int s_watchNavHoldDir = 0; /* -1 up, +1 down, 0 none */
static int s_watchNavHoldFrames = 0;
static s32 geWatchStickFastStep(void)
{
int dir = 0;
if (joyGetStickY(PLAYER_1) >= 0x46) dir = -1;
else if (joyGetStickY(PLAYER_1) <= -0x45) dir = 1;
if (dir != s_watchNavHoldDir) {
s_watchNavHoldDir = dir;
s_watchNavHoldFrames = 0;
return 0; /* new direction: the latched press path already stepped */
}
if (dir == 0)
return 0;
if (++s_watchNavHoldFrames > GE_WATCH_NAV_DELAY &&
s_watchNavHoldFrames % GE_WATCH_NAV_RATE == 0)
return dir;
return 0;
}
#else
static s32 geWatchStickFastStep(void) { return 0; }
#endif
// bss // bss
Mtx gfx_background_8007B0A0; Mtx gfx_background_8007B0A0;
Mtx gfx_background_8007B0E0; Mtx gfx_background_8007B0E0;
@@ -1107,6 +1145,19 @@ after_updown:
watch_inventory_cursor_pos += 1.0f; watch_inventory_cursor_pos += 1.0f;
} }
#ifdef PORT
/* v0.2.1: bounded auto-repeat for a stick held past the smooth band
* (see geWatchStickFastStep). */
{
s32 fast = geWatchStickFastStep();
if (fast < 0 && g_curWatchItemIndex > 0 && !watch_item_is_actively_selected)
watch_inventory_cursor_pos += (f32) fast;
else if (fast > 0 && (s32) watch_inventory_cursor_pos < count - 1
&& !watch_item_is_actively_selected)
watch_inventory_cursor_pos += (f32) fast;
}
#endif
if (is_holding_less_than_10_up_on_stick() || is_holding_less_than_10_down_on_stick()) if (is_holding_less_than_10_up_on_stick() || is_holding_less_than_10_down_on_stick())
{ {
watch_stick_y_prev_active = 1; watch_stick_y_prev_active = 1;