1use std::fs::File;
2use std::io::{Read, Seek, SeekFrom, Write};
3
4use crate::io_ext::ReadExt;
5use std::path::Path;
6
7use crate::chunks::animation::M2Animation;
8use crate::chunks::bone::M2Bone;
9use crate::chunks::material::M2Material;
10use crate::chunks::{M2Texture, M2Vertex};
11use crate::common::{M2Array, read_array};
12use crate::error::{M2Error, Result};
13use crate::header::M2Header;
14use crate::version::M2Version;
15
16#[derive(Debug, Clone)]
18pub struct M2Model {
19 pub header: M2Header,
21 pub name: Option<String>,
23 pub global_sequences: Vec<u32>,
25 pub animations: Vec<M2Animation>,
27 pub animation_lookup: Vec<u16>,
29 pub bones: Vec<M2Bone>,
31 pub key_bone_lookup: Vec<u16>,
33 pub vertices: Vec<M2Vertex>,
35 pub textures: Vec<M2Texture>,
37 pub materials: Vec<M2Material>,
39 pub raw_data: M2RawData,
42}
43
44#[derive(Debug, Clone, Default)]
46pub struct M2RawData {
47 pub transparency: Vec<u8>,
49 pub texture_animations: Vec<u8>,
51 pub color_animations: Vec<u8>,
53 pub render_flags: Vec<u8>,
55 pub bone_lookup_table: Vec<u16>,
57 pub texture_lookup_table: Vec<u16>,
59 pub texture_units: Vec<u16>,
61 pub transparency_lookup_table: Vec<u16>,
63 pub texture_animation_lookup: Vec<u16>,
65 pub bounding_triangles: Vec<u8>,
67 pub bounding_vertices: Vec<u8>,
69 pub bounding_normals: Vec<u8>,
71 pub attachments: Vec<u8>,
73 pub attachment_lookup_table: Vec<u16>,
75 pub events: Vec<u8>,
77 pub lights: Vec<u8>,
79 pub cameras: Vec<u8>,
81 pub camera_lookup_table: Vec<u16>,
83 pub ribbon_emitters: Vec<u8>,
85 pub particle_emitters: Vec<u8>,
87 pub texture_combiner_combos: Option<Vec<u8>>,
89}
90
91impl M2Model {
92 pub fn parse<R: Read + Seek>(reader: &mut R) -> Result<Self> {
94 let header = M2Header::parse(reader)?;
96
97 let _version = header
99 .version()
100 .ok_or(M2Error::UnsupportedVersion(header.version.to_string()))?;
101
102 let name = if header.name.count > 0 {
104 reader.seek(SeekFrom::Start(header.name.offset as u64))?;
106
107 let name_bytes = read_array(reader, &header.name, |r| Ok(r.read_u8()?))?;
109
110 let name_end = name_bytes
112 .iter()
113 .position(|&b| b == 0)
114 .unwrap_or(name_bytes.len());
115 let name_str = String::from_utf8_lossy(&name_bytes[..name_end]).to_string();
116 Some(name_str)
117 } else {
118 None
119 };
120
121 let global_sequences =
123 read_array(reader, &header.global_sequences, |r| Ok(r.read_u32_le()?))?;
124
125 let animations = read_array(reader, &header.animations.convert(), |r| {
127 M2Animation::parse(r, header.version)
128 })?;
129
130 let animation_lookup =
132 read_array(reader, &header.animation_lookup, |r| Ok(r.read_u16_le()?))?;
133
134 let bones = if header.version == 260 && header.bones.count == 203 {
137 let current_pos = reader.stream_position()?;
139 let file_size = reader.seek(SeekFrom::End(0))?;
140 reader.seek(SeekFrom::Start(current_pos))?; let bone_size = 92; let expected_end = header.bones.offset as u64 + (header.bones.count as u64 * bone_size);
144
145 if expected_end > file_size {
146 Vec::new()
151 } else {
152 read_array(reader, &header.bones.convert(), |r| {
154 M2Bone::parse(r, header.version)
155 })?
156 }
157 } else {
158 read_array(reader, &header.bones.convert(), |r| {
160 M2Bone::parse(r, header.version)
161 })?
162 };
163
164 let key_bone_lookup =
166 read_array(reader, &header.key_bone_lookup, |r| Ok(r.read_u16_le()?))?;
167
168 let vertices = read_array(reader, &header.vertices.convert(), |r| {
170 M2Vertex::parse(r, header.version)
171 })?;
172
173 let textures = read_array(reader, &header.textures.convert(), |r| {
175 M2Texture::parse(r, header.version)
176 })?;
177
178 let materials = read_array(reader, &header.render_flags.convert(), |r| {
180 M2Material::parse(r, header.version)
181 })?;
182
183 let mut raw_data = M2RawData::default();
186
187 if header.transparency_animations.count > 0 {
189 reader.seek(SeekFrom::Start(
190 header.transparency_animations.offset as u64,
191 ))?;
192 let mut transparency = vec![
193 0u8;
194 header.transparency_animations.count as usize
195 * std::mem::size_of::<u32>()
196 ];
197 reader.read_exact(&mut transparency)?;
198 raw_data.transparency = transparency;
199 }
200
201 raw_data.transparency_lookup_table =
203 read_array(reader, &header.transparency_lookup_table, |r| {
204 Ok(r.read_u16_le()?)
205 })?;
206
207 raw_data.texture_animation_lookup =
209 read_array(reader, &header.texture_animation_lookup, |r| {
210 Ok(r.read_u16_le()?)
211 })?;
212
213 raw_data.bone_lookup_table =
215 read_array(reader, &header.bone_lookup_table, |r| Ok(r.read_u16_le()?))?;
216
217 raw_data.texture_lookup_table = read_array(reader, &header.texture_lookup_table, |r| {
219 Ok(r.read_u16_le()?)
220 })?;
221
222 raw_data.texture_units =
224 read_array(reader, &header.texture_units, |r| Ok(r.read_u16_le()?))?;
225
226 raw_data.camera_lookup_table =
228 read_array(
229 reader,
230 &header.camera_lookup_table,
231 |r| Ok(r.read_u16_le()?),
232 )?;
233
234 Ok(Self {
235 header,
236 name,
237 global_sequences,
238 animations,
239 animation_lookup,
240 bones,
241 key_bone_lookup,
242 vertices,
243 textures,
244 materials,
245 raw_data,
246 })
247 }
248
249 pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
251 let mut file = File::open(path)?;
252 Self::parse(&mut file)
253 }
254
255 pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
257 let mut file = File::create(path)?;
258 self.write(&mut file)
259 }
260
261 pub fn write<W: Write + Seek>(&self, writer: &mut W) -> Result<()> {
263 let mut data_section = Vec::new();
265 let mut header = self.header.clone();
266
267 let header_size = self.calculate_header_size();
269 let mut current_offset = header_size as u32;
270
271 if let Some(ref name) = self.name {
273 let name_bytes = name.as_bytes();
274 let name_len = name_bytes.len() as u32 + 1; header.name = M2Array::new(name_len, current_offset);
276
277 data_section.extend_from_slice(name_bytes);
278 data_section.push(0); current_offset += name_len;
280 } else {
281 header.name = M2Array::new(0, 0);
282 }
283
284 if !self.global_sequences.is_empty() {
286 header.global_sequences =
287 M2Array::new(self.global_sequences.len() as u32, current_offset);
288
289 for &seq in &self.global_sequences {
290 data_section.extend_from_slice(&seq.to_le_bytes());
291 }
292
293 current_offset += (self.global_sequences.len() * std::mem::size_of::<u32>()) as u32;
294 } else {
295 header.global_sequences = M2Array::new(0, 0);
296 }
297
298 if !self.animations.is_empty() {
300 header.animations = M2Array::new(self.animations.len() as u32, current_offset);
301
302 for anim in &self.animations {
303 let mut anim_data = Vec::new();
305 anim.write(&mut anim_data, header.version)?;
306 data_section.extend_from_slice(&anim_data);
307 }
308
309 let anim_size = if header.version <= 256 { 24 } else { 52 };
311 current_offset += (self.animations.len() * anim_size) as u32;
312 } else {
313 header.animations = M2Array::new(0, 0);
314 }
315
316 if !self.animation_lookup.is_empty() {
318 header.animation_lookup =
319 M2Array::new(self.animation_lookup.len() as u32, current_offset);
320
321 for &lookup in &self.animation_lookup {
322 data_section.extend_from_slice(&lookup.to_le_bytes());
323 }
324
325 current_offset += (self.animation_lookup.len() * std::mem::size_of::<u16>()) as u32;
326 } else {
327 header.animation_lookup = M2Array::new(0, 0);
328 }
329
330 if !self.bones.is_empty() {
332 header.bones = M2Array::new(self.bones.len() as u32, current_offset);
333
334 for bone in &self.bones {
335 let mut bone_data = Vec::new();
336 bone.write(&mut bone_data, self.header.version)?;
337 data_section.extend_from_slice(&bone_data);
338 }
339
340 let bone_size = 92;
342 current_offset += (self.bones.len() * bone_size) as u32;
343 } else {
344 header.bones = M2Array::new(0, 0);
345 }
346
347 if !self.key_bone_lookup.is_empty() {
349 header.key_bone_lookup =
350 M2Array::new(self.key_bone_lookup.len() as u32, current_offset);
351
352 for &lookup in &self.key_bone_lookup {
353 data_section.extend_from_slice(&lookup.to_le_bytes());
354 }
355
356 current_offset += (self.key_bone_lookup.len() * std::mem::size_of::<u16>()) as u32;
357 } else {
358 header.key_bone_lookup = M2Array::new(0, 0);
359 }
360
361 if !self.vertices.is_empty() {
363 header.vertices = M2Array::new(self.vertices.len() as u32, current_offset);
364
365 let vertex_size =
366 if self.header.version().unwrap_or(M2Version::Classic) >= M2Version::Cataclysm {
367 44
369 } else {
370 36
372 };
373
374 for vertex in &self.vertices {
375 let mut vertex_data = Vec::new();
376 vertex.write(&mut vertex_data, self.header.version)?;
377 data_section.extend_from_slice(&vertex_data);
378 }
379
380 current_offset += (self.vertices.len() * vertex_size) as u32;
381 } else {
382 header.vertices = M2Array::new(0, 0);
383 }
384
385 if !self.textures.is_empty() {
387 header.textures = M2Array::new(self.textures.len() as u32, current_offset);
388
389 let mut texture_name_offsets = Vec::new();
391 let texture_def_size = 16; for texture in &self.textures {
394 texture_name_offsets
396 .push(current_offset + (self.textures.len() * texture_def_size) as u32);
397
398 let mut texture_def = Vec::new();
400
401 texture_def.extend_from_slice(&(texture.texture_type as u32).to_le_bytes());
403
404 texture_def.extend_from_slice(&texture.flags.bits().to_le_bytes());
406
407 texture_def.extend_from_slice(&0u32.to_le_bytes()); texture_def.extend_from_slice(&0u32.to_le_bytes()); data_section.extend_from_slice(&texture_def);
412 }
413
414 current_offset += (self.textures.len() * texture_def_size) as u32;
416
417 for (i, texture) in self.textures.iter().enumerate() {
419 let filename_offset = texture.filename.array.offset as usize;
421 let filename_len = texture.filename.array.count as usize;
422 if filename_offset == 0 || filename_len == 0 {
424 continue;
425 }
426
427 let base_data_offset = std::mem::size_of::<M2Header>();
430 let def_offset_in_data = (header.textures.offset as usize - base_data_offset)
431 + (i * texture_def_size)
432 + 8;
433
434 data_section[def_offset_in_data..def_offset_in_data + 4]
436 .copy_from_slice(&(filename_len as u32).to_le_bytes());
437 data_section[def_offset_in_data + 4..def_offset_in_data + 8]
438 .copy_from_slice(¤t_offset.to_le_bytes());
439
440 data_section.extend_from_slice(&texture.filename.string.data);
442 data_section.push(0); current_offset += filename_len as u32;
445 }
446 } else {
447 header.textures = M2Array::new(0, 0);
448 }
449
450 if !self.materials.is_empty() {
452 header.render_flags = M2Array::new(self.materials.len() as u32, current_offset);
453
454 for material in &self.materials {
455 let mut material_data = Vec::new();
456 material.write(&mut material_data, self.header.version)?;
457 data_section.extend_from_slice(&material_data);
458 }
459
460 let material_size = match self.header.version().unwrap_or(M2Version::Classic) {
461 v if v >= M2Version::WoD => 18, v if v >= M2Version::Cataclysm => 16, _ => 12, };
465
466 current_offset += (self.materials.len() * material_size) as u32;
467 } else {
468 header.render_flags = M2Array::new(0, 0);
469 }
470
471 if !self.raw_data.bone_lookup_table.is_empty() {
473 header.bone_lookup_table =
474 M2Array::new(self.raw_data.bone_lookup_table.len() as u32, current_offset);
475
476 for &lookup in &self.raw_data.bone_lookup_table {
477 data_section.extend_from_slice(&lookup.to_le_bytes());
478 }
479
480 current_offset +=
481 (self.raw_data.bone_lookup_table.len() * std::mem::size_of::<u16>()) as u32;
482 } else {
483 header.bone_lookup_table = M2Array::new(0, 0);
484 }
485
486 if !self.raw_data.texture_lookup_table.is_empty() {
488 header.texture_lookup_table = M2Array::new(
489 self.raw_data.texture_lookup_table.len() as u32,
490 current_offset,
491 );
492
493 for &lookup in &self.raw_data.texture_lookup_table {
494 data_section.extend_from_slice(&lookup.to_le_bytes());
495 }
496
497 current_offset +=
498 (self.raw_data.texture_lookup_table.len() * std::mem::size_of::<u16>()) as u32;
499 } else {
500 header.texture_lookup_table = M2Array::new(0, 0);
501 }
502
503 if !self.raw_data.texture_units.is_empty() {
505 header.texture_units =
506 M2Array::new(self.raw_data.texture_units.len() as u32, current_offset);
507
508 for &unit in &self.raw_data.texture_units {
509 data_section.extend_from_slice(&unit.to_le_bytes());
510 }
511
512 current_offset +=
513 (self.raw_data.texture_units.len() * std::mem::size_of::<u16>()) as u32;
514 } else {
515 header.texture_units = M2Array::new(0, 0);
516 }
517
518 if !self.raw_data.transparency_lookup_table.is_empty() {
520 header.transparency_lookup_table = M2Array::new(
521 self.raw_data.transparency_lookup_table.len() as u32,
522 current_offset,
523 );
524
525 for &lookup in &self.raw_data.transparency_lookup_table {
526 data_section.extend_from_slice(&lookup.to_le_bytes());
527 }
528
529 current_offset +=
530 (self.raw_data.transparency_lookup_table.len() * std::mem::size_of::<u16>()) as u32;
531 } else {
532 header.transparency_lookup_table = M2Array::new(0, 0);
533 }
534
535 if !self.raw_data.texture_animation_lookup.is_empty() {
537 header.texture_animation_lookup = M2Array::new(
538 self.raw_data.texture_animation_lookup.len() as u32,
539 current_offset,
540 );
541
542 for &lookup in &self.raw_data.texture_animation_lookup {
543 data_section.extend_from_slice(&lookup.to_le_bytes());
544 }
545
546 } else {
549 header.texture_animation_lookup = M2Array::new(0, 0);
550 }
551
552 header.write(writer)?;
554 writer.write_all(&data_section)?;
555
556 Ok(())
557 }
558
559 pub fn convert(&self, target_version: M2Version) -> Result<Self> {
561 let source_version = self.header.version().ok_or(M2Error::ConversionError {
562 from: self.header.version,
563 to: target_version.to_header_version(),
564 reason: "Unknown source version".to_string(),
565 })?;
566
567 if source_version == target_version {
568 return Ok(self.clone());
569 }
570
571 let header = self.header.convert(target_version)?;
573
574 let vertices = self
576 .vertices
577 .iter()
578 .map(|v| v.convert(target_version))
579 .collect();
580
581 let textures = self
583 .textures
584 .iter()
585 .map(|t| t.convert(target_version))
586 .collect();
587
588 let bones = self
590 .bones
591 .iter()
592 .map(|b| b.convert(target_version))
593 .collect();
594
595 let materials = self
597 .materials
598 .iter()
599 .map(|m| m.convert(target_version))
600 .collect();
601
602 let mut new_model = self.clone();
604 new_model.header = header;
605 new_model.vertices = vertices;
606 new_model.textures = textures;
607 new_model.bones = bones;
608 new_model.materials = materials;
609
610 Ok(new_model)
611 }
612
613 fn calculate_header_size(&self) -> usize {
615 let version = self.header.version().unwrap_or(M2Version::Classic);
616
617 let mut size = 4 + 4; size += 2 * 4; size += 4; size += 2 * 4; size += 2 * 4; size += 2 * 4; size += 2 * 4; size += 2 * 4; size += 2 * 4; size += 2 * 4; size += 2 * 4; size += 2 * 4; size += 2 * 4; size += 2 * 4; size += 2 * 4; size += 2 * 4; size += 2 * 4; size += 2 * 4; size += 2 * 4; size += 2 * 4; size += 2 * 4; size += 2 * 4; size += 3 * 4; size += 3 * 4; size += 4; size += 3 * 4; size += 3 * 4; size += 4; size += 2 * 4; size += 2 * 4; size += 2 * 4; size += 2 * 4; size += 2 * 4; size += 2 * 4; size += 2 * 4; size += 2 * 4; size += 2 * 4; size += 2 * 4; size += 2 * 4; if version >= M2Version::Cataclysm {
672 size += 2 * 4; }
674
675 if version >= M2Version::Legion {
676 size += 2 * 4; }
678
679 size
680 }
681
682 pub fn validate(&self) -> Result<()> {
684 if self.header.version().is_none() {
686 return Err(M2Error::UnsupportedVersion(self.header.version.to_string()));
687 }
688
689 if self.vertices.is_empty() {
691 return Err(M2Error::ValidationError(
692 "Model has no vertices".to_string(),
693 ));
694 }
695
696 for (i, bone) in self.bones.iter().enumerate() {
698 if bone.parent_bone >= 0 && bone.parent_bone as usize >= self.bones.len() {
700 return Err(M2Error::ValidationError(format!(
701 "Bone {} has invalid parent bone {}",
702 i, bone.parent_bone
703 )));
704 }
705 }
706
707 for (i, texture) in self.textures.iter().enumerate() {
709 if texture.filename.array.count > 0 && texture.filename.array.offset == 0 {
711 return Err(M2Error::ValidationError(format!(
712 "Texture {i} has invalid filename offset"
713 )));
714 }
715 }
716
717 for (i, _material) in self.materials.iter().enumerate() {
719 let _material_index = i; }
723
724 Ok(())
725 }
726}