Declare dScMgBase_c slot 29 (OnAimedAtWithEgg) across the minigame family - #2100
Conversation
…mily
Twelfth of the eighteen slot commits. Declares slot 29 on dScMgBase_c and on
the two descendants that override it, so every one of the 32 vtables that
reaches this slot now emits its word from source instead of stopping one
entry short.
romdata_check, against slot 28's tree:
verifiedBytes 35404 -> 35408 (+4)
partialBytes 11376 -> 11500 (+124)
verified/partial/differs counts 465/253/6 unchanged
128 bytes = the 32 tables in the census, four bytes each: 31 of them are
PARTIAL and one is already VERIFIED, which is where the split comes from.
rombuild -j 16 --no-rom: 11,088/11,088 reproducing, 106/106 exact,
100.000000% of compared bytes, ROM-build analysis PASS.
WHAT SETTLED THE SIGNATURE
Arity is measured three times and the third witness is destructive rather
than circumstantial. dScMgSlot3_c's override clobbers r1 with a masked read
of the sub display-control register and only then tail-branches into the
base, so a second argument in r1 would arrive at dScMgBase_c's body as a
DISPCNT word. It cannot be reading one.
Return type is a hint, not a measurement -- the second consecutive slot no
body pins. Neither exit sets r0 deliberately. No caller settles it either,
and that is measured, not assumed: scanning arm9 and all 103 overlays for
the dispatch PAIR (`ldr rN,[rM,#0x74]` immediately followed by `blx rN`,
not any load at +0x74) finds 35 sites, of which exactly ONE is in ov004 or
ov006. That one is inside dScMgBase_c::OnKicked, and the instruction after
the blx is `add r0,r4,#0x4000` -- the result is dead before anything reads
it. The other 34 are in enemy overlays, dActor_c's branch.
No reconciliation pass. This is the first slot since 26 where no descendant
had already declared the member early, so nothing had to be un-declared
alongside it.
A NAME CORRECTION, AND A CAUTION ABOUT ALL OF THEM
ov006:0x020e6d24 carried `recovered name: dScMgTrampoline2_c_OnAimedAtWithEgg`.
Five vtables hold that address, so the body is dScMgD3DBase_c's -- the
seventh such misattribution in this campaign and the sixth on that class.
Unlike slots 26-28 this one is not a twelve-byte veneer but a real 0x68-byte
body, so its include and the type it casts `this` to moved with the name.
The caution is larger and is written into the header. Every method name in
this range is transplanted from include/dActor_c.h at the same index; the
cartridge carries class names only, so the `recovered name:` comments are
themselves index transplants, not independent recoveries. There was never a
second witness. Here the ROM contradicts the transplant: the sole in-family
dispatch is edge-guarded on mMenuOpen, and the body saves POWCNT1's
screen-swap bit and both BG-enable bytes, clears every layer- and
window-enable bit in both DISPCNTs, saves 0x400 bytes of BG palette and
0x2000 bytes of sub-screen OBJ VRAM into the object, and draws the menu's
own palettes and a per-language image over the top. That is the three-item
overlay menu going up. It is not a Yoshi egg. The name is kept, because
coining a replacement would put an unverifiable identifier on a byte-exact
symbol -- but it is kept as a LABEL, and slots 18-30 should be read that way.
AND A REFUTATION BANKED FOR SLOT 30
include/dActor_c.h:151 returns a Vector3 by value at slot 30. That cannot be
transplanted here: at ov004:0x020ae168 slot 30 is dispatched with r0 still
holding `this`, where a 12-byte return would put a hidden result pointer in
r0 and `this` in r1 under AAPCS. Recorded in the header and in the notes now
rather than rediscovered later.
READABILITY
The base body drops its 27-method shadow scaffold -- a local `struct Base`
of twenty-six placeholder virtuals plus `struct Obj : Base` with a
`char pad[0x4700]`, which existed only so `self->Init()` would compile to a
load of vtable+0x68. Slot 26 is OnHitByCannonBlastedChar and dScMgBase_c
declares it, so the class does that job and both calls say what they are.
Field offsets give way to the names the header already carried; the two
buffers inside pad_228 stay as raw offsets, since this body is the only
evidence for their extent.
Gates: rombuild PASS, romdata_check no regressions, check_dead_references
clean (two stale prose references to the renamed file retargeted),
port_refcheck 405/405, check_header_offsets 7 changed headers 0 mismatched,
langmode ratchet PASS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QhhAeJwXBnfPp7B5DNjCwh
✅ PR validation — PassedCommitted merge passes; 2 contributor credit reassignment(s) noted, not a blocker. Full merge validation
Contributor credit moved (2)
Byte-verified means the range carries Warnings: contributor attribution changed, not a blocker (2 changed, 0 lost -- src/_ZN14dScMgD3DBase_c16OnAimedAtWithEggEv.cpp: tangosdev -> andrewboudreau; src/_ZN12dScMgSlot3_c16OnAimedAtWithEggEv.c: ruspecial -> andrewboudreau); 5 linkcheck result(s) have unresolved relocations. Per-file link-check detailAll 1192 changed file(s) compile to the ROM byte-for-byte with correct relocation targets.
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. |
|
Self-review (one shared account here, so this is a signed-off review in a comment rather than an Approve). What I checked, and what I'd push back on if someone else had written it.
What this does not prove. No ROM was built by the static gates, and a local Ready as far as I can take it; landing is the repo owner's call, and #2099 goes first. |
andrewboudreau
left a comment
There was a problem hiding this comment.
CHANGES REQUESTED — one ask, and it is the same one still open on #2099 rather than a new class of problem. Reviewed at 50604acdb.
First, the part I went looking to disprove and could not: your return-type framing is right, and I can now upgrade it from a disassembly reading to a build measurement.
The header says slot 29's int is "A HINT, the second consecutive slot that no body pins." I tried to pin it. Built in a wired worktree detached at 50604acdb, 2004/b56, against ov004:0x020af094 size 0x1e8:
CONTROL shipped void MATCH
void + local (restructure control) MATCH
int, early exit `return r26;` MATCH <- int is spellable
int, early exit `return 2;` size 0x1ec (+1 word)
int, early exit `return 0;` size 0x1ec (+1 word)
int, both exits valued `return 0;` size 0x1f0 (+2 words)
d3dbase int, tail call ov006:0x020e6d24 MATCH
slot3 int, tail call ov006:0x0210aa3c MATCH
where r26 is the slot-26 result hoisted into a local:
int r26 = self->OnHitByCannonBlastedChar();
if (r26 == 2)
return r26;That is the spelling your own disassembly note describes — "returning the 2 it has just compared against slot 26's result" — and it costs nothing, because r0 already holds it at the bxeq. The constant spellings cost a word precisely because they force a mov. So the ROM is neutral on slot 29, in both directions, by build. int is not refuted and neither is void.
I am recording the negative rows too, because they are the trap: had I stopped at return 0; I would have posted "the ROM refutes int" with a real-looking measurement behind it. A return-type refutation has to test the free spelling — the one that returns the value already in r0 — not just the one that is easy to write. That generalizes past this slot and past this PR.
The ask
The ROM being neutral is exactly why this has to be resolved by consistency, and right now four places disagree with one:
| where | spelling |
|---|---|
include/dScMgBase_c.h:503 |
virtual int OnAimedAtWithEgg(); |
include/dScMgD3DBase_c.h:98 |
virtual int |
include/dScMgSlot3_c.h:59 |
virtual int |
include/decl_common.h:2322 |
extern void _ZN11dScMgBase_c16OnAimedAtWithEggEv(void*); |
src/_ZN11dScMgBase_c16OnAimedAtWithEggEv.cpp:47 |
extern "C" void |
src/_ZN14dScMgD3DBase_c16OnAimedAtWithEggEv.cpp |
void |
src/_ZN12dScMgSlot3_c16OnAimedAtWithEggEv.c |
void |
decl_common.h:2322 is the one worth naming explicitly: _ZN11dScMgBase_c16OnAimedAtWithEggEv is the mangled name of the member the class declares virtual int. The compiler never sees the collision — one is a class member, the other an extern "C" free function — but the linker resolves them to a single symbol carrying two declared return types. That is also why a naive flip of just the definition does not build (illegal function overloading); it cost me a run to find out, so: the decl has to move with it.
Either resolution merges, same as I said on #2099:
- Honor the headers. All three definitions plus
decl_common.h:2322becomeint, the base gets ther26hoist above, and the two forwardersreturntheir tail call. All four are byte-verifiedMATCHin the table above — this is a known-good edit, not a suggestion to go find one. - Or make the headers
void, matching what the four definitions already say and whatdecl_common.halready declares.
I have no preference between them. What I will not wave through is the slot shipping with the vtable's own declaration contradicting the symbol it points at, when the ROM has declined to break the tie.
Carried from #2099
src/_ZN12dScMgSlot1_c19OnHitFromUnderneathEv.cpp still carries the third copy of the "nothing loads vtable+0x70" claim that the ov004/ov006 scan refuted. I confirmed this PR does not touch it. It is #2099's debt, not yours, and I am noting it here only so it does not fall through the gap between two stacked PRs.
Sequencing
This is stacked on #2099 (cpp/minigame-slot28), which is blocked on the two asks above, so #2100 cannot merge ahead of it regardless of what happens in this thread. Resolve #2099 and this one collapses to a small edit.
Credit where it is due
The caller measurement is the best work in this stack. Scanning for the dispatch pair — ldr rN,[rM,#0x74] immediately followed by blx rN, not any load at +0x74 — is the difference between a measurement and a coincidence, and finding 35 sites of which exactly one is in this class's two modules, then reading that one and showing its result is dead before anything can consume it, is what turned "no caller settles it" from an assumption into a fact. It is also what let me trust that the neutral result above is the real answer rather than an untested caller waiting to contradict it.
The SEMANTIC CAUTION block is the right call and I want it to stay. Every name on this class is an index transplant from dActor_c.h, there was never a second witness, and this is the slot where the body — save POWCNT1's swap bit, blank both DISPCNTs, stash the BG palette and 0x2000 bytes of sub-OBJ VRAM, decompress a per-language image over it — visibly is not a Yoshi egg. Keeping the name as a label rather than coining a replacement is right: a coined name on a byte-exact symbol would block TU promotion later and would read as evidence it is not.
Banking the slot-30 warning forward is the cheapest thing in this PR and possibly the most valuable. dActor_c.h:151 declaring a by-value Vector3 cannot survive a dispatch site that has this in r0 and the loaded pointer in r1; under AAPCS a 12-byte return needs the hidden result pointer in r0. Whoever writes slot 30 will save a day because you wrote that down here.
And the sixth name correction on dScMgD3DBase_c is handled correctly — five tables referencing 0x020e6d24 makes it D3DBase's, and noting that this one is a real 0x68-byte body rather than the twelve-byte veneers of slots 26–28 is the detail that justifies the include and cast changes riding along with it.
Twelfth of the eighteen slot commits in the
dScMgBase_ckeystone campaign. Stacked on #2099 (slot 28); base retargets tomainwhen that lands.What it does
Declares slot 29 on
dScMgBase_cand on the two descendants that override it, so all 32 vtables that reach this slot emit their word from source instead of stopping one entry short.verifiedBytespartialBytes128 bytes total = the census's 32 tables at four bytes each; 31 are PARTIAL and one is already VERIFIED, which is the whole of the split.
rombuild -j 16 --no-rom: 11,088 / 11,088 reproducing, 106/106 exact, 100.000000% of compared bytes, ROM-build analysis PASS.What settled the signature
Arity — measured three times, and the third witness is destructive.
dScMgSlot3_c's override clobbersr1with a masked read of the sub display-control register and only then tail-branches into the base. A second argument passed inr1would arrive atdScMgBase_c's body as a DISPCNT word, so the base cannot be reading one.Return type — a hint, not a measurement, and the second consecutive slot no body pins. Neither exit sets
r0deliberately. No caller settles it either, and here that is measured: scanning arm9 and all 103 overlays for the dispatch pair (ldr rN,[rM,#0x74]immediately followed byblx rN— not any load at+0x74, which also matches an ordinary field read) finds 35 sites, of which exactly one is in ov004 or ov006. That one is insidedScMgBase_c::OnKicked, and the instruction after theblxisadd r0,r4,#0x4000: the result is dead before anything can read it. The other 34 sit in enemy overlays —dActor_c's branch, sharing the index and nothing else.No reconciliation pass. First slot since 26 where no descendant had already declared the member early, so nothing had to be un-declared in the same commit.
A name correction, and a caution about all of them
ov006:0x020e6d24carriedrecovered name: dScMgTrampoline2_c_OnAimedAtWithEgg. Five vtables hold that address, so the body isdScMgD3DBase_c's — the seventh such misattribution in this campaign, the sixth on that class. Unlike slots 26–28 this one is not a twelve-byte veneer but a real 0x68-byte body, so its#includeand the type it caststhisto had to move with the name.The larger caution is written into the header, and it is worth reading before the next slot:
Here the ROM contradicts the transplant. The sole in-family dispatch is edge-guarded on
mMenuOpen, and the body saves POWCNT1's screen-swap bit and both BG-enable bytes into the object, clears every layer- and window-enable bit in both DISPCNTs, saves 0x400 bytes of BG palette and 0x2000 bytes of sub-screen OBJ VRAM into the object, then draws the menu's own palettes and a per-language image over the top. That is the three-item overlay menu going up. It is not a Yoshi egg.The name is kept anyway — coining a replacement would put an unverifiable identifier on a byte-exact symbol, and that blocks data verification. It is kept as a label, not as a claim, and the header now says so. Whether slots 18–30 get a systematic rename to menu/interaction semantics is a call for the repo owner, not for this PR.
A refutation banked for slot 30
include/dActor_c.h:151returns aVector3by value at slot 30. That cannot be transplanted here: atov004:0x020ae168slot 30 is dispatched withr0still holdingthis, where a 12-byte return would put a hidden result pointer inr0andthisinr1under AAPCS. Recorded in the header and innotes/dScMgBase_c-slots-18-35.mdnow rather than rediscovered next week.Readability
The base body drops its 27-method shadow scaffold — a local
struct Baseof twenty-six placeholder virtuals plusstruct Obj : Base { char pad[0x4700]; }, which existed only soself->Init()would compile to a load ofvtable+0x68. Slot 26 isOnHitByCannonBlastedCharanddScMgBase_cdeclares it now, so the class does that job and both calls say what they are. Field offsets give way to the names the header already carried (mSavedScreenSwap,mSavedMainBgBits,mSavedSubBgBits,mSceneKind); the two buffers insidepad_228stay as raw offsets, since this body is the only evidence for their extent. Bothvolatile unsigned short tmp = 0;round-trips are preserved verbatim — they generate thestrh/ldrhpair.Gates
rombuild -j 16 --no-rom— PASS, 11,088/11,088, 106/106 exactromdata_check -j 16— no regressions; deltas as tabled abovecheck_dead_references— clean; two stale prose references to the renamed file retargeted (notes/minigame-provenance.md, andmSavedMainBgBits's own comment indScMgBase_c.h)port_refcheck— 405 references, all resolvecheck_header_offsets --changed origin/main— 7 changed headers, 0 mismatched, 0 unparsedlangmode_audit --check— ratchet PASS🤖 Generated with Claude Code
https://claude.ai/code/session_01QhhAeJwXBnfPp7B5DNjCwh