Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7,201 changes: 7,201 additions & 0 deletions docs/GHIDRA_VERIFICATION.md

Large diffs are not rendered by default.

25 changes: 17 additions & 8 deletions game/222.c
Original file line number Diff line number Diff line change
Expand Up @@ -418,14 +418,13 @@ void AA_EndEvent_DrawMenu(void)

RECTMENU_ClearInput();

sdata->Loading.OnBegin.AddBitsConfig0 |= ADVENTURE_ARENA;
sdata->Loading.OnBegin.RemBitsConfig0 |= ADVENTURE_BOSS;
sdata->Loading.OnBegin.RemBitsConfig8 |= TOKEN_RACE;

if (IS_BOSS_RACE(gGT->gameMode1))
{
sdata->Loading.OnBegin.AddBitsConfig8 |= SPAWN_AT_BOSS;
}
// NOTE(claude): Ghidra 0x800a03b8-0x800a0648 — retail sets the Loading.OnBegin
// configs ONLY in the confirmed-WIN path (after the tap), split across the boss
// vs normal branches; the LOSE path (0x800a0658) sets NO configs — it just shows
// the retry/exit menu. The prior code set them here, before the win/lose split,
// so a LOST adventure race queued gameMode changes (remove ADVENTURE_BOSS/TOKEN_RACE,
// add ADVENTURE_ARENA) that persist into Retry (UI_RaceEnd_MenuProc case 4/RESTART
// clears none of them). Moved into the win path below to match the binary.

if (!didWin)
{
Expand All @@ -439,11 +438,18 @@ void AA_EndEvent_DrawMenu(void)
sdata->framesSinceRaceEnded = 0;
sdata->numIconsEOR = 1;

// NOTE(claude): Ghidra 0x800a041c — set in the win path, before the boss/normal split.
sdata->Loading.OnBegin.AddBitsConfig0 |= ADVENTURE_ARENA;

// Load the levelID for Adventure Hub that you came from
s16 levSpawn = gGT->prevLEV;

if (IS_BOSS_RACE(gGT->gameMode1))
{
// NOTE(claude): Ghidra 0x800a0440/0x800a0454 — boss-win only.
sdata->Loading.OnBegin.AddBitsConfig8 |= SPAWN_AT_BOSS;
sdata->Loading.OnBegin.RemBitsConfig0 |= ADVENTURE_BOSS;

// Reward bit of key unlocked, and boss beaten.
rewardBit = gGT->bossID + ADV_REWARD_FIRST_BOSS_KEY;

Expand Down Expand Up @@ -505,6 +511,9 @@ void AA_EndEvent_DrawMenu(void)
gGT->podiumRewardID = STATIC_TROPHY;
}

// NOTE(claude): Ghidra 0x800a063c — normal-win only, right before the final load.
sdata->Loading.OnBegin.RemBitsConfig8 |= TOKEN_RACE;

MainRaceTrack_RequestLoad(levSpawn);
}

Expand Down
7 changes: 5 additions & 2 deletions game/223.c
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,11 @@ void RR_EndEvent_UnlockAward(void)
sdata->relicTime_1sec = (relicTime / RR_RACE_TIME_ONE_SECOND) % 10;
sdata->relicTime_1ms = ((relicTime * 100) / RR_RACE_TIME_ONE_SECOND) % 10;

// [Not Done]
sdata->relicTime_10ms = 0;
// NOTE(claude): Ghidra 0x8009f948-0x8009fa60 (gold) / 0x8009fbac-0x8009fcc4
// (platinum) store FIVE relic-time digits; the 4th is (relicTime / 0x60) % 10
// — the tenths-of-a-second digit (0x60 = 96 = RR_RACE_TIME_ONE_SECOND/10). The
// prior `= 0` ("[Not Done]") dropped that digit from the displayed relic time.
sdata->relicTime_10ms = (relicTime / (RR_RACE_TIME_ONE_SECOND / 10)) % 10;
}
}

Expand Down
7 changes: 6 additions & 1 deletion game/230/230_02_MM_Title_MenuUpdate.c
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,12 @@ void MM_Title_MenuUpdate(void)
gGT->boolDemoMode = 1;

// set number of players to 1
gGT->numPlyrCurrGame = 1;
// NOTE(claude): Ghidra 0x800abf78 `sb v0,0x1ca9(gGT)` writes numPlyrNextGame
// (0x1ca9), not numPlyrCurrGame (0x1ca8). Demo mode configures the NEXT game
// (the demo race being loaded); the prior write set the current menu game's
// count, leaving numPlyrNextGame stale so the demo could load with the wrong
// player count. Write numPlyrNextGame to match retail.
gGT->numPlyrNextGame = 1;

// 60 seconds
gGT->demoCountdownTimer = 1800;
Expand Down
6 changes: 5 additions & 1 deletion game/230/230_44_MM_Characters_DrawWindows.c
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,11 @@ void MM_Characters_DrawWindows(int boolShowDrivers)
ptrCurr = &D230.characterSelect_charIDs_curr[iVar6];

iVar10->animFrame = 0;
iVar10->vertSplit = 0;
// NOTE(claude): Ghidra 0x800adea8 `sb zero,0x52(inst)` clears animIndex (u8 @0x52), not
// vertSplit (s16 @0x56). The prior code reset the wrong field — it left animIndex holding
// stale animation-set state on the char-select driver models and needlessly zeroed vertSplit
// (retail doesn't touch vertSplit here). Reset animIndex to match retail (with animFrame@0x54).
iVar10->animIndex = 0;

uVar4 = MM_Characters_GetModelByName((int *)data.MetaDataCharacters[(int)*ptrCurr].name_Debug);

Expand Down
7 changes: 6 additions & 1 deletion game/230/230_59_MM_CupSelect_MenuProc.c
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,12 @@ void MM_CupSelect_MenuProc(struct RectMenu *menu)
if (cupIndex == menu->rowSelected)
{
// Make text flash
if ((sdata->frameCounter & 2) != 0)
// NOTE(claude): Ghidra 0x800b1218-0x800b1228 sets the +4 flash bit
// (0xffff8004) when `(frameCounter & 2) == 0`, and keeps 0xffff8000
// when the branch `bne v0,zero` is taken (i.e. `!= 0`). The prior
// `!= 0` here inverted the flash phase of the selected cup name vs
// retail; use `== 0` to match the binary.
if ((sdata->frameCounter & 2) == 0)
txtColor |= 4;
}

Expand Down
18 changes: 13 additions & 5 deletions game/230/230_65_MM_HighScore_Draw.c
Original file line number Diff line number Diff line change
Expand Up @@ -86,17 +86,25 @@ void MM_HighScore_Draw(u16 trackIndex, u32 rowIndex, u32 posX, u32 posY)
MM_HighScore_Text3D(sdata->lngStrings[LNG_BEST_LAP_TIME], D230.transitionMeta_HighScores[7].currX + offsetX + 0x124,
D230.transitionMeta_HighScores[7].currY + offsetY + 0x2b, FONT_SMALL, 0);

// NOTE(claude): Ghidra 0x800b3544/3594/35d0 anchor the #1 best-lap entry's
// name/time/icon to transitionMeta_HighScores[8] (lhu 0x56(s1) = 0x5926 =
// meta[8].currX), NOT [7] — only the "BEST LAP TIME" label at 0x800b3500
// (lhu 0x4c(s1) = meta[7]) uses [7]. The project put the entry data on the
// label's anchor, mispositioning it during the slide-in transition. Also, the
// time-string Y base reuses meta[8].currX (asm reuses a2=lhu 0x56(s1) for both
// X and Y — a retail quirk), not currY. Corrected to match the binary.

// Character Name
MM_HighScore_Text3D(entry[0].name, D230.transitionMeta_HighScores[7].currX + offsetX + 0x160, D230.transitionMeta_HighScores[7].currY + offsetY + 0x39,
MM_HighScore_Text3D(entry[0].name, D230.transitionMeta_HighScores[8].currX + offsetX + 0x160, D230.transitionMeta_HighScores[8].currY + offsetY + 0x39,
FONT_BIG, entry[0].characterID + 5);

// Draw time string
MM_HighScore_Text3D(RECTMENU_DrawTime(entry[0].time), D230.transitionMeta_HighScores[7].currX + offsetX + 0x160,
D230.transitionMeta_HighScores[7].currY + offsetY + 0x4a, FONT_SMALL, 0);
MM_HighScore_Text3D(RECTMENU_DrawTime(entry[0].time), D230.transitionMeta_HighScores[8].currX + offsetX + 0x160,
D230.transitionMeta_HighScores[8].currX + offsetY + 0x4a, FONT_SMALL, 0);

// Character Icon
RECTMENU_DrawPolyGT4(gGT->ptrIcons[data.MetaDataCharacters[entry[0].characterID].iconID], D230.transitionMeta_HighScores[7].currX + (offsetX + 0x124),
D230.transitionMeta_HighScores[7].currY + (offsetY + 0x38), &gGT->backBuffer->primMem, (gGT->pushBuffer_UI).ptrOT, iconColor,
RECTMENU_DrawPolyGT4(gGT->ptrIcons[data.MetaDataCharacters[entry[0].characterID].iconID], D230.transitionMeta_HighScores[8].currX + (offsetX + 0x124),
D230.transitionMeta_HighScores[8].currY + (offsetY + 0x38), &gGT->backBuffer->primMem, (gGT->pushBuffer_UI).ptrOT, iconColor,
iconColor, iconColor, iconColor, 1, 0x1000);
}

Expand Down
8 changes: 7 additions & 1 deletion game/230/230_78_MM_Video_KickCD.c
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@ void MM_Video_KickCD(CdlLOC *location)
int CdlMode;
u8 mode[4];

if ((location != NULL) && (V230.ptrCdLoc != &V230.cdLocation2))
// NOTE(claude): Ghidra 0x800b5b9c-0x800b5ba0 `addiu v0,0x6824; beq v1,v0` — the
// sentinel is &cdLocation1 (0x800b6824), the address StartStream seeds and points
// ptrCdLoc at. The prior &cdLocation2 (0x800b6828) is a distinct offset, so this
// guard would wrongly fire on the restart KickCD(&field@0x682c) call while
// ptrCdLoc still == &cdLocation1, resetting the CD state machine (field12_0x20=0)
// instead of continuing it. Compare against cdLocation1 to match the binary.
if ((location != NULL) && (V230.ptrCdLoc != &V230.cdLocation1))
{
V230.field12_0x20 = 0;
V230.ptrCdLoc = location;
Expand Down
18 changes: 14 additions & 4 deletions game/231/231_011_RB_Hazard_ThCollide_Generic.c
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,21 @@ void RB_Hazard_ThCollide_Generic(struct Thread *thread)
// "thread", or else you'll kill the wrong thread
// at the end of the function

crateObj = (struct Crate *)crateInst->thread->object;

if (crateObj != 0)
// NOTE(claude): Ghidra 0x800ac4f8/0x800ac500 — retail guards
// crateInst->thread != 0 before reading ->object (a three-level
// null check). The prior code dereferenced crateInst->thread->object
// unconditionally; on native a NULL crate thread faults at (NULL+0x30)
// where retail safely skips. Restore the middle null check.
crateThread = crateInst->thread;

if (crateThread != 0)
{
crateObj->boolPauseCooldown = 0;
crateObj = (struct Crate *)crateThread->object;

if (crateObj != 0)
{
crateObj->boolPauseCooldown = 0;
}
}
}

Expand Down
4 changes: 4 additions & 0 deletions game/231/231_015_RB_GenericMine_LInB.c
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ void RB_GenericMine_LInB(struct Instance *inst)
mw->velocity.z = 0;
mw->boolDestroyed = 0;
mw->frameCount_DontHurtParent = 0;
// NOTE(claude): Ghidra 0x800acb2c `sh zero,0x26(s0)` — retail zeroes tntSpinY
// here too. The pooled MineWeapon is not auto-zeroed (every other field is
// explicitly cleared), so the prior code left tntSpinY holding recycled garbage.
mw->tntSpinY = 0;
mw->extraFlags = 0;
mw->stopFallAtY = inst->matrix.t[1];

Expand Down
36 changes: 26 additions & 10 deletions game/231/231_016_RB_GenericMine_ThTick.c
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,33 @@ void RB_GenericMine_ThTick(struct Thread *t)
inst->scale.x = 0x800;
inst->scale.y = 0x800;
inst->scale.z = 0x800;
}
else
{
// cooldown of 0.24s
mw->cooldown = 0xf0;

func = RB_Potion_ThTick_InAir;
// NOTE(claude): a thrown TNT tail-calls RB_TNT_ThTick_ThrowOffHead, which is
// terminal — it ends via ThTick_FastRET (0x80071694), the scheduler trampoline
// that restores the saved sp/ra/s0-s8 (@ 0x1f8000b0) and never returns to this
// frame. So the grounded pass below does NOT run for a thrown TNT. The native
// port drops ThTick_FastRET, so emulate that terminal-ness with a return here.
ThTick_SetAndExec(t, func);
return;
}

// this also quits the function
// cooldown of 0.24s
mw->cooldown = 0xf0;

func = RB_Potion_ThTick_InAir;

// NOTE(claude): Ghidra 0x800acbf4 `jal ThTick_SetAndExec` (0x800716ec) is a
// tail-call (jr through the fn-ptr, ra preserved). RB_Potion_ThTick_InAir ends in
// `jr ra` (0x800aca48), NOT ThTick_FastRET, so it returns to 0x800acbfc and falls
// straight through into the grounded pass below. Retail therefore runs the grounded
// pass — gravity, scale-grow, CollideWithDrivers — on the throw frame for a thrown
// potion/nitro; the prior `return` wrongly skipped it. Only the TNT branch above is
// terminal.
ThTick_SetAndExec(t, func);
return;
// fall through to the grounded pass
}

// === If not "thrown" ===
// === Grounded pass (also runs after InAir on a thrown potion/nitro) ===

// reduce cooldown
mw->cooldown -= gGT->elapsedTimeMS;
Expand Down Expand Up @@ -271,8 +283,12 @@ void RB_GenericMine_ThTick(struct Thread *t)
mw->deltaPos.z = 0;
mw->stopFallAtY = 0x3fff;

// NOTE(claude): Ghidra 0x800ad010 `jal ThTick_SetAndExec` then 0x800ad018
// `j 0x800ad17c` — the tail-call returns here and drops to the frameCount/
// boolDestroyed tail, it does NOT return. Match with goto (the prior
// `return` skipped this frame's parent-immunity countdown decrement).
ThTick_SetAndExec(t, RB_TNT_ThTick_ThrowOnHead);
return;
goto LAB_800ad17c;
}

// if this TNT has an InstDef, then it is part of LEV,
Expand Down
9 changes: 9 additions & 0 deletions game/231/231_019_RB_TNT_ThTick_SitOnHead.c
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,15 @@ void RB_TNT_ThTick_SitOnHead(struct Thread *t)
t->flags |= THREAD_FLAG_DEAD;

mw->driverTarget->instTntRecv = NULL;

// NOTE(claude): Ghidra 0x800ad6b0 — this explode path ends via ThTick_FastRET
// (0x80071694, the scheduler trampoline that restores saved sp/ra/s0-s8 and never
// returns to this frame), so the scale-from-table below is NOT reached in retail
// (unlike the < 0x5a path, which falls into it). The value written happens to match
// (numFramesOnHead == 0x5a both before and at the explode frame), so this is
// behaviourally benign, but returning here matches the binary and avoids indexing
// s_tntSitScale out of bounds should numFramesOnHead ever exceed 0x5a.
return;
}

// set scale of TNT, given frame of animation
Expand Down
10 changes: 8 additions & 2 deletions game/231/231_020_RB_TNT_ThTick_ThrowOnHead.c
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,14 @@ void RB_TNT_ThTick_ThrowOnHead(struct Thread *t)
// Set TNT timer to 0, it blows up at 0x5a
mw->numFramesOnHead = 0;

// number of jumps is 7 or 8
mw->jumpsRemaining = 8 - (MixRNG_Scramble() & 1);
// number of jumps is 7 or 8 (or 9 — see NOTE)
// NOTE(claude): Ghidra 0x800ad7d8-f0 computes 8 - (rng % 2) via the signed
// truncating-modulo idiom (srl 31 / addu / sra 1 / sll 1 / subu). MixRNG_Scramble
// returns a signed int that can be negative, so an odd-negative result gives
// rng % 2 == -1 → 9 jumps. The prior `& 1` capped it at 7/8, diverging ~25% of
// throws. Use signed % 2 to match retail.
int jumpRng = MixRNG_Scramble();
mw->jumpsRemaining = 8 - (jumpRng % 2);

// play sound that you hit a TNT
PlaySound3D(0x51, inst);
Expand Down
10 changes: 9 additions & 1 deletion game/231/231_054_RB_Burst_DrawAll.c
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,15 @@ void RB_Burst_DrawAll(struct GameTracker *gGT)
pos.vz = burstInst->matrix.t[2];

gte_ldv0(&pos);
gte_mvmva(0, 0, 0, 3, 0);
// NOTE(claude): Ghidra 0x800b2694 `cop2 0x0480012` = gte_rt = MVMVA(sf=1, mx=RT,
// v=V0, cv=TR, lm=0) → (TR·0x1000 + R·V0) >> 12, i.e. the full camera-space transform
// (rotate + translate + >>12; SetRot/SetTransMatrix above load both from the view-proj
// matrix). The prior gte_mvmva(0,0,0,3,0) = cop2 0x0406012 = MVMVA(sf=0, cv=none) is
// rotate-ONLY — it drops the camera translation AND the >>12, so absX/Y/Z came out
// un-translated and ~0x1000× too large, making the |x|<0x100 && |y|<0x100 cull
// essentially always fail: the explosion screen fade-flash never fired and pass 2
// drew every burst on every split-screen. gte_rt matches the binary.
gte_rt();
gte_stlvnl(&transformed);

absX = transformed.vx;
Expand Down
7 changes: 7 additions & 0 deletions game/231/231_059_RB_Blade_LInB.c
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ void RB_Blade_LInB(struct Instance *inst)

struct Blade *bladeObj;

// NOTE(claude): Ghidra 0x800b3988 `lw v0,0x6c(s0); bne v0,zero,end` — retail guards the
// entire birth on inst->thread==0 (as RB_Baron_LInB and the sibling LInB handlers do). The
// decomp dropped it; without it a repeat LInB call re-births a second blade thread and
// overwrites inst->thread, orphaning (leaking) the first still-spinning blade. Match the binary.
if (inst->thread != 0)
return;

struct Thread *t = PROC_BirthWithObject(
// creation flags
SIZE_RELATIVE_POOL_BUCKET(sizeof(struct Blade), NONE, SMALL, STATIC),
Expand Down
Loading