WS4 level sweep driven to completion: 12/21 solo levels PASS (load + render + no-crash). Full matrix in docs/LEVEL-STATUS.md, reusable runner tools_pc/level_sweep.sh, WS6 checklist docs/LEVEL-PLAYTEST.md. Fixes: - D123 (tools_pc/d88_propdefs.py): crash class C1 on 6 levels (Dam/Runway/Frigate/Statue/Streets/Cradle). D122 fallout - the widened Vehichle/AircraftRecord.ailist slot was zeroed instead of carrying its read-before-write integer AI-list id, so prop.c -> ailistFindById(0) -> GAILIST_AIM_AT_BOND -> ai() ran a CHR aim list with a NULL chr -> chrIsNotDeadOrShot(NULL). New OBJ_ID_WORDS arm. 4 levels -> PASS, Runway falls through to C2. BUNKER1/Silo unregressed. - D124 (port/src/gimgfixup.c): crash class C2 on Jungle. Compiled explosion-DL pointer-sync keyed on an IMAGESEG marker that texLoad() had already erased -> compiled globalDL_0xNNN arrays kept link-time IMAGESEG words -> 0xABCDxxxx into fast3d on first explosion draw. Latent on every level. Detect the slot from the compiled array instead. Diagnosed, NOT fixed (write-ups in PCPortResearch.md SF): - C2-GDL (D124-Facility addendum): Facility/Runway - model texture-pool path yields a garbage tex->data; every model file loads at an odd bank base (mempAllocBytesInBank does no alignment). Suspect d43_emit.py/pcmodels sidecar texture-blob offsets (N64 8B vs PC 16B Gfx). - C3/C6 (D125): Aztec/Bunker2/Surface2 - converted propDefs blob in RAM doesn't match d88_propdefs.py offline output (garbage after record 0); runtime sizepropdef() walk drifts ~100 records -> wrong door/modelid. d88_propdefs.py convert_stream + sizepropdef() PORT strides verified correct; suspect a dest-offset/pd_end bug in d88_emit.py propdefs emit. Remaining: 9 crashes / 6 classes (C2-GDL, C3/C6, C2m Jungle G_MTX, C5 Control BG portal, C4 Depot BG tile/room, C7 Surface1 sndSetupSound). Priority order in docs/LEVEL-STATUS.md. Also folds in the in-progress docs restructure (HANDOFF/AGENTS/CLAUDE, PCPortResearch D121/D122 SF/SH entries, PLAN-linear-level-sweep, HANDOFF-ARCHIVE, BACKLOG).
12 KiB
PLAN — Linear pass: boot → menu → every solo level loads & renders
Paste-ready brief for a fresh session. Pivot from depth-first (BUNKER1 + graphics rabbit-holes) to breadth-first: make the normal player path work end-to-end (intro → credits → main menu → mission/level select), then confirm all 21 solo levels load and render without crashing. Turns the remaining work into a scoped list.
Preflight (unchanged)
CLAUDE.md + AGENTS.md non-negotiables → docs/HANDOFF.md →
docs/PORT-LEARNINGS.md → docs/AGENT-WORKFLOW.md before any subagent.
Scope decisions (locked with the user)
- Intro / credits / menus: bar is functional, not crashing. Menus
navigable start to finish; the intro is skippable with any button.
Cosmetic defects are out of scope here — log them in
docs/GRAPHICS-BACKLOG.md: gun-barrel Bond model missing (D75), Nintendo-logo transform (D75), mirrored glyphs (D114/D116), no audio (D77), partial disclaimer screen (D76). - Text X-mirror (D114/D116): stays parked. Menus are navigable with mirrored text.
- Level sweep depth: load + render + no immediate crash only. A "soak" is pointless with no input (level triggers never fire). Play-to- completion validation is a separate human step (complex input), driven by a per-level checklist this plan produces (WS6).
- N64 reference captures: deferred. Use PC golden frames +
tools_pc/framediff.py; eyeball against memory/video for "matches original".tools_pc/mupen64/is available if a level looks wrong.
Facts established (exploration, this session)
Front end is a stage — LEVELID_TITLE (0x5A). Logic in
src/game/front.c (~8700 lines) + src/game/title.c (gun-barrel/logo
3D) + src/game/ramromreplay.c (attract demos). State machine = MENU_*
enum (src/bondconstants.h:1831), driven per frame by menu_init()
(front.c:8536) through three switch tables (update_/init_/interface_ menuXX_* at front.c:8598/8637/8666); rendered by
menu_jump_constructor_handler() (front.c:8718). Transitions:
frontChangeMenu(menu, reload) (front.c:8513). Boot entry:
initgamedata.c:9 sets menu_update = MENU_LEGAL_SCREEN.
Intro states (each: init_ loads assets, interface_ advances on
timer OR any button, constructor_ renders):
MENU_LEGAL_SCREEN → NINTENDO_LOGO → RAREWARE_LOGO → EYE_INTRO
(gun-barrel walk, "007", blood) → GOLDENEYE_LOGO → DISPLAY_CAST
(credit roll) → idle → select_ramrom_to_play() (attract demo).
Any button during intro sets prev_keypresses → logos short-circuit to
MENU_FILE_SELECT, cast roll skipped. Gun-barrel sub-state machine is
gunbarrel_mode (u8) in title.c (insert_bond_eye_intro title.c:297,
renderGunbarrelEyeIntroSequence title.c:608).
Menu → level launch: mission select →
selected_stage = mission_folder_setup_entries[…].stage_id
(front.c:3301) → MENU_DIFFICULTY → MENU_BRIEFING /
MENU_007_OPTIONS → init_menu0B_runstage() (front.c:6900) →
bossSetLoadedStage(selected_stage) → g_MainStageNum → bossMainloop
inner loop exits → g_StageNum = g_MainStageNum (boss.c:647) →
lvlStageLoad. Level → level always routes back through the front end
(MENU_MISSION_COMPLETE, interface_menu0D_missioncomplete
front.c:7119; auto-advance scans mission_folder_setup_entries for the
next stage_id >= 0). No direct in-stage jump.
21 solo levels; LEVELID value == -level_XX number (boss.c:337
decode d0*10 + d1 - 0x210). Mission order + numbers:
Dam 33, Facility 34, Runway 35, Surface1 36, Bunker1 09, Silo 20,
Frigate 26, Surface2 43, Bunker2 27, Statue 22, Archives 24, Streets 29,
Depot 30, Train 25, Jungle 37, Control 23, Caverns 39, Cradle 41,
Aztec 28, Egypt 32, Cuba 54. Enum: src/bondconstants.h:1633-1685;
mission table mission_folder_setup_entries[] front.c:434-466.
Per-level memory args already have a table — memallocstringtable[]
(boss.c:101-142), one row/LEVELID (-ml -me -mgfx -mvtx -mt -ma). The
auto-inject loop boss.c:369-414 already selects the right row per stage
load — but it is gated on g_DebugAndUpdateStageFlag, which the port
only sets when -level_ is absent (boss.c:199-202;
rmonGetToken() → 0 on PC, port/src/n64stubs.c:134). So bare
-level_09 today uses default pools; hence the manual
-ml0 -me0 -mgfx100 -mvtx50 -mt700 -ma150 in the HANDOFF repro line.
Documented TODO: PCPortResearch.md §H (D95 area) and docs/HANDOFF.md
"Next task".
D88.4 is a cross-level blocker. Every level's Usetup*Z feeds the
propDefs polymorphic record stream, which tools_pc/d88_emit.py
deliberately leaves un-byteswapped. BUNKER1 crashes in setupDoor
(prop.c:971) on a garbage BE door->obj. Any level hits the same class
when its prop set is walked. Groundwork: tools_pc/d88_propdefs.py,
tools_pc/d88_layoutprobe.c (machine SZ/PTR lines),
tools_pc/d88_propdef_scan.py (per-level type histogram).
Placeholder sizes still to resolve in d88_propdefs.py:
VEHICLE/AIRCRAFT/TANK/AMMO/DEPOSIT_IN_ROOM.
Tooling: GE_PCDUMP="a-b:step" → ./ppm/ (port/src/video.c:181);
tools_pc/pixcount.py (non-black pixel count / content check),
tools_pc/framediff.py (structural regression vs tools_pc/golden/ —
the port is NOT frame-deterministic, D117), tools_pc/ppm2bmp.py.
Crash log ge007.crash.log; symbolicate
addr2line -e build-pc/ge007.x86_64.exe -f -C <pc> (image base
0x140000000). Objectives: propDefs PROPDEF_OBJECTIVE_*
(bondconstants.h:4332), status src/game/objective_status.c
(get_status_of_objective() :161) — needed only for WS6.
Workstreams
WS1 is a prerequisite for all. WS2 (front.c/title.c/lv.c/
ramromreplay.c/initmenus.c/port/) and WS3 (tools_pc/d88* +
narrow prop.c) are disjoint → parallelisable. WS4 depends on WS3.
WS5 iterative. WS6 is the user's.
WS1 — Frictionless per-level boot (small, first)
Goal: ./build-pc/ge007.x86_64.exe -level_XX with no other args boots
any level with its correct pools.
Change: in src/boss.c under #ifdef PORT, also set
g_DebugAndUpdateStageFlag = 1 when -level_ is present (mirror of
boss.c:199-202). The existing loop boss.c:369-414 then supplies the
right memallocstringtable row per stage; the generic preload at
boss.c:207-210 is harmless (per-loop tokenSetString at :413
overrides). Debug-harness / ABI-class #ifdef PORT edit (same character
as the -level_ ASCII parse already in the file). Document as a new Dxx
in PCPortResearch.md §F. Alt if boss.c is judged out of bounds:
synthesize the string in the port token layer (port/src/system.c
sysGetTokenString). Boss.c is simpler and preferred.
Also update docs/HANDOFF.md repro line; drop the manual -m* list once
verified.
Verify: -level_09 alone == today's full-arg behaviour (same frame,
framediff.py green vs tools_pc/golden/).
WS2 — Front-end walkthrough & crash repair (human playtest loop)
Goal: a human can go boot → (watch or skip intro) → file/folder select → mode select → mission select → difficulty → briefing → level launches, and after a level, mission-complete → next briefing, with no crash / hang / dead-end. Cosmetic wrongness acceptable and logged.
Method: same loop as the M-10 input playtest — overseer builds, human
drives, defects come back as a list, overseer root-causes each (expect
the pointer-width / BE-rodata / N64-Gfx-sizing classes in
PORT-LEARNINGS.md), narrow #ifdef PORT fix, re-test.
Files in scope: src/game/front.c, src/game/lv.c
(interface_menu05_fileselect is here), src/game/title.c,
src/game/initmenus.c, src/game/ramromreplay.c, port/src/*. NOT
prop.c / tools_pc/d88* (WS3).
Human checklist:
- Boot → legal → Nintendo → Rareware → gun-barrel → GE logo → cast roll → (idle) attract demo. Note any crash/hang; cosmetic noted separately.
- Press a key during intro → confirm short-circuit to file select.
- File/folder select: create a folder, enter it; folder-delete sub-UI; 30 s idle → back to legal screen.
- Mode select → Mission select: page through all 20 mission entries, each briefing text/photo loads without fault.
- Pick mission + Agent difficulty + briefing → Start → level loads.
- In-level: die / abort → mission-failed → menu. Complete a short level (or cheat) → mission-complete → auto-advance to next briefing.
- MP chain (lower priority): MP options → scenario/stage/char/handicap/ teams → MP match loads.
Do NOT fix here (log/confirm in GRAPHICS-BACKLOG.md): D75, D76, D77,
D114/D116.
Deliverable: front-end crash list + root causes + fixes;
GRAPHICS-BACKLOG.md updated with confirmed cosmetic state.
WS3 — D88.4 propDefs converter (cross-level blocker)
Goal: Usetup*Z propDefs converts correctly offline for all 21 levels
so setupDoor / prop instantiation stops reading BE garbage.
Method: subagent brief, same shape as D69 / D88.1-3 (offline sidecar
converter). Byte-spec every PROPDEF_* record type (~40, enum
bondconstants.h), per-type field byteswap + pointer-width audit,
extend tools_pc/d88_emit.py. Use d88_propdefs.py /
d88_layoutprobe.c / d88_propdef_scan.py. Resolve the placeholder
sizes (VEHICLE/AIRCRAFT/TANK/AMMO/DEPOSIT_IN_ROOM) via
d88_layoutprobe.c.
Files: tools_pc/d88_emit.py, tools_pc/d88_propdefs.py,
tools_pc/d88_layoutprobe.c, + narrow #ifdef PORT in src/game/prop.c
if a struct-width mismatch surfaces (D79/D88 class).
Budget: structural — generous. On expiry: write up spec progress in §F/D88 with confidence rating.
Verify: -level_09 walks past setupDoor to a rendered BUNKER1 frame
with props; spot-check 2-3 other levels' prop walk.
WS4 — Per-level load + render sweep
Goal: status matrix for all 21 solo levels — (a) lvlStageLoad
completes without fault, (b) ≥1 non-degenerate frame renders, (c)
survives ~300 frames / ~15 s with no input (immediate-crash check only).
Prereq: WS1 + WS3 landed.
Method: agent-driven, one level at a time (-level_XX). Each: launch
GE_PCDUMP="1-300:30", capture ge007.crash.log, run pixcount.py.
Record PASS / CRASH@ / RENDERS-EMPTY / HANG. Symbolicate every
crash (addr2line). Group failures by root-cause class.
Deliverable: docs/LEVEL-STATUS.md — table (level, number, load, render,
crash site, notes), refreshed as WS5 closes items.
WS5 — Per-level crash triage (iterative)
Goal: drive the WS4 matrix to "all 21 load + render".
Method: one focused investigation per distinct failure class (subagent
brief per AGENT-WORKFLOW.md template; read PORT-LEARNINGS.md first).
Expected classes: pointer-width struct growth in per-level assets,
BE-rodata not converted, N64-Gfx/Vtx sizing, per-level pool exhaustion
(memallocstringtable PC bumps, cf. D95). Fix = narrow #ifdef PORT or
converter extension; document each in §F.
Verify per fix: target level's WS4 check → PASS and -level_09
framediff stays green.
WS6 — Human completion-validation pass (user-run, after WS4 green)
Goal: confirm each level is playable start to finish and matches the original — the part needing real input.
Deliverable produced for the user: docs/LEVEL-PLAYTEST.md — per-level
checklist template: spawn point; each difficulty-gated objective
(objective_status.c) reachable and registering COMPLETE; guards / props
/ doors behave; level exit / mission-complete fires clean; timing +
layout match retail. User fills it per level; findings feed a WS5-style
triage round.
Verification (overall)
- Per merged patch:
export PATH="/c/msys64/mingw64/bin:$PATH" && ./build-pc.sh ntsc-finalgreen,/linkcheckclean, single-frameGE_PCDUMPdiff vstools_pc/golden/viaframediff.py. - WS1:
-level_09alone == old manual-arg behaviour. - WS3: BUNKER1 renders props past
setupDoor. - WS4/WS5:
docs/LEVEL-STATUS.mdall 21 rows = load+render PASS. - Regenerate sidecars after any converter change:
python tools_pc/d43_emit.py ntsc-final && python tools_pc/d69_emit.py ntsc-final && python tools_pc/d88_emit.py ntsc-final --regen.
Out of scope (tracked elsewhere — do not start here)
- D75 / D76 / D114 / D116 / D77 and all
docs/GRAPHICS-BACKLOG.mditems. - D118a/b input polish, rebinding, gamepad hotplug,
ge007.ini. - Weapon-model-doesn't-draw (AUDIT-M6 #5), rest of the
struct playeroffset audit. - Audio (Phase 3), saves/EEPROM (Phase 4),
docs/BACKLOG.md(PC graphics options, co-op, netplay).