Commit Graph

2 Commits

Author SHA1 Message Date
90e0e66c7b re(achievements): earned state comes from XAM, and my bit numbering was wrong
Two findings, one of them a correction to the previous commit.

FOUND: GamePart_Debriefing enumerates XACHIEVEMENT_DETAILS from XAM
(0x8218F888). The records are 36 bytes -- confirmed by the arithmetic, not by
eye: the count is a byte count divided by 36 through the 0x38E38E39 multiply-high
magic plus srawi 3. Field +0 is used as a shift amount, field +32 is tested for
0x00020000, and the buffer sits behind a handle that is waited on (0x824AA330
with -1) then closed (0x824AA3E0). That is the XDK struct exactly, with
XACHIEVEMENT_DETAILS_ACHIEVED == 0x20000, via the
XamUserCreateAchievementEnumerator / XEnumerate pattern.

So the title does NOT persist earned achievements itself -- it asks the console.
The lever for achievement-gated content is the emulator's PROFILE data, not the
savegame. Independently, the singleton the challenge gate reads is not the object
holding the save block: re-scanning all 202 accessor call sites WITH function-
boundary stops touches only +80/+1956/+1960/+1964 and none of the known save
offsets (+304/+316/+320/+336/+380/+440). The earlier scan that seemed to find
them had register tracking bleeding into the next function -- a false positive I
am recording rather than quietly dropping.

CORRECTED: the previous commit claimed "bit n <-> achievement n+1" and treated
the Debriefing award pass and the challenge gate as the same bit space, both as
confirmed. Neither holds up:

- the only place the image is observed turning an achievement into a bit does
  1 << dwId with 1-based ids, so bit = the id and bit 0 is unused;
- the "list index is the bit index" step assumed 0x82448338's out-parameter is
  the loop ordinal. It is the child entry's first word out of a 12-byte array,
  and whether that is an ordinal, an id or a name hash is not pinned;
- the Debriefing's masks are its own fields (+208/+736/+740). Nothing observed
  copies them into the singleton's +80, and no writer of that bitmask has been
  found at all.

The requirement-type/id agreement still stands, but it confirms the LIST ORDER,
not the bit numbering. Consequence: if the join holds, TimeAttack's REQUIREMENT
16 is achievement 16 (Night Ravens Patch), not 17 (Solar System Defense Award).
Both read plausibly as a first-challenge gate, which is exactly why it needs
evidence and not the better story.

Also noted: +1960 is a third bitfield on the same singleton, and a what-changed
pass at 0x8219F3A4 diffs it and reports each newly set bit as bit + 64 -- so
there is a wider flag-id space whose bases are not yet worked out.
2026-08-13 19:43:35 +00:00
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