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 = self.parse_textures(&chunks, reader)?;
126 debug!("Found {} textures", textures.len());
127
128 let materials = self.parse_materials(&chunks, reader, version, 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 bounding_box = self.calculate_global_bounding_box(&groups);
171
172 Ok(WmoRoot {
173 version,
174 materials,
175 groups,
176 portals,
177 portal_references,
178 visible_block_lists,
179 lights,
180 doodad_defs,
181 doodad_sets,
182 bounding_box,
183 textures,
184 header,
185 skybox,
186 })
187 }
188
189 fn read_chunks<R: Read + Seek>(&self, reader: &mut R) -> Result<HashMap<ChunkId, Chunk>> {
191 let mut chunks = HashMap::new();
192 let start_pos = reader.stream_position()?;
193 reader.seek(SeekFrom::Start(start_pos))?;
194
195 loop {
197 match ChunkHeader::read(reader) {
198 Ok(header) => {
199 trace!("Found chunk: {}, size: {}", header.id, header.size);
200 let data_pos = reader.stream_position()?;
201
202 chunks.insert(
203 header.id,
204 Chunk {
205 header,
206 data_position: data_pos,
207 },
208 );
209
210 reader.seek(SeekFrom::Current(header.size as i64))?;
211 }
212 Err(WmoError::UnexpectedEof) => {
213 break;
215 }
216 Err(e) => return Err(e),
217 }
218 }
219
220 reader.seek(SeekFrom::Start(start_pos))?;
222
223 Ok(chunks)
224 }
225
226 fn parse_version<R: Read + Seek>(
228 &self,
229 chunks: &HashMap<ChunkId, Chunk>,
230 reader: &mut R,
231 ) -> Result<WmoVersion> {
232 let version_chunk = chunks
233 .get(&chunks::MVER)
234 .ok_or_else(|| WmoError::MissingRequiredChunk("MVER".to_string()))?;
235
236 version_chunk.seek_to_data(reader)?;
237 let raw_version = reader.read_u32_le()?;
238
239 WmoVersion::from_raw(raw_version).ok_or(WmoError::InvalidVersion(raw_version))
240 }
241
242 fn parse_header<R: Read + Seek>(
244 &self,
245 chunks: &HashMap<ChunkId, Chunk>,
246 reader: &mut R,
247 _version: WmoVersion,
248 ) -> Result<WmoHeader> {
249 let header_chunk = chunks
250 .get(&chunks::MOHD)
251 .ok_or_else(|| WmoError::MissingRequiredChunk("MOHD".to_string()))?;
252
253 header_chunk.seek_to_data(reader)?;
254
255 let n_materials = reader.read_u32_le()?;
257 let n_groups = reader.read_u32_le()?;
258 let n_portals = reader.read_u32_le()?;
259 let n_lights = reader.read_u32_le()?;
260 let n_doodad_names = reader.read_u32_le()?;
261 let n_doodad_defs = reader.read_u32_le()?;
262 let n_doodad_sets = reader.read_u32_le()?;
263 let color_bytes = reader.read_u32_le()?;
264 let flags = WmoFlags::from_bits_truncate(reader.read_u32_le()?);
265
266 reader.seek(SeekFrom::Current(8))?; let ambient_color = Color {
271 r: ((color_bytes >> 16) & 0xFF) as u8,
272 g: ((color_bytes >> 8) & 0xFF) as u8,
273 b: (color_bytes & 0xFF) as u8,
274 a: ((color_bytes >> 24) & 0xFF) as u8,
275 };
276
277 Ok(WmoHeader {
278 n_materials,
279 n_groups,
280 n_portals,
281 n_lights,
282 n_doodad_names,
283 n_doodad_defs,
284 n_doodad_sets,
285 flags,
286 ambient_color,
287 })
288 }
289
290 fn parse_textures<R: Read + Seek>(
292 &self,
293 chunks: &HashMap<ChunkId, Chunk>,
294 reader: &mut R,
295 ) -> Result<Vec<String>> {
296 let motx_chunk = match chunks.get(&chunks::MOTX) {
297 Some(chunk) => chunk,
298 None => return Ok(Vec::new()), };
300
301 let motx_data = motx_chunk.read_data(reader)?;
302 let mut textures = Vec::new();
303
304 let mut current_string = String::new();
306
307 for &byte in &motx_data {
308 if byte == 0 {
309 if !current_string.is_empty() {
311 textures.push(current_string);
312 current_string = String::new();
313 }
314 } else {
315 current_string.push(byte as char);
317 }
318 }
319
320 Ok(textures)
321 }
322
323 fn parse_materials<R: Read + Seek>(
325 &self,
326 chunks: &HashMap<ChunkId, Chunk>,
327 reader: &mut R,
328 version: WmoVersion,
329 n_materials: u32,
330 ) -> Result<Vec<WmoMaterial>> {
331 let momt_chunk = match chunks.get(&chunks::MOMT) {
332 Some(chunk) => chunk,
333 None => return Ok(Vec::new()), };
335
336 momt_chunk.seek_to_data(reader)?;
337 let mut materials = Vec::with_capacity(n_materials as usize);
338
339 let material_size = if version >= WmoVersion::Mop { 64 } else { 40 };
341
342 for _ in 0..n_materials {
343 let flags = WmoMaterialFlags::from_bits_truncate(reader.read_u32_le()?);
344 let shader = reader.read_u32_le()?;
345 let blend_mode = reader.read_u32_le()?;
346 let texture1 = reader.read_u32_le()?;
347
348 let emissive_color = Color {
349 r: reader.read_u8()?,
350 g: reader.read_u8()?,
351 b: reader.read_u8()?,
352 a: reader.read_u8()?,
353 };
354
355 let sidn_color = Color {
356 r: reader.read_u8()?,
357 g: reader.read_u8()?,
358 b: reader.read_u8()?,
359 a: reader.read_u8()?,
360 };
361
362 let framebuffer_blend = Color {
363 r: reader.read_u8()?,
364 g: reader.read_u8()?,
365 b: reader.read_u8()?,
366 a: reader.read_u8()?,
367 };
368
369 let texture2 = reader.read_u32_le()?;
370
371 let diffuse_color = Color {
372 r: reader.read_u8()?,
373 g: reader.read_u8()?,
374 b: reader.read_u8()?,
375 a: reader.read_u8()?,
376 };
377
378 let ground_type = reader.read_u32_le()?;
379
380 let remaining_size = material_size - 40;
382 if remaining_size > 0 {
383 reader.seek(SeekFrom::Current(remaining_size as i64))?;
384 }
385
386 materials.push(WmoMaterial {
387 flags,
388 shader,
389 blend_mode,
390 texture1,
391 emissive_color,
392 sidn_color,
393 framebuffer_blend,
394 texture2,
395 diffuse_color,
396 ground_type,
397 });
398 }
399
400 Ok(materials)
401 }
402
403 fn parse_group_info<R: Read + Seek>(
405 &self,
406 chunks: &HashMap<ChunkId, Chunk>,
407 reader: &mut R,
408 _version: WmoVersion,
409 n_groups: u32,
410 ) -> Result<Vec<WmoGroupInfo>> {
411 let mogn_chunk = match chunks.get(&chunks::MOGN) {
413 Some(chunk) => chunk,
414 None => return Ok(Vec::new()), };
416
417 let group_names_data = mogn_chunk.read_data(reader)?;
418
419 let mogi_chunk = match chunks.get(&chunks::MOGI) {
421 Some(chunk) => chunk,
422 None => return Ok(Vec::new()), };
424
425 mogi_chunk.seek_to_data(reader)?;
426 let mut groups = Vec::with_capacity(n_groups as usize);
427
428 for i in 0..n_groups {
429 let flags = WmoGroupFlags::from_bits_truncate(reader.read_u32_le()?);
430
431 let min_x = reader.read_f32_le()?;
432 let min_y = reader.read_f32_le()?;
433 let min_z = reader.read_f32_le()?;
434 let max_x = reader.read_f32_le()?;
435 let max_y = reader.read_f32_le()?;
436 let max_z = reader.read_f32_le()?;
437
438 let name_offset = reader.read_u32_le()?;
439
440 let name = if name_offset < group_names_data.len() as u32 {
442 let name = self.get_string_at_offset(&group_names_data, name_offset as usize);
443 if name.is_empty() {
444 format!("Group_{i}")
445 } else {
446 name
447 }
448 } else {
449 format!("Group_{i}")
450 };
451
452 groups.push(WmoGroupInfo {
453 flags,
454 bounding_box: BoundingBox {
455 min: Vec3 {
456 x: min_x,
457 y: min_y,
458 z: min_z,
459 },
460 max: Vec3 {
461 x: max_x,
462 y: max_y,
463 z: max_z,
464 },
465 },
466 name,
467 });
468 }
469
470 Ok(groups)
471 }
472
473 fn parse_portals<R: Read + Seek>(
475 &self,
476 chunks: &HashMap<ChunkId, Chunk>,
477 reader: &mut R,
478 n_portals: u32,
479 ) -> Result<Vec<WmoPortal>> {
480 let mopv_chunk = match chunks.get(&chunks::MOPV) {
482 Some(chunk) => chunk,
483 None => return Ok(Vec::new()), };
485
486 let mopv_data = mopv_chunk.read_data(reader)?;
487 let n_vertices = mopv_data.len() / 12; let mut portal_vertices = Vec::with_capacity(n_vertices);
489
490 for i in 0..n_vertices {
491 let offset = i * 12;
492
493 let x = f32::from_le_bytes([
495 mopv_data[offset],
496 mopv_data[offset + 1],
497 mopv_data[offset + 2],
498 mopv_data[offset + 3],
499 ]);
500
501 let y = f32::from_le_bytes([
502 mopv_data[offset + 4],
503 mopv_data[offset + 5],
504 mopv_data[offset + 6],
505 mopv_data[offset + 7],
506 ]);
507
508 let z = f32::from_le_bytes([
509 mopv_data[offset + 8],
510 mopv_data[offset + 9],
511 mopv_data[offset + 10],
512 mopv_data[offset + 11],
513 ]);
514
515 portal_vertices.push(Vec3 { x, y, z });
516 }
517
518 let mopt_chunk = match chunks.get(&chunks::MOPT) {
520 Some(chunk) => chunk,
521 None => return Ok(Vec::new()), };
523
524 mopt_chunk.seek_to_data(reader)?;
525 let mut portals = Vec::with_capacity(n_portals as usize);
526
527 for _ in 0..n_portals {
528 let vertex_index = reader.read_u16_le()? as usize;
529 let n_vertices = reader.read_u16_le()? as usize;
530
531 let normal_x = reader.read_f32_le()?;
532 let normal_y = reader.read_f32_le()?;
533 let normal_z = reader.read_f32_le()?;
534
535 reader.seek(SeekFrom::Current(4))?;
537
538 let mut vertices = Vec::with_capacity(n_vertices);
540 for i in 0..n_vertices {
541 let vertex_idx = vertex_index + i;
542 if vertex_idx < portal_vertices.len() {
543 vertices.push(portal_vertices[vertex_idx]);
544 } else {
545 warn!("Portal vertex index out of bounds: {}", vertex_idx);
546 }
547 }
548
549 portals.push(WmoPortal {
550 vertices,
551 normal: Vec3 {
552 x: normal_x,
553 y: normal_y,
554 z: normal_z,
555 },
556 });
557 }
558
559 Ok(portals)
560 }
561
562 fn parse_portal_references<R: Read + Seek>(
564 &self,
565 chunks: &HashMap<ChunkId, Chunk>,
566 reader: &mut R,
567 ) -> Result<Vec<WmoPortalReference>> {
568 let mopr_chunk = match chunks.get(&chunks::MOPR) {
569 Some(chunk) => chunk,
570 None => return Ok(Vec::new()), };
572
573 let mopr_data = mopr_chunk.read_data(reader)?;
574 let n_refs = mopr_data.len() / 8; let mut refs = Vec::with_capacity(n_refs);
576
577 for i in 0..n_refs {
578 let offset = i * 8;
579
580 let portal_index = u16::from_le_bytes([mopr_data[offset], mopr_data[offset + 1]]);
581
582 let group_index = u16::from_le_bytes([mopr_data[offset + 2], mopr_data[offset + 3]]);
583
584 let side = u16::from_le_bytes([mopr_data[offset + 4], mopr_data[offset + 5]]);
585
586 refs.push(WmoPortalReference {
589 portal_index,
590 group_index,
591 side,
592 });
593 }
594
595 Ok(refs)
596 }
597
598 fn parse_visible_block_lists<R: Read + Seek>(
600 &self,
601 chunks: &HashMap<ChunkId, Chunk>,
602 reader: &mut R,
603 ) -> Result<Vec<Vec<u16>>> {
604 let movv_chunk = match chunks.get(&chunks::MOVV) {
606 Some(chunk) => chunk,
607 None => return Ok(Vec::new()), };
609
610 let movv_data = movv_chunk.read_data(reader)?;
611 let n_entries = movv_data.len() / 4; let mut offsets = Vec::with_capacity(n_entries);
613
614 for i in 0..n_entries {
615 let offset = u32::from_le_bytes([
616 movv_data[i * 4],
617 movv_data[i * 4 + 1],
618 movv_data[i * 4 + 2],
619 movv_data[i * 4 + 3],
620 ]);
621
622 offsets.push(offset);
623 }
624
625 let movb_chunk = match chunks.get(&chunks::MOVB) {
627 Some(chunk) => chunk,
628 None => return Ok(Vec::new()), };
630
631 let movb_data = movb_chunk.read_data(reader)?;
632 let mut visible_lists = Vec::with_capacity(offsets.len());
633
634 for &offset in &offsets {
635 let mut index = offset as usize;
636 let mut list = Vec::new();
637
638 while index + 1 < movb_data.len() {
640 let value = u16::from_le_bytes([movb_data[index], movb_data[index + 1]]);
641
642 if value == 0xFFFF {
643 break;
645 }
646
647 list.push(value);
648 index += 2;
649 }
650
651 visible_lists.push(list);
652 }
653
654 Ok(visible_lists)
655 }
656
657 fn parse_lights<R: Read + Seek>(
659 &self,
660 chunks: &HashMap<ChunkId, Chunk>,
661 reader: &mut R,
662 _version: WmoVersion,
663 n_lights: u32,
664 ) -> Result<Vec<WmoLight>> {
665 let molt_chunk = match chunks.get(&chunks::MOLT) {
666 Some(chunk) => chunk,
667 None => return Ok(Vec::new()), };
669
670 molt_chunk.seek_to_data(reader)?;
671 let mut lights = Vec::with_capacity(n_lights as usize);
672
673 for _ in 0..n_lights {
674 let light_type_raw = reader.read_u8()?;
675 let light_type = WmoLightType::from_raw(light_type_raw).ok_or_else(|| {
676 WmoError::InvalidFormat(format!("Invalid light type: {light_type_raw}"))
677 })?;
678
679 let use_attenuation = reader.read_u8()? != 0;
681 let _use_unknown1 = reader.read_u8()?; let _use_unknown2 = reader.read_u8()?; let color_bytes = reader.read_u32_le()?;
686 let color = Color {
687 b: (color_bytes & 0xFF) as u8,
688 g: ((color_bytes >> 8) & 0xFF) as u8,
689 r: ((color_bytes >> 16) & 0xFF) as u8,
690 a: ((color_bytes >> 24) & 0xFF) as u8,
691 };
692
693 let pos_x = reader.read_f32_le()?;
694 let pos_y = reader.read_f32_le()?;
695 let pos_z = reader.read_f32_le()?;
696
697 let position = Vec3 {
698 x: pos_x,
699 y: pos_y,
700 z: pos_z,
701 };
702
703 let intensity = reader.read_f32_le()?;
704
705 let attenuation_start = reader.read_f32_le()?;
706 let attenuation_end = reader.read_f32_le()?;
707
708 reader.seek(SeekFrom::Current(16))?;
711
712 let properties = match light_type {
714 WmoLightType::Spot => WmoLightProperties::Spot {
715 direction: Vec3 {
716 x: 0.0,
717 y: 0.0,
718 z: -1.0,
719 }, hotspot: 0.0,
721 falloff: 0.0,
722 },
723 WmoLightType::Directional => WmoLightProperties::Directional {
724 direction: Vec3 {
725 x: 0.0,
726 y: 0.0,
727 z: -1.0,
728 }, },
730 WmoLightType::Omni => WmoLightProperties::Omni,
731 WmoLightType::Ambient => WmoLightProperties::Ambient,
732 };
733
734 lights.push(WmoLight {
735 light_type,
736 position,
737 color,
738 intensity,
739 attenuation_start,
740 attenuation_end,
741 use_attenuation,
742 properties,
743 });
744 }
745
746 Ok(lights)
747 }
748
749 fn parse_doodad_names<R: Read + Seek>(
751 &self,
752 chunks: &HashMap<ChunkId, Chunk>,
753 reader: &mut R,
754 ) -> Result<Vec<String>> {
755 let modn_chunk = match chunks.get(&chunks::MODN) {
756 Some(chunk) => chunk,
757 None => return Ok(Vec::new()), };
759
760 let modn_data = modn_chunk.read_data(reader)?;
761 Ok(self.parse_string_list(&modn_data))
762 }
763
764 fn parse_doodad_defs<R: Read + Seek>(
766 &self,
767 chunks: &HashMap<ChunkId, Chunk>,
768 reader: &mut R,
769 _version: WmoVersion,
770 n_doodad_defs: u32,
771 ) -> Result<Vec<WmoDoodadDef>> {
772 let modd_chunk = match chunks.get(&chunks::MODD) {
773 Some(chunk) => chunk,
774 None => return Ok(Vec::new()), };
776
777 modd_chunk.seek_to_data(reader)?;
778
779 let actual_doodad_count = modd_chunk.header.size / 40;
782 if actual_doodad_count != n_doodad_defs {
783 warn!(
784 "MODD chunk size indicates {} doodads, but header says {}. Using chunk size.",
785 actual_doodad_count, n_doodad_defs
786 );
787 }
788
789 let mut doodads = Vec::with_capacity(actual_doodad_count as usize);
790
791 for _ in 0..actual_doodad_count {
792 let name_index_raw = reader.read_u32_le()?;
793 let name_offset = name_index_raw & 0x00FFFFFF;
795
796 let pos_x = reader.read_f32_le()?;
797 let pos_y = reader.read_f32_le()?;
798 let pos_z = reader.read_f32_le()?;
799
800 let quat_x = reader.read_f32_le()?;
801 let quat_y = reader.read_f32_le()?;
802 let quat_z = reader.read_f32_le()?;
803 let quat_w = reader.read_f32_le()?;
804
805 let scale = reader.read_f32_le()?;
806
807 let color_bytes = reader.read_u32_le()?;
808 let color = Color {
809 r: ((color_bytes >> 16) & 0xFF) as u8,
810 g: ((color_bytes >> 8) & 0xFF) as u8,
811 b: (color_bytes & 0xFF) as u8,
812 a: ((color_bytes >> 24) & 0xFF) as u8,
813 };
814
815 let set_index = 0; doodads.push(WmoDoodadDef {
820 name_offset,
821 position: Vec3 {
822 x: pos_x,
823 y: pos_y,
824 z: pos_z,
825 },
826 orientation: [quat_x, quat_y, quat_z, quat_w],
827 scale,
828 color,
829 set_index,
830 });
831 }
832
833 Ok(doodads)
834 }
835
836 fn parse_doodad_sets<R: Read + Seek>(
838 &self,
839 chunks: &HashMap<ChunkId, Chunk>,
840 reader: &mut R,
841 _doodad_names: Vec<String>,
842 n_doodad_sets: u32,
843 ) -> Result<Vec<WmoDoodadSet>> {
844 let mods_chunk = match chunks.get(&chunks::MODS) {
845 Some(chunk) => chunk,
846 None => return Ok(Vec::new()), };
848
849 mods_chunk.seek_to_data(reader)?;
850 let mut sets = Vec::with_capacity(n_doodad_sets as usize);
851
852 for _i in 0..n_doodad_sets {
853 let mut name_bytes = [0u8; 20];
855 reader.read_exact(&mut name_bytes)?;
856
857 let null_pos = name_bytes.iter().position(|&b| b == 0).unwrap_or(20);
859 let name = String::from_utf8_lossy(&name_bytes[0..null_pos]).to_string();
860
861 let start_doodad = reader.read_u32_le()?;
862 let n_doodads = reader.read_u32_le()?;
863
864 reader.seek(SeekFrom::Current(4))?;
866
867 sets.push(WmoDoodadSet {
868 name,
869 start_doodad,
870 n_doodads,
871 });
872 }
873
874 Ok(sets)
875 }
876
877 fn parse_skybox<R: Read + Seek>(
879 &self,
880 chunks: &HashMap<ChunkId, Chunk>,
881 reader: &mut R,
882 version: WmoVersion,
883 header: &WmoHeader,
884 ) -> Result<Option<String>> {
885 if !version.supports_feature(WmoFeature::SkyboxReferences) {
887 return Ok(None);
888 }
889
890 if !header.flags.contains(WmoFlags::HAS_SKYBOX) {
892 return Ok(None);
893 }
894
895 let mosb_chunk = match chunks.get(&chunks::MOSB) {
896 Some(chunk) => chunk,
897 None => return Ok(None), };
899
900 let mosb_data = mosb_chunk.read_data(reader)?;
901
902 let null_pos = mosb_data
904 .iter()
905 .position(|&b| b == 0)
906 .unwrap_or(mosb_data.len());
907 let skybox_path = String::from_utf8_lossy(&mosb_data[0..null_pos]).to_string();
908
909 if skybox_path.is_empty() {
910 Ok(None)
911 } else {
912 Ok(Some(skybox_path))
913 }
914 }
915
916 fn parse_string_list(&self, buffer: &[u8]) -> Vec<String> {
918 let mut strings = Vec::new();
919 let mut start = 0;
920
921 for i in 0..buffer.len() {
922 if buffer[i] == 0 {
923 if i > start {
924 if let Ok(s) = std::str::from_utf8(&buffer[start..i]) {
925 strings.push(s.to_string());
926 }
927 }
928 start = i + 1;
929 }
930 }
931
932 strings
933 }
934
935 fn get_string_at_offset(&self, buffer: &[u8], offset: usize) -> String {
937 if offset >= buffer.len() {
938 return String::new();
939 }
940
941 let end = buffer[offset..]
943 .iter()
944 .position(|&b| b == 0)
945 .map(|pos| offset + pos)
946 .unwrap_or(buffer.len());
947
948 if let Ok(s) = std::str::from_utf8(&buffer[offset..end]) {
949 s.to_string()
950 } else {
951 String::new()
952 }
953 }
954
955 fn calculate_global_bounding_box(&self, groups: &[WmoGroupInfo]) -> BoundingBox {
957 if groups.is_empty() {
958 return BoundingBox {
959 min: Vec3 {
960 x: 0.0,
961 y: 0.0,
962 z: 0.0,
963 },
964 max: Vec3 {
965 x: 0.0,
966 y: 0.0,
967 z: 0.0,
968 },
969 };
970 }
971
972 let mut min_x = f32::MAX;
973 let mut min_y = f32::MAX;
974 let mut min_z = f32::MAX;
975 let mut max_x = f32::MIN;
976 let mut max_y = f32::MIN;
977 let mut max_z = f32::MIN;
978
979 for group in groups {
980 min_x = min_x.min(group.bounding_box.min.x);
981 min_y = min_y.min(group.bounding_box.min.y);
982 min_z = min_z.min(group.bounding_box.min.z);
983 max_x = max_x.max(group.bounding_box.max.x);
984 max_y = max_y.max(group.bounding_box.max.y);
985 max_z = max_z.max(group.bounding_box.max.z);
986 }
987
988 BoundingBox {
989 min: Vec3 {
990 x: min_x,
991 y: min_y,
992 z: min_z,
993 },
994 max: Vec3 {
995 x: max_x,
996 y: max_y,
997 z: max_z,
998 },
999 }
1000 }
1001}