diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index a63a70bd..b69a3fd9 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -335,6 +335,52 @@ prior behaviour. Full review: **`docs/M-26-QOL-REVIEW.md`**. boot crash-free 6/6 GE_PCDUMP frames (unregressed); `ge007.ini` gains a populated `[Window]` block on exit. +## Done this session (M-27) — front-end/pause bucket: D140 + D141 FIXED + +Started the parked "front-end / transitions / pause / watch" bucket. +**Pressing Start in-level no longer crashes** — the pause / watch menu +renders (weapon page verified via a temp `GE_AUTOPAUSE` input probe, +since removed). Cosmetics unchanged and still parked: mirrored watch text +(D114/D116), dark/faint weapon model (D75). + +- **D140 (FIXED)** `src/game/bondview.h`, `src/game/bondview2.c` — + pause-menu crash `bondviewRenderWatch` → `bondviewTransformManyPos- + ToViewMatrix(field_23C=NULL)`. `something_with_watch_object_instance` + (N64 player +0x230) is a **`struct Model` + RW-pool punned into a + 0x184-byte field-run**; `field_23C`/`watch_scale_destination`/ + `pause_watch_related_adjust` are actually `.render_pos`/`.scale`/ + `.animframe1`. PC `sizeof(struct Model)` grows → the aliases break → + NULL `render_pos`. Fix: real inline `struct Model` + + `u32 watchRwPool[192]` under `#ifdef PORT`; the 3 named reads redirect to + the member via `GE_WATCH_{ANIMFRAME,SCALE,RENDERPOS}` macros (N64 `#else` + = verbatim field). D56 branch of `sub_GAME_7F07E7CC` now uses the inline + pool. D100/D102 pattern. §F/§H **D140**, PORT-LEARNINGS §A. +- **D118d (FIXED, feel unconfirmed)** `src/game/options.c` — user bug report: + watch inventory list over-scrolls with keyboard W/S (one press skips several + items). GE's "slam the stick" fast-scroll is a raw per-frame level check + (`joyGetStickY < -0x46`); keyboard/digital pads sit at max every frame. + `#ifdef PORT` drops the stick term → list-nav goes through the latched + single-step path (one step per press). Build green, `-level_09` unregressed; + couldn't verify the feel headlessly (INVENTORY watch page unreachable without + page-cycling input) — **needs an in-game check**. Latent sibling in `front.c` + menu nav (same `joyGetStick*InRange` level-check pattern). §F **D118d**. +- **D141 (FIXED)** `src/game/gunfire.c` — the crash D140 exposed. + `set_enviro_fog_for_items_in_solo_watch_menu` walks `bodymodel->Switches[]` + (a `ModelNode*` array) with raw byte offsets `+0x48`/`+0x5c`, `j += 4` + (4-byte-pointer constants) → PC 8-byte stride reads garbage → bogus node + → AV in `modelGetNodeRwData`. Fix: `#ifdef PORT` uses + `Switches[18 + (j>>2)]` / `Switches[23 + (j>>2)]`. §F **D141**, + PORT-LEARNINGS §B (cf. D128). +- **Verified:** build green (`ntsc-final`); `-level_09` framediff 3/3, + `-level_20` crash-free (both unregressed); autopause probe → watch page + renders 400+ frames, no `ge007.crash.log`. +- **D139** (stage-unload, M-23, still UNVERIFIED) not exercised — it needs + a real level-exit teardown, not a pause. Next front-end item. +- **Still in the bucket:** D139 verify; unpause / watch-page navigation + (the temp probe didn't test exiting the watch); level exit → MISSION + COMPLETE → debrief → auto-advance; main-menu / mission-select walkthrough + (WS2); watch objectives page + gadgets. + ## Next task (M-24 — Opus 5) **Scope call (user, M-23):** D139 (stage unload) + D140 (pause menu / watch) @@ -366,9 +412,11 @@ during normal `-level_XX` play. parser to D135, `~3373-3646`) — do it before a level where wall-shooting is heavy (i.e. soon). -**Parked bucket — front-end / transitions (one focused session):** -- D139 verify (stage unload / `cleanupObjects`) -- D140 (pause menu → `bondviewRenderWatch` → NULL `field_23C` matrix) +**Parked bucket — front-end / transitions (STARTED M-27):** +- ~~D140 (pause menu → `bondviewRenderWatch` → NULL `field_23C`)~~ FIXED M-27 + (+ D141, the watch-page crash it exposed). Pause now renders. +- D139 verify (stage unload / `cleanupObjects`) — still needs a real exit +- unpause / watch-page navigation (M-27 probe only tested opening it) - level exit → MISSION COMPLETE → debrief → auto-advance to next briefing - main menu / mission-select / difficulty-select playtest (WS2) - the watch (objectives page, gadgets) — needed for the WS6 exit/objective diff --git a/docs/PCPortResearch.md b/docs/PCPortResearch.md index 8a89dd27..7467bcc8 100644 --- a/docs/PCPortResearch.md +++ b/docs/PCPortResearch.md @@ -829,7 +829,9 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D135 | **Firefight crash — `bgTestHitOnObj` (`propobj.c:8446`) is an unported N64 GBI parser (M-22, WS6 playtest).** The object bullet-hit test walks a model's display list command stream with hardcoded 8-byte-N64-`Gfx` byte/word indices (`*(s8*)gdl`, `((u32*)gdl)[1]`, `((u8*)gdl)[5..7]`, the G_TRI4 nibble reads, the backward `G_SETTIMG` scan). On PC the model GDL is the 16-byte `Gfx` layout (`d43_emit.py`, `{u64 w0; u64 w1}`), so every access reads the wrong bytes → the walk desyncs on the first `G_VTX`/`G_TRI1` → `v->coord.x` deref off the end of the DL → AV (`0x709ae02a`, a near-miss past a live DRAM ptr = the §B signature). Triggered by `propobjFindHit` every time a shot ray resolves a hit on an object model (guards' dropped weapons, attached objects, shootable props) — invisible to `level_sweep.sh` (no weapon fire). Two sibling faults seen the same session (`texSelect`/`texSetRenderMode` wild `gdl`, `objFreeEmbedmentOrProjectile` `prop->obj`) are likely the same corrupted-object cascade. PD `bgTestHitOnObj` (`pd_port/src/game/bg.c:3635`) is the fully-ported ground truth. | FIXED (`src/game/propobj.c`, `#ifdef PORT` — reads `gdl->words.w0/.w1` shifts). **Second fault, same walker:** after the parser fix the Facility firefight then hit `0xc0000094` (int div-by-zero) at `objHit` `propobj.c:9717` `randomGetNext() % impact_sounds->thing2_len` — the ported texnum-recovery (`*(s16*)(phys(w1) − 8)`) returned a bogus positive `texturenum` (`(u32)` cast truncated a 64-bit `words.w1`; the N64 "texnum 8 bytes before the texture data" layout isn't guaranteed for the port's converted/replaced GDLs) → `g_HitTypeSounds[texnum & 0xf]` picked an entry with `thing2_len == 0`. Fixed: the PORT texnum branch now always returns `-1` (→ `g_HitTypeSounds[0]` = `isnd_default`, `thing2_len == 1`, safe). `texturenum` only flavours the bullet-impact sound/decal, so generic-hit is an acceptable degradation (park with D77). **Follow-up:** the BG room-geometry sibling `sub_GAME_…` (`bg.c:~3373-3646`, walks the D85-widened `ptr_expanded_mapping_info`) has the identical unported parser pattern — latent, fires on shooting walls/floor; not yet ported. | | D136 | **Stage-unload crash — `objFree` `obj->prop` reads float garbage (M-22, WS6 Facility playthrough).** After D135 cleared the firefight crashes the level is completable; then on teardown `lvlUnloadStageTextData` → `cleanupObjects` (`cleanup_objects.c:42`) → `objFreePermanently` → `objFree` (`propobj.c:994`) → `objFreeEmbedmentOrProjectile(obj->prop)` faults at `:888` (`prop->obj` deref). Real gdb `bt`: `obj` = `0x701bd730` (valid) but `obj->prop` = `0x3e567400405c30c0` — two packed `f32` (~0.21, ~3.44), i.e. a `coord3d` read as a pointer → `ObjectRecord.prop` (N64 `/*0x10*/`, after `inherits PropDefHeaderRecord` + `s16 obj; s16 pad; u32 flags; u32 flags2;`) lands at the wrong PC offset for this object's propDef type, OR the record is undersized and setup's `->prop` store never happened / was clobbered. Same family as D122/D132 (`tools_pc/d88_propdefs.py` per-type record sizing vs the compiled `ObjectRecord`/subtype layout). Type not yet identified — needs a gdb run with a breakpoint on `objFree` to read `obj->type` (`cleanupObjects` frees WEAPON/AMMO/MAGAZINE/COLLECTABLE/MONITOR/RACK/AUTOGUN/HAT/ARMOUR/GAS_RELEASING/VEHICHLE/AIRCRAFT/GLASS/SAFE/TANK/TINTED_GLASS via this path). Mission completes; crash is before the next briefing loads. | ROOT-CAUSED → **D139** (it was NOT a layout bug — `cleanupObjects` read the type from the wrong byte on LE and walked off the propDef blob). | | D139 | **Stage-unload crash root cause + fix (M-23).** `cleanupObjects` (`cleanup_objects.c`) walks `g_CurrentSetup.propDefs` with `(u8)obj[0]` as the record type — correct only because the header word `[u16 extrascale][u8 state][u8 type]` is **big-endian** on N64, so `type` is the low byte. On little-endian PC the low byte is `extrascale`, so `(u8)obj[0]` is never `PROPDEF_END` → the `while` runs off the end of the blob, and the `switch` dispatches on garbage → `objFreePermanently()` on records that aren't `ObjectRecord`s → `obj->prop`/`obj->model` read adjacent float data (`0x3e567400405c30c0`) → AV in `objFreeEmbedmentOrProjectile` (`propobj.c:888`). `sizepropdef()` and every other propDef consumer already use `pdef->type` (struct member, offset 3) — only this one spot used the raw byte cast. Not a `d88_propdefs.py` layout issue at all. | FIXED (`src/game/cleanup_objects.c`, `#ifdef PORT` — `CLEANUP_PDTYPE(o)` = `((PropDefHeaderRecord*)o)->type`). **UNVERIFIED** — the fast teardown test (pause→abort) hit D140 first; `-level_09` unregressed. PORT-LEARNINGS §C. | -| D140 | **Pause-menu crash (M-23, OPEN).** `maybe_mp_interface` → `bondviewRenderWatch` (`bondview2.c:8604`) → `bondviewTransformManyPosToViewMatrix(g_CurrentPlayer->field_23C, objheader->numMatrices)` with `field_23C == NULL` (`arg1 == 9`) → `matrix_4x4_copy(src=0x0, …)` (`matrixmath.c:130`) AV. `struct player.field_23C` is the watch model's per-node view-matrix cache — never allocated on PC, or `field_23C` is at the wrong `struct player` offset (`docs/AUDIT-M6-player-offsets.md`). Blocks the WS6 pause-menu / `objective_status` checks + watch gadgets. D75 3D-model-transform family. Files: `src/game/bondview2.c` (`bondviewRenderWatch` ~8540-8608), `src/bondtypes.h` (`struct player`). | OPEN | +| D140 | **Pause-menu crash (M-23 open → M-27 fixed).** `bondviewRenderWatch` (`bondview2.c:8604`) → `bondviewTransformManyPosToViewMatrix(g_CurrentPlayer->field_23C, objheader->numMatrices)` with `field_23C == NULL` → `matrix_4x4_copy(src=0x0)` AV. Root cause (NOT "cache never allocated"): `something_with_watch_object_instance` (N64 player +0x230) is a **`struct Model` (0xBC) + its RW-data pool (0xC8) punned into a 0x184-byte hole** (`field_234..field_3B0`); three of those "fields" are Model members the game reads by name — `watch_scale_destination`==`.scale`, `pause_watch_related_adjust`==`.animframe1`, **`field_23C`==`.render_pos`**. `render_pos` IS populated (by `instcalcmatrices` via `subcalcmatrices` at `:8524`), but on x86-64 `sizeof(struct Model)` grows (4→8B ptrs) so `field_23C` no longer overlaps `.render_pos` → reads Model padding (~0) → NULL. Same class as D100 (gait Model) / D102 (weapon Model) / D56 (this very watch Model's pool). Fix: `#ifdef PORT` makes `something_with_watch_object_instance` a real inline `struct Model` + `u32 watchRwPool[192]` in `struct player` (`bondview.h`); `bondview2.c` redirects the 3 named reads to the member via `GE_WATCH_{ANIMFRAME,SCALE,RENDERPOS}` macros (N64 `#else` = the verbatim field). The D56 branch of `sub_GAME_7F07E7CC` drops its `static u8 watchRwPool[0xC8]` for the inline pool. | FIXED (`src/game/bondview.h`, `src/game/bondview2.c`; `#ifdef PORT`). Exposed the next watch-render crash → **D141**. `-level_09`/`-level_20` unregressed (3/3). PORT-LEARNINGS §A. | +| D118d | **Watch-menu list over-scrolls with keyboard (M-27, user bug report).** `game_options_inventory_navigation` / `sub_GAME_7F0A611C` (`options.c`) have a "slam the stick" fast-scroll: a raw *level* check `joyGetStickY(PLAYER_1) < -0x46` / `>= 0x47` that steps `watch_inventory_cursor_pos` by 1 **every frame** it's held. An N64 stick rarely sustains past ~0x46 on-axis so a human taps it; keyboard W/S (and SDL pads pegged at 0x80) sit there every frame → one keypress skips several items (breaks item selection needed to finish the game). Fix: `#ifdef PORT` macro `GE_WATCH_STICK_FAST{UP,DOWN}` → `0` for the port — stick list-nav then goes only through the latched single-step path (`watch_stick_y_pressed_*`, one step per press, existing game mechanism) and the -0x1e..-0x45 smooth-scroll band; D-pad/C-buttons keep edge-repeat. N64 `#else` = verbatim. | FIXED (`src/game/options.c`, `#ifdef PORT`). Build green, `-level_09` unregressed. **Feel to be confirmed in playtest** (probe couldn't reach the INVENTORY watch page headlessly). D118 input family. Latent sibling: `front.c` menu nav uses `joyGetStick*InRange` level checks the same way. | +| D141 | **Watch-menu item-model crash (M-27), exposed by D140.** Pressing Start → watch page renders → `set_enviro_fog_for_items_in_solo_watch_menu` (`gunfire.c:1620`) → `modelGetNodeRwData(&model, root)` with `root` garbage → AV reading `root->Opcode` (`model.c:478`). `gunfire.c:1720-1743` walks `bodymodel->Switches[]` (a `ModelNode*` array) with **raw byte offsets** `((u8*)Switches) + j + 0x48` / `+ 0x5c` and `j += 4` — assumes 4-byte N64 pointers. `0x48/4=18`, `0x5c/4=23`, `k=j>>2=0..4` → intent is `Switches[18+k]` and `Switches[23+k]`. PC's 8-byte stride makes the raw math read misaligned garbage → bogus non-NULL `ModelNode*`. PORT-LEARNINGS §B (raw hardcoded stride into a pointer array, cf. D128). Fix: `#ifdef PORT` uses `bodymodel->Switches[18 + (j>>2)]` / `[23 + (j>>2)]`. | FIXED (`src/game/gunfire.c`, `#ifdef PORT`). Watch page now renders (weapon page verified — mirrored text D114/D116, dark weapon model D75, both parked cosmetic). `-level_09`/`-level_20` unregressed. | | D130 | crash class C2 (Facility `-level_34`, Runway `-level_35`): `import_texture_i8` AV on a wild texture pointer (`0x72181ee8`), baked into the HUD glyph DL by `gDPLoadTextureBlock(gdl, curchar->pixeldata, …)` when the level-title string ("Chemical Warfare Facility #2") renders `#`/`"`. Root cause: `romdataFixupFont` (`port/src/romdata.c`) converts the N64 24B fontchar array to the PC 32B array **in place**, forward-field, backward-glyph. PC−N64 stride = 8, so for glyphs 0/1/2 `d` overlaps `s` by less than the 24B read span and an early field write clobbers a later field's source mid-`for k<5` loop → glyph 1's `width` = `bswap(index)` = `0x01000000`, glyph 2's `pixeldata` = `pcPixOff+(index−pixStart)` ≈ `0x020002e8` → `+= font_base` → wild → fast3d AV. On N64 the array is read-only rodata, never re-laid-out, so no bug there. NOT the model-GDL relocation (`sub_GAME_7F0762E0` / `texLoadFromGdl`) the D124-Facility addendum suspected — `gdl` there is still a segmented `0x05xxxxxx` value so the `& 0x00ffffff` masks are correct, and `texLoadFromGdl` never copies a `G_SETTIMG` for these levels. | resolved — stage all 6 N64 fields in a local `f[6]` before writing any. Facility + Runway now PASS (16→18/21). Probes reverted. | Phase 2 replaced the Phase-1 demo loop with the real `mainproc()` on real OS diff --git a/docs/PORT-LEARNINGS.md b/docs/PORT-LEARNINGS.md index 05e752cb..28e501c6 100644 --- a/docs/PORT-LEARNINGS.md +++ b/docs/PORT-LEARNINGS.md @@ -21,6 +21,11 @@ state. (rwdata record count), D67 (image_entry), D79 (bg_room_data), D98 (struct player alloc), D100 (player.model inline Model), D101/D102 (ModelNode*/weapon Model puns), D115 (gunfire THROW* raw offsets), + D140 (watch Model punned into a `struct player` field-run: PC + `sizeof(struct Model)` grows so the "fields" that aliased `.render_pos` / + `.scale` / `.animframe1` on N64 no longer overlap → NULL `render_pos` → + pause-menu crash; fix = real inline `struct Model` + pool, redirect the + named reads to the member — same as D100/D102), D119 (`weapons_held[]->chr` punned as `ChrRecord*` to read `.act_*.attack_item` — aliased `WeaponObjRecord.weaponnum` at 0x80 on N64 via act-union@0x2C+84; act union moves to ~0x38 on PC). @@ -127,6 +132,14 @@ expressed in N64 `Gfx`/`Vtx` units is **half-size** on PC. `g_BgPortals[portal].controlbytes1 |= PORTALFLAG_SPECIAL`). Grep for `<< 3` / `* 8` / `+ 6` style raw offsets into any struct that gained a pointer field. + D141: `set_enviro_fog_for_items_in_solo_watch_menu` (`gunfire.c:1720`) + indexed a `ModelNode*` array (`ModelFileHeader.Switches`) as + `*(ModelNode**)((u8*)Switches + j + 0x48)` with `j += 4` — the `0x48`/ + `0x5c` and the step are 4-byte-pointer constants. PC 8-byte stride → wrong + + misaligned slot → bogus non-NULL node → crash in `modelGetNodeRwData`. + Fix: `Switches[18 + (j>>2)]` / `Switches[23 + (j>>2)]` under `#ifdef PORT` + (`0x48/4=18`, `0x5c/4=23`). Grep every `(TYPE**)((u8*)arr + )` and + `arr[i << k]` where the array element is a pointer. ## C. Big-endian rodata / ROM data read on little-endian PC diff --git a/src/game/bondview.h b/src/game/bondview.h index 1bf1e550..1b7508ab 100644 --- a/src/game/bondview.h +++ b/src/game/bondview.h @@ -708,6 +708,23 @@ struct player */ f32 pause_watch_related_scaled; +#ifdef PORT + /* D140 / D56 / D53.2: N64 puns a `struct Model` (0xBC) followed immediately + * by its RW-data pool (0xC8) into this 0x184-byte hole (player +0x230 .. + * +0x3B4). The decomp splits it as this field + `s32 field_234..field_3B0`; + * three of those "fields" are actually watch-Model members read by name: + * watch_scale_destination == .scale (Model +0x14) + * pause_watch_related_adjust == .animframe1 (Model +0x28) + * field_23C == .render_pos (Model +0x0c) + * On x86-64 `sizeof(struct Model)` grows (4->8B pointers) so the punned + * layout desyncs: field_23C stops overlapping render_pos -> NULL -> crash in + * bondviewTransformManyPosToViewMatrix on pause (D140), and the grown Model + * overruns the live fields below. Give the Model + pool real inline storage + * (D100 gait-model / D102 weapon-model pattern); the three named reads are + * redirected to the member at their bondview2.c use sites. */ + struct Model something_with_watch_object_instance; + u32 watchRwPool[192]; /* N64 pool 0xC8 B; PC RW records ~2x (cf. D102) */ +#else s32 something_with_watch_object_instance; s32 field_234; @@ -811,6 +828,7 @@ struct player s32 field_3A8; s32 field_3AC; s32 field_3B0; +#endif /** * Copy of buttons pressed. diff --git a/src/game/bondview2.c b/src/game/bondview2.c index e75692e3..412d757f 100644 --- a/src/game/bondview2.c +++ b/src/game/bondview2.c @@ -168,6 +168,19 @@ #define aZ4_0f "z %4.0f" #define aS3d "%s %3d" +#ifdef PORT +/* D140: on N64 these three struct-player "fields" alias members of the watch + * Model punned at `something_with_watch_object_instance` (see bondview.h). + * The pun grows on PC, so the alias breaks; redirect to the real member. */ +#define GE_WATCH_ANIMFRAME (g_CurrentPlayer->something_with_watch_object_instance.animframe1) +#define GE_WATCH_SCALE (g_CurrentPlayer->something_with_watch_object_instance.scale) +#define GE_WATCH_RENDERPOS (g_CurrentPlayer->something_with_watch_object_instance.render_pos) +#else +#define GE_WATCH_ANIMFRAME (g_CurrentPlayer->pause_watch_related_adjust) +#define GE_WATCH_SCALE (g_CurrentPlayer->watch_scale_destination) +#define GE_WATCH_RENDERPOS (g_CurrentPlayer->field_23C) +#endif + vec3d g_ForceBondMoveOffset; @@ -3143,20 +3156,16 @@ void sub_GAME_7F07E7CC(void) { } #endif #ifdef PORT - /* PC port (D56, docs/PCPortResearch.md): the watch Model is embedded in - * struct player at N64 +0x230 and its RW-data pool at +0x2EC. On x86-64 - * the pointer fields before 0x230 shift the region to +0x24C (probe: - * vsize.c, not 8-aligned — unaligned pointer stores are fine on x86-64, - * D53.2), and the grown Model (0xE8 vs 0xBC) leaves only 0x9C of the N64 - * 0xC8-byte pool capacity in-struct — so take the Model by field name and - * host the pool in a static buffer of the N64 capacity. All rwdata access - * goes through Model.datas (set by modelInit), so nothing else references - * the old raw offsets. */ + /* PC port (D56/D140): the watch Model was punned at N64 player+0x230 with + * its RW-data pool at +0x2EC. On x86-64 `sizeof(struct Model)` grows so the + * pun overruns the live watch fields below it — D140 gave the Model and its + * pool real inline storage in `struct player` (`something_with_watch_object_ + * instance` is now a `struct Model`, `watchRwPool` follows it). All rwdata + * access goes through Model.datas (set by animInit). */ { - static u8 watchRwPool[0xC8]; /* N64: embedded at player+0x2EC */ - Model *watch = (Model *)&g_CurrentPlayer->something_with_watch_object_instance; + Model *watch = &g_CurrentPlayer->something_with_watch_object_instance; - animInit(watch, itemheader, (u32 *)watchRwPool); + animInit(watch, itemheader, g_CurrentPlayer->watchRwPool); modelSetScale(watch, c_item_entries[41].scale * 0.10000001f); modelSetAnimation(watch, (ModelAnimation *)&ptr_animation_table->data[(s32)&ANIM_DATA_bond_watch], 0, 0.0f, 0.5f * watch_transition_time, 0.0f); g_CurrentPlayer->step_in_view_watch_animation = 0; /* N64: *(s32*)(player+0x220) */ @@ -3184,7 +3193,7 @@ void bondviewSetPauseWatchRelated(f32 arg0) } else { - g_CurrentPlayer->pause_watch_related_scaled = (20.0f - g_CurrentPlayer->pause_watch_related_adjust) / arg0; + g_CurrentPlayer->pause_watch_related_scaled = (20.0f - GE_WATCH_ANIMFRAME) / arg0; } g_CurrentPlayer->step_in_view_watch_animation = 1; @@ -3205,7 +3214,7 @@ void bondviewSetPauseWatchRelatedAlt(f32 arg0) } else { - g_CurrentPlayer->pause_watch_related_scaled = g_CurrentPlayer->pause_watch_related_adjust / arg0; + g_CurrentPlayer->pause_watch_related_scaled = GE_WATCH_ANIMFRAME / arg0; } g_CurrentPlayer->step_in_view_watch_animation = 2; @@ -3463,7 +3472,7 @@ void bondviewWatchAnimationTick(void) if ((g_CurrentPlayer->step_in_view_watch_animation != 0) && (g_CurrentPlayer->step_in_view_watch_animation != 3)) { - sp30 = ((20.0f - g_CurrentPlayer->pause_watch_related_adjust) * 40.0f) / 20.0f; + sp30 = ((20.0f - GE_WATCH_ANIMFRAME) * 40.0f) / 20.0f; } if ((g_CurrentPlayer->pause_state == 0) || (g_CurrentPlayer->pause_state == 2) || (g_CurrentPlayer->pause_state == 3)) @@ -3587,7 +3596,7 @@ void bondviewWatchAnimationTick(void) if ((g_CurrentPlayer->step_in_view_watch_animation != 0) && (g_CurrentPlayer->step_in_view_watch_animation != 3)) { - sp28 = (g_CurrentPlayer->pause_watch_related_adjust * 40.0f) / 20.0f; + sp28 = (GE_WATCH_ANIMFRAME * 40.0f) / 20.0f; } sp20 = sp28 + 20.0f; @@ -8524,16 +8533,16 @@ Gfx *bondviewRenderWatch(Gfx *gdl) subcalcmatrices(&renderdata, (Model *) (&g_CurrentPlayer->something_with_watch_object_instance)); nodepos = (f32 *) objheader->Switches[0]->Data; time = watch_time_0; - t = g_CurrentPlayer->pause_watch_related_adjust / 20.0f; + t = GE_WATCH_ANIMFRAME / 20.0f; if (t > 1.0f) { t = 1.0f; } - targetpos.x = matrices->m[3][0] + (((g_CurrentPlayer->field_1D4 - (nodepos[0] * g_CurrentPlayer->watch_scale_destination)) - matrices->m[3][0]) * t); - targetpos.y = matrices->m[3][1] + (((g_CurrentPlayer->field_1D8 + (nodepos[2] * g_CurrentPlayer->watch_scale_destination)) - matrices->m[3][1]) * t); - targetpos.z = matrices->m[3][2] + (((g_CurrentPlayer->pause_watch_position - (nodepos[1] * g_CurrentPlayer->watch_scale_destination)) - matrices->m[3][2]) * t); + targetpos.x = matrices->m[3][0] + (((g_CurrentPlayer->field_1D4 - (nodepos[0] * GE_WATCH_SCALE)) - matrices->m[3][0]) * t); + targetpos.y = matrices->m[3][1] + (((g_CurrentPlayer->field_1D8 + (nodepos[2] * GE_WATCH_SCALE)) - matrices->m[3][1]) * t); + targetpos.z = matrices->m[3][2] + (((g_CurrentPlayer->pause_watch_position - (nodepos[1] * GE_WATCH_SCALE)) - matrices->m[3][2]) * t); matrix_4x4_set_basis_and_position_target(&targetmtx, 0.0f, 0.0f, 0.0f, g_CurrentPlayer->field_1E0, g_CurrentPlayer->field_1E4, g_CurrentPlayer->field_1E8, g_CurrentPlayer->field_1EC, g_CurrentPlayer->field_1F0, g_CurrentPlayer->field_1F4); matrix_4x4_get_rotation_around_xyz(matrices, &currot); @@ -8544,7 +8553,7 @@ Gfx *bondviewRenderWatch(Gfx *gdl) quaternion_slerp(quat1, quat2, t, quat3); quaternion_to_matrix(quat3, matrices->m); matrix_4x4_set_position(&targetpos, matrices); - matrix_scalar_multiply(g_CurrentPlayer->watch_scale_destination, (f32 *) matrices); + matrix_scalar_multiply(GE_WATCH_SCALE, (f32 *) matrices); total_seconds = time / 60; seconds = total_seconds % 60; total_minutes = total_seconds / 60; @@ -8601,7 +8610,7 @@ Gfx *bondviewRenderWatch(Gfx *gdl) matrix_4x4_7F058C88(); gdl = draw_watch_current_page(gdl, finalmtx, (g_CurrentPlayer->watch_animation_state == 5) || (g_CurrentPlayer->watch_animation_state == 12)); matrix_4x4_7F058C64(); - bondviewTransformManyPosToViewMatrix(g_CurrentPlayer->field_23C, objheader->numMatrices); + bondviewTransformManyPosToViewMatrix(GE_WATCH_RENDERPOS, objheader->numMatrices); matrix_4x4_7F058C88(); } diff --git a/src/game/gunfire.c b/src/game/gunfire.c index 224de6b9..8680769a 100644 --- a/src/game/gunfire.c +++ b/src/game/gunfire.c @@ -1721,6 +1721,35 @@ Gfx *set_enviro_fog_for_items_in_solo_watch_menu(Gfx *gdl, ITEM_IDS itemid, Mtxf { for (j = 0; j != 20; j += 4) { +#ifdef PORT + /* D140: the raw byte offsets 0x48/0x5c into `Switches` (a ModelNode* + * array) assume 4-byte N64 pointers. 0x48/4 = 18, 0x5c/4 = 23; j + * steps 0..16 by 4, so k = j>>2 selects Switches[18+k] / [23+k]. On + * PC the 8-byte stride made the raw math read misaligned garbage -> + * bogus non-NULL ModelNode* -> crash in modelGetNodeRwData. */ + ModelNode *sw48 = bodymodel->Switches[18 + (j >> 2)]; + ModelNode *sw5c = bodymodel->Switches[23 + (j >> 2)]; + + if (sw48 != NULL) + { + rwdata = modelGetNodeRwData((Model *) &model, sw48); + + if (rwdata != NULL) + { + rwdata->Raw.unk00 = 1; + } + } + + if (sw5c != NULL) + { + rwdata = modelGetNodeRwData((Model *) &model, sw5c); + + if (rwdata != NULL) + { + rwdata->Raw.unk00 = 1; + } + } +#else if ((*((ModelNode **) ((((u8 *) bodymodel->Switches) + j) + 0x48))) != NULL) { rwdata = modelGetNodeRwData((Model *) &model, *((ModelNode **) ((((u8 *) bodymodel->Switches) + j) + 0x48))); @@ -1740,6 +1769,7 @@ Gfx *set_enviro_fog_for_items_in_solo_watch_menu(Gfx *gdl, ITEM_IDS itemid, Mtxf rwdata->Raw.unk00 = 1; } } +#endif } } diff --git a/src/game/options.c b/src/game/options.c index a38897e9..dd0cce0a 100644 --- a/src/game/options.c +++ b/src/game/options.c @@ -50,6 +50,23 @@ #define OPTLABELS_HINT_Y 0xc3 #endif +#ifdef PORT +/* D118d: the watch inventory list has a "slam the stick hard" fast-scroll — + * a raw *level* check `joyGetStickY < -0x46` / `>= 0x47` that steps the + * cursor one item per frame while held. An N64 stick rarely sustains past + * ~0x46 along an axis, so a human taps it; the port's digital sources + * (keyboard W/S, and SDL pads which peg at 0x80) sit there every frame -> + * runaway scroll (one W press skips several items). Drop the stick term for + * the port: stick nav still works via the latched single-step path + * (`watch_stick_y_pressed_*`, one step per press) and the -0x1e..-0x45 + * smooth-scroll band; D-pad / C-buttons keep their edge-detected repeat. */ +#define GE_WATCH_STICK_FASTUP(pad) 0 +#define GE_WATCH_STICK_FASTDOWN(pad) 0 +#else +#define GE_WATCH_STICK_FASTUP(pad) (joyGetStickY(pad) >= 0x47) +#define GE_WATCH_STICK_FASTDOWN(pad) (joyGetStickY(pad) < -0x46) +#endif + // bss Mtx gfx_background_8007B0A0; Mtx gfx_background_8007B0E0; @@ -1024,7 +1041,7 @@ void game_options_inventory_navigation(void) if (!get_debug_gunwatchpos_flag()) { - if (joyGetButtonsPressedThisFrame(PLAYER_1, U_JPAD | U_CBUTTONS) || joyGetStickY(PLAYER_1) >= 0x47) + if (joyGetButtonsPressedThisFrame(PLAYER_1, U_JPAD | U_CBUTTONS) || GE_WATCH_STICK_FASTUP(PLAYER_1)) { if (((s32) watch_inventory_cursor_pos > 0) && !watch_item_is_actively_selected) { @@ -1033,7 +1050,7 @@ void game_options_inventory_navigation(void) } else { - if (joyGetButtonsPressedThisFrame(PLAYER_1, D_JPAD | D_CBUTTONS) || joyGetStickY(PLAYER_1) < -0x46) + if (joyGetButtonsPressedThisFrame(PLAYER_1, D_JPAD | D_CBUTTONS) || GE_WATCH_STICK_FASTDOWN(PLAYER_1)) { goto down_body; } @@ -1156,14 +1173,14 @@ void sub_GAME_7F0A611C(f32 *arg0, s32 *arg1, s32 arg2, s32 *arg3, s32 *arg4, s32 { if (!get_debug_gunwatchpos_flag()) { - if (joyGetButtonsPressedThisFrame(PLAYER_1, U_JPAD | U_CBUTTONS) || joyGetStickY(PLAYER_1) >= 0x47) + if (joyGetButtonsPressedThisFrame(PLAYER_1, U_JPAD | U_CBUTTONS) || GE_WATCH_STICK_FASTUP(PLAYER_1)) { if ((s32)*arg0 > 0 && arg7) { *arg0 -= 1.0f; } } - else if (joyGetButtonsPressedThisFrame(PLAYER_1, D_JPAD | D_CBUTTONS) || joyGetStickY(PLAYER_1) < -0x46) + else if (joyGetButtonsPressedThisFrame(PLAYER_1, D_JPAD | D_CBUTTONS) || GE_WATCH_STICK_FASTDOWN(PLAYER_1)) { if ((s32)*arg0 < arg2 - 1 && arg7) {