Files
Syplheed-Reborn/crates/sylpheed-formats/examples/achievements_map.rs
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

56 lines
2.1 KiB
Rust

//! RE probe: the `ACHIEVEMENTS_REQUIREMENTS` config list.
//!
//! `GamePart_Debriefing` (`0x8218CF38`..`0x82191B18`) walks this list after a
//! mission (`sub_8218F9A8`): for each entry it takes the entry's **index** `n`,
//! tests bit `n` of an awarded-mask, and if the bit is clear it evaluates the
//! entry (`0x8218FAB0`) and sets the bit when satisfied. `GamePart_ChallengeMission`
//! then gates each challenge mission on a bit of the same space via its own
//! `REQUIREMENT` key. So this list *is* the achievement/bit numbering.
//!
//! Run: cargo run --release -p sylpheed-formats --example achievements_map -- <disc-root>
use sylpheed_formats::{idxd::IdxdObject, pak::PakArchive};
fn find(h: &[u8], n: &[u8]) -> bool {
h.windows(n.len()).any(|w| w == n)
}
fn main() {
let disc = std::env::args().nth(1).unwrap_or_else(|| {
std::env::var("SYLPHEED_DISC").expect("pass disc root or set SYLPHEED_DISC")
});
let dat = format!("{disc}/dat");
let mut paks: Vec<_> = std::fs::read_dir(&dat)
.expect("dat dir")
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| p.extension().is_some_and(|x| x == "pak"))
.collect();
paks.sort();
for p in &paks {
let Ok(arc) = PakArchive::open(p) else { continue };
for (i, e) in arc.entries().iter().enumerate() {
let Ok(b) = arc.read(e) else { continue };
if !find(&b, b"ACHIEVEMENTS_REQUIREMENTS") {
continue;
}
let name = p.file_name().unwrap().to_string_lossy().to_string();
match IdxdObject::parse(&b) {
Ok(o) => {
let t = o.tokens();
println!(
"\n===== {name} entry #{i} schema {:08x} {} tokens =====",
o.schema_hash,
t.len()
);
for (j, tok) in t.iter().enumerate() {
println!(" {j:3} {tok}");
}
}
Err(err) => println!("\n===== {name} entry #{i}: not IDXD ({err}) ====="),
}
}
}
}