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>
64 lines
2.5 KiB
Rust
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);
|
|
}
|