Files
Syplheed-Reborn/docs/re/structures/achievements.md
Claude (auto-RE) 33ae20896e 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.
2026-08-13 19:32:43 +00:00

6.2 KiB
Raw Blame History

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 · examples/achievements_map.rs Evidence: 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), 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 (0x8218CF380x82191B18, bounded by the factory creator thunks either side) runs sub_8218F9A8 after a mission:

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 ACHIEVEMENT01ACHIEVEMENT24, 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 1924 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

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