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.
56 lines
2.1 KiB
Rust
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}) ====="),
|
|
}
|
|
}
|
|
}
|
|
}
|