Files
Syplheed-Reborn/crates/sylpheed-formats/examples/challenge_map.rs
Claude (auto-RE) 10a96844ba re(challenge): the EX missions are a MODE, not a stage number
Static-only (no emulator, no pad input). Three findings, each with its own
evidence:

- The disc holds exactly 29 StageResource records in three families --
  S01-S16 story, S18-S23 tutorial (all bg=Original), S24-S29 challenge, plus
  Test. That is 16 + 6 + 6 + 1, matching weapon.tbl's stage01..16 /
  tutorial01..06 / challenge01..06 key set exactly. S17 does not exist.
  GP_CHALLENGE.pak has 0 IDXD objects -- it is the menu screen; challenge
  missions reuse GP_MAIN_GAME_E.pak's records.

- The GamePart id table is at 0x820A1630 (29 ids). Indices are confirmed by
  the image's own RegisterToFactory<N, class silph::GamePart_*> text, not by
  position: GP_CHALLENGE = 26, GP_TUTORIAL = 25, GP_BUNK = 10.

- The stage loader selects its config section from a mission-KIND field at
  object+144: 3 -> EXTRA, 5|6 -> CHALLENGE, else FILE (two independent sites,
  0x82184df0 and 0x82185ed0; two more classify {3,5,6} as one group). The
  constructor sets it to 0 and every write inside the class only clears it,
  and no immediate 3/5/6 store to it exists image-wide -- so the kind is
  supplied by the launching GamePart, never derived from the stage number.

That last point is a mechanism (unproven) for why patching the save's stage
field to 27 kills the load: the record is a challenge stage but the kind stays
FILE. Names an untried, zero-cost discriminator -- try stage 18-23.

Also flagged, not resolved: roster_target says S10 (a STORY stage) still
fields an unharvested unit, which contradicts the "story campaign complete"
claim by one unit.
2026-08-13 18:59:52 +00:00

82 lines
3.3 KiB
Rust

//! RE probe: what does the disc say about CHALLENGE / EXTRA missions?
//!
//! The title's stage-config reader (`0x82184b98`..`0x82184e94`) selects one of three
//! config sections by a mode field at `obj+144`:
//! mode == 3 -> "EXTRA"
//! mode == 5 or 6 -> "CHALLENGE"
//! otherwise -> "FILE"
//! so challenge missions are a *mode*, not a separate stage numbering. This probe
//! asks the disc which stage records exist and what GP_CHALLENGE.pak carries.
//!
//! Run: cargo run --release -p sylpheed-formats --example challenge_map -- <disc-root>
use sylpheed_formats::{idxd::IdxdObject, pak::PakArchive};
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")
});
println!("=== 1. StageResource records in GP_MAIN_GAME_E.pak ===");
let arc = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap();
let mut rows = vec![];
for e in arc.entries() {
let Ok(b) = arc.read(e) else { continue };
let Ok(o) = IdxdObject::parse(&b) else { continue };
if o.schema_hash != 0x3c9ae32e {
continue;
}
let t = o.tokens();
let stage = t
.iter()
.find_map(|s| s.strip_prefix("EnumUnit_").map(|x| x.trim_end_matches(".tbl").to_string()))
.unwrap_or("?".into());
let bg = o.get_raw("BackGroundID").unwrap_or("?").to_string();
rows.push((stage, bg, t.len()));
}
rows.sort();
println!(" {} stage records", rows.len());
for (s, bg, n) in &rows {
println!(" {s:8} bg={bg:16} tokens={n}");
}
println!("\n=== 2. GP_CHALLENGE.pak contents ===");
match PakArchive::open(format!("{disc}/dat/GP_CHALLENGE.pak")) {
Ok(ch) => {
let mut by_schema: std::collections::BTreeMap<u32, usize> = Default::default();
let mut all_tokens: Vec<(u32, Vec<String>)> = vec![];
for e in ch.entries() {
let Ok(b) = ch.read(e) else { continue };
let Ok(o) = IdxdObject::parse(&b) else { continue };
*by_schema.entry(o.schema_hash).or_default() += 1;
all_tokens.push((o.schema_hash, o.tokens().iter().map(|s| s.to_string()).collect()));
}
println!(" {} entries, {} IDXD objects", ch.entries().len(), all_tokens.len());
for (h, n) in &by_schema {
println!(" schema {h:08x} x{n}");
}
for (h, t) in all_tokens.iter().take(12) {
println!(" -- {h:08x}: {:?}", &t[..t.len().min(60)]);
}
}
Err(e) => println!(" open failed: {e}"),
}
println!("\n=== 3. tokens mentioning Challenge / EX across the main pak ===");
let mut hits: std::collections::BTreeSet<String> = Default::default();
for e in arc.entries() {
let Ok(b) = arc.read(e) else { continue };
let Ok(o) = IdxdObject::parse(&b) else { continue };
for t in o.tokens() {
let l = t.to_ascii_lowercase();
if l.contains("challenge") || t.ends_with("_EX") || t.contains("_EX4") || t.contains("_EX5") {
hits.insert(format!("{:08x} {t}", o.schema_hash));
}
}
}
for h in hits.iter().take(120) {
println!(" {h}");
}
println!(" ({} distinct)", hits.len());
}