Item (a): mine the screen population found last iteration. Resolving every one's
key list via the string-xref join over its own body gives 298 distinct keys and a
three-way split: 95 = 9 corpus-known + 48 new with a readable key list + 38 bare,
where "bare" means BASE_INFO and nothing else. The bare 38 are the honest limit
of the instrument - a screen whose only catalogued string is BASE_INFO cannot be
named this way.
The control turned the finding into a law. Only 9 of the 19 loaders the corpus
documents appear among the 95, and which nine is not arbitrary. The nine that
reference BASE_INFO all read a SCREEN's own configuration: debriefing, career,
save, medals, evaluation list, gamma, POINT_RANKING, CLEAR_TIME_RANKING, hangar.
The ten that do not all read a DATA TABLE: unit definitions, weapon datasheet,
AIParams, stage settings, chatter, PlayerParams, ControlTweak, the PG* HUD
roster, material slots, and the leaderboard. 9/9 and 10/10, both off-diagonals
empty. The leaderboard falling on the table side is a check rather than an
exception - its own doc records that all 18 of its names are tables.pak names, so
it is a table reader that happens to paint a screen. That exclusion is what last
commit's "universal screen marker" claim was missing.
48 new screens are named by their most distinctive keys, including the title
screen (sub_821C4EB0: TITLE_SCREEN/TITLE_MENU/LOADING), the ready room
(sub_821D6D28: SCRIPT_ID/SCRIPT_PATH, confirmed independently by its own error
string silph::GamePart_ReadyRoom::Impl::PrepareScript), movie subtitles, the
three menu modes STANDARD/EXTRA/TUTORIAL, the speaker window, loadout and gallery
views, mission select, and an equip screen carrying the game's own typo
EQUIIP_LIMITATION.
One caution worth recording: a high key count is not a rich screen. The two
27-key entries sub_82297550 and sub_822A2F00 resolve to pure coordinate pairs
(1080,163 / 734,502 / 1164,403) - a layout table, not configuration. Read the
values before trusting the rank.
Still open: the 38 bare screens, and whether the EX_ in EX_MENU_DISABLE_SKIP and
EX_LETTERS is the same EXTRA mode word as +184.
All seventeen artefacts byte-identical.
Item (a): can the vtable scan name the corpus's anonymous classes?
The route I proposed last turn - "the RTTI behind those two anon classes" - does
not exist. Of all 1150 catalogued vtables, 1150 carry an ANON_ class name, 0 have
rtti_present set, and 0 have a non-empty base_classes_json. Not one named class,
not one RTTI flag, not one base-class edge in the whole table. Withdrawn as
unrunnable; the ANON_Class_XXXXXXXX labels are synthesised, not recovered.
The sibling methods cannot name a class either: slots 0, 2, 3 and 4 of both EX_
classes reference zero strings. Every naming datum sits in slot 1, the method the
corpus already had.
Re-verified at the true addresses (0x820A9760 and 0x820A9350) that sub_822814D8
and sub_8227A3A0 are each slot 1 uniquely, and sub_82286BC8 is in no vtable.
What the shape DID give: both classes are 5-slot with an exactly-88-byte slot 0.
83 vtables have 5 slots, 16 also have the 88-byte slot 0, 13 of those have a
catalogued slot 1, and 10 build a key list. Ranked by key count the two known
screens sit at the top and the rest name themselves - a medals screen
(MEDALS/ACHIEVED_MEDALS/NEW_MEDAL), an evaluation list (EVALUATION_LIST plus
eval_%02d_eval / eval_%02d_points), the gamma screen (GAMMA_RGB/GAMMA_WB),
POINT_RANKING, CLEAR_TIME_RANKING, and three bare ones.
POINT_RANKING and CLEAR_TIME_RANKING being two separate screens is a direct lead
for the open item "which challenge stage is which leaderboard mode": the
leaderboard is ranked two ways and each way has its own class.
But the obvious follow-on claim - that BASE_INFO marks this family - is false,
and the control says so. BASE_INFO is a single string at 0x820A20EC referenced by
95 functions: 10 inside the family, 85 outside, including sub_82286BC8 (the save
screen, in no vtable at all) and sub_822AE628 (the hangar loader). So the
direction inverts: BASE_INFO is the universal first key of every screen key list,
a 95-function population, while the 5-slot/88-byte shape is only a weak filter
that catches 10 of them plus 6 non-screens. Enumerate screens by BASE_INFO, not
by vtable shape.
All seventeen artefacts byte-identical.
The protective sweep after last commit's refutation. The question was which
existing claims the ind_call cross product voids. Answer: none of the ones the
corpus makes.
Every caller-count claim in the corpus verifies against call edges, with zero
ind_call contamination:
title-crash-stl-tree sub_82457780 "one caller" call=1 ind_call=0
isl-trigger-queue sub_8226EAB8 "16 callers" call=16 ind_call=0
isl-coroutine-spawner sub_822737C8 "seven callers" call=7 ind_call=0
challenge-mission-gate 0x82175110 "22 callers" call=22 ind_call=0
4 of 4 exact, and not one of the four carries a single ind_call edge.
There is a structural reason for that rather than luck. Partitioning the two
target populations: 1710 targets have ind_call edges, 12170 have call edges, 103
have BOTH, 1607 are ind_call-only, 12067 are call-only. And all 1710 ind_call
targets are vtable entries - 1710 of 1710, zero partials - against a control
where only 273 of the 12170 call targets are. So ind_call fires only on virtual
methods, and 1607 of them have no direct caller at all, which is exactly why the
two EX_ screens came back empty on call and flooded on ind_call.
The residual risk is narrow and nameable: the 103 targets carrying both kinds,
where a caller count taken without a kind filter would be inflated by about 633.
Everywhere else the two populations are disjoint, so a call-based count is safe
and an ind_call-based one is meaningless.
Rule for future work: always filter kind='call'; and when a function has no call
edges, that is a fact about it - it is a virtual method - not a gap to be filled
from ind_call.
All seventeen artefacts byte-identical.
Chasing what supplies the EX_ mode word. Two routes, both measured to have no
power, plus one structural fact that did come out.
The offset route is dead. stw ..., 184(rN) occurs 301 times in the image, lwz
from +184 occurs 351 times, and 115 functions touch both +144 and +184. +184 is
an ordinary small offset shared by many unrelated classes - the same shape the
corpus already recorded as failing three times. Nothing narrows to a writer.
The bigger result is an instrument refutation with corpus-wide reach. Asking
xrefs for the callers of the two screens returns 633 sources for EACH, and the
two lists are IDENTICAL, which cannot be right. Measuring the relation itself:
ind_call rows 1827297
distinct targets 1710
distinct sources 6992
targets with EXACTLY 633 sources 236
236 different functions sharing an identical source count is the signature of an
unresolved-indirect-call cross product, not of a call graph. Any reading that
treats an ind_call edge as "X calls Y" is void, here and anywhere else in the
corpus it may have been used.
The control shows the other kinds are sound: sub_82286BC8 has exactly one
caller, kind call. And both EX_ screens have ZERO non-ind_call edges - they are
reached only through function pointers, which is why the direct graph is empty
for them.
What did come out: scanning the entries of all 1150 catalogued vtables in the
flat .pe, both EX_ screens are slot 1 of their own class - sub_822814D8 in
ANON_Class_271D5F25 and sub_8227A3A0 in ANON_Class_CAA8AD62 - while sub_82286BC8
is in no catalogued vtable at all. So the 2/0/0/1 partition from the previous
commit reflects a structural difference rather than a coincidence: the two
screens that select on the mode word are vtable methods of their own classes and
the one that does not select is not a vtable method.
Still not settled: what writes +184. Both the offset sweep and the call graph are
exhausted for it. A route with actual power would be the RTTI behind those two
anonymous classes, or a runtime watch on the field - not another static offset
search.
All seventeen artefacts byte-identical.
result-screens.md left this explicitly open: "the selection has not been shown".
It is shown now. Both screens that reference EX_ names carry the same
five-instruction idiom immediately before the choice - sub_822814D8 at 0x822815b4
and sub_8227A3A0 at 0x8227a408:
lwz r11, 4(rX) the owning object
lwz r11, 184(r11) a mode word at +184
addi r11, r11, -3
cntlzw r11, r11
rlwinm r11, r11, 27, 31, 31 r11 = 1 iff [+184] == 3
cmpli cr6, 0, r11, 0x0
bc 12, 4*cr6+eq, <plain> not 3 -> STAGE_RESULT / OVERVIEW
else -> EX_STAGE_RESULT / EX_OVERVIEW
cntlzw of zero is 32, and rotating left 27 lifts bit 5 into bit 31, so the
sequence is a branchless equals-3 test. The equal-to-zero branch takes the plain
name, so EX_ is chosen exactly when the word is 3.
A partition over the three key-list loaders the corpus already names:
loads +184 does not
references EX_ names 2 0
does not 0 1
sub_822814D8 and sub_8227A3A0 both load it; sub_82286BC8, which references no EX_
name, has no +184 access at all. Both off-diagonal cells are empty.
3 is the game's own EXTRA. challenge-mission-gate already established the
vocabulary for this constant at a three-way switch: == 3 is EXTRA, == 5 or 6 is
CHALLENGE, otherwise FILE. So the prefix reads literally - EX_ is the EXTRA
variant.
That REFINES the doc's tentative reading rather than confirming it. The section
proposed "EX_ = the challenge-mission debriefing" by analogy with _EXn on asset
names; the measured constant is EXTRA, which in the game's own three-way
vocabulary is a DIFFERENT kind from CHALLENGE (5/6).
Boundary of what was measured: the EXTRA = 3 constant is established at +144 of
the stage-loader class, while the word tested here is at +184 of [object + 4].
Same constant and the same EX/EXTRA naming, but two different offsets in what may
be two different objects - that the two fields are the same field is NOT shown,
only that both compare a mode word against 3.
All seventeen artefacts byte-identical.
Reading the docs first paid: unit-substructure-records already had
UN_e001_ADAN_Elan_EX4 and arsenal-item-weapon-chain already had _T_EX5_el, so
last pass's "the only two _EX5 names on the disc" was too narrow. Withdrawn and
replaced by a census.
Every name ENDING in _EX<digit> across all 41 archives, taken from parsed IDXD
record names, field names and string values (never a raw byte scan): 26 names,
two digits only - _EX4 (7 units, 6 ADAN + 1 TCAF) and _EX5 (4 TCAF units + 1
weapon), each with a UnitName_/WeaponCannonName_ twin.
The stage join is exact. _EX4 appears only in tables S27 declares
(EnumUnit_S27.tbl, UnitGroup_S27.tbl); _EX5 only in S28's (EnumUnit_S28.tbl,
UnitGroup_S28.tbl, EnumWeapon_EX5.tbl); the other 20 stages have neither. The
challenge missions are S24-S29 in order, so S27 is challenge #4 and S28 is
challenge #5: _EXn names the n-th challenge ("EXtra") mission's bespoke
variants. S28 declares both schemes at once - EnumUnit_S28.tbl by stage number,
EnumWeapon_EX5.tbl by challenge index, for the same mission.
Refuted: _EXn is not the Extra0n numbering. There are four Extra slots but _EX5
exists, and under that reading Extra04 would be S29, which has no _EX assets.
Makes available but does not settle result-screens' open "what does EX_ mean":
"the challenge-mission debriefing" fits its evidence that the EX_ twin drops
overview_rank/overview_medals. Recorded there as a reading, not adopted.
All fifteen artefacts byte-identical.
Read-before-claiming caught one of the three. savegame-format.md already
documents the shape AND this exact list -- the LOAD/SAVE screen's config key list
is compiled into the executable as a pointer array of key strings, run starting
0x820A0074. sub_82286BC8's 18 names are that list (17/18 are a tables.pak field
or record). Nothing new; residual PLAYER_AMMO_LESS_10, not a tables.pak name.
The other two are new screens of the same shape and they close exactly. All
names resolve into tables.pak: sub_822814D8 24/24, sub_8227A3A0 21/21.
sub_822814D8 = the debriefing screen. 24 = 2 screen ids (STAGE_RESULT,
EX_STAGE_RESULT) + 21 stage_* fields + one sound cue (SE_BOSS_CORE_CHARGE); the
tables.pak STAGE_RESULT record has exactly 21 fields.
sub_8227A3A0 = the pilot record / career screen. 21 = 5 screen ids (LAST_RESULT,
EX_BASE, EX_MAIN, EX_OVERVIEW, OVERVIEW) + 7 ex_overview_* + 9 overview_*; the
records EX_OVERVIEW and OVERVIEW have exactly 7 and 9 fields.
2+21+1 = 24 and 5+7+9 = 21, with 21/7/9 measured independently off the pak.
The debriefing readout is nine num/points pairs plus three points-only lines:
kills by class, objectives, clear time, shoot-down ratio and both damage
categories carry a raw count and its score contribution, while friendly_fire,
shoot_down_others and weight have no counter column -- the same partition
mission-scoring.md measures on the settings side. OVERVIEW is the seven
EX_OVERVIEW fields plus overview_rank and overview_medals.
Not settled: what EX_ means (its ids have no record of that name), and
PLAYER_AMMO_LESS_10.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE