From 20299c05a1034f35926f079425925ef21a8c0913 Mon Sep 17 00:00:00 2001 From: "Claude (auto-RE)" Date: Thu, 13 Aug 2026 19:54:39 +0000 Subject: [PATCH] re(challenge): the gate is CLEARED STAGES -- my achievement reading was wrong Third revision of this claim, and this one has the writer. Word A (singleton +80) has exactly ONE writer in the image. Scanning all 22 callers of the +80 struct copier (0x82175110) for one that stores to the copy's word 0 gives a single hit, 0x821C1820, inside GamePart_StageClear: if (this+1004 & 0x20000) skip ; already recorded copy local = singleton->progress ; src = obj+80 local.word0 |= 1 << (this+84) if changed: singleton->set(local) ; assign + async persist and this+84 is the STAGE NUMBER, by two independent uses: 0x821C1760 indexes a 20-byte per-stage record array with it (this + (x+7)*20, three words plus an 8-byte timestamp -- the savegame's 16x20 SHAB shape), and 0x821C1EEC/0x821C1924 pass it as the index into the config key "STAGE" (0x820A2540), the debriefing's px_deb_stage01..16 sprite list. So a challenge mission's REQUIREMENT n means "stage n has been cleared", and the <24 / >=24 split is the disc's own stage numbering: story 1-16 and tutorial 18-23 in word A, challenge 24-29 as word B bits 0-5. TimeAttack needs stage 16 -- the last story mission -- and the other five chain off challenge stages 25-29. That is what the raw numbers suggested in the first place. REFUTED: "the bit space is the game's 24 achievements". ACHIEVEMENTS_REQUIREMENTS having 24 entries and the gate splitting at 24 is a COINCIDENCE -- 24 is also the first challenge stage's id. Nothing copies the Debriefing's masks (+208/+736/+740) into +80, which is the check that should have preceded the claim. The achievement work itself stands and is kept, retitled and rescoped in structures/achievements.md: the XACH table (.pe 0x8FBCBC, 36-byte records, 1000G self-check), the 24 names/descriptions, the on-disc ACHIEVEMENTS_REQUIREMENTS list, and the XACHIEVEMENT_DETAILS/XAM read path. It just does not gate the challenge missions. Still open: word B (+1956) has no known writer -- GamePart_StageClear's unconditional 1 << x would land a challenge stage on word A bits 24-29, not word B, so clearing a challenge mission must be recorded by another path (presumably the one that also stores its Time/Points record). --- docs/re/INDEX.md | 4 +- docs/re/challenge-mission-gate.md | 113 +++++++++++++---------- docs/re/structures/achievements.md | 138 ++++++++++++----------------- 3 files changed, 128 insertions(+), 127 deletions(-) diff --git a/docs/re/INDEX.md b/docs/re/INDEX.md index da35274..177357f 100644 --- a/docs/re/INDEX.md +++ b/docs/re/INDEX.md @@ -43,5 +43,5 @@ Promote to a prose `structures/โ€ฆmd` file when a format needs behavioural notes | Function | Conf. | Reimpl. | Summary | |----------|-------|---------|---------| -| Achievement evaluation + content gating | โœ…/๐ŸŸก | [achievements](structures/achievements.md) | The XEX's `XACH` resource (`.pe` `0x8FBCBC`, 36-byte records) defines **24 achievements summing to 1000G** โ€” the retail total, which self-checks the stride and field offsets. `GamePart_Debriefing` (`0x8218CF38`โ€“`0x82191B18`) does two things: it walks the on-disc `ACHIEVEMENTS_REQUIREMENTS` list (`tables.pak` #16, entries `ACHIEVEMENT01โ€ฆ24`, in achievement-id order โ€” its `ShootDownAircrafts`/`ShootDownShips`/`ShootDownWeight`/`GetAllWeapons`/`GetAllAchievements` types line up with ids 19โ€“24 exactly as `XACH` names them), evaluating each and setting a bit; **and it enumerates `XACHIEVEMENT_DETAILS` from XAM** โ€” 36-byte records confirmed by the `0x38E38E39`+`srawi 3` divide-by-36, `dwId` at `+0`, `dwFlags & 0x00020000` (`โ€ฆ_ACHIEVED`) at `+32`, behind a waited-then-closed async handle. **So earned state comes from the console profile, not the 545-byte save** โ€” and the masks it builds are `1 << dwId`, i.e. **bit = the 1-based id**. ๐ŸŸก the join to the challenge gate's word at singleton `+80` is *not* proven (the Debriefing's masks are its own `+208`/`+736`/`+740`, and no writer of `+80`'s bitmask has been found). `GetAllAchievements`/`GetAllWeapons` are requirement **types**, not debug cheats | -| Stage-config section switch (`0x82184df0`, `0x82185ed0`) | โœ… | [challenge-mission-gate](challenge-mission-gate.md) | The stage loader picks its config section from a **mission-kind field at `object+144`**: `3` โ†’ `EXTRA`, `5`/`6` โ†’ `CHALLENGE`, anything else โ†’ `FILE`; two further sites treat `{3,5,6}` as one class. Constructed as `0` (`sub_821783D8`) and only ever *cleared* inside the class, so the kind comes from the launching GamePart, **not** from the stage number โ€” which is a mechanism (๐ŸŸก, unproven) for why patching the save's stage field to a challenge stage kills the load. Same note carries the **GamePart id table** (`0x820A1630`, 29 ids, `GP_CHALLENGE` = 26, cross-checked against the image's own `RegisterToFactory<26, โ€ฆ>` text) and the disc's **three stage families** โ€” `S01`โ€“`S16` story, `S18`โ€“`S23` tutorial, `S24`โ€“`S29` challenge, plus `Test`, matching `weapon.tbl`'s 16 + 6 + 6 key set exactly. `GP_CHALLENGE.pak` holds **0 IDXD objects** โ€” it is the menu screen; challenge missions reuse `GP_MAIN_GAME_E.pak`'s stage records | +| Achievement table + XAM read path | โœ…/๐ŸŸก | [achievements](structures/achievements.md) | The XEX's `XACH` resource (`.pe` `0x8FBCBC`, 36-byte records) defines **24 achievements summing to 1000G** โ€” the retail total, which self-checks the stride and field offsets. `GamePart_Debriefing` (`0x8218CF38`โ€“`0x82191B18`) does two things: it walks the on-disc `ACHIEVEMENTS_REQUIREMENTS` list (`tables.pak` #16, entries `ACHIEVEMENT01โ€ฆ24`, in achievement-id order โ€” its `ShootDownAircrafts`/`ShootDownShips`/`ShootDownWeight`/`GetAllWeapons`/`GetAllAchievements` types line up with ids 19โ€“24 exactly as `XACH` names them), evaluating each and setting a bit; **and it enumerates `XACHIEVEMENT_DETAILS` from XAM** โ€” 36-byte records confirmed by the `0x38E38E39`+`srawi 3` divide-by-36, `dwId` at `+0`, `dwFlags & 0x00020000` (`โ€ฆ_ACHIEVED`) at `+32`, behind a waited-then-closed async handle. **So earned state comes from the console profile, not the 545-byte save** โ€” and the masks it builds are `1 << dwId`, i.e. **bit = the 1-based id**. **The challenge missions do NOT gate on this** โ€” an earlier claim here, refuted by finding `+80`'s sole writer: it is `GamePart_StageClear` setting `1 << stage`, so that word is a *cleared-stage* mask and the shared "24" was a coincidence. `GetAllAchievements`/`GetAllWeapons` are requirement **types**, not debug cheats | +| Challenge-mission gate + stage-config switch | โœ… | [challenge-mission-gate](challenge-mission-gate.md) | **`GamePart_ChallengeMission` gates each of the six challenge missions on a CLEARED-STAGE bit**: `REQUIREMENT` absent or `"Always"` โ†’ available, else `n = atoi(v)` and it tests bit `n` of singleton `+80` (`n < 24`) or bit `n-24` of `+1956` (`n โ‰ฅ 24`) โ€” which is the disc's own stage numbering (story 1โ€“16 and tutorial 18โ€“23 below 24, challenge 24โ€“29 above). Word A's **sole writer** is `0x821C1820` in `GamePart_StageClear`, doing `1 << (this+84)` where `this+84` is the stage number (it also indexes a 20-byte per-stage record array and the debriefing `STAGE` sprite list). So `TimeAttack` needs **stage 16** โ€” the last story mission โ€” and the rest chain off challenge stages 25โ€“29. The six-mission table is on disc in `tables.pak` (schema `54a10697`). Separately, the stage loader picks its config section from a **mission-kind field at `object+144`**: `3` โ†’ `EXTRA`, `5`/`6` โ†’ `CHALLENGE`, anything else โ†’ `FILE`; two further sites treat `{3,5,6}` as one class. Constructed as `0` (`sub_821783D8`) and only ever *cleared* inside the class, so the kind comes from the launching GamePart, **not** from the stage number โ€” which is a mechanism (๐ŸŸก, unproven) for why patching the save's stage field to a challenge stage kills the load. Same note carries the **GamePart id table** (`0x820A1630`, 29 ids, `GP_CHALLENGE` = 26, cross-checked against the image's own `RegisterToFactory<26, โ€ฆ>` text) and the disc's **three stage families** โ€” `S01`โ€“`S16` story, `S18`โ€“`S23` tutorial, `S24`โ€“`S29` challenge, plus `Test`, matching `weapon.tbl`'s 16 + 6 + 6 key set exactly. `GP_CHALLENGE.pak` holds **0 IDXD objects** โ€” it is the menu screen; challenge missions reuse `GP_MAIN_GAME_E.pak`'s stage records | diff --git a/docs/re/challenge-mission-gate.md b/docs/re/challenge-mission-gate.md index d74457f..cfa6f7d 100644 --- a/docs/re/challenge-mission-gate.md +++ b/docs/re/challenge-mission-gate.md @@ -1,9 +1,9 @@ # 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) and for the **unlock mechanism** (a bit test against the achievement mask); -๐ŸŸก for the crash mechanism and for which mission consumes which bit; โ” for where the -mask persists. +switch) and for the **unlock mechanism** (a bit test against a *cleared-stage* mask, +whose sole writer is `GamePart_StageClear`); ๐ŸŸก for the crash mechanism, for word B's +writer, and for which mission consumes which bit. **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), @@ -162,50 +162,65 @@ offsets **`+80`** and **`+1956`**. So challenge availability is one bit in a progress bitfield, and `REQUIREMENT` is that bit's index โ€” **not** a stage number, a score, or a difficulty. -### 5.2 The bit space is the game's 24 ACHIEVEMENTS โœ… +### 5.2 The bit space is CLEARED STAGES โœ… -The `< 24` / `>= 24` split is not arbitrary. `GamePart_Debriefing` -(`0x8218CF38`โ€“`0x82191B18`) walks a disc config list called -**`ACHIEVEMENTS_REQUIREMENTS`** (`tables.pak` entry #16, schema `744c0519`) and, for -entry index `n`, tests and sets **bit `n`** of an awarded-mask โ€” and that list is -exactly `ACHIEVEMENT01` โ€ฆ `ACHIEVEMENT24`, **24 entries**. The XEX's own `XACH` -resource holds the matching 24 achievement definitions, summing to **1000G**, the -retail total. Full table and record layout: -[`structures/achievements.md`](structures/achievements.md). +Word A has exactly **one** writer in the image, and finding it settles the question. +Scanning all 22 callers of the `+80` struct copier (`0x82175110`) for one that stores +to the copy's **word 0** gives a single hit, `0x821C1820`, inside +**`GamePart_StageClear`** (`0x821C09D8`โ€“`0x821C29F0`): -So **word A (`+80`) reads as the earned-achievement mask** and **word B (`+1956`) as a -second, different flag space** that requirement values `โ‰ฅ 24` index as `bit n-24`. +```asm +if (this+1004 & 0x20000) skip ; already recorded +this+80 = 1 +copy local = singleton->progress ; bl 0x82175110, src = obj+80 +x = this+84 +local.word0 |= 1 << x ; slw r10, r26, r10 +if changed: singleton->set(local) ; bl 0x8216FF70 (assign + async persist) +``` -โš ๏ธ **Corrected the same day:** an earlier draft said "bit `n` = achievement `n+1`" and -called the join โœ…. Both were over-claimed. The Debriefing's masks live on the -*Debriefing* object (`+208`/`+736`/`+740`), nothing observed copies them into the -singleton's `+80`, and the one place the image is *seen* turning an achievement into a -bit does `1 << dwId` with **1-based** ids. So the mapping is ๐ŸŸก and, if it holds, is -`REQUIREMENT n` = achievement **`n`** โ€” see -[achievements ยง3.1 and ยง4](structures/achievements.md). +**`this+84` is the stage number**, and two independent uses prove it: -### 5.3 What the six requirement values are ๐ŸŸก +- `0x821C1760` indexes a **20-byte record array** with it โ€” `this + (x+7)*20`, whose + records are written as three words plus an 8-byte timestamp, i.e. the shape of the + savegame's 16ร—20-byte `SHAB` table; +- `0x821C1EEC` and `0x821C1924` pass it as the **index into the config key `STAGE`** + (`0x820A2540`) โ€” the debriefing config's `px_deb_stage01โ€ฆ16` sprite list. -The record's numeric tokens are `16`, `25`, `26`, `27`, `29`, and `24`/`28` are -already in the pool earlier (they double as font metrics), so they would be **deduped -away** if used. **IDXD dedupes the string pool, so positional key/value pairing is not -sound here** (the same trap the movie/subtitle map hit) โ€” with one exception: `16` sits -*immediately* before `REQUIREMENT` (tokens 96 โ†’ 97), which is the documented -value-before-key adjacency, so the **first** mission (`TimeAttack`) requiring **bit 16** -is well-supported. +So **word A is a "stage cleared" bitmask**, and a challenge mission's `REQUIREMENT n` +means **"stage `n` has been cleared"**. The `< 24` / `>= 24` split then lines up with +the disc's own stage numbering from ยง1: story `1`โ€“`16` and tutorial `18`โ€“`23` sit in +word A, and the challenge stages `24`โ€“`29` are exactly word B's bits `0`โ€“`5`. -Under the 1-based reading that ยง5.2 now prefers, bit 16 is achievement **16, -"Night Ravens Patch"** โ€” *"for challenging and eradicating the Night Raven -Squadron"*, a stage-15-era award. (The withdrawn 0-based reading gave achievement 17, -"Solar System Defense Award"; both are plausible gates for a first challenge mission, -which is precisely why the numbering has to be settled by evidence rather than by -which story reads better.) +> โš ๏ธ **Third revision of this claim โ€” the first two were wrong, and how they went +> wrong is worth keeping.** An earlier pass read the split at 24 as "the game's 24 +> achievements" because `ACHIEVEMENTS_REQUIREMENTS` has exactly 24 entries. That is a +> **coincidence**: the achievement count and the first challenge stage id are both 24 +> for unrelated reasons. The achievement work itself stands โ€” the `XACH` table, the +> 1000G self-check, and the `XACHIEVEMENT_DETAILS`/XAM enumeration are all solid, and +> are documented in [structures/achievements.md](structures/achievements.md) โ€” it just +> **does not gate the challenge missions**. The lesson: a numeric coincidence is not a +> join; find the writer. -The remaining five values (`25`โ€“`29`, all `โ‰ฅ 24`) therefore index **word B**, not -achievements โ€” most plausibly a challenge-clear chain, since there are six challenge -missions and word B's bits `0`โ€“`5` would be `24`โ€“`29`. Recorded as a hypothesis; -resolving the per-mission pairing needs the record's binary index section, not the -pool. +### 5.3 What the six requirement values are โœ…/๐ŸŸก + +The record's numeric tokens are `16`, `25`, `26`, `27`, `29`, with `24`/`28` already +in the pool earlier (they double as font metrics) and so **deduped away** if used. +IDXD dedup makes positional key/value pairing unsound in general โ€” but `16` sits +*immediately* before `REQUIREMENT` (tokens 96 โ†’ 97), the documented value-before-key +adjacency, so the first mission's value is well-supported. + +**`TimeAttack` requires stage 16 cleared** โ€” the final story mission. That is the +natural gate, and it is what the *first* reading of these numbers suggested before the +achievement detour talked me out of it. + +The other five values are `โ‰ฅ 24`, i.e. **challenge stages 24โ€“29**: the challenge +missions chain off each other. ๐ŸŸก on the exact pairing (which mission needs which), +which needs the record's binary index section rather than the string pool. + +๐ŸŸก **Word B has no known writer.** `GamePart_StageClear` sets `1 << x` into word A +unconditionally, which for a challenge stage (`x โ‰ฅ 24`) would land on word A bits +24โ€“29, *not* word B โ€” so clearing a challenge mission must be recorded by a different +path, presumably one that also stores its `Time`/`Points` record. Not yet found. **Negative worth keeping:** the requirement *text* (`TimeAttackRequirement`, `Extra01Requirement`, โ€ฆ) is **not** in `GP_CHALLENGE.pak` โ€” building a `TextIndex` @@ -215,12 +230,20 @@ keys resolve through a naming scheme the current loader does not reproduce. Read them would say in plain English what each mission asks for โ€” worth one more attempt via `hash::TOC_NAME_SCHEMES`. -### 5.4 Why this matters operationally โ€” and one lead already refuted +### 5.4 Where the mask lives, and one lead refuted -If word A / word B are restored from the save, a **hand-written save with those bits -set unlocks all six challenge missions** โ€” and the savegame round-trip is already -solved. That would turn the last 42 units of the Route-B harvest into one run instead -of an unreachable menu. +Word A is **word 0 of a 232-byte progress struct at singleton `+80`** (`0x82175110` +copies it out: two words, an 8-byte pair, a 184-byte `memcpy`, then 8 words from +`+200`). `0x8216FF70` compares, assigns, and then spawns a worker (`0x82170168`) under +a lock โ€” i.e. the struct is **persisted asynchronously**. `GamePart_Debriefing` uses +the same get/modify/set on it to accumulate saturating career counters +(`+200`โ€ฆ`+224`, the last a 64-bit total), which are exactly the quantities the +achievement requirements test. + +So a **hand-written progress record with the stage bits set would unlock the challenge +missions**, and the last 42 units of the Route-B harvest become one run instead of an +unreachable menu โ€” *if* this struct is what the savegame carries. That is the open +question. **The obvious lead is dead.** The three `stw`s to `+1956` in `0x822AF278` / `sub_822C8748` looked promising because they sit in the save serializer's code region diff --git a/docs/re/structures/achievements.md b/docs/re/structures/achievements.md index 7bd663c..b3a25a0 100644 --- a/docs/re/structures/achievements.md +++ b/docs/re/structures/achievements.md @@ -1,15 +1,23 @@ -# Achievements โ€” the 24-bit progress space the game gates content on +# Achievements โ€” the 24-entry table, and where the earned state comes from -**Conf.:** โœ… for the table, the award path and the bit numbering; ๐ŸŸก for which -challenge mission consumes which bit; โ” for where the earned bits persist. +**Conf.:** โœ… for the table and for the XAM read path; ๐ŸŸก for the requirement list's +bit numbering. **Spec:** [`tools/xach_dump.py`](../../../tools/xach_dump.py) ยท [`examples/achievements_map.rs`](../../../crates/sylpheed-formats/examples/achievements_map.rs) **Evidence:** [`captures/achievements-xach.txt`](../captures/achievements-xach.txt) -This is not just Xbox metadata. The title keeps its own **earned-achievement -bitmask** and gates content on it โ€” the challenge missions read it directly (see -[challenge-mission-gate](../challenge-mission-gate.md)), so the achievement ids -*are* the bit numbers other systems reference. +> **Retracted here, and it matters:** earlier drafts of this file claimed the +> challenge missions gate on this achievement mask. **They do not.** That gate reads a +> *cleared-stage* bitmask whose sole writer is `GamePart_StageClear` โ€” see +> [challenge-mission-gate ยง5.2](../challenge-mission-gate.md). The link was invented +> out of a numeric coincidence: `ACHIEVEMENTS_REQUIREMENTS` has 24 entries and the +> challenge gate splits its two words at 24, for entirely unrelated reasons (24 is +> also the first challenge stage's id). Everything below is what survived checking the +> writer. + +The title reads its earned-achievement state from the console, and separately carries +a disc table of what each achievement requires. Both are useful to the +reimplementation on their own terms. ## 1. The table: 24 achievements, 1000G โœ… @@ -45,40 +53,44 @@ Strings come from one `XSTR` section per language (7 present); each is Ids run `1โ€ฆ24` with no gaps. -> **Correction (same day).** This file first said "bit `n` โ†” achievement id `n+1`", +> **Correction 1 of 2 (same day).** This file first said "bit `n` โ†” achievement id `n+1`", > i.e. 0-based bits. That was wrong-headed: the only place the image is *observed* > turning an achievement into a bit does `1 << dwId` with `dwId` 1-based (ยง3.1), so > the bit index is the **id itself** and bit 0 is unused. The 0-based reading came > from assuming the config list's position was the bit, which ยง3 no longer supports. -## 2. What each bit is โœ… +## 2. The 24 achievements โœ… -| bit | id | G | name | -|---|---|---|---| -| 0 | 1 | 20 | Space Combat Award | -| 1 | 2 | 20 | Schlos Base Defense Award | -| 2 | 3 | 20 | Aegis of the People Medal | -| 3 | 4 | 20 | TCAF Luna Medal | -| 4 | 5 | 40 | TCAF Mars Medal | -| 5 | 6 | 50 | Soldier's Charm Amulet | -| 6 | 7 | 20 | White Griffons Patch | -| 7 | 8 | 30 | TCAF Jupiter Medal | -| 8 | 9 | 40 | Furious Pursuit Badge | -| 9 | 10 | 30 | Solo Aerospace Combat Award | -| 10 | 11 | 30 | Operation Nebula Blaze Award | -| 11 | 12 | 40 | Guilty Roses Patch | -| 12 | 13 | 30 | Super Battleship Slayer Patch | -| 13 | 14 | 40 | TCAF Terra Medal | -| 14 | 15 | 40 | Hellfires Patch | -| 15 | 16 | 50 | Night Ravens Patch | -| **16** | **17** | 40 | **Solar System Defense Award** โ€” "great achievements during the campaign to defend the Solar System" | -| 17 | 18 | 40 | Special Operations Medal | -| 18 | 19 | 30 | 1,000 Units Destroyed Medal | -| 19 | 20 | 70 | 10,000 Units Destroyed Medal | -| 20 | 21 | 50 | Ship Hunter Award | -| 21 | 22 | 70 | Gigaton Club Patch | -| 22 | 23 | 80 | Weapon Lord Patch | -| 23 | 24 | 100 | TCAF Pilot's Commendation | +Ids run `1โ€ฆ24` with no gaps. Where the image turns one into a bit it uses `1 << dwId` +(ยง3.1), so the id is the bit and bit 0 is unused โ€” but note that this mask is *not* +the challenge gate's (ยง4). + +| id | G | name | +|---|---|---| +| 1 | 20 | Space Combat Award | +| 2 | 20 | Schlos Base Defense Award | +| 3 | 20 | Aegis of the People Medal | +| 4 | 20 | TCAF Luna Medal | +| 5 | 40 | TCAF Mars Medal | +| 6 | 50 | Soldier's Charm Amulet | +| 7 | 20 | White Griffons Patch | +| 8 | 30 | TCAF Jupiter Medal | +| 9 | 40 | Furious Pursuit Badge | +| 10 | 30 | Solo Aerospace Combat Award | +| 11 | 30 | Operation Nebula Blaze Award | +| 12 | 40 | Guilty Roses Patch | +| 13 | 30 | Super Battleship Slayer Patch | +| 14 | 40 | TCAF Terra Medal | +| 15 | 40 | Hellfires Patch | +| 16 | 50 | Night Ravens Patch | +| 17 | 40 | Solar System Defense Award | +| 18 | 40 | Special Operations Medal | +| 19 | 30 | 1,000 Units Destroyed Medal | +| 20 | 70 | 10,000 Units Destroyed Medal | +| 21 | 50 | Ship Hunter Award | +| 22 | 70 | Gigaton Club Patch | +| 23 | 80 | Weapon Lord Patch | +| 24 | 100 | TCAF Pilot's Commendation | ## 3. The game evaluates them itself, from a disc config โœ… (numbering ๐ŸŸก) @@ -146,53 +158,19 @@ the code shifts by is that position or the id (see the โš ๏ธ above). they are requirement *types* in the achievement table. Worth stating because the strings sit next to genuinely debug-looking ones in the image. -## 4. The gate's own words โ€” related, but not yet joined up ๐ŸŸก +## 4. What this is *not*: the challenge-mission gate โœ… (refuted) -`GamePart_ChallengeMission` reads its two words off a **different** object, the -singleton at `0x821707C0` (lazily built behind the global `0x828F48B0`): +`GamePart_ChallengeMission` gates each mission on a bit of a word at singleton `+80`. +That word has exactly one writer in the image โ€” `0x821C1820` in `GamePart_StageClear` +โ€” and it sets `1 << (this+84)` where `this+84` is the **stage number** (it also +indexes a 20-byte per-stage record array and the debriefing config's `STAGE` sprite +list). So that word is a **cleared-stage** mask, not this achievement mask, and the +`< 24` / `>= 24` split is the disc's stage numbering (story 1โ€“16 and tutorial 18โ€“23 +below 24; challenge 24โ€“29 above), not a count of achievements. -- **`+80`** โ€” tested for requirement values `< 24`; -- **`+1956`** โ€” tested for values `>= 24` with bit `n-24`, so a **second, different - flag space** (the challenge screen's own five later requirements land here). - -The split at exactly 24, against a table of exactly 24 achievements, is why `+80` -reads as the achievement mask. - -โš ๏ธ **But the join is not proven.** The Debriefing's masks live on the *Debriefing* -object (`+208`, `+736`, `+740`); nothing observed copies them into the singleton's -`+80`, and no writer of the singleton's `+80` bitmask has been found at all. Until -that link exists, "challenge `REQUIREMENT` *n* = achievement *n*" is a **hypothesis**. -Which reading wins also moves the answer by one: with ยง3.1's 1-based ids, the -`TimeAttack` requirement `16` would be achievement **16, `Night Ravens Patch`**, not -17 (`Solar System Defense Award`) โ€” an earlier draft of this note asserted 17. - -Note also that `+1960` is a third bitfield on the same singleton, and a "what changed" -pass (`0x8219F3A4`) diffs it before/after and reports each newly-set bit as -**`bit + 64`** โ€” evidence of a wider flag-id space whose base for `+1956`/`+1960` is -not yet worked out. - -**Not yet known: what writes them.** No `stw` to `+1956` anywhere in the image -targets this object โ€” the ones that exist belong to a different singleton (reached -via `0x822CEB30`, an object with a `+2652` flag that stores *string pointers* at -`+1956`/`+2024`), so an earlier guess that the save serializer region wrote it is -**refuted**. `+80` is handled by address (`addi r4, obj, 80` into a copy helper at -`0x82175110`, written back through `0x8216FF70`), so it is a small struct read and -written as a value โ€” consistent with being serialised somewhere, but that is not yet -demonstrated. Note also that XEX imports are **by ordinal**, so the absence of -`XamUser*` name strings in the `.pe` is *not* evidence that the mask does not come -from the profile. - -**Ruled out on the save side:** the singleton is *not* the object carrying the -545-byte save block. Scanning all 202 call sites of the accessor with -function-boundary stops, only `+80`, `+1956`, `+1960` and `+1964` are ever touched on -it โ€” **none** of the known save offsets (`+304` block start, `+316` flight time, -`+320` clear ratio, `+336` Points, `+380` develop blob, `+440` SHAB) appear. An -earlier pass without those boundary stops appeared to find them and was a -false positive from register tracking bleeding into the next function. - -So the persistence question now has a positive answer on the other side (ยง3.1: the -title reads earned achievements from **XAM**) and the remaining work is to join the -XAM-derived mask to the gate's `+80`. +Nothing observed copies the Debriefing's achievement masks (`+208`, `+736`, `+740`) +into that word, which is the check that should have been done before the claim was +made rather than after. ## Reproduce