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.
82 lines
3.3 KiB
Rust
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());
|
|
}
|