wow_wmo/
group_parser.rs

1use std::collections::HashMap;
2use std::io::{Read, Seek, SeekFrom};
3use tracing::{debug, trace};
4
5use crate::chunk::{Chunk, ChunkHeader};
6use crate::error::{Result, WmoError};
7use crate::parser::chunks;
8use crate::types::{BoundingBox, ChunkId, Color, Vec3};
9use crate::version::{WmoFeature, WmoVersion};
10use crate::wmo_group_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/// Parser for WMO group files
55pub struct WmoGroupParser;
56
57impl Default for WmoGroupParser {
58    fn default() -> Self {
59        Self::new()
60    }
61}
62
63impl WmoGroupParser {
64    /// Create a new WMO group parser
65    pub fn new() -> Self {
66        Self
67    }
68
69    /// Parse a WMO group file
70    pub fn parse_group<R: Read + Seek>(
71        &self,
72        reader: &mut R,
73        group_index: u32,
74    ) -> Result<WmoGroup> {
75        // Read all chunks in the file
76        let chunks = self.read_chunks(reader)?;
77
78        // Parse version
79        let version = self.parse_version(&chunks, reader)?;
80        debug!("WMO version: {:?}", version);
81
82        // Parse group header
83        let header = self.parse_group_header(&chunks, reader, version, group_index)?;
84        debug!("WMO group header: {:?}", header);
85
86        // Parse materials
87        let materials = self.parse_materials(&chunks, reader)?;
88        debug!("Found {} materials", materials.len());
89
90        // Parse vertices
91        let vertices = self.parse_vertices(&chunks, reader)?;
92        debug!("Found {} vertices", vertices.len());
93
94        // Parse normals
95        let normals = if header.flags.contains(WmoGroupFlags::HAS_NORMALS) {
96            self.parse_normals(&chunks, reader)?
97        } else {
98            Vec::new()
99        };
100        debug!("Found {} normals", normals.len());
101
102        // Parse texture coordinates
103        let tex_coords = self.parse_texture_coords(&chunks, reader)?;
104        debug!("Found {} texture coordinates", tex_coords.len());
105
106        // Parse batches
107        let batches = self.parse_batches(&chunks, reader)?;
108        debug!("Found {} batches", batches.len());
109
110        // Parse indices
111        let indices = self.parse_indices(&chunks, reader)?;
112        debug!("Found {} indices", indices.len());
113
114        // Parse vertex colors (if present)
115        let vertex_colors = if header.flags.contains(WmoGroupFlags::HAS_VERTEX_COLORS) {
116            Some(self.parse_vertex_colors(&chunks, reader)?)
117        } else {
118            None
119        };
120        debug!("Vertex colors: {}", vertex_colors.is_some());
121
122        // Parse BSP nodes (if present)
123        let bsp_nodes = self.parse_bsp_nodes(&chunks, reader)?;
124        debug!("BSP nodes: {}", bsp_nodes.is_some());
125
126        // Parse liquid data (if present)
127        let liquid = if header.flags.contains(WmoGroupFlags::HAS_WATER) {
128            self.parse_liquid(&chunks, reader, version)?
129        } else {
130            None
131        };
132        debug!("Liquid data: {}", liquid.is_some());
133
134        // Parse doodad references (if present)
135        let doodad_refs = if header.flags.contains(WmoGroupFlags::HAS_DOODADS) {
136            Some(self.parse_doodad_refs(&chunks, reader)?)
137        } else {
138            None
139        };
140        debug!("Doodad references: {}", doodad_refs.is_some());
141
142        Ok(WmoGroup {
143            header,
144            materials,
145            vertices,
146            normals,
147            tex_coords,
148            batches,
149            indices,
150            vertex_colors,
151            bsp_nodes,
152            liquid,
153            doodad_refs,
154        })
155    }
156
157    /// Read all chunks in a file
158    fn read_chunks<R: Read + Seek>(&self, reader: &mut R) -> Result<HashMap<ChunkId, Chunk>> {
159        let mut chunks = HashMap::new();
160        let start_pos = reader.stream_position()?;
161        reader.seek(SeekFrom::Start(start_pos))?;
162
163        // Read chunks until end of file
164        loop {
165            match ChunkHeader::read(reader) {
166                Ok(header) => {
167                    trace!("Found chunk: {}, size: {}", header.id, header.size);
168                    let data_pos = reader.stream_position()?;
169
170                    chunks.insert(
171                        header.id,
172                        Chunk {
173                            header,
174                            data_position: data_pos,
175                        },
176                    );
177
178                    reader.seek(SeekFrom::Current(header.size as i64))?;
179                }
180                Err(WmoError::UnexpectedEof) => {
181                    // End of file reached
182                    break;
183                }
184                Err(e) => return Err(e),
185            }
186        }
187
188        // Reset position to start
189        reader.seek(SeekFrom::Start(start_pos))?;
190
191        Ok(chunks)
192    }
193
194    /// Parse the WMO version
195    fn parse_version<R: Read + Seek>(
196        &self,
197        chunks: &HashMap<ChunkId, Chunk>,
198        reader: &mut R,
199    ) -> Result<WmoVersion> {
200        let version_chunk = chunks
201            .get(&chunks::MVER)
202            .ok_or_else(|| WmoError::MissingRequiredChunk("MVER".to_string()))?;
203
204        version_chunk.seek_to_data(reader)?;
205        let raw_version = reader.read_u32_le()?;
206
207        WmoVersion::from_raw(raw_version).ok_or(WmoError::InvalidVersion(raw_version))
208    }
209
210    /// Parse the group header
211    fn parse_group_header<R: Read + Seek>(
212        &self,
213        chunks: &HashMap<ChunkId, Chunk>,
214        reader: &mut R,
215        _version: WmoVersion,
216        group_index: u32,
217    ) -> Result<WmoGroupHeader> {
218        let mogp_chunk = chunks
219            .get(&chunks::MOGP)
220            .ok_or_else(|| WmoError::MissingRequiredChunk("MOGP".to_string()))?;
221
222        mogp_chunk.seek_to_data(reader)?;
223
224        // First 4 bytes are group name offset
225        let name_offset = reader.read_u32_le()?;
226
227        // Next 4 bytes are group flags
228        let flags = WmoGroupFlags::from_bits_truncate(reader.read_u32_le()?);
229
230        // Next 24 bytes are bounding box
231        let min_x = reader.read_f32_le()?;
232        let min_y = reader.read_f32_le()?;
233        let min_z = reader.read_f32_le()?;
234
235        let max_x = reader.read_f32_le()?;
236        let max_y = reader.read_f32_le()?;
237        let max_z = reader.read_f32_le()?;
238
239        // Skip the rest of the header (varies by version)
240        reader.seek(SeekFrom::Current(8))?; // Skip 8 bytes
241
242        Ok(WmoGroupHeader {
243            flags,
244            bounding_box: BoundingBox {
245                min: Vec3 {
246                    x: min_x,
247                    y: min_y,
248                    z: min_z,
249                },
250                max: Vec3 {
251                    x: max_x,
252                    y: max_y,
253                    z: max_z,
254                },
255            },
256            name_offset,
257            group_index,
258        })
259    }
260
261    /// Parse materials used in the group
262    fn parse_materials<R: Read + Seek>(
263        &self,
264        chunks: &HashMap<ChunkId, Chunk>,
265        reader: &mut R,
266    ) -> Result<Vec<u16>> {
267        let moba_chunk = match chunks.get(&chunks::MOBA) {
268            Some(chunk) => chunk,
269            None => return Ok(Vec::new()), // No batches
270        };
271
272        let batch_data = moba_chunk.read_data(reader)?;
273        let mut materials = Vec::new();
274
275        // Each batch is 24 bytes
276        let batch_count = batch_data.len() / 24;
277
278        for i in 0..batch_count {
279            let offset = i * 24 + 2; // Material ID is 2 bytes in
280
281            let material_id = u16::from_le_bytes([batch_data[offset], batch_data[offset + 1]]);
282
283            if !materials.contains(&material_id) {
284                materials.push(material_id);
285            }
286        }
287
288        Ok(materials)
289    }
290
291    /// Parse vertices
292    fn parse_vertices<R: Read + Seek>(
293        &self,
294        chunks: &HashMap<ChunkId, Chunk>,
295        reader: &mut R,
296    ) -> Result<Vec<Vec3>> {
297        let movt_chunk = match chunks.get(&chunks::MOVT) {
298            Some(chunk) => chunk,
299            None => return Ok(Vec::new()), // No vertices
300        };
301
302        let movt_data = movt_chunk.read_data(reader)?;
303        let vertex_count = movt_data.len() / 12; // 12 bytes per vertex (3 floats)
304        let mut vertices = Vec::with_capacity(vertex_count);
305
306        for i in 0..vertex_count {
307            let offset = i * 12;
308
309            let x = f32::from_le_bytes([
310                movt_data[offset],
311                movt_data[offset + 1],
312                movt_data[offset + 2],
313                movt_data[offset + 3],
314            ]);
315
316            let y = f32::from_le_bytes([
317                movt_data[offset + 4],
318                movt_data[offset + 5],
319                movt_data[offset + 6],
320                movt_data[offset + 7],
321            ]);
322
323            let z = f32::from_le_bytes([
324                movt_data[offset + 8],
325                movt_data[offset + 9],
326                movt_data[offset + 10],
327                movt_data[offset + 11],
328            ]);
329
330            vertices.push(Vec3 { x, y, z });
331        }
332
333        Ok(vertices)
334    }
335
336    /// Parse normals
337    fn parse_normals<R: Read + Seek>(
338        &self,
339        chunks: &HashMap<ChunkId, Chunk>,
340        reader: &mut R,
341    ) -> Result<Vec<Vec3>> {
342        let monr_chunk = match chunks.get(&chunks::MONR) {
343            Some(chunk) => chunk,
344            None => return Ok(Vec::new()), // No normals
345        };
346
347        let monr_data = monr_chunk.read_data(reader)?;
348        let normal_count = monr_data.len() / 12; // 12 bytes per normal (3 floats)
349        let mut normals = Vec::with_capacity(normal_count);
350
351        for i in 0..normal_count {
352            let offset = i * 12;
353
354            let x = f32::from_le_bytes([
355                monr_data[offset],
356                monr_data[offset + 1],
357                monr_data[offset + 2],
358                monr_data[offset + 3],
359            ]);
360
361            let y = f32::from_le_bytes([
362                monr_data[offset + 4],
363                monr_data[offset + 5],
364                monr_data[offset + 6],
365                monr_data[offset + 7],
366            ]);
367
368            let z = f32::from_le_bytes([
369                monr_data[offset + 8],
370                monr_data[offset + 9],
371                monr_data[offset + 10],
372                monr_data[offset + 11],
373            ]);
374
375            normals.push(Vec3 { x, y, z });
376        }
377
378        Ok(normals)
379    }
380
381    /// Parse texture coordinates
382    fn parse_texture_coords<R: Read + Seek>(
383        &self,
384        chunks: &HashMap<ChunkId, Chunk>,
385        reader: &mut R,
386    ) -> Result<Vec<TexCoord>> {
387        let motv_chunk = match chunks.get(&chunks::MOTV) {
388            Some(chunk) => chunk,
389            None => return Ok(Vec::new()), // No texture coordinates
390        };
391
392        let motv_data = motv_chunk.read_data(reader)?;
393        let tex_coord_count = motv_data.len() / 8; // 8 bytes per texture coordinate (2 floats)
394        let mut tex_coords = Vec::with_capacity(tex_coord_count);
395
396        for i in 0..tex_coord_count {
397            let offset = i * 8;
398
399            let u = f32::from_le_bytes([
400                motv_data[offset],
401                motv_data[offset + 1],
402                motv_data[offset + 2],
403                motv_data[offset + 3],
404            ]);
405
406            let v = f32::from_le_bytes([
407                motv_data[offset + 4],
408                motv_data[offset + 5],
409                motv_data[offset + 6],
410                motv_data[offset + 7],
411            ]);
412
413            tex_coords.push(TexCoord { u, v });
414        }
415
416        Ok(tex_coords)
417    }
418
419    /// Parse batches
420    fn parse_batches<R: Read + Seek>(
421        &self,
422        chunks: &HashMap<ChunkId, Chunk>,
423        reader: &mut R,
424    ) -> Result<Vec<WmoBatch>> {
425        let moba_chunk = match chunks.get(&chunks::MOBA) {
426            Some(chunk) => chunk,
427            None => return Ok(Vec::new()), // No batches
428        };
429
430        let moba_data = moba_chunk.read_data(reader)?;
431        let batch_count = moba_data.len() / 24; // 24 bytes per batch
432        let mut batches = Vec::with_capacity(batch_count);
433
434        for i in 0..batch_count {
435            let offset = i * 24;
436
437            let flags = moba_data[offset];
438
439            let material_id = u16::from_le_bytes([moba_data[offset + 2], moba_data[offset + 3]]);
440
441            let start_index = u32::from_le_bytes([
442                moba_data[offset + 4],
443                moba_data[offset + 5],
444                moba_data[offset + 6],
445                moba_data[offset + 7],
446            ]);
447
448            let count = u16::from_le_bytes([moba_data[offset + 8], moba_data[offset + 9]]);
449
450            let start_vertex = u16::from_le_bytes([moba_data[offset + 10], moba_data[offset + 11]]);
451
452            let end_vertex = u16::from_le_bytes([moba_data[offset + 12], moba_data[offset + 13]]);
453
454            // Skip position (8 bytes) at offset + 16
455
456            batches.push(WmoBatch {
457                flags,
458                material_id,
459                start_index,
460                count,
461                start_vertex,
462                end_vertex,
463            });
464        }
465
466        Ok(batches)
467    }
468
469    /// Parse indices
470    fn parse_indices<R: Read + Seek>(
471        &self,
472        chunks: &HashMap<ChunkId, Chunk>,
473        reader: &mut R,
474    ) -> Result<Vec<u16>> {
475        let movi_chunk = match chunks.get(&chunks::MOVI) {
476            Some(chunk) => chunk,
477            None => return Ok(Vec::new()), // No indices
478        };
479
480        let movi_data = movi_chunk.read_data(reader)?;
481        let index_count = movi_data.len() / 2; // 2 bytes per index
482        let mut indices = Vec::with_capacity(index_count);
483
484        for i in 0..index_count {
485            let offset = i * 2;
486
487            let index = u16::from_le_bytes([movi_data[offset], movi_data[offset + 1]]);
488
489            indices.push(index);
490        }
491
492        Ok(indices)
493    }
494
495    /// Parse vertex colors
496    fn parse_vertex_colors<R: Read + Seek>(
497        &self,
498        chunks: &HashMap<ChunkId, Chunk>,
499        reader: &mut R,
500    ) -> Result<Vec<Color>> {
501        let mocv_chunk = match chunks.get(&chunks::MOCV) {
502            Some(chunk) => chunk,
503            None => return Ok(Vec::new()), // No vertex colors
504        };
505
506        let mocv_data = mocv_chunk.read_data(reader)?;
507        let color_count = mocv_data.len() / 4; // 4 bytes per color
508        let mut colors = Vec::with_capacity(color_count);
509
510        for i in 0..color_count {
511            let offset = i * 4;
512
513            let color = Color {
514                b: mocv_data[offset],
515                g: mocv_data[offset + 1],
516                r: mocv_data[offset + 2],
517                a: mocv_data[offset + 3],
518            };
519
520            colors.push(color);
521        }
522
523        Ok(colors)
524    }
525
526    /// Parse BSP nodes
527    fn parse_bsp_nodes<R: Read + Seek>(
528        &self,
529        chunks: &HashMap<ChunkId, Chunk>,
530        reader: &mut R,
531    ) -> Result<Option<Vec<WmoBspNode>>> {
532        let mobn_chunk = match chunks.get(&chunks::MOBN) {
533            Some(chunk) => chunk,
534            None => return Ok(None), // No BSP nodes
535        };
536
537        let mobn_data = mobn_chunk.read_data(reader)?;
538        let node_count = mobn_data.len() / 16; // 16 bytes per node
539        let mut nodes = Vec::with_capacity(node_count);
540
541        for i in 0..node_count {
542            let offset = i * 16;
543
544            let plane_normal_x = f32::from_le_bytes([
545                mobn_data[offset],
546                mobn_data[offset + 1],
547                mobn_data[offset + 2],
548                mobn_data[offset + 3],
549            ]);
550
551            let plane_distance = f32::from_le_bytes([
552                mobn_data[offset + 4],
553                mobn_data[offset + 5],
554                mobn_data[offset + 6],
555                mobn_data[offset + 7],
556            ]);
557
558            let child0 = i16::from_le_bytes([mobn_data[offset + 8], mobn_data[offset + 9]]);
559
560            let child1 = i16::from_le_bytes([mobn_data[offset + 10], mobn_data[offset + 11]]);
561
562            let first_face = u16::from_le_bytes([mobn_data[offset + 12], mobn_data[offset + 13]]);
563
564            let num_faces = u16::from_le_bytes([mobn_data[offset + 14], mobn_data[offset + 15]]);
565
566            // Extract plane normal components (x is stored, y and z are packed)
567            let plane_flags = (plane_normal_x as u32) & 0x3;
568            let normal = match plane_flags {
569                0 => Vec3 {
570                    x: 1.0,
571                    y: 0.0,
572                    z: 0.0,
573                },
574                1 => Vec3 {
575                    x: 0.0,
576                    y: 1.0,
577                    z: 0.0,
578                },
579                2 => Vec3 {
580                    x: 0.0,
581                    y: 0.0,
582                    z: 1.0,
583                },
584                3 => {
585                    // Custom normal
586                    let x = ((plane_normal_x as u32) >> 2) as f32 / 32767.0;
587
588                    // Extract y and z from child0 and child1 if they are flagged
589                    // This is approximate since the exact encoding is complex
590                    let y = if child0 < 0 { -0.5 } else { 0.5 };
591                    let z = if child1 < 0 { -0.5 } else { 0.5 };
592
593                    Vec3 { x, y, z }
594                }
595                _ => unreachable!(),
596            };
597
598            nodes.push(WmoBspNode {
599                plane: WmoPlane {
600                    normal,
601                    distance: plane_distance,
602                },
603                children: [child0, child1],
604                first_face,
605                num_faces,
606            });
607        }
608
609        Ok(Some(nodes))
610    }
611
612    /// Parse liquid data
613    fn parse_liquid<R: Read + Seek>(
614        &self,
615        chunks: &HashMap<ChunkId, Chunk>,
616        reader: &mut R,
617        version: WmoVersion,
618    ) -> Result<Option<WmoLiquid>> {
619        let mliq_chunk = match chunks.get(&chunks::MLIQ) {
620            Some(chunk) => chunk,
621            None => return Ok(None), // No liquid data
622        };
623
624        mliq_chunk.seek_to_data(reader)?;
625
626        // Parse liquid header
627        let liquid_type = reader.read_u32_le()?;
628        let flags = reader.read_u32_le()?;
629
630        let width = reader.read_u32_le()? + 1; // Grid is 1 larger than stored value
631        let height = reader.read_u32_le()? + 1;
632
633        // Skip bounding box for now
634        reader.seek(SeekFrom::Current(24))?;
635
636        // Determine format based on version
637        // In older versions, liquid data was simpler
638        let vertex_count = (width * height) as usize;
639        let mut vertices = Vec::with_capacity(vertex_count);
640
641        if version >= WmoVersion::Wod && version.supports_feature(WmoFeature::LiquidV2) {
642            // WoD and later - more complex liquid format
643            // Read vertices and heights
644            for y in 0..height {
645                for x in 0..width {
646                    let base_x = reader.read_f32_le()?;
647                    let base_y = reader.read_f32_le()?;
648                    let base_z = reader.read_f32_le()?;
649
650                    let height = reader.read_f32_le()?;
651
652                    vertices.push(WmoLiquidVertex {
653                        position: Vec3 {
654                            x: base_x + x as f32,
655                            y: base_y + y as f32,
656                            z: base_z,
657                        },
658                        height,
659                    });
660                }
661            }
662
663            // Read tile flags if available
664            let has_tile_flags = flags & 2 != 0;
665            let tile_flags = if has_tile_flags {
666                let tile_count = ((width - 1) * (height - 1)) as usize;
667                let mut flags = Vec::with_capacity(tile_count);
668
669                for _ in 0..tile_count {
670                    flags.push(reader.read_u8()?);
671                }
672
673                Some(flags)
674            } else {
675                None
676            };
677
678            Ok(Some(WmoLiquid {
679                liquid_type,
680                flags,
681                width,
682                height,
683                vertices,
684                tile_flags,
685            }))
686        } else {
687            // Classic through MoP - simpler liquid format
688            // Just read heights
689            for y in 0..height {
690                for x in 0..width {
691                    let depth = reader.read_f32_le()?;
692
693                    vertices.push(WmoLiquidVertex {
694                        position: Vec3 {
695                            x: x as f32,
696                            y: y as f32,
697                            z: 0.0, // Base height set to 0, adjusted by depth
698                        },
699                        height: depth,
700                    });
701                }
702            }
703
704            // Read tile flags
705            let tile_count = ((width - 1) * (height - 1)) as usize;
706            let mut tile_flags = Vec::with_capacity(tile_count);
707
708            for _ in 0..tile_count {
709                tile_flags.push(reader.read_u8()?);
710            }
711
712            Ok(Some(WmoLiquid {
713                liquid_type,
714                flags,
715                width,
716                height,
717                vertices,
718                tile_flags: Some(tile_flags),
719            }))
720        }
721    }
722
723    /// Parse doodad references
724    fn parse_doodad_refs<R: Read + Seek>(
725        &self,
726        chunks: &HashMap<ChunkId, Chunk>,
727        reader: &mut R,
728    ) -> Result<Vec<u16>> {
729        let modr_chunk = match chunks.get(&chunks::MODR) {
730            Some(chunk) => chunk,
731            None => return Ok(Vec::new()), // No doodad refs
732        };
733
734        let modr_data = modr_chunk.read_data(reader)?;
735        let doodad_count = modr_data.len() / 2; // 2 bytes per doodad reference
736        let mut doodad_refs = Vec::with_capacity(doodad_count);
737
738        for i in 0..doodad_count {
739            let offset = i * 2;
740
741            let doodad_ref = u16::from_le_bytes([modr_data[offset], modr_data[offset + 1]]);
742
743            doodad_refs.push(doodad_ref);
744        }
745
746        Ok(doodad_refs)
747    }
748}