Commit Graph

12 Commits

Author SHA1 Message Date
MechaCat02
95de29f290 feat(formats,viewer): movie subtitles, voice decode, and the movie manifest
The movie cutscene subtitle + voice pipeline, driven by the ADVERTISE_MOVIE
manifest (the authoritative movie -> subtitle -> voice index). Also flushes
several sessions of local WIP (async viewer loading, grouped-pool XBG7/hero-ship
decode, drawlog tooling). See docs/HANDOFF-movie-voice-subtitles-2026-07-19.md.

Subtitles (movie_subtitle.rs):
- Full movie->track->text chain; join multi-line captions sharing one timing
  (fixes S13A dropped "Look at it father" line); overlap-safe active_cues();
  Latin-1 accents preserved.

Voice (slb.rs): XACT .slb -> XMA1 RIFF; take the FIRST sub-wave bounded by its
declared data size (fixes S10-S16 alternate-take garble); list_voice_clips.

Manifest (movie_manifest.rs): parse ADVERTISE_MOVIE (0x5B983A08) for the real
movie->voice binding (not always VOICE_<movie>; e.g. hokyu -> VOICE_D_* in etc\).
Resolve the token's sound.pak path via sounds.tbl. DIRECT bindings only — the
demo-id shared-clip fallback for unbound hokyu movies was verified WRONG in-game
and reverted (unbound hokyu stay unvoiced; correct join key is an OPEN problem).

Viewer: manifest-driven voice (movie player toggle + solo button), standalone
"Voice Lines" browser, stacked caption overlay.

Tests: 46 formats-lib + 11 viewer-lib + movie_manifest/movie_subtitle/slb disc
tests; full workspace green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 20:15:41 +02:00
MechaCat02
578c71a1b9 [formats,viewer] Decode grouped-pool XBG7 models; fix mesh routing + albedo
Revert the mis-guided "triangle STRIP" reading (a937779): XBG7 index
buffers are triangle LISTS (prim=4 in the GPU capture; stored-normal
agreement 1.000 as a list vs ~0.49 as a strip). Reinstate the
winding-consistency gate.

Crack the grouped-pool layout used by the hero ship and ~150 detailed
models. A resource's sub-meshes share one index pool (buffers 4-byte
aligned, in descriptor-marker order) followed by one vertex pool (each
vtx_count*stride, same order); the vertex pool is 4-byte aligned after
the index pool. The whole resource is derived from one anchored pivot:
  ib0 = vb0 - span - pad   (pad in 0..=3, alignment)
  ib[i] = align4(ib[i-1] + idx_count[i-1]*2)
  vb[i] = vb[i-1] + vtx_count[i-1]*stride
vb0 is found via the unit-normal vertex-run scan; the alignment is
confirmed by validating the LARGEST sub-mesh (most reliable), after
which the rest are read/validated. A single marker reduces this to the
existing adjacency anchor, so grouped generalises it.

Results (cross-checked against a Canary GPU draw-log capture of
DeltaSaber_T.xpr):
- DeltaSaber_T f001 = body + 7 detail parts = 8650 tris, every sub-mesh
  0-degenerate / full-coverage / winding-agreement 1.000.
- All 19 previously-declined weapon models now decode (they hit pad 2
  and/or lead with a tiny bracket that broke a markers[0] pivot). Corpus
  audit: 0/146 geometry files fail (was 19 -> flat-texture in the viewer).
- Stages unchanged (single-marker path is byte-identical; grouped falls
  back to the old anchor on failure). 7/7 disc tests green incl. the
  strict stage quality audit; new hero_ship_grouped_pool_decodes test.

Viewer: route by count_xbg7; --only matches an exact model name (so the
neutral pose renders without the mnv*/turn180 animation poses). Fix the
albedo matcher: match a sub-model to its _col map by entity stem
(e007_bdy_01 -> e007_col) instead of a full-name prefix, lifting stage
sub-model texturing from ~25% to ~95% (the rest were flat grey). Colour
correctness (channel order/sRGB) remains a separate dynamic-RE item.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 19:54:39 +02:00
MechaCat02
a937779c77 [formats] XBG7 meshes: index buffers are triangle STRIPS, not lists
The weapon/prop models (rou_fxxx_wep_nn.xpr) rendered with triangular holes in
the hull and misplaced/extra spikes. Root cause: XBG7 sub-mesh index buffers are
triangle STRIPS, but the decoder read them as triangle LISTS — a strip of N
indices is N-2 triangles, a list is only N/3, so ~2/3 of every hull was missing
(the holes) and each list-triple of strip data spanned unrelated vertices (the
spikes). The tell: triangles-per-vertex was 0.5-1.0 with 0 unreferenced vertices
(a closed surface needs ~2.0).

- mesh.rs: expand_triangle_strip() converts strip -> list with alternating
  winding, skipping degenerate triangles (repeated index = strip restart).
  Applied in both from_xpr2 (weapons) and read_pool_mesh (stage sub-models).
  All 71 weapon submeshes now have healthy ratios; hulls fill in (wep_03 cannon,
  wep_04 pod, wep_11, wep_37 verified coherent).
- Module doc updated (strip, not list).
- sylpheed-cli: `mesh info` reports per-submesh degenerate/unref/spanning-triangle
  counts; `mesh render` gains --dist (camera zoom). XMESHDBG env dumps the
  descriptor index markers.

Known residual: an index buffer may concatenate several strips with no degenerate
bridge, leaving ~2 spanning "spike" triangles at each restart (index jump to a new
vertex region) — <1% of tris on most models. Decoding the restart mechanism is a
follow-up (a blanket spanning-filter is unsafe: clean models have legit elongated
tip triangles).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 13:04:12 +02:00
MechaCat02
7143ea18fe [formats] Fully crack T8aD: decode the header's per-tile offset table
Follow-up to the 256×256-tiling reversal — the residual boundary seams on
≥2×2-tile textures are gone. Decoding the file header revealed the exact
layout (no oracle capture needed):

  0x00  44        base header
  0x1c  4         tile count = ceil(w/256) * ceil(h/256)
  0x2c  tiles*4   BE-u32 absolute offset of each row-major 256×256 tile
  <off> 16        per-tile header, then tile_w*tile_h*4 A8R8G8B8 pixels

The old seams came from ignoring the offset table and the 16-byte per-tile
header (contiguous-packing drifted 16 bytes per tile). Small textures decoded
before only by luck: one tile puts pixels at 44+4+16 = 64, the old type-1
"header size". Now the 8AX title background, the prselect_win1 window frame,
and preff04 all decode pixel-perfect (verified against the title screen).

- t8ad.rs: parse() walks the offset table; dropped detile_256 + the
  header-size-by-type table. Non-tilecount entries (field != ceil*ceil) return
  None (likely DXT/other, deferred). New multi-tile round-trip test.
- sylpheed-cli: pak textures decodes via parse(); XDUMPHDR=1 dumps the base
  header + offset table for RE. Removed the now-obsolete XTILE/XSCORE/XDESTRIP
  experiment knobs and the reconstruct_tiles/TV-sweep helpers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 11:55:25 +02:00
MechaCat02
4ba723b9a5 [formats] Crack large-T8aD tiling: 256×256 row-major raster tiles
Verified against the running game (freeze-fixed Canary title screen) that
T8aD colours are correct (no R↔B swap) and that wide textures were garbled
by an unreversed tile layout, not a colour bug.

Reversed the layout: large T8aD are stored as 256×256 raster tiles in
row-major order, each tile raster internally, edge tiles clipped to the
image (payload is exactly w*h*4, no padding). Surfaces ≤256px wide are a
single tile column, identical to plain linear — which is why small UI
textures always decoded correctly.

- t8ad.rs: add detile_256() + apply in parse(). Single tile-row / single
  tile-column textures now decode exactly (ptcopyright, ptbtn, ptlogo1 =
  clean "PROJECT" logo, previously pure noise). Known residual: ≥2×2-tile
  textures (>256 in both dims, e.g. the 8AX background) come out coherent
  but with boundary seams — the multi-tile order is a subtle swizzle, TBD.
- sylpheed-cli: new `pak textures <pak> <out>` command — decodes every
  T8aD (direct, RATC-nested, LSTA frames) to PNG for A/B against the game.
  Env debug knobs for layout RE: XTILE/XDESTRIP/XDETILE_W/XREINTERPRET_PITCH
  /XSCORE (TV-ranked tile sweep), plus --verbose size classification.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 23:37:35 +02:00
MechaCat02
2053f31d17 feat(formats,cli): decode XBG7 stage containers + fix weapon layout
Stage containers (hidden/resource3d/Stage_S*.xpr) are collections of up to
~450 enemy/prop sub-models, not single meshes. Xbg7Model::stage_models decodes
them: each resource is a [12-byte header][index buffer][vertex buffer] block
(index count = descriptor marker, vertex count = u32 32 bytes before it) whose
on-disc offset is NOT stored, so it is located by content — one O(file) pass per
stride finds vertex-buffer starts (unit NORMAL at +12 whose previous slot isn't)
and each resource is pinned to the candidate whose indices validate and produce
non-degenerate, well-connected triangles. A connectivity gate (mean triangle
edge <= 0.28x the bbox diagonal) rejects spiky mis-anchors. ~4993 sub-models
decode across the 22 stages.

The same insight fixes the weapon single-model layout: it is [12-byte header]
[index][vertex] too, not [index][12-byte gap][vertex]. Reading indices from the
block start turned the 12 header bytes into 6 junk indices (2 leading degenerate
triangles — the recurring stray-triangle artifact) and dropped the last 6 real
indices. Skipping the header leaves vertex offsets identical (coverage unchanged
at 36/166) and corrects the triangle list. Verified on wep_00/03/04.

Adds `sylpheed-cli mesh {info,render}` — a headless software rasterizer that
writes a shaded PNG (orthographic, z-buffered, two-sided), so recovered geometry
can be verified without the GUI. Stages render as a normalised thumbnail grid;
--only filters sub-models.

Tests: stage_models_{decode,sweep,quality_audit}; docs/re updated (xbg7-mesh.md
evidence log + INDEX.md).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 20:56:35 +02:00
MechaCat02
fbd4550d62 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>
2026-07-10 23:17:10 +02:00
MechaCat02
2658dd20a6 feat(viewer): directory-tree browser + plain-text viewer
Explorer drawer was a flat list of all 353 disc files and only textures had a
preview. Two additions:

1. Directory tree — the left drawer is now a collapsible `📁` tree, rebuilt each
   frame from the path list (ui.rs: build_tree / split_for_tree / render_dir).
   Filtering narrows leaves and auto-expands matches; `.p00…p04` data segments
   nest under a virtual `<stem>.pak` node. Selection still keys off the original
   file index, so the loader pipeline is untouched.

2. Plain-text viewer — config.ini and any file that sniffs as text now show
   read-only, selectable, monospace content with a Wrap toggle and an encoding
   label. Detection/decoding live in the Bevy-free formats crate:
   vfs::is_probably_text + vfs::decode_text (BOM-aware UTF-8/UTF-16LE/BE, BOM-less
   UTF-16LE heuristic, NUL-reject + 95%-printable fallback) — pure std, WASM-safe,
   unit-tested (6 new tests), and reused by the CLI sniffer (new cyan `txt`).

New TextPreview resource (registered unconditionally so it exists on wasm);
populated in apply_loaded_texture with a 2 MiB char-boundary-safe cap. Also fixes
a latent bug: switching from a texture to a non-texture left stale preview state —
both texture and text state now reset on every selection.

ViewerState gains text_wrap. Formats tests: 22 pass; workspace compiles.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 23:05:58 +02:00
MechaCat02
840db9c549 fix(texture): correct Xenos de-tile + endian + cubemaps + PNG export
Reworks the XPR2 texture path after RE against the retail disc (viewer
previews were scrambled/failed).

- de-tile: replace the naive Morton-over-32×32 approximation with the
  faithful Xenos Tiled2D bank/pipe/macro-tile address formula (ported
  from xenia texture_address.h). Verified correct: the Acheron backdrop
  decodes to a sharp planet with its spiral storm.
- endian: undo the X360 word byte-swap per the fetch-constant endianness
  field (k8in16/k8in32/k16in32) — without it BCn/ARGB data is noise.
- cubemaps: parse TXCM resources (skybox/backdrops), decoding face 0,
  instead of erroring "No TX2D found".
- A8R8G8B8: correct channel order after the k8in32 swap ([A,R,G,B]→RGBA)
  — the Acheron backdrop is now correctly green, not red.
- XPR files are texture PACKS; add XPR_RES_INDEX to select a resource.
- CLI `texture export` now writes real PNGs (texpresso BCn decode +
  image), replacing the stub. Adds texpresso + image deps.
- tests/texture_disc.rs: integration test running the pipeline over real
  disc .xpr files (28/28 parse). RE debug knobs: XPR_NO_DETILE /
  XPR_NO_ENDIAN / XPR_FORCE_ENDIAN.

KNOWN ISSUE: mipmapped DXT1 textures still decode to noise — mip 0 is
not at base_address in the multi-resource packs (localized to a mip
storage-offset quirk; uncompressed + de-tile + endian are all verified).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 21:37:20 +02:00
MechaCat02
5dd526c1de 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>
2026-07-09 21:56:42 +02:00
MechaCat02
84dd806f5b feat(formats): IPFB archive + IDXD definition readers
Add readers for Project Sylpheed's on-disc data format so the
reimplementation can load exact ship/weapon constants straight from
the game files.

- pak.rs: PakArchive — parse the IPFB `.pak` index + concatenated
  `.pNN` segments, binary-search the TOC by name-hash, and transparently
  inflate the per-entry "Z1" (zlib) container. Adds flate2.
- idxd.rs: IdxdObject — parse the IDXD reflective object serialization.
  The string pool stores properties value-before-key with defaulted
  fields omitted; typed getters (get_f32/get_i64/get_str/get_bool) read
  explicit values reliably, get_raw exposes identifier fields, and
  resolved_fields() enumerates every explicit (key,value). Defaulted
  fields correctly return None rather than a neighbouring key.
- sylpheed-cli: `pak list` (inventory entries + identity) and
  `pak dump <hash>` (full stat sheet for one object).

Verified against the real disc (auto-skipped without it): DeltaSaber
craft (HP=1000, Acceleration=600, velocity curve 100/700/1200, Turn=100,
RadarRange=500000) and the DSaber missile (LoadingCount=144, Interval=3.00,
Mass=0.77). 10 unit + 3 integration tests pass.

Not yet decoded: defaulted fields (many ratios, the *Count family) whose
values come from schema defaults or the hash-keyed binary node region.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 20:35:55 +02:00
MechaCat02
f8127e73b0 feat: initialise workspace — Milestone 1 asset explorer
Three-crate Cargo workspace structured per PROJECT.md spec:
- crates/sylpheed-formats  — Xbox 360 format parsers (no Bevy)
- crates/sylpheed-viewer   — Bevy 0.15 asset viewer + egui UI
- crates/sylpheed-cli      — CLI tools (extract/list/sniff/texture)

Milestone 1 features:
- XISO disc image reading via xdvdfs 0.8
- XPR2 texture container parsing + Morton de-tiling
- D3DFORMAT → wgpu TextureFormat mapping (DXT1/3/5, DXN, ARGB)
- Custom Bevy AssetLoader for .xpr files
- Orbit camera (LMB orbit, RMB pan, scroll zoom)
- egui file browser + RE notes panel
- CLI: extract / list / sniff / texture info / texture export
- GitHub Actions CI (Linux, macOS, Windows, WASM)
- Trunk WASM build config

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 21:04:07 +01:00