Files
Sylpheed/crates/sylpheed-formats/tests/movie_manifest_disc.rs
MechaCat02 ccd49ac31f fix(lint): clear the clippy gate across examples and tests
80 findings, not the 14 the first run showed -- clippy stops at the first
failing compilation unit, so `--keep-going` is what makes the list complete.

60 were machine-applicable (`cargo clippy --fix`). The rest by hand:

* five descending `sort_by` -> `sort_by_key(Reverse(..))`
* `chunks_exact(4)` on both sides of four zips, so the compared items stay
  `[u8; 4]` rather than one array against one slice
* three `type` aliases for the census maps and the captured-quad tuple
* `&PathBuf` -> `&Path` in two disc tests
* two range loops; one of them keeps `#[allow(needless_range_loop)]` with the
  reason -- the index is into a map's value, which changes each iteration
* the module doc list in `invert_capture` re-indented to markdown's rules
* `blit`'s eight arguments get `#[allow(too_many_arguments)]`, not a struct

One dead `let off = b.len();` in a `ratc` test is dropped rather than renamed.
The sibling test at :162 is the one that asserts an offset; if this one was
meant to as well, that is a test change and not a lint fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 16:42:41 +02:00

248 lines
9.2 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Real-disc test for the movie manifest → voice binding. Skipped without
//! `SYLPHEED_DISC`.
use std::path::{Path, PathBuf};
use sylpheed_formats::movie_manifest;
use sylpheed_formats::slb::VoiceLang;
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)
}
/// Read the manifest + `eng\sounds.tbl` out of `tables.pak`.
fn load_manifest_and_sounds(root: &Path) -> (Vec<u8>, Vec<u8>) {
let pak = PakArchive::open(root.join("dat/tables.pak")).unwrap();
let manifest = pak
.entries()
.iter()
.find_map(|e| pak.read(e).ok().filter(|b| movie_manifest::is_manifest(b)))
.expect("movie manifest present in tables.pak");
let sounds = pak
.read_by_name("eng\\sounds.tbl")
.expect("sounds.tbl present")
.unwrap();
(manifest, sounds)
}
#[test]
fn binds_and_resolves_movie_voice() {
let Some(root) = disc_root() else {
eprintln!("SKIP: set SYLPHEED_DISC");
return;
};
let (manifest, sounds) = load_manifest_and_sounds(&root);
let entries = movie_manifest::parse(&manifest);
// 104 cutscene SLOTS binding 101 distinct movies — three resupply movies are
// bound by two slots each. Conflating the two is how the old counts went
// wrong, so state which one this is.
assert!(
entries.len() > 90,
"expected 101 distinct movies (104 slots), got {}",
entries.len()
);
// Standard story movie → VOICE_<movie> in <lang>\Movie\.
assert_eq!(
movie_manifest::voice_token(&manifest, "S13A").as_deref(),
Some("VOICE_S13A")
);
assert_eq!(
movie_manifest::resolve_voice_entry(&manifest, &sounds, "S13A", VoiceLang::English)
.as_deref(),
Some("eng\\Movie\\VOICE_S13A.slb")
);
// A resupply movie bound to an in-mission radio clip → VOICE_D_* in \etc\.
// (This is what a `VOICE_<movie>` guess would miss entirely.)
assert_eq!(
movie_manifest::voice_token(&manifest, "hokyu_LS_s02A").as_deref(),
Some("VOICE_D_450")
);
assert_eq!(
movie_manifest::resolve_voice_entry(
&manifest,
&sounds,
"hokyu_LS_s02A",
VoiceLang::English
)
.as_deref(),
Some("eng\\etc\\VOICE_D_450.slb")
);
// ❌ This movie was recorded as having NO voice binding, on the strength of
// an in-game test that rejected `VOICE_D_452`. Both halves of that were
// wrong. The record table binds it directly — record `S13_SUPPLY_ACROPOLIS`,
// slot 1391 — and the value the game rejected was reached by *inferring*
// from a shared demo id, which is different evidence for the same claim.
// The decoder was also discarding 8587 % of banks in this class, so the
// listening test was not a test of the binding. See
// docs/re/voice-bank-leading-region.md.
//
// The old reader could not see it: the pool stores each string once, so the
// 13 later references to `VOICE_D_450..454` contribute no token at all.
let e = entries
.iter()
.find(|e| e.movie == "hokyu_DS_s13A")
.expect("hokyu_DS_s13A present in manifest");
assert_eq!(e.voice_token.as_deref(), Some("VOICE_D_452"));
assert_eq!(
movie_manifest::resolve_voice_entry(
&manifest,
&sounds,
"hokyu_DS_s13A",
VoiceLang::English
)
.as_deref(),
Some("eng\\etc\\VOICE_D_452.slb")
);
// Every resolved voice entry must actually exist in sound.pak.
let snd = std::fs::read(root.join("dat/sound.pak")).unwrap();
let keys: std::collections::HashSet<u32> = PakArchive::parse_toc(&snd)
.unwrap()
.iter()
.map(|e| e.name_hash)
.collect();
let mut resolved = 0;
let mut missing = Vec::new();
for e in &entries {
if let Some(full) =
movie_manifest::resolve_voice_entry(&manifest, &sounds, &e.movie, VoiceLang::English)
{
resolved += 1;
if !keys.contains(&sylpheed_formats::hash::name_hash(&full)) {
missing.push(full);
}
}
}
assert!(resolved > 80, "expected 80+ voiced movies, got {resolved}");
assert!(
missing.is_empty(),
"resolved but absent in sound.pak: {missing:?}"
);
}
/// The manifest's shape, pinned. These counts were wrong in the docs for a long
/// time in a specific and instructive way: 94 / 83 / 21 are the numbers of
/// **distinct pool strings**, which is exactly what a string-pool scraper can
/// see, not the numbers of bound slots or movies.
#[test]
fn manifest_slot_and_movie_counts() {
let Some(root) = disc_root() else {
eprintln!("SKIP: set SYLPHEED_DISC");
return;
};
let (manifest, _sounds) = load_manifest_and_sounds(&root);
let entries = movie_manifest::parse(&manifest);
use std::collections::{HashMap, HashSet};
let movies: HashSet<&str> = entries.iter().map(|e| e.movie.as_str()).collect();
assert_eq!(entries.len(), 104, "cutscene slots");
assert_eq!(movies.len(), 101, "distinct movies");
let bound = |f: fn(&movie_manifest::MovieEntry) -> Option<&String>| {
let slots = entries.iter().filter(|e| f(e).is_some()).count();
let movies: HashSet<&str> = entries
.iter()
.filter(|e| f(e).is_some())
.map(|e| e.movie.as_str())
.collect();
let distinct: HashSet<&str> = entries
.iter()
.filter_map(|e| f(e))
.map(String::as_str)
.collect();
(slots, movies.len(), distinct.len())
};
// (slots, movies, distinct strings) — the third is what the docs used to report.
assert_eq!(bound(|e| e.voice_token.as_ref()), (99, 96, 83));
assert_eq!(bound(|e| e.subtitle.as_ref()), (99, 96, 94));
assert_eq!(bound(|e| e.telop.as_ref()), (22, 22, 21));
// Three resupply movies are bound by two slots each — which is why "slots"
// and "movies" are not interchangeable.
let mut per_movie: HashMap<&str, Vec<&str>> = HashMap::new();
for e in &entries {
per_movie.entry(&e.movie).or_default().push(&e.slot);
}
let mut shared: Vec<&str> = per_movie
.iter()
.filter(|(_, slots)| slots.len() > 1)
.map(|(m, _)| *m)
.collect();
shared.sort_unstable();
assert_eq!(shared, ["hokyu_DS_s07A", "hokyu_DS_s07H", "hokyu_LS_s02A"]);
// Every id names a real record, so nothing dangles.
assert!(entries
.iter()
.all(|e| !e.slot.is_empty() && !e.movie.is_empty()));
}
/// Everything the Cutscenes browser shows, asserted against the disc.
///
/// The window's value is that it answers "which cutscenes exist, which mission
/// is each one in, and can I read it without playing it" — so the test checks
/// exactly those three, including the **negative**: five manifest-bound movies
/// have no `.wmv`, and nine more have no English transcript. A browser that
/// quietly omitted them would look complete and be wrong, so the counts are
/// pinned here rather than left to the eye.
#[test]
fn cutscene_catalog_binds_movies_and_transcripts() {
let Some(root) = disc_root() else {
eprintln!("SKIP: set SYLPHEED_DISC");
return;
};
use sylpheed_formats::movie_subtitle as ms;
let (manifest, _) = load_manifest_and_sounds(&root);
let rows = movie_manifest::parse(&manifest);
assert_eq!(rows.len(), 104, "manifest slots");
let movies: std::collections::BTreeSet<&str> = rows.iter().map(|r| r.movie.as_str()).collect();
assert_eq!(movies.len(), 101, "distinct movies");
assert_eq!(rows.iter().filter(|r| r.subtitle.is_some()).count(), 99);
assert_eq!(rows.iter().filter(|r| r.voice_token.is_some()).count(), 99);
assert_eq!(rows.iter().filter(|r| r.telop.is_some()).count(), 22);
// Bound-but-absent: the four boot logos and one encoder test clip are named
// by the manifest and are not on the disc. The browser marks these in red
// rather than offering a Play button that would fail.
let on_disc: std::collections::BTreeSet<String> = std::fs::read_dir(root.join("dat/movie"))
.unwrap()
.filter_map(|e| {
let p = e.ok()?.path();
(p.extension()?.to_str()? == "wmv")
.then(|| p.file_stem()?.to_str().map(|s| s.to_ascii_lowercase()))?
})
.collect();
let mut absent: Vec<&str> = movies
.iter()
.copied()
.filter(|m| !on_disc.contains(&m.to_ascii_lowercase()))
.collect();
absent.sort_unstable();
assert_eq!(
absent,
[
"SYLPH_HD720p_8M-CBR_2ch",
"logo1",
"logo2",
"logo3",
"logo4"
],
"manifest-bound movies with no .wmv on the disc"
);
// Transcripts, which is what makes a cutscene readable without playback.
let lang_pak = PakArchive::open(root.join("dat/movie/eng.pak")).unwrap();
let text_pak = PakArchive::open(root.join("dat/GP_MAIN_GAME_E.pak")).unwrap();
let resolved = movies
.iter()
.filter(|m| !ms::load(m, &lang_pak, &text_pak).is_empty())
.count();
assert_eq!(resolved, 92, "movies with an English transcript");
}