re(challenge): the gate's bit space is the game's 24 ACHIEVEMENTS
Static only. Last commit left "REQUIREMENT is a bit index into a progress bitfield" with the space unidentified. It is the achievement space, and both halves are now readable off the disc and the executable. - GamePart_Debriefing (0x8218CF38-0x82191B18) awards them: sub_8218F9A8 walks the on-disc ACHIEVEMENTS_REQUIREMENTS list (tables.pak #16, schema 744c0519), and for entry index n tests bit n, evaluates the entry when clear, and sets the bit when satisfied. The list is literally ACHIEVEMENT01..ACHIEVEMENT24 -- 24 entries, which is exactly where the challenge gate splits word A from word B. - The XEX carries the definitions: XACH at .pe 0x8FBCBC, 36-byte records {id, name_id, unlocked_desc_id, locked_desc_id, image_id u32, gamerscore u16, pad, flags u32, 16 zero bytes}, strings from one XSTR per language (English is table #5). tools/xach_dump.py parses it. SELF-CHECK: the 24 gamerscores sum to exactly 1000, the retail total -- a wrong stride does not land on a round 1000. - The two sources agree on ORDER independently: the requirement types ShootDownAircrafts 1000/10000, ShootDownShips 100, ShootDownWeight MegaTons, GetAllWeapons and GetAllAchievements line up with ids 19-24 exactly as XACH names them. So bit n <-> achievement n+1 is evidence, not inference. (Those last two are requirement TYPES, not debug cheats, despite how they read.) - Corollary: TimeAttack's REQUIREMENT 16 -- the one value that sits in direct value-before-key adjacency, so it survives IDXD dedup -- is bit 16 = achievement 17, "Solar System Defense Award", i.e. finish the story campaign. The other five values (25-29) are >= 24 and so index word B, a second flag space, plausibly a challenge-clear chain. Still 🟡. REFUTED, from the last commit: the stores to +1956 in 0x822AF278 / sub_822C8748 are NOT this singleton. That object comes from 0x822CEB30, checks a +2652 flag and stores string POINTERS at +1956/+2024 -- and a pointer ANDed with 1<<n is meaningless as a gate. So nothing in the image writes this singleton's +1956 field-wise, and where the mask persists (save vs Xbox profile) is open. XEX imports are by ordinal, so absent XamUser* strings are not evidence either way.
This commit is contained in:
@@ -43,4 +43,5 @@ Promote to a prose `structures/…md` file when a format needs behavioural notes
|
||||
|
||||
| Function | Conf. | Reimpl. | Summary |
|
||||
|----------|-------|---------|---------|
|
||||
| Achievement award + content gating | ✅/❔ | [achievements](structures/achievements.md) | The title keeps its **own earned-achievement bitmask** and gates content on it. The XEX's `XACH` resource (`.pe` `0x8FBCBC`, 36-byte records) defines **24 achievements summing to 1000G** — the retail total, which self-checks the stride. `GamePart_Debriefing`'s `sub_8218F9A8` walks the on-disc `ACHIEVEMENTS_REQUIREMENTS` list (`tables.pak` #16, entries `ACHIEVEMENT01…24`) and sets **bit = list index** when the requirement evaluates true, so bit `n` ↔ achievement `n+1`. The list's requirement *types* (`ShootDownAircrafts` 1000/10000, `ShootDownShips` 100, `ShootDownWeight` MegaTons, `GetAllWeapons`, `GetAllAchievements`) line up with ids 19–24 exactly as `XACH` names them — two independent sources agreeing on the order. `GetAllAchievements`/`GetAllWeapons` are requirement **types**, not debug cheats. ❔ **where the mask persists** (save vs Xbox profile) is open; the obvious "save serializer writes `+1956`" lead is **refuted** (those stores are a different singleton's string pointers) |
|
||||
| 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 |
|
||||
|
||||
76
docs/re/captures/achievements-xach.txt
Normal file
76
docs/re/captures/achievements-xach.txt
Normal file
@@ -0,0 +1,76 @@
|
||||
XACH @0x8FBCBC 24 achievements (string table #5 of 7)
|
||||
|
||||
id 1 | bit 0 | 20G | Space Combat Award
|
||||
unlocked: Received after your first space battle in the Glasner Training Area.
|
||||
locked : Awarded for participating in fighter combat in outer space.
|
||||
id 2 | bit 1 | 20G | Schlos Base Defense Award
|
||||
unlocked: Received for stopping the missile attack on Schlos Base.
|
||||
locked : Awarded for stopping enemy attacks on Schlos Base.
|
||||
id 3 | bit 2 | 20G | Aegis of the People Medal
|
||||
unlocked: Received for escorting all 7 refugee ships to safety.
|
||||
locked : Awarded for escorting the 7 refugee ships to safety.
|
||||
id 4 | bit 3 | 20G | TCAF Luna Medal
|
||||
unlocked: Received for bravely helping the fleet escape during the invasion of the Matisse System.
|
||||
locked : Awarded for bravery beyond the call of duty to escort allied vessels to safety.
|
||||
id 5 | bit 4 | 40G | TCAF Mars Medal
|
||||
unlocked: Received for bravery beyond the call of duty during the escape from the Matisse System.
|
||||
locked : Awarded for bravery beyond the call of duty under fierce enemy attacks.
|
||||
id 6 | bit 5 | 50G | Soldier's Charm Amulet
|
||||
unlocked: Given to you by Raymond as a token of his trust.
|
||||
locked : Given to you by Raymond as a token of his trust.
|
||||
id 7 | bit 6 | 20G | White Griffons Patch
|
||||
unlocked: Received by the commander and pilots of the White Griffon Squadron when it is formed.
|
||||
locked : Awarded to the commander and pilots of the White Griffon Squadron when it is formed.
|
||||
id 8 | bit 7 | 30G | TCAF Jupiter Medal
|
||||
unlocked: Received for bravery during the enemy's attack on the Alberti System.
|
||||
locked : Awarded for bravery beyond the call of duty during withdrawal of the allied fleet.
|
||||
id 9 | bit 8 | 40G | Furious Pursuit Badge
|
||||
unlocked: Received for continuing attacks on the enemy and shooting down a large number of ships.
|
||||
locked : Awarded for diligently shooting down large numbers of enemy ships.
|
||||
id 10 | bit 9 | 30G | Solo Aerospace Combat Award
|
||||
unlocked: Received for descending into Acheron's atmosphere and engaging in combat alone.
|
||||
locked : Awarded for descending into the atmosphere and engaging in combat alone.
|
||||
id 11 | bit 10 | 30G | Operation Nebula Blaze Award
|
||||
unlocked: Received for completing the extremely hazardous Operation Nebula Blaze.
|
||||
locked : Awarded for fulfilling duty and fighting bravely in this difficult operation.
|
||||
id 12 | bit 11 | 40G | Guilty Roses Patch
|
||||
unlocked: Received for repelling the Guilty Roses Squadron.
|
||||
locked : Awarded for repelling the enemy Guilty Roses Squadron.
|
||||
id 13 | bit 12 | 30G | Super Battleship Slayer Patch
|
||||
unlocked: Received for shooting down the second S battleship.
|
||||
locked : Awarded for shooting down the second S battleship supporting the enemy fleet.
|
||||
id 14 | bit 13 | 40G | TCAF Terra Medal
|
||||
unlocked: Received for shielding the fleet and seeing that all ships safely fled the Ingres System.
|
||||
locked : Awarded for shielding the fleet to allow its safe escape.
|
||||
id 15 | bit 14 | 40G | Hellfires Patch
|
||||
unlocked: Received for repelling the enemy Hellfire Squadron.
|
||||
locked : Awarded for repelling the enemy Hellfire Squadron.
|
||||
id 16 | bit 15 | 50G | Night Ravens Patch
|
||||
unlocked: Received for challenging and eradicating the Night Raven Squadron.
|
||||
locked : Awarded for challenging and eradicating the enemy Night Raven Squadron.
|
||||
id 17 | bit 16 | 40G | Solar System Defense Award
|
||||
unlocked: Received for great achievements during the campaign to defend the Solar System.
|
||||
locked : Awarded for great achievement during the campaign to defend the Solar System.
|
||||
id 18 | bit 17 | 40G | Special Operations Medal
|
||||
unlocked: Received for heroically destroying the Prometheus Driver.
|
||||
locked : Awarded for heroism in destroying the enemy's main weapon.
|
||||
id 19 | bit 18 | 30G | 1,000 Units Destroyed Medal
|
||||
unlocked: Received for shooting down 1,000 enemy fighters and attackers in combat.
|
||||
locked : Awarded for shooting down 1,000 enemy fighters and attackers in space combat.
|
||||
id 20 | bit 19 | 70G | 10,000 Units Destroyed Medal
|
||||
unlocked: Received for shooting down 10,000 enemy fighters and attackers in combat.
|
||||
locked : Awarded for shooting down 10,000 enemy fighters and attackers in space combat.
|
||||
id 21 | bit 20 | 50G | Ship Hunter Award
|
||||
unlocked: Received for shooting down 100 enemy warships in combat.
|
||||
locked : Awarded for shooting down 100 warships in space combat.
|
||||
id 22 | bit 21 | 70G | Gigaton Club Patch
|
||||
unlocked: Received for shooting down enemy vessels with a total weight of one gigaton.
|
||||
locked : Awarded for downing several enemy vessels with a combined weight of one gigaton.
|
||||
id 23 | bit 22 | 80G | Weapon Lord Patch
|
||||
unlocked: Received after you collect all usable equipment for the Delta Saber.
|
||||
locked : Awarded for collecting all usable equipment for the Delta Saber.
|
||||
id 24 | bit 23 | 100G | TCAF Pilot's Commendation
|
||||
unlocked: Commemmorates you as one of the greatest pilots in history.
|
||||
locked : Awarded to the greatest pilots in the TCAF.
|
||||
|
||||
total gamerscore = 1000 [OK: retail total]
|
||||
@@ -1,11 +1,15 @@
|
||||
# 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.
|
||||
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.
|
||||
**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`.
|
||||
[`captures/challenge-screen-config.txt`](captures/challenge-screen-config.txt),
|
||||
[`structures/achievements.md`](structures/achievements.md);
|
||||
`examples/challenge_map.rs`, `examples/challenge_screen.rs`.
|
||||
|
||||
## Why this was worth doing
|
||||
|
||||
@@ -152,20 +156,47 @@ lookup REQUIREMENT in the mission record ; bl 0x82448C50
|
||||
bit set ? AVAILABLE : LOCKED
|
||||
```
|
||||
|
||||
Word A and word B are read from a **singleton** (`0x821707C0`, lazily constructed
|
||||
behind the global at `0x828F48BC`) at offsets **`+80`** and **`+1956`**. So challenge
|
||||
Word A and word B are read from a **singleton** (`0x821707C0`; the object pointer
|
||||
lives at the global `0x828F48B0`, with a construct-once flag at `0x828F48BC`) at
|
||||
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 What the six requirement values are 🟡
|
||||
### 5.2 The bit space is the game's 24 ACHIEVEMENTS ✅
|
||||
|
||||
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).
|
||||
|
||||
So **word A (`+80`) is the earned-achievement mask** (bit `n` = achievement `n+1`),
|
||||
and **word B (`+1956`) is a second, different flag space** that requirement values
|
||||
`≥ 24` index as `bit n-24`.
|
||||
|
||||
### 5.3 What the six requirement values are 🟡
|
||||
|
||||
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. That is consistent with the six values being `24`–`29` — the same
|
||||
range as the challenge stage records — but **IDXD dedupes the string pool, so
|
||||
positional key/value pairing is not sound here** (the same trap the movie/subtitle map
|
||||
hit). Recorded as a hypothesis: reading the values properly needs the record's binary
|
||||
index section, not the pool.
|
||||
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.
|
||||
|
||||
Bit 16 is achievement **17, "Solar System Defense Award"** — *"great achievements
|
||||
during the campaign to defend the Solar System"*, i.e. **finish the story campaign**.
|
||||
That is exactly the shape of gate you would expect on the first challenge mission, and
|
||||
it is independent corroboration that the bit space is the achievement space.
|
||||
|
||||
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.
|
||||
|
||||
**Negative worth keeping:** the requirement *text* (`TimeAttackRequirement`,
|
||||
`Extra01Requirement`, …) is **not** in `GP_CHALLENGE.pak` — building a `TextIndex`
|
||||
@@ -175,14 +206,27 @@ 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.3 Why this matters operationally
|
||||
### 5.4 Why this matters operationally — and one lead already 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. Three `stw`s to `+1956` sit in `0x822C7DD0` / `0x822C8748`,
|
||||
i.e. the same code region as the save serializer (`0x822C00E8`) and deserializer
|
||||
(`0x822C0380`) — suggestive, **not yet checked**. That is the next step.
|
||||
of an unreachable menu.
|
||||
|
||||
**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
|
||||
— they are on a **different object**. That one is fetched through `0x822CEB30`, has a
|
||||
`+2652` flag the code checks first, and stores **string pointers** at `+1956`/`+2024`
|
||||
(built by `0x822D35F8`); a pointer `AND`ed with `1 << n` would be meaningless as a
|
||||
gate. So **nothing in the image stores to this singleton's `+1956` field-wise**, which
|
||||
means it is filled by a bulk copy or by a path not yet found.
|
||||
|
||||
Two candidates remain and they need different levers: the **save** (hand-write it) or
|
||||
the **Xbox profile** (the emulator's profile data). Note that XEX imports are resolved
|
||||
**by ordinal**, so the absence of `XamUser*` name strings in the `.pe` is not evidence
|
||||
against the profile route. `+80` at least is handled as a small struct by address
|
||||
(`addi r4, obj, 80` → copy helper `0x82175110`, written back via `0x8216FF70`), which
|
||||
is what a serialised value object looks like.
|
||||
|
||||
## 6. The unlock condition, from the strings ❔
|
||||
|
||||
|
||||
143
docs/re/structures/achievements.md
Normal file
143
docs/re/structures/achievements.md
Normal file
@@ -0,0 +1,143 @@
|
||||
# Achievements — the 24-bit progress space the game gates content on
|
||||
|
||||
**Conf.:** ✅ for the table, the award path and the bit numbering; 🟡 for which
|
||||
challenge mission consumes which bit; ❔ for where the earned bits persist.
|
||||
**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.
|
||||
|
||||
## 1. The table: 24 achievements, 1000G ✅
|
||||
|
||||
The XEX embeds an SPA/XDBF resource. `XACH` sits at `.pe` offset **`0x8FBCBC`**:
|
||||
|
||||
| field | type | notes |
|
||||
|---|---|---|
|
||||
| magic | `char[4]` | `XACH` |
|
||||
| version | `u32` | 1 |
|
||||
| size | `u32` | 874 |
|
||||
| count | `u16` | **24** |
|
||||
|
||||
then `count` records of **36 bytes**:
|
||||
|
||||
| off | type | field |
|
||||
|---|---|---|
|
||||
| 0 | `u16` | achievement id (1…24) |
|
||||
| 2 | `u16` | name string id |
|
||||
| 4 | `u16` | unlocked-description string id |
|
||||
| 6 | `u16` | locked-description string id |
|
||||
| 8 | `u32` | image id |
|
||||
| 12 | `u16` | gamerscore |
|
||||
| 14 | `u16` | pad (0) |
|
||||
| 16 | `u32` | flags (`0x0C` throughout) |
|
||||
| 20 | — | 16 bytes of zeroes |
|
||||
|
||||
Strings come from one `XSTR` section per language (7 present); each is
|
||||
`magic[4] "XSTR"`, `version u32`, `size u32`, `count u16`, then entries of
|
||||
`id u16, len u16, len bytes ASCII`. English is table **#5**.
|
||||
|
||||
**The stride and field offsets are self-checked**: the 24 gamerscores sum to
|
||||
**exactly 1000**, the retail total. A wrong stride does not add up to a round 1000.
|
||||
|
||||
Ids run `1…24` with no gaps, so **bit `n` ↔ achievement id `n+1`**.
|
||||
|
||||
## 2. What each bit is ✅
|
||||
|
||||
| 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 |
|
||||
|
||||
## 3. The game awards them itself, from a disc config ✅
|
||||
|
||||
`GamePart_Debriefing` (`0x8218CF38`–`0x82191B18`, bounded by the factory creator
|
||||
thunks either side) runs `sub_8218F9A8` after a mission:
|
||||
|
||||
```asm
|
||||
for each entry of the ACHIEVEMENTS_REQUIREMENTS config list, index n:
|
||||
bit = 1 << n
|
||||
if (this+208 & bit) continue ; already awarded
|
||||
if (evaluate(entry)) ; bl 0x8218FAB0
|
||||
this+208 |= bit
|
||||
```
|
||||
|
||||
so **the list index is the bit index**. The list is on disc — `tables.pak` entry
|
||||
**#16**, schema `744c0519`, the `GP_DEBRIEFING_PILOTLOG.pak+eng` config — and its
|
||||
entries are literally `ACHIEVEMENT01` … `ACHIEVEMENT24`, in order.
|
||||
|
||||
The record also carries each requirement's **type and parameters**:
|
||||
`StageClear`(`Stage`), `MissionObjective`, `Item`, `Rank` (`S`),
|
||||
`ShootDownAircrafts`(`Count` 1000 / 10000), `ShootDownShips`(`Count` 100),
|
||||
`ShootDownWeight`(`MegaTons`), `GetAllWeapons`, `GetAllAchievements`.
|
||||
|
||||
**That set independently confirms the ordering.** The last five types line up with
|
||||
ids 19–24 exactly as the XACH table names them: 1 000 units, 10 000 units, 100
|
||||
warships, one gigaton, all Delta Saber equipment, and finally the meta
|
||||
"`GetAllAchievements`" → id 24, `TCAF Pilot's Commendation`, the 100G one. Two
|
||||
independent sources agreeing on the order is what makes the bit mapping ✅ rather
|
||||
than an inference.
|
||||
|
||||
⚠️ `GetAllAchievements` and `GetAllWeapons` **look like debug cheats and are not** —
|
||||
they are requirement *types* in the achievement table. Worth stating because the
|
||||
strings sit next to genuinely debug-looking ones in the image.
|
||||
|
||||
## 4. Where the earned bits live ❔
|
||||
|
||||
`GamePart_ChallengeMission` reads them off a singleton (`0x821707C0`, lazily built
|
||||
behind the global at `0x828F48B0`):
|
||||
|
||||
- **`+80`** — tested for requirement values `< 24`, i.e. **the 24 achievements above**;
|
||||
- **`+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 matching a table of exactly 24 achievements is the reason to
|
||||
read `+80` as the achievement mask.
|
||||
|
||||
**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.
|
||||
|
||||
Settling this is worth doing: if the mask is save-backed, a hand-written save
|
||||
unlocks the challenge missions (the savegame round-trip is already solved); if it is
|
||||
profile-backed, the emulator's profile is the lever instead.
|
||||
|
||||
## Reproduce
|
||||
|
||||
```bash
|
||||
python3 tools/xach_dump.py "<disc>/…/Project Sylpheed ….pe"
|
||||
cargo run --release -q -p sylpheed-formats --example achievements_map -- <disc-root>
|
||||
python3 xenia-rs/zq.py dis 0x8218f9a8 0x8218fa60 # the award pass
|
||||
```
|
||||
Reference in New Issue
Block a user