Skip to content

Enemy/bosstelesa: decompile Boss Telesa behavior - #134

Open
KakarottoCake wants to merge 18 commits into
doldecomp:mainfrom
KakarottoCake:decomp-bosstelesa
Open

Enemy/bosstelesa: decompile Boss Telesa behavior#134
KakarottoCake wants to merge 18 commits into
doldecomp:mainfrom
KakarottoCake:decomp-bosstelesa

Conversation

@KakarottoCake

@KakarottoCake KakarottoCake commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Summary

  • decompile the Boss Telesa fight, roulette/slot machinery, collision actors, managers, bubbles, and all boss/bubble nerves
  • restore the BossTelesa class layouts, save parameters, static tuning data, animation tables, model data, and supporting actor helpers
  • reproduce the retail TU's deferred function emission order, including the interleaved BubbleLive symbols
  • reconstruct the inlined UNUSED bodies the map requires, rather than fencing them off with #pragma dont_inline

Matching

Current figures for the BossTelesa TU:

fuzzy text match 90.455%
exact functions 61 / 100
matched code 8672 / 39636 bytes (21.88%)
matched data 100% across .data, .rodata, .sdata, .sdata2, .sbss

Note that fuzzy_match_percent is lower than it was earlier in the PR's history despite the TU being further along. Reconstructing an inlined UNUSED body adds a function with no counterpart on the target side, so it counts as an extra symbol and drags the average down. This TU is better judged on exact functions and matched code.

Not everything is close yet — TBossTelesaKillSmallEnemy::checkHit and TBossTelesa::forceHide are the weakest, and are genuinely unreconstructed rather than near-misses.

Inline reconstruction

Six pragma-fenced blocks have been reduced to two (randomReset and forceStopSlot). The rest were replaced with the real call structure, using the compiler's own inlining rules rather than suppression:

  • a callee is refused outright above 15 statements; otherwise a per-pass budget applies, where pass 0 (a call written directly in the function's own source) has no limit, pass 1 allows 10, pass 2 allows 7, pass 3 allows 3, and pass 4 never runs
  • a call appearing as an argument to another call counts one pass deeper

This is what makes wrapper methods load-bearing. TBossTelesa::tongueHitWater, checkMessage, checkSlot, slotStart and setBckAnm all exist in the map as UNUSED, and routing calls through them puts the nerve-singleton constructors and MActor accessors at the pass depth where retail refuses to inline them.

setBckAnm is the clearest case. Its real body is the animation-switch block that had been open-coded in setSpicy; moving it there and keeping it under the pass-1 budget of 10 statements is what makes MActor::getCurBckAnmPtr (4 statements) land at pass 3, where MWCC gives up and emits the weak out-of-line copy the map expects from this TU. Four sizes land exactly:

symbol map ours
setBckAnm 0xd4 0xd4
setSpicy 0x19c 0x19c
TBossTelesaTongue::checkHit 0x23c 0x23c
getCurBckAnmPtr (weak) 0x1c 0x1c

TU structure

  • audited all 138 map entries, including all 38 UNUSED symbols
  • all map symbols present, with linkage matching the map
  • mandatory non-weak symbol order passes; what remains is compiler-controlled weak ordering and 18 best-effort UNUSED size warnings, i.e. inlined bodies not yet reconstructed to their retail size

Known gaps

  • 18 UNUSED bodies are still the wrong size, the largest being fanfale (map 496, ours 4), prepareGenerate (472 vs 12) and calcObjCollision (412 vs 4)
  • two #pragma dont_inline blocks remain (randomReset, forceStopSlot); the wrapper that would replace randomReset's has not been identified
  • several functions differ only in stack frame size, with retail always larger; these are left unmatched rather than padded

Verification

  • build/GMSJ01/mario.dol: OK
  • python tools/check-changed-symbol-order.py src/Enemy/bosstelesa.cpp — PASS
  • clang-format clean
  • both CI checks green

@decomp-dev

decomp-dev Bot commented Aug 3, 2026

Copy link
Copy Markdown

Report for GMSJ01 (9b62e09 - fe7bed2)

📈 Matched code: 37.39% (+0.24%, +8672 bytes)
📈 Matched data: 49.97% (+0.88%, +5644 bytes)

✅ 68 new matches
Unit Item Bytes Before After
mario/Enemy/bosstelesa .rodata +2816 0.00% 100.00%
mario/Enemy/bosstelesa .data +2096 0.00% 100.00%
mario/Enemy/bosstelesa TBossTelesaSaveLoadParams::TBossTelesaSaveLoadParams(const char*) +928 0.00% 100.00%
mario/Enemy/bosstelesa __sinit_bosstelesa_cpp +764 0.00% 100.00%
mario/Enemy/bosstelesa TBossTelesaBody::receiveMessage(THitActor*, unsigned long) +584 0.00% 100.00%
mario/Enemy/bosstelesa TBubble::split() +440 0.00% 100.00%
mario/Enemy/bosstelesa TBubbleManager::load(JSUMemoryInputStream&) +412 0.00% 100.00%
mario/Enemy/bosstelesa .bss +328 0.00% 100.00%
mario/Enemy/bosstelesa TBubble::kill() +292 0.00% 100.00%
mario/Enemy/bosstelesa TBossTelesaTongue::receiveMessage(THitActor*, unsigned long) +292 0.00% 100.00%
mario/Enemy/bosstelesa TTelesaSlot::getForcastResult(int) +284 0.00% 100.00%
mario/Enemy/bosstelesa TBossTelesa::kill() +280 0.00% 100.00%
mario/Enemy/bosstelesa TBubble::reset() +276 0.00% 100.00%
mario/Enemy/bosstelesa .sdata2 +272 0.00% 100.00%
mario/Enemy/bosstelesa TBubble::behaveToWater(THitActor*) +236 0.00% 100.00%
mario/Enemy/bosstelesa TTelesaSlot::randomReset() +236 0.00% 100.00%
mario/Enemy/bosstelesa TNerveBubbleSplit::execute(TSpineBase<TLiveActor>*) const +192 0.00% 100.00%
mario/Enemy/bosstelesa TBossTelesa::TBossTelesa(const char*) +188 0.00% 100.00%
mario/Enemy/bosstelesa TTelesaSlot::getResultFromAng(float) +168 0.00% 100.00%
mario/Enemy/bosstelesa TTelesaSlot::moveStart() +164 0.00% 100.00%
mario/Enemy/bosstelesa TBubble::~TBubble() +156 0.00% 100.00%
mario/Enemy/bosstelesa TTelesaSlot::~TTelesaSlot() +156 0.00% 100.00%
mario/Enemy/bosstelesa TBossTelesaBody::~TBossTelesaBody() +132 0.00% 100.00%
mario/Enemy/bosstelesa TBossTelesaTongue::~TBossTelesaTongue() +132 0.00% 100.00%
mario/Enemy/bosstelesa TBossTelesaKillSmallEnemy::~TBossTelesaKillSmallEnemy() +132 0.00% 100.00%
mario/Enemy/bosstelesa TBubbleManager::~TBubbleManager() +116 0.00% 100.00%
mario/Enemy/bosstelesa TBossTelesa::~TBossTelesa() +108 0.00% 100.00%
mario/Enemy/bosstelesa TBossTelesaManager::load(JSUMemoryInputStream&) +108 0.00% 100.00%
mario/Enemy/bosstelesa TBossTelesaManager::~TBossTelesaManager() +100 0.00% 100.00%
mario/Enemy/bosstelesa .sbss +96 0.00% 100.00%

...and 38 more new matches

📈 38 improvements in unmatched items
Unit Item Bytes Before After
mario/Enemy/bosstelesa TBossTelesa::loadAfter() +3232 0.00% 96.77%
mario/Enemy/bosstelesa TBossTelesa::generateSlotItem() +2250 0.00% 77.61%
mario/Enemy/bosstelesa TBossTelesa::moveObject() +1941 0.00% 85.29%
mario/Enemy/bosstelesa TNerveBossTelesaDie::execute(TSpineBase<TLiveActor>*) const +1913 0.00% 85.11%
mario/Enemy/bosstelesa TBossTelesa::init(TLiveManager*) +1717 0.00% 88.91%
mario/Enemy/bosstelesa TNerveBossTelesaPrepareSlot::execute(TSpineBase<TLiveActor>*) const +1315 0.00% 80.21%
mario/Enemy/bosstelesa TBossTelesa::calcRootMatrix() +1301 0.00% 94.59%
mario/Enemy/bosstelesa TNerveBossTelesaAppear::execute(TSpineBase<TLiveActor>*) const +971 0.00% 99.95%
mario/Enemy/bosstelesa TNerveBubbleLive::execute(TSpineBase<TLiveActor>*) const +946 0.00% 94.67%
mario/Enemy/bosstelesa TTelesaSlot::moveObject() +848 0.00% 76.33%
mario/Enemy/bosstelesa TBossTelesa::genAttacker() +786 0.00% 88.94%
mario/Enemy/bosstelesa TBossTelesa::checkHitObject(THitActor*) +711 0.00% 98.85%
mario/Enemy/bosstelesa TNerveBossTelesaSlotStart::execute(TSpineBase<TLiveActor>*) const +691 0.00% 99.97%
mario/Enemy/bosstelesa TBossTelesa::rouletteStart() +624 0.00% 89.23%
mario/Enemy/bosstelesa TBossTelesa::flashItem(int) +550 0.00% 85.41%
mario/Enemy/bosstelesa TNerveBossTelesaSpitSlotItem::execute(TSpineBase<TLiveActor>*) const +537 0.00% 90.86%
mario/Enemy/bosstelesa TBubble::appendEnemy() +531 0.00% 99.87%
mario/Enemy/bosstelesa TBossTelesa::damageRecover() +523 0.00% 81.82%
mario/Enemy/bosstelesa TNerveBossTelesaHide::execute(TSpineBase<TLiveActor>*) const +467 0.00% 99.01%
mario/Enemy/bosstelesa TNerveBossTelesaHideWait::execute(TSpineBase<TLiveActor>*) const +445 0.00% 86.99%
mario/Enemy/bosstelesa TNerveBossTelesaFallDemo::execute(TSpineBase<TLiveActor>*) const +443 0.00% 90.23%
mario/Enemy/bosstelesa TNerveBossTelesaFreeze::execute(TSpineBase<TLiveActor>*) const +426 0.00% 98.70%
mario/Enemy/bosstelesa TBossTelesa::setSpicy(TLiveActor*) +402 0.00% 97.66%
mario/Enemy/bosstelesa TTelesaSlot::initMapObj() +378 0.00% 97.68%
mario/Enemy/bosstelesa TTelesaSlot::forceStopSlot(int) +340 0.00% 89.53%
mario/Enemy/bosstelesa TBossTelesa::perform(unsigned long, JDrama::TGraphics*) +339 0.00% 99.89%
mario/Enemy/bosstelesa TBossTelesa::slotFall() +312 0.00% 95.30%
mario/Enemy/bosstelesa TBossTelesa::rouletteFall() +305 0.00% 84.77%
mario/Enemy/bosstelesa TNerveBossTelesaSpit::execute(TSpineBase<TLiveActor>*) const +303 0.00% 99.93%
mario/Enemy/bosstelesa TBubble::init(TLiveManager*) +295 0.00% 99.88%

...and 8 more improvements in unmatched items

@KakarottoCake KakarottoCake changed the title Enemy/bosstelesa: reconstruct BossTelesa TU scaffolding Enemy/bosstelesa: decompile Boss Telesa behavior Aug 3, 2026
@KakarottoCake

Copy link
Copy Markdown
Contributor Author

Follow-up on the latest commit (19063ba) — a matching pass over the boss/bubble nerves and helpers.

Header

  • isRollDrum(), rouletteFall(), slotFall() changed BOOLbool, matching the clrlwi. bool-test the target emits at the call sites.

Source changes

  • Rewrote the repeated set-old-motion-blend / setBckFromIndex / setMotionBlendRatio / setAnmSound blocks to cache mMActor (MActor* oldAnmActor, blendActor/blendRatio locals, explicit basName if/else) in setSpicy, TNerveBossTelesaDie, SlotStart, Appear, PrepareSlot.
  • genAttacker: raw rand() * 0.000030517578fTMsRange<f32>(0.0f, 1.0f).rand().
  • TNerveBossTelesaDie: unrolled setRollSp calls → for loop.
  • TNerveBubbleLive: swapped the unk1D0 if/else to match branch layout; mVelocity.set(0,0,0)mVelocity = JGeometry::TVec3<f32>(0,0,0).

Effect

function before → after
TNerveBossTelesaDie 73.9% → 94.4%
TNerveBossTelesaSlotStart 79.5% → 94.9%
TNerveBossTelesaAppear 85.9% → 99.5%
TNerveBossTelesaPrepareSlot 83.0% → 95.7%
TBossTelesa::setSpicy 87.7% → 95.7%
TNerveBossTelesaFallDemo 86.6% → 90.2%
TBossTelesa::genAttacker 80.8% → 88.8%
TNerveBubbleLive 86.8% → 94.6%

TU fuzzy match: 90.38% → 93.18%. Remaining residuals are MWCC stack-frame padding, register-allocation shifts, and inlining depth (e.g. TVec3::sub/TMatrix34 ctor kept out-of-line in the target) — left as-is rather than faked.

@KakarottoCake

Copy link
Copy Markdown
Contributor Author

Addressed most of the review in 1ae7da3. Notes on the tricky ones:

Done

  • Sounds: gpMSound->gateCheck(...) + MSoundSESystem::MSoundSE::startSoundActor(...)SMSGetMSound()->startSoundActor(...) with the MSD_SE_* enum (all 23 sites).
  • Particles: added SCENE_BTELESA_JPA_MS_BTLS_* (+ PARTICLE_MS_TLS_CHANGE for 0xCD) and switched all SMS_LoadParticle/emit* calls to the enum.
  • MsMtxSetXYZRPH now uses the f32 overload (drops the (s16)(... * 182.04445f) casts).
  • Dropped the fabricated isActorTypeOf; the (mActorType - base) == 1 was just mActorType == base — wrote the comparison inline (and != for the negated cases).
  • MActor::getAnmBckgetCurBckAnmPtr (matches the real mario.MAP name; made it the weak header inline and dropped the out-of-line def).
  • TTelesaSlot::unk198/unk1A8 are now bool[3] arrays, killing the *(&unk198 + i) / u8* = &unk198 + i fakematch.
  • rolling/allStoppedbool; __fabsffabsf; (EnumCamShakeMode)0xNNCAM_SHAKE_MODE_*; gpMarDirector->fireStartDemoCameraSMSGetMarDirector()->....
  • Removed the volatile f32 min/max fakematch in forceStopSlot, replaced with TMsRange<f32>.

Pushed back on

  • MsWrap: I tried MsWrap(x, 0, 360) for the drum-angle wrap, but the two-if form (>= 360.0f / <= 0.0f) is what matches 100% (MsWrap uses while + strict <, which generates different branches). Left it as the two ifs.

Still TODO (not attempted / couldn't resolve cleanly)

  • #pragma dont_inline blocks are still there (haven't found the real inlines they're masking).
  • checkHit/checkHit UNUSED bodies are still empty stubs (sizes don't match the map).
  • getCurBckAnmPtr body is still a guess (return unkC;, 8B vs map 0x1C).

One thing to flag: routing the sounds through the inline SMSGetMSound()->startSoundActor causes an inlining cascade — TBossTelesaKillSmallEnemy::checkHit drops 99.7% → 0% because forceHide() (which now carries the inline sound helper) gets inlined into it instead of staying a bl. A couple of other sound-bearing functions shift a few % for the same reason. I kept the sound change since it's what the cleanup wants, but wanted you to know it's not a pure win here.

Comment thread src/Enemy/bosstelesa.cpp Outdated
Comment thread src/Enemy/bosstelesa.cpp
}
}

BOOL TNerveBubbleLive::execute(TSpineBase<TLiveActor>* spine) const

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nerve definitions/declarations should use the macros

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not done — I think this one has to stay hand-written, and I would rather flag it than quietly skip it.

DEFINE_NERVE emits theNerve() and execute() adjacent. The map has them separated by two other functions:

002d2c38 0003e8  execute__16TNerveBubbleLiveCFP24TSpineBase<10TLiveActor>
002d3020 000214  appendEnemy__7TBubbleFv
UNUSED   0000d8  appendItem__7TBubbleFv
UNUSED   000064  theNerve__16TNerveBubbleLiveFv
002d3234 00005c  __dt__16TNerveBubbleLiveFv

Since .text emission is reverse definition order, that means the retail source defined theNerve, then appendItem, then appendEnemy, then execute — so the two halves cannot be adjacent.

I tried it anyway to be sure: converting to DEFINE_NERVE makes check-changed-symbol-order.py fail with Non-weak symbol ORDER differs from the map. Reverted. Every other nerve in the file does use the macro — this is the only one that cannot. Happy to add a comment above it explaining why if you would like.

🤖 Addressed by Claude Code

Comment thread src/Enemy/bosstelesa.cpp Outdated
Comment thread src/Enemy/bosstelesa.cpp Outdated
Comment thread src/Enemy/bosstelesa.cpp Outdated
Comment thread src/Enemy/bosstelesa.cpp Outdated
Comment thread src/Enemy/bosstelesa.cpp Outdated
Comment thread src/Enemy/bosstelesa.cpp Outdated
Comment thread src/Enemy/bosstelesa.cpp Outdated
Comment thread src/Enemy/bosstelesa.cpp Outdated
@KakarottoCake

Copy link
Copy Markdown
Contributor Author

Pushed two more commits addressing feedback:

d78a664 — use MActor motion-blend accessors
Collapsed 42 copies of the open-coded motion-blend boilerplate (null-checking unkC, pulling unk24, calling setOldMotionBlendAnmPtr/setMotionBlendRatio by hand) into the existing MActor wrapper calls (getBckAnm(), setBckOldMotionBlendAnmPtr(), setMotionBlendRatioForBck()). Net -183 lines, no regressions (had to cache boss->mMActor in a local before the two calls to keep codegen matching register allocation on the double-indirect case).

fc9fd83 — reconstruct the inlined checkHit bodies
TBossTelesaBody::checkHit and TBossTelesaTongue::checkHit are UNUSED in mario.MAP (fully inlined into moveObject). Moved the open-coded collision loops out of moveObject and back into these methods, matching the map. Also swapped the hand-rolled isEnemy bool + branch for checkActorType(ACTOR_TYPE_ENEMY), and the player-type comparison for isActorType(0x80000001), per the actor-type inline feedback.

Still open from the review: the 6 remaining #pragma dont_inline fakematches, and ~20 other UNUSED bodies that still need reconstructing to their map sizes (fanfale, checkMessage, prepareGenerate, calcObjCollision, tongueHitWater, appendItem, etc.). Also flagging two spots where I think the review note and the ROM disagree — will follow up in a separate comment/reply on those threads rather than silently going against them.

@KakarottoCake

KakarottoCake commented Aug 21, 2026

Copy link
Copy Markdown
Contributor Author

Edited: this predated the rebase onto main and the d2c364b9 fix. The commit SHAs below were rewritten by the rebase, and the open question at the end has since been answered — corrected inline so nobody spends time on it.

Status update — five commits.

match TBubble::kill
onLiveFlag(LIVE_FLAG_UNK20000) belongs inside the nerve check, not after it. The ROM's early-exit branch lands on the 0x40 flag block and skips the 0x20000 one, so when the current nerve is already TNerveSmallEnemyDie only 0x40 is set. 100%.

tongueHitWater and checkMessage wrappers
Both collision-actor receiveMessage overrides forward to a TBossTelesa method instead of touching the spine directly. Both are UNUSED in the map, so they inline straight back and the emitted sizes are unchanged.

The extra layer is what fixes the nerve singletons. Reaching pushNerve directly put TNerveBase<TLiveActor>::TNerveBase() at inline pass 3, where its 2 statements fit the budget of 3 and it got inlined; the ROM has a bl. The wrapper pushes it to pass 4, which never runs. Both overrides now match 100%.

reconstruct checkSlot
The tail of the SlotStart nerve is a bool-returning method (UNUSED, 0xd4), not an inline early return. An inlined bool-returning function whose result is then tested is what emits the ROM's li r0,1 / b / li r0,0 / clrlwi. / beq; we were branching directly.

reconstruct slotStart, drop four dont_inline pragmas
moveStart is 19 statements, so at pass 0 — which has no limit — it expanded, and the pragma existed only to stop that. Routing through TBossTelesa::slotStart() puts it at pass 1 where 19 > 10, so it stays out-of-line on its own. The pragmas around isRollDrum, getSlotResult and forceAllItemKill turned out to do nothing at all — byte-identical with or without — and were removed.

getCurBckAnmPtr naming
Per your answer: the null-checked unkC->unk24 body returning J3DAnmTransformKey* takes the real name, and the 8-byte return unkC; getter becomes getUnkC(). The map confirms it — one entry, 28 bytes, only in this TU.


The open question in the original version of this comment is now resolved, in d2c364b9. I had asked which method causes getCurBckAnmPtr__6MActorFv to be emitted, and reported that I had tried setBckAnm and reverted it after it made TNerveBossTelesaSpit::execute worse.

setBckAnm was in fact the right answer; I had it at the wrong depth. A compiler inline trace gives getCurBckAnmPtr as 4 statements, so it is refused only at pass 3 (budget 3), never at pass 2 (budget 7). The chain that produces the emission is:

TBossTelesaTongue::checkHit (source)
  -> setSpicy                     pass 0  inlined
  -> setBckAnm                    pass 1  inlined - must be <= 10 statements
  -> setBckOldMotionBlendAnmPtr   pass 2
  -> getCurBckAnmPtr (nested arg) pass 3  REFUSED -> emitted

My first attempt routed it from the Spit nerve instead, which is one layer too shallow, and setBckAnm was 15 statements — small enough to inline at pass 0 but refused at pass 1, so the chain died one link short and nothing was emitted. Collapsing one if/else in it to a ternary brought it under 10. setBckAnm's real body is the animation-switch block that had been open-coded in setSpicy.

Four sizes now land exactly on the map — setBckAnm 0xd4, setSpicy 0x19c, TBossTelesaTongue::checkHit 0x23c (was 0xec), and getCurBckAnmPtr 0x1c (was absent) — and it is emitted at exactly the map's position, immediately after checkHit. check-changed-symbol-order.py goes from FAIL to PASS. No question outstanding here.

For current figures see the description; the ones originally quoted in this comment were against a base the rebase has since superseded.

@KakarottoCake
KakarottoCake force-pushed the decomp-bosstelesa branch 2 times, most recently from eeb747a to 574381a Compare August 22, 2026 07:45
KakarottoCake and others added 17 commits August 22, 2026 22:22
- route sounds through SMSGetMSound()->startSoundActor + SoundEffects enum
- add boss telesa particle enum entries, use them in emit/load
- use MsMtxSetXYZRPH f32 overload in TBubble::calcRootMatrix
- drop fabricated isActorTypeOf, use direct actor-type comparisons
- rename MActor::getAnmBck -> getCurBckAnmPtr (real map name)
- unk198/unk1A8 -> bool[3] arrays (remove pointer fakematch)
- rolling/allStopped -> bool, MsWrap-free 360 wraps restored
- fabsf instead of __fabsf, CAM_SHAKE_MODE enum, SMSGetMarDirector
Replace 42 hand-expanded copies of the bck motion-blend boilerplate
with the MActor wrappers that already exist for them, matching the
idiom Mrkol used when cleaning up bosseel:

    actor->setBckOldMotionBlendAnmPtr(actor->getBckAnm());
    actor->setBckFromIndex(n);
    actor->setMotionBlendRatioForBck(ratio);

No raw ->unkC accesses are left in the TU. Net -183 lines, and the
match improves 92.242% -> 92.414% (TNerveBossTelesaAppear 99.5 -> 99.9,
TNerveBossTelesaHide 96.3 -> 99.0, setSpicy 95.7 -> 97.7,
moveObject 78.2 -> 80.1).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TBossTelesaBody::checkHit and TBossTelesaTongue::checkHit are UNUSED in
mario.MAP -- they are real functions that the compiler fully inlined and
the linker then stripped. Their bodies were open-coded inside
TBossTelesa::moveObject instead of living in the methods themselves.

Move each loop into its own method and let moveObject just call them, the
same way TBGCork::perform was pulled out of TBossGesso::perform. Body's
out-of-line copy now matches the map size exactly (0xa0); Tongue is at
0xec of 0x23c, so it still has code left to recover.

Also use the actor-type inlines rather than open-coded comparisons:
checkActorType(ACTOR_TYPE_ENEMY) for the "is an enemy" test Mrkol pointed
at, and isActorType(0x80000001) for the Mario checks -- the boss's own
collision loop was comparing against ACTOR_TYPE_PLAYER (0x80000000)
rather than the 0x80000001 the game actually uses.

unk16C/unk170/unk174 now carry their real types instead of THitActor*.

moveObject 78.2% -> 85.3%; TU 92.414% -> 92.712%.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The 0x20000 live flag is set inside the nerve check, not after it. The
ROM's early-exit branch lands on the 0x40 flag block and skips the
0x20000 one, so when the current nerve is already TNerveSmallEnemyDie
only 0x40 gets set.

TBubble::kill() now matches 100%.
…ppers

Both collision-actor receiveMessage overrides forward to a TBossTelesa
method rather than touching the spine directly. Both of those methods are
UNUSED in mario.MAP, so they are fully inlined back into the caller and
the emitted code is unchanged in size.

The extra layer is what makes the nerve singletons match. Reaching
pushNerve directly put TNerveBase<TLiveActor>::TNerveBase() at inline
pass 3, where its 2 statements fit the budget of 3 and it was inlined.
Going through the wrapper pushes it to pass 4, which never runs, so it
stays an out-of-line call exactly as the ROM has it.

TBossTelesaTongue::receiveMessage and TBossTelesaBody::receiveMessage
both now match 100%.
The tail of the SlotStart nerve is a bool-returning TBossTelesa method,
not an inline early return. checkSlot is UNUSED in mario.MAP at 0xd4,
which is large enough for the isRollDrum test, the unk18C reset and the
whole SpitSlotItem nerve singleton block.

Routing through it fixes two things at once: the inlined bool result is
materialised and then tested, which is the li/b/li/clrlwi./beq sequence
the ROM emits, and the extra call layer pushes
TNerveBase<TLiveActor>::TNerveBase() out to inline pass 4 so it stays an
out-of-line call.

TNerveBossTelesaSlotStart::execute now differs only in stack frame size.
moveStart is 19 statements, so a call to it at inline pass 0 (which has
no limit) was being expanded; the pragma existed only to stop that. The
SlotStart nerve reaches it through TBossTelesa::slotStart(), which is
UNUSED in mario.MAP at 0x40 -- exactly the three statements it needs.
Going through the wrapper puts moveStart at pass 1, where 19 statements
exceed the budget of 10 and it stays an out-of-line call on its own.

The pragmas around isRollDrum, getSlotResult and forceAllItemKill turn
out to do nothing at all: those three are byte-identical with or without
them.

Verified by diffing the full nonmatching symbol set before and after:
identical, no regressions. Two pragmas remain, around randomReset (its
caller TNerveBossTelesaAppear::execute drops to 80% without it) and
forceStopSlot.
mario.MAP records getCurBckAnmPtr__6MActorFv at 0x1c (28 bytes), but the
accessor we had under that name was `return unkC;`, which compiles to 8.
28 bytes is the shape of the null-checked `unkC->unk24` body we had named
getBckAnm, so the two were swapped.

Per Mrkol: getCurBckAnmPtr is the null-checked one returning
J3DAnmTransformKey*, and the 8-byte `return unkC;` getter is ours to name,
so it becomes getUnkC() alongside the existing fabricated getUnk28().

They are not interchangeable -- different return types and different
callers -- so this is a rename of both, not a merge. 8 references to the
old getCurBckAnmPtr (gatekeeper, hinokuri2, NpcInitPrg) and 27 to
getBckAnm (CameraBck, bosseel, bosstelesa) updated.

No change to the nonmatching set in any of the six affected translation
units, CameraBck included, which is at 0.

This does not yet clear the validator's MISSING report for the symbol:
the map wants bosstelesa.o to define a weak out-of-line copy, and that
only appears once some call to it fails to inline.
main removed the base-class LIVE_FLAG_UNK10000 because 0x10000 is
overloaded between derived classes, so declare a class-scoped one on
TBossTelesa and qualify the use, following TBossEel.

Also reflow the six setBckOldMotionBlendAnmPtr calls that went over the
80-column limit after the getCurBckAnmPtr rename.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
setBckAnm was a two-statement stub with no callers; the map wants 0xd4.
Its real body is the animation-switch block that was open-coded in
setSpicy, so move it there and have setSpicy call setBckAnm(1).

That also fixes the missing getCurBckAnmPtr__6MActorFv weak copy the map
expects from this TU. getCurBckAnmPtr is 4 statements, so it is only
refused at inline pass 3 (budget 3). Writing the block in setSpicy put it
at pass 2; routing through setBckAnm and letting setSpicy inline into
TBossTelesaTongue::checkHit puts it at pass 3, where MWCC gives up and
emits the out-of-line copy. setBckAnm has to stay under the pass-1 budget
of 10 statements for that to happen, hence the ternary for the bas name.

All four sizes now land exactly on the map:

  setBckAnm                    0xd4 (212)
  setSpicy                     0x19c (412)
  TBossTelesaTongue::checkHit  0x23c (572), was 0xec
  getCurBckAnmPtr              0x1c (28), was absent

check-changed-symbol-order.py goes from FAIL (1 missing symbol, 20 UNUSED
size mismatches) to PASS with 18. No change to the nonmatching set.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- reconstruct TBubble::appendItem from the block open-coded at the bubble
  generation site, symmetric with appendEnemy; lands on the map's 0xd8
- offAllCollision/onAllCollision had a fourth collision actor the map does
  not account for. Dropping unk174 puts both at the map's 0x30, and the
  three open-coded sites now call offAllCollision()
- use MActor/J3DModel accessors instead of mMActor->unk4->unk20
- use the MTX* wrapper macros instead of PSMTX* throughout
- use checkLiveFlag() instead of testing mLiveFlag directly
- hardcode the composed actor types rather than composing them from the
  ACTOR_TYPE_ macros
- give unk2A8 and unk2F8 their real TMapObjBase* type and drop the twelve
  casts at the use sites; likewise hoist the TItem* cast in slotFall

UNUSED size mismatches 18 -> 15. No change to the matched set: 61/100
functions and 8672 bytes before and after, per-function percentages
identical. mario.dol: OK.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@KakarottoCake
KakarottoCake force-pushed the decomp-bosstelesa branch 2 times, most recently from c045766 to 0aa5dbd Compare August 23, 2026 06:53
unk178/unk17C/unk180 were three adjacent TRoulette* fields that four
different sites already treated as an array, three of them by casting:

    ((TRoulette**)&unk178)[i]->setRollSp(unk184->unk1E4[i]);
    (&unk178)[found] = (TRoulette*)actor;

Declaring `TRoulette* unk178[3]` says what those sites are doing and
removes all three casts. The original almost certainly declared it this
way, since indexing three separately-named pointers is not something the
code would do by choice.

No effect on the match: 61 of 100 functions byte-identical and 8672 bytes
before and after, per-function percentages unchanged. mario.dol: OK.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants