Static-only (no emulator, no pad input). Three findings, each with its own
evidence:
- The disc holds exactly 29 StageResource records in three families --
S01-S16 story, S18-S23 tutorial (all bg=Original), S24-S29 challenge, plus
Test. That is 16 + 6 + 6 + 1, matching weapon.tbl's stage01..16 /
tutorial01..06 / challenge01..06 key set exactly. S17 does not exist.
GP_CHALLENGE.pak has 0 IDXD objects -- it is the menu screen; challenge
missions reuse GP_MAIN_GAME_E.pak's records.
- The GamePart id table is at 0x820A1630 (29 ids). Indices are confirmed by
the image's own RegisterToFactory<N, class silph::GamePart_*> text, not by
position: GP_CHALLENGE = 26, GP_TUTORIAL = 25, GP_BUNK = 10.
- The stage loader selects its config section from a mission-KIND field at
object+144: 3 -> EXTRA, 5|6 -> CHALLENGE, else FILE (two independent sites,
0x82184df0 and 0x82185ed0; two more classify {3,5,6} as one group). The
constructor sets it to 0 and every write inside the class only clears it,
and no immediate 3/5/6 store to it exists image-wide -- so the kind is
supplied by the launching GamePart, never derived from the stage number.
That last point is a mechanism (unproven) for why patching the save's stage
field to 27 kills the load: the record is a challenge stage but the kind stays
FILE. Names an untried, zero-cost discriminator -- try stage 18-23.
Also flagged, not resolved: roster_target says S10 (a STORY stage) still
fields an unharvested unit, which contradicts the "story campaign complete"
claim by one unit.
158 lines
8.1 KiB
Markdown
158 lines
8.1 KiB
Markdown
# 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<NN>` 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 -- <disc-root>
|
||
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
|
||
```
|