re: wire the IXUD record table into the crate — captions go 537 to 8800 of 8800
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
This commit is contained in:
@@ -48,17 +48,19 @@ fn all_eight_caption_families_are_read() {
|
||||
);
|
||||
|
||||
let total: usize = all.values().map(|v| v.len()).sum();
|
||||
assert_eq!(total, 8074, "recovered caption lines");
|
||||
assert_eq!(all.len(), 3721, "recovered caption ids");
|
||||
// 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: the DEMO family must come out identical through the new reader,
|
||||
/// so generalising cannot have changed what already worked.
|
||||
/// 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_unchanged_by_generalising() {
|
||||
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);
|
||||
@@ -70,10 +72,14 @@ fn demo_family_is_unchanged_by_generalising() {
|
||||
.filter(|(k, _)| k.starts_with("DEMO_"))
|
||||
.map(|(_, v)| v.len())
|
||||
.sum();
|
||||
assert_eq!(old, 537);
|
||||
assert_eq!(new, old, "DEMO must be identical through both readers");
|
||||
// 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 15x more text overall than the DEMO-only path saw.
|
||||
// …and 16x more text overall than the DEMO-only path saw.
|
||||
let total: usize = all.values().map(|v| v.len()).sum();
|
||||
assert!(total > old * 14, "expected a large gain, got {total} vs {old}");
|
||||
assert!(total > old * 16, "expected a large gain, got {total} vs {old}");
|
||||
}
|
||||
|
||||
95
crates/sylpheed-formats/tests/ixud_records_disc.rs
Normal file
95
crates/sylpheed-formats/tests/ixud_records_disc.rs
Normal file
@@ -0,0 +1,95 @@
|
||||
//! The IXUD record/field table, checked against every IXUD object on the disc.
|
||||
//!
|
||||
//! The decode was verified with a standalone parser hours before it existed in
|
||||
//! the crate; this is the same check through `IxudObject`.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use sylpheed_formats::hash::ixud_hash_str;
|
||||
use sylpheed_formats::{IxudObject, 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;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
fn all_paks(root: &Path) -> Vec<PathBuf> {
|
||||
let mut out = Vec::new();
|
||||
let mut stack = vec![root.to_path_buf()];
|
||||
while let Some(dir) = stack.pop() {
|
||||
let Ok(rd) = std::fs::read_dir(&dir) else { continue };
|
||||
for e in rd.flatten() {
|
||||
let p = e.path();
|
||||
if p.is_dir() {
|
||||
stack.push(p);
|
||||
} else if p.extension().is_some_and(|x| x == "pak") {
|
||||
out.push(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
out.sort();
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ixud_records_roundtrip_disc() {
|
||||
skip_without_disc!(root);
|
||||
let (mut objects, mut records, mut named, mut positional, mut bad) = (0, 0, 0, 0, 0);
|
||||
for pak in all_paks(&root) {
|
||||
let Ok(ar) = PakArchive::open(&pak) else { continue };
|
||||
for entry in ar.entries() {
|
||||
let Ok(bytes) = ar.read(entry) else { continue };
|
||||
if bytes.len() < 4 || bytes[0..4] != *b"IXUD" {
|
||||
continue;
|
||||
}
|
||||
let Some(obj) = IxudObject::parse(&bytes) else {
|
||||
bad += 1;
|
||||
continue;
|
||||
};
|
||||
objects += 1;
|
||||
// The header word is record 0's hash, not a schema id.
|
||||
assert_eq!(
|
||||
obj.first_record_hash,
|
||||
obj.records()[0].name_hash,
|
||||
"{}: header word is record 0's hash",
|
||||
pak.display()
|
||||
);
|
||||
for r in obj.records() {
|
||||
records += 1;
|
||||
assert_eq!(r.name_hash, ixud_hash_str(&r.name), "{}", pak.display());
|
||||
for f in &r.fields {
|
||||
match &f.name {
|
||||
Some(n) => {
|
||||
named += 1;
|
||||
assert_eq!(f.key, ixud_hash_str(n), "{}", pak.display());
|
||||
}
|
||||
None => positional += 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
eprintln!("objects {objects}, records {records}, named {named}, positional {positional}");
|
||||
assert_eq!(bad, 0, "every IXUD object must parse");
|
||||
assert_eq!(objects, 1104);
|
||||
assert_eq!(records, 1476);
|
||||
assert_eq!(named, 628_165);
|
||||
assert_eq!(positional, 48);
|
||||
}
|
||||
Reference in New Issue
Block a user