From 5dd526c1de2519ffdf12ef0f5f8d2f5db06ac3c9 Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Thu, 9 Jul 2026 21:56:42 +0200 Subject: [PATCH] feat(formats,cli): recover entry TOC paths from IDXD identity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add hash::recover_toc_name + TOC_NAME_SCHEMES (confirmed path schemes: unit\.tbl, weapon\.tbl, message\.tbl, effect\.tbl, .tbl) that reproduce an entry's original backslash path from its internal identity string via the recovered name-hash. `pak list` now extracts identifier candidates from each IDXD entry (ID/Name/Model + pool tokens) and prints the resolved path when a scheme matches — e.g. unit\UN_f001_TCAF_DeltaSaber_T.tbl, weapon\Weapon_…_Missile.tbl, message\CharacterCARL.tbl — plus a name-resolved count. 308/1004 resolved on GP_MAIN_GAME_E; the remainder use deeper cross-referenced paths (deferred). Co-Authored-By: Claude Opus 4.8 --- crates/sylpheed-cli/src/main.rs | 46 ++++++++++++++++++++++---- crates/sylpheed-formats/src/hash.rs | 51 +++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 7 deletions(-) diff --git a/crates/sylpheed-cli/src/main.rs b/crates/sylpheed-cli/src/main.rs index af4ebe4..e519f00 100644 --- a/crates/sylpheed-cli/src/main.rs +++ b/crates/sylpheed-cli/src/main.rs @@ -398,6 +398,24 @@ fn idxd_identity(obj: &IdxdObject) -> String { format!("schema {:08x}", obj.schema_hash) } +/// Try to recover an IDXD entry's original TOC path from its identity tokens. +/// Uses the entry's ID/Name/Model fields plus identifier-like pool tokens as +/// candidates for [`sylpheed_formats::hash::recover_toc_name`]. +fn idxd_toc_name(obj: &IdxdObject, name_hash: u32) -> Option { + let mut cands: Vec<&str> = Vec::new(); + for key in ["ID", "Name", "Model"] { + if let Some(v) = obj.get_raw(key) { + cands.push(v); + } + } + for t in obj.tokens() { + if t.contains('_') || t.len() >= 5 { + cands.push(t.as_str()); + } + } + sylpheed_formats::hash::recover_toc_name(name_hash, &cands) +} + fn cmd_pak_list(pak: &Path, idxd_only: bool) -> Result<()> { let arc = PakArchive::open(pak).with_context(|| format!("opening {}", pak.display()))?; println!( @@ -409,6 +427,7 @@ fn cmd_pak_list(pak: &Path, idxd_only: bool) -> Result<()> { ); let mut shown = 0usize; + let mut named = 0usize; for e in arc.entries() { let payload = match arc.read(e) { Ok(p) => p, @@ -422,23 +441,36 @@ fn cmd_pak_list(pak: &Path, idxd_only: bool) -> Result<()> { if idxd_only && !is_idxd { continue; } - let detail = if is_idxd { - IdxdObject::parse(&payload) - .map(|o| idxd_identity(&o)) - .unwrap_or_default() + let (detail, name) = if is_idxd { + match IdxdObject::parse(&payload) { + Ok(o) => (idxd_identity(&o), idxd_toc_name(&o, e.name_hash)), + Err(_) => (String::new(), None), + } } else { - String::new() + (String::new(), None) + }; + let name_col = match &name { + Some(p) => p.clone(), + None => "?".into(), }; println!( - " {:08x} {:>9} B {:<6} {}", + " {:08x} {:<28} {:>9} B {:<6} {}", e.name_hash, + name_col.green(), payload.len(), label.yellow(), detail.dimmed(), ); + if name.is_some() { + named += 1; + } shown += 1; } - println!("\n {} entries shown", shown.to_string().yellow()); + println!( + "\n {} entries shown ({} name-resolved)", + shown.to_string().yellow(), + named.to_string().green(), + ); Ok(()) } diff --git a/crates/sylpheed-formats/src/hash.rs b/crates/sylpheed-formats/src/hash.rs index 3f8b4e5..5a6af50 100644 --- a/crates/sylpheed-formats/src/hash.rs +++ b/crates/sylpheed-formats/src/hash.rs @@ -73,10 +73,61 @@ pub fn name_hash(name: &str) -> u32 { name_hash_raw(&bytes) } +/// Confirmed TOC key path schemes, as `(prefix, suffix)` around an entry's +/// internal identity string. The key is `name_hash("")` +/// (backslash separators). Recovered by RE — see `xenia-rs/RE_SYMBOLS.md`. +/// +/// - `("", ".tbl")` — root manifests / DefTables resource entries (`files.tbl`). +/// - `("unit\\", ".tbl")` — craft/ship definitions (`unit\UN_f001_…_EX5.tbl`). +/// - `("weapon\\", ".tbl")` — weapon definitions (`weapon\Weapon_…_Missile.tbl`). +/// - `("message\\", ".tbl")` — character/dialog entries (`message\CharacterCARL.tbl`). +/// - `("effect\\", ".tbl")` — effect definitions. +/// +/// Not every entry type is covered yet (some use deeper, cross-referenced +/// directory paths, e.g. per-mission dialog); those return `None`. +pub const TOC_NAME_SCHEMES: &[(&str, &str)] = &[ + ("", ".tbl"), + ("unit\\", ".tbl"), + ("weapon\\", ".tbl"), + ("message\\", ".tbl"), + ("effect\\", ".tbl"), +]; + +/// Try to recover an entry's original path from its TOC `name_hash` and a set of +/// candidate identity strings (e.g. identifier-like tokens from an IDXD string +/// pool). Returns the first `(id)` whose hash matches, or `None`. +pub fn recover_toc_name>(entry_hash: u32, id_candidates: &[S]) -> Option { + for id in id_candidates { + let id = id.as_ref(); + for (prefix, suffix) in TOC_NAME_SCHEMES { + let name = format!("{prefix}{id}{suffix}"); + if name_hash(&name) == entry_hash { + return Some(name); + } + } + } + None +} + #[cfg(test)] mod tests { use super::*; + #[test] + fn recovers_names_via_schemes() { + // Craft entry (unit\.tbl) and weapon entry, verified on the real disc. + assert_eq!( + recover_toc_name(0x7C96_296C, &["UN_f001_TCAF_DeltaSaber_T_EX5"]), + Some("unit\\UN_f001_TCAF_DeltaSaber_T_EX5.tbl".to_string()) + ); + // First matching candidate wins; non-matching candidates are skipped. + assert_eq!( + recover_toc_name(0x7C96_296C, &["nope", "UN_f001_TCAF_DeltaSaber_T_EX5"]), + Some("unit\\UN_f001_TCAF_DeltaSaber_T_EX5.tbl".to_string()) + ); + assert_eq!(recover_toc_name(0x7C96_296C, &["unrelated"]), None); + } + #[test] fn matches_known_disc_hashes() { // Confirmed present in retail TOCs (see module docs).