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).
This commit is contained in:
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user