diff options
Diffstat (limited to 'crates/msh-core/src/tests.rs')
| -rw-r--r-- | crates/msh-core/src/tests.rs | 296 |
1 files changed, 296 insertions, 0 deletions
diff --git a/crates/msh-core/src/tests.rs b/crates/msh-core/src/tests.rs new file mode 100644 index 0000000..1eefb31 --- /dev/null +++ b/crates/msh-core/src/tests.rs @@ -0,0 +1,296 @@ +use super::*; +use nres::Archive; +use std::fs; +use std::path::{Path, PathBuf}; + +fn collect_files_recursive(root: &Path, out: &mut Vec<PathBuf>) { + let Ok(entries) = fs::read_dir(root) else { + return; + }; + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + collect_files_recursive(&path, out); + } else if path.is_file() { + out.push(path); + } + } +} + +fn nres_test_files() -> Vec<PathBuf> { + let root = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("..") + .join("..") + .join("testdata"); + let mut files = Vec::new(); + collect_files_recursive(&root, &mut files); + files.sort(); + files + .into_iter() + .filter(|path| { + fs::read(path) + .map(|bytes| bytes.get(0..4) == Some(b"NRes")) + .unwrap_or(false) + }) + .collect() +} + +fn is_msh_name(name: &str) -> bool { + name.to_ascii_lowercase().ends_with(".msh") +} + +#[test] +fn parse_all_game_msh_models() { + let archives = nres_test_files(); + if archives.is_empty() { + eprintln!("skipping parse_all_game_msh_models: no NRes files in testdata"); + return; + } + + let mut model_count = 0usize; + let mut renderable_count = 0usize; + let mut legacy_stride24_count = 0usize; + + for archive_path in archives { + let archive = Archive::open_path(&archive_path) + .unwrap_or_else(|err| panic!("failed to open {}: {err}", archive_path.display())); + + for entry in archive.entries() { + if !is_msh_name(&entry.meta.name) { + continue; + } + model_count += 1; + let payload = archive.read(entry.id).unwrap_or_else(|err| { + panic!( + "failed to read model '{}' in {}: {err}", + entry.meta.name, + archive_path.display() + ) + }); + let model = parse_model_payload(payload.as_slice()).unwrap_or_else(|err| { + panic!( + "failed to parse model '{}' in {}: {err}", + entry.meta.name, + archive_path.display() + ) + }); + + if model.node_stride == 24 { + legacy_stride24_count += 1; + } + + for node_index in 0..model.node_count { + for lod in 0..3 { + for group in 0..5 { + if let Some(slot_idx) = model.slot_index(node_index, lod, group) { + assert!( + slot_idx < model.slots.len(), + "slot index out of bounds in '{}' ({})", + entry.meta.name, + archive_path.display() + ); + } + } + } + } + + let mut has_renderable_batch = false; + for node_index in 0..model.node_count { + let Some(slot_idx) = model.slot_index(node_index, 0, 0) else { + continue; + }; + let slot = &model.slots[slot_idx]; + let batch_end = + usize::from(slot.batch_start).saturating_add(usize::from(slot.batch_count)); + if batch_end > model.batches.len() { + continue; + } + for batch in &model.batches[usize::from(slot.batch_start)..batch_end] { + let index_start = usize::try_from(batch.index_start).unwrap_or(usize::MAX); + let index_count = usize::from(batch.index_count); + let end = index_start.saturating_add(index_count); + if end <= model.indices.len() && index_count >= 3 { + has_renderable_batch = true; + break; + } + } + if has_renderable_batch { + break; + } + } + if has_renderable_batch { + renderable_count += 1; + } + } + } + + assert!(model_count > 0, "no .msh entries found"); + assert!( + renderable_count > 0, + "no renderable models (lod0/group0) were detected" + ); + assert!( + legacy_stride24_count <= model_count, + "internal test accounting error" + ); +} + +#[test] +fn parse_minimal_synthetic_model() { + // Nested NRes with required resources only. + let mut payload = Vec::new(); + payload.extend_from_slice(b"NRes"); + payload.extend_from_slice(&0x100u32.to_le_bytes()); + payload.extend_from_slice(&5u32.to_le_bytes()); // entry_count + payload.extend_from_slice(&0u32.to_le_bytes()); // total_size placeholder + + let mut resource_offsets = Vec::new(); + let mut resource_sizes = Vec::new(); + let mut resource_types = Vec::new(); + let mut resource_attr3 = Vec::new(); + let mut resource_names = Vec::new(); + + let add_resource = |payload: &mut Vec<u8>, + offsets: &mut Vec<u32>, + sizes: &mut Vec<u32>, + types: &mut Vec<u32>, + attr3: &mut Vec<u32>, + names: &mut Vec<String>, + kind: u32, + name: &str, + data: &[u8], + attr3_val: u32| { + offsets.push(u32::try_from(payload.len()).expect("offset overflow")); + payload.extend_from_slice(data); + while !payload.len().is_multiple_of(8) { + payload.push(0); + } + sizes.push(u32::try_from(data.len()).expect("size overflow")); + types.push(kind); + attr3.push(attr3_val); + names.push(name.to_string()); + }; + + let node = { + let mut b = vec![0u8; 38]; + // slot[0][0] = 0 + b[8..10].copy_from_slice(&0u16.to_le_bytes()); + for i in 1..15 { + let off = 8 + i * 2; + b[off..off + 2].copy_from_slice(&u16::MAX.to_le_bytes()); + } + b + }; + let mut res2 = vec![0u8; 0x8C + 68]; + res2[0x8C..0x8C + 2].copy_from_slice(&0u16.to_le_bytes()); // tri_start + res2[0x8C + 2..0x8C + 4].copy_from_slice(&0u16.to_le_bytes()); // tri_count + res2[0x8C + 4..0x8C + 6].copy_from_slice(&0u16.to_le_bytes()); // batch_start + res2[0x8C + 6..0x8C + 8].copy_from_slice(&1u16.to_le_bytes()); // batch_count + let positions = [0f32, 0f32, 0f32, 1f32, 0f32, 0f32, 0f32, 1f32, 0f32] + .iter() + .flat_map(|v| v.to_le_bytes()) + .collect::<Vec<_>>(); + let indices = [0u16, 1, 2] + .iter() + .flat_map(|v| v.to_le_bytes()) + .collect::<Vec<_>>(); + let batch = { + let mut b = vec![0u8; 20]; + b[0..2].copy_from_slice(&0u16.to_le_bytes()); + b[2..4].copy_from_slice(&0u16.to_le_bytes()); + b[8..10].copy_from_slice(&3u16.to_le_bytes()); // index_count + b[10..14].copy_from_slice(&0u32.to_le_bytes()); // index_start + b[16..20].copy_from_slice(&0u32.to_le_bytes()); // base_vertex + b + }; + + add_resource( + &mut payload, + &mut resource_offsets, + &mut resource_sizes, + &mut resource_types, + &mut resource_attr3, + &mut resource_names, + RES1_NODE_TABLE, + "Res1", + &node, + 38, + ); + add_resource( + &mut payload, + &mut resource_offsets, + &mut resource_sizes, + &mut resource_types, + &mut resource_attr3, + &mut resource_names, + RES2_SLOTS, + "Res2", + &res2, + 68, + ); + add_resource( + &mut payload, + &mut resource_offsets, + &mut resource_sizes, + &mut resource_types, + &mut resource_attr3, + &mut resource_names, + RES3_POSITIONS, + "Res3", + &positions, + 12, + ); + add_resource( + &mut payload, + &mut resource_offsets, + &mut resource_sizes, + &mut resource_types, + &mut resource_attr3, + &mut resource_names, + RES6_INDICES, + "Res6", + &indices, + 2, + ); + add_resource( + &mut payload, + &mut resource_offsets, + &mut resource_sizes, + &mut resource_types, + &mut resource_attr3, + &mut resource_names, + RES13_BATCHES, + "Res13", + &batch, + 20, + ); + + let directory_offset = payload.len(); + for i in 0..resource_types.len() { + payload.extend_from_slice(&resource_types[i].to_le_bytes()); + payload.extend_from_slice(&1u32.to_le_bytes()); // attr1 + payload.extend_from_slice(&0u32.to_le_bytes()); // attr2 + payload.extend_from_slice(&resource_sizes[i].to_le_bytes()); + payload.extend_from_slice(&resource_attr3[i].to_le_bytes()); + let mut name_raw = [0u8; 36]; + let bytes = resource_names[i].as_bytes(); + name_raw[..bytes.len()].copy_from_slice(bytes); + payload.extend_from_slice(&name_raw); + payload.extend_from_slice(&resource_offsets[i].to_le_bytes()); + payload.extend_from_slice(&(i as u32).to_le_bytes()); // sort index + } + let total_size = u32::try_from(payload.len()).expect("size overflow"); + payload[12..16].copy_from_slice(&total_size.to_le_bytes()); + assert_eq!( + directory_offset + resource_types.len() * 64, + payload.len(), + "synthetic nested NRes layout invalid" + ); + + let model = parse_model_payload(&payload).expect("failed to parse synthetic model"); + assert_eq!(model.node_count, 1); + assert_eq!(model.positions.len(), 3); + assert_eq!(model.indices.len(), 3); + assert_eq!(model.batches.len(), 1); + assert_eq!(model.slot_index(0, 0, 0), Some(0)); +} |
