Fix hokyu voice: resolve by subtitle demo-id, not ship category

The 13 manifest-unbound hokyu (resupply) cutscenes reuse 5 recordings
(VOICE_D_450..454). The category guess (ship LS/DS × source carrier/
tanker) was wrong for some. The real selector is the cutscene's subtitle
demo-id: 600→450, 601→451, 602→452, 603→453, 604→454 — derived from the
5 BOUND hokyu (each carries both a demo-id and a VOICETRACK), so it
self-validates. Fixes hokyu_LS_s11A/s15A (→451, were wrongly 450) and
hokyu_LS_s24A/s27A (→silent, no voice cue). User-confirmed all correct.

hokyu_voice_token() builds the demo→token map from the bound hokyu and
looks up the target movie's demo-id; replaces the category fallback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-23 19:48:56 +02:00
parent 053aa81953
commit 161359e151
2 changed files with 75 additions and 21 deletions

View File

@@ -3213,29 +3213,40 @@ fn decode_riffs_to_wav(riffs: Vec<Vec<u8>>, tag: &str, duration: f32) -> Result<
}
}
/// Categorical fallback voice token for a hokyu (resupply) cutscene the manifest
/// leaves unbound. Only 5 of the 18 hokyu movies carry an explicit `VOICETRACK`;
/// the game reuses those 5 generic recordings across stages, keyed by
/// ship (`LS`/`DS`) × resupply source (carrier `…A` / tanker `…H`). The four
/// category→recording pairs are read off the bound examples; `LS`/carrier has two
/// takes (450 from s02A, 451 from s09A) — we default unbound LS/carrier to 450.
/// Returns `None` for non-hokyu movies (they resolve via the manifest only).
fn hokyu_fallback_token(movie: &str) -> Option<String> {
/// Voice token for a hokyu (resupply) cutscene the manifest leaves unbound.
///
/// Only 5 of the 18 hokyu movies carry an explicit `VOICETRACK`; the rest reuse
/// those 5 recordings. The selector is the cutscene's **demo id** (from its
/// subtitle track), NOT the ship/source category: `hokyu_LS_s02A` and
/// `hokyu_LS_s11A` are both LS/carrier but use demos 600 vs 601 → `VOICE_D_450`
/// vs `_451` (their lines differ: "Rhino 3 has landed" vs "Rhino Leader has
/// landed"). We derive the demo→token map from the 5 BOUND hokyu (each has both a
/// subtitle demo id and a VOICETRACK), then look up the target movie's demo id.
/// Returns `None` for hokyu with no voice cue (`hokyu_LS_s24A`/`s27A` — correctly
/// silent) and for non-hokyu movies (they resolve via the manifest only).
#[cfg(not(target_arch = "wasm32"))]
fn hokyu_voice_token(
source: &SourceKind,
movie: &str,
lang: sylpheed_formats::slb::VoiceLang,
manifest: &[u8],
) -> Option<String> {
use sylpheed_formats::{movie_manifest, movie_subtitle as ms};
if !movie.starts_with("hokyu_") {
return None;
}
let ls = movie.contains("_LS_");
let ds = movie.contains("_DS_");
let carrier = movie.ends_with('A');
let tanker = movie.ends_with('H');
let token = match (ls, ds, carrier, tanker) {
(true, _, true, _) => "VOICE_D_450", // light ship, carrier resupply
(true, _, _, true) => "VOICE_D_453", // light ship, tanker resupply
(_, true, true, _) => "VOICE_D_452", // Delta Saber, carrier resupply
(_, true, _, true) => "VOICE_D_454", // Delta Saber, tanker resupply
_ => return None,
};
Some(token.to_string())
let lang_pak =
read_pak_archive_blocking(source, &format!("dat/movie/{}.pak", lang.code_pub())).ok()?;
// The target cutscene's demo id (its subtitle's single voice cue).
let want = ms::track_voice_cues(&lang_pak, movie).first().map(|&(d, _)| d)?;
// Match it against a bound hokyu carrying the same demo id → that VOICETRACK.
movie_manifest::parse(manifest).into_iter().find_map(|e| {
let tok = e.voice_token.filter(|_| e.movie.starts_with("hokyu_"))?;
ms::track_voice_cues(&lang_pak, &e.movie)
.iter()
.any(|&(d, _)| d == want)
.then_some(tok)
})
}
/// Resolve a movie's cutscene voice to a **continuous byte region** of the
@@ -3264,7 +3275,7 @@ fn resolve_movie_voice_region(
.iter()
.find_map(|e| tpak.read(e).ok().filter(|b| movie_manifest::is_manifest(b)))?;
let token = movie_manifest::voice_token(&manifest, movie)
.or_else(|| hokyu_fallback_token(movie))?;
.or_else(|| hokyu_voice_token(source, movie, lang, &manifest))?;
// token → sound-id via the master registry (the large per-language IDXD entry
// carrying the `<lang>\Movie\VOICE_*.slb` paths).
let marker = format!("{code}\\Movie\\VOICE_ADV.slb");