Drove the retail game headless (Canary + lavapipe, vgamepad) to the Ready Room Arsenal and read the Gallery-Mode DATA SHEET for every weapon the 5%-progress save reveals. Two UI->field mappings are CONFIRMED against weapons whose values ARE on disc: Ammo Capacity == LoadingCount (9/9 exact matches) Max. Lock Ons == TriggerShotCount (FALCON 9AM 12 == wep_02; BUZZARD 22 == wep_04) That makes the panel an oracle for fields the disc leaves defaulted, and it is shown for undeveloped weapons too, so no points need spending. Recovered TriggerShotCount for wep_05 and wep_60 (both 4, on-disc absent), and bracketed the defaulted Power / MaximumRange via the Range/Damage letter classes. Ruled out first: the defaults are not in hidden/DefTables.pak (no WEAPON-schema objects there) nor in the localized GP_MAIN_GAME_* duplicates. Also adds the two analysis examples that produce the shopping list of defaulted fields, the screenshot harness used to drive the menus, and the evidence PNGs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
136 lines
4.8 KiB
Rust
136 lines
4.8 KiB
Rust
//! Scratch analysis: which IDXD fields are DECLARED but left at their default on
|
|
//! disc, per schema. Those defaults live in title code, so they can only be read
|
|
//! from the running game — this prints the shopping list for that dynamic capture.
|
|
//!
|
|
//! Run: cargo run -p sylpheed-formats --example defaulted_fields -- <disc-root>
|
|
|
|
use std::collections::{BTreeMap, BTreeSet};
|
|
use sylpheed_formats::idxd::IdxdObject;
|
|
use sylpheed_formats::pak::PakArchive;
|
|
|
|
fn is_number(s: &str) -> bool {
|
|
let s = s.strip_suffix(['f', 'F']).unwrap_or(s);
|
|
let t = s.strip_prefix(['-', '+']).unwrap_or(s);
|
|
!t.is_empty() && t.chars().all(|c| c.is_ascii_digit() || c == '.')
|
|
}
|
|
|
|
/// Same shape-test the parser uses: an identifier-looking token that is not a
|
|
/// value literal is a field-name key.
|
|
fn is_key(s: &str) -> bool {
|
|
!s.is_empty()
|
|
&& s.chars()
|
|
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
|
|
&& s.chars().next().is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
|
|
&& !is_number(s)
|
|
}
|
|
|
|
fn is_value(s: &str) -> bool {
|
|
is_number(s) || !is_key(s)
|
|
}
|
|
|
|
fn main() {
|
|
let root = std::env::args()
|
|
.nth(1)
|
|
.unwrap_or_else(|| "/home/fabi/RE - Project Sylpheed/sylph_extract".into());
|
|
let paks: Vec<String> = std::env::args().skip(2).collect();
|
|
let paks = if paks.is_empty() {
|
|
vec![
|
|
"dat/GP_MAIN_GAME_E.pak".to_string(),
|
|
"dat/GP_HANGAR_ARSENAL.pak".to_string(),
|
|
]
|
|
} else {
|
|
paks
|
|
};
|
|
|
|
for pak_rel in &paks {
|
|
let path = std::path::Path::new(&root).join(pak_rel);
|
|
let Ok(arc) = PakArchive::open(&path) else {
|
|
eprintln!("-- skip {pak_rel} (open failed)");
|
|
continue;
|
|
};
|
|
println!("\n================ {pak_rel} ================");
|
|
|
|
// schema -> key -> (n_set, n_defaulted, distinct values, example owners)
|
|
type Stat = (usize, usize, BTreeSet<String>, Vec<String>);
|
|
let mut per_schema: BTreeMap<u32, (usize, BTreeMap<String, Stat>)> = BTreeMap::new();
|
|
|
|
for e in arc.entries() {
|
|
let Ok(bytes) = arc.read(e) else { continue };
|
|
let Ok(obj) = IdxdObject::parse(&bytes) else {
|
|
continue;
|
|
};
|
|
let ident = obj.identity();
|
|
let toks = obj.tokens().to_vec();
|
|
let entry = per_schema.entry(obj.schema_hash).or_default();
|
|
entry.0 += 1;
|
|
// Track which keys this object declares, and whether each is valued.
|
|
let mut seen_here: BTreeMap<String, Option<String>> = BTreeMap::new();
|
|
for (i, t) in toks.iter().enumerate() {
|
|
if !is_key(t) {
|
|
continue;
|
|
}
|
|
let prev = if i == 0 { None } else { Some(&toks[i - 1]) };
|
|
let valued = prev.map(|p| is_value(p)).unwrap_or(false);
|
|
let v = if valued { Some(toks[i - 1].clone()) } else { None };
|
|
seen_here.entry(t.clone()).or_insert(v);
|
|
}
|
|
for (k, v) in seen_here {
|
|
let s = entry.1.entry(k).or_default();
|
|
match v {
|
|
Some(val) => {
|
|
s.0 += 1;
|
|
if s.2.len() < 12 {
|
|
s.2.insert(val);
|
|
}
|
|
}
|
|
None => {
|
|
s.1 += 1;
|
|
if s.3.len() < 6 {
|
|
s.3.push(ident.clone());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (schema, (n_obj, keys)) in per_schema {
|
|
if n_obj < 2 {
|
|
continue;
|
|
}
|
|
let name = match schema {
|
|
0x0426_e81d => "PLAYER",
|
|
0x6ab4_825a => "WEAPON",
|
|
0x43fa_a517 => "UNIT",
|
|
0x3c5b_0549 => "VESSEL",
|
|
0xbd86_d41c => "CHARACTER",
|
|
0x3c9a_e32e => "STAGE",
|
|
0xb412_e6d8 => "MESSAGE",
|
|
_ => "?",
|
|
};
|
|
let defaulted: Vec<_> = keys
|
|
.iter()
|
|
.filter(|(_, s)| s.1 > 0)
|
|
.collect();
|
|
if defaulted.is_empty() {
|
|
continue;
|
|
}
|
|
println!("\n--- schema 0x{schema:08x} {name} ({n_obj} objects) ---");
|
|
println!(
|
|
"{:<30} {:>5} {:>5} {}",
|
|
"KEY", "set", "dflt", "values seen (≤12) | owners defaulting"
|
|
);
|
|
for (k, (n_set, n_def, vals, owners)) in defaulted {
|
|
let vv: Vec<&str> = vals.iter().map(|s| s.as_str()).collect();
|
|
println!(
|
|
"{:<30} {:>5} {:>5} {} | {}",
|
|
k,
|
|
n_set,
|
|
n_def,
|
|
vv.join(","),
|
|
owners.join(",")
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|