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.
This commit is contained in:
81
crates/sylpheed-formats/examples/challenge_map.rs
Normal file
81
crates/sylpheed-formats/examples/challenge_map.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
//! 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());
|
||||
}
|
||||
Reference in New Issue
Block a user