use binrw::{BinRead, BinWrite};
#[derive(Debug, Clone, Copy, BinRead, BinWrite)]
#[brw(little)]
pub struct DoodadPlacement {
pub name_id: u32,
pub unique_id: u32,
pub position: [f32; 3],
pub rotation: [f32; 3],
pub scale: u16,
pub flags: u16,
}
impl DoodadPlacement {
#[must_use]
pub fn get_scale(&self) -> f32 {
f32::from(self.scale) / 1024.0
}
#[must_use]
pub fn accepts_proj_textures(&self) -> bool {
self.flags & 0x1000 != 0
}
#[must_use]
pub fn uses_file_data_id(&self) -> bool {
self.flags & 0x40 != 0
}
}
#[derive(Debug, Clone, Default, BinRead, BinWrite)]
#[brw(little)]
pub struct MddfChunk {
#[br(parse_with = binrw::helpers::until_eof)]
pub placements: Vec<DoodadPlacement>,
}
impl MddfChunk {
#[must_use]
pub fn count(&self) -> usize {
self.placements.len()
}
#[must_use]
pub fn validate_name_ids(&self, mmid_count: usize) -> bool {
self.placements.iter().all(|p| {
if p.uses_file_data_id() {
true
} else {
(p.name_id as usize) < mmid_count
}
})
}
}
#[derive(Debug, Clone, Copy, BinRead, BinWrite)]
#[brw(little)]
pub struct WmoPlacement {
pub name_id: u32,
pub unique_id: u32,
pub position: [f32; 3],
pub rotation: [f32; 3],
pub extents_min: [f32; 3],
pub extents_max: [f32; 3],
pub flags: u16,
pub doodad_set: u16,
pub name_set: u16,
pub scale: u16,
}
impl WmoPlacement {
#[must_use]
pub fn get_scale(&self) -> f32 {
f32::from(self.scale) / 1024.0
}
#[must_use]
pub fn is_destroyable(&self) -> bool {
self.flags & 0x1 != 0
}
#[must_use]
pub fn uses_lod(&self) -> bool {
self.flags & 0x2 != 0
}
#[must_use]
pub fn uses_file_data_id(&self) -> bool {
self.flags & 0x8 != 0
}
#[must_use]
pub fn bounding_box_volume(&self) -> f32 {
let width = self.extents_max[0] - self.extents_min[0];
let height = self.extents_max[1] - self.extents_min[1];
let depth = self.extents_max[2] - self.extents_min[2];
width * height * depth
}
}
#[derive(Debug, Clone, Default, BinRead, BinWrite)]
#[brw(little)]
pub struct ModfChunk {
#[br(parse_with = binrw::helpers::until_eof)]
pub placements: Vec<WmoPlacement>,
}
impl ModfChunk {
#[must_use]
pub fn count(&self) -> usize {
self.placements.len()
}
#[must_use]
pub fn validate_name_ids(&self, mwid_count: usize) -> bool {
self.placements.iter().all(|p| {
if p.uses_file_data_id() {
true
} else {
(p.name_id as usize) < mwid_count
}
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
#[test]
fn test_doodad_placement_parse() {
let data = vec![
0x05, 0x00, 0x00, 0x00, 0xAB, 0xCD, 0xEF, 0x01, 0x00, 0x00, 0x80, 0x44, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x40, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB4, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x10, ];
let mut cursor = Cursor::new(data);
let placement = DoodadPlacement::read_le(&mut cursor).unwrap();
assert_eq!(placement.name_id, 5);
assert_eq!(placement.unique_id, 0x01EF_CDAB);
assert_eq!(placement.position, [1024.0, 32.0, 768.0]);
assert_eq!(placement.rotation, [0.0, 90.0, 0.0]);
assert_eq!(placement.scale, 1024);
assert_eq!(placement.flags, 0x1000);
assert_eq!(placement.get_scale(), 1.0);
assert!(placement.accepts_proj_textures());
assert!(!placement.uses_file_data_id());
}
#[test]
fn test_doodad_placement_scale_conversion() {
let placement = DoodadPlacement {
name_id: 0,
unique_id: 0,
position: [0.0; 3],
rotation: [0.0; 3],
scale: 2048, flags: 0,
};
assert_eq!(placement.get_scale(), 2.0);
}
#[test]
fn test_doodad_placement_flags() {
let mut placement = DoodadPlacement {
name_id: 0,
unique_id: 0,
position: [0.0; 3],
rotation: [0.0; 3],
scale: 1024,
flags: 0x40, };
assert!(placement.uses_file_data_id());
assert!(!placement.accepts_proj_textures());
placement.flags = 0x1040; assert!(placement.uses_file_data_id());
assert!(placement.accepts_proj_textures());
}
#[test]
fn test_mddf_chunk_parse() {
let mut data = Vec::new();
data.extend_from_slice(&[
0x01, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, ]);
data.extend_from_slice(&[
0x02, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, ]);
let mut cursor = Cursor::new(data);
let mddf = MddfChunk::read_le(&mut cursor).unwrap();
assert_eq!(mddf.count(), 2);
assert_eq!(mddf.placements[0].name_id, 1);
assert_eq!(mddf.placements[0].unique_id, 16);
assert_eq!(mddf.placements[1].name_id, 2);
assert_eq!(mddf.placements[1].position, [1.0, 2.0, 3.0]);
assert_eq!(mddf.placements[1].get_scale(), 2.0);
}
#[test]
fn test_mddf_chunk_validate_name_ids() {
let mddf = MddfChunk {
placements: vec![
DoodadPlacement {
name_id: 0,
unique_id: 1,
position: [0.0; 3],
rotation: [0.0; 3],
scale: 1024,
flags: 0,
},
DoodadPlacement {
name_id: 2,
unique_id: 2,
position: [0.0; 3],
rotation: [0.0; 3],
scale: 1024,
flags: 0,
},
],
};
assert!(mddf.validate_name_ids(3));
assert!(!mddf.validate_name_ids(2));
}
#[test]
fn test_mddf_chunk_validate_file_data_ids() {
let mddf = MddfChunk {
placements: vec![DoodadPlacement {
name_id: 9999999, unique_id: 1,
position: [0.0; 3],
rotation: [0.0; 3],
scale: 1024,
flags: 0x40, }],
};
assert!(mddf.validate_name_ids(10));
}
#[test]
fn test_wmo_placement_parse() {
let data = vec![
0x03, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x44, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x40, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB4, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x40, 0x03, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x04, ];
let mut cursor = Cursor::new(data);
let placement = WmoPlacement::read_le(&mut cursor).unwrap();
assert_eq!(placement.name_id, 3);
assert_eq!(placement.unique_id, 66);
assert_eq!(placement.position, [1024.0, 32.0, 768.0]);
assert_eq!(placement.rotation, [0.0, 90.0, 0.0]);
assert_eq!(placement.extents_min, [0.0, 0.0, 0.0]);
assert_eq!(placement.extents_max, [1.0, 2.0, 3.0]);
assert_eq!(placement.flags, 0x0003);
assert_eq!(placement.doodad_set, 5);
assert_eq!(placement.name_set, 0);
assert_eq!(placement.scale, 1024);
assert_eq!(placement.get_scale(), 1.0);
assert!(placement.is_destroyable());
assert!(placement.uses_lod());
assert!(!placement.uses_file_data_id());
}
#[test]
fn test_wmo_placement_bounding_box_volume() {
let placement = WmoPlacement {
name_id: 0,
unique_id: 0,
position: [0.0; 3],
rotation: [0.0; 3],
extents_min: [0.0, 0.0, 0.0],
extents_max: [10.0, 20.0, 30.0],
flags: 0,
doodad_set: 0,
name_set: 0,
scale: 1024,
};
assert_eq!(placement.bounding_box_volume(), 6000.0); }
#[test]
fn test_wmo_placement_flags() {
let mut placement = WmoPlacement {
name_id: 0,
unique_id: 0,
position: [0.0; 3],
rotation: [0.0; 3],
extents_min: [0.0; 3],
extents_max: [0.0; 3],
flags: 0x8, doodad_set: 0,
name_set: 0,
scale: 1024,
};
assert!(placement.uses_file_data_id());
assert!(!placement.is_destroyable());
assert!(!placement.uses_lod());
placement.flags = 0x0B; assert!(placement.uses_file_data_id());
assert!(placement.is_destroyable());
assert!(placement.uses_lod());
}
#[test]
fn test_modf_chunk_parse() {
let data = vec![
0x01, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x80, 0x3F, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, ];
let mut cursor = Cursor::new(data);
let modf = ModfChunk::read_le(&mut cursor).unwrap();
assert_eq!(modf.count(), 1);
assert_eq!(modf.placements[0].name_id, 1);
assert_eq!(modf.placements[0].unique_id, 16);
assert!(modf.placements[0].is_destroyable());
}
#[test]
fn test_modf_chunk_validate_name_ids() {
let modf = ModfChunk {
placements: vec![
WmoPlacement {
name_id: 0,
unique_id: 1,
position: [0.0; 3],
rotation: [0.0; 3],
extents_min: [0.0; 3],
extents_max: [0.0; 3],
flags: 0,
doodad_set: 0,
name_set: 0,
scale: 1024,
},
WmoPlacement {
name_id: 1,
unique_id: 2,
position: [0.0; 3],
rotation: [0.0; 3],
extents_min: [0.0; 3],
extents_max: [0.0; 3],
flags: 0,
doodad_set: 0,
name_set: 0,
scale: 1024,
},
],
};
assert!(modf.validate_name_ids(2));
assert!(!modf.validate_name_ids(1));
}
#[test]
fn test_modf_chunk_round_trip() {
let original = ModfChunk {
placements: vec![WmoPlacement {
name_id: 5,
unique_id: 100,
position: [1.0, 2.0, 3.0],
rotation: [0.0, 90.0, 0.0],
extents_min: [0.0, 0.0, 0.0],
extents_max: [10.0, 10.0, 10.0],
flags: 0x03,
doodad_set: 2,
name_set: 1,
scale: 2048,
}],
};
let mut buffer = Cursor::new(Vec::new());
original.write_le(&mut buffer).unwrap();
let data = buffer.into_inner();
let mut cursor = Cursor::new(data);
let parsed = ModfChunk::read_le(&mut cursor).unwrap();
assert_eq!(original.placements.len(), parsed.placements.len());
assert_eq!(original.placements[0].name_id, parsed.placements[0].name_id);
assert_eq!(
original.placements[0].position,
parsed.placements[0].position
);
assert_eq!(parsed.placements[0].get_scale(), 2.0);
}
#[test]
fn test_mddf_chunk_empty() {
let data = vec![];
let mut cursor = Cursor::new(data);
let mddf = MddfChunk::read_le(&mut cursor).unwrap();
assert_eq!(mddf.count(), 0);
assert!(mddf.validate_name_ids(0));
}
#[test]
fn test_modf_chunk_empty() {
let data = vec![];
let mut cursor = Cursor::new(data);
let modf = ModfChunk::read_le(&mut cursor).unwrap();
assert_eq!(modf.count(), 0);
assert!(modf.validate_name_ids(0));
}
}