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.offset as usize;
421 let filename_len = texture.filename.count as usize;
422
423 if filename_offset == 0 || filename_len == 0 {
425 continue;
426 }
427
428 let base_data_offset = std::mem::size_of::<M2Header>();
431 let def_offset_in_data = (header.textures.offset as usize - base_data_offset)
432 + (i * texture_def_size)
433 + 8;
434
435 data_section[def_offset_in_data..def_offset_in_data + 4]
437 .copy_from_slice(&(filename_len as u32).to_le_bytes());
438 data_section[def_offset_in_data + 4..def_offset_in_data + 8]
439 .copy_from_slice(¤t_offset.to_le_bytes());
440
441 let filename = format!("texture{i}.blp");
444 let filename_bytes = filename.as_bytes();
445
446 data_section.extend_from_slice(filename_bytes);
448 data_section.push(0); current_offset += (filename_bytes.len() + 1) as u32;
451 }
452 } else {
453 header.textures = M2Array::new(0, 0);
454 }
455
456 if !self.materials.is_empty() {
458 header.render_flags = M2Array::new(self.materials.len() as u32, current_offset);
459
460 for material in &self.materials {
461 let mut material_data = Vec::new();
462 material.write(&mut material_data, self.header.version)?;
463 data_section.extend_from_slice(&material_data);
464 }
465
466 let material_size = match self.header.version().unwrap_or(M2Version::Classic) {
467 v if v >= M2Version::WoD => 18, v if v >= M2Version::Cataclysm => 16, _ => 12, };
471
472 current_offset += (self.materials.len() * material_size) as u32;
473 } else {
474 header.render_flags = M2Array::new(0, 0);
475 }
476
477 if !self.raw_data.bone_lookup_table.is_empty() {
479 header.bone_lookup_table =
480 M2Array::new(self.raw_data.bone_lookup_table.len() as u32, current_offset);
481
482 for &lookup in &self.raw_data.bone_lookup_table {
483 data_section.extend_from_slice(&lookup.to_le_bytes());
484 }
485
486 current_offset +=
487 (self.raw_data.bone_lookup_table.len() * std::mem::size_of::<u16>()) as u32;
488 } else {
489 header.bone_lookup_table = M2Array::new(0, 0);
490 }
491
492 if !self.raw_data.texture_lookup_table.is_empty() {
494 header.texture_lookup_table = M2Array::new(
495 self.raw_data.texture_lookup_table.len() as u32,
496 current_offset,
497 );
498
499 for &lookup in &self.raw_data.texture_lookup_table {
500 data_section.extend_from_slice(&lookup.to_le_bytes());
501 }
502
503 current_offset +=
504 (self.raw_data.texture_lookup_table.len() * std::mem::size_of::<u16>()) as u32;
505 } else {
506 header.texture_lookup_table = M2Array::new(0, 0);
507 }
508
509 if !self.raw_data.texture_units.is_empty() {
511 header.texture_units =
512 M2Array::new(self.raw_data.texture_units.len() as u32, current_offset);
513
514 for &unit in &self.raw_data.texture_units {
515 data_section.extend_from_slice(&unit.to_le_bytes());
516 }
517
518 current_offset +=
519 (self.raw_data.texture_units.len() * std::mem::size_of::<u16>()) as u32;
520 } else {
521 header.texture_units = M2Array::new(0, 0);
522 }
523
524 if !self.raw_data.transparency_lookup_table.is_empty() {
526 header.transparency_lookup_table = M2Array::new(
527 self.raw_data.transparency_lookup_table.len() as u32,
528 current_offset,
529 );
530
531 for &lookup in &self.raw_data.transparency_lookup_table {
532 data_section.extend_from_slice(&lookup.to_le_bytes());
533 }
534
535 current_offset +=
536 (self.raw_data.transparency_lookup_table.len() * std::mem::size_of::<u16>()) as u32;
537 } else {
538 header.transparency_lookup_table = M2Array::new(0, 0);
539 }
540
541 if !self.raw_data.texture_animation_lookup.is_empty() {
543 header.texture_animation_lookup = M2Array::new(
544 self.raw_data.texture_animation_lookup.len() as u32,
545 current_offset,
546 );
547
548 for &lookup in &self.raw_data.texture_animation_lookup {
549 data_section.extend_from_slice(&lookup.to_le_bytes());
550 }
551
552 } else {
555 header.texture_animation_lookup = M2Array::new(0, 0);
556 }
557
558 header.write(writer)?;
560 writer.write_all(&data_section)?;
561
562 Ok(())
563 }
564
565 pub fn convert(&self, target_version: M2Version) -> Result<Self> {
567 let source_version = self.header.version().ok_or(M2Error::ConversionError {
568 from: self.header.version,
569 to: target_version.to_header_version(),
570 reason: "Unknown source version".to_string(),
571 })?;
572
573 if source_version == target_version {
574 return Ok(self.clone());
575 }
576
577 let header = self.header.convert(target_version)?;
579
580 let vertices = self
582 .vertices
583 .iter()
584 .map(|v| v.convert(target_version))
585 .collect();
586
587 let textures = self
589 .textures
590 .iter()
591 .map(|t| t.convert(target_version))
592 .collect();
593
594 let bones = self
596 .bones
597 .iter()
598 .map(|b| b.convert(target_version))
599 .collect();
600
601 let materials = self
603 .materials
604 .iter()
605 .map(|m| m.convert(target_version))
606 .collect();
607
608 let mut new_model = self.clone();
610 new_model.header = header;
611 new_model.vertices = vertices;
612 new_model.textures = textures;
613 new_model.bones = bones;
614 new_model.materials = materials;
615
616 Ok(new_model)
617 }
618
619 fn calculate_header_size(&self) -> usize {
621 let version = self.header.version().unwrap_or(M2Version::Classic);
622
623 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 {
678 size += 2 * 4; }
680
681 if version >= M2Version::Legion {
682 size += 2 * 4; }
684
685 size
686 }
687
688 pub fn validate(&self) -> Result<()> {
690 if self.header.version().is_none() {
692 return Err(M2Error::UnsupportedVersion(self.header.version.to_string()));
693 }
694
695 if self.vertices.is_empty() {
697 return Err(M2Error::ValidationError(
698 "Model has no vertices".to_string(),
699 ));
700 }
701
702 for (i, bone) in self.bones.iter().enumerate() {
704 if bone.parent_bone >= 0 && bone.parent_bone as usize >= self.bones.len() {
706 return Err(M2Error::ValidationError(format!(
707 "Bone {} has invalid parent bone {}",
708 i, bone.parent_bone
709 )));
710 }
711 }
712
713 for (i, texture) in self.textures.iter().enumerate() {
715 if texture.filename.count > 0 && texture.filename.offset == 0 {
717 return Err(M2Error::ValidationError(format!(
718 "Texture {i} has invalid filename offset"
719 )));
720 }
721 }
722
723 for (i, _material) in self.materials.iter().enumerate() {
725 let _material_index = i; }
729
730 Ok(())
731 }
732}