wow_wmo/
parser.rs

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/// Helper trait for reading little-endian values
13#[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
54/// WMO chunk identifiers
55pub mod chunks {
56    use crate::types::ChunkId;
57
58    // Root file chunks
59    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    // Group file chunks
79    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
96/// WMO parser for reading WMO files
97pub struct WmoParser;
98
99impl Default for WmoParser {
100    fn default() -> Self {
101        Self::new()
102    }
103}
104
105impl WmoParser {
106    /// Create a new WMO parser
107    pub fn new() -> Self {
108        Self
109    }
110
111    /// Parse a WMO root file
112    pub fn parse_root<R: Read + Seek>(&self, reader: &mut R) -> Result<WmoRoot> {
113        // Read all chunks in the file
114        let chunks = self.read_chunks(reader)?;
115
116        // Parse version
117        let version = self.parse_version(&chunks, reader)?;
118        debug!("WMO version: {:?}", version);
119
120        // Parse header
121        let header = self.parse_header(&chunks, reader, version)?;
122        debug!("WMO header: {:?}", header);
123
124        // Parse textures
125        let textures = self.parse_textures(&chunks, reader)?;
126        debug!("Found {} textures", textures.len());
127
128        // Parse materials
129        let materials = self.parse_materials(&chunks, reader, version, header.n_materials)?;
130        debug!("Found {} materials", materials.len());
131
132        // Parse group info
133        let groups = self.parse_group_info(&chunks, reader, version, header.n_groups)?;
134        debug!("Found {} groups", groups.len());
135
136        // Parse portals
137        let portals = self.parse_portals(&chunks, reader, header.n_portals)?;
138        debug!("Found {} portals", portals.len());
139
140        // Parse portal references
141        let portal_references = self.parse_portal_references(&chunks, reader)?;
142        debug!("Found {} portal references", portal_references.len());
143
144        // Parse visible block lists
145        let visible_block_lists = self.parse_visible_block_lists(&chunks, reader)?;
146        debug!("Found {} visible block lists", visible_block_lists.len());
147
148        // Parse lights
149        let lights = self.parse_lights(&chunks, reader, version, header.n_lights)?;
150        debug!("Found {} lights", lights.len());
151
152        // Parse doodad names
153        let doodad_names = self.parse_doodad_names(&chunks, reader)?;
154        debug!("Found {} doodad names", doodad_names.len());
155
156        // Parse doodad definitions
157        let doodad_defs = self.parse_doodad_defs(&chunks, reader, version, header.n_doodad_defs)?;
158        debug!("Found {} doodad definitions", doodad_defs.len());
159
160        // Parse doodad sets
161        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        // Parse skybox
166        let skybox = self.parse_skybox(&chunks, reader, version, &header)?;
167        debug!("Skybox: {:?}", skybox);
168
169        // Create global bounding box from all groups
170        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    /// Read all chunks in a file
190    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        // Read chunks until end of file
196        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                    // End of file reached
214                    break;
215                }
216                Err(e) => return Err(e),
217            }
218        }
219
220        // Reset position to start
221        reader.seek(SeekFrom::Start(start_pos))?;
222
223        Ok(chunks)
224    }
225
226    /// Parse the WMO version
227    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    /// Parse the WMO header
243    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        // Parse basic header fields
256        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        // Skip some fields (depending on version)
267        reader.seek(SeekFrom::Current(8))?; // Skip bounding box - we'll calculate this from groups
268
269        // Create color from bytes
270        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    /// Parse texture filenames
291    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()), // No textures
299        };
300
301        let motx_data = motx_chunk.read_data(reader)?;
302        let mut textures = Vec::new();
303
304        // MOTX chunk is a list of null-terminated strings
305        let mut current_string = String::new();
306
307        for &byte in &motx_data {
308            if byte == 0 {
309                // End of string
310                if !current_string.is_empty() {
311                    textures.push(current_string);
312                    current_string = String::new();
313                }
314            } else {
315                // Add to current string
316                current_string.push(byte as char);
317            }
318        }
319
320        Ok(textures)
321    }
322
323    /// Parse materials
324    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()), // No materials
334        };
335
336        momt_chunk.seek_to_data(reader)?;
337        let mut materials = Vec::with_capacity(n_materials as usize);
338
339        // Material size depends on version
340        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            // Skip remaining fields depending on version
381            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    /// Parse group info
404    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        // Parse group names first
412        let mogn_chunk = match chunks.get(&chunks::MOGN) {
413            Some(chunk) => chunk,
414            None => return Ok(Vec::new()), // No group names
415        };
416
417        let group_names_data = mogn_chunk.read_data(reader)?;
418
419        // Now parse group info
420        let mogi_chunk = match chunks.get(&chunks::MOGI) {
421            Some(chunk) => chunk,
422            None => return Ok(Vec::new()), // No group info
423        };
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            // Get group name from offset
441            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    /// Parse portals
474    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        // Parse portal vertices first
481        let mopv_chunk = match chunks.get(&chunks::MOPV) {
482            Some(chunk) => chunk,
483            None => return Ok(Vec::new()), // No portal vertices
484        };
485
486        let mopv_data = mopv_chunk.read_data(reader)?;
487        let n_vertices = mopv_data.len() / 12; // 3 floats per vertex (x, y, z)
488        let mut portal_vertices = Vec::with_capacity(n_vertices);
489
490        for i in 0..n_vertices {
491            let offset = i * 12;
492
493            // Use byteorder to read from the data buffer
494            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        // Now parse portal data
519        let mopt_chunk = match chunks.get(&chunks::MOPT) {
520            Some(chunk) => chunk,
521            None => return Ok(Vec::new()), // No portal data
522        };
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            // Skip plane distance
536            reader.seek(SeekFrom::Current(4))?;
537
538            // Get portal vertices
539            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    /// Parse portal references
563    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()), // No portal references
571        };
572
573        let mopr_data = mopr_chunk.read_data(reader)?;
574        let n_refs = mopr_data.len() / 8; // 4 u16 values per reference
575        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            // Skip unused field
587
588            refs.push(WmoPortalReference {
589                portal_index,
590                group_index,
591                side,
592            });
593        }
594
595        Ok(refs)
596    }
597
598    /// Parse visible block lists
599    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        // First get the visible block offsets
605        let movv_chunk = match chunks.get(&chunks::MOVV) {
606            Some(chunk) => chunk,
607            None => return Ok(Vec::new()), // No visible blocks
608        };
609
610        let movv_data = movv_chunk.read_data(reader)?;
611        let n_entries = movv_data.len() / 4; // u32 offset per entry
612        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        // Now get the visible block data
626        let movb_chunk = match chunks.get(&chunks::MOVB) {
627            Some(chunk) => chunk,
628            None => return Ok(Vec::new()), // No visible block data
629        };
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            // Read until we hit a 0xFFFF marker or end of data
639            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                    // End of list marker
644                    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    /// Parse lights
658    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()), // No lights
668        };
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            // Read 3 flag bytes
680            let use_attenuation = reader.read_u8()? != 0;
681            let _use_unknown1 = reader.read_u8()?; // Unknown flag
682            let _use_unknown2 = reader.read_u8()?; // Unknown flag
683
684            // Read BGRA color
685            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            // Skip the remaining 16 bytes (unknown radius values)
709            // These might be used for spot/directional lights but we'll keep it simple for now
710            reader.seek(SeekFrom::Current(16))?;
711
712            // For now, use simple properties without directional info
713            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                    }, // Default down
720                    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                    }, // Default down
729                },
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    /// Parse doodad names
750    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()), // No doodad names
758        };
759
760        let modn_data = modn_chunk.read_data(reader)?;
761        Ok(self.parse_string_list(&modn_data))
762    }
763
764    /// Parse doodad definitions
765    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()), // No doodad definitions
775        };
776
777        modd_chunk.seek_to_data(reader)?;
778
779        // Calculate actual number of doodad defs based on chunk size
780        // Each doodad def is 40 bytes
781        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            // Only the lower 24 bits are used for the name index
794            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            // The set_index field doesn't exist in Classic/TBC/WotLK
816            // It might be part of the upper bits of name_index_raw or added in later versions
817            let set_index = 0; // Default to 0 for now
818
819            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    /// Parse doodad sets
837    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()), // No doodad sets
847        };
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            // Read 20 bytes for the set name (including null terminator)
854            let mut name_bytes = [0u8; 20];
855            reader.read_exact(&mut name_bytes)?;
856
857            // Find null terminator position
858            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            // Skip unused field
865            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    /// Parse skybox model path (if present)
878    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        // Skybox was introduced in WotLK
886        if !version.supports_feature(WmoFeature::SkyboxReferences) {
887            return Ok(None);
888        }
889
890        // Check if this WMO has a skybox
891        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), // No skybox
898        };
899
900        let mosb_data = mosb_chunk.read_data(reader)?;
901
902        // Find null terminator position
903        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    /// Parse a list of null-terminated strings from a buffer
917    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    /// Get string at specific offset from buffer
936    fn get_string_at_offset(&self, buffer: &[u8], offset: usize) -> String {
937        if offset >= buffer.len() {
938            return String::new();
939        }
940
941        // Find the null terminator
942        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    /// Calculate a global bounding box from all groups
956    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}