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