Files
Syplheed-Reborn/crates/sylpheed-formats/tests/movie_subtitle_disc.rs
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

64 lines
2.5 KiB
Rust

//! Real-disc test for the movie subtitle chain. Skipped when the extracted disc
//! is absent (set `SYLPHEED_DISC` to the extract root to enable).
use std::path::PathBuf;
use sylpheed_formats::movie_subtitle::{self, SubLang};
use sylpheed_formats::PakArchive;
fn disc_root() -> Option<PathBuf> {
let p = PathBuf::from(std::env::var("SYLPHEED_DISC").ok()?);
p.join("dat").is_dir().then_some(p)
}
#[test]
fn resolves_english_radio_subtitles() {
let Some(root) = disc_root() else {
eprintln!("SKIP: set SYLPHEED_DISC");
return;
};
let lang = SubLang::English;
let lang_pak = PakArchive::open(root.join(format!("dat/movie/{}.pak", lang.pak_code()))).unwrap();
let text_pak =
PakArchive::open(root.join(format!("dat/GP_MAIN_GAME_{}.pak", lang.game_code()))).unwrap();
// A radio movie: real timed English dialogue.
let cues = movie_subtitle::load("RT01C_1", &lang_pak, &text_pak);
assert!(!cues.is_empty(), "RT01C_1 should have timed cues");
assert!(cues[0].start >= 0.0 && cues[0].start < 5.0);
assert!(
cues.iter().any(|c| c.text.contains("Rhino Leader")),
"expected recognizable dialogue, got: {:?}",
cues.iter().map(|c| &c.text).collect::<Vec<_>>()
);
// No MSG_DEMO keys should leak into resolved caption text.
assert!(
cues.iter().all(|c| !c.text.contains("MSG_DEMO")),
"unresolved key leaked: {:?}",
cues.iter().map(|c| &c.text).collect::<Vec<_>>()
);
// A resupply movie: the canonical resupply line.
let hokyu = movie_subtitle::load("hokyu_DS_s02A", &lang_pak, &text_pak);
assert!(hokyu.iter().any(|c| c.text.contains("Resupply complete")));
// S00A carries inline narration text (not MSG_DEMO refs) with timecodes.
let story = movie_subtitle::load("S00A", &lang_pak, &text_pak);
assert!(!story.is_empty() && story.iter().any(|c| c.text.contains("27th century")));
// S13A stores a two-line caption as two consecutive text tokens sharing one
// timing; both lines must survive (the "Look at it father" regression — the
// first line used to be dropped).
let s13 = movie_subtitle::load("S13A", &lang_pak, &text_pak);
let multiline = s13
.iter()
.find(|c| c.text.contains("Look at it father"))
.expect("S13A should contain the 'Look at it father' caption");
assert_eq!(
multiline.text, "Look at it father\n& beautiful isn't it",
"both lines of the caption must be present"
);
assert!((multiline.start - 74.8).abs() < 0.1, "start {}", multiline.start);
}