This repository has been archived on 2026-09-16. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Syplheed-Reborn/docs/re/structures/result-screens.md
Sylpheed RE agent 80406ce3e4 re: the RTTI route is dead - BASE_INFO enumerates 95 screens instead
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.
2026-08-28 11:41:52 +00:00

14 KiB

The debriefing and pilot-record screens — STAGE_RESULT, OVERVIEW, EX_OVERVIEW

  • Where: tables.pak (the menu config pak), records STAGE_RESULT, OVERVIEW, EX_OVERVIEW; key lists compiled into the executable at sub_822814D8 and sub_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) + 21 stage_* fields + one sound cue (SE_BOSS_CORE_CHARGE). The tables.pak record STAGE_RESULT has 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) + 7 ex_overview_* + 9 overview_*. The records EX_OVERVIEW and OVERVIEW have 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 shownEX_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: == 3EXTRA, == 5 or 6CHALLENGE, 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_RESULTthe debriefing
ANON_Class_CAA8AD62 sub_8227A3A0 EX_OVERVIEW / OVERVIEW / LAST_RESULTthe 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.