Declare dScMgBase_c slot 30 (OnAimedAtWithEggReturnVec) across the minigame family - #2102
Conversation
…nigame family
Slot 30 of the 18-35 keystone range. Six vtables gain their thirtieth word;
two of them need a declaration, four inherit dScMgD3DBase_c's. As at slot 29
there is no occupied-slot trap: nothing in the family had declared this member
early, so nothing has to be un-declared alongside it.
romdata_check vs slot 29: verifiedBytes 35408 -> 35412 (+4)
partialBytes 11500 -> 11624 (+124)
128 bytes = 32 tables x one word. _ZTV14dScMgD3DBase_c is the one table
already VERIFIED, so its word lands in the verified pool and the other
thirty-one land in the partial one. Every count -- differs 6, partial 253,
verified 465, symbols 1246, totalRecords 7560 -- is unchanged.
rombuild -j16 --no-rom: 11,088/11,088 reproducing, 106/106 exact,
100.000000% of compared bytes, ROM-build analysis PASS.
THE SLOT THAT SETTLES SLOT 29
0x020aeed8 is the word-for-word mirror of 0x020af094. Slot 29 saves POWCNT1's
screen-swap bit and both BG-enable bytes into the object, blanks both DISPCNTs,
spools 0x400 bytes of BG palette into a buffer at +0x4228 and 0x2000 bytes of
sub-screen OBJ VRAM into +0x2228, then draws the menu over the top. This one
restores every one of those. dScMgBase_c::OnKicked picks between them on an
edge -- it compares mMenuOpen (+0x4628) against unk_462c, does nothing when
they agree, calls slot 30 when mMenuOpen has fallen to zero and slot 29 when it
has risen, then latches mMenuOpen into unk_462c. Slot 29 is the three-item
overlay menu going up; slot 30 is it coming back down.
BOTH HALVES OF THE NAME ARE REFUTED, AND THE NAME IS KEPT ANYWAY
Nothing in the cartridge carries a method name for this class -- its RTTI
carries class names only. Every name in the 18-35 range is transplanted from
include/dActor_c.h at the same index, and the `recovered name:` comments in the
bodies are that same transplant reached a second time, not an independent
witness. Here the ROM contradicts it outright:
* `AimedAtWithEgg` -- see above. It is a screen restore.
* `ReturnVec` -- include/dActor_c.h:151 declares slot 30 returning a Vector3
by value, and in THAT hierarchy that is a real measurement of 0x020100dc.
In this one it cannot hold. A 12-byte return goes back in memory under
AAPCS: r0 would carry a hidden result pointer and `this` would move to r1.
At the dispatch, ov004:0x020ae168, r0 still holds `this` and r1 holds the
loaded function pointer -- and the callee's own first instruction is
`ldr r1,[r0]`, reading its vptr out of r0.
Over in dActor_c, slots 29 and 30 are a lock-on radius and the aim point
derived from it. Here they are a screen save and the restore that undoes it.
Same two indices, same shared ancestor, nothing else in common. The names stay
because coining replacements would put unverifiable identifiers on byte-exact
symbols -- but they are labels, not claims, and the evidence block in
include/dScMgBase_c.h now says so at the point of use.
ALSO IN THIS CHANGE
* Eighth `recovered name:` correction, seventh on dScMgD3DBase_c:
ov006:0x020e6cac was labelled dScMgTrampoline2_c's and included that
header. Five vtables reference it, so it is dScMgD3DBase_c's; the include
and the cast move with the name. Like 29's it is a real body, not a veneer.
* The 27-method shadow scaffold in the base is retired. Its only use was
`o->f68()`, index 26, which is dScMgBase_c::OnHitByCannonBlastedChar now.
* The base takes void* rather than char*, agreeing with the declaration in
decl_common.h that both overrides reach it through -- the char* form was an
illegal overload under C linkage, already logged in the enrollment report.
* Return type is unpinned for the third consecutive slot, and that is
measured rather than assumed: OnKicked is the only caller of slot 30 in
ov004 or ov006, and the instruction after its `blx`, on both arms of the
branch, is `add r0,r4,#0x4000`.
* Prose re-counted in dScMgBase_c.h, dScMgD3DBase_c.h, dScMgSingle3DBase_c.h
and dScMgAmida_c.h (Unk36 now lands on 31), plus the two notes files.
Gates: check_dead_references clean, port_refcheck 405/405, check_header_offsets
--changed origin/main 7 headers 0 mismatched, langmode ratchet PASS.
Five slots left: 31-35.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QhhAeJwXBnfPp7B5DNjCwh
✅ PR validation — PassedCommitted merge passes; 3 contributor credit reassignment(s) noted, not a blocker. Full merge validation
Contributor credit moved (3)
Byte-verified means the range carries Warnings: contributor attribution changed, not a blocker (3 changed, 0 lost -- src/_ZN11dScMgBase_c25OnAimedAtWithEggReturnVecEv.cpp: tangosdev -> andrewboudreau; src/_ZN14dScMgD3DBase_c25OnAimedAtWithEggReturnVecEv.c: tangosdev -> andrewboudreau; src/_ZN12dScMgSlot3_c25OnAimedAtWithEggReturnVecEv.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 — what I checked, and the one thing a reviewer should push back onNo PR in this repo can be formally APPROVED (one shared account), so this is the sign-off in a comment. Byte evidence. Merge-tree gate. Static gates on the branch. Occupied-slot trap: absent, and that is checked, not assumed. Every header in the family was scanned for a class-level virtual that is unmarked or marked past 30 before this was written. The thing to push back onI kept a name the ROM refutes in both halves. Slot 30 is declared I kept it because coining a replacement puts an unverifiable identifier on a byte-exact symbol, and because a mismatched class/method name blocks TU promotion later. The refutation is written into The wider finding behind that, which is worth more than this PR: the Also worth a reviewer's eye
Stacked on #2100 (slot 29), which is stacked on #2099 (slot 28). Five slots left: 31–35. Landing order is yours. |
andrewboudreau
left a comment
There was a problem hiding this comment.
CHANGES REQUESTED — one consolidated ask that is really about the whole stack, plus one prose correction. The slot-30 analysis itself is correct and I verified it against the cartridge instruction by instruction.
What I re-derived independently, and it holds
I disassembled ov004 rather than reading your summary, and every load-bearing claim checks out.
The ReturnVec refutation is sound. At the dispatch:
0x020ae144 mov r4, r0 <- `this` captured, r0 never rewritten after
0x020ae164 ldr r1, [r0]
0x020ae168 ldr r1, [r1, #0x78] <- 0x78 = 4*30, slot 30
0x020ae16c blx r1
r0 still holds this and r1 holds the loaded pointer, so there is no room for a hidden result pointer. A by-value Vector3 is refuted. Good catch, and the contrast you draw with dActor_c's genuine measurement at 0x020100dc is the right way to state it — a divergence between branches, not a defect over there.
The return type is unpinned, and your measurement is the right one. The result is dead one instruction later on both arms:
0x020ae16c blx r1 <- slot 30
0x020ae170 b #0x20ae180
0x020ae17c blx r1 <- slot 29
0x020ae180 add r0, r4, #0x4000 <- r0 overwritten on both paths
This is a caller discarding r0, which is exactly the shape that settles a return type. It also independently corroborates the slot-29 correction I posted on #2100.
The mirror claim is earned, not asserted. mov r1,#0x6600000 / add r1,r1,#0x6000 / mov r2,#0x2000 is slot 29's buffer at 0x06606000 and its 0x2000 length, in reverse; ldr lr,[r4,#0x224] lands exactly on mSavedScreenSwap, which corroborates the header's field map from the other direction; cmp r0,#2; popeq {r4,lr}; bxeq lr is the bare early return you describe; and the tail really is a bic #0x1f00 / orr / str read-modify-write of DISPCNT. Both slot-26 dispatches are ldr r1,[r1,#0x68], 0x68 = 4*26, as they should be.
The OnKicked edge-latch reading is right too — beq when mMenuOpen and unk_462c agree, slot 30 when it has fallen, slot 29 when it has risen, then the latch. Save at 29, restore at 30.
BLOCKING, but fix it ONCE at the bottom of the stack — not here
This PR adds decl_common.h:2321:
extern void _ZN11dScMgBase_c25OnAimedAtWithEggReturnVecEv(void*);while dScMgBase_c.h:577, dScMgD3DBase_c.h:99 and dScMgSlot3_c.h:60 all declare:
virtual int OnAimedAtWithEggReturnVec(); /* slot 30 */and all three definitions are void. One mangled symbol, two declared return types, in headers that never see each other — the compiler cannot diagnose it and the linker binds by name alone. This is the third time in this stack: I raised the identical shape on #2099 (OnHitFromUnderneath) and #2100 (OnAimedAtWithEgg).
I mechanically matched every zero-argument virtual in include/ to its decl_common.h row. Of the 8 that matched, 3 disagree:
| class | method | class decl | decl_common.h |
|---|---|---|---|
| dScMgBase_c | BeforeInitResources |
bool |
int |
| dScMgBase_c | OnAimedAtWithEgg |
int |
void |
| dScMgBase_c | OnAimedAtWithEggReturnVec |
int |
void |
That 3/8 is a floor, not a census — my matcher only walks zero-arg virtuals in the first struct of each header, so the true count is higher. BeforeInitResources is pre-existing and explicitly not your debt; I am recording it because it shows this is systemic rather than a slot-by-slot slip.
So I am not asking for a separate patch here. #2102 sits on #2100 sits on #2099, both already blocked on this. Fix it once, family-wide, in #2099, and #2100 and #2102 inherit it. Either direction resolves it:
- flip the
decl_common.hrows tointand have the definitions return their tail call — I byte-verified this path end-to-end for slot 29 on #2100, including theillegal function overloadingtrap a naive edit hits; or - declare
virtual voidin the class headers, which matches both the definitions and the measurement that the result is dead. Given you have now measured three consecutive slots as unpinned, this is arguably the more honest spelling — but please confirm it is byte-free rather than assuming it.
Either resolution merges the whole stack as far as I am concerned.
One factual correction to the header prose
include/dScMgBase_c.h, in the new slot-30 block:
And the base's own body opens
ldr r1,[r0], reading its vptr out of r0.
It does not. 0x020aeed8 opens:
0x020aeed8 push {r4, lr}
0x020aeedc ldr r2, [pc, #0x148]
Its first use of r0 is mov r4,r0 at 0x020aef18, +0x40 in. The ldr r1,[r0] you are describing is at 0x020aef8c, +0xb4 in, and it is the slot-26 dispatch (ldr r1,[r1,#0x68]), not an entry vptr read.
I suspect a transposition with slot 29, whose body does read its vptr near the top — push {r4,r5,r6,r7,r8,lr}; sub sp,sp,#8; ldr r1,[r0] at 0x020af09c. Easy to do with two mirrored bodies open side by side.
This does not weaken your conclusion, which is why it is a correction and not the blocker: the AAPCS argument is already carried entirely by the dispatch site. The stronger replacement is the callee's own use of r0 as this:
and the callee consumes r0 as
this:mov r4,r0at 0x020aef18, immediately dereferenced asldr lr,[r4,#0x224]— mSavedScreenSwap. Nothing anywhere in the body stores through a result pointer.
Worth fixing because this block is the durable record. The next reader will disassemble 0x020aeed8, see push {r4,lr}, and have to work out which of you is wrong.
Still owed from #2099, not fixed here
src/_ZN12dScMgSlot1_c19OnHitFromUnderneathEv.cpp still carries the third copy of the "nothing loads vtable+0x70" claim, which is false. #2100 did not fix it and #2102 does not touch the file. Carrying it forward so it is not lost when the stack lands.
For my part, I owe a correction of my own in dScMgBase_c.h: the row-18 sentence claiming "declaring void would have changed its bytes" is not supported. That is mine to fix in a follow-up once the slot stack stops moving at 35 — do not take it on here.
Sequencing
#2099 first with the family-wide type fix, then #2100, then this. I will merge all three in that order once the bottom one is coherent. Five slots left.
Slot 30 of the 18-35 keystone range. Six vtables gain their thirtieth word;
two of them need a declaration, four inherit dScMgD3DBase_c's. As at slot 29
there is no occupied-slot trap: nothing in the family had declared this member
early, so nothing has to be un-declared alongside it.
romdata_check vs slot 29: verifiedBytes 35408 -> 35412 (+4)
partialBytes 11500 -> 11624 (+124)
128 bytes = 32 tables x one word. _ZTV14dScMgD3DBase_c is the one table
already VERIFIED, so its word lands in the verified pool and the other
thirty-one land in the partial one. Every count -- differs 6, partial 253,
verified 465, symbols 1246, totalRecords 7560 -- is unchanged.
rombuild -j16 --no-rom: 11,088/11,088 reproducing, 106/106 exact,
100.000000% of compared bytes, ROM-build analysis PASS.
THE SLOT THAT SETTLES SLOT 29
0x020aeed8 is the word-for-word mirror of 0x020af094. Slot 29 saves POWCNT1's
screen-swap bit and both BG-enable bytes into the object, blanks both DISPCNTs,
spools 0x400 bytes of BG palette into a buffer at +0x4228 and 0x2000 bytes of
sub-screen OBJ VRAM into +0x2228, then draws the menu over the top. This one
restores every one of those. dScMgBase_c::OnKicked picks between them on an
edge -- it compares mMenuOpen (+0x4628) against unk_462c, does nothing when
they agree, calls slot 30 when mMenuOpen has fallen to zero and slot 29 when it
has risen, then latches mMenuOpen into unk_462c. Slot 29 is the three-item
overlay menu going up; slot 30 is it coming back down.
BOTH HALVES OF THE NAME ARE REFUTED, AND THE NAME IS KEPT ANYWAY
Nothing in the cartridge carries a method name for this class -- its RTTI
carries class names only. Every name in the 18-35 range is transplanted from
include/dActor_c.h at the same index, and the
recovered name:comments in thebodies are that same transplant reached a second time, not an independent
witness. Here the ROM contradicts it outright:
AimedAtWithEgg-- see above. It is a screen restore.ReturnVec-- include/dActor_c.h:151 declares slot 30 returning a Vector3by value, and in THAT hierarchy that is a real measurement of 0x020100dc.
In this one it cannot hold. A 12-byte return goes back in memory under
AAPCS: r0 would carry a hidden result pointer and
thiswould move to r1.At the dispatch, ov004:0x020ae168, r0 still holds
thisand r1 holds theloaded function pointer -- and the callee's own first instruction is
ldr r1,[r0], reading its vptr out of r0.Over in dActor_c, slots 29 and 30 are a lock-on radius and the aim point
derived from it. Here they are a screen save and the restore that undoes it.
Same two indices, same shared ancestor, nothing else in common. The names stay
because coining replacements would put unverifiable identifiers on byte-exact
symbols -- but they are labels, not claims, and the evidence block in
include/dScMgBase_c.h now says so at the point of use.
ALSO IN THIS CHANGE
recovered name:correction, seventh on dScMgD3DBase_c:ov006:0x020e6cac was labelled dScMgTrampoline2_c's and included that
header. Five vtables reference it, so it is dScMgD3DBase_c's; the include
and the cast move with the name. Like 29's it is a real body, not a veneer.
o->f68(), index 26, which is dScMgBase_c::OnHitByCannonBlastedChar now.decl_common.h that both overrides reach it through -- the char* form was an
illegal overload under C linkage, already logged in the enrollment report.
measured rather than assumed: OnKicked is the only caller of slot 30 in
ov004 or ov006, and the instruction after its
blx, on both arms of thebranch, is
add r0,r4,#0x4000.and dScMgAmida_c.h (Unk36 now lands on 31), plus the two notes files.
Gates: check_dead_references clean, port_refcheck 405/405, check_header_offsets
--changed origin/main 7 headers 0 mismatched, langmode ratchet PASS.
Five slots left: 31-35.