//! Integration tests against the real extracted Project Sylpheed disc. //! //! These are **skipped** (pass as no-ops) when the extracted disc is not present, //! so the suite still runs on machines/CI without the game. Point at the disc via //! the `SYLPHEED_DISC` env var, or drop it at the default dev path below. use std::path::{Path, PathBuf}; use sylpheed_formats::texture::X360Texture; use sylpheed_formats::{IdxdObject, PakArchive}; /// Locate the extracted disc root, or `None` to skip. 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; }; }; } #[test] fn deftables_header_and_first_entry() { skip_without_disc!(root); let arc = PakArchive::open(root.join("hidden/DefTables.pak")).unwrap(); assert_eq!(arc.len(), 1465); assert_eq!(arc.block_size, 0x800); assert_eq!(arc.flags, 0x1000_0000); // TOC is sorted ascending by name_hash. assert!(arc .entries() .windows(2) .all(|w| w[0].name_hash < w[1].name_hash)); // First entries decompress to a known inner format (IDXD or XML). let first = &arc.entries()[0]; let payload = arc.read(first).unwrap(); assert!( payload.starts_with(b"IDXD") || payload.starts_with(b" 0); } /// Real T8aD entries decode to sane RGBA surfaces, and a good fraction of the /// pack's T8aD entries are decodable (the RGBA-variant rule holds broadly). #[test] fn hangar_t8ad_textures_decode() { skip_without_disc!(root); let arc = PakArchive::open(root.join("dat/GP_HANGAR_ARSENAL.pak")).unwrap(); let (mut seen, mut decoded) = (0usize, 0usize); for e in arc.entries() { if e.comp_size > 4_000_000 { continue; } let Ok(p) = arc.read(e) else { continue }; if !sylpheed_formats::t8ad::is_t8ad(&p) { continue; } seen += 1; if let Some(img) = sylpheed_formats::t8ad::parse(&p) { decoded += 1; assert!((1..=4096).contains(&img.width) && (1..=4096).contains(&img.height)); assert_eq!(img.rgba.len(), (img.width * img.height * 4) as usize); } } assert!(seen > 100, "expected many T8aD, saw {seen}"); // The RGBA rule should cover the large majority (≈85% disc-wide). assert!(decoded * 100 >= seen * 70, "decoded {decoded}/{seen}"); } /// A real LSTA sprite list yields inline T8aD frames. #[test] fn lsta_sprite_list_decodes() { skip_without_disc!(root); let arc = PakArchive::open(root.join("dat/GP_HANGAR_ARSENAL.pak")).unwrap(); let mut found = false; for e in arc.entries() { let Ok(p) = arc.read(e) else { continue }; if !sylpheed_formats::lsta::is_lsta(&p) { continue; } if let Some(frames) = sylpheed_formats::lsta::parse(&p) { if !frames.is_empty() { found = true; assert!(frames.iter().all(|f| !f.rgba.is_empty())); } } } assert!(found, "no decodable LSTA found"); } /// A real RATC bundle lists named children including a decodable T8aD. #[test] fn ratc_bundle_lists_children() { skip_without_disc!(root); let arc = PakArchive::open(root.join("dat/GP_HANGAR_ARSENAL.pak")).unwrap(); let mut ok = false; for e in arc.entries() { if e.comp_size > 4_000_000 { continue; } let Ok(p) = arc.read(e) else { continue }; if !sylpheed_formats::ratc::is_ratc(&p) { continue; } let Some(kids) = sylpheed_formats::ratc::parse(&p) else { continue }; let has_named = kids.iter().any(|c| c.name.contains('.')); let has_t8ad = kids .iter() .any(|c| c.kind == "T8aD" && sylpheed_formats::t8ad::parse(&p[c.offset..c.offset + c.size]).is_some()); if !kids.is_empty() && has_named && has_t8ad { ok = true; break; } } assert!(ok, "no RATC with named children + a decodable T8aD"); } /// The English movie subtitle track (an `IXUD` entry) decodes to timed cues. #[test] fn eng_movie_subtitle_track() { skip_without_disc!(root); let arc = PakArchive::open(root.join("dat/movie/eng.pak")).unwrap(); // S04A's track — the longest inline English one (starts "Calm down!"). let bytes = arc.read_by_hash(0x73d2_2a3b).expect("entry present").unwrap(); let sub = sylpheed_formats::ixud::parse(&bytes).expect("IXUD parses as subtitle"); assert!(sub.cues.len() > 50, "cues={}", sub.cues.len()); assert!(!sub.is_reference_only()); assert_eq!(sub.cues[0].text, "Calm down!"); assert!((sub.cues[0].start - 9.4).abs() < 0.01); assert_eq!(sub.cues[0].end, Some(11.0)); } /// The subtitle tracks are genuinely localized: the same entry hash differs /// between the English and German packs. #[test] fn subtitles_are_localized() { skip_without_disc!(root); let eng = PakArchive::open(root.join("dat/movie/eng.pak")).unwrap(); let deu = PakArchive::open(root.join("dat/movie/deu.pak")).unwrap(); let e = eng.read_by_hash(0x73d2_2a3b).unwrap().unwrap(); let d = deu.read_by_hash(0x73d2_2a3b).unwrap().unwrap(); assert_ne!(e, d, "eng/deu subtitle payloads should differ"); } /// The English movie pack's embedded subtitle font parses to sane metadata. #[test] fn eng_movie_font_metadata() { skip_without_disc!(root); let arc = PakArchive::open(root.join("dat/movie/eng.pak")).unwrap(); let bytes = arc.read_by_hash(0x5cd0_fca6).expect("entry present").unwrap(); assert!(sylpheed_formats::font::is_font(&bytes)); let info = sylpheed_formats::font::parse_info(&bytes).expect("font parses"); assert!(info.glyphs > 0, "glyphs={}", info.glyphs); assert!(!info.family.is_empty(), "family should be readable"); } /// The Acheron backdrop is a TXCM world cubemap: 6 faces, and the cube path's /// face 0 matches the (validated) 2D `from_xpr2` face-0 decode. #[test] fn acheron_world_cubemap_six_faces() { skip_without_disc!(root); let bytes = std::fs::read(root.join("hidden/resource3d/BG_Acheron.xpr")).unwrap(); let cube = X360Texture::cube_faces_from_xpr2(&bytes) .expect("cube decode ok") .expect("BG_Acheron is a TXCM cubemap"); assert_eq!((cube.width, cube.height), (1024, 1024)); assert_eq!(cube.faces.len(), 6); // A8R8G8B8 → 4 bytes/texel, de-tiled to width×height. for face in &cube.faces { assert_eq!(face.len(), 1024 * 1024 * 4); } // Face 0 must equal what the existing 2D path decodes (the green planet). let face0_2d = X360Texture::from_xpr2(&bytes).unwrap(); assert!(face0_2d.is_cubemap); assert_eq!(cube.faces[0], face0_2d.data); }