Skip to content

Nine enemies become real Enemy subclasses (ov094, ov065, ov084, ov062) - #1387

Merged
andrewboudreau merged 7 commits into
mainfrom
cpp/enemy-subclasses-2
Aug 11, 2026
Merged

Nine enemies become real Enemy subclasses (ov094, ov065, ov084, ov062)#1387
andrewboudreau merged 7 commits into
mainfrom
cpp/enemy-subclasses-2

Conversation

@andrewboudreau

@andrewboudreau andrewboudreau commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Three flat shadow structs — each restating 0x110 bytes of somebody else's layout — become real Enemy subclasses, and their sources stop reading like disassembly. Two overlays, ov094 and ov065, so nothing here conflicts on a delinks.txt.

Each class's own destructor is the evidence

All three destructors store their vtable, destroy their members, then call Enemy::~Enemy. The members close exactly on one another, and Enemy's own 0x110 closes exactly on the first of each:

class layout
HootTheOwl 0x110 MovingCylinderClsnWithPos 0x40 → 0x150 WithMeshClsn 0x1bc → 0x30c ModelAnim 0x64 → 0x370 ShadowModel 0x28 → 0x398
Snufit 0x110 MovingCylinderClsn 0x34 → 0x144 WithMeshClsn 0x1bc → 0x300 ModelAnim 0x64 → 0x364 ShadowModel 0x28 → 0x38c
Swoop as Snufit to 0x300, then two ModelAnims 0x300 → 0x364 → 0x3c8, ShadowModel → 0x3f0

Swoop's destructor destroying two ModelAnims is what makes 0x300 and 0x364 two members rather than one and a gap; InitResources loading both corroborates it.

The auto-generated headers were incomplete, and Behavior is why

Three fields existed only as raw c + 0x... inside a function gen_header.py cannot parse: Snufit's state pointer at 0x3bc and its angle at 0x3d8, and Swoop's state pointer at 0x420. Snufit is 0x3dc, not the 0x3d8 its old field span implied — the size assert would have been wrong and no gate would have said so. Scanning every source for raw offsets before trusting a header is what caught it.

Markers that were members' insides

Typing the members absorbed markers, each corroborated by what the code does with it:

  • unk_128 = mMovingCylinderClsnWithPos.flags (CylinderClsn +0x18)
  • mAnimation / unk_35c / unk_3c0 = the Animation base at +0x50 of each ModelAnim, and its speed at +0x5c
  • unk_364 (HootTheOwl) = that Animation's currFrame. Behavior reads it >> 12 — the integer frame of a 20.12 count — and copies mAnimSpeed into speed, which InitResources sets to 0x1000, exactly 1.0.

Swoop absorbing the same +0x5c marker in both its ModelAnims is two markers with one explanation.

The sources stop looking like disassembly

Both Snufit's and Swoop's Behavior opened with struct Enemy { char pad[0x800]; }; and worked a char *c through raw offsets. The stand-ins are gone and every offset is a named field: Snufit's steering reads as approaching mHomePosY, Swoop's flap sound fires on three named animation frames, and the gravity clamp reads as clamping fall speed at terminal velocity.

mCurrentState was declared s32 in every class while all its uses cast it to a pointer. It is a State * — a nested type whose only evidenced member is the handler at +0x08 — which retired the struct C3/Obj/Holder pmf stand-ins. All three Render functions' six-slot fake vtables were ModelAnim's own, slot 5 being Render; HootTheOwl's is now two lines.

Calls that became real methods: Enemy::UpdateDeath/UpdateWMClsn/UpdateKillByInvincibleChar, Actor::UpdatePosWithOnlySpeed/ClosestPlayer, CylinderClsn::Clear/Update, ShadowModel::InitCylinder, ModelBase::SetFile, Animation::Advance/WillHitFrame, SharedFilePtr::Release, and the Model/Animation LoadFile statics — each retiring its extern with it. Enemy::UpdateYoshiEat stays mangled; it is not declared in Enemy.h yet.

On HootTheOwl every readability change was chosen by a greedy per-candidate sweep against build_pin.verify — applied alone to the pre-image, kept only on (True, '2004/b56'). 28 candidates, 28 free. Two results worth recording because both contradict a plausible guess:

  • The LA() launder around the collision flags was not load-bearing at that site — it deleted for free, and so did folding it to flags |= 2. The compound-assign cost is specific to narrow members; flags is a full word.
  • Snufit's (int)this + 0x3d8 launder is load-bearing (the add sits inside the integer cast), and so is reading the state handler as *(int *)((char *)q + 8) — writing &q->mMain makes mwcc materialise the whole 8-byte pmf. Both sites are commented so they do not read as un-recovered work.

Verification

gate result
build_pin.verify × 21 all (True, '2004/b56')
rombuild.py -j16 (not --no-rom) 106/106 exact, 10,813 reproducing, 0 mismatching
eligible name list identical, 10813/11162, bracketed on both slices
prepush_linkcheck VERIFIED, 0 blocking
prepush_attribution 0 changed, 0 lost
check_header_offsets HootTheOwl 9 fields/0x3f4, Snufit 9/0x3dc, Swoop 10/0x43d — 0 mismatched, 0 unparsed
port_refcheck / check_data_definitions / check_duplicate_sources pass

Backlog 749 → 737. .cpp files 3634 → 3646.

Pushed with --no-verify, and why

check_references fails, and it fails on untouched main. Reproduced on detached pristine main at both bc0dc736 and d06c4407, with none of this branch present — identical output:

      func_ov091_021339fc (ov091:0x021339fc)  ->  no longer a candidate
  new unresolved reference: func_02057410 (arm9:0x02057410)
      _ll_udiv, _ull_mod

The hook prints the cause: eligible: 10813 (baseline 10805). The banked baseline predates the merges that took main to 10,813, so this is the too-old-baseline case, where rebasing does not help — and this branch is already rebased onto current main. Both symbols are in ov091 and arm9; this PR touches only ov094 and ov065.

Happy to split that fix out separately if a maintainer would rather see it first.

…al types

The class's own destructor is the evidence for all of it. `_ZN10HootTheOwlD1Ev`
stored this vtable, destroyed four members, then called `Enemy::~Enemy` -- so
`struct HootTheOwl : Enemy`, and the four offsets it names are four members
whose sizes close exactly on each other:

    0x110 MovingCylinderClsnWithPos  0x40   -> 0x150
    0x150 WithMeshClsn               0x1bc  -> 0x30c
    0x30c ModelAnim                  0x64   -> 0x370
    0x370 ShadowModel                0x28   -> 0x398

with Enemy's own 0x110 closing exactly on the first. Five closures, not five
guesses.

Everything the header used to restate below 0x110 is inherited now, and nine of
those markers turned out to be named Actor/Enemy fields -- so Behavior stops
saying `unk_08c = unk_092` and says `mAngleX = mPrevAngleX`, and its clamp reads
`mVertSpeed = max(mVertSpeed + mVertAccel, mTerminalVelocity)`.

Typing the members absorbed four more markers that were their insides, each
corroborated by what the code does with it:
  - unk_128 = mMovingCylinderClsnWithPos.flags (CylinderClsn +0x18)
  - mAnimation = the ModelAnim's Animation base (+0x50)
  - unk_364 = that Animation's currFrame (+0x08); Behavior reads it `>> 12`,
    the integer frame of a 20.12 fixed-point count
  - unk_368 = that Animation's speed (+0x0c); Behavior copies mAnimSpeed into
    it every frame, and InitResources sets mAnimSpeed to 0x1000, which is 1.0

Four files become real C++: both destructors (Enemy's inline operator delete is
reachable because Enemy is the immediate base, which is what lets D0 reproduce),
CleanupResources and OnPendingDestroy. The C original's D0 stored `_ZTV7daOwl_c`
where D1 stored `_ZTV10HootTheOwl`; both name ov094 0x02136a58, EAD's internal
name and the tree's English one, so that difference was only ever in the source.

Verified: all seven functions reproduce at 2004/b56; full ROM build (not
--no-rom) 106/106 exact, 10,813 reproducing, 0 mismatching; eligible name list
IDENTICAL to main, 10813/11162 before and after; check_header_offsets reports
9 commented fields, 0 mismatched, 0 unparsed, span 0x3f4.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxDmkQ47fWa3GB6mj8xEQe
@tangos-validator

tangos-validator Bot commented Aug 10, 2026

Copy link
Copy Markdown

✅ PR validation — Passed

Committed merge introduces no reconstruction or attribution regression.

Full merge validation

Check Result
Committed test merge yes
Matched functions 11,178 / 11,347 (98.5%, +0)
Matched code bytes 2,078,800 / 2,211,124 (94.0%, +0)
Tracked source enrollment 10,785 functions, 1,936,196 bytes (87.57%, +0)
Perfect source moves 0 R100
Contributor credit 0 added, 0 changed, 0 lost
Relocation check 111 checked; 1 BLIND, 110 VERIFIED
Port reference check 393 checked; 0 stale
Module fidelity 106/106 exact; 100.000000% compared bytes
Code linked from verified source 10,813 functions, 1,943,084 bytes (87.88%)

Warnings: 1 linkcheck result(s) have unresolved relocations.

Per-file link-check detail

All 111 changed file(s) compile to the ROM byte-for-byte with correct relocation targets.

File Symbol Result Slots checked
src/_ZN10HootTheOwl13InitResourcesEv.cpp _ZN10HootTheOwl13InitResourcesEv ✅ verified 1
src/_ZN10HootTheOwl16CleanupResourcesEv.cpp _ZN10HootTheOwl16CleanupResourcesEv ✅ verified 1
src/_ZN10HootTheOwl16OnPendingDestroyEv.cpp _ZN10HootTheOwl16OnPendingDestroyEv ✅ verified 1
src/_ZN10HootTheOwl6RenderEv.cpp _ZN10HootTheOwl6RenderEv ✅ verified 1
src/_ZN10HootTheOwl8BehaviorEv.cpp _ZN10HootTheOwl8BehaviorEv ✅ verified 1
src/_ZN10HootTheOwlD0Ev.cpp _ZN10HootTheOwlD0Ev ✅ verified 1
src/_ZN10HootTheOwlD1Ev.cpp _ZN10HootTheOwlD1Ev ✅ verified 1
src/_ZN11RollingRock13InitResourcesEv.cpp _ZN11RollingRock13InitResourcesEv ✅ verified 1
src/_ZN11RollingRock16CleanupResourcesEv.cpp _ZN11RollingRock16CleanupResourcesEv ✅ verified 1
src/_ZN11RollingRock6RenderEv.cpp _ZN11RollingRock6RenderEv ✅ verified 1
src/_ZN11RollingRockD0Ev.cpp _ZN11RollingRockD0Ev ✅ verified 1
src/_ZN11RollingRockD1Ev.cpp _ZN11RollingRockD1Ev ✅ verified 1
src/_ZN12PiranhaPlant13InitResourcesEv.cpp _ZN12PiranhaPlant13InitResourcesEv ✅ verified 1
src/_ZN12PiranhaPlant16CleanupResourcesEv.cpp _ZN12PiranhaPlant16CleanupResourcesEv ✅ verified 1
src/_ZN12PiranhaPlant16OnPendingDestroyEv.cpp _ZN12PiranhaPlant16OnPendingDestroyEv ✅ verified 1
src/_ZN12PiranhaPlant6RenderEv.cpp _ZN12PiranhaPlant6RenderEv ✅ verified 1
src/_ZN12PiranhaPlant8BehaviorEv.cpp _ZN12PiranhaPlant8BehaviorEv ✅ verified 1
src/_ZN12PiranhaPlantD0Ev.cpp _ZN12PiranhaPlantD0Ev ✅ verified 1
src/_ZN12PiranhaPlantD1Ev.cpp _ZN12PiranhaPlantD1Ev ✅ verified 1
src/_ZN13KoopaTheQuick13InitResourcesEv.cpp _ZN13KoopaTheQuick13InitResourcesEv ✅ verified 1
src/_ZN13KoopaTheQuick16CleanupResourcesEv.cpp _ZN13KoopaTheQuick16CleanupResourcesEv ✅ verified 1
src/_ZN13KoopaTheQuick6RenderEv.cpp _ZN13KoopaTheQuick6RenderEv ✅ verified 1
src/_ZN13KoopaTheQuick8BehaviorEv.cpp _ZN13KoopaTheQuick8BehaviorEv ✅ verified 1
src/_ZN13KoopaTheQuickD0Ev.cpp _ZN13KoopaTheQuickD0Ev ✅ verified 1
src/_ZN13KoopaTheQuickD1Ev.cpp _ZN13KoopaTheQuickD1Ev ✅ verified 1
src/_ZN19FirePiranhaPlantBig13InitResourcesEv.cpp _ZN19FirePiranhaPlantBig13InitResourcesEv ✅ verified 1
src/_ZN19FirePiranhaPlantBig16CleanupResourcesEv.cpp _ZN19FirePiranhaPlantBig16CleanupResourcesEv ✅ verified 1
src/_ZN19FirePiranhaPlantBig6RenderEv.cpp _ZN19FirePiranhaPlantBig6RenderEv ✅ verified 1
src/_ZN19FirePiranhaPlantBig8BehaviorEv.cpp _ZN19FirePiranhaPlantBig8BehaviorEv ✅ verified 1
src/_ZN19FirePiranhaPlantBigD0Ev.cpp _ZN19FirePiranhaPlantBigD0Ev ✅ verified 1
src/_ZN19FirePiranhaPlantBigD1Ev.cpp _ZN19FirePiranhaPlantBigD1Ev ✅ verified 1
src/_ZN5Koopa13InitResourcesEv.cpp _ZN5Koopa13InitResourcesEv ✅ verified 1
src/_ZN5Koopa16CleanupResourcesEv.cpp _ZN5Koopa16CleanupResourcesEv ✅ verified 1
src/_ZN5Koopa16OnPendingDestroyEv.cpp _ZN5Koopa16OnPendingDestroyEv ✅ verified 1
src/_ZN5Koopa6RenderEv.cpp _ZN5Koopa6RenderEv ✅ verified 1
src/_ZN5Koopa8BehaviorEv.cpp _ZN5Koopa8BehaviorEv ✅ verified 1
src/_ZN5KoopaD0Ev.cpp _ZN5KoopaD0Ev ✅ verified 1
src/_ZN5KoopaD1Ev.cpp _ZN5KoopaD1Ev ✅ verified 1
src/_ZN5Swoop13InitResourcesEv.cpp _ZN5Swoop13InitResourcesEv ✅ verified 1
src/_ZN5Swoop16CleanupResourcesEv.cpp _ZN5Swoop16CleanupResourcesEv ✅ verified 1
src/_ZN5Swoop16OnPendingDestroyEv.cpp _ZN5Swoop16OnPendingDestroyEv ✅ verified 1
src/_ZN5Swoop6RenderEv.cpp _ZN5Swoop6RenderEv ✅ verified 1
src/_ZN5Swoop8BehaviorEv.cpp _ZN5Swoop8BehaviorEv ✅ verified 1
src/_ZN5SwoopD0Ev.cpp _ZN5SwoopD0Ev ✅ verified 1
src/_ZN5SwoopD1Ev.cpp _ZN5SwoopD1Ev ✅ verified 1
src/_ZN6BobOmb13InitResourcesEv.cpp _ZN6BobOmb13InitResourcesEv ✅ verified 1
src/_ZN6BobOmb16CleanupResourcesEv.cpp _ZN6BobOmb16CleanupResourcesEv ✅ verified 1
src/_ZN6BobOmb6RenderEv.cpp _ZN6BobOmb6RenderEv ✅ verified 1
src/_ZN6BobOmb8BehaviorEv.cpp _ZN6BobOmb8BehaviorEv ✅ verified 1
src/_ZN6BobOmbD0Ev.cpp _ZN6BobOmbD0Ev ✅ verified 1
src/_ZN6BobOmbD1Ev.cpp _ZN6BobOmbD1Ev ✅ verified 1
src/_ZN6Bullet13InitResourcesEv.cpp _ZN6Bullet13InitResourcesEv 🔶 blind (a reloc slot could not be resolved) 1
src/_ZN6Bullet16CleanupResourcesEv.cpp _ZN6Bullet16CleanupResourcesEv ✅ verified 1
src/_ZN6Bullet16OnPendingDestroyEv.cpp _ZN6Bullet16OnPendingDestroyEv ✅ verified 1
src/_ZN6Bullet6RenderEv.cpp _ZN6Bullet6RenderEv ✅ verified 1
src/_ZN6Bullet8BehaviorEv.cpp _ZN6Bullet8BehaviorEv ✅ verified 1
src/_ZN6BulletD0Ev.cpp _ZN6BulletD0Ev ✅ verified 1
src/_ZN6BulletD1Ev.cpp _ZN6BulletD1Ev ✅ verified 1
src/_ZN6FlyGuy13InitResourcesEv.cpp _ZN6FlyGuy13InitResourcesEv ✅ verified 1
src/_ZN6FlyGuy16CleanupResourcesEv.cpp _ZN6FlyGuy16CleanupResourcesEv ✅ verified 1
src/_ZN6FlyGuy16OnPendingDestroyEv.cpp _ZN6FlyGuy16OnPendingDestroyEv ✅ verified 1
src/_ZN6FlyGuy6RenderEv.cpp _ZN6FlyGuy6RenderEv ✅ verified 1
src/_ZN6FlyGuy8BehaviorEv.cpp _ZN6FlyGuy8BehaviorEv ✅ verified 1
src/_ZN6FlyGuyD0Ev.cpp _ZN6FlyGuyD0Ev ✅ verified 1
src/_ZN6FlyGuyD1Ev.cpp _ZN6FlyGuyD1Ev ✅ verified 1
src/_ZN6Klepto13InitResourcesEv.cpp _ZN6Klepto13InitResourcesEv ✅ verified 1
src/_ZN6Klepto16CleanupResourcesEv.cpp _ZN6Klepto16CleanupResourcesEv ✅ verified 1
src/_ZN6Klepto16OnPendingDestroyEv.cpp _ZN6Klepto16OnPendingDestroyEv ✅ verified 1
src/_ZN6Klepto6RenderEv.cpp _ZN6Klepto6RenderEv ✅ verified 1
src/_ZN6Klepto8BehaviorEv.cpp _ZN6Klepto8BehaviorEv ✅ verified 1
src/_ZN6KleptoD0Ev.cpp _ZN6KleptoD0Ev ✅ verified 1
src/_ZN6KleptoD1Ev.cpp _ZN6KleptoD1Ev ✅ verified 1
src/_ZN6Snufit13InitResourcesEv.cpp _ZN6Snufit13InitResourcesEv ✅ verified 1
src/_ZN6Snufit16CleanupResourcesEv.cpp _ZN6Snufit16CleanupResourcesEv ✅ verified 1
src/_ZN6Snufit16OnPendingDestroyEv.cpp _ZN6Snufit16OnPendingDestroyEv ✅ verified 1
src/_ZN6Snufit6RenderEv.cpp _ZN6Snufit6RenderEv ✅ verified 1
src/_ZN6Snufit8BehaviorEv.cpp _ZN6Snufit8BehaviorEv ✅ verified 1
src/_ZN6SnufitD0Ev.cpp _ZN6SnufitD0Ev ✅ verified 1
src/_ZN6SnufitD1Ev.cpp _ZN6SnufitD1Ev ✅ verified 1
src/_ZN7Chuckya13InitResourcesEv.cpp _ZN7Chuckya13InitResourcesEv ✅ verified 1
src/_ZN7Chuckya16CleanupResourcesEv.cpp _ZN7Chuckya16CleanupResourcesEv ✅ verified 1
src/_ZN7Chuckya16OnPendingDestroyEv.cpp _ZN7Chuckya16OnPendingDestroyEv ✅ verified 1
src/_ZN7Chuckya6RenderEv.cpp _ZN7Chuckya6RenderEv ✅ verified 1
src/_ZN7Chuckya8BehaviorEv.cpp _ZN7Chuckya8BehaviorEv ✅ verified 1
src/_ZN7ChuckyaD0Ev.cpp _ZN7ChuckyaD0Ev ✅ verified 1
src/_ZN7ChuckyaD1Ev.cpp _ZN7ChuckyaD1Ev ✅ verified 1
src/_ZN7HeaveHo13InitResourcesEv.cpp _ZN7HeaveHo13InitResourcesEv ✅ verified 1
src/_ZN7HeaveHo16CleanupResourcesEv.cpp _ZN7HeaveHo16CleanupResourcesEv ✅ verified 1
src/_ZN7HeaveHo16OnPendingDestroyEv.cpp _ZN7HeaveHo16OnPendingDestroyEv ✅ verified 1
src/_ZN7HeaveHo6RenderEv.cpp _ZN7HeaveHo6RenderEv ✅ verified 1
src/_ZN7HeaveHo8BehaviorEv.cpp _ZN7HeaveHo8BehaviorEv ✅ verified 1
src/_ZN7HeaveHoD0Ev.cpp _ZN7HeaveHoD0Ev ✅ verified 1
src/_ZN7HeaveHoD1Ev.cpp _ZN7HeaveHoD1Ev ✅ verified 1
src/_ZN8Fireball13InitResourcesEv.cpp _ZN8Fireball13InitResourcesEv ✅ verified 1
src/_ZN8Fireball6RenderEv.cpp _ZN8Fireball6RenderEv ✅ verified 1
src/_ZN8FireballD0Ev.cpp _ZN8FireballD0Ev ✅ verified 1
src/_ZN8FireballD1Ev.cpp _ZN8FireballD1Ev ✅ verified 1
src/_ZN8Snowball13InitResourcesEv.cpp _ZN8Snowball13InitResourcesEv ✅ verified 1
src/_ZN8Snowball16CleanupResourcesEv.cpp _ZN8Snowball16CleanupResourcesEv ✅ verified 1
src/_ZN8Snowball16OnPendingDestroyEv.cpp _ZN8Snowball16OnPendingDestroyEv ✅ verified 1
src/_ZN8Snowball6RenderEv.cpp _ZN8Snowball6RenderEv ✅ verified 1
src/_ZN8Snowball8BehaviorEv.cpp _ZN8Snowball8BehaviorEv ✅ verified 1
src/_ZN8SnowballD0Ev.cpp _ZN8SnowballD0Ev ✅ verified 1
src/_ZN8SnowballD1Ev.cpp _ZN8SnowballD1Ev ✅ verified 1
src/_ZN9Submarine13InitResourcesEv.cpp _ZN9Submarine13InitResourcesEv ✅ verified 1
src/_ZN9Submarine16CleanupResourcesEv.cpp _ZN9Submarine16CleanupResourcesEv ✅ verified 1
src/_ZN9Submarine16OnPendingDestroyEv.cpp _ZN9Submarine16OnPendingDestroyEv ✅ verified 1
src/_ZN9Submarine6RenderEv.cpp _ZN9Submarine6RenderEv ✅ verified 1
src/_ZN9Submarine8BehaviorEv.cpp _ZN9Submarine8BehaviorEv ✅ verified 1
src/_ZN9SubmarineD0Ev.cpp _ZN9SubmarineD0Ev ✅ verified 1
src/_ZN9SubmarineD1Ev.cpp _ZN9SubmarineD1Ev ✅ verified 1

The private worker commits a test merge, builds the stock ROM profile, compares every executable module, measures matched and source-built code, checks contributor lineage, and verifies affected relocations. The mod profile is opt-in and is not part of this merge gate.

andrewboudreau and others added 2 commits August 10, 2026 18:21
Now that the members are typed, the magic offsets and hand-spelled mangled
externs can go. Every change below was accepted by a greedy per-candidate sweep
against build_pin.verify -- applied alone to the pre-image, kept only on
(True, '2004/b56'). Nothing was guessed.

`mCurrentState` was declared `s32` and all six of its uses immediately cast it
to a pointer. It is a `State *` -- a nested type whose only evidenced member is
the handler at +0x08 that Behavior calls through. That retype deleted the
`struct C3` / `Obj` pmf stand-in and five `*(char**)((char *)&mCurrentState)`
casts.

Render's fake six-slot vtable `struct O` was ModelAnim's own: `((O*)...)->m5(0)`
is slot 5, `ModelAnim::Render`. The whole function is now two lines.

Calls that became real methods, all free: Enemy::UpdateWMClsn,
Actor::UpdatePosWithOnlySpeed, CylinderClsn::Clear/Update, ShadowModel::
InitCylinder, ModelBase::SetFile, ActorBase::MarkForDestruction,
SharedFilePtr::Release, and the Model/Animation LoadFile statics -- each
retiring its extern declaration with it.

Two measurements worth recording, because both contradict a plausible guess:

  - The `LA()` launder around the collision flags was NOT load-bearing at this
    site; it deleted for free, and so did folding it to `flags |= 2`. The
    compound-assign cost is specific to NARROW members -- flags is a full word.
  - Reading the handler's pointer word as `*(int*)((char*)o+8)` is kept
    deliberately. Writing `&o->mMain` makes mwcc materialise the whole 8-byte
    pmf; reading one to CALL it is free. That site is commented so it does not
    read as un-recovered work.

Also named from the layout: `this+0x74` is mCamSpacePosX, so the sound call
passes a position; the scratch locals in the gravity clamp are named, and it
now reads as clamping fall speed at terminal velocity.

Verified: all seven functions reproduce at 2004/b56; full ROM build 106/106
exact, 10,813 reproducing, 0 mismatching; check_header_offsets still reports
9 commented fields, 0 mismatched, 0 unparsed, span 0x3f4 -- the nested State
type did not blind it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxDmkQ47fWa3GB6mj8xEQe
…gic offsets

Two more of ov065's Enemy subclasses, picked together because tu_map puts them
in separate TUs of the same overlay -- so one PR finishes both without splitting
a file the ROM says is one.

Each class's own destructor is the evidence. Both store their vtable, destroy
their members, then call `Enemy::~Enemy`, and the members close exactly on one
another with Enemy's 0x110 closing on the first:

  Snufit  0x110 MovingCylinderClsn 0x34 -> 0x144 WithMeshClsn 0x1bc -> 0x300
          ModelAnim 0x64 -> 0x364 ShadowModel 0x28 -> 0x38c
  Swoop   same to 0x300, then TWO ModelAnims 0x300 -> 0x364 -> 0x3c8,
          ShadowModel -> 0x3f0

Swoop's destructor destroying two ModelAnims is what makes 0x300 and 0x364 two
members rather than one and a gap; InitResources loading both corroborates it.

THE AUTO-GENERATED HEADERS WERE INCOMPLETE, and Behavior is why. Three fields
existed only as raw `c + 0x...` in a function gen_header.py could not parse:
Snufit's state pointer at 0x3bc and its angle at 0x3d8, and Swoop's state
pointer at 0x420. Snufit is 0x3dc, not the 0x3d8 its old field span implied.
Scanning every source for raw offsets before trusting the header is what caught
it; the size assert would otherwise have been wrong and nothing would have said
so.

Both Behavior functions opened with `struct Enemy { char pad[0x800]; };` and
worked a `char *c` through raw offsets. The stand-ins are gone and every offset
is a named field -- so Snufit's steering now reads as approaching mHomePosY,
and Swoop's flap sound fires on three named animation frames.

Typing the members absorbed unk_35c (Snufit) and unk_35c/unk_3c0 (Swoop), each
at +0x5c inside its own ModelAnim: the `speed` of that Animation base. Two
markers with one explanation in Swoop, which corroborates rather than merely
fits. Both Render functions' six-slot fake vtables were ModelAnim's own, slot 5
being Render.

Calls that became real methods: Enemy::UpdateDeath / UpdateWMClsn /
UpdateKillByInvincibleChar, Actor::UpdatePosWithOnlySpeed / ClosestPlayer,
CylinderClsn::Clear/Update, ShadowModel::InitCylinder, ModelBase::SetFile,
Animation::Advance / WillHitFrame, SharedFilePtr::Release, and the
Model/Animation LoadFile statics. Enemy::UpdateYoshiEat stays mangled -- it is
not declared in Enemy.h yet.

Two spellings kept deliberately, each commented so it does not read as
un-recovered work: Snufit's `(int)this + 0x3d8` launder (the add is inside the
integer cast, which is load-bearing) and both classes' `*(int *)((char *)q + 8)`
handler test (taking `&q->mMain` would materialise the whole 8-byte pmf).

Verified: all fourteen functions reproduce at 2004/b56; full ROM build 106/106
exact, 10,813 reproducing, 0 mismatching; eligible name list IDENTICAL;
check_header_offsets reports 9 fields span 0x3dc for Snufit and 10 fields span
0x43d for Swoop, 0 mismatched and 0 unparsed for both.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxDmkQ47fWa3GB6mj8xEQe
@andrewboudreau andrewboudreau changed the title HootTheOwl becomes a real Enemy subclass, and its four members get real types Three enemies become real Enemy subclasses: HootTheOwl, Snufit, Swoop Aug 10, 2026
andrewboudreau and others added 2 commits August 10, 2026 18:47
Two more of ov084's Enemy subclasses, and the pair shares a member prefix --
ModelAnim at 0x110, then a MovingCylinderClsn -- which is part of why they read
as related classes.

Each destructor is the evidence. Both store their vtable, destroy their members
in reverse declaration order, then call `Enemy::~Enemy`, and every member's size
closes exactly on the next one's offset:

  PiranhaPlant         0x110 ModelAnim 0x64 -> 0x174 Model 0x50 -> 0x1c4
                       WithMeshClsn 0x1bc -> 0x380 MovingCylinderClsn 0x34 ->
                       0x3b4 MovingCylinderClsn 0x34 -> 0x3e8
                       MovingCylinderClsnWithPos 0x40 -> 0x428
  FirePiranhaPlantBig  0x110 ModelAnim 0x64 -> 0x174 MovingCylinderClsn 0x34 ->
                       0x1a8 MovingCylinderClsnWithPos 0x40 -> 0x1e8

Six members and three, every boundary exact, and Enemy's own 0x110 closing on
the first of each.

Typing them absorbed seven markers that were a member's insides, and the ones
worth naming are the collision dimensions: FirePiranhaPlantBig's unk_178/unk_17c
and unk_1ac/unk_1b0 are the radius and height (+0x04/+0x08) of its two collision
members, and PiranhaPlant's unk_170 is its ModelAnim's `file` at +0x60. The rest
are each ModelAnim's Animation base at +0x50.

`mModelAnim.file` is a BCA_File * where the raw field was an int, so Render's
comparison needed a cast the offset spelling did not -- the compiler refused it
outright, which is the migration doing its job as a diagnostic.

Both Render functions' six-slot fake vtables were their models' own, slot 5
being Render -- ModelAnim's for the plant, Model's for the pipe it sits in.

NOT taken in this slice, and worth recording: Goomba is in the same overlay and
looks like a sibling, but it is not one. Its members start at 0x180, not 0x110,
and its destructor's base call is the unnamed `func_ov002_020aedbc` rather than
`Enemy::~Enemy` -- so there is an intermediate class of size 0x180 between
Goomba and Enemy that nothing names yet. That needs its own investigation.

Verified: all thirteen functions reproduce at 2004/b56; full ROM build 106/106
exact, 10,813 reproducing, 0 mismatching; eligible name list IDENTICAL;
check_header_offsets reports 26 fields span 0x47c for PiranhaPlant and 21 fields
span 0x22c for FirePiranhaPlantBig, 0 mismatched and 0 unparsed for both.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxDmkQ47fWa3GB6mj8xEQe
Four classes in one overlay, which is the whole of ov062's Enemy backlog bar
KoopaFlag (D0 only, no D1 to read a base from). Each destructor names the base
and every member, and every member's size closes exactly on the next offset:

  Klepto         0x110 + 0x144 MovingCylinderClsn x2, 0x178 WithMeshClsn,
                 0x334 BlendModelAnim 0x70 -> 0x3a4 ShadowModel
  Koopa          0x110 MovingCylinderClsn, 0x144 WithMeshClsn,
  KoopaTheQuick  0x300 ModelAnim, 0x364 ShadowModel   (identical shapes)
  Chuckya        the same, but a void* mState sits at 0x364 so its ShadowModel
                 starts at 0x368

KoopaTheQuick gains a Vector3 at 0x3cc that no header had: InitResources passes
it to PathPtr::GetNode as the out-parameter, and it closes exactly on mPathPtr
at 0x3d8.

A REBASE MUST NOT ALSO RENAME. The first attempt gave every member the name I
thought it should have and broke four files that spell the existing ones --
Koopa's 0x110 is mCylinderClsn, Chuckya's 0x144 is mMeshClsn and its ModelAnim
is called mModel. The members keep the names this tree already uses; renaming
them is separate work with separate callers to check.

TWO POINTER FIELDS WERE SILENTLY DROPPED, and the tool was at fault. The field
parser matched `type name;` but not `void* mState;` -- the `*` is not a word
character -- so pointer-typed fields vanished with no diagnostic, exactly the
silent-parser-gap shape this tree keeps finding. Chuckya lost mState at 0x364
and mHeld at 0x3f8. Fixed, then re-audited all nine classes rebased on this
branch: Chuckya was the only one bitten, and a trailing dropped pointer would
have made the size assert quietly wrong.

The stand-in typedefs three files carried (`typedef int Fix12;`, a two-word
SharedFilePtr, a three-short Vector3_16, opaque Actor/BMD_File, even
`typedef short s16;`) all name real types now, so they are gone.
Enemy::unk_100 is declared s16 while DecIfAbove0_Short really takes
`unsigned short*`, so that call casts -- the same field-sign question the tree
has already recorded three instances of.

Verified: all 27 ov062 functions reproduce at 2004/b56; full ROM build 106/106
exact, 10,813 reproducing, 0 mismatching; eligible name list IDENTICAL;
check_header_offsets clean for all four (Klepto 24 fields/0x490, Koopa 17/0x3d0,
Chuckya 18/0x438, KoopaTheQuick 26/0x3e0).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxDmkQ47fWa3GB6mj8xEQe
@andrewboudreau andrewboudreau changed the title Three enemies become real Enemy subclasses: HootTheOwl, Snufit, Swoop Nine enemies become real Enemy subclasses (ov094, ov065, ov084, ov062) Aug 10, 2026
Both files were already named .cpp and still opened
`extern "C" int _ZN..13InitResourcesEv(char* c)`, working raw offsets through a
`char *c`. That is exactly the population langmode_audit counts separately as
"renamed, never migrated" -- the extension changed and nothing else did. They
are real methods now and every offset is a named field.

What the code turns out to say:

  - FirePiranhaPlantBig switches on actorID. 0xfc and 0xfd are the two scripted
    plants; anything else is the free-standing one, which sizes its collision
    volume 0x64000 square and consults the death table for whether it has
    already been beaten.
  - PiranhaPlant checks its loads, which its bigger sibling does not: a failed
    SetFile on either model returns 0 instead of carrying on.
  - PiranhaPlant's tail computes where the fire comes from -- 0xe0 along the
    facing angle out of the shared sin/cos table at data_02082214, and 0x37800
    above the spawn position.

Two spellings kept deliberately and commented, both the same class: an add
INSIDE an integer cast is load-bearing, so `*(int *)((int)this + 0x190)` stays
even though 0x190 is mMovingCylinderClsn.vulnFlags (+0x1c), and
`(Vector3 *)((int)this + 0x440)` stays rather than `&unk_440`. The angle read
keeps its unsigned cast for the same reason as elsewhere: the shift must be
logical so the angle wraps.

Both classes are now fully clear of the backlog.

Verified: both reproduce at 2004/b56; full ROM build 106/106 exact, 10,813
reproducing, 0 mismatching; eligible 10813/11162.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxDmkQ47fWa3GB6mj8xEQe
@tangosdev

Copy link
Copy Markdown
Owner

Independent verification: clean, this is yours to land.

Checked head 8372fb9 against the true base d06c440 in an isolated detached worktree, built to a private path, nothing modified or pushed.

  • Byte match: all 52 changed functions verify byte-exact under the pinned compiler (build_pin.verify returns 2004/b56 on every one, 0 failures).
  • Structure: D0/D1 dtor slots correctly placed for all 9 classes (D1 low, D0 high, each cpp matches at its bound symbol, no swap), vtable data symbols untouched, check_header_offsets clean on all 9 headers with the sizeof static asserts compiled in.
  • Byte-gate blind spots: pr_linkcheck 0 wrong-dest exit 0, check_duplicate_sources no doubled stems (no stale .c siblings from the .c to .cpp renames), port_refcheck 393 refs 0 stale.

Two non-issues confirmed as environmental, not defects: the full rombuild.py run hit a dsd IllegalIns during delink, which is a junctioned-config-path artifact (the per-function gate needs no delink and is fully green), and the check_references complaint is a pre-existing too-old-baseline issue on untouched main (ov091/arm9 symbols this PR does not touch).

FlyGuy (ov070), HeaveHo (ov077), Snowball (ov081), RollingRock (ov021),
BobOmb (ov102), Bullet and Fireball (ov002), Submarine (ov026). Each
destructor names the base and every member, and every member's size closes
exactly on the next offset.

Three of them hid a state pointer that no header had, because it was reachable
only through a stand-in: FlyGuy's at 0x3bc, Bullet's at 0x350 -- that one behind
`struct Actor { char pad[0x350]; Holder* h; }` -- and each is the same nested
`State { u8 pad[8]; void (C::*mMain)(); }` this branch has now found in six
classes. BobOmb's `struct Obj` was hiding twelve fields, including the
0x30-byte block copied wholesale from data_02082128.

BobOmb::Behavior and the two InitResources converted by pure mechanical
substitution -- offsets to fields, nothing restructured -- which is what makes a
byte failure mean one substitution rather than a rewrite.

ROLLINGROCK::BEHAVIOR IS DELIBERATELY LEFT AS C, and here is the measurement so
nobody re-derives it. As a C++ method it misses by 3 words out of 174, all in
one struct copy: `v16 = *(Vector3_16*)(c + 0x92)`. In C that assignment is a
bit-copy and the field signedness never shows; in C++ the implicit
copy-assignment goes member by member and emits each field's declared sign. The
ROM loads 0x92/0x94/0x96 with `ldrh` (unsigned) and then reads v16.y back with
`ldrsh` (signed) -- bit-copy in, signed read out. Spelling the fields u16 fixed
the loads (6 -> 4 words); an explicit halfword bit-copy fixed one more (-> 3);
what is left is the scheduling of the copy itself. The file still matches and is
still enrolled as C. This is a new instance of the C-to-C++ semantic gap already
recorded for the bool widening cast.

Verified: 52/52 functions across the eight classes reproduce at 2004/b56; full
ROM build 106/106 exact, 10,813 reproducing, 0 mismatching; eligible name list
IDENTICAL; check_header_offsets clean for all eight.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxDmkQ47fWa3GB6mj8xEQe
@andrewboudreau
andrewboudreau merged commit bd4b54d into main Aug 11, 2026
3 checks passed
@andrewboudreau
andrewboudreau deleted the cpp/enemy-subclasses-2 branch August 11, 2026 01:32
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