//! 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 -- 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 = 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, Vec); let mut per_schema: BTreeMap)> = 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> = 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(",") ); } } } }