wow_alchemy_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        // Parse convex volume planes (Cataclysm+)
170        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        // Create global bounding box from all groups
176        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            header,
191            skybox,
192            convex_volume_planes,
193        })
194    }
195
196    /// Read all chunks in a file
197    fn read_chunks<R: Read + Seek>(&self, reader: &mut R) -> Result<HashMap<ChunkId, Chunk>> {
198        let mut chunks = HashMap::new();
199        let start_pos = reader.stream_position()?;
200        reader.seek(SeekFrom::Start(start_pos))?;
201
202        // Read chunks until end of file
203        loop {
204            match ChunkHeader::read(reader) {
205                Ok(header) => {
206                    trace!("Found chunk: {}, size: {}", header.id, header.size);
207                    let data_pos = reader.stream_position()?;
208
209                    chunks.insert(
210                        header.id,
211                        Chunk {
212                            header,
213                            data_position: data_pos,
214                        },
215                    );
216
217                    reader.seek(SeekFrom::Current(header.size as i64))?;
218                }
219                Err(WmoError::UnexpectedEof) => {
220                    // End of file reached
221                    break;
222                }
223                Err(e) => return Err(e),
224            }
225        }
226
227        // Reset position to start
228        reader.seek(SeekFrom::Start(start_pos))?;
229
230        Ok(chunks)
231    }
232
233    /// Parse the WMO version
234    fn parse_version<R: Read + Seek>(
235        &self,
236        chunks: &HashMap<ChunkId, Chunk>,
237        reader: &mut R,
238    ) -> Result<WmoVersion> {
239        let version_chunk = chunks
240            .get(&chunks::MVER)
241            .ok_or_else(|| WmoError::MissingRequiredChunk("MVER".to_string()))?;
242
243        version_chunk.seek_to_data(reader)?;
244        let raw_version = reader.read_u32_le()?;
245
246        WmoVersion::from_raw(raw_version).ok_or(WmoError::InvalidVersion(raw_version))
247    }
248
249    /// Parse the WMO header
250    fn parse_header<R: Read + Seek>(
251        &self,
252        chunks: &HashMap<ChunkId, Chunk>,
253        reader: &mut R,
254        _version: WmoVersion,
255    ) -> Result<WmoHeader> {
256        let header_chunk = chunks
257            .get(&chunks::MOHD)
258            .ok_or_else(|| WmoError::MissingRequiredChunk("MOHD".to_string()))?;
259
260        header_chunk.seek_to_data(reader)?;
261
262        // Parse basic header fields
263        let n_materials = reader.read_u32_le()?;
264        let n_groups = reader.read_u32_le()?;
265        let n_portals = reader.read_u32_le()?;
266        let n_lights = reader.read_u32_le()?;
267        let n_doodad_names = reader.read_u32_le()?;
268        let n_doodad_defs = reader.read_u32_le()?;
269        let n_doodad_sets = reader.read_u32_le()?;
270        let color_bytes = reader.read_u32_le()?;
271        let flags = WmoFlags::from_bits_truncate(reader.read_u32_le()?);
272
273        // Skip some fields (depending on version)
274        reader.seek(SeekFrom::Current(8))?; // Skip bounding box - we'll calculate this from groups
275
276        // Create color from bytes
277        let ambient_color = Color {
278            r: ((color_bytes >> 16) & 0xFF) as u8,
279            g: ((color_bytes >> 8) & 0xFF) as u8,
280            b: (color_bytes & 0xFF) as u8,
281            a: ((color_bytes >> 24) & 0xFF) as u8,
282        };
283
284        Ok(WmoHeader {
285            n_materials,
286            n_groups,
287            n_portals,
288            n_lights,
289            n_doodad_names,
290            n_doodad_defs,
291            n_doodad_sets,
292            flags,
293            ambient_color,
294        })
295    }
296
297    /// Parse texture filenames
298    fn parse_textures<R: Read + Seek>(
299        &self,
300        chunks: &HashMap<ChunkId, Chunk>,
301        reader: &mut R,
302    ) -> Result<Vec<String>> {
303        let motx_chunk = match chunks.get(&chunks::MOTX) {
304            Some(chunk) => chunk,
305            None => return Ok(Vec::new()), // No textures
306        };
307
308        let motx_data = motx_chunk.read_data(reader)?;
309        let mut textures = Vec::new();
310
311        // MOTX chunk is a list of null-terminated strings
312        let mut current_string = String::new();
313
314        for &byte in &motx_data {
315            if byte == 0 {
316                // End of string
317                if !current_string.is_empty() {
318                    textures.push(current_string);
319                    current_string = String::new();
320                }
321            } else {
322                // Add to current string
323                current_string.push(byte as char);
324            }
325        }
326
327        Ok(textures)
328    }
329
330    /// Parse materials
331    fn parse_materials<R: Read + Seek>(
332        &self,
333        chunks: &HashMap<ChunkId, Chunk>,
334        reader: &mut R,
335        version: WmoVersion,
336        n_materials: u32,
337    ) -> Result<Vec<WmoMaterial>> {
338        let momt_chunk = match chunks.get(&chunks::MOMT) {
339            Some(chunk) => chunk,
340            None => return Ok(Vec::new()), // No materials
341        };
342
343        momt_chunk.seek_to_data(reader)?;
344        let mut materials = Vec::with_capacity(n_materials as usize);
345
346        // Material size depends on version
347        let material_size = if version >= WmoVersion::Mop { 64 } else { 40 };
348
349        for _ in 0..n_materials {
350            let flags = WmoMaterialFlags::from_bits_truncate(reader.read_u32_le()?);
351            let shader = reader.read_u32_le()?;
352            let blend_mode = reader.read_u32_le()?;
353            let texture1 = reader.read_u32_le()?;
354
355            let emissive_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 sidn_color = 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 framebuffer_blend = Color {
370                r: reader.read_u8()?,
371                g: reader.read_u8()?,
372                b: reader.read_u8()?,
373                a: reader.read_u8()?,
374            };
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            // Skip remaining fields depending on version
388            let remaining_size = material_size - 40;
389            if remaining_size > 0 {
390                reader.seek(SeekFrom::Current(remaining_size as i64))?;
391            }
392
393            materials.push(WmoMaterial {
394                flags,
395                shader,
396                blend_mode,
397                texture1,
398                emissive_color,
399                sidn_color,
400                framebuffer_blend,
401                texture2,
402                diffuse_color,
403                ground_type,
404            });
405        }
406
407        Ok(materials)
408    }
409
410    /// Parse group info
411    fn parse_group_info<R: Read + Seek>(
412        &self,
413        chunks: &HashMap<ChunkId, Chunk>,
414        reader: &mut R,
415        _version: WmoVersion,
416        n_groups: u32,
417    ) -> Result<Vec<WmoGroupInfo>> {
418        // Parse group names first
419        let mogn_chunk = match chunks.get(&chunks::MOGN) {
420            Some(chunk) => chunk,
421            None => return Ok(Vec::new()), // No group names
422        };
423
424        let group_names_data = mogn_chunk.read_data(reader)?;
425
426        // Now parse group info
427        let mogi_chunk = match chunks.get(&chunks::MOGI) {
428            Some(chunk) => chunk,
429            None => return Ok(Vec::new()), // No group info
430        };
431
432        mogi_chunk.seek_to_data(reader)?;
433        let mut groups = Vec::with_capacity(n_groups as usize);
434
435        for i in 0..n_groups {
436            let flags = WmoGroupFlags::from_bits_truncate(reader.read_u32_le()?);
437
438            let min_x = reader.read_f32_le()?;
439            let min_y = reader.read_f32_le()?;
440            let min_z = reader.read_f32_le()?;
441            let max_x = reader.read_f32_le()?;
442            let max_y = reader.read_f32_le()?;
443            let max_z = reader.read_f32_le()?;
444
445            let name_offset = reader.read_u32_le()?;
446
447            // Get group name from offset
448            let name = if name_offset < group_names_data.len() as u32 {
449                let name = self.get_string_at_offset(&group_names_data, name_offset as usize);
450                if name.is_empty() {
451                    format!("Group_{i}")
452                } else {
453                    name
454                }
455            } else {
456                format!("Group_{i}")
457            };
458
459            groups.push(WmoGroupInfo {
460                flags,
461                bounding_box: BoundingBox {
462                    min: Vec3 {
463                        x: min_x,
464                        y: min_y,
465                        z: min_z,
466                    },
467                    max: Vec3 {
468                        x: max_x,
469                        y: max_y,
470                        z: max_z,
471                    },
472                },
473                name,
474            });
475        }
476
477        Ok(groups)
478    }
479
480    /// Parse portals
481    fn parse_portals<R: Read + Seek>(
482        &self,
483        chunks: &HashMap<ChunkId, Chunk>,
484        reader: &mut R,
485        n_portals: u32,
486    ) -> Result<Vec<WmoPortal>> {
487        // Parse portal vertices first
488        let mopv_chunk = match chunks.get(&chunks::MOPV) {
489            Some(chunk) => chunk,
490            None => return Ok(Vec::new()), // No portal vertices
491        };
492
493        let mopv_data = mopv_chunk.read_data(reader)?;
494        let n_vertices = mopv_data.len() / 12; // 3 floats per vertex (x, y, z)
495        let mut portal_vertices = Vec::with_capacity(n_vertices);
496
497        for i in 0..n_vertices {
498            let offset = i * 12;
499
500            // Use byteorder to read from the data buffer
501            let x = f32::from_le_bytes([
502                mopv_data[offset],
503                mopv_data[offset + 1],
504                mopv_data[offset + 2],
505                mopv_data[offset + 3],
506            ]);
507
508            let y = f32::from_le_bytes([
509                mopv_data[offset + 4],
510                mopv_data[offset + 5],
511                mopv_data[offset + 6],
512                mopv_data[offset + 7],
513            ]);
514
515            let z = f32::from_le_bytes([
516                mopv_data[offset + 8],
517                mopv_data[offset + 9],
518                mopv_data[offset + 10],
519                mopv_data[offset + 11],
520            ]);
521
522            portal_vertices.push(Vec3 { x, y, z });
523        }
524
525        // Now parse portal data
526        let mopt_chunk = match chunks.get(&chunks::MOPT) {
527            Some(chunk) => chunk,
528            None => return Ok(Vec::new()), // No portal data
529        };
530
531        mopt_chunk.seek_to_data(reader)?;
532        let mut portals = Vec::with_capacity(n_portals as usize);
533
534        for _ in 0..n_portals {
535            let vertex_index = reader.read_u16_le()? as usize;
536            let n_vertices = reader.read_u16_le()? as usize;
537
538            let normal_x = reader.read_f32_le()?;
539            let normal_y = reader.read_f32_le()?;
540            let normal_z = reader.read_f32_le()?;
541
542            // Skip plane distance
543            reader.seek(SeekFrom::Current(4))?;
544
545            // Get portal vertices
546            let mut vertices = Vec::with_capacity(n_vertices);
547            for i in 0..n_vertices {
548                let vertex_idx = vertex_index + i;
549                if vertex_idx < portal_vertices.len() {
550                    vertices.push(portal_vertices[vertex_idx]);
551                } else {
552                    warn!("Portal vertex index out of bounds: {}", vertex_idx);
553                }
554            }
555
556            portals.push(WmoPortal {
557                vertices,
558                normal: Vec3 {
559                    x: normal_x,
560                    y: normal_y,
561                    z: normal_z,
562                },
563            });
564        }
565
566        Ok(portals)
567    }
568
569    /// Parse portal references
570    fn parse_portal_references<R: Read + Seek>(
571        &self,
572        chunks: &HashMap<ChunkId, Chunk>,
573        reader: &mut R,
574    ) -> Result<Vec<WmoPortalReference>> {
575        let mopr_chunk = match chunks.get(&chunks::MOPR) {
576            Some(chunk) => chunk,
577            None => return Ok(Vec::new()), // No portal references
578        };
579
580        let mopr_data = mopr_chunk.read_data(reader)?;
581        let n_refs = mopr_data.len() / 8; // 4 u16 values per reference
582        let mut refs = Vec::with_capacity(n_refs);
583
584        for i in 0..n_refs {
585            let offset = i * 8;
586
587            let portal_index = u16::from_le_bytes([mopr_data[offset], mopr_data[offset + 1]]);
588
589            let group_index = u16::from_le_bytes([mopr_data[offset + 2], mopr_data[offset + 3]]);
590
591            let side = u16::from_le_bytes([mopr_data[offset + 4], mopr_data[offset + 5]]);
592
593            // Skip unused field
594
595            refs.push(WmoPortalReference {
596                portal_index,
597                group_index,
598                side,
599            });
600        }
601
602        Ok(refs)
603    }
604
605    /// Parse visible block lists
606    fn parse_visible_block_lists<R: Read + Seek>(
607        &self,
608        chunks: &HashMap<ChunkId, Chunk>,
609        reader: &mut R,
610    ) -> Result<Vec<Vec<u16>>> {
611        // First get the visible block offsets
612        let movv_chunk = match chunks.get(&chunks::MOVV) {
613            Some(chunk) => chunk,
614            None => return Ok(Vec::new()), // No visible blocks
615        };
616
617        let movv_data = movv_chunk.read_data(reader)?;
618        let n_entries = movv_data.len() / 4; // u32 offset per entry
619        let mut offsets = Vec::with_capacity(n_entries);
620
621        for i in 0..n_entries {
622            let offset = u32::from_le_bytes([
623                movv_data[i * 4],
624                movv_data[i * 4 + 1],
625                movv_data[i * 4 + 2],
626                movv_data[i * 4 + 3],
627            ]);
628
629            offsets.push(offset);
630        }
631
632        // Now get the visible block data
633        let movb_chunk = match chunks.get(&chunks::MOVB) {
634            Some(chunk) => chunk,
635            None => return Ok(Vec::new()), // No visible block data
636        };
637
638        let movb_data = movb_chunk.read_data(reader)?;
639        let mut visible_lists = Vec::with_capacity(offsets.len());
640
641        for &offset in &offsets {
642            let mut index = offset as usize;
643            let mut list = Vec::new();
644
645            // Read until we hit a 0xFFFF marker or end of data
646            while index + 1 < movb_data.len() {
647                let value = u16::from_le_bytes([movb_data[index], movb_data[index + 1]]);
648
649                if value == 0xFFFF {
650                    // End of list marker
651                    break;
652                }
653
654                list.push(value);
655                index += 2;
656            }
657
658            visible_lists.push(list);
659        }
660
661        Ok(visible_lists)
662    }
663
664    /// Parse lights
665    fn parse_lights<R: Read + Seek>(
666        &self,
667        chunks: &HashMap<ChunkId, Chunk>,
668        reader: &mut R,
669        _version: WmoVersion,
670        n_lights: u32,
671    ) -> Result<Vec<WmoLight>> {
672        let molt_chunk = match chunks.get(&chunks::MOLT) {
673            Some(chunk) => chunk,
674            None => return Ok(Vec::new()), // No lights
675        };
676
677        molt_chunk.seek_to_data(reader)?;
678        let mut lights = Vec::with_capacity(n_lights as usize);
679
680        for _ in 0..n_lights {
681            let light_type_raw = reader.read_u8()?;
682            let light_type = WmoLightType::from_raw(light_type_raw).ok_or_else(|| {
683                WmoError::InvalidFormat(format!("Invalid light type: {light_type_raw}"))
684            })?;
685
686            // Read 3 flag bytes
687            let use_attenuation = reader.read_u8()? != 0;
688            let _use_unknown1 = reader.read_u8()?; // Unknown flag
689            let _use_unknown2 = reader.read_u8()?; // Unknown flag
690
691            // Read BGRA color
692            let color_bytes = reader.read_u32_le()?;
693            let color = Color {
694                b: (color_bytes & 0xFF) as u8,
695                g: ((color_bytes >> 8) & 0xFF) as u8,
696                r: ((color_bytes >> 16) & 0xFF) as u8,
697                a: ((color_bytes >> 24) & 0xFF) as u8,
698            };
699
700            let pos_x = reader.read_f32_le()?;
701            let pos_y = reader.read_f32_le()?;
702            let pos_z = reader.read_f32_le()?;
703
704            let position = Vec3 {
705                x: pos_x,
706                y: pos_y,
707                z: pos_z,
708            };
709
710            let intensity = reader.read_f32_le()?;
711
712            let attenuation_start = reader.read_f32_le()?;
713            let attenuation_end = reader.read_f32_le()?;
714
715            // Skip the remaining 16 bytes (unknown radius values)
716            // These might be used for spot/directional lights but we'll keep it simple for now
717            reader.seek(SeekFrom::Current(16))?;
718
719            // For now, use simple properties without directional info
720            let properties = match light_type {
721                WmoLightType::Spot => WmoLightProperties::Spot {
722                    direction: Vec3 {
723                        x: 0.0,
724                        y: 0.0,
725                        z: -1.0,
726                    }, // Default down
727                    hotspot: 0.0,
728                    falloff: 0.0,
729                },
730                WmoLightType::Directional => WmoLightProperties::Directional {
731                    direction: Vec3 {
732                        x: 0.0,
733                        y: 0.0,
734                        z: -1.0,
735                    }, // Default down
736                },
737                WmoLightType::Omni => WmoLightProperties::Omni,
738                WmoLightType::Ambient => WmoLightProperties::Ambient,
739            };
740
741            lights.push(WmoLight {
742                light_type,
743                position,
744                color,
745                intensity,
746                attenuation_start,
747                attenuation_end,
748                use_attenuation,
749                properties,
750            });
751        }
752
753        Ok(lights)
754    }
755
756    /// Parse doodad names
757    fn parse_doodad_names<R: Read + Seek>(
758        &self,
759        chunks: &HashMap<ChunkId, Chunk>,
760        reader: &mut R,
761    ) -> Result<Vec<String>> {
762        let modn_chunk = match chunks.get(&chunks::MODN) {
763            Some(chunk) => chunk,
764            None => return Ok(Vec::new()), // No doodad names
765        };
766
767        let modn_data = modn_chunk.read_data(reader)?;
768        Ok(self.parse_string_list(&modn_data))
769    }
770
771    /// Parse doodad definitions
772    fn parse_doodad_defs<R: Read + Seek>(
773        &self,
774        chunks: &HashMap<ChunkId, Chunk>,
775        reader: &mut R,
776        _version: WmoVersion,
777        n_doodad_defs: u32,
778    ) -> Result<Vec<WmoDoodadDef>> {
779        let modd_chunk = match chunks.get(&chunks::MODD) {
780            Some(chunk) => chunk,
781            None => return Ok(Vec::new()), // No doodad definitions
782        };
783
784        modd_chunk.seek_to_data(reader)?;
785
786        // Calculate actual number of doodad defs based on chunk size
787        // Each doodad def is 40 bytes
788        let actual_doodad_count = modd_chunk.header.size / 40;
789        if actual_doodad_count != n_doodad_defs {
790            warn!(
791                "MODD chunk size indicates {} doodads, but header says {}. Using chunk size.",
792                actual_doodad_count, n_doodad_defs
793            );
794        }
795
796        let mut doodads = Vec::with_capacity(actual_doodad_count as usize);
797
798        for _ in 0..actual_doodad_count {
799            let name_index_raw = reader.read_u32_le()?;
800            // Only the lower 24 bits are used for the name index
801            let name_offset = name_index_raw & 0x00FFFFFF;
802
803            let pos_x = reader.read_f32_le()?;
804            let pos_y = reader.read_f32_le()?;
805            let pos_z = reader.read_f32_le()?;
806
807            let quat_x = reader.read_f32_le()?;
808            let quat_y = reader.read_f32_le()?;
809            let quat_z = reader.read_f32_le()?;
810            let quat_w = reader.read_f32_le()?;
811
812            let scale = reader.read_f32_le()?;
813
814            let color_bytes = reader.read_u32_le()?;
815            let color = Color {
816                r: ((color_bytes >> 16) & 0xFF) as u8,
817                g: ((color_bytes >> 8) & 0xFF) as u8,
818                b: (color_bytes & 0xFF) as u8,
819                a: ((color_bytes >> 24) & 0xFF) as u8,
820            };
821
822            // The set_index field doesn't exist in Classic/TBC/WotLK
823            // It might be part of the upper bits of name_index_raw or added in later versions
824            let set_index = 0; // Default to 0 for now
825
826            doodads.push(WmoDoodadDef {
827                name_offset,
828                position: Vec3 {
829                    x: pos_x,
830                    y: pos_y,
831                    z: pos_z,
832                },
833                orientation: [quat_x, quat_y, quat_z, quat_w],
834                scale,
835                color,
836                set_index,
837            });
838        }
839
840        Ok(doodads)
841    }
842
843    /// Parse doodad sets
844    fn parse_doodad_sets<R: Read + Seek>(
845        &self,
846        chunks: &HashMap<ChunkId, Chunk>,
847        reader: &mut R,
848        _doodad_names: Vec<String>,
849        n_doodad_sets: u32,
850    ) -> Result<Vec<WmoDoodadSet>> {
851        let mods_chunk = match chunks.get(&chunks::MODS) {
852            Some(chunk) => chunk,
853            None => return Ok(Vec::new()), // No doodad sets
854        };
855
856        mods_chunk.seek_to_data(reader)?;
857        let mut sets = Vec::with_capacity(n_doodad_sets as usize);
858
859        for _i in 0..n_doodad_sets {
860            // Read 20 bytes for the set name (including null terminator)
861            let mut name_bytes = [0u8; 20];
862            reader.read_exact(&mut name_bytes)?;
863
864            // Find null terminator position
865            let null_pos = name_bytes.iter().position(|&b| b == 0).unwrap_or(20);
866            let name = String::from_utf8_lossy(&name_bytes[0..null_pos]).to_string();
867
868            let start_doodad = reader.read_u32_le()?;
869            let n_doodads = reader.read_u32_le()?;
870
871            // Skip unused field
872            reader.seek(SeekFrom::Current(4))?;
873
874            sets.push(WmoDoodadSet {
875                name,
876                start_doodad,
877                n_doodads,
878            });
879        }
880
881        Ok(sets)
882    }
883
884    /// Parse skybox model path (if present)
885    fn parse_skybox<R: Read + Seek>(
886        &self,
887        chunks: &HashMap<ChunkId, Chunk>,
888        reader: &mut R,
889        version: WmoVersion,
890        header: &WmoHeader,
891    ) -> Result<Option<String>> {
892        // Skybox was introduced in WotLK
893        if !version.supports_feature(WmoFeature::SkyboxReferences) {
894            return Ok(None);
895        }
896
897        // Check if this WMO has a skybox
898        if !header.flags.contains(WmoFlags::HAS_SKYBOX) {
899            return Ok(None);
900        }
901
902        let mosb_chunk = match chunks.get(&chunks::MOSB) {
903            Some(chunk) => chunk,
904            None => return Ok(None), // No skybox
905        };
906
907        let mosb_data = mosb_chunk.read_data(reader)?;
908
909        // Find null terminator position
910        let null_pos = mosb_data
911            .iter()
912            .position(|&b| b == 0)
913            .unwrap_or(mosb_data.len());
914        let skybox_path = String::from_utf8_lossy(&mosb_data[0..null_pos]).to_string();
915
916        if skybox_path.is_empty() {
917            Ok(None)
918        } else {
919            Ok(Some(skybox_path))
920        }
921    }
922
923    /// Parse convex volume planes (MCVP chunk) - Cataclysm+ transport WMOs
924    fn parse_convex_volume_planes<R: Read + Seek>(
925        &self,
926        chunks: &HashMap<ChunkId, Chunk>,
927        reader: &mut R,
928        version: WmoVersion,
929    ) -> Result<Option<WmoConvexVolumePlanes>> {
930        // MCVP was introduced in Cataclysm for transport WMOs
931        if !version.supports_feature(WmoFeature::ConvexVolumePlanes) {
932            return Ok(None);
933        }
934
935        let mcvp_chunk = match chunks.get(&chunks::MCVP) {
936            Some(chunk) => chunk,
937            None => return Ok(None), // No MCVP chunk
938        };
939
940        let mcvp_data = mcvp_chunk.read_data(reader)?;
941
942        // Each convex volume plane is 20 bytes: Vec3 normal (12) + f32 distance (4) + u32 flags (4)
943        const PLANE_SIZE: usize = 20;
944
945        if mcvp_data.len() % PLANE_SIZE != 0 {
946            warn!(
947                "MCVP chunk size {} is not a multiple of plane size {}",
948                mcvp_data.len(),
949                PLANE_SIZE
950            );
951            return Ok(None);
952        }
953
954        let plane_count = mcvp_data.len() / PLANE_SIZE;
955        let mut planes = Vec::with_capacity(plane_count);
956
957        let mut cursor = std::io::Cursor::new(&mcvp_data);
958
959        for _ in 0..plane_count {
960            let normal = Vec3 {
961                x: cursor.read_f32_le()?,
962                y: cursor.read_f32_le()?,
963                z: cursor.read_f32_le()?,
964            };
965            let distance = cursor.read_f32_le()?;
966            let flags = cursor.read_u32_le()?;
967
968            planes.push(WmoConvexVolumePlane {
969                normal,
970                distance,
971                flags,
972            });
973        }
974
975        Ok(Some(WmoConvexVolumePlanes { planes }))
976    }
977
978    /// Parse a list of null-terminated strings from a buffer
979    fn parse_string_list(&self, buffer: &[u8]) -> Vec<String> {
980        let mut strings = Vec::new();
981        let mut start = 0;
982
983        for i in 0..buffer.len() {
984            if buffer[i] == 0 {
985                if i > start {
986                    if let Ok(s) = std::str::from_utf8(&buffer[start..i]) {
987                        strings.push(s.to_string());
988                    }
989                }
990                start = i + 1;
991            }
992        }
993
994        strings
995    }
996
997    /// Get string at specific offset from buffer
998    fn get_string_at_offset(&self, buffer: &[u8], offset: usize) -> String {
999        if offset >= buffer.len() {
1000            return String::new();
1001        }
1002
1003        // Find the null terminator
1004        let end = buffer[offset..]
1005            .iter()
1006            .position(|&b| b == 0)
1007            .map(|pos| offset + pos)
1008            .unwrap_or(buffer.len());
1009
1010        if let Ok(s) = std::str::from_utf8(&buffer[offset..end]) {
1011            s.to_string()
1012        } else {
1013            String::new()
1014        }
1015    }
1016
1017    /// Calculate a global bounding box from all groups
1018    fn calculate_global_bounding_box(&self, groups: &[WmoGroupInfo]) -> BoundingBox {
1019        if groups.is_empty() {
1020            return BoundingBox {
1021                min: Vec3 {
1022                    x: 0.0,
1023                    y: 0.0,
1024                    z: 0.0,
1025                },
1026                max: Vec3 {
1027                    x: 0.0,
1028                    y: 0.0,
1029                    z: 0.0,
1030                },
1031            };
1032        }
1033
1034        let mut min_x = f32::MAX;
1035        let mut min_y = f32::MAX;
1036        let mut min_z = f32::MAX;
1037        let mut max_x = f32::MIN;
1038        let mut max_y = f32::MIN;
1039        let mut max_z = f32::MIN;
1040
1041        for group in groups {
1042            min_x = min_x.min(group.bounding_box.min.x);
1043            min_y = min_y.min(group.bounding_box.min.y);
1044            min_z = min_z.min(group.bounding_box.min.z);
1045            max_x = max_x.max(group.bounding_box.max.x);
1046            max_y = max_y.max(group.bounding_box.max.y);
1047            max_z = max_z.max(group.bounding_box.max.z);
1048        }
1049
1050        BoundingBox {
1051            min: Vec3 {
1052                x: min_x,
1053                y: min_y,
1054                z: min_z,
1055            },
1056            max: Vec3 {
1057                x: max_x,
1058                y: max_y,
1059                z: max_z,
1060            },
1061        }
1062    }
1063}