# Challenge / EX missions — the stage set, the GamePart graph, and the kind field **Status:** ✅ for the static structure (stage set, GamePart ids, the config-section switch); 🟡 for the crash mechanism; ❔ for the unlock condition. **Method:** static only — `.pe` string/pointer analysis + DuckDB disassembly + disc records. No emulator run, no gamepad input. **Evidence:** [`captures/challenge-map.txt`](captures/challenge-map.txt), `crates/sylpheed-formats/examples/challenge_map.rs`. ## Why this was worth doing The Route-B unit harvest is complete for the **story** campaign (68 of 110 units, 7 115 defaulted-on-disc values) and stalled there: the remaining units are `*_EX4` / `*_EX5` / `*EX` variants that only the challenge missions field. The save's stage field addresses those stage records — but setting it to **27** boots to the title and then the emulator **exits during the load**, while 1–16 all load normally. This note answers *what the challenge missions are on disc* and *why the stage field alone is not enough*, so the next runtime attempt is targeted rather than another probe sweep. ## 1. The disc has exactly 29 stage records, in three families ✅ `StageResource` (schema `0x3c9ae32e`) in `dat/GP_MAIN_GAME_E.pak`: | ids | count | `BackGroundID` | reading | |---|---|---|---| | `S01`–`S16` | 16 | Lebendorf … PD (real locations) | the **story** campaign | | `S18`–`S23` | 6 | `Original` (all six) | the **tutorial** missions | | `S24`–`S29` | 6 | Anastasis, Hargenteen ×2, Planet_Lebendorf, Lebendorf, Earth | the **challenge** missions | | `Test` | 1 | Earth | developer stage | `S17` does not exist. This **matches `weapon.tbl`'s sprite-key set exactly** — `stage01…16` + `tutorial01…06` + `challenge01…06` (recorded in [static screen config](#)) — which is an independent confirmation of the three-way split: 16 + 6 + 6 + Test = 29. The `Original` six carry ~43 tokens each while the story and challenge records carry 51–59, consistent with tutorials having no squadron/route/formation tables. ## 2. `GP_CHALLENGE.pak` is a screen, not mission data ✅ 151 entries, **0 IDXD objects** — sprites and layout only. So challenge missions are *not* a separate data set: they are ordinary `StageResource` records in `GP_MAIN_GAME_E.pak`, reached through a different menu. That is why the EX unit rosters show up in the same `EnumUnit_S` tables the story stages use. ## 3. The GamePart id table — the game's whole screen graph ✅ `.rdata` pointer array at **`0x820A1630`**, 29 entries, index = GamePart id: ``` 0 GP_TITLE 10 GP_BUNK 20 GP_STAGE_CLEAR 1 GP_ADVERTISE_DEMO 11 GP_READY_ROOM 21 GP_MISSION_LOG 2 GP_SELECT_STORAGE 12 GP_HANGAR 22 GP_GAMEOVER 3 GP_LOAD 13 GP_ARSENAL 23 GP_DEBRIEFING 4 GP_SAVE 14 GP_PILOT_LOG 24 GP_DIALOG 5 GP_EXTRAS 15 GP_SYSTEM 25 GP_TUTORIAL 6 GP_MOVIE_THEATER 16 GP_DEMO *26 GP_CHALLENGE 7 GP_MISSION_SELECT 17 GP_MAIN_GAME 27 GP_LEADERBOARD 8 GP_OPTIONS 18 GP_SELECTOR 28 GP_TEST 9 GP_MOVIE 19 GP_PAUSE_MENU ``` The indices are **not inferred from position** — the image carries the factory registration text, e.g. `silph::GamePartTask::RegisterToFactory<26, class silph::GamePart_ChallengeMission>::RegisterToFactory is failed!` and `<10, … GamePart_Bunk>`, both of which agree with this table. So `GP_CHALLENGE` is GamePart **26** and `GP_TUTORIAL` is **25**. ## 4. A mission-**kind** field selects the stage config section ✅ Immediately before the array above, `0x820A1600`–`0x820A162C` holds the stage-config key list: `BASE_INFO`, `StageResource`, `Background`, `PlayerUnit`, `EQUIIP_LIMITATION` *(sic)*, `MODEL_PATH`, `NEW_ITEM`, `LOADING`, `CHALLENGE`, `EXTRA`, `FILE`. The stage loader picks **one of the last three** by a field at **`object + 144`**, and the same three-way switch appears at two independent sites (`0x82184df0` inside the all-keys config reader, and `0x82185ed0` in `sub_82185E80`): ```asm lwz r11, 144(r30) cmpi r11, 3 ; == 3 -> "EXTRA" (0x820A2160) beq extra addi r11, r11, -5 cmpli r11, 1 ; == 5 or 6 -> "CHALLENGE" (0x820A2154) bgt file ; otherwise -> "FILE" (0x820A2168) ``` Two further sites (`0x82186a60`, `0x82186cb8`) classify the same field as `{3, 5, 6}` versus everything else — i.e. **EXTRA and CHALLENGE together are "not an ordinary story load"**. The field is initialised to **0** in the constructor (`sub_821783D8`, `0x8217851c`, alongside `+148 = 0` — the stage index — and `+132 = 2`, `+136/+140 = -1`), and every write to it inside this class (`0x82184b10`, `0x82185d48`, `0x82186ab4`) only *clears* it. Nothing in the image stores an immediate 3/5/6 into it, so the kind is **supplied from outside the class** — by whichever GamePart launches the mission — not derived from the stage number. ### 4.1 Why the stage-field probe crashes 🟡 That gives a mechanism for the observed failure: patching the save's stage field to 27 selects the `S27` **record** while the kind stays **0**, so the loader reads the `FILE` section for a stage whose config lives under `CHALLENGE`. A missing section then propagates into the load, and the title exits. It fits the evidence (1–16 fine, every 24–29 the same failure, `/dev/shm` empty and disk free, so not the resource trap) but it is **not proven** — proving it needs a run. **Cheap untried discriminator, no new tooling:** patch the stage field to **18–23** (the tutorials). If those also die, the failure tracks "record outside the story range", and the kind field is the likely gate. It has never been tested — only 1–16 and 24–29 were. ## 5. The unlock condition ❔ Three strings say challenge missions are *announced*, not menu-browsed: - `AVSCRIPT_COMMAND_ATTAINMENT_CHALLENGE_MISSION_CARGO_SCORE` — a mission-script command that grants challenge-mission attainment from a **cargo score**; - `DLG_CHALLENGE_MISSION_AVAILABLE` — the "a challenge mission is now available" dialog; - `DLG_GO_CHALLENGE_MISSION_MENU` — the prompt that takes you to GamePart 26. So the gate is plausibly an in-mission achievement, not a title-menu state — which is consistent with `Game Status = GAME_CLEAR` unlocking nothing (measured, refuted). Neither dialog key is reachable by xref: they are selected **by index** through a config lookup, exactly like the save screen's sprite keys, so there is nothing to grep back to. Naming the flag needs the challenge-availability check disassembled from the screen side. ## 6. What this makes actionable `roster_target` against the harvested CSV, with the stage families now named: | stage | family | roster units not yet read | |---|---|---| | `S27` | challenge | `f003_ArrowHead_EX4`, `e107_AAFrigate_EX4`, `e010_Attacker_S_EX4`, `e011_Attacker_B_EX4`, `e007_Turret_EX4`, `e008_TurretPlus_EX4` | | `S28` | challenge | `f004_DeltaSaber_A_Player`, `f001_DeltaSaber_T_EX5(_el)`, `f003_ArrowHead_EX5`, `f104_Battleship_EX5`, `f105_Cruiser_EX5` | | `S25` | challenge | `e101_SDBattleshipEX`, `e102_BattleshipEX`, `e105_CruiserEX`, `e108_ASFrigateEX` | | `S29` | challenge | `e101_SDBattleshipEX`, `e102_BattleshipEX`, `e105_CruiserEX` | | `S24` | challenge | `e105_CruiserEX`, `e106_DestroyerEX` | | `S10` | **story** | `e005_ADAN_ElanTypeQ_Margras` | ⚠️ **The `S10` row contradicts "the story campaign is complete."** One story stage still fields a unit that has never been read. Either `S10` was never flown (it is the smallest stage container on the disc — 7 XBG7 resources — so it may be a cutscene stage that is not flyable), or the completeness claim was one unit optimistic. Flagged rather than resolved: it costs one ordinary story run to settle. Remaining coverage: 42 units missing, of which the rosters above account for ~23. The rest are not in any stage roster table and need a different lever. ## Reproduce ```bash cargo run --release -q -p sylpheed-formats --example challenge_map -- python3 xenia-rs/zq.py dis 0x82184df0 0x82184e30 # the three-way section switch python3 xenia-rs/zq.py dis 0x82185ed0 0x82185f10 # the same switch, second site ```