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.
18 KiB
✅ The debriefing and pilot-record screens — STAGE_RESULT, OVERVIEW, EX_OVERVIEW
- Where:
tables.pak(the menu config pak), recordsSTAGE_RESULT,OVERVIEW,EX_OVERVIEW; key lists compiled into the executable atsub_822814D8andsub_8227A3A0(found by the base-solver, player-tuning-tables). - Related: mission-scoring owns the rules (
Score_*in the stage settings); this is the readout. savegame-format documents the same compiled-key-list shape for the LOAD/SAVE screen.
The shape was already known — for other screens
savegame-format establishes it: "the LOAD/SAVE screen's config key list is
compiled into the executable, as a pointer array of key strings — the same shape
as the Arsenal's (whose keys sit a few hundred bytes earlier and match the pak
record exactly)", run starting 0x820A0074. sub_82286BC8's 18-name block is
exactly that list — 17 of its 18 are a tables.pak field or record name, so it
is the documented save-screen list and nothing new. Its lone residual is
PLAYER_AMMO_LESS_10, which is not a tables.pak name at all.
✅ Two more screens, and the key lists match their records exactly
| block | names | all present in tables.pak? |
|---|---|---|
sub_822814D8 |
24 | 24 / 24 |
sub_8227A3A0 |
21 | 21 / 21 |
And the arithmetic closes against the records themselves:
sub_822814D8= the debriefing screen. 24 names = 2 screen ids (STAGE_RESULT,EX_STAGE_RESULT) + 21stage_*fields + one sound cue (SE_BOSS_CORE_CHARGE). Thetables.pakrecordSTAGE_RESULThas exactly 21 fields (2 objects, both 21).sub_8227A3A0= the pilot record / career screen. 21 names = 5 screen ids (LAST_RESULT,EX_BASE,EX_MAIN,EX_OVERVIEW,OVERVIEW) + 7ex_overview_*+ 9overview_*. The recordsEX_OVERVIEWandOVERVIEWhave exactly 7 and 9 fields (2 objects each).
2+21+1 = 24 and 5+7+9 = 21, with 21/7/9 measured independently off the pak.
The debriefing readout, in full
stage_num_shoot_down_aircrafts stage_points_shoot_down_aircrafts
stage_num_shoot_down_ships stage_points_shoot_down_ships
stage_points_shoot_down_others
stage_weight_shoot_down
stage_num_main_objectives stage_points_main_objectives
stage_num_sub_objectives stage_points_sub_objectives
stage_clear_time stage_points_clear_time
stage_shoot_down_ratio stage_points_shoot_down_ratio
stage_damages_friendly_ships stage_points_damages_friendly_ships
stage_damages_wingman stage_points_damages_wingman
stage_points_friendly_fire
stage_total_points stage_rank
🔑 Nine num/points pairs plus three points-only lines — the screen shows
a raw count and its score contribution for kills by class, objectives, time,
ratio and both damage categories; friendly_fire, shoot_down_others and
weight have no counter column. That is the same partition
mission-scoring measures on the settings side.
The career readout
overview_points, _total_play_time, _clear_stages, _retry_times,
_shoot_down_aircrafts, _shoot_down_ships, _shoot_down_weight — seven, and
OVERVIEW adds overview_rank and overview_medals. The EX_ twin has
the seven and neither of the two, which is what makes EX_ the reduced variant
rather than a different screen.
🟡 Not settled: what EX_ means (a second profile? the online/leaderboard
variant?) — but see
stage-numbering-and-player-craft:
_EXn on an asset name is measured to mean "the n-th challenge mission", which
makes "EX_ = the challenge-mission debriefing" a consistent reading of why
this twin drops overview_rank and overview_medals. Not adopted — the
selection has not been shown — EX_BASE, EX_MAIN, EX_STAGE_RESULT and EX_FONT are keys with
no record of that name, so the prefix is a screen-id convention, not a table.
And PLAYER_AMMO_LESS_10 is unexplained.
Reproduce: parse tables.pak with tools/re-capture/unitgroup.py and count
the fields of the three records; take the key lists from the sub_822814D8 /
sub_8227A3A0 / sub_82286BC8 sections of ../data/name-block-bases.txt.
✅ The EX_ selection, shown at last — a mode word tested against 3
The section above left this 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; 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 small 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 — and that is not CHALLENGE
challenge-mission-gate already established the vocabulary for this constant
at a three-way switch: == 3 → EXTRA, == 5 or 6 → CHALLENGE,
otherwise FILE. So the prefix reads literally: EX_ is the EXTRA variant.
⚠️ This refines the tentative reading above rather than confirming it. That
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). EX_ tracks
EXTRA specifically.
⚠️ The 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, same EX/EXTRA naming, 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.
🔴 Who writes +184 — blocked, by two independent instrument failures
The next question after the selector is what supplies the mode. Two routes were tried and both are measured to have no power here.
The offset route has no power. stw …, 184(rN) occurs 301 times in the
image, lwz from +184 351 times, and 115 functions touch both +144
and +184. +184 is an ordinary small offset shared by many unrelated
classes — the same shape that the corpus already recorded as failing three
times. Nothing here narrows to a writer.
❌ The xrefs ind_call edges are a cross product — do not use them as
callers. Asking 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 |
1 827 297 |
| distinct targets | 1 710 |
| distinct sources | 6 992 |
| targets with exactly 633 sources | 236 |
236 different functions sharing the 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 elsewhere in
the corpus.
⚠️ The control shows the other kinds are fine: 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: both EX_ screens are slot 1 of their own class
Scanning the 1 150 catalogued vtables' entries in the flat .pe:
| function | vtable | slot |
|---|---|---|
sub_822814D8 (debriefing) |
ANON_Class_271D5F25 |
1 |
sub_8227A3A0 (career) |
ANON_Class_CAA8AD62 |
1 |
sub_82286BC8 (save) |
in no catalogued vtable | — |
So each EX_-selecting screen is a class whose slot-1 virtual is its
key-list builder, and the third — which selects nothing — is not a vtable method
at all. That is consistent with the 2 / 0 / 0 / 1 partition above arriving from a
structural difference rather than a coincidence.
🔴 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 class
behind ANON_Class_271D5F25 / _CAA8AD62, or a runtime watch on the field —
not another static offset search.
✅ How far the ind_call damage reaches — bounded, and the corpus is clean
The refutation above is corpus-wide in principle, so the next question is which existing claims it voids. Answer: none of the ones the corpus makes.
Every caller-count claim in the corpus checks out against call edges, with
zero ind_call contamination:
| doc | claim | call |
ind_call |
|---|---|---|---|
title-crash-stl-tree |
sub_82457780 "one caller" |
1 | 0 |
isl-trigger-queue |
sub_8226EAB8 "16 callers" |
16 | 0 |
isl-coroutine-spawner |
sub_822737C8 "seven callers" |
7 | 0 |
challenge-mission-gate |
0x82175110 "22 callers" |
22 | 0 |
4 of 4 exact, and none of the four has a single ind_call edge.
And there is a structural reason, not luck. Partitioning the two target populations:
| count | |
|---|---|
targets with ind_call edges |
1 710 |
targets with call edges |
12 170 |
| both | 103 |
ind_call only |
1 607 |
call only |
12 067 |
🔑 All 1 710 ind_call targets are vtable entries — 1 710 of 1 710, zero
partials. (Against a control: only 273 of the 12 170 call targets are.) So
ind_call fires only on virtual methods, and 1 607 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, named precisely: the 103 targets carrying both
kinds. There a caller count taken without a kind filter would be inflated by
~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.
❌ The RTTI route is dead — and ✅ what the vtable scan gave instead
Last iteration proposed "the RTTI behind those two anonymous classes" as the next route with power. Measured, it does not exist.
| over all 1 150 catalogued vtables | |
|---|---|
class_name starting ANON_ |
1 150 of 1 150 |
rtti_present true |
0 |
non-empty base_classes_json |
0 |
There is not one named class, not one RTTI flag, and not one base-class edge in
the whole table. ⚠️ Withdrawn: my own "next route" from last turn was
unrunnable. The ANON_Class_XXXXXXXX labels are synthesised, not recovered
names.
✅ Re-verified, though: reading the two vtables at their true addresses
(0x820A9760, 0x820A9350) confirms sub_822814D8 and sub_8227A3A0 are each
slot 1, uniquely, and sub_82286BC8 is in no vtable.
❌ The sibling methods cannot name the class either. Slots 0, 2, 3 and 4 of both classes reference zero strings. Every naming datum in both classes sits in slot 1 — the method the corpus already had.
✅ But the shape found eight more screens
Both classes are 5-slot with an exactly-88-byte slot 0. Filtering the whole table on that shape: 83 vtables have 5 slots, 16 also have the 88-byte slot 0, 13 of those have a catalogued slot 1, and 10 of the 13 build a key list. Ranked by key count, the two known screens are at the top and the rest name themselves:
| class | slot 1 | its key vocabulary |
|---|---|---|
ANON_Class_271D5F25 |
sub_822814D8 |
EX_STAGE_RESULT / STAGE_RESULT — the debriefing |
ANON_Class_CAA8AD62 |
sub_8227A3A0 |
EX_OVERVIEW / OVERVIEW / LAST_RESULT — the career screen |
ANON_Class_5FFD2641 |
sub_82278960 |
MEDALS, ACHIEVED_MEDALS, NEW_MEDAL, DETAIL_NEW |
ANON_Class_8501FBF7 |
sub_82275F80 |
EVALUATION_LIST, eval_%02d_eval, eval_%02d_points |
ANON_Class_B09C08C7 |
sub_822B4838 |
GAMMA_RGB, GAMMA_WB |
ANON_Class_9795AB66 |
sub_8227F6E0 |
POINT_RANKING |
ANON_Class_A7CB2C37 |
sub_82282C28 |
CLEAR_TIME_RANKING |
ANON_Class_7EE7DC6F / 431702E3 / 22F09B71 |
sub_822B6F60 / sub_822B84A8 / sub_822B9B78 |
BASE_INFO only |
🔑 POINT_RANKING and CLEAR_TIME_RANKING are two separate screens — 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.
⚠️ Note eval_%02d_eval — a printf numeric wildcard, the code-side analogue
of the datasheet schema's ??? suffix wildcard.
❌ …but BASE_INFO is NOT this family's signature
The obvious next claim — "BASE_INFO marks the family" — is false, and the
control says so plainly. BASE_INFO is a single string (0x820A20EC) and
95 functions reference it: 10 inside the family, 85 outside (95 = 10 + 85).
Among the outsiders are 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 happens to catch 10 of them (and 6 non-screens). Use
BASE_INFO to enumerate screens; do not use the vtable shape.
✅ The 95 BASE_INFO functions, partitioned — and what BASE_INFO actually discriminates
Mining the population found last iteration. Resolving every one's key list (the string-xref join over the function's own body) gives 298 distinct keys and a three-way split:
| already documented in the corpus | 9 |
| new, with a readable key list | 48 |
bare — BASE_INFO and nothing else |
38 |
9 + 48 + 38 = 95. The bare 38 are the honest limit of this instrument: a screen
whose only catalogued string is BASE_INFO cannot be named from its key list.
🔑 The control turned it into a law
Only 9 of the 19 loaders the corpus documents appear among the 95. Reading which nine, against which ten, the split is not arbitrary:
references BASE_INFO (9 of 9) |
does not (10 of 10) |
|---|---|
debriefing, career, save, medals, evaluation list, gamma, POINT_RANKING, CLEAR_TIME_RANKING, hangar |
unit definitions, weapon datasheet, AIParams, stage settings, chatter, PlayerParams, ControlTweak, PG* HUD roster, material slots, leaderboard |
✅ BASE_INFO separates a screen's own config from a data-table read — 9/9 and
10/10, both off-diagonals empty. Every function in the left column reads the
configuration of a screen; every one in the right reads a table
(tables.pak, a datasheet). The leaderboard sits on the right, correctly — 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 is what last iteration's "universal screen marker" claim needed: a population it excludes.
What the 48 new screens are
Named by their most distinctive keys:
| function | keys | reading |
|---|---|---|
sub_821C4EB0 |
TITLE_SCREEN, TITLE_MENU, LOADING, LOADING2 |
the title screen |
sub_821D6D28 |
SCRIPT_ID, SCRIPT_PATH + its own error string silph::GamePart_ReadyRoom::Impl::PrepareScript |
the ready room |
sub_8219F6D0 |
LIVE_BOARD, LOCAL_BOARD, LIVE_FOCUS, LOCAL_FOCUS, DEBRIEFING, DETAIL |
the board/focus selector |
sub_821B26F8, sub_821B1448 |
NO_RECORD, RECORD_DIFFICULTY, %02d:%02d.%02d |
record display, with a time format |
sub_821BDF88, sub_821BDBB0 |
STANDARD_MENU, EXTRA_MENU, TUTORIAL_MENU |
the three menu modes |
sub_821AE188, sub_821AED00 |
SPEAKER_FONT, SPEAKER_HEIGHT, IN_DEMO, WINDOW |
the speaker/dialogue window |
sub_821B55D8 |
SUBTITLE_FONT, SUBTITLE_Y + silph::SoundManager::Impl::SetMovieM… |
movie subtitles |
sub_8229F018 |
PlayerLOD, PlayerMotion, PlayerPackage, MODEL_PATH |
the player-model viewer |
sub_8229E878, sub_822A8EF0 |
PlayerUnit, WEAPON, WEAPONS, UnitID |
loadout views |
sub_82299448, sub_822A46B8 |
WEAPON_CATEGORIES, OBJ_GALLERY, OBJ_MENU_DELTA_SABER, MAP_PYLON |
the gallery / arsenal menu |
sub_821849E8 |
CHALLENGE, NEW_ITEM, EQUIIP_LIMITATION |
an equip screen — note the game's own typo, a join key |
sub_8218AE58 |
MISSIONS, MISSION_ID, STRINGS |
mission select |
sub_82198120, sub_82198808 |
MENU_ENABLE_SKIP, MENU_DISABLE_SKIP, EX_MENU_DISABLE_SKIP |
skip gating, with its own EX_ variant |
⚠️ Two of the 27-key entries are not key lists at all. sub_82297550 and
sub_822A2F00 resolve to pure coordinate pairs — 1080,163, 734,502,
1164,403 — i.e. a layout table, not configuration. A high key count does
not mean a rich screen; read the values before believing the rank.
🟡 Not settled: the 38 bare screens, and whether the EX_ variant seen in
EX_MENU_DISABLE_SKIP / EX_LETTERS is the same EXTRA mode word as +184.