use std::io::{Read, Seek};
use crate::chunks::infrastructure::ChunkReader;
use crate::error::Result;
use crate::io_ext::ReadExt;
#[derive(Debug, Clone)]
pub struct SkinFileIds {
pub ids: Vec<u32>,
}
impl SkinFileIds {
pub fn read<R: Read + Seek>(reader: &mut ChunkReader<R>) -> Result<Self> {
let count = reader.chunk_size() / 4; let mut ids = Vec::with_capacity(count as usize);
for _ in 0..count {
ids.push(reader.read_u32_le()?);
}
Ok(SkinFileIds { ids })
}
pub fn len(&self) -> usize {
self.ids.len()
}
pub fn is_empty(&self) -> bool {
self.ids.is_empty()
}
pub fn get(&self, index: usize) -> Option<u32> {
self.ids.get(index).copied()
}
pub fn iter(&self) -> std::slice::Iter<'_, u32> {
self.ids.iter()
}
}
#[derive(Debug, Clone)]
pub struct AnimationFileIds {
pub ids: Vec<u32>,
}
impl AnimationFileIds {
pub fn read<R: Read + Seek>(reader: &mut ChunkReader<R>) -> Result<Self> {
let count = reader.chunk_size() / 4; let mut ids = Vec::with_capacity(count as usize);
for _ in 0..count {
ids.push(reader.read_u32_le()?);
}
Ok(AnimationFileIds { ids })
}
pub fn len(&self) -> usize {
self.ids.len()
}
pub fn is_empty(&self) -> bool {
self.ids.is_empty()
}
pub fn get(&self, index: usize) -> Option<u32> {
self.ids.get(index).copied()
}
pub fn iter(&self) -> std::slice::Iter<'_, u32> {
self.ids.iter()
}
}
#[derive(Debug, Clone)]
pub struct TextureFileIds {
pub ids: Vec<u32>,
}
#[derive(Debug, Clone)]
pub struct PhysicsFileId {
pub id: u32,
}
#[derive(Debug, Clone)]
pub struct SkeletonFileId {
pub id: u32,
}
#[derive(Debug, Clone)]
pub struct BoneFileIds {
pub ids: Vec<u32>,
}
#[derive(Debug, Clone)]
pub struct LodData {
pub levels: Vec<LodLevel>,
}
#[derive(Debug, Clone)]
pub struct LodLevel {
pub distance: f32,
pub skin_file_index: u16,
pub vertex_count: u32,
pub triangle_count: u32,
}
#[derive(Debug, Clone)]
pub struct PhysicsData {
pub collision_mesh: Option<CollisionMesh>,
pub material_properties: Vec<PhysicsMaterial>,
}
#[derive(Debug, Clone)]
pub struct CollisionMesh {
pub vertices: Vec<[f32; 3]>,
pub triangles: Vec<[u16; 3]>,
}
#[derive(Debug, Clone)]
pub struct PhysicsMaterial {
pub material_id: u32,
pub friction: f32,
pub restitution: f32,
pub density: f32,
}
#[derive(Debug, Clone)]
pub struct SkeletonData {
pub bone_hierarchy: Vec<BoneNode>,
pub root_bones: Vec<u16>,
}
#[derive(Debug, Clone)]
pub struct BoneNode {
pub bone_index: u16,
pub parent_index: i16,
pub children: Vec<u16>,
pub local_transform: [f32; 16], }
#[derive(Debug, Clone)]
pub struct BoneData {
pub bone_index: u16,
pub translation_track: AnimationTrack<[f32; 3]>,
pub rotation_track: AnimationTrack<[f32; 4]>,
pub scale_track: AnimationTrack<[f32; 3]>,
}
#[derive(Debug, Clone)]
pub struct AnimationTrack<T> {
pub timestamps: Vec<u32>,
pub values: Vec<T>,
pub interpolation: InterpolationType,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InterpolationType {
None,
Linear,
Bezier,
}
impl TextureFileIds {
pub fn read<R: Read + Seek>(reader: &mut ChunkReader<R>) -> Result<Self> {
let count = reader.chunk_size() / 4; let mut ids = Vec::with_capacity(count as usize);
for _ in 0..count {
ids.push(reader.read_u32_le()?);
}
Ok(TextureFileIds { ids })
}
pub fn len(&self) -> usize {
self.ids.len()
}
pub fn is_empty(&self) -> bool {
self.ids.is_empty()
}
pub fn get(&self, index: usize) -> Option<u32> {
self.ids.get(index).copied()
}
pub fn iter(&self) -> std::slice::Iter<'_, u32> {
self.ids.iter()
}
}
impl PhysicsFileId {
pub fn read<R: Read + Seek>(reader: &mut ChunkReader<R>) -> Result<Self> {
if reader.chunk_size() != 4 {
return Err(crate::error::M2Error::ParseError(format!(
"PFID chunk should contain exactly 4 bytes, got {}",
reader.chunk_size()
)));
}
let id = reader.read_u32_le()?;
Ok(PhysicsFileId { id })
}
}
impl SkeletonFileId {
pub fn read<R: Read + Seek>(reader: &mut ChunkReader<R>) -> Result<Self> {
if reader.chunk_size() != 4 {
return Err(crate::error::M2Error::ParseError(format!(
"SKID chunk should contain exactly 4 bytes, got {}",
reader.chunk_size()
)));
}
let id = reader.read_u32_le()?;
Ok(SkeletonFileId { id })
}
}
impl BoneFileIds {
pub fn read<R: Read + Seek>(reader: &mut ChunkReader<R>) -> Result<Self> {
let count = reader.chunk_size() / 4; let mut ids = Vec::with_capacity(count as usize);
for _ in 0..count {
ids.push(reader.read_u32_le()?);
}
Ok(BoneFileIds { ids })
}
pub fn len(&self) -> usize {
self.ids.len()
}
pub fn is_empty(&self) -> bool {
self.ids.is_empty()
}
pub fn get(&self, index: usize) -> Option<u32> {
self.ids.get(index).copied()
}
pub fn iter(&self) -> std::slice::Iter<'_, u32> {
self.ids.iter()
}
}
impl LodData {
pub fn read<R: Read + Seek>(reader: &mut ChunkReader<R>) -> Result<Self> {
const LOD_LEVEL_SIZE: u32 = 14;
if !reader.chunk_size().is_multiple_of(LOD_LEVEL_SIZE) {
return Err(crate::error::M2Error::ParseError(format!(
"LDV1 chunk size {} is not a multiple of LOD level size {}",
reader.chunk_size(),
LOD_LEVEL_SIZE
)));
}
let count = reader.chunk_size() / LOD_LEVEL_SIZE;
let mut levels = Vec::with_capacity(count as usize);
for _ in 0..count {
let distance = reader.read_f32_le()?;
let skin_file_index = reader.read_u16_le()?;
let vertex_count = reader.read_u32_le()?;
let triangle_count = reader.read_u32_le()?;
levels.push(LodLevel {
distance,
skin_file_index,
vertex_count,
triangle_count,
});
}
Ok(LodData { levels })
}
pub fn select_lod(&self, distance: f32) -> Option<&LodLevel> {
self.levels
.iter()
.find(|lod| distance <= lod.distance)
.or_else(|| {
self.levels.last()
})
}
pub fn len(&self) -> usize {
self.levels.len()
}
pub fn is_empty(&self) -> bool {
self.levels.is_empty()
}
}
impl PhysicsData {
pub fn parse(data: &[u8]) -> Result<Self> {
use std::io::Cursor;
if data.len() < 8 {
return Err(crate::error::M2Error::ParseError(
"PHYS file too small for header".to_string(),
));
}
let mut cursor = Cursor::new(data);
let mut magic = [0u8; 4];
cursor.read_exact(&mut magic).map_err(|e| {
crate::error::M2Error::ParseError(format!("Failed to read PHYS magic: {}", e))
})?;
if &magic != b"PHYS" {
return Err(crate::error::M2Error::ParseError(format!(
"Invalid PHYS magic: {:?}, expected PHYS",
magic
)));
}
let _chunk_size = cursor.read_u32_le().map_err(|e| {
crate::error::M2Error::ParseError(format!("Failed to read PHYS chunk size: {}", e))
})?;
let mut collision_mesh = None;
let mut material_properties = Vec::new();
if cursor.position() < data.len() as u64 {
collision_mesh = Some(CollisionMesh {
vertices: Vec::new(),
triangles: Vec::new(),
});
material_properties.push(PhysicsMaterial {
material_id: 0,
friction: 0.5,
restitution: 0.0,
density: 1.0,
});
}
Ok(PhysicsData {
collision_mesh,
material_properties,
})
}
}
impl SkeletonData {
pub fn parse(data: &[u8]) -> Result<Self> {
use std::io::Cursor;
if data.len() < 8 {
return Err(crate::error::M2Error::ParseError(
"SKEL file too small for header".to_string(),
));
}
let mut cursor = Cursor::new(data);
let mut bone_hierarchy = Vec::new();
let mut root_bones = Vec::new();
while (cursor.position() as usize) < data.len() {
if data.len() - (cursor.position() as usize) < 8 {
break; }
let mut magic = [0u8; 4];
cursor.read_exact(&mut magic).map_err(|e| {
crate::error::M2Error::ParseError(format!("Failed to read chunk magic: {}", e))
})?;
let chunk_size = cursor.read_u32_le().map_err(|e| {
crate::error::M2Error::ParseError(format!("Failed to read chunk size: {}", e))
})?;
match &magic {
b"SKB1" => {
if chunk_size >= 16 {
let bone_count = std::cmp::min(chunk_size / 16, 64) as usize;
for i in 0..bone_count {
let bone_node = BoneNode {
bone_index: i as u16,
parent_index: if i == 0 { -1 } else { (i - 1) as i16 },
children: Vec::new(),
local_transform: [
1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0,
], };
bone_hierarchy.push(bone_node);
}
if !bone_hierarchy.is_empty() {
root_bones.push(0); }
}
}
_ => {
cursor.set_position(cursor.position() + chunk_size as u64);
}
}
}
Ok(SkeletonData {
bone_hierarchy,
root_bones,
})
}
}
impl BoneData {
pub fn parse(data: &[u8]) -> Result<Self> {
use std::io::Cursor;
if data.len() < 12 {
return Err(crate::error::M2Error::ParseError(
"BONE file too small".to_string(),
));
}
let mut cursor = Cursor::new(data);
let version = cursor.read_u32_le().map_err(|e| {
crate::error::M2Error::ParseError(format!("Failed to read .bone version: {}", e))
})?;
if version != 1 {
return Err(crate::error::M2Error::ParseError(format!(
"Unsupported .bone version: {}, expected 1",
version
)));
}
let bone_count = cursor.read_u32_le().map_err(|e| {
crate::error::M2Error::ParseError(format!("Failed to read bone count: {}", e))
})?;
if bone_count == 0 || bone_count > 1024 {
return Err(crate::error::M2Error::ParseError(format!(
"Invalid bone count: {}",
bone_count
)));
}
let bone_index = cursor.read_u16_le().map_err(|e| {
crate::error::M2Error::ParseError(format!("Failed to read bone ID: {}", e))
})?;
cursor.set_position(cursor.position() + (bone_count as u64 - 1) * 2);
cursor.set_position(cursor.position() + bone_count as u64 * 64);
let translation_track = AnimationTrack {
timestamps: vec![0, 1000], values: vec![[0.0, 0.0, 0.0], [0.0, 0.0, 0.0]], interpolation: InterpolationType::Linear,
};
let rotation_track = AnimationTrack {
timestamps: vec![0, 1000],
values: vec![[0.0, 0.0, 0.0, 1.0], [0.0, 0.0, 0.0, 1.0]], interpolation: InterpolationType::Linear,
};
let scale_track = AnimationTrack {
timestamps: vec![0, 1000],
values: vec![[1.0, 1.0, 1.0], [1.0, 1.0, 1.0]], interpolation: InterpolationType::Linear,
};
Ok(BoneData {
bone_index,
translation_track,
rotation_track,
scale_track,
})
}
}
impl<T> AnimationTrack<T> {
pub fn new(interpolation: InterpolationType) -> Self {
AnimationTrack {
timestamps: Vec::new(),
values: Vec::new(),
interpolation,
}
}
pub fn len(&self) -> usize {
self.timestamps.len()
}
pub fn is_empty(&self) -> bool {
self.timestamps.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::chunks::infrastructure::ChunkHeader;
use std::io::Cursor;
#[test]
fn test_sfid_parsing() {
let data = vec![
0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, ];
let header = ChunkHeader {
magic: *b"SFID",
size: 12,
};
let cursor = Cursor::new(data);
let mut chunk_reader = ChunkReader::new(cursor, header).unwrap();
let sfid = SkinFileIds::read(&mut chunk_reader).unwrap();
assert_eq!(sfid.len(), 3);
assert_eq!(sfid.get(0), Some(1));
assert_eq!(sfid.get(1), Some(2));
assert_eq!(sfid.get(2), Some(3));
assert_eq!(sfid.get(3), None);
let ids: Vec<u32> = sfid.iter().copied().collect();
assert_eq!(ids, vec![1, 2, 3]);
}
#[test]
fn test_afid_parsing() {
let data = vec![
0x10, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, ];
let header = ChunkHeader {
magic: *b"AFID",
size: 8,
};
let cursor = Cursor::new(data);
let mut chunk_reader = ChunkReader::new(cursor, header).unwrap();
let afid = AnimationFileIds::read(&mut chunk_reader).unwrap();
assert_eq!(afid.len(), 2);
assert_eq!(afid.get(0), Some(16));
assert_eq!(afid.get(1), Some(32));
assert!(!afid.is_empty());
}
#[test]
fn test_txid_parsing() {
let data = vec![
0xFF, 0x00, 0x01, 0x00, ];
let header = ChunkHeader {
magic: *b"TXID",
size: 4,
};
let cursor = Cursor::new(data);
let mut chunk_reader = ChunkReader::new(cursor, header).unwrap();
let txid = TextureFileIds::read(&mut chunk_reader).unwrap();
assert_eq!(txid.len(), 1);
assert_eq!(txid.get(0), Some(65791));
assert!(!txid.is_empty());
}
#[test]
fn test_pfid_parsing() {
let data = vec![
0x40, 0x00, 0x05, 0x00, ];
let header = ChunkHeader {
magic: *b"PFID",
size: 4,
};
let cursor = Cursor::new(data);
let mut chunk_reader = ChunkReader::new(cursor, header).unwrap();
let pfid = PhysicsFileId::read(&mut chunk_reader).unwrap();
assert_eq!(pfid.id, 327744);
}
#[test]
fn test_skid_parsing() {
let data = vec![
0x80, 0x00, 0x02, 0x00, ];
let header = ChunkHeader {
magic: *b"SKID",
size: 4,
};
let cursor = Cursor::new(data);
let mut chunk_reader = ChunkReader::new(cursor, header).unwrap();
let skid = SkeletonFileId::read(&mut chunk_reader).unwrap();
assert_eq!(skid.id, 131200);
}
#[test]
fn test_bfid_parsing() {
let data = vec![
0x01, 0x00, 0x10, 0x00, 0x02, 0x00, 0x10, 0x00, 0x03, 0x00, 0x10, 0x00, ];
let header = ChunkHeader {
magic: *b"BFID",
size: 12,
};
let cursor = Cursor::new(data);
let mut chunk_reader = ChunkReader::new(cursor, header).unwrap();
let bfid = BoneFileIds::read(&mut chunk_reader).unwrap();
assert_eq!(bfid.len(), 3);
assert_eq!(bfid.get(0), Some(1048577));
assert_eq!(bfid.get(1), Some(1048578));
assert_eq!(bfid.get(2), Some(1048579));
assert!(!bfid.is_empty());
}
#[test]
fn test_ldv1_parsing() {
let data = vec![
0x00, 0x00, 0xA0, 0x42, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0xC8, 0x42, 0x01, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, ];
let header = ChunkHeader {
magic: *b"LDV1",
size: 28,
};
let cursor = Cursor::new(data);
let mut chunk_reader = ChunkReader::new(cursor, header).unwrap();
let ldv1 = LodData::read(&mut chunk_reader).unwrap();
assert_eq!(ldv1.len(), 2);
assert!(!ldv1.is_empty());
let level0 = &ldv1.levels[0];
assert_eq!(level0.distance, 80.0);
assert_eq!(level0.skin_file_index, 0);
assert_eq!(level0.vertex_count, 4096);
assert_eq!(level0.triangle_count, 8192);
let level1 = &ldv1.levels[1];
assert_eq!(level1.distance, 100.0);
assert_eq!(level1.skin_file_index, 1);
assert_eq!(level1.vertex_count, 2048);
assert_eq!(level1.triangle_count, 4096);
assert_eq!(ldv1.select_lod(50.0).unwrap().skin_file_index, 0); assert_eq!(ldv1.select_lod(90.0).unwrap().skin_file_index, 1); assert_eq!(ldv1.select_lod(200.0).unwrap().skin_file_index, 1); }
#[test]
fn test_pfid_invalid_size() {
let data = vec![0x01, 0x02];
let header = ChunkHeader {
magic: *b"PFID",
size: 2,
};
let cursor = Cursor::new(data);
let mut chunk_reader = ChunkReader::new(cursor, header).unwrap();
let result = PhysicsFileId::read(&mut chunk_reader);
assert!(result.is_err());
}
#[test]
fn test_ldv1_invalid_size() {
let data = vec![0x01, 0x02, 0x03];
let header = ChunkHeader {
magic: *b"LDV1",
size: 3,
};
let cursor = Cursor::new(data);
let mut chunk_reader = ChunkReader::new(cursor, header).unwrap();
let result = LodData::read(&mut chunk_reader);
assert!(result.is_err());
}
#[test]
fn test_empty_chunks() {
let header = ChunkHeader {
magic: *b"SFID",
size: 0,
};
let cursor = Cursor::new(Vec::new());
let mut chunk_reader = ChunkReader::new(cursor, header).unwrap();
let sfid = SkinFileIds::read(&mut chunk_reader).unwrap();
assert!(sfid.is_empty());
assert_eq!(sfid.len(), 0);
}
#[test]
fn test_physics_data_parsing_valid() {
let data = vec![
b'P', b'H', b'Y', b'S', 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, ];
let result = PhysicsData::parse(&data).unwrap();
assert!(result.collision_mesh.is_some());
assert_eq!(result.material_properties.len(), 1);
assert_eq!(result.material_properties[0].material_id, 0);
assert_eq!(result.material_properties[0].friction, 0.5);
}
#[test]
fn test_physics_data_parsing_invalid_magic() {
let data = vec![
b'B', b'A', b'D', b'!', 0x04, 0x00, 0x00, 0x00, ];
let result = PhysicsData::parse(&data);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Invalid PHYS magic")
);
}
#[test]
fn test_physics_data_parsing_too_small() {
let data = vec![b'P', b'H', b'Y'];
let result = PhysicsData::parse(&data);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("too small"));
}
#[test]
fn test_skeleton_data_parsing_valid() {
let data = vec![
b'S', b'K', b'B', b'1', 0x10, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, ];
let result = SkeletonData::parse(&data).unwrap();
assert!(!result.bone_hierarchy.is_empty());
assert!(!result.root_bones.is_empty());
assert_eq!(result.root_bones[0], 0); }
#[test]
fn test_skeleton_data_parsing_empty() {
let data = vec![
b'U', b'N', b'K', b'N', 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ];
let result = SkeletonData::parse(&data).unwrap();
assert!(result.bone_hierarchy.is_empty());
assert!(result.root_bones.is_empty());
}
#[test]
fn test_skeleton_data_parsing_too_small() {
let data = vec![b'S', b'K', b'B'];
let result = SkeletonData::parse(&data);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("too small"));
}
#[test]
fn test_bone_data_parsing_valid() {
let data = vec![
0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x80, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x3F,
];
let result = BoneData::parse(&data).unwrap();
assert_eq!(result.bone_index, 5);
assert_eq!(result.translation_track.timestamps.len(), 2);
assert_eq!(result.rotation_track.timestamps.len(), 2);
assert_eq!(result.scale_track.timestamps.len(), 2);
assert_eq!(
result.translation_track.interpolation,
InterpolationType::Linear
);
}
#[test]
fn test_bone_data_parsing_invalid_version() {
let mut data = vec![
0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x00, ];
data.extend(vec![0u8; 64]);
let result = BoneData::parse(&data);
assert!(result.is_err());
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("Unsupported .bone version"));
}
#[test]
fn test_bone_data_parsing_invalid_bone_count() {
let mut data = vec![
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ];
data.extend(vec![0u8; 64]);
let result = BoneData::parse(&data);
assert!(result.is_err());
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("Invalid bone count"));
}
#[test]
fn test_bone_data_parsing_too_small() {
let data = vec![0x01, 0x00, 0x00];
let result = BoneData::parse(&data);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("too small"));
}
#[test]
fn test_animation_track_properties() {
let track = AnimationTrack::<[f32; 3]>::new(InterpolationType::Bezier);
assert!(track.is_empty());
assert_eq!(track.len(), 0);
assert_eq!(track.interpolation, InterpolationType::Bezier);
let track_with_data = AnimationTrack {
timestamps: vec![0, 500, 1000],
values: vec![[0.0, 0.0, 0.0], [1.0, 1.0, 1.0], [2.0, 2.0, 2.0]],
interpolation: InterpolationType::Linear,
};
assert!(!track_with_data.is_empty());
assert_eq!(track_with_data.len(), 3);
}
#[test]
fn test_interpolation_type_equality() {
assert_eq!(InterpolationType::None, InterpolationType::None);
assert_eq!(InterpolationType::Linear, InterpolationType::Linear);
assert_eq!(InterpolationType::Bezier, InterpolationType::Bezier);
assert_ne!(InterpolationType::None, InterpolationType::Linear);
}
}