[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;
|
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<()> {
|
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!(
|
||||||
@@ -752,7 +734,7 @@ fn cmd_pak_list(pak: &Path, idxd_only: bool) -> Result<()> {
|
|||||||
}
|
}
|
||||||
let (detail, name) = if is_idxd {
|
let (detail, name) = if is_idxd {
|
||||||
match IdxdObject::parse(&payload) {
|
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),
|
Err(_) => (String::new(), None),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -183,6 +183,27 @@ impl IdxdObject {
|
|||||||
}
|
}
|
||||||
format!("schema {:08x}", self.schema_hash)
|
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
|
/// Extract the trailing string pool. Finds the smallest offset whose suffix is
|
||||||
|
|||||||
@@ -130,6 +130,9 @@ pub struct PakRow {
|
|||||||
pub format: String,
|
pub format: String,
|
||||||
/// Short identity (`ID=…` / `Name=…`), empty for non-IDXD entries.
|
/// Short identity (`ID=…` / `Name=…`), empty for non-IDXD entries.
|
||||||
pub identity: String,
|
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.
|
/// Parsed IDXD detail for the property table, when the entry is an object.
|
||||||
pub detail: Option<PakDetail>,
|
pub detail: Option<PakDetail>,
|
||||||
/// Decoded presentable payload for the detail pane (subtitle / font / image
|
/// 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,
|
size: e.comp_size as usize,
|
||||||
format: "(not decoded)".into(),
|
format: "(not decoded)".into(),
|
||||||
identity: String::new(),
|
identity: String::new(),
|
||||||
|
name: None,
|
||||||
detail: None,
|
detail: None,
|
||||||
content: PakContent::None,
|
content: PakContent::None,
|
||||||
});
|
});
|
||||||
@@ -808,7 +812,7 @@ fn build_pak_rows(index: &[u8], data: Vec<u8>) -> Result<Vec<PakRow>, String> {
|
|||||||
Ok(payload) => {
|
Ok(payload) => {
|
||||||
decoded_budget += payload.len();
|
decoded_budget += payload.len();
|
||||||
let format = inner_format_label(&payload);
|
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) {
|
match IdxdObject::parse(&payload) {
|
||||||
Ok(obj) => {
|
Ok(obj) => {
|
||||||
let fields = obj
|
let fields = obj
|
||||||
@@ -818,6 +822,7 @@ fn build_pak_rows(index: &[u8], data: Vec<u8>) -> Result<Vec<PakRow>, String> {
|
|||||||
.collect();
|
.collect();
|
||||||
(
|
(
|
||||||
obj.identity(),
|
obj.identity(),
|
||||||
|
obj.recover_toc_path(e.name_hash),
|
||||||
Some(PakDetail {
|
Some(PakDetail {
|
||||||
schema_hash: obj.schema_hash,
|
schema_hash: obj.schema_hash,
|
||||||
count: obj.count,
|
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 {
|
} else {
|
||||||
(String::new(), None)
|
(String::new(), None, None)
|
||||||
};
|
};
|
||||||
// Presentable content for non-IDXD entries (subtitle/font/png/text).
|
// Presentable content for non-IDXD entries (subtitle/font/png/text).
|
||||||
let content = if detail.is_some() {
|
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(),
|
size: payload.len(),
|
||||||
format,
|
format,
|
||||||
identity,
|
identity,
|
||||||
|
name,
|
||||||
detail,
|
detail,
|
||||||
content,
|
content,
|
||||||
});
|
});
|
||||||
@@ -852,6 +858,7 @@ fn build_pak_rows(index: &[u8], data: Vec<u8>) -> Result<Vec<PakRow>, String> {
|
|||||||
size: e.comp_size as usize,
|
size: e.comp_size as usize,
|
||||||
format: "(read error)".into(),
|
format: "(read error)".into(),
|
||||||
identity: err.to_string(),
|
identity: err.to_string(),
|
||||||
|
name: None,
|
||||||
detail: None,
|
detail: None,
|
||||||
content: PakContent::None,
|
content: PakContent::None,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -374,7 +374,7 @@ fn draw_viewer_ui(
|
|||||||
// No file selected — show welcome / RE notes
|
// No file selected — show welcome / RE notes
|
||||||
egui::CentralPanel::default().show(ctx, |ui| {
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
ui.heading("Project Sylpheed: Arc of Deception");
|
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.separator();
|
||||||
|
|
||||||
ui.collapsing("Getting Started", |ui| {
|
ui.collapsing("Getting Started", |ui| {
|
||||||
@@ -385,37 +385,48 @@ fn draw_viewer_ui(
|
|||||||
ui.code(" xdvdfs unpack sylpheed.iso ./assets/");
|
ui.code(" xdvdfs unpack sylpheed.iso ./assets/");
|
||||||
ui.code(" File → Open extracted folder…");
|
ui.code(" File → Open extracted folder…");
|
||||||
ui.separator();
|
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.collapsing("Reverse-engineered formats", |ui| {
|
||||||
ui.label("Document your findings here as you RE the formats.");
|
let done = egui::Color32::LIGHT_GREEN;
|
||||||
ui.separator();
|
let partial = egui::Color32::from_rgb(150, 210, 255);
|
||||||
egui::Grid::new("re_notes").striped(true).show(ui, |ui| {
|
let todo = egui::Color32::YELLOW;
|
||||||
ui.strong("Extension");
|
egui::Grid::new("re_notes").striped(true).num_columns(3).show(ui, |ui| {
|
||||||
|
ui.strong("Format");
|
||||||
ui.strong("Status");
|
ui.strong("Status");
|
||||||
ui.strong("Notes");
|
ui.strong("Notes");
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
|
|
||||||
ui.label(".xpr");
|
let mut row = |ui: &mut egui::Ui, fmt: &str, c, status: &str, notes: &str| {
|
||||||
ui.colored_label(egui::Color32::LIGHT_GREEN, "✓ Partial");
|
ui.label(fmt);
|
||||||
ui.label("XPR2 container + DXT1/3/5 textures");
|
ui.colored_label(c, status);
|
||||||
ui.end_row();
|
ui.label(notes);
|
||||||
|
ui.end_row();
|
||||||
ui.label("?");
|
};
|
||||||
ui.colored_label(egui::Color32::YELLOW, "⏳ TODO");
|
row(ui, "IPFB .pak/.pNN", done, "✓ done",
|
||||||
ui.label("Mesh format — need to identify extension");
|
"Archive TOC + name-hash recovered (paths resolved)");
|
||||||
ui.end_row();
|
row(ui, "IDXD objects", partial, "◑ most",
|
||||||
|
"Reflective ship/weapon/effect defs; some fields defaulted in-code");
|
||||||
ui.label("?");
|
row(ui, "XBG7 mesh", done, "✓ done",
|
||||||
ui.colored_label(egui::Color32::YELLOW, "⏳ TODO");
|
"3D models — position + normal + UV from vertex decl");
|
||||||
ui.label("Audio format — likely XWB wave banks");
|
row(ui, "XPR2 texture", done, "✓ done",
|
||||||
ui.end_row();
|
"A8R8G8B8 / DXT1/3/5, de-tiled; 2D + cubemaps");
|
||||||
|
row(ui, "T8aD / RATC / LSTA", partial, "◑ most",
|
||||||
ui.label("?");
|
"2D UI textures, bundles, sprite lists (colours unverified)");
|
||||||
ui.colored_label(egui::Color32::YELLOW, "⏳ TODO");
|
row(ui, "IXUD subtitles", done, "✓ done",
|
||||||
ui.label("Mission/level data — format unknown");
|
"Localized movie subtitle cue tables");
|
||||||
ui.end_row();
|
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")
|
.id_salt("pak_list")
|
||||||
.show(&mut cols[0], |ui| {
|
.show(&mut cols[0], |ui| {
|
||||||
for (i, row) in rows.iter().enumerate() {
|
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} {}",
|
"{:08x} {:<7} {}",
|
||||||
row.hash,
|
row.hash, row.format, label
|
||||||
row.format,
|
|
||||||
row_kind(row)
|
|
||||||
))
|
))
|
||||||
.monospace();
|
.monospace();
|
||||||
|
if row.name.is_some() {
|
||||||
|
text = text.color(egui::Color32::LIGHT_GREEN);
|
||||||
|
}
|
||||||
if ui
|
if ui
|
||||||
.add(egui::SelectableLabel::new(*selected == Some(i), text))
|
.add(egui::SelectableLabel::new(*selected == Some(i), text))
|
||||||
.clicked()
|
.clicked()
|
||||||
@@ -536,6 +551,10 @@ fn draw_idxd_detail(ui: &mut egui::Ui, row: &crate::iso_loader::PakRow) {
|
|||||||
row.identity.as_str()
|
row.identity.as_str()
|
||||||
};
|
};
|
||||||
ui.heading(title);
|
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!("schema 0x{:08x} count {}", d.schema_hash, d.count));
|
||||||
ui.label(format!("{} bytes hash {:08x}", row.size, row.hash));
|
ui.label(format!("{} bytes hash {:08x}", row.size, row.hash));
|
||||||
ui.separator();
|
ui.separator();
|
||||||
|
|||||||
Reference in New Issue
Block a user