[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 <noreply@anthropic.com>
This commit is contained in:
@@ -707,24 +707,6 @@ fn decode_to_rgba8(tex: &sylpheed_formats::texture::X360Texture) -> Result<Vec<u
|
||||
|
||||
use sylpheed_formats::pak::inner_format_label as inner_label;
|
||||
|
||||
/// 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<()> {
|
||||
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 {
|
||||
|
||||
@@ -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<String> {
|
||||
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
|
||||
|
||||
@@ -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<String>,
|
||||
/// Parsed IDXD detail for the property table, when the entry is an object.
|
||||
pub detail: Option<PakDetail>,
|
||||
/// Decoded presentable payload for the detail pane (subtitle / font / image
|
||||
@@ -799,6 +802,7 @@ fn build_pak_rows(index: &[u8], data: Vec<u8>) -> Result<Vec<PakRow>, 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<u8>) -> Result<Vec<PakRow>, 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<u8>) -> Result<Vec<PakRow>, 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<u8>) -> Result<Vec<PakRow>, 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<u8>) -> Result<Vec<PakRow>, String> {
|
||||
size: payload.len(),
|
||||
format,
|
||||
identity,
|
||||
name,
|
||||
detail,
|
||||
content,
|
||||
});
|
||||
@@ -852,6 +858,7 @@ fn build_pak_rows(index: &[u8], data: Vec<u8>) -> Result<Vec<PakRow>, String> {
|
||||
size: e.comp_size as usize,
|
||||
format: "(read error)".into(),
|
||||
identity: err.to_string(),
|
||||
name: None,
|
||||
detail: None,
|
||||
content: PakContent::None,
|
||||
}),
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user