CapEnemy derives from Enemy, and the type it holds gets a name - #1402
Merged
Conversation
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxDmkQ47fWa3GB6mj8xEQe
The file byte-matched and had a delink entry, but named the cap-model table `func_020ff028`. Nothing in the tree defines that; the table is `data_ov002_020ff028`, which the sibling UnloadCapModel already spells correctly. eligible.py refuses a file whose references it cannot resolve, so AddCap was never enrolled and the ROM build served the original bytes. Respelling it changes no bytes -- match.py wildcards every relocated word, which is exactly why nothing caught this -- and the function enrolls. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxDmkQ47fWa3GB6mj8xEQe
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxDmkQ47fWa3GB6mj8xEQe
✅ PR validation — PassedCommitted merge introduces no reconstruction or attribution regression. Full merge validation
Warnings: 23 linkcheck result(s) have unresolved relocations; 1 affected source file(s) could not be fully link-checked. Per-file link-check detail1 of 1110 changed file(s) do not match the ROM (NO-SYM).
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. |
This was referenced Aug 12, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
dCapEnemy_cis one of the three intermediate bases betweendEnemyBase_cand itsleaves, and the only one the decomp had already named: it is
CapEnemy, with elevenmatched functions and a hand-annotated header. What it never got was its base class.
It derives from
Enemynow, its constructor and destructor are real C++, and theunnamed 0x1c-byte type it holds gets a name and a header of its own.
It has two children,
daKrb_c(Goomba) anddaTrs_c(Boo), and neither can berebased until this class is.
Two witnesses, in opposite directions
CapEnemy::CapEnemycalls_ZN5EnemyC2Ev, stores the vtable, then constructs theModelat 0x114 and the CapIcon at 0x164. The destructor tears the same two down inthe reverse order and chains to
_ZN5EnemyD2Ev. Forward in one, backward in theother, at the same offsets and with the same types — a layout read twice, not once.
Size 0x180. Nothing allocates a plain
CapEnemy, so there is nooperator newliteral to read the size off the way a leaf class has. Two other factsclose it: the CapIcon at 0x164 is 0x1c bytes and ends exactly at 0x180, and Goomba,
which derives from this class, puts its own first member at 0x180 — a derived member
cannot begin inside its base. Floor and ceiling agree.
Vtable. All 31 slots were diffed against
Enemy's. Exactly two differ, 16 and 17,the destructor pair. That is why the class declares no virtual but its destructor.
The finding that outlives this PR: an undeclared constructor is an inlined one
The first attempt emitted 0x74 bytes against the ROM's 0x34, and the disassembly said
why — where the ROM has
bl _ZN5EnemyC2Evandbl _ZN5ModelC1Ev, the compiler hadwritten out seven vtable stores in place. Neither
Enemy.hnorModel.hdeclared aconstructor at all, so mwcc synthesised one for each and inlined it, dragging the
whole
Actor/ActorBasestore chain into the holder.Declaring
Enemy();andModel();— declared, never defined — fixes it exactly.This is general: every future real C++ constructor in this tree will hit it, and
the failure is not subtle once you look, because
999 word(s) differmeans the sizesdiffer, not that some words do.
Both are widely-included headers, so the change was bracketed with
eligible.pyon aclean
origin/mainand on this branch. The two name lists are identical — zerogained, zero lost. The count alone would not have shown that; a rename can swap one
name for another and leave the total unchanged.
CapIcon, and a struct that had prev and next the wrong way round
The 0x1c-byte record an actor carries while wearing a cap had no name and no header —
every user spelt it as raw offsets. It is reconstructed in
include/CapIcon.hfrom thefour functions that are entirely about it (construct / destruct / link / unlink), the
last two of which between them read and write every word.
The local struct this replaces had
nextat 0x0c andprevat 0x10. That isbackwards:
func_ov001_020ab228links a node by writing the old list head into +0x10and back-patching that head's +0x0c, which is only consistent with 0x0c = prev. The
unlink agrees. Field names cannot change codegen, so nothing caught it and nothing
would have.
This also types the member that blocks
WaterfallMist: its unnamed 0x3d0 member hasthe same constructor and destructor pair,
func_ov001_020ab3c4/020ab3a0. That isa separate PR, but it is unblocked now.
And one function that byte-matched but was never in the ROM
CapEnemy::AddCapnamed the cap-model tablefunc_020ff028. Nothing in the treedefines that symbol — the table is
data_ov002_020ff028, which the siblingUnloadCapModelalready spells correctly.eligible.pyrefuses a file whosereferences it cannot resolve, so despite matching all 0x130 bytes and having a delink
entry, AddCap was never enrolled and the ROM build served the original bytes.
match.pywildcards every relocated word, so respelling it changes nothing — which isexactly why nothing caught it. It enrolls here, and the entry leaves the unresolved
backlog.
Verified
rombuild -j16eligible.pybracketcheck_referencescheck_header_offsetsprepush_attributioncheck_data_definitionscheck_duplicate_sourcesport_refchecklayout_checkNot claimed
_ZN8CapEnemy12UpdateCapPosERK7Vector3RK10Vector3_16does not reproduce, and doesnot on clean
origin/maineither — it is built entirely from local shadow structsand includes none of these headers. Untouched here.
The D1/D2 naming is also left alone.
func_ov002_020aedbchas a body identical to_ZN8CapEnemyD2Evand is very likely the other of the pair, but the vtable's slot 16points at the arm9 copy, and with identical bodies the evidence does not settle which
name belongs to which address. Renaming on a guess would be worse than the placeholder.
🤖 Generated with Claude Code
https://claude.ai/code/session_01XxDmkQ47fWa3GB6mj8xEQe