ixud.rs now has an IdxdObject-shaped reader, IxudObject, and build_caption_text reads captions as FIELDS instead of pairing them with whatever token follows in the pool. build_demo_text token adjacency 134 ids 537 lines build_caption_text token adjacency 3721 8074 build_caption_text record fields 4085 8800 = all of them Verified over the whole disc by tests/ixud_records_disc.rs: 1104/1104 objects parse, 1476/1476 records and 628165/628165 named fields reproduce their ixud_hash, 48 positional, zero failures. The header word at 0x08 is record 0's hash, asserted per object -- there is no schema field, exactly as for IDXD. The module doc described a 12-byte record directory and a "schema/type hash"; both were wrong and are corrected. I also have to correct my own number from the previous commit. "1.3% of the game's text" counted OCCURRENCES: each family lives in 24-45 IXUD blocks and the same key repeats across them. Distinct text-bearing MSG_* keys number 8800, not 44579, and every one has the <id>_<page>_<line> shape. So the real coverage was 537/8800 = 6.1%, and I overstated the gap about fivefold. Direction right, magnitude wrong. The DEMO control is the sharpest evidence for the change: token adjacency finds 537 lines there, the field reader 541. It was dropping lines even in the one family it was written for -- which is why the test now asserts "must not lose lines" rather than "must be identical". Same lesson twice in one session: pool adjacency is a consequence of how records are written, not a rule of the format. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
86 lines
3.2 KiB
Rust
86 lines
3.2 KiB
Rust
//! Caption recovery across all eight `MSG_*` families.
|
|
//!
|
|
//! `build_demo_text` reads only `MSG_DEMO_*`, the smallest family.
|
|
//! `build_caption_text` generalises the key parser to all eight.
|
|
|
|
use std::collections::BTreeMap;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use sylpheed_formats::{movie_subtitle, PakArchive};
|
|
|
|
fn disc_root() -> Option<PathBuf> {
|
|
if let Ok(p) = std::env::var("SYLPHEED_DISC") {
|
|
let p = PathBuf::from(p);
|
|
if p.join("dat").is_dir() {
|
|
return Some(p);
|
|
}
|
|
}
|
|
let d = Path::new(
|
|
"/home/fabi/RE - Project Sylpheed/Project Sylpheed - Arc of Deception (USA, Europe) (En,Ja)",
|
|
);
|
|
d.join("dat").is_dir().then(|| d.to_path_buf())
|
|
}
|
|
|
|
macro_rules! skip_without_disc {
|
|
($root:ident) => {
|
|
let Some($root) = disc_root() else {
|
|
eprintln!("SKIP: set SYLPHEED_DISC");
|
|
return;
|
|
};
|
|
};
|
|
}
|
|
|
|
#[test]
|
|
fn all_eight_caption_families_are_read() {
|
|
skip_without_disc!(root);
|
|
let pak = PakArchive::open(root.join("dat/GP_MAIN_GAME_E.pak")).expect("pak");
|
|
let all = movie_subtitle::build_caption_text(&pak);
|
|
|
|
let mut per: BTreeMap<String, usize> = BTreeMap::new();
|
|
for (id, lines) in &all {
|
|
*per.entry(id.split('_').next().unwrap().to_string()).or_default() += lines.len();
|
|
}
|
|
let fams: Vec<&str> = per.keys().map(String::as_str).collect();
|
|
assert_eq!(
|
|
fams,
|
|
["ACRO", "ADAN", "ADPL", "BIRD", "DEMO", "RHIN", "TCAF", "VOICE"],
|
|
"all eight families must appear"
|
|
);
|
|
|
|
let total: usize = all.values().map(|v| v.len()).sum();
|
|
// 8800 is ALL of them: every distinct text-bearing MSG_* key on the disc has
|
|
// the <id>_<page>_<line> shape, and the field reader recovers 8800 of 8800.
|
|
assert_eq!(total, 8800, "recovered caption lines");
|
|
assert_eq!(all.len(), 4085, "recovered caption ids");
|
|
|
|
// `VOICE` is the only family with a letter before the id; its ids must keep it.
|
|
assert!(all.contains_key("VOICE_A_150"), "VOICE ids keep their family letter");
|
|
}
|
|
|
|
/// The control: generalising must not lose anything the DEMO-only reader had.
|
|
/// It does not — it gains, because token adjacency was dropping lines there too.
|
|
#[test]
|
|
fn demo_family_is_not_lost_by_generalising() {
|
|
skip_without_disc!(root);
|
|
let pak = PakArchive::open(root.join("dat/GP_MAIN_GAME_E.pak")).expect("pak");
|
|
let demo = movie_subtitle::build_demo_text(&pak);
|
|
let all = movie_subtitle::build_caption_text(&pak);
|
|
|
|
let old: usize = demo.values().map(|v| v.len()).sum();
|
|
let new: usize = all
|
|
.iter()
|
|
.filter(|(k, _)| k.starts_with("DEMO_"))
|
|
.map(|(_, v)| v.len())
|
|
.sum();
|
|
// The token-adjacency reader misses 4 DEMO lines that the field reader gets,
|
|
// so the record route is strictly better even on the family it was written
|
|
// for. It must never be WORSE.
|
|
assert_eq!(old, 537, "build_demo_text, token adjacency");
|
|
assert_eq!(new, 541, "build_caption_text, record fields");
|
|
assert!(new >= old, "the record route must not lose lines");
|
|
|
|
// …and 16x more text overall than the DEMO-only path saw.
|
|
let total: usize = all.values().map(|v| v.len()).sum();
|
|
assert!(total > old * 16, "expected a large gain, got {total} vs {old}");
|
|
}
|