//! Integration test: decode XBG7 geometry from REAL `.xpr` model files. //! //! Uses loose files extracted from the retail disc (models live in //! `hidden/resource3d/*.xpr`). Skipped unless the directory is found; point //! `SYLPHEED_RES3D` at it to override. //! //! Run: `cargo test -p sylpheed-formats --test mesh_disc -- --ignored --nocapture` use std::path::PathBuf; use sylpheed_formats::mesh::Xbg7Model; fn res3d_dir() -> Option { if let Ok(p) = std::env::var("SYLPHEED_RES3D") { let p = PathBuf::from(p); if p.is_dir() { return Some(p); } } let default = PathBuf::from("/home/fabi/RE Project Sylpheed/sylph_extract/hidden/resource3d"); default.is_dir().then_some(default) } #[test] #[ignore = "requires extracted disc models — set SYLPHEED_RES3D"] fn weapon_model_decodes_to_expected_geometry() { let Some(dir) = res3d_dir() else { eprintln!("SKIP: resource3d dir not found (set SYLPHEED_RES3D)"); return; }; // rou_f001_wep_00 = the player ship's first weapon: 1 sub-mesh, // 215 vertices, 364 triangles (verified by hex analysis). let bytes = std::fs::read(dir.join("rou_f001_wep_00.xpr")).unwrap(); let model = Xbg7Model::from_xpr2(&bytes).expect("weapon must decode"); assert_eq!(model.meshes.len(), 1); let (v, t) = model.totals(); assert_eq!(v, 215, "vertex count"); assert_eq!(t, 364, "triangle count"); let m = &model.meshes[0]; assert_eq!(m.positions.len(), 215); assert_eq!(m.uvs.len(), 215); assert_eq!(m.normals.len(), 215); assert_eq!(m.indices.len(), 1092); // every index in range assert!(m.indices.iter().all(|&i| (i as usize) < m.positions.len())); // positions are real geometry within the model's ~2-unit bbox let ys: Vec = m.positions.iter().map(|p| p[1]).collect(); let span = ys.iter().cloned().fold(f32::MIN, f32::max) - ys.iter().cloned().fold(f32::MAX, f32::min); assert!(span > 1.0 && span < 10.0, "y-span {span} out of range"); // Correct vertex alignment ⇒ normals are unit-length (the pin for the // +12 vertex offset) and UVs land in a sane texture range. let mean_nlen: f32 = m .normals .iter() .map(|n| (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt()) .sum::() / m.normals.len() as f32; assert!((mean_nlen - 1.0).abs() < 0.05, "mean |normal| {mean_nlen} ≠ 1"); assert!( m.uvs.iter().all(|uv| uv[0] > -0.1 && uv[0] < 2.0 && uv[1] > -0.1 && uv[1] < 2.0), "UVs out of expected [0,1]-ish range" ); } #[test] #[ignore = "requires extracted disc models — set SYLPHEED_RES3D"] fn complex_body_mesh_is_declined_not_garbage() { let Some(dir) = res3d_dir() else { return; }; // The hero-ship body uses the multi-stream layout we do not decode; it must // be cleanly rejected, never returned as partial geometry. let bytes = std::fs::read(dir.join("DeltaSaber_A.xpr")).unwrap(); match Xbg7Model::from_xpr2(&bytes) { Err(_) => {} // expected: declined Ok(m) => { // If it ever does decode, it must at least be self-consistent. for mesh in &m.meshes { assert!(mesh.indices.iter().all(|&i| (i as usize) < mesh.positions.len())); } } } }