From f34280476dc1fe00c03b9861991919c12cf6a2c1 Mon Sep 17 00:00:00 2001 From: "Claude (auto-RE)" Date: Tue, 11 Aug 2026 23:50:20 +0000 Subject: [PATCH] test: lock today's format findings into disc-wide guards Each finding today was originally missed by reasoning from a sample, so these assert DISC-WIDE invariants rather than one hand-picked file. ui_surfaces_disc.rs (3 passing) every_t8ad_on_the_disc_decodes -- all ~19216 surfaces; the old 256-grid model looked like 96% lsta_count_equals_sprites_plus_primitives -- header counts T8aD AND PRMD, 64/64, which is what made the count look unreliable ratc_nesting_is_exactly_one_level -- nested records are leaves; zero grandchildren disc-wide mesh_consistency_disc.rs (1 ignored, deliberately) shared_resources_decode_identically_in_every_container The mesh test is written as the TARGET state, not a snapshot of the bug: a resource shared by several containers must decode to the same bounds, which today fails for 125 of 681 shared resources. Fixing the anchor scan makes it pass; un-ignoring it is then the last step rather than a rewrite. It only compares decodes that agree on vertex/triangle counts, so "found different geometry" stays a separate question from "placed the same geometry differently". All suites green: 81 lib + the disc guards, with 2 ignored. Co-Authored-By: Claude Opus 5 (1M context) --- .../tests/mesh_consistency_disc.rs | 104 ++++++++++++ .../tests/ui_surfaces_disc.rs | 152 ++++++++++++++++++ 2 files changed, 256 insertions(+) create mode 100644 crates/sylpheed-formats/tests/mesh_consistency_disc.rs create mode 100644 crates/sylpheed-formats/tests/ui_surfaces_disc.rs diff --git a/crates/sylpheed-formats/tests/mesh_consistency_disc.rs b/crates/sylpheed-formats/tests/mesh_consistency_disc.rs new file mode 100644 index 0000000..a2a0738 --- /dev/null +++ b/crates/sylpheed-formats/tests/mesh_consistency_disc.rs @@ -0,0 +1,104 @@ +//! Cross-container consistency for XBG7 geometry. +//! +//! A geometry resource shared by several stage containers must decode to the +//! same bounds in each. This needs no ground truth, and on 2026-08-11 it found +//! **125 of 681** shared resources decoding to different bounds while reporting +//! identical vertex and triangle counts — the anchor scan locating a different +//! buffer of the same size (see `docs/re/structures/xbg7-mesh.md`). +//! +//! The test is `#[ignore]`d because the decoder does not satisfy it yet. It is +//! written as the *target* state so that fixing the anchor scan makes it pass, +//! rather than as a snapshot of the bug. + +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; + +use sylpheed_formats::mesh::Xbg7Model; + +fn disc_root() -> Option { + if let Ok(p) = std::env::var("SYLPHEED_DISC") { + let p = PathBuf::from(p); + if p.join("dat").is_dir() { + return Some(p); + } + } + let default = Path::new( + "/home/fabi/RE - Project Sylpheed/Project Sylpheed - Arc of Deception (USA, Europe) (En,Ja)", + ); + if default.join("dat").is_dir() { + return Some(default.to_path_buf()); + } + None +} + +/// Rounded (w, h, d) of a model's own geometry. +fn span(m: &Xbg7Model) -> Option<[i64; 3]> { + let (mut lo, mut hi) = ([f32::MAX; 3], [f32::MIN; 3]); + for s in &m.meshes { + for q in &s.positions { + for k in 0..3 { + lo[k] = lo[k].min(q[k]); + hi[k] = hi[k].max(q[k]); + } + } + } + if lo[0] == f32::MAX { + return None; + } + Some([ + (hi[0] - lo[0]).round() as i64, + (hi[1] - lo[1]).round() as i64, + (hi[2] - lo[2]).round() as i64, + ]) +} + +#[test] +#[ignore = "known-failing: 125 of 681 shared resources decode inconsistently (2026-08-11)"] +fn shared_resources_decode_identically_in_every_container() { + let Some(root) = disc_root() else { + eprintln!("SKIP: extracted disc not found (set SYLPHEED_DISC to enable)"); + return; + }; + let dir = root.join("hidden/resource3d"); + let mut files: Vec = std::fs::read_dir(&dir) + .expect("resource3d/") + .flatten() + .map(|e| e.path()) + .filter(|p| p.extension().and_then(|s| s.to_str()) == Some("xpr")) + .collect(); + files.sort(); + + // name -> (verts, tris) -> set of spans seen + let mut seen: BTreeMap> = BTreeMap::new(); + for f in &files { + let Ok(bytes) = std::fs::read(f) else { continue }; + let where_ = f.file_name().unwrap().to_string_lossy().to_string(); + for m in Xbg7Model::anchor_models_cancellable(&bytes, 0.0, &|| false) { + let Some(sp) = span(&m) else { continue }; + let v: usize = m.meshes.iter().map(|s| s.positions.len()).sum(); + let t: usize = m.meshes.iter().map(|s| s.indices.len() / 3).sum(); + seen.entry(m.name.clone()).or_default().push((sp, v, t, where_.clone())); + } + } + + let mut bad: Vec = Vec::new(); + for (name, list) in &seen { + if list.len() < 2 { + continue; + } + // Only compare decodes that agree on how much geometry they found; + // a differing vertex/triangle count is a different question. + if !list.iter().all(|e| e.1 == list[0].1 && e.2 == list[0].2) { + continue; + } + let spans: std::collections::BTreeSet<[i64; 3]> = list.iter().map(|e| e.0).collect(); + if spans.len() > 1 && bad.len() < 10 { + bad.push(format!("{name}: {spans:?}")); + } + } + assert!( + bad.is_empty(), + "{} shared resources decode to different bounds; first: {bad:?}", + bad.len() + ); +} diff --git a/crates/sylpheed-formats/tests/ui_surfaces_disc.rs b/crates/sylpheed-formats/tests/ui_surfaces_disc.rs new file mode 100644 index 0000000..c103c4d --- /dev/null +++ b/crates/sylpheed-formats/tests/ui_surfaces_disc.rs @@ -0,0 +1,152 @@ +//! Disc-wide guards for the 2D surface formats, locking in what was measured on +//! 2026-08-11. Skipped (as no-ops) when the extracted disc is absent. +//! +//! These assert *disc-wide invariants* rather than one hand-picked file, because +//! each of the findings they guard was originally missed by reasoning from a +//! sample: T8aD's "~15 % unsupported variants" were a wrong model, and LSTA's +//! "a few entries disagree with the count" was a miscount. + +use std::path::{Path, PathBuf}; + +use sylpheed_formats::{lsta, pak::PakArchive, ratc, t8ad}; + +fn disc_root() -> Option { + if let Ok(p) = std::env::var("SYLPHEED_DISC") { + let p = PathBuf::from(p); + if p.join("dat").is_dir() { + return Some(p); + } + } + let default = Path::new( + "/home/fabi/RE - Project Sylpheed/Project Sylpheed - Arc of Deception (USA, Europe) (En,Ja)", + ); + if default.join("dat").is_dir() { + return Some(default.to_path_buf()); + } + None +} + +macro_rules! skip_without_disc { + ($root:ident) => { + let Some($root) = disc_root() else { + eprintln!("SKIP: extracted disc not found (set SYLPHEED_DISC to enable)"); + return; + }; + }; +} + +/// Every entry of every pak, plus every RATC child, as raw bytes. +fn for_each_blob(root: &Path, mut f: impl FnMut(&str, &str, &[u8])) { + let mut paks: Vec = std::fs::read_dir(root.join("dat")) + .expect("dat/") + .flatten() + .map(|e| e.path()) + .filter(|p| p.extension().and_then(|s| s.to_str()) == Some("pak")) + .collect(); + paks.sort(); + for p in &paks { + let pak_name = p.file_name().unwrap().to_string_lossy().to_string(); + let Ok(arc) = PakArchive::open(p) else { continue }; + for e in arc.entries() { + let Ok(bytes) = arc.read(e) else { continue }; + f(&pak_name, &format!("{:08x}", e.name_hash), &bytes); + if ratc::is_ratc(&bytes) { + if let Some(kids) = ratc::parse(&bytes) { + for k in &kids { + if k.offset + k.size <= bytes.len() { + f(&pak_name, &k.name, &bytes[k.offset..k.offset + k.size]); + } + } + } + } + } + } +} + +/// A T8aD surface is a list of sub-rectangles, and on this disc **every** one +/// decodes. Regressing the rectangle model would show up here as a decode gap, +/// which is exactly how the old 256-grid reading looked (96 %, not 100 %). +#[test] +fn every_t8ad_on_the_disc_decodes() { + skip_without_disc!(root); + let (mut total, mut ok) = (0usize, 0usize); + let mut first_failure = None; + for_each_blob(&root, |pak, name, b| { + if !t8ad::is_t8ad(b) || b.len() < 0x40 { + return; + } + total += 1; + if t8ad::parse(b).is_some() { + ok += 1; + } else if first_failure.is_none() { + first_failure = Some(format!("{pak}:{name}")); + } + }); + assert!(total > 19_000, "expected the disc's ~19 216 surfaces, saw {total}"); + assert_eq!(ok, total, "first failure: {first_failure:?}"); +} + +/// An LSTA's header count is exact and counts **both** kinds of element: T8aD +/// sprites and `PRMD` primitives. (It was long read as unreliable because the +/// comparison ignored primitives.) +#[test] +fn lsta_count_equals_sprites_plus_primitives() { + skip_without_disc!(root); + let count_magic = |b: &[u8], magic: &[u8; 4]| { + let (mut n, mut i) = (0usize, 4usize); + while i + 4 <= b.len() { + if &b[i..i + 4] == magic { + n += 1; + i += 4; + } else { + i += 1; + } + } + n + }; + let (mut lists, mut exact) = (0usize, 0usize); + let mut bad = Vec::new(); + for_each_blob(&root, |pak, name, b| { + if !lsta::is_lsta(b) || b.len() < 8 { + return; + } + lists += 1; + let declared = u32::from_be_bytes([b[4], b[5], b[6], b[7]]) as usize; + let sprites = count_magic(b, b"T8aD"); + let prims = count_magic(b, b"PRMD"); + if declared == sprites + prims { + exact += 1; + } else if bad.len() < 4 { + bad.push(format!("{pak}:{name} declared {declared} != {sprites}+{prims}")); + } + }); + assert!(lists >= 60, "expected the disc's 64 LSTA lists, saw {lists}"); + assert_eq!(exact, lists, "mismatches: {bad:?}"); +} + +/// Nested RATC records are **leaves**: they carry no child list of their own. +/// "One level deep" describes the data, not a parser limit. +#[test] +fn ratc_nesting_is_exactly_one_level() { + skip_without_disc!(root); + let mut grandchildren = 0usize; + let mut bundles = 0usize; + for_each_blob(&root, |_, _, b| { + if !ratc::is_ratc(b) { + return; + } + let Some(kids) = ratc::parse(b) else { return }; + bundles += 1; + for k in &kids { + if k.offset + k.size > b.len() { + continue; + } + let sub = &b[k.offset..k.offset + k.size]; + if ratc::is_ratc(sub) { + grandchildren += ratc::parse(sub).map(|g| g.len()).unwrap_or(0); + } + } + }); + assert!(bundles > 2_000, "expected thousands of RATC bundles, saw {bundles}"); + assert_eq!(grandchildren, 0, "a nested RATC record listed children"); +}