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 attenuation_start = reader.read_f32_le()?;
714 let attenuation_end = reader.read_f32_le()?;
715
716 reader.seek(SeekFrom::Current(16))?;
719
720 let properties = match light_type {
722 WmoLightType::Spot => WmoLightProperties::Spot {
723 direction: Vec3 {
724 x: 0.0,
725 y: 0.0,
726 z: -1.0,
727 }, hotspot: 0.0,
729 falloff: 0.0,
730 },
731 WmoLightType::Directional => WmoLightProperties::Directional {
732 direction: Vec3 {
733 x: 0.0,
734 y: 0.0,
735 z: -1.0,
736 }, },
738 WmoLightType::Omni => WmoLightProperties::Omni,
739 WmoLightType::Ambient => WmoLightProperties::Ambient,
740 };
741
742 lights.push(WmoLight {
743 light_type,
744 position,
745 color,
746 intensity,
747 attenuation_start,
748 attenuation_end,
749 use_attenuation,
750 properties,
751 });
752 }
753
754 Ok(lights)
755 }
756
757 fn parse_doodad_names<R: Read + Seek>(
759 &self,
760 chunks: &HashMap<ChunkId, Chunk>,
761 reader: &mut R,
762 ) -> Result<Vec<String>> {
763 let modn_chunk = match chunks.get(&chunks::MODN) {
764 Some(chunk) => chunk,
765 None => return Ok(Vec::new()), };
767
768 let modn_data = modn_chunk.read_data(reader)?;
769 Ok(self.parse_string_list(&modn_data))
770 }
771
772 fn parse_doodad_defs<R: Read + Seek>(
774 &self,
775 chunks: &HashMap<ChunkId, Chunk>,
776 reader: &mut R,
777 _version: WmoVersion,
778 n_doodad_defs: u32,
779 ) -> Result<Vec<WmoDoodadDef>> {
780 let modd_chunk = match chunks.get(&chunks::MODD) {
781 Some(chunk) => chunk,
782 None => return Ok(Vec::new()), };
784
785 modd_chunk.seek_to_data(reader)?;
786
787 let actual_doodad_count = modd_chunk.header.size / 40;
790 if actual_doodad_count != n_doodad_defs {
791 warn!(
792 "MODD chunk size indicates {} doodads, but header says {}. Using chunk size.",
793 actual_doodad_count, n_doodad_defs
794 );
795 }
796
797 let mut doodads = Vec::with_capacity(actual_doodad_count as usize);
798
799 for _ in 0..actual_doodad_count {
800 let name_index_raw = reader.read_u32_le()?;
801 let name_offset = name_index_raw & 0x00FFFFFF;
803
804 let pos_x = reader.read_f32_le()?;
805 let pos_y = reader.read_f32_le()?;
806 let pos_z = reader.read_f32_le()?;
807
808 let quat_x = reader.read_f32_le()?;
809 let quat_y = reader.read_f32_le()?;
810 let quat_z = reader.read_f32_le()?;
811 let quat_w = reader.read_f32_le()?;
812
813 let scale = reader.read_f32_le()?;
814
815 let color_bytes = reader.read_u32_le()?;
816 let color = Color {
817 r: ((color_bytes >> 16) & 0xFF) as u8,
818 g: ((color_bytes >> 8) & 0xFF) as u8,
819 b: (color_bytes & 0xFF) as u8,
820 a: ((color_bytes >> 24) & 0xFF) as u8,
821 };
822
823 let set_index = 0; doodads.push(WmoDoodadDef {
828 name_offset,
829 position: Vec3 {
830 x: pos_x,
831 y: pos_y,
832 z: pos_z,
833 },
834 orientation: [quat_x, quat_y, quat_z, quat_w],
835 scale,
836 color,
837 set_index,
838 });
839 }
840
841 Ok(doodads)
842 }
843
844 fn parse_doodad_sets<R: Read + Seek>(
846 &self,
847 chunks: &HashMap<ChunkId, Chunk>,
848 reader: &mut R,
849 _doodad_names: Vec<String>,
850 n_doodad_sets: u32,
851 ) -> Result<Vec<WmoDoodadSet>> {
852 let mods_chunk = match chunks.get(&chunks::MODS) {
853 Some(chunk) => chunk,
854 None => return Ok(Vec::new()), };
856
857 mods_chunk.seek_to_data(reader)?;
858 let mut sets = Vec::with_capacity(n_doodad_sets as usize);
859
860 for _i in 0..n_doodad_sets {
861 let mut name_bytes = [0u8; 20];
863 reader.read_exact(&mut name_bytes)?;
864
865 let null_pos = name_bytes.iter().position(|&b| b == 0).unwrap_or(20);
867 let name = String::from_utf8_lossy(&name_bytes[0..null_pos]).to_string();
868
869 let start_doodad = reader.read_u32_le()?;
870 let n_doodads = reader.read_u32_le()?;
871
872 reader.seek(SeekFrom::Current(4))?;
874
875 sets.push(WmoDoodadSet {
876 name,
877 start_doodad,
878 n_doodads,
879 });
880 }
881
882 Ok(sets)
883 }
884
885 fn parse_skybox<R: Read + Seek>(
887 &self,
888 chunks: &HashMap<ChunkId, Chunk>,
889 reader: &mut R,
890 version: WmoVersion,
891 header: &WmoHeader,
892 ) -> Result<Option<String>> {
893 if !version.supports_feature(WmoFeature::SkyboxReferences) {
895 return Ok(None);
896 }
897
898 if !header.flags.contains(WmoFlags::HAS_SKYBOX) {
900 return Ok(None);
901 }
902
903 let mosb_chunk = match chunks.get(&chunks::MOSB) {
904 Some(chunk) => chunk,
905 None => return Ok(None), };
907
908 let mosb_data = mosb_chunk.read_data(reader)?;
909
910 let null_pos = mosb_data
912 .iter()
913 .position(|&b| b == 0)
914 .unwrap_or(mosb_data.len());
915 let skybox_path = String::from_utf8_lossy(&mosb_data[0..null_pos]).to_string();
916
917 if skybox_path.is_empty() {
918 Ok(None)
919 } else {
920 Ok(Some(skybox_path))
921 }
922 }
923
924 fn parse_convex_volume_planes<R: Read + Seek>(
926 &self,
927 chunks: &HashMap<ChunkId, Chunk>,
928 reader: &mut R,
929 version: WmoVersion,
930 ) -> Result<Option<WmoConvexVolumePlanes>> {
931 if !version.supports_feature(WmoFeature::ConvexVolumePlanes) {
933 return Ok(None);
934 }
935
936 let mcvp_chunk = match chunks.get(&chunks::MCVP) {
937 Some(chunk) => chunk,
938 None => return Ok(None), };
940
941 let mcvp_data = mcvp_chunk.read_data(reader)?;
942
943 const PLANE_SIZE: usize = 20;
945
946 if mcvp_data.len() % PLANE_SIZE != 0 {
947 warn!(
948 "MCVP chunk size {} is not a multiple of plane size {}",
949 mcvp_data.len(),
950 PLANE_SIZE
951 );
952 return Ok(None);
953 }
954
955 let plane_count = mcvp_data.len() / PLANE_SIZE;
956 let mut planes = Vec::with_capacity(plane_count);
957
958 let mut cursor = std::io::Cursor::new(&mcvp_data);
959
960 for _ in 0..plane_count {
961 let normal = Vec3 {
962 x: cursor.read_f32_le()?,
963 y: cursor.read_f32_le()?,
964 z: cursor.read_f32_le()?,
965 };
966 let distance = cursor.read_f32_le()?;
967 let flags = cursor.read_u32_le()?;
968
969 planes.push(WmoConvexVolumePlane {
970 normal,
971 distance,
972 flags,
973 });
974 }
975
976 Ok(Some(WmoConvexVolumePlanes { planes }))
977 }
978
979 fn parse_string_list(&self, buffer: &[u8]) -> Vec<String> {
981 let mut strings = Vec::new();
982 let mut start = 0;
983
984 for i in 0..buffer.len() {
985 if buffer[i] == 0 {
986 if i > start
987 && let Ok(s) = std::str::from_utf8(&buffer[start..i])
988 {
989 strings.push(s.to_string());
990 }
991 start = i + 1;
992 }
993 }
994
995 strings
996 }
997
998 fn get_string_at_offset(&self, buffer: &[u8], offset: usize) -> String {
1000 if offset >= buffer.len() {
1001 return String::new();
1002 }
1003
1004 let end = buffer[offset..]
1006 .iter()
1007 .position(|&b| b == 0)
1008 .map(|pos| offset + pos)
1009 .unwrap_or(buffer.len());
1010
1011 if let Ok(s) = std::str::from_utf8(&buffer[offset..end]) {
1012 s.to_string()
1013 } else {
1014 String::new()
1015 }
1016 }
1017
1018 fn calculate_global_bounding_box(&self, groups: &[WmoGroupInfo]) -> BoundingBox {
1020 if groups.is_empty() {
1021 return BoundingBox {
1022 min: Vec3 {
1023 x: 0.0,
1024 y: 0.0,
1025 z: 0.0,
1026 },
1027 max: Vec3 {
1028 x: 0.0,
1029 y: 0.0,
1030 z: 0.0,
1031 },
1032 };
1033 }
1034
1035 let mut min_x = f32::MAX;
1036 let mut min_y = f32::MAX;
1037 let mut min_z = f32::MAX;
1038 let mut max_x = f32::MIN;
1039 let mut max_y = f32::MIN;
1040 let mut max_z = f32::MIN;
1041
1042 for group in groups {
1043 min_x = min_x.min(group.bounding_box.min.x);
1044 min_y = min_y.min(group.bounding_box.min.y);
1045 min_z = min_z.min(group.bounding_box.min.z);
1046 max_x = max_x.max(group.bounding_box.max.x);
1047 max_y = max_y.max(group.bounding_box.max.y);
1048 max_z = max_z.max(group.bounding_box.max.z);
1049 }
1050
1051 BoundingBox {
1052 min: Vec3 {
1053 x: min_x,
1054 y: min_y,
1055 z: min_z,
1056 },
1057 max: Vec3 {
1058 x: max_x,
1059 y: max_y,
1060 z: max_z,
1061 },
1062 }
1063 }
1064}