From 390c67cb61d5b006903ee51a39d3de17ff381601 Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Mon, 13 Jul 2026 20:42:40 +0200 Subject: [PATCH] [viewer] Surface recovered TOC names in the PAK browser + refresh RE panel - Move the IDXD->TOC-path resolver into sylpheed-formats as IdxdObject::recover_toc_path() so the CLI `pak list` and the GUI pack browser share one implementation (behaviour-preserving; CLI still resolves 308/1004 on GP_MAIN_GAME_E). - PAK browser now shows each entry's recovered original path (via the name-hash) in green, in both the entry list and the IDXD detail heading. - Replace the stale welcome "RE Notes" table (mesh/audio "unknown") with an accurate reverse-engineered-formats status grid + correct authorship. Co-Authored-By: Claude Opus 4.8 --- crates/sylpheed-cli/src/main.rs | 20 +----- crates/sylpheed-formats/src/idxd.rs | 21 +++++++ crates/sylpheed-viewer/src/iso_loader.rs | 13 +++- crates/sylpheed-viewer/src/ui.rs | 79 +++++++++++++++--------- 4 files changed, 81 insertions(+), 52 deletions(-) diff --git a/crates/sylpheed-cli/src/main.rs b/crates/sylpheed-cli/src/main.rs index 62c1b3d..b20c1a8 100644 --- a/crates/sylpheed-cli/src/main.rs +++ b/crates/sylpheed-cli/src/main.rs @@ -707,24 +707,6 @@ fn decode_to_rgba8(tex: &sylpheed_formats::texture::X360Texture) -> Result 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!( @@ -752,7 +734,7 @@ fn cmd_pak_list(pak: &Path, idxd_only: bool) -> Result<()> { } let (detail, name) = if is_idxd { match IdxdObject::parse(&payload) { - Ok(o) => (o.identity(), idxd_toc_name(&o, e.name_hash)), + Ok(o) => (o.identity(), o.recover_toc_path(e.name_hash)), Err(_) => (String::new(), None), } } else { diff --git a/crates/sylpheed-formats/src/idxd.rs b/crates/sylpheed-formats/src/idxd.rs index 896ddf7..7aee93e 100644 --- a/crates/sylpheed-formats/src/idxd.rs +++ b/crates/sylpheed-formats/src/idxd.rs @@ -183,6 +183,27 @@ impl IdxdObject { } format!("schema {:08x}", self.schema_hash) } + + /// Recover this entry's original TOC path (e.g. `unit\rou_f001.tbl`) from its + /// identity/pool tokens by re-hashing candidates against `name_hash` with the + /// known path schemes. Returns `None` when no scheme reproduces the hash. + /// + /// Shared by the CLI `pak list` and the GUI pack browser so both surface the + /// same recovered names. See [`crate::hash::recover_toc_name`]. + pub fn recover_toc_path(&self, name_hash: u32) -> Option { + let mut cands: Vec<&str> = Vec::new(); + for key in ["ID", "Name", "Model"] { + if let Some(v) = self.get_raw(key) { + cands.push(v); + } + } + for t in self.tokens() { + if t.contains('_') || t.len() >= 5 { + cands.push(t.as_str()); + } + } + crate::hash::recover_toc_name(name_hash, &cands) + } } /// Extract the trailing string pool. Finds the smallest offset whose suffix is diff --git a/crates/sylpheed-viewer/src/iso_loader.rs b/crates/sylpheed-viewer/src/iso_loader.rs index b970cf8..ea78b40 100644 --- a/crates/sylpheed-viewer/src/iso_loader.rs +++ b/crates/sylpheed-viewer/src/iso_loader.rs @@ -130,6 +130,9 @@ pub struct PakRow { pub format: String, /// Short identity (`ID=…` / `Name=…`), empty for non-IDXD entries. pub identity: String, + /// Recovered original TOC path (e.g. `unit\rou_f001.tbl`) via the name-hash, + /// when a known scheme reproduces `hash`. `None` = unresolved (~70% of entries). + pub name: Option, /// Parsed IDXD detail for the property table, when the entry is an object. pub detail: Option, /// Decoded presentable payload for the detail pane (subtitle / font / image @@ -799,6 +802,7 @@ fn build_pak_rows(index: &[u8], data: Vec) -> Result, String> { size: e.comp_size as usize, format: "(not decoded)".into(), identity: String::new(), + name: None, detail: None, content: PakContent::None, }); @@ -808,7 +812,7 @@ fn build_pak_rows(index: &[u8], data: Vec) -> Result, String> { Ok(payload) => { decoded_budget += payload.len(); let format = inner_format_label(&payload); - let (identity, detail) = if IdxdObject::is_idxd(&payload) { + let (identity, name, detail) = if IdxdObject::is_idxd(&payload) { match IdxdObject::parse(&payload) { Ok(obj) => { let fields = obj @@ -818,6 +822,7 @@ fn build_pak_rows(index: &[u8], data: Vec) -> Result, String> { .collect(); ( obj.identity(), + obj.recover_toc_path(e.name_hash), Some(PakDetail { schema_hash: obj.schema_hash, count: obj.count, @@ -825,10 +830,10 @@ fn build_pak_rows(index: &[u8], data: Vec) -> Result, String> { }), ) } - Err(_) => (String::new(), None), + Err(_) => (String::new(), None, None), } } else { - (String::new(), None) + (String::new(), None, None) }; // Presentable content for non-IDXD entries (subtitle/font/png/text). let content = if detail.is_some() { @@ -842,6 +847,7 @@ fn build_pak_rows(index: &[u8], data: Vec) -> Result, String> { size: payload.len(), format, identity, + name, detail, content, }); @@ -852,6 +858,7 @@ fn build_pak_rows(index: &[u8], data: Vec) -> Result, String> { size: e.comp_size as usize, format: "(read error)".into(), identity: err.to_string(), + name: None, detail: None, content: PakContent::None, }), diff --git a/crates/sylpheed-viewer/src/ui.rs b/crates/sylpheed-viewer/src/ui.rs index 303ae56..b1b96a9 100644 --- a/crates/sylpheed-viewer/src/ui.rs +++ b/crates/sylpheed-viewer/src/ui.rs @@ -374,7 +374,7 @@ fn draw_viewer_ui( // No file selected — show welcome / RE notes egui::CentralPanel::default().show(ctx, |ui| { ui.heading("Project Sylpheed: Arc of Deception"); - ui.label("Asset Viewer — Milestone 1"); + ui.label("Asset Viewer · Game Arts / SETA · Square Enix (Xbox 360)"); ui.separator(); ui.collapsing("Getting Started", |ui| { @@ -385,37 +385,48 @@ fn draw_viewer_ui( ui.code(" xdvdfs unpack sylpheed.iso ./assets/"); ui.code(" File → Open extracted folder…"); ui.separator(); - ui.label("Click a .xpr file in the left panel to preview the texture."); + ui.label( + "Open a .pak to browse its entries, a .xpr for textures, \ + an .xbg mesh for the 3D preview, or a movie .wmv to play it.", + ); }); - ui.collapsing("Reverse Engineering Notes", |ui| { - ui.label("Document your findings here as you RE the formats."); - ui.separator(); - egui::Grid::new("re_notes").striped(true).show(ui, |ui| { - ui.strong("Extension"); + ui.collapsing("Reverse-engineered formats", |ui| { + let done = egui::Color32::LIGHT_GREEN; + let partial = egui::Color32::from_rgb(150, 210, 255); + let todo = egui::Color32::YELLOW; + egui::Grid::new("re_notes").striped(true).num_columns(3).show(ui, |ui| { + ui.strong("Format"); ui.strong("Status"); ui.strong("Notes"); ui.end_row(); - ui.label(".xpr"); - ui.colored_label(egui::Color32::LIGHT_GREEN, "✓ Partial"); - ui.label("XPR2 container + DXT1/3/5 textures"); - ui.end_row(); - - ui.label("?"); - ui.colored_label(egui::Color32::YELLOW, "⏳ TODO"); - ui.label("Mesh format — need to identify extension"); - ui.end_row(); - - ui.label("?"); - ui.colored_label(egui::Color32::YELLOW, "⏳ TODO"); - ui.label("Audio format — likely XWB wave banks"); - ui.end_row(); - - ui.label("?"); - ui.colored_label(egui::Color32::YELLOW, "⏳ TODO"); - ui.label("Mission/level data — format unknown"); - ui.end_row(); + let mut row = |ui: &mut egui::Ui, fmt: &str, c, status: &str, notes: &str| { + ui.label(fmt); + ui.colored_label(c, status); + ui.label(notes); + ui.end_row(); + }; + row(ui, "IPFB .pak/.pNN", done, "✓ done", + "Archive TOC + name-hash recovered (paths resolved)"); + row(ui, "IDXD objects", partial, "◑ most", + "Reflective ship/weapon/effect defs; some fields defaulted in-code"); + row(ui, "XBG7 mesh", done, "✓ done", + "3D models — position + normal + UV from vertex decl"); + row(ui, "XPR2 texture", done, "✓ done", + "A8R8G8B8 / DXT1/3/5, de-tiled; 2D + cubemaps"); + row(ui, "T8aD / RATC / LSTA", partial, "◑ most", + "2D UI textures, bundles, sprite lists (colours unverified)"); + row(ui, "IXUD subtitles", done, "✓ done", + "Localized movie subtitle cue tables"); + row(ui, "Font (OTF/TTF/ttcf)", done, "✓ done", + "Embedded subtitle fonts — metadata + glyph sample"); + row(ui, "WMV cutscene", done, "✓ done", + "wmv3 / wmapro playback with transport + subtitles"); + row(ui, "XISO disc", done, "✓ done", + "XDVDFS filesystem — direct ISO browsing"); + row(ui, "XMA audio", todo, "⏳ wip", + "Xbox 360 XMA → PCM (scaffold)"); }); }); }); @@ -456,13 +467,17 @@ fn draw_pak_browser(ui: &mut egui::Ui, pak: &mut PakView) { .id_salt("pak_list") .show(&mut cols[0], |ui| { for (i, row) in rows.iter().enumerate() { - let text = egui::RichText::new(format!( + // Prefer the recovered TOC path (name-hash) over the generic + // kind hint; colour resolved names green so they stand out. + let label = row.name.clone().unwrap_or_else(|| row_kind(row)); + let mut text = egui::RichText::new(format!( "{:08x} {:<7} {}", - row.hash, - row.format, - row_kind(row) + row.hash, row.format, label )) .monospace(); + if row.name.is_some() { + text = text.color(egui::Color32::LIGHT_GREEN); + } if ui .add(egui::SelectableLabel::new(*selected == Some(i), text)) .clicked() @@ -536,6 +551,10 @@ fn draw_idxd_detail(ui: &mut egui::Ui, row: &crate::iso_loader::PakRow) { row.identity.as_str() }; ui.heading(title); + // Recovered original path (name-hash) — the entry's real TOC key. + if let Some(name) = &row.name { + ui.colored_label(egui::Color32::LIGHT_GREEN, format!("📄 {name}")); + } ui.label(format!("schema 0x{:08x} count {}", d.schema_hash, d.count)); ui.label(format!("{} bytes hash {:08x}", row.size, row.hash)); ui.separator();