use crate::io_ext::{ReadExt, WriteExt};
use bitflags::bitflags;
use std::io::{Read, Seek, Write};
use crate::common::M2Array;
use crate::error::{M2Error, Result};
use crate::version::M2Version;
pub const M2_MAGIC_LEGACY: [u8; 4] = *b"MD20";
pub const M2_MAGIC_CHUNKED: [u8; 4] = *b"MD21";
pub const M2_MAGIC: [u8; 4] = M2_MAGIC_LEGACY;
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct M2ModelFlags: u32 {
const TILT_X = 0x0001;
const TILT_Y = 0x0002;
const ADD_BACK_REFERENCE = 0x0004;
const USE_TEXTURE_COMBINERS = 0x0008;
const IS_CAMERA = 0x0010;
const UNUSED = 0x0020;
const NO_PARTICLE_TRAILS = 0x0040;
const UNKNOWN_0x80 = 0x0080;
const LOAD_PHYS_DATA = 0x0100;
const UNKNOWN_0x200 = 0x0200;
const HAS_BONES = 0x0400;
const UNUSED_0x800 = 0x0800;
const UNKNOWN_0x1000 = 0x1000;
const USE_TEXTURE_IDS = 0x2000;
const CAMERA_MODIFIABLE = 0x4000;
const NEW_PARTICLE_SYSTEM = 0x8000;
const UNKNOWN_0x10000 = 0x10000;
const UNKNOWN_0x20000 = 0x20000;
const UNKNOWN_0x40000 = 0x40000;
const UNKNOWN_0x80000 = 0x80000;
const UNKNOWN_0x100000 = 0x100000;
const UNKNOWN_0x200000 = 0x200000;
const UNKNOWN_0x400000 = 0x400000;
const UNKNOWN_0x800000 = 0x800000;
const UNKNOWN_0x1000000 = 0x1000000;
const UNKNOWN_0x2000000 = 0x2000000;
const UNKNOWN_0x4000000 = 0x4000000;
const UNKNOWN_0x8000000 = 0x8000000;
const UNKNOWN_0x10000000 = 0x10000000;
const UNKNOWN_0x20000000 = 0x20000000;
const UNKNOWN_0x40000000 = 0x40000000;
const UNKNOWN_0x80000000 = 0x80000000;
}
}
#[derive(Debug, Clone)]
pub struct M2Header {
pub magic: [u8; 4],
pub version: u32,
pub name: M2Array<u8>,
pub flags: M2ModelFlags,
pub global_sequences: M2Array<u32>,
pub animations: M2Array<u32>,
pub animation_lookup: M2Array<u16>,
pub playable_animation_lookup: Option<M2Array<u16>>,
pub bones: M2Array<u32>,
pub key_bone_lookup: M2Array<u16>,
pub vertices: M2Array<u32>,
pub views: M2Array<u32>,
pub num_skin_profiles: Option<u32>,
pub color_animations: M2Array<u32>,
pub textures: M2Array<u32>,
pub transparency_lookup: M2Array<u16>,
pub texture_flipbooks: Option<M2Array<u32>>,
pub texture_animations: M2Array<u32>,
pub color_replacements: M2Array<u32>,
pub render_flags: M2Array<u32>,
pub bone_lookup_table: M2Array<u16>,
pub texture_lookup_table: M2Array<u16>,
pub texture_units: M2Array<u16>,
pub transparency_lookup_table: M2Array<u16>,
pub texture_animation_lookup: M2Array<u16>,
pub bounding_box_min: [f32; 3],
pub bounding_box_max: [f32; 3],
pub bounding_sphere_radius: f32,
pub collision_box_min: [f32; 3],
pub collision_box_max: [f32; 3],
pub collision_sphere_radius: f32,
pub bounding_triangles: M2Array<u32>,
pub bounding_vertices: M2Array<u32>,
pub bounding_normals: M2Array<u32>,
pub attachments: M2Array<u32>,
pub attachment_lookup_table: M2Array<u16>,
pub events: M2Array<u32>,
pub lights: M2Array<u32>,
pub cameras: M2Array<u32>,
pub camera_lookup_table: M2Array<u16>,
pub ribbon_emitters: M2Array<u32>,
pub particle_emitters: M2Array<u32>,
pub blend_map_overrides: Option<M2Array<u32>>,
pub texture_combiner_combos: Option<M2Array<u32>>,
pub texture_transforms: Option<M2Array<u32>>,
}
impl M2Header {
pub fn parse<R: Read + Seek>(reader: &mut R) -> Result<Self> {
let mut magic = [0u8; 4];
reader.read_exact(&mut magic)?;
if magic != M2_MAGIC_LEGACY {
return Err(M2Error::InvalidMagic {
expected: String::from_utf8_lossy(&M2_MAGIC_LEGACY).to_string(),
actual: String::from_utf8_lossy(&magic).to_string(),
});
}
let version = reader.read_u32_le()?;
if M2Version::from_header_version(version).is_none() {
return Err(M2Error::UnsupportedVersion(version.to_string()));
}
let name = M2Array::parse(reader)?;
let flags = M2ModelFlags::from_bits_retain(reader.read_u32_le()?);
let global_sequences = M2Array::parse(reader)?;
let animations = M2Array::parse(reader)?;
let animation_lookup = M2Array::parse(reader)?;
let playable_animation_lookup = if (256..=263).contains(&version) {
Some(M2Array::parse(reader)?)
} else {
None
};
let bones = M2Array::parse(reader)?;
let key_bone_lookup = M2Array::parse(reader)?;
let vertices = M2Array::parse(reader)?;
let (views, num_skin_profiles) = if version <= 263 {
(M2Array::parse(reader)?, None)
} else {
let count = reader.read_u32_le()?;
(M2Array::new(0, 0), Some(count))
};
let color_animations = M2Array::parse(reader)?;
let textures = M2Array::parse(reader)?;
let transparency_lookup = M2Array::parse(reader)?;
let texture_flipbooks = if version <= 263 {
Some(M2Array::parse(reader)?)
} else {
None
};
let texture_animations = M2Array::parse(reader)?;
let color_replacements = M2Array::parse(reader)?;
let render_flags = M2Array::parse(reader)?;
let bone_lookup_table = M2Array::parse(reader)?;
let texture_lookup_table = M2Array::parse(reader)?;
let texture_units = M2Array::parse(reader)?;
let transparency_lookup_table = M2Array::parse(reader)?;
let mut texture_animation_lookup = M2Array::parse(reader)?;
if texture_animation_lookup.count > 1_000_000 {
texture_animation_lookup = M2Array::new(0, 0);
}
let mut bounding_box_min = [0.0; 3];
let mut bounding_box_max = [0.0; 3];
for item in &mut bounding_box_min {
*item = reader.read_f32_le()?;
}
for item in &mut bounding_box_max {
*item = reader.read_f32_le()?;
}
let bounding_sphere_radius = reader.read_f32_le()?;
let mut collision_box_min = [0.0; 3];
let mut collision_box_max = [0.0; 3];
for item in &mut collision_box_min {
*item = reader.read_f32_le()?;
}
for item in &mut collision_box_max {
*item = reader.read_f32_le()?;
}
let collision_sphere_radius = reader.read_f32_le()?;
let bounding_triangles = M2Array::parse(reader)?;
let bounding_vertices = M2Array::parse(reader)?;
let bounding_normals = M2Array::parse(reader)?;
let attachments = M2Array::parse(reader)?;
let attachment_lookup_table = M2Array::parse(reader)?;
let events = M2Array::parse(reader)?;
let lights = M2Array::parse(reader)?;
let cameras = M2Array::parse(reader)?;
let camera_lookup_table = M2Array::parse(reader)?;
let ribbon_emitters = M2Array::parse(reader)?;
let particle_emitters = M2Array::parse(reader)?;
let m2_version = M2Version::from_header_version(version).unwrap();
let blend_map_overrides = if version >= 260 && (flags.bits() & 0x8000000 != 0) {
Some(M2Array::parse(reader)?)
} else {
None
};
let texture_combiner_combos = if m2_version >= M2Version::Cataclysm {
Some(M2Array::parse(reader)?)
} else {
None
};
let texture_transforms = if m2_version >= M2Version::Legion {
Some(M2Array::parse(reader)?)
} else {
None
};
Ok(Self {
magic,
version,
name,
flags,
global_sequences,
animations,
animation_lookup,
playable_animation_lookup,
bones,
key_bone_lookup,
vertices,
views,
num_skin_profiles,
color_animations,
textures,
transparency_lookup,
texture_flipbooks,
texture_animations,
color_replacements,
render_flags,
bone_lookup_table,
texture_lookup_table,
texture_units,
transparency_lookup_table,
texture_animation_lookup,
bounding_box_min,
bounding_box_max,
bounding_sphere_radius,
collision_box_min,
collision_box_max,
collision_sphere_radius,
bounding_triangles,
bounding_vertices,
bounding_normals,
attachments,
attachment_lookup_table,
events,
lights,
cameras,
camera_lookup_table,
ribbon_emitters,
particle_emitters,
blend_map_overrides,
texture_combiner_combos,
texture_transforms,
})
}
pub fn write<W: Write>(&self, writer: &mut W) -> Result<()> {
writer.write_all(&self.magic)?;
writer.write_u32_le(self.version)?;
self.name.write(writer)?;
writer.write_u32_le(self.flags.bits())?;
self.global_sequences.write(writer)?;
self.animations.write(writer)?;
self.animation_lookup.write(writer)?;
if self.version <= 263
&& let Some(ref pal) = self.playable_animation_lookup
{
pal.write(writer)?;
}
self.bones.write(writer)?;
self.key_bone_lookup.write(writer)?;
self.vertices.write(writer)?;
if self.version <= 263 {
self.views.write(writer)?;
} else {
let count = self.num_skin_profiles.unwrap_or(0);
writer.write_u32_le(count)?;
}
self.color_animations.write(writer)?;
self.textures.write(writer)?;
self.transparency_lookup.write(writer)?;
if self.version <= 263
&& let Some(ref flipbooks) = self.texture_flipbooks
{
flipbooks.write(writer)?;
}
self.texture_animations.write(writer)?;
self.color_replacements.write(writer)?;
self.render_flags.write(writer)?;
self.bone_lookup_table.write(writer)?;
self.texture_lookup_table.write(writer)?;
self.texture_units.write(writer)?;
self.transparency_lookup_table.write(writer)?;
self.texture_animation_lookup.write(writer)?;
for &value in &self.bounding_box_min {
writer.write_f32_le(value)?;
}
for &value in &self.bounding_box_max {
writer.write_f32_le(value)?;
}
writer.write_f32_le(self.bounding_sphere_radius)?;
for &value in &self.collision_box_min {
writer.write_f32_le(value)?;
}
for &value in &self.collision_box_max {
writer.write_f32_le(value)?;
}
writer.write_f32_le(self.collision_sphere_radius)?;
self.bounding_triangles.write(writer)?;
self.bounding_vertices.write(writer)?;
self.bounding_normals.write(writer)?;
self.attachments.write(writer)?;
self.attachment_lookup_table.write(writer)?;
self.events.write(writer)?;
self.lights.write(writer)?;
self.cameras.write(writer)?;
self.camera_lookup_table.write(writer)?;
self.ribbon_emitters.write(writer)?;
self.particle_emitters.write(writer)?;
if let Some(ref overrides) = self.blend_map_overrides {
overrides.write(writer)?;
}
if let Some(ref combos) = self.texture_combiner_combos {
combos.write(writer)?;
}
if let Some(ref transforms) = self.texture_transforms {
transforms.write(writer)?;
}
Ok(())
}
pub fn version(&self) -> Option<M2Version> {
M2Version::from_header_version(self.version)
}
pub fn new(version: M2Version) -> Self {
let version_num = version.to_header_version();
let texture_combiner_combos = if version >= M2Version::Cataclysm {
Some(M2Array::new(0, 0))
} else {
None
};
let texture_transforms = if version >= M2Version::Legion {
Some(M2Array::new(0, 0))
} else {
None
};
let playable_animation_lookup = if (260..=263).contains(&version_num) {
Some(M2Array::new(0, 0))
} else {
None
};
let texture_flipbooks = if version_num <= 263 {
Some(M2Array::new(0, 0))
} else {
None
};
let num_skin_profiles = if version_num > 263 { Some(0) } else { None };
Self {
magic: M2_MAGIC_LEGACY,
version: version_num,
name: M2Array::new(0, 0),
flags: M2ModelFlags::empty(),
global_sequences: M2Array::new(0, 0),
animations: M2Array::new(0, 0),
animation_lookup: M2Array::new(0, 0),
playable_animation_lookup,
bones: M2Array::new(0, 0),
key_bone_lookup: M2Array::new(0, 0),
vertices: M2Array::new(0, 0),
views: M2Array::new(0, 0),
num_skin_profiles,
color_animations: M2Array::new(0, 0),
textures: M2Array::new(0, 0),
transparency_lookup: M2Array::new(0, 0),
texture_flipbooks,
texture_animations: M2Array::new(0, 0),
color_replacements: M2Array::new(0, 0),
render_flags: M2Array::new(0, 0),
bone_lookup_table: M2Array::new(0, 0),
texture_lookup_table: M2Array::new(0, 0),
texture_units: M2Array::new(0, 0),
transparency_lookup_table: M2Array::new(0, 0),
texture_animation_lookup: M2Array::new(0, 0),
bounding_box_min: [0.0, 0.0, 0.0],
bounding_box_max: [0.0, 0.0, 0.0],
bounding_sphere_radius: 0.0,
collision_box_min: [0.0, 0.0, 0.0],
collision_box_max: [0.0, 0.0, 0.0],
collision_sphere_radius: 0.0,
bounding_triangles: M2Array::new(0, 0),
bounding_vertices: M2Array::new(0, 0),
bounding_normals: M2Array::new(0, 0),
attachments: M2Array::new(0, 0),
attachment_lookup_table: M2Array::new(0, 0),
events: M2Array::new(0, 0),
lights: M2Array::new(0, 0),
cameras: M2Array::new(0, 0),
camera_lookup_table: M2Array::new(0, 0),
ribbon_emitters: M2Array::new(0, 0),
particle_emitters: M2Array::new(0, 0),
blend_map_overrides: None,
texture_combiner_combos,
texture_transforms,
}
}
pub fn convert(&self, target_version: M2Version) -> Result<Self> {
let source_version = self.version().ok_or(M2Error::ConversionError {
from: self.version,
to: target_version.to_header_version(),
reason: "Unknown source version".to_string(),
})?;
if source_version == target_version {
return Ok(self.clone());
}
let mut new_header = self.clone();
new_header.version = target_version.to_header_version();
if target_version >= M2Version::WotLK && source_version < M2Version::WotLK {
let count = if self.views.count > 0 {
self.views.count
} else {
1 };
new_header.num_skin_profiles = Some(count);
new_header.views = M2Array::new(0, 0); } else if target_version < M2Version::WotLK && source_version >= M2Version::WotLK {
new_header.num_skin_profiles = None;
new_header.views = M2Array::new(0, 0);
}
if target_version >= M2Version::WotLK && source_version < M2Version::WotLK {
new_header.playable_animation_lookup = None;
} else if target_version < M2Version::WotLK && source_version >= M2Version::WotLK {
new_header.playable_animation_lookup = Some(M2Array::new(0, 0));
}
if target_version >= M2Version::WotLK && source_version < M2Version::WotLK {
new_header.texture_flipbooks = None;
} else if target_version < M2Version::WotLK && source_version >= M2Version::WotLK {
new_header.texture_flipbooks = Some(M2Array::new(0, 0));
}
if target_version >= M2Version::Cataclysm && source_version < M2Version::Cataclysm {
new_header.texture_combiner_combos = Some(M2Array::new(0, 0));
} else if target_version < M2Version::Cataclysm && source_version >= M2Version::Cataclysm {
new_header.texture_combiner_combos = None;
}
if target_version >= M2Version::Legion && source_version < M2Version::Legion {
new_header.texture_transforms = Some(M2Array::new(0, 0));
} else if target_version < M2Version::Legion && source_version >= M2Version::Legion {
new_header.texture_transforms = None;
}
Ok(new_header)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
fn create_test_header(version: M2Version) -> Vec<u8> {
let mut data = Vec::new();
data.extend_from_slice(&M2_MAGIC_LEGACY);
data.extend_from_slice(&version.to_header_version().to_le_bytes());
data.extend_from_slice(&0u32.to_le_bytes()); data.extend_from_slice(&0u32.to_le_bytes());
data.extend_from_slice(&0u32.to_le_bytes());
data.extend_from_slice(&0u32.to_le_bytes()); data.extend_from_slice(&0u32.to_le_bytes());
for _ in 0..100 {
data.extend_from_slice(&0u32.to_le_bytes());
}
data
}
#[test]
fn test_header_parse_classic() {
let data = create_test_header(M2Version::Vanilla);
let mut cursor = Cursor::new(data);
let header = M2Header::parse(&mut cursor).unwrap();
assert_eq!(header.magic, M2_MAGIC_LEGACY);
assert_eq!(header.version, M2Version::Vanilla.to_header_version());
assert_eq!(header.texture_combiner_combos, None);
assert_eq!(header.texture_transforms, None);
}
#[test]
fn test_header_parse_cataclysm() {
let data = create_test_header(M2Version::Cataclysm);
let mut cursor = Cursor::new(data);
let header = M2Header::parse(&mut cursor).unwrap();
assert_eq!(header.magic, M2_MAGIC_LEGACY);
assert_eq!(header.version, M2Version::Cataclysm.to_header_version());
assert!(header.texture_combiner_combos.is_some());
assert_eq!(header.texture_transforms, None);
}
#[test]
fn test_header_parse_legion() {
let data = create_test_header(M2Version::Legion);
let mut cursor = Cursor::new(data);
let header = M2Header::parse(&mut cursor).unwrap();
assert_eq!(header.magic, M2_MAGIC_LEGACY);
assert_eq!(header.version, M2Version::Legion.to_header_version());
assert!(header.texture_combiner_combos.is_some());
assert!(header.texture_transforms.is_some());
}
#[test]
fn test_header_conversion() {
let classic_header = M2Header::new(M2Version::Vanilla);
let cataclysm_header = classic_header.convert(M2Version::Cataclysm).unwrap();
assert_eq!(
cataclysm_header.version,
M2Version::Cataclysm.to_header_version()
);
assert!(cataclysm_header.texture_combiner_combos.is_some());
assert_eq!(cataclysm_header.texture_transforms, None);
let legion_header = cataclysm_header.convert(M2Version::Legion).unwrap();
assert_eq!(legion_header.version, M2Version::Legion.to_header_version());
assert!(legion_header.texture_combiner_combos.is_some());
assert!(legion_header.texture_transforms.is_some());
let classic_header_2 = legion_header.convert(M2Version::Vanilla).unwrap();
assert_eq!(
classic_header_2.version,
M2Version::Vanilla.to_header_version()
);
assert_eq!(classic_header_2.texture_combiner_combos, None);
assert_eq!(classic_header_2.texture_transforms, None);
}
}