From 4cd8ccaeaced47bfdf9c7a6390263a45218591e6 Mon Sep 17 00:00:00 2001 From: James Dansereau <106631954+jkdansereau@users.noreply.github.com> Date: Mon, 31 Aug 2026 10:25:16 -0400 Subject: [PATCH] D157: objective MinDificulty read at wrong byte on LE -> campaign never unlocks ROOT CAUSE of 'complete Dam, Facility stays locked'. The type-23 objective record's MinDificulty is a big-endian s32 at word 3; on N64 the value is its low byte == offset 0xF, and struct objective_entry reads 'difficulty' as s8 @0xF. d88_propdefs.py bswap32's word 3, so on little-endian the value moves to offset 0xC -> objective_entry.difficulty @0xF reads 0 for EVERY objective. get_difficulty_for_objective() then returns Agent(0) for all, so objectiveIsAllComplete() on Agent evaluates objectives that should be difficulty-gated out (Dam obj 0 'Neutralize all alarms' is Secret Agent; obj 1/2 are 00 Agent) -> they're incomplete -> never returns TRUE -> end_of_mission_briefing() / the unlock write never runs. User was right: you don't touch an alarm on Agent Dam. Fix (D151-class, #ifdef PORT): reorder struct objective_entry's word-3 tail so 'difficulty' reads @0xC (post-swap position); N64 layout kept under #else. Also fixes dump_objectives.py (was reading byte 0xC = the BE high byte = always 0; hence LEVEL-OBJECTIVES.md said 'Agent' for everything). Dam now decodes obj0=Secret Agent, obj1/2=00 Agent, obj3=Agent. -level_09 coverage identical (phash delta is the M-30 RC2 texture change, not this). --- src/bondtypes.h | 10 +++++++++- src/game/objective_status.c | 9 +++++---- tools_pc/dump_objectives.py | 4 +++- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/bondtypes.h b/src/bondtypes.h index 0c09d876..7bf1350f 100644 --- a/src/bondtypes.h +++ b/src/bondtypes.h @@ -4055,13 +4055,21 @@ struct objective_entry enum WATCH_BRIEFING_PAGE menu; // 4 #ifdef PORT u32 text; // 8 (D151: full-word slot id, see above) + /* D157: the setup converter (d88_propdefs.py) bswap32's word 3 of the + type-23 record (MissionObjectiveRecord.MinDificulty). On N64 (BE) the + difficulty is the low byte of that word == offset 0xF; after the swap it + lands at 0xC on LE. N64 layout kept under #else. */ + s8 difficulty; // c + u8 _md_b2; // d + u8 unkD; // e (N64 0xD, post-swap) + u8 _md_b0; // f #else u16 reserved; // 8 u16 text; // a -#endif u16 unkC; // c u8 unkD; // d s8 difficulty; // f +#endif }; #pragma endregion Objectives diff --git a/src/game/objective_status.c b/src/game/objective_status.c index 0f633497..53c5814f 100644 --- a/src/game/objective_status.c +++ b/src/game/objective_status.c @@ -299,12 +299,13 @@ OBJECTIVESTATUS get_status_of_objective(s32 objectiveNum) //#MATCH #ifdef PORT if (g_savelogObjOnce && getenv("GE_SAVELOG")) { u32 *rw = (u32 *)objective; - osSyncPrintf("SAVELOG: crit obj=%d type=%d ObjRefID=%d(0x%x) " - "TextID=0x%x stride=%d words=[%08x %08x %08x] curstat=%d\n", + osSyncPrintf("SAVELOG: crit obj=%d type=%d ObjRefID=%d " + "TextID=0x%x MinDif=%d stride=%d words=[%08x %08x %08x %08x] " + "curstat=%d\n", objectiveNum, (int)objective->type, (int)objective->ObjRefID, - (unsigned)objective->ObjRefID, (unsigned)objective->TextID, + (unsigned)objective->TextID, (int)objective->MinDificulty, (int)sizepropdef((PropDefHeaderRecord *)objective), - rw[0], rw[1], rw[2], (int)currentstatus); + rw[0], rw[1], rw[2], rw[3], (int)currentstatus); } #endif if (status == OBJECTIVESTATUS_COMPLETE) diff --git a/tools_pc/dump_objectives.py b/tools_pc/dump_objectives.py index 1c088e7d..ad73e96d 100644 --- a/tools_pc/dump_objectives.py +++ b/tools_pc/dump_objectives.py @@ -156,7 +156,9 @@ def dump_level(levelid, lav_name): nblocks += 1 idx = bs32(buf, off + 4) textid = be32(buf, off + 8) - diff = buf[off + 12] + # MinDificulty is a big-endian s32 at word 3 (offset 0xC); the value + # lives in its low byte == offset 0xF. + diff = buf[off + 15] print(f"Objective {idx} [briefing text id {textid}] min difficulty: " f"{DIFF_NAMES.get(diff, diff)}") j = i + 1