From 161359e15134ef746db0d088d081adfaae1bbce5 Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Thu, 23 Jul 2026 19:48:56 +0200 Subject: [PATCH] Fix hokyu voice: resolve by subtitle demo-id, not ship category MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../examples/hokyu_demo_resolve.rs | 43 +++++++++++++++ crates/sylpheed-viewer/src/iso_loader.rs | 53 +++++++++++-------- 2 files changed, 75 insertions(+), 21 deletions(-) create mode 100644 crates/sylpheed-formats/examples/hokyu_demo_resolve.rs diff --git a/crates/sylpheed-formats/examples/hokyu_demo_resolve.rs b/crates/sylpheed-formats/examples/hokyu_demo_resolve.rs new file mode 100644 index 0000000..ceb3f99 --- /dev/null +++ b/crates/sylpheed-formats/examples/hokyu_demo_resolve.rs @@ -0,0 +1,43 @@ +use sylpheed_formats::{hash::name_hash, movie_manifest, movie_subtitle as ms, movie_voice, slb, PakArchive}; +use std::fs;use std::io::{Read,Seek,SeekFrom};use std::process::Command; +fn rg(disc:&str,g:u64,n:usize)->Vec{let mut segs=vec![];let mut cum=0u64;for i in 0..5{let p=format!("{disc}/dat/sound.p{i:02}");if let Ok(m)=fs::metadata(&p){segs.push((cum,m.len(),p));cum+=m.len();}}let mut out=vec![];let(mut need,mut pos)=(n,g);for(base,len,path)in &segs{if need==0||pos>=base+len||pos<*base{continue;}let local=pos-base;let take=need.min((len-local)as usize);let mut f=fs::File::open(path).unwrap();f.seek(SeekFrom::Start(local)).unwrap();let mut b=vec![0u8;take];f.read_exact(&mut b).unwrap();out.extend_from_slice(&b);need-=take;pos+=take as u64;}out} +fn ct(h:&[u8],n:&[u8])->bool{h.windows(n.len()).any(|w|w==n)} +fn dur(w:&str)->String{let o=Command::new("ffprobe").args(["-v","error","-show_entries","format=duration","-of","default=nw=1:nk=1",w]).output().unwrap();String::from_utf8_lossy(&o.stdout).trim().to_string()} +fn main(){ + let disc=std::env::var("SYLPHEED_DISC").unwrap(); + let tpak=PakArchive::open(format!("{disc}/dat/tables.pak")).unwrap(); + let man=tpak.entries().iter().find_map(|e|tpak.read(e).ok().filter(|b|movie_manifest::is_manifest(b))).unwrap(); + let lpak=PakArchive::open(format!("{disc}/dat/movie/eng.pak")).unwrap(); + let reg=tpak.entries().iter().find_map(|e|tpak.read(e).ok().filter(|b|ct(b,b"eng\\Movie\\VOICE_ADV.slb"))).unwrap(); + let ids=movie_voice::registry_voice_ids(®); + let stoc=fs::read(format!("{disc}/dat/sound.pak")).unwrap(); + let ents=PakArchive::parse_toc(&stoc).unwrap(); + // demo->token from bound hokyu + let hok:Vec<_>=movie_manifest::parse(&man).into_iter().filter(|m|m.movie.starts_with("hokyu_")).collect(); + let resolve=|movie:&str|->Option{ + movie_manifest::voice_token(&man,movie).or_else(||{ + let want=ms::track_voice_cues(&lpak,movie).first().map(|&(d,_)|d)?; + hok.iter().find_map(|e|{let t=e.voice_token.clone().filter(|_|e.movie.starts_with("hokyu_"))?; ms::track_voice_cues(&lpak,&e.movie).iter().any(|&(d,_)|d==want).then_some(t)}) + }) + }; + for e in &hok{ + let mv=&e.movie; + let bound=e.voice_token.is_some(); + let tok=resolve(mv); + let demo=ms::track_voice_cues(&lpak,mv).first().map(|&(d,_)|d); + let mut d="—".to_string(); + if let Some(tok)=&tok{ + if let Some(&id)=ids.get(tok){ + if let Some(anchor)=["Movie","etc","Voice"].iter().find_map(|dir|{let h=name_hash(&format!("eng\\{dir}\\{tok}.slb"));ents.binary_search_by_key(&h,|x|x.name_hash).ok().map(|i|ents[i].offset as u64)}){ + let ws=(anchor.saturating_sub(2*1024*1024))&!3; let win=rg(&disc,ws,8*1024*1024); + if let Some(el)=movie_voice::find_descriptor(&win,id){let end=ws+el as u64; + let start=movie_voice::find_descriptor(&win,id.wrapping_sub(1)).or_else(||movie_voice::find_descriptor_before(&win,el)).map(|o|ws+o as u64).filter(|&s|s {:11} voice={d}s", if bound{"BOUND"}else{"unbound"}, demo, tok.unwrap_or("(silent)".into())); + } +} diff --git a/crates/sylpheed-viewer/src/iso_loader.rs b/crates/sylpheed-viewer/src/iso_loader.rs index 5d46da2..a157a46 100644 --- a/crates/sylpheed-viewer/src/iso_loader.rs +++ b/crates/sylpheed-viewer/src/iso_loader.rs @@ -3213,29 +3213,40 @@ fn decode_riffs_to_wav(riffs: Vec>, 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 { +/// 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 { + 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 `\Movie\VOICE_*.slb` paths). let marker = format!("{code}\\Movie\\VOICE_ADV.slb");