diff options
Diffstat (limited to 'crates/fparkan-inspection')
| -rw-r--r-- | crates/fparkan-inspection/src/lib.rs | 66 |
1 files changed, 66 insertions, 0 deletions
diff --git a/crates/fparkan-inspection/src/lib.rs b/crates/fparkan-inspection/src/lib.rs index 7a6a2c7..e2b9068 100644 --- a/crates/fparkan-inspection/src/lib.rs +++ b/crates/fparkan-inspection/src/lib.rs @@ -484,6 +484,43 @@ pub fn load_wear_material_texture_mip0_rgba8_from_root( }) } +/// Resolves phase-zero diffuse TEXM through a standalone map-local WEAR file. +/// +/// Map terrain keeps its two WEAR tables adjacent to `Land.msh`, while their +/// MAT0 and TEXM resources remain in the game root's normal archives. This +/// preserves that split instead of treating the sidecar as an archive entry. +/// +/// # Errors +/// +/// Returns a string error if the standalone WEAR file, global material +/// resources, or selected diffuse TEXM cannot be decoded. +pub fn load_standalone_wear_material_texture_mip0_rgba8_from_root( + root: &Path, + wear_path: &Path, + material_index: u16, +) -> Result<WearMaterialTexture, String> { + let wear_bytes = + fs::read(wear_path).map_err(|err| format!("{}: {err}", wear_path.display()))?; + let wear = decode_wear(&wear_bytes).map_err(|err| err.to_string())?; + let repository = CachedResourceRepository::new(Arc::new(DirectoryVfs::new(root))); + let material = + resolve_material(&repository, &wear, material_index).map_err(|err| err.to_string())?; + let texture = material.document.primary_texture().ok_or_else(|| { + "MAT0 phase zero declares an intentionally untextured material".to_string() + })?; + let texture_name = String::from_utf8_lossy(&texture.0).into_owned(); + let image = load_texture_mip0_rgba8_from_root(root, "Textures.lib", &texture_name)?; + Ok(WearMaterialTexture { + wear_archive: "<standalone>".to_string(), + wear_resource: wear_path.display().to_string(), + material_index, + material_name: String::from_utf8_lossy(&material.name.0).into_owned(), + material_fallback: material.fallback, + texture_name, + image, + }) +} + /// Inspects a terrain land file by path. /// /// # Errors @@ -814,6 +851,35 @@ mod tests { } #[test] + fn standalone_wear_material_texture_loader_keeps_map_sidecar_separate() { + let dir = temp_dir("standalone-wear-material-texture"); + let wear_path = dir.join("Land2.wea"); + fs::write(&wear_path, b"1\n0 MAT\n").expect("standalone wear"); + let mut mat0 = vec![0; 4 + 34]; + mat0[0..2].copy_from_slice(&1_u16.to_le_bytes()); + mat0[22..25].copy_from_slice(b"TEX"); + fs::write( + dir.join("material.lib"), + build_single_entry_nres_with_meta(b"MAT", fparkan_material::MAT0_KIND, 0, &mat0), + ) + .expect("material archive"); + fs::write( + dir.join("Textures.lib"), + build_single_entry_nres(b"TEX", &texm_argb8888_pixel([0x40, 0x11, 0x22, 0x33])), + ) + .expect("texture archive"); + + let selected = + load_standalone_wear_material_texture_mip0_rgba8_from_root(&dir, &wear_path, 0) + .expect("resolved material texture"); + + assert_eq!(selected.wear_archive, "<standalone>"); + assert_eq!(selected.wear_resource, wear_path.display().to_string()); + assert_eq!(selected.texture_name, "TEX"); + assert_eq!(selected.image.rgba8, vec![0x11, 0x22, 0x33, 0x40]); + } + + #[test] fn land_mesh_bounds_preserve_each_source_axis() { let mesh = LandMeshDocument { streams: Vec::new(), |
