1use std::collections::HashMap;
2use std::io::{Read, Seek, SeekFrom};
3use tracing::{debug, trace, warn};
4
5use crate::chunk::{Chunk, ChunkHeader};
6use crate::error::{Result, WmoError};
7use crate::types::{BoundingBox, ChunkId, Color, Vec3};
8use crate::version::{WmoFeature, WmoVersion};
9use crate::wmo_group_types::WmoGroupFlags;
10use crate::wmo_types::*;
11
12#[allow(dead_code)]
14trait ReadLittleEndian: Read {
15 fn read_u8(&mut self) -> Result<u8> {
16 let mut buf = [0u8; 1];
17 self.read_exact(&mut buf)?;
18 Ok(buf[0])
19 }
20
21 fn read_u16_le(&mut self) -> Result<u16> {
22 let mut buf = [0u8; 2];
23 self.read_exact(&mut buf)?;
24 Ok(u16::from_le_bytes(buf))
25 }
26
27 fn read_u32_le(&mut self) -> Result<u32> {
28 let mut buf = [0u8; 4];
29 self.read_exact(&mut buf)?;
30 Ok(u32::from_le_bytes(buf))
31 }
32
33 fn read_i16_le(&mut self) -> Result<i16> {
34 let mut buf = [0u8; 2];
35 self.read_exact(&mut buf)?;
36 Ok(i16::from_le_bytes(buf))
37 }
38
39 fn read_i32_le(&mut self) -> Result<i32> {
40 let mut buf = [0u8; 4];
41 self.read_exact(&mut buf)?;
42 Ok(i32::from_le_bytes(buf))
43 }
44
45 fn read_f32_le(&mut self) -> Result<f32> {
46 let mut buf = [0u8; 4];
47 self.read_exact(&mut buf)?;
48 Ok(f32::from_le_bytes(buf))
49 }
50}
51
52impl<R: Read> ReadLittleEndian for R {}
53
54pub mod chunks {
56 use crate::types::ChunkId;
57
58 pub const MVER: ChunkId = ChunkId::from_str("MVER");
60 pub const MOHD: ChunkId = ChunkId::from_str("MOHD");
61 pub const MOTX: ChunkId = ChunkId::from_str("MOTX");
62 pub const MOMT: ChunkId = ChunkId::from_str("MOMT");
63 pub const MOGN: ChunkId = ChunkId::from_str("MOGN");
64 pub const MOGI: ChunkId = ChunkId::from_str("MOGI");
65 pub const MOSB: ChunkId = ChunkId::from_str("MOSB");
66 pub const MOPV: ChunkId = ChunkId::from_str("MOPV");
67 pub const MOPT: ChunkId = ChunkId::from_str("MOPT");
68 pub const MOPR: ChunkId = ChunkId::from_str("MOPR");
69 pub const MOVV: ChunkId = ChunkId::from_str("MOVV");
70 pub const MOVB: ChunkId = ChunkId::from_str("MOVB");
71 pub const MOLT: ChunkId = ChunkId::from_str("MOLT");
72 pub const MODS: ChunkId = ChunkId::from_str("MODS");
73 pub const MODN: ChunkId = ChunkId::from_str("MODN");
74 pub const MODD: ChunkId = ChunkId::from_str("MODD");
75 pub const MFOG: ChunkId = ChunkId::from_str("MFOG");
76 pub const MCVP: ChunkId = ChunkId::from_str("MCVP");
77
78 pub const MOGP: ChunkId = ChunkId::from_str("MOGP");
80 pub const MOPY: ChunkId = ChunkId::from_str("MOPY");
81 pub const MOVI: ChunkId = ChunkId::from_str("MOVI");
82 pub const MOVT: ChunkId = ChunkId::from_str("MOVT");
83 pub const MONR: ChunkId = ChunkId::from_str("MONR");
84 pub const MOTV: ChunkId = ChunkId::from_str("MOTV");
85 pub const MOBA: ChunkId = ChunkId::from_str("MOBA");
86 pub const MOLR: ChunkId = ChunkId::from_str("MOLR");
87 pub const MODR: ChunkId = ChunkId::from_str("MODR");
88 pub const MOBN: ChunkId = ChunkId::from_str("MOBN");
89 pub const MOBR: ChunkId = ChunkId::from_str("MOBR");
90 pub const MOCV: ChunkId = ChunkId::from_str("MOCV");
91 pub const MLIQ: ChunkId = ChunkId::from_str("MLIQ");
92 pub const MORI: ChunkId = ChunkId::from_str("MORI");
93 pub const MORB: ChunkId = ChunkId::from_str("MORB");
94}
95
96pub struct WmoParser;
98
99impl Default for WmoParser {
100 fn default() -> Self {
101 Self::new()
102 }
103}
104
105impl WmoParser {
106 pub fn new() -> Self {
108 Self
109 }
110
111 pub fn parse_root<R: Read + Seek>(&self, reader: &mut R) -> Result<WmoRoot> {
113 let chunks = self.read_chunks(reader)?;
115
116 let version = self.parse_version(&chunks, reader)?;
118 debug!("WMO version: {:?}", version);
119
120 let header = self.parse_header(&chunks, reader, version)?;
122 debug!("WMO header: {:?}", header);
123
124 let (textures, texture_offset_index_map) = self.parse_textures(&chunks, reader)?;
126 debug!("Found {} textures", textures.len());
127
128 let materials = self.parse_materials(&chunks, reader, header.n_materials)?;
130 debug!("Found {} materials", materials.len());
131
132 let groups = self.parse_group_info(&chunks, reader, version, header.n_groups)?;
134 debug!("Found {} groups", groups.len());
135
136 let portals = self.parse_portals(&chunks, reader, header.n_portals)?;
138 debug!("Found {} portals", portals.len());
139
140 let portal_references = self.parse_portal_references(&chunks, reader)?;
142 debug!("Found {} portal references", portal_references.len());
143
144 let visible_block_lists = self.parse_visible_block_lists(&chunks, reader)?;
146 debug!("Found {} visible block lists", visible_block_lists.len());
147
148 let lights = self.parse_lights(&chunks, reader, version, header.n_lights)?;
150 debug!("Found {} lights", lights.len());
151
152 let doodad_names = self.parse_doodad_names(&chunks, reader)?;
154 debug!("Found {} doodad names", doodad_names.len());
155
156 let doodad_defs = self.parse_doodad_defs(&chunks, reader, version, header.n_doodad_defs)?;
158 debug!("Found {} doodad definitions", doodad_defs.len());
159
160 let doodad_sets =
162 self.parse_doodad_sets(&chunks, reader, doodad_names, header.n_doodad_sets)?;
163 debug!("Found {} doodad sets", doodad_sets.len());
164
165 let skybox = self.parse_skybox(&chunks, reader, version, &header)?;
167 debug!("Skybox: {:?}", skybox);
168
169 let convex_volume_planes = self.parse_convex_volume_planes(&chunks, reader, version)?;
171 if let Some(ref cvp) = convex_volume_planes {
172 debug!("Found {} convex volume planes", cvp.planes.len());
173 }
174
175 let bounding_box = self.calculate_global_bounding_box(&groups);
177
178 Ok(WmoRoot {
179 version,
180 materials,
181 groups,
182 portals,
183 portal_references,
184 visible_block_lists,
185 lights,
186 doodad_defs,
187 doodad_sets,
188 bounding_box,
189 textures,
190 texture_offset_index_map,
191 header,
192 skybox,
193 convex_volume_planes,
194 })
195 }
196
197 fn read_chunks<R: Read + Seek>(&self, reader: &mut R) -> Result<HashMap<ChunkId, Chunk>> {
199 let mut chunks = HashMap::new();
200 let start_pos = reader.stream_position()?;
201 reader.seek(SeekFrom::Start(start_pos))?;
202
203 loop {
205 match ChunkHeader::read(reader) {
206 Ok(header) => {
207 trace!("Found chunk: {}, size: {}", header.id, header.size);
208 let data_pos = reader.stream_position()?;
209
210 chunks.insert(
211 header.id,
212 Chunk {
213 header,
214 data_position: data_pos,
215 },
216 );
217
218 reader.seek(SeekFrom::Current(header.size as i64))?;
219 }
220 Err(WmoError::UnexpectedEof) => {
221 break;
223 }
224 Err(e) => return Err(e),
225 }
226 }
227
228 reader.seek(SeekFrom::Start(start_pos))?;
230
231 Ok(chunks)
232 }
233
234 fn parse_version<R: Read + Seek>(
236 &self,
237 chunks: &HashMap<ChunkId, Chunk>,
238 reader: &mut R,
239 ) -> Result<WmoVersion> {
240 let version_chunk = chunks
241 .get(&chunks::MVER)
242 .ok_or_else(|| WmoError::MissingRequiredChunk("MVER".to_string()))?;
243
244 version_chunk.seek_to_data(reader)?;
245 let raw_version = reader.read_u32_le()?;
246
247 WmoVersion::from_raw(raw_version).ok_or(WmoError::InvalidVersion(raw_version))
248 }
249
250 fn parse_header<R: Read + Seek>(
252 &self,
253 chunks: &HashMap<ChunkId, Chunk>,
254 reader: &mut R,
255 _version: WmoVersion,
256 ) -> Result<WmoHeader> {
257 let header_chunk = chunks
258 .get(&chunks::MOHD)
259 .ok_or_else(|| WmoError::MissingRequiredChunk("MOHD".to_string()))?;
260
261 header_chunk.seek_to_data(reader)?;
262
263 let n_materials = reader.read_u32_le()?;
265 let n_groups = reader.read_u32_le()?;
266 let n_portals = reader.read_u32_le()?;
267 let n_lights = reader.read_u32_le()?;
268 let n_doodad_names = reader.read_u32_le()?;
269 let n_doodad_defs = reader.read_u32_le()?;
270 let n_doodad_sets = reader.read_u32_le()?;
271 let color_bytes = reader.read_u32_le()?;
272 let flags = WmoFlags::from_bits_truncate(reader.read_u32_le()?);
273
274 reader.seek(SeekFrom::Current(8))?; let ambient_color = Color {
279 r: ((color_bytes >> 16) & 0xFF) as u8,
280 g: ((color_bytes >> 8) & 0xFF) as u8,
281 b: (color_bytes & 0xFF) as u8,
282 a: ((color_bytes >> 24) & 0xFF) as u8,
283 };
284
285 Ok(WmoHeader {
286 n_materials,
287 n_groups,
288 n_portals,
289 n_lights,
290 n_doodad_names,
291 n_doodad_defs,
292 n_doodad_sets,
293 flags,
294 ambient_color,
295 })
296 }
297
298 fn parse_textures<R: Read + Seek>(
300 &self,
301 chunks: &HashMap<ChunkId, Chunk>,
302 reader: &mut R,
303 ) -> Result<(Vec<String>, HashMap<u32, u32>)> {
304 let motx_chunk = match chunks.get(&chunks::MOTX) {
305 Some(chunk) => chunk,
306 None => return Ok((Vec::new(), HashMap::new())), };
308
309 let motx_data = motx_chunk.read_data(reader)?;
310 let mut textures = Vec::new();
311 let mut offset_index_map = HashMap::new();
312
313 let mut current_string = String::new();
315
316 for (i, &byte) in motx_data.iter().enumerate() {
317 if byte == 0 {
318 if !current_string.is_empty() {
320 textures.push(current_string);
321 current_string = String::new();
322 }
323 } else {
324 if current_string.is_empty() {
325 let offset = i as u32;
326 let texture_index = textures.len() as u32;
327 offset_index_map.insert(offset, texture_index);
328 }
329 current_string.push(byte as char);
331 }
332 }
333
334 Ok((textures, offset_index_map))
335 }
336
337 fn parse_materials<R: Read + Seek>(
339 &self,
340 chunks: &HashMap<ChunkId, Chunk>,
341 reader: &mut R,
342 n_materials: u32,
343 ) -> Result<Vec<WmoMaterial>> {
344 let momt_chunk = match chunks.get(&chunks::MOMT) {
345 Some(chunk) => chunk,
346 None => return Ok(Vec::new()), };
348
349 momt_chunk.seek_to_data(reader)?;
350 let mut materials = Vec::with_capacity(n_materials as usize);
351
352 const MATERIAL_SIZE: usize = 64;
353
354 for _ in 0..n_materials {
355 let flags = WmoMaterialFlags::from_bits_truncate(reader.read_u32_le()?);
356 let shader = reader.read_u32_le()?;
357 let blend_mode = reader.read_u32_le()?;
358 let texture1 = reader.read_u32_le()?;
359
360 let emissive_color = Color {
361 r: reader.read_u8()?,
362 g: reader.read_u8()?,
363 b: reader.read_u8()?,
364 a: reader.read_u8()?,
365 };
366
367 let sidn_color = Color {
368 r: reader.read_u8()?,
369 g: reader.read_u8()?,
370 b: reader.read_u8()?,
371 a: reader.read_u8()?,
372 };
373
374 let framebuffer_blend = Color::default();
375
376 let texture2 = reader.read_u32_le()?;
377
378 let diffuse_color = Color {
379 r: reader.read_u8()?,
380 g: reader.read_u8()?,
381 b: reader.read_u8()?,
382 a: reader.read_u8()?,
383 };
384
385 let ground_type = reader.read_u32_le()?;
386
387 let ret = WmoMaterial {
388 flags,
389 shader,
390 blend_mode,
391 texture1,
392 emissive_color,
393 sidn_color,
394 framebuffer_blend,
395 texture2,
396 diffuse_color,
397 ground_type,
398 };
399
400 let remaining_size = MATERIAL_SIZE - 36;
401 if remaining_size > 0 {
402 reader.seek(SeekFrom::Current(remaining_size as i64))?;
403 }
404
405 materials.push(ret);
406 }
407
408 Ok(materials)
409 }
410
411 fn parse_group_info<R: Read + Seek>(
413 &self,
414 chunks: &HashMap<ChunkId, Chunk>,
415 reader: &mut R,
416 _version: WmoVersion,
417 n_groups: u32,
418 ) -> Result<Vec<WmoGroupInfo>> {
419 let mogn_chunk = match chunks.get(&chunks::MOGN) {
421 Some(chunk) => chunk,
422 None => return Ok(Vec::new()), };
424
425 let group_names_data = mogn_chunk.read_data(reader)?;
426
427 let mogi_chunk = match chunks.get(&chunks::MOGI) {
429 Some(chunk) => chunk,
430 None => return Ok(Vec::new()), };
432
433 mogi_chunk.seek_to_data(reader)?;
434 let mut groups = Vec::with_capacity(n_groups as usize);
435
436 for i in 0..n_groups {
437 let flags = WmoGroupFlags::from_bits_truncate(reader.read_u32_le()?);
438
439 let min_x = reader.read_f32_le()?;
440 let min_y = reader.read_f32_le()?;
441 let min_z = reader.read_f32_le()?;
442 let max_x = reader.read_f32_le()?;
443 let max_y = reader.read_f32_le()?;
444 let max_z = reader.read_f32_le()?;
445
446 let name_offset = reader.read_u32_le()?;
447
448 let name = if name_offset < group_names_data.len() as u32 {
450 let name = self.get_string_at_offset(&group_names_data, name_offset as usize);
451 if name.is_empty() {
452 format!("Group_{i}")
453 } else {
454 name
455 }
456 } else {
457 format!("Group_{i}")
458 };
459
460 groups.push(WmoGroupInfo {
461 flags,
462 bounding_box: BoundingBox {
463 min: Vec3 {
464 x: min_x,
465 y: min_y,
466 z: min_z,
467 },
468 max: Vec3 {
469 x: max_x,
470 y: max_y,
471 z: max_z,
472 },
473 },
474 name,
475 });
476 }
477
478 Ok(groups)
479 }
480
481 fn parse_portals<R: Read + Seek>(
483 &self,
484 chunks: &HashMap<ChunkId, Chunk>,
485 reader: &mut R,
486 n_portals: u32,
487 ) -> Result<Vec<WmoPortal>> {
488 let mopv_chunk = match chunks.get(&chunks::MOPV) {
490 Some(chunk) => chunk,
491 None => return Ok(Vec::new()), };
493
494 let mopv_data = mopv_chunk.read_data(reader)?;
495 let n_vertices = mopv_data.len() / 12; let mut portal_vertices = Vec::with_capacity(n_vertices);
497
498 for i in 0..n_vertices {
499 let offset = i * 12;
500
501 let x = f32::from_le_bytes([
503 mopv_data[offset],
504 mopv_data[offset + 1],
505 mopv_data[offset + 2],
506 mopv_data[offset + 3],
507 ]);
508
509 let y = f32::from_le_bytes([
510 mopv_data[offset + 4],
511 mopv_data[offset + 5],
512 mopv_data[offset + 6],
513 mopv_data[offset + 7],
514 ]);
515
516 let z = f32::from_le_bytes([
517 mopv_data[offset + 8],
518 mopv_data[offset + 9],
519 mopv_data[offset + 10],
520 mopv_data[offset + 11],
521 ]);
522
523 portal_vertices.push(Vec3 { x, y, z });
524 }
525
526 let mopt_chunk = match chunks.get(&chunks::MOPT) {
528 Some(chunk) => chunk,
529 None => return Ok(Vec::new()), };
531
532 mopt_chunk.seek_to_data(reader)?;
533 let mut portals = Vec::with_capacity(n_portals as usize);
534
535 for _ in 0..n_portals {
536 let vertex_index = reader.read_u16_le()? as usize;
537 let n_vertices = reader.read_u16_le()? as usize;
538
539 let normal_x = reader.read_f32_le()?;
540 let normal_y = reader.read_f32_le()?;
541 let normal_z = reader.read_f32_le()?;
542
543 reader.seek(SeekFrom::Current(4))?;
545
546 let mut vertices = Vec::with_capacity(n_vertices);
548 for i in 0..n_vertices {
549 let vertex_idx = vertex_index + i;
550 if vertex_idx < portal_vertices.len() {
551 vertices.push(portal_vertices[vertex_idx]);
552 } else {
553 warn!("Portal vertex index out of bounds: {}", vertex_idx);
554 }
555 }
556
557 portals.push(WmoPortal {
558 vertices,
559 normal: Vec3 {
560 x: normal_x,
561 y: normal_y,
562 z: normal_z,
563 },
564 });
565 }
566
567 Ok(portals)
568 }
569
570 fn parse_portal_references<R: Read + Seek>(
572 &self,
573 chunks: &HashMap<ChunkId, Chunk>,
574 reader: &mut R,
575 ) -> Result<Vec<WmoPortalReference>> {
576 let mopr_chunk = match chunks.get(&chunks::MOPR) {
577 Some(chunk) => chunk,
578 None => return Ok(Vec::new()), };
580
581 let mopr_data = mopr_chunk.read_data(reader)?;
582 let n_refs = mopr_data.len() / 8; let mut refs = Vec::with_capacity(n_refs);
584
585 for i in 0..n_refs {
586 let offset = i * 8;
587
588 let portal_index = u16::from_le_bytes([mopr_data[offset], mopr_data[offset + 1]]);
589
590 let group_index = u16::from_le_bytes([mopr_data[offset + 2], mopr_data[offset + 3]]);
591
592 let side = u16::from_le_bytes([mopr_data[offset + 4], mopr_data[offset + 5]]);
593
594 refs.push(WmoPortalReference {
597 portal_index,
598 group_index,
599 side,
600 });
601 }
602
603 Ok(refs)
604 }
605
606 fn parse_visible_block_lists<R: Read + Seek>(
608 &self,
609 chunks: &HashMap<ChunkId, Chunk>,
610 reader: &mut R,
611 ) -> Result<Vec<Vec<u16>>> {
612 let movv_chunk = match chunks.get(&chunks::MOVV) {
614 Some(chunk) => chunk,
615 None => return Ok(Vec::new()), };
617
618 let movv_data = movv_chunk.read_data(reader)?;
619 let n_entries = movv_data.len() / 4; let mut offsets = Vec::with_capacity(n_entries);
621
622 for i in 0..n_entries {
623 let offset = u32::from_le_bytes([
624 movv_data[i * 4],
625 movv_data[i * 4 + 1],
626 movv_data[i * 4 + 2],
627 movv_data[i * 4 + 3],
628 ]);
629
630 offsets.push(offset);
631 }
632
633 let movb_chunk = match chunks.get(&chunks::MOVB) {
635 Some(chunk) => chunk,
636 None => return Ok(Vec::new()), };
638
639 let movb_data = movb_chunk.read_data(reader)?;
640 let mut visible_lists = Vec::with_capacity(offsets.len());
641
642 for &offset in &offsets {
643 let mut index = offset as usize;
644 let mut list = Vec::new();
645
646 while index + 1 < movb_data.len() {
648 let value = u16::from_le_bytes([movb_data[index], movb_data[index + 1]]);
649
650 if value == 0xFFFF {
651 break;
653 }
654
655 list.push(value);
656 index += 2;
657 }
658
659 visible_lists.push(list);
660 }
661
662 Ok(visible_lists)
663 }
664
665 fn parse_lights<R: Read + Seek>(
667 &self,
668 chunks: &HashMap<ChunkId, Chunk>,
669 reader: &mut R,
670 _version: WmoVersion,
671 n_lights: u32,
672 ) -> Result<Vec<WmoLight>> {
673 let molt_chunk = match chunks.get(&chunks::MOLT) {
674 Some(chunk) => chunk,
675 None => return Ok(Vec::new()), };
677
678 molt_chunk.seek_to_data(reader)?;
679 let mut lights = Vec::with_capacity(n_lights as usize);
680
681 for _ in 0..n_lights {
682 let light_type_raw = reader.read_u8()?;
683 let light_type = WmoLightType::from_raw(light_type_raw).ok_or_else(|| {
684 WmoError::InvalidFormat(format!("Invalid light type: {light_type_raw}"))
685 })?;
686
687 let use_attenuation = reader.read_u8()? != 0;
689 let _use_unknown1 = reader.read_u8()?; let _use_unknown2 = reader.read_u8()?; let color_bytes = reader.read_u32_le()?;
694 let color = Color {
695 b: (color_bytes & 0xFF) as u8,
696 g: ((color_bytes >> 8) & 0xFF) as u8,
697 r: ((color_bytes >> 16) & 0xFF) as u8,
698 a: ((color_bytes >> 24) & 0xFF) as u8,
699 };
700
701 let pos_x = reader.read_f32_le()?;
702 let pos_y = reader.read_f32_le()?;
703 let pos_z = reader.read_f32_le()?;
704
705 let position = Vec3 {
706 x: pos_x,
707 y: pos_y,
708 z: pos_z,
709 };
710
711 let intensity = reader.read_f32_le()?;
712
713 let rot_x = reader.read_f32_le()?;
715 let rot_y = reader.read_f32_le()?;
716 let rot_z = reader.read_f32_le()?;
717 let rot_w = reader.read_f32_le()?;
718 let rotation = [rot_x, rot_y, rot_z, rot_w];
719
720 let attenuation_start = reader.read_f32_le()?;
722 let attenuation_end = reader.read_f32_le()?;
723
724 let properties = match light_type {
725 WmoLightType::Spot => WmoLightProperties::Spot {
726 direction: Vec3 {
727 x: 0.0,
728 y: 0.0,
729 z: -1.0,
730 }, hotspot: 0.0,
732 falloff: 0.0,
733 },
734 WmoLightType::Directional => WmoLightProperties::Directional {
735 direction: Vec3 {
736 x: 0.0,
737 y: 0.0,
738 z: -1.0,
739 }, },
741 WmoLightType::Omni => WmoLightProperties::Omni,
742 WmoLightType::Ambient => WmoLightProperties::Ambient,
743 };
744
745 lights.push(WmoLight {
746 light_type,
747 position,
748 color,
749 intensity,
750 rotation,
751 attenuation_start,
752 attenuation_end,
753 use_attenuation,
754 properties,
755 });
756 }
757
758 Ok(lights)
759 }
760
761 fn parse_doodad_names<R: Read + Seek>(
763 &self,
764 chunks: &HashMap<ChunkId, Chunk>,
765 reader: &mut R,
766 ) -> Result<Vec<String>> {
767 let modn_chunk = match chunks.get(&chunks::MODN) {
768 Some(chunk) => chunk,
769 None => return Ok(Vec::new()), };
771
772 let modn_data = modn_chunk.read_data(reader)?;
773 Ok(self.parse_string_list(&modn_data))
774 }
775
776 fn parse_doodad_defs<R: Read + Seek>(
778 &self,
779 chunks: &HashMap<ChunkId, Chunk>,
780 reader: &mut R,
781 _version: WmoVersion,
782 n_doodad_defs: u32,
783 ) -> Result<Vec<WmoDoodadDef>> {
784 let modd_chunk = match chunks.get(&chunks::MODD) {
785 Some(chunk) => chunk,
786 None => return Ok(Vec::new()), };
788
789 modd_chunk.seek_to_data(reader)?;
790
791 let actual_doodad_count = modd_chunk.header.size / 40;
794 if actual_doodad_count != n_doodad_defs {
795 warn!(
796 "MODD chunk size indicates {} doodads, but header says {}. Using chunk size.",
797 actual_doodad_count, n_doodad_defs
798 );
799 }
800
801 let mut doodads = Vec::with_capacity(actual_doodad_count as usize);
802
803 for _ in 0..actual_doodad_count {
804 let name_index_raw = reader.read_u32_le()?;
805 let name_offset = name_index_raw & 0x00FFFFFF;
807
808 let pos_x = reader.read_f32_le()?;
809 let pos_y = reader.read_f32_le()?;
810 let pos_z = reader.read_f32_le()?;
811
812 let quat_x = reader.read_f32_le()?;
813 let quat_y = reader.read_f32_le()?;
814 let quat_z = reader.read_f32_le()?;
815 let quat_w = reader.read_f32_le()?;
816
817 let scale = reader.read_f32_le()?;
818
819 let color_bytes = reader.read_u32_le()?;
820 let color = Color {
821 r: ((color_bytes >> 16) & 0xFF) as u8,
822 g: ((color_bytes >> 8) & 0xFF) as u8,
823 b: (color_bytes & 0xFF) as u8,
824 a: ((color_bytes >> 24) & 0xFF) as u8,
825 };
826
827 let set_index = 0; doodads.push(WmoDoodadDef {
832 name_offset,
833 position: Vec3 {
834 x: pos_x,
835 y: pos_y,
836 z: pos_z,
837 },
838 orientation: [quat_x, quat_y, quat_z, quat_w],
839 scale,
840 color,
841 set_index,
842 });
843 }
844
845 Ok(doodads)
846 }
847
848 fn parse_doodad_sets<R: Read + Seek>(
850 &self,
851 chunks: &HashMap<ChunkId, Chunk>,
852 reader: &mut R,
853 _doodad_names: Vec<String>,
854 n_doodad_sets: u32,
855 ) -> Result<Vec<WmoDoodadSet>> {
856 let mods_chunk = match chunks.get(&chunks::MODS) {
857 Some(chunk) => chunk,
858 None => return Ok(Vec::new()), };
860
861 mods_chunk.seek_to_data(reader)?;
862 let mut sets = Vec::with_capacity(n_doodad_sets as usize);
863
864 for _i in 0..n_doodad_sets {
865 let mut name_bytes = [0u8; 20];
867 reader.read_exact(&mut name_bytes)?;
868
869 let null_pos = name_bytes.iter().position(|&b| b == 0).unwrap_or(20);
871 let name = String::from_utf8_lossy(&name_bytes[0..null_pos]).to_string();
872
873 let start_doodad = reader.read_u32_le()?;
874 let n_doodads = reader.read_u32_le()?;
875
876 reader.seek(SeekFrom::Current(4))?;
878
879 sets.push(WmoDoodadSet {
880 name,
881 start_doodad,
882 n_doodads,
883 });
884 }
885
886 Ok(sets)
887 }
888
889 fn parse_skybox<R: Read + Seek>(
891 &self,
892 chunks: &HashMap<ChunkId, Chunk>,
893 reader: &mut R,
894 version: WmoVersion,
895 header: &WmoHeader,
896 ) -> Result<Option<String>> {
897 if !version.supports_feature(WmoFeature::SkyboxReferences) {
899 return Ok(None);
900 }
901
902 if !header.flags.contains(WmoFlags::HAS_SKYBOX) {
904 return Ok(None);
905 }
906
907 let mosb_chunk = match chunks.get(&chunks::MOSB) {
908 Some(chunk) => chunk,
909 None => return Ok(None), };
911
912 let mosb_data = mosb_chunk.read_data(reader)?;
913
914 let null_pos = mosb_data
916 .iter()
917 .position(|&b| b == 0)
918 .unwrap_or(mosb_data.len());
919 let skybox_path = String::from_utf8_lossy(&mosb_data[0..null_pos]).to_string();
920
921 if skybox_path.is_empty() {
922 Ok(None)
923 } else {
924 Ok(Some(skybox_path))
925 }
926 }
927
928 fn parse_convex_volume_planes<R: Read + Seek>(
930 &self,
931 chunks: &HashMap<ChunkId, Chunk>,
932 reader: &mut R,
933 version: WmoVersion,
934 ) -> Result<Option<WmoConvexVolumePlanes>> {
935 if !version.supports_feature(WmoFeature::ConvexVolumePlanes) {
937 return Ok(None);
938 }
939
940 let mcvp_chunk = match chunks.get(&chunks::MCVP) {
941 Some(chunk) => chunk,
942 None => return Ok(None), };
944
945 let mcvp_data = mcvp_chunk.read_data(reader)?;
946
947 const PLANE_SIZE: usize = 20;
949
950 if mcvp_data.len() % PLANE_SIZE != 0 {
951 warn!(
952 "MCVP chunk size {} is not a multiple of plane size {}",
953 mcvp_data.len(),
954 PLANE_SIZE
955 );
956 return Ok(None);
957 }
958
959 let plane_count = mcvp_data.len() / PLANE_SIZE;
960 let mut planes = Vec::with_capacity(plane_count);
961
962 let mut cursor = std::io::Cursor::new(&mcvp_data);
963
964 for _ in 0..plane_count {
965 let normal = Vec3 {
966 x: cursor.read_f32_le()?,
967 y: cursor.read_f32_le()?,
968 z: cursor.read_f32_le()?,
969 };
970 let distance = cursor.read_f32_le()?;
971 let flags = cursor.read_u32_le()?;
972
973 planes.push(WmoConvexVolumePlane {
974 normal,
975 distance,
976 flags,
977 });
978 }
979
980 Ok(Some(WmoConvexVolumePlanes { planes }))
981 }
982
983 fn parse_string_list(&self, buffer: &[u8]) -> Vec<String> {
985 let mut strings = Vec::new();
986 let mut start = 0;
987
988 for i in 0..buffer.len() {
989 if buffer[i] == 0 {
990 if i > start
991 && let Ok(s) = std::str::from_utf8(&buffer[start..i])
992 {
993 strings.push(s.to_string());
994 }
995 start = i + 1;
996 }
997 }
998
999 strings
1000 }
1001
1002 fn get_string_at_offset(&self, buffer: &[u8], offset: usize) -> String {
1004 if offset >= buffer.len() {
1005 return String::new();
1006 }
1007
1008 let end = buffer[offset..]
1010 .iter()
1011 .position(|&b| b == 0)
1012 .map(|pos| offset + pos)
1013 .unwrap_or(buffer.len());
1014
1015 if let Ok(s) = std::str::from_utf8(&buffer[offset..end]) {
1016 s.to_string()
1017 } else {
1018 String::new()
1019 }
1020 }
1021
1022 fn calculate_global_bounding_box(&self, groups: &[WmoGroupInfo]) -> BoundingBox {
1024 if groups.is_empty() {
1025 return BoundingBox {
1026 min: Vec3 {
1027 x: 0.0,
1028 y: 0.0,
1029 z: 0.0,
1030 },
1031 max: Vec3 {
1032 x: 0.0,
1033 y: 0.0,
1034 z: 0.0,
1035 },
1036 };
1037 }
1038
1039 let mut min_x = f32::MAX;
1040 let mut min_y = f32::MAX;
1041 let mut min_z = f32::MAX;
1042 let mut max_x = f32::MIN;
1043 let mut max_y = f32::MIN;
1044 let mut max_z = f32::MIN;
1045
1046 for group in groups {
1047 min_x = min_x.min(group.bounding_box.min.x);
1048 min_y = min_y.min(group.bounding_box.min.y);
1049 min_z = min_z.min(group.bounding_box.min.z);
1050 max_x = max_x.max(group.bounding_box.max.x);
1051 max_y = max_y.max(group.bounding_box.max.y);
1052 max_z = max_z.max(group.bounding_box.max.z);
1053 }
1054
1055 BoundingBox {
1056 min: Vec3 {
1057 x: min_x,
1058 y: min_y,
1059 z: min_z,
1060 },
1061 max: Vec3 {
1062 x: max_x,
1063 y: max_y,
1064 z: max_z,
1065 },
1066 }
1067 }
1068}