feat(formats,cli): recover entry TOC paths from IDXD identity
Add hash::recover_toc_name + TOC_NAME_SCHEMES (confirmed path schemes: unit\<ID>.tbl, weapon\<ID>.tbl, message\<ID>.tbl, effect\<ID>.tbl, <name>.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 <noreply@anthropic.com>
This commit is contained in:
@@ -398,6 +398,24 @@ fn idxd_identity(obj: &IdxdObject) -> String {
|
|||||||
format!("schema {:08x}", obj.schema_hash)
|
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<String> {
|
||||||
|
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<()> {
|
fn cmd_pak_list(pak: &Path, idxd_only: bool) -> Result<()> {
|
||||||
let arc = PakArchive::open(pak).with_context(|| format!("opening {}", pak.display()))?;
|
let arc = PakArchive::open(pak).with_context(|| format!("opening {}", pak.display()))?;
|
||||||
println!(
|
println!(
|
||||||
@@ -409,6 +427,7 @@ fn cmd_pak_list(pak: &Path, idxd_only: bool) -> Result<()> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let mut shown = 0usize;
|
let mut shown = 0usize;
|
||||||
|
let mut named = 0usize;
|
||||||
for e in arc.entries() {
|
for e in arc.entries() {
|
||||||
let payload = match arc.read(e) {
|
let payload = match arc.read(e) {
|
||||||
Ok(p) => p,
|
Ok(p) => p,
|
||||||
@@ -422,23 +441,36 @@ fn cmd_pak_list(pak: &Path, idxd_only: bool) -> Result<()> {
|
|||||||
if idxd_only && !is_idxd {
|
if idxd_only && !is_idxd {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let detail = if is_idxd {
|
let (detail, name) = if is_idxd {
|
||||||
IdxdObject::parse(&payload)
|
match IdxdObject::parse(&payload) {
|
||||||
.map(|o| idxd_identity(&o))
|
Ok(o) => (idxd_identity(&o), idxd_toc_name(&o, e.name_hash)),
|
||||||
.unwrap_or_default()
|
Err(_) => (String::new(), None),
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
String::new()
|
(String::new(), None)
|
||||||
|
};
|
||||||
|
let name_col = match &name {
|
||||||
|
Some(p) => p.clone(),
|
||||||
|
None => "?".into(),
|
||||||
};
|
};
|
||||||
println!(
|
println!(
|
||||||
" {:08x} {:>9} B {:<6} {}",
|
" {:08x} {:<28} {:>9} B {:<6} {}",
|
||||||
e.name_hash,
|
e.name_hash,
|
||||||
|
name_col.green(),
|
||||||
payload.len(),
|
payload.len(),
|
||||||
label.yellow(),
|
label.yellow(),
|
||||||
detail.dimmed(),
|
detail.dimmed(),
|
||||||
);
|
);
|
||||||
|
if name.is_some() {
|
||||||
|
named += 1;
|
||||||
|
}
|
||||||
shown += 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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -73,10 +73,61 @@ pub fn name_hash(name: &str) -> u32 {
|
|||||||
name_hash_raw(&bytes)
|
name_hash_raw(&bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Confirmed TOC key path schemes, as `(prefix, suffix)` around an entry's
|
||||||
|
/// internal identity string. The key is `name_hash("<prefix><id><suffix>")`
|
||||||
|
/// (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 `<scheme>(id)` whose hash matches, or `None`.
|
||||||
|
pub fn recover_toc_name<S: AsRef<str>>(entry_hash: u32, id_candidates: &[S]) -> Option<String> {
|
||||||
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn recovers_names_via_schemes() {
|
||||||
|
// Craft entry (unit\<ID>.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]
|
#[test]
|
||||||
fn matches_known_disc_hashes() {
|
fn matches_known_disc_hashes() {
|
||||||
// Confirmed present in retail TOCs (see module docs).
|
// Confirmed present in retail TOCs (see module docs).
|
||||||
|
|||||||
Reference in New Issue
Block a user