//! RE diagnostic: emit every `weapon\Weapon_*.tbl` in a pak as machine-readable //! records, split at the sub-object boundaries the schema declares. //! //! An IDXD `.tbl` for a weapon holds `count` sub-records — a `Weapon` and its //! `Shell` — flattened into one string pool. `sylpheed-cli pak dump` shows them //! merged, which is ambiguous (both declare `ID`/`Name`). The title's schema //! name pool gives the split point: the pool contains the literal type names //! (`Weapon`, `Shell`), and each sub-record's fields follow its type name. //! //! Here we split on a type-name token appearing as a *key* position, so each //! record is emitted with its own ID and field set: //! //! ```text //! REC //! F //! ``` //! //! Run: cargo run --release -p sylpheed-formats --example idxd_tokens -- [types...] 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 == '.') } /// `Yes`/`No`-style scalar literals are values, not field names, even though /// they are identifier-shaped. fn is_enum_value(s: &str) -> bool { matches!(s, "Yes" | "No" | "YES" | "NO" | "On" | "Off" | "ON" | "OFF") } fn is_key_like(s: &str) -> bool { !s.is_empty() && s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') && s.chars().next().is_some_and(|c| c.is_ascii_alphabetic() || c == '_') && !is_number(s) && !is_enum_value(s) } fn main() { let mut args = std::env::args().skip(1); let pak_path = args.next().expect("usage: idxd_tokens [TypeName...]"); let types: Vec = { let v: Vec = args.collect(); if v.is_empty() { vec!["Weapon".into(), "Shell".into()] } else { v } }; let pak = PakArchive::open(&pak_path).expect("open pak"); for entry in pak.entries() { let Ok(bytes) = pak.read(entry) else { continue }; let Ok(obj) = IdxdObject::parse(&bytes) else { continue }; let toks = obj.tokens(); // Only entries that actually declare one of the requested sub-record // types (the pool-start heuristic can prefix one stray byte, so match a // short suffix rather than equality -- same rule as the splitter below). if !toks .iter() .any(|t| types.iter().any(|ty| t == ty || (t.ends_with(ty.as_str()) && t.len() <= ty.len() + 2))) { continue; } if std::env::var("SYLPH_RAW").is_ok() { println!("RAW {:08x}", entry.name_hash); for t in toks { println!("T {t}"); } } // Sub-record boundaries: a bare type-name token. The first one is the // schema's own declaration (`EnumWeapon`-style header lives in title // code, not here), so we take every occurrence in order. let mut bounds: Vec<(usize, &str)> = Vec::new(); for (i, t) in toks.iter().enumerate() { // The pool-start heuristic can glue one stray binary byte onto the // first token ("YWeapon"), so match a short suffix, not equality. if let Some(ty) = types .iter() .find(|ty| t == *ty || (t.ends_with(ty.as_str()) && t.len() <= ty.len() + 2)) { bounds.push((i, ty.as_str())); } } for (n, &(start, ty)) in bounds.iter().enumerate() { let end = bounds.get(n + 1).map(|b| b.0).unwrap_or(toks.len()); let slice = &toks[start..end]; // The record's own ID value is the token straight after its type // name (`Shell` -> `Shell_DSaber_P_wep_01_Beam`). The `ID`/`Name` // *keys* are interned once in the pool, so only the first record // shows them -- position-of-key lookup would miss the rest. let id = slice.get(1).map(|s| s.as_str()).unwrap_or("?"); println!("REC {:08x} {n} {ty} {id}", entry.name_hash); for i in 1..slice.len() { let (k, v) = (slice[i].as_str(), slice[i - 1].as_str()); if is_key_like(k) && (!is_key_like(v) || is_number(v)) { println!("F {k} {v}"); } } } } }