# ✅ 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](stage-numbering-and-player-craft.md): `_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`](../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`: ```asm 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, ; 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`.