feat(viewer): data pack (.pak / IDXD) browser

Roadmap #3. The explorer can now open IPFB data packs — the game's ship/weapon/
mission definition tables. Selecting a `.pak` shows a master-detail browser:
entry list (hash · inner-format · identity) on the left, the selected IDXD
object's schema + explicit-field property table on the right.

Load path: a `.pak` selection reads the index plus its sibling `.pNN` segments
(ISO reader or extracted dir, probing p00.. until the first gap), assembles via
PakArchive::from_parts, and builds owned PakRow/PakDetail entirely off-thread —
so the UI holds only plain data (no borrow of a PakArchive, WASM-safe). Two
decode caps (64 MiB cumulative, 16 MiB/entry) keep a huge sound.pak from hanging;
over-budget entries show as "(not decoded)".

Wiring mirrors the texture path: new IsoLoaderMsg::PakLoaded → PendingPak staging
→ apply_pak. A shared reset (free GPU handle + clear texture/text/pak previews)
runs in both apply systems, so exactly one viewer is active per selection and the
prior GPU texture is always freed when switching modes.

Reuse over duplication: the CLI's inner_label / idxd_identity move into the
formats crate as pak::inner_format_label and IdxdObject::identity (bodies
verbatim → CLI output unchanged), now shared by CLI and GUI.

PakView registered unconditionally (wasm-safe; population native-only). ViewMode
unchanged — the central panel dispatches on the populated resource. Workspace
builds; 22 formats tests + all crate tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-10 23:17:10 +02:00
parent 2658dd20a6
commit fbd4550d62
5 changed files with 391 additions and 36 deletions

View File

@@ -395,33 +395,7 @@ fn decode_to_rgba8(tex: &sylpheed_formats::texture::X360Texture) -> Result<Vec<u
// ── pak list ────────────────────────────────────────────────────────────────
/// Best-effort human label for a decompressed entry's inner format.
fn inner_label(payload: &[u8]) -> String {
if payload.len() < 4 {
return "empty".into();
}
let m = &payload[0..4];
if m == b"IDXD" {
"IDXD".into()
} else if payload.starts_with(b"<?xml") {
"xml".into()
} else if m.iter().all(|&b| b.is_ascii_graphic()) {
// Many inner formats are 4-char tags (RATC, T8aD, IXUD, LSTA, ttcf, …).
String::from_utf8_lossy(m).into_owned()
} else {
format!("{:02X}{:02X}{:02X}{:02X}", m[0], m[1], m[2], m[3])
}
}
/// For an IDXD entry, a short identity string (ID / Name / Model if present).
fn idxd_identity(obj: &IdxdObject) -> String {
for key in ["ID", "Name", "Model"] {
if let Some(v) = obj.get_raw(key) {
return format!("{key}={v}");
}
}
format!("schema {:08x}", obj.schema_hash)
}
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
@@ -468,7 +442,7 @@ fn cmd_pak_list(pak: &Path, idxd_only: bool) -> Result<()> {
}
let (detail, name) = if is_idxd {
match IdxdObject::parse(&payload) {
Ok(o) => (idxd_identity(&o), idxd_toc_name(&o, e.name_hash)),
Ok(o) => (o.identity(), idxd_toc_name(&o, e.name_hash)),
Err(_) => (String::new(), None),
}
} else {