- README, LICENSE (MIT for the port layer), NOTICE, CONTRIBUTING. - Restructure docs: internals.md, porting-notes.md, dev-process.md + dev/agentic-development.md (the research write-up), dev/ (raw record). - Replace decomp status-badge CI with a PC-port validate workflow. - Remove session/agent scaffolding and stale planning docs from the tree. - Add .gitattributes (normalize line endings to LF); add screenshots. The pre-2026-08 history is the GoldenEye 007 decompilation by Larry Ficken (KholdFuzion) and contributors, preserved unchanged. The PC-port work from 2026-08-17 on is mine.
12 KiB
AUDIT-M6 — struct player / struct hand raw-offset pass
Survey only (Track 3 of docs/PLAN-M6-playable.md). No source changed, no
build. Feeds a follow-up fix session. Companion to §F D98/D100/D101/D102.
Method
Grep of src/ for: handoffset, (u8 *)g_CurrentPlayer, g_CurrentPlayer + 0x,
+ 0x[3-hex], sizeof(struct hand|player), * sizeof(struct, hardcoded
0x2A80/0x2A70, ->hands[i] +, punned ->m[ / inline-Model accesses,
bondheadmatrices, copy_of_body_obj_header, field_59C..field_6CC,
field_654, gaitRwData. Cross-checked every hit against the live
struct player / struct hand definitions in src/game/bondview.h
(player @ line 314, hand @ line 119).
PC layout facts (from bondview.h + bondtypes.h)
struct hand(bondview.h:119): N64sizeof≈ 0x3B8 (base 0x870→0xC28 insideplayer). PC adds:audioHandle+4,rocket(AttachedObj*) +4,mtxlist(Mtxf*) +4, and the D102#ifdef PORTtailstruct Model weaponModel(~0x2A8) +u32 weaponRwPool[192](0x300). PCsizeof(struct hand)≈ 0x968 — roughly +0x5B0 vs N64, and not a clean multiple, sohandnum * sizeof(struct hand)de-syncs forhandnum==1.struct playerpointer growth below N64 offset 0x594 (contradicts the D100 note that “anything below 0x594 is unaffected”):cameratile(0x34),prop(0xA8),bodyModel(0xD4),autoaim_target_y(0x130),autoaim_target_x(0x140) → +0x14 by offset ~0x144.field_10C4.. and thebloodImg*/InvItem*/textoverride*blocks add much more higher up.model(N64 0x598): D100 made it an inlinestruct Model(~0x2A8) replacing an 8-byte pointer → +0x2A0 for everything after it, beforefield_59C..field_6CC(which D100 grep-verified dead exceptfield_654).ptr_hand_weapon_buffer[2]+0x8;copy_of_body_obj_header[2]— inlineModelFileHeadergrew 0x20→0x30 (4 pointers ×+4) → +0x20;item_related[2]struct texpoolpointer growth;bondheadmatrices/viewportsunchanged.- Net delta from
playerbase tohands[]: ≈ +0x2E0 (0x14 + 0x2A0 + 0x08 + 0x20 + texpool). So N64hands[0]@ 0x870 is at PC ≈ 0xB50; N64hands[0].throw_item_pos_related@ 0x870+0x268 = 0xAD8 is at PC ≈ 0xDB8.
Findings table
| # | file:line | accessor | targets (N64 field) | N64 off | PC off (real) | PORT-adj? | BUNKER1 FP reachable? | severity | recommended fix |
|---|---|---|---|---|---|---|---|---|---|
| 1 | src/game/gunfire.c:4960 THROWMTX (macro) used @ 5043,5047,5070,5126,5360,5364,5387,5443 |
(Mtxf*)((u8*)g_CurrentPlayer + handoffset + 0xAD8) (0xAD0 EU) |
hands[handnum].throw_item_pos_related |
0xAD8 (h0) / 0xAD8+0x3B8 (h1) | ≈0xDB8 (h0) / ≈0xDB8+0x968 (h1) | NO | YES — sub_GAME_7F068508 runs on every shot with a cartridge (PP7/KF7/…), solo only (getPlayerCount()>=2 early-out) |
HIGH — memory corruption. Raw 0xAD8 on PC lands inside the inline model / field_59C region of struct player; matrix_4x4_copy(THROWMTX,…) writes 64 bytes there. handnum==1 (dual wield) additionally uses the wrong stride. Corrupts the gait Model / bondhead matrices → wrong Bond-body render or a delayed fault in model code. |
Replace macro body with (&g_CurrentPlayer->hands[handnum].throw_item_pos_related). Drop THROWMTX_OFFSET, handoffset. |
| 2 | src/game/gunfire.c:4961 THROWPOS(k) used @ 5112-5114, 5164-5166, 5429-5431, 5481-5483 |
((f32*)((u8*)g_CurrentPlayer + handoffset + 0xB08))[k] (0xB00 EU) |
hands[handnum].throw_item_pos_related.m[3][k] (translation row) |
0xB08 | ≈0xDE8 | NO | YES (same path as #1) | HIGH — silent-wrong + reads corrupted memory. Feeds casing->vel from the frame-to-frame delta of a garbage location → casings fly wrong / NaN, and the read is OOB-ish relative to intent. |
g_CurrentPlayer->hands[handnum].throw_item_pos_related.m[3][k] |
| 3 | src/game/gunfire.c:4962 THROWPREV(k) used @ 5112-5114, 5164-5166, 5429-5431, 5481-5483 |
((f32*)((u8*)g_CurrentPlayer + handoffset + 0xB48))[k] (0xB40 EU) |
hands[handnum].throw_item_pos_related_prev.m[3][k] |
0xB48 | ≈0xE28 | NO | YES (same path) | HIGH (same as #2) | g_CurrentPlayer->hands[handnum].throw_item_pos_related_prev.m[3][k] |
| 4 | src/game/gunfire.c:5031 and :5348 |
handoffset = handnum * sizeof(struct hand) |
array stride for hands[handnum] |
0x3B8 | 0x968 (and non-uniform) | NO (the raw math is only consumed by #1–#3) | YES | HIGH — root of #1–#3’s handnum==1 error; harmless once #1–#3 use hands[handnum].field. |
delete; fold into the field accessors above. |
| 5 | src/game/gunfire.c:600-661 weapon-model setup (copy_of_body_obj_header[handnum] → modelInit(HAND_WEAPON_MODEL, mdlhdr, HAND_WEAPON_RWPOOL); hand->mtxlist = rwmtx; render_pos = (RenderPosView*)rwmtx) |
field-name access + D102 macros | hands[h].weaponModel / weaponRwPool / mtxlist; copy_of_body_obj_header[h] (inline ModelFileHeader) |
— | — | YES (D102) for the Model/pool pun; copy_of_body_obj_header is a plain inline struct (layout-consistent) |
YES — this is the 1P weapon draw path | MED — prime suspect for “weapon model doesn’t draw.” rwmtx is dynAllocate’d (transient per-frame arena) but weaponModel.render_pos is set to it once at setup and consumed at render (gunRenderFirstPersonGunModels:1605); on N64 render_pos aliased the persistent mtxlist field, here it can point at reused arena memory. Also flashvisptr = HAND_WEAPON_RWPOOL + node->…RwDataIndex (gunfire.c:640) assumes N64 word indices into a pool whose PC records are ~2×. |
Verify dynAllocate lifetime vs. render; consider persisting the matrix list. Re-check weaponRwPool[192] sizing against modelCalculateRwDataLen(mdlhdr) on PC. Confirm subdraw matrix path (shared with Track 2). |
| 6 | src/game/bondview2.c:3155-3162 (PORT branch of sub_GAME_7F07E7CC) |
(Model*)&g_CurrentPlayer->something_with_watch_object_instance; static u8 watchRwPool[0xC8]; g_CurrentPlayer->step_in_view_watch_animation = 0 |
watch-menu item preview Model punned at N64 player+0x230, pool at +0x2EC | 0x230 / 0x2EC | 0x244 / 0x300-ish | YES (D56) but suspect | YES — open watch menu / cycle to an item page | MED — under-reservation. D56 hosts the pool in a 0xC8 static (N64 capacity) but PC RW records are wider; and animInit writes a full inline struct Model (~0x2A8) starting at &something_with_watch_object_instance, overrunning field_234..watch_scale_destination.. (N64 those were the tail of the 0xBC Model hole; on PC they are live watch fields). |
Give the watch Model real inline storage in struct player under #ifdef PORT (D100 pattern) + size watchRwPool from modelCalculateRwDataLen. |
| 7 | src/game/gunfire.c:4951-4957 |
#define THROWMTX_OFFSET 0xAD8 etc. |
(the constants behind #1–#3) | — | — | NO (EU/non-EU split only, both N64) | — | INFO — remove with #1–#3. | delete block. |
| 8 | src/game/player.c:136 |
mempAllocBytesInBank((sizeof(struct player)+0xF)&~0xF, …) |
player allocation | — | — | YES (D98) | — | OK — listed for completeness; correct. | none |
| 9 | src/game/initBondDATAdefaults.c:103 |
animInit(&g_CurrentPlayer->model, &player_gait_object_header, g_CurrentPlayer->gaitRwData) |
inline gait Model + gaitRwData[256] |
— | — | YES (D100) | YES | OK. | none |
| 10 | src/game/bondhead.c:217-480, bondview2.c:922-9102 |
&g_CurrentPlayer->model, bondheadmatrices[0].m[..] (field-name) |
inline gait Model + head matrices | — | — | YES (D100 makes field access correct; single TU layout) | YES | OK — no raw offsets; safe as long as D100 inline-Model stays. | none |
Not player/hand but same class (noted, out of scope for the fix list)
src/game/gunfire.c:5513render_data.mtxlist = (Mtxf*)model_matrices— local, fine.initexplosioncasing.c,initobjects.c,propobj.c,glass2.c,initunk_005520.c— allN * sizeof(struct X)allocations, alreadysizeof-correct (D57 covered the rwdata capacities).ramromreplay.c+0x110/+0x1F8/+0x21E—ramromfilestructure, handled by D66/D87, unrelated.
Total
10 distinct offset sites; 3 live HIGH-severity bugs (findings #1–#3, one underlying cause #4), 1 dead constant block (#7), 2 MED items to verify (#5, #6), 4 already-correct (#8–#10).
All 3 HIGH bugs are the same construct in gunfire.c and are reachable in
BUNKER1 first-person the instant the player fires a weapon that ejects a
casing (every ballistic weapon). They are NOT reachable before the first
shot, so they do not explain a weapon model that is missing from frame 1 —
but once firing starts they corrupt the inline gait Model region and will
produce cascading render/anim breakage or a fault.
Where the “1P weapon model doesn’t draw” bug most likely lives
Not in a surviving raw offset — D102 already gave the weapon Model and its
RW pool dedicated storage, and gunRenderFirstPersonGunModels /
gunTickHandState use field names. Best candidates, in order:
- Finding #5 — the D102 wiring in
gunfire.c:600-661.weaponModel.render_posis pointed at adynAllocate’d transient (rwmtx) that on N64 aliased the persistenthand->mtxlistfield; if the arena is recycled between the hand-tick setup andgunRenderFirstPersonGunModels, the model draws with garbage / off-screen matrices (looks like “not drawn”). CheckdynAllocatelifetime and whetherweaponModelshould own its matrix list on PC. - The
subdraw/subcalcmatrices/process_02_positionmatrix composition — shared with Track 2 (inverted characters). A handedness / lookat-sign error there hides the weapon model just as it inverts guards. field_87Fgating (gunfire.c:520,:587-598) — if the setup branch that setsfield_87F = 1and callsmodelInitisn’t taken (e.g.get_ptr_weapon_model_header_line(item)returns NULL becauseptr_hand_weapon_buffer/ weapon-model load failed), the render loopcontinues and nothing is emitted. Cheap to instrument.
Recommended fix order
- #1–#4 & #7 (gunfire.c THROW macros) — one mechanical change, no layout
work: rewrite
THROWMTX/THROWPOS/THROWPREVasg_CurrentPlayer->hands[handnum].throw_item_pos_related[_prev]field accesses (.m[3][k]for the POS/PREV variants), deletehandoffsetand theTHROW*_OFFSETdefines. Wrap in#ifdef PORTkeeping the N64 macro for the N64 build (or, since the field access is identical semantics on both, use it unconditionally — verify byte-match). Log as §F “D115”. - #5 — investigate the weapon-model
render_pos/mtxlistlifetime andweaponRwPoolsizing; this is the likely playability blocker. May fold into the Track-2 matrix work. - #6 — give the watch preview Model inline PORT storage + sized pool (only matters once the player opens the watch in-level).
- Add a debug assert in
gunfire.cweapon-model setup loggingmdlhdr->numRecordsvsweaponRwPoolcapacity andfield_87Fstate, to settle candidate 3 above.
§F pointer
Add to docs/internals.md §F after D112: “D115 (audit) —
struct player/struct hand raw-offset pass: see
docs/dev/AUDIT-M6-player-offsets.md. Live HIGH bugs = gunfire.c THROWMTX/POS/PREV
macros (+handoffset+0xAD8/0xB08/0xB48), not PORT-adjusted; fix = field
accessors. MED = D102 weapon render_pos transient-arena aliasing (likely
the ‘weapon model doesn’t draw’ cause) and the D56 watch-Model pool
under-reservation.”