# 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 "/โ€ฆ/Project Sylpheed โ€ฆ.pe" cargo run --release -q -p sylpheed-formats --example achievements_map -- python3 xenia-rs/zq.py dis 0x8218f9a8 0x8218fa60 # the award pass ```