wow_wmo/
group_parser.rs

1use crate::chunk_discovery::ChunkDiscovery;
2use crate::chunk_header::ChunkHeader;
3use crate::chunks::{
4    MliqHeader, MobaEntry, MobnEntry, MobsEntry, MocvEntry, MonrEntry, MopyEntry, MorbEntry,
5    MotaEntry, MotvEntry, MovtEntry, Mpy2Entry,
6};
7use crate::error::{Result, WmoError};
8use crate::wmo_group_types::WmoGroup as LegacyWmoGroup;
9use binrw::{BinRead, BinReaderExt};
10use std::io::{Read, Seek, SeekFrom};
11
12/// WMO Group file structure with extended chunk support
13#[derive(Debug, Clone)]
14pub struct WmoGroup {
15    /// Version (always 17 for supported versions)
16    pub version: u32,
17    /// Group index (calculated from MOGN)
18    pub group_index: u32,
19    /// Group name index (offset into MOGN)
20    pub group_name_index: u32,
21    /// Descriptive group name index (offset into MOGN)
22    pub descriptive_name_index: u32,
23    /// Group flags
24    pub flags: u32,
25    /// Bounding box [min_x, min_y, min_z, max_x, max_y, max_z]
26    pub bounding_box: Vec<f32>,
27    /// Portal information
28    pub portal_start: u16,
29    pub portal_count: u16,
30    /// Batch counts (trans, int, ext)
31    pub trans_batch_count: u16,
32    pub int_batch_count: u16,
33    pub ext_batch_count: u16,
34    /// Unknown batch type (padding or batch_type_d)
35    pub batch_type_d: u16,
36    /// Fog indices (up to 4)
37    pub fog_ids: Vec<u8>,
38    /// Liquid group ID
39    pub group_liquid: u32,
40    /// WMOAreaTable ID
41    pub area_table_id: u32,
42    /// Additional flags (Cataclysm+)
43    pub flags2: u32,
44    /// Parent or first child split group index
45    pub parent_split_group: i16,
46    /// Next split child group index
47    pub next_split_child: i16,
48    /// Number of triangles in this group (calculated)
49    pub n_triangles: u32,
50    /// Number of vertices in this group (calculated)
51    pub n_vertices: u32,
52
53    // Extended chunk data
54    /// Material info per triangle (MOPY)
55    pub material_info: Vec<MopyEntry>,
56    /// Vertex indices (MOVI)
57    pub vertex_indices: Vec<u16>,
58    /// Vertex positions (MOVT)
59    pub vertex_positions: Vec<MovtEntry>,
60    /// Vertex normals (MONR)
61    pub vertex_normals: Vec<MonrEntry>,
62    /// Texture coordinates (MOTV)
63    pub texture_coords: Vec<MotvEntry>,
64    /// Render batches (MOBA)
65    pub render_batches: Vec<MobaEntry>,
66    /// Vertex colors (MOCV)
67    pub vertex_colors: Vec<MocvEntry>,
68    /// Light references (MOLR)
69    pub light_refs: Vec<u16>,
70    /// Doodad references (MODR)
71    pub doodad_refs: Vec<u16>,
72    /// BSP tree nodes (MOBN)
73    pub bsp_nodes: Vec<MobnEntry>,
74    /// BSP face indices (MOBR)
75    pub bsp_face_indices: Vec<u16>,
76    /// Liquid data (MLIQ)
77    pub liquid_header: Option<MliqHeader>,
78    /// Query face start (MOGX - Dragonflight+)
79    pub query_face_start: Option<u32>,
80    /// Extended material info (MPY2 - Dragonflight+)
81    pub extended_materials: Vec<Mpy2Entry>,
82    /// Extended vertex indices (MOVX - Shadowlands+)
83    pub extended_vertex_indices: Vec<u32>,
84    /// Query faces (MOQG - Dragonflight+)
85    pub query_faces: Vec<u32>,
86    /// Triangle strip indices (MORI)
87    pub triangle_strip_indices: Vec<u16>,
88    /// Additional render batches (MORB)
89    pub additional_render_batches: Vec<MorbEntry>,
90    /// Tangent arrays (MOTA)
91    pub tangent_arrays: Vec<MotaEntry>,
92    /// Shadow batches (MOBS)
93    pub shadow_batches: Vec<MobsEntry>,
94}
95
96/// MOGP chunk header structure (corrected based on wowdev.wiki)
97#[derive(Debug, Clone, BinRead)]
98#[br(little)]
99pub struct MogpHeader {
100    group_name: u32,             // offset into MOGN
101    descriptive_group_name: u32, // offset into MOGN
102    flags: u32,                  // group flags
103    #[br(count = 6)]
104    bounding_box: Vec<f32>, // min xyz, max xyz
105    portal_start: u16,           // index into portal references
106    portal_count: u16,           // number of portal items
107    trans_batch_count: u16,
108    int_batch_count: u16,
109    ext_batch_count: u16,
110    padding_or_batch_type_d: u16,
111    #[br(count = 4)]
112    fog_ids: Vec<u8>, // fog IDs from MFOG
113    group_liquid: u32, // liquid-related
114    unique_id: u32,    // WMOAreaTable reference
115    flags2: u32,
116    parent_or_first_child_split_group_index: i16,
117    next_split_child_group_index: i16,
118}
119
120/// Parse a WMO group file using discovered chunks
121pub fn parse_group_file<R: Read + Seek>(
122    reader: &mut R,
123    discovery: ChunkDiscovery,
124) -> std::result::Result<WmoGroup, Box<dyn std::error::Error>> {
125    let mut group = WmoGroup {
126        version: 0,
127        group_index: 0,
128        group_name_index: 0,
129        descriptive_name_index: 0,
130        flags: 0,
131        bounding_box: Vec::new(),
132        portal_start: 0,
133        portal_count: 0,
134        trans_batch_count: 0,
135        int_batch_count: 0,
136        ext_batch_count: 0,
137        batch_type_d: 0,
138        fog_ids: Vec::new(),
139        group_liquid: 0,
140        area_table_id: 0,
141        flags2: 0,
142        parent_split_group: 0,
143        next_split_child: 0,
144        n_triangles: 0,
145        n_vertices: 0,
146        material_info: Vec::new(),
147        vertex_indices: Vec::new(),
148        vertex_positions: Vec::new(),
149        vertex_normals: Vec::new(),
150        texture_coords: Vec::new(),
151        render_batches: Vec::new(),
152        vertex_colors: Vec::new(),
153        light_refs: Vec::new(),
154        doodad_refs: Vec::new(),
155        bsp_nodes: Vec::new(),
156        bsp_face_indices: Vec::new(),
157        liquid_header: None,
158        query_face_start: None,
159        extended_materials: Vec::new(),
160        extended_vertex_indices: Vec::new(),
161        query_faces: Vec::new(),
162        triangle_strip_indices: Vec::new(),
163        additional_render_batches: Vec::new(),
164        tangent_arrays: Vec::new(),
165        shadow_batches: Vec::new(),
166    };
167
168    // Process chunks in order
169    for chunk_info in &discovery.chunks {
170        // Seek to chunk data (skip header)
171        reader.seek(SeekFrom::Start(chunk_info.offset + 8))?;
172
173        match chunk_info.id.as_str() {
174            "MVER" => {
175                // Read version
176                group.version = reader.read_le()?;
177            }
178            "MOGP" => {
179                // Read group header
180                let header = MogpHeader::read(reader)?;
181
182                // Populate all header fields into the group structure
183                group.group_index = 0; // Will be set from filename or external context
184                group.group_name_index = header.group_name;
185                group.descriptive_name_index = header.descriptive_group_name;
186                group.flags = header.flags;
187                group.bounding_box = header.bounding_box;
188                group.portal_start = header.portal_start;
189                group.portal_count = header.portal_count;
190                group.trans_batch_count = header.trans_batch_count;
191                group.int_batch_count = header.int_batch_count;
192                group.ext_batch_count = header.ext_batch_count;
193                group.batch_type_d = header.padding_or_batch_type_d;
194                group.fog_ids = header.fog_ids;
195                group.group_liquid = header.group_liquid;
196                group.area_table_id = header.unique_id;
197                group.flags2 = header.flags2;
198                group.parent_split_group = header.parent_or_first_child_split_group_index;
199                group.next_split_child = header.next_split_child_group_index;
200
201                // MOGP contains sub-chunks - we need to parse them
202                // The remaining chunk data contains nested chunks
203                // MogpHeader is 68 bytes when serialized (not std::mem::size_of due to Vec fields)
204                let data_size = chunk_info.size - 68;
205                let mut data_reader = std::io::Cursor::new(read_chunk_data(reader, data_size)?);
206
207                // Parse nested chunks within MOGP
208                parse_nested_chunks(&mut data_reader, &mut group)?;
209            }
210            "MOPY" => {
211                // Read material info
212                let count = chunk_info.size / 2; // Each entry is 2 bytes
213                for _ in 0..count {
214                    group.material_info.push(MopyEntry::read(reader)?);
215                }
216            }
217            "MOVI" => {
218                // Read vertex indices
219                let count = chunk_info.size / 2; // Each index is 2 bytes
220                for _ in 0..count {
221                    group.vertex_indices.push(reader.read_le()?);
222                }
223            }
224            "MOVT" => {
225                // Read vertex positions
226                let count = chunk_info.size / 12; // Each position is 3 floats (12 bytes)
227                for _ in 0..count {
228                    group.vertex_positions.push(MovtEntry::read(reader)?);
229                }
230            }
231            "MONR" => {
232                // Read vertex normals
233                let count = chunk_info.size / 12; // Each normal is 3 floats (12 bytes)
234                for _ in 0..count {
235                    group.vertex_normals.push(MonrEntry::read(reader)?);
236                }
237            }
238            "MOTV" => {
239                // Read texture coordinates
240                let count = chunk_info.size / 8; // Each coord is 2 floats (8 bytes)
241                for _ in 0..count {
242                    group.texture_coords.push(MotvEntry::read(reader)?);
243                }
244            }
245            "MOBA" => {
246                // Read render batches
247                let count = chunk_info.size / 16; // Each batch is 16 bytes
248                for _ in 0..count {
249                    group.render_batches.push(MobaEntry::read(reader)?);
250                }
251            }
252            "MOCV" => {
253                // Read vertex colors
254                let count = chunk_info.size / 4; // Each color is 4 bytes
255                for _ in 0..count {
256                    group.vertex_colors.push(MocvEntry::read(reader)?);
257                }
258            }
259            "MOLR" => {
260                // Read light references
261                let count = chunk_info.size / 2; // Each ref is 2 bytes
262                for _ in 0..count {
263                    group.light_refs.push(reader.read_le()?);
264                }
265            }
266            "MODR" => {
267                // Read doodad references
268                let count = chunk_info.size / 2; // Each ref is 2 bytes
269                for _ in 0..count {
270                    group.doodad_refs.push(reader.read_le()?);
271                }
272            }
273            "MOBN" => {
274                // Read BSP tree nodes
275                let count = chunk_info.size / 16; // Each node is 16 bytes
276                for _ in 0..count {
277                    group.bsp_nodes.push(MobnEntry::read(reader)?);
278                }
279            }
280            "MOBR" => {
281                // Read BSP face indices
282                let count = chunk_info.size / 2; // Each index is 2 bytes
283                for _ in 0..count {
284                    group.bsp_face_indices.push(reader.read_le()?);
285                }
286            }
287            "MLIQ" => {
288                // Read liquid header (just the header for now)
289                if chunk_info.size >= 32 {
290                    group.liquid_header = Some(MliqHeader::read(reader)?);
291                }
292            }
293            "MOGX" => {
294                // Read query face start (Dragonflight+)
295                if chunk_info.size >= 4 {
296                    group.query_face_start = Some(reader.read_le()?);
297                }
298            }
299            "MPY2" => {
300                // Read extended material info (Dragonflight+)
301                let count = chunk_info.size / 4; // Each entry is 4 bytes (2 u16s)
302                for _ in 0..count {
303                    group.extended_materials.push(Mpy2Entry::read(reader)?);
304                }
305            }
306            "MOVX" => {
307                // Read extended vertex indices (Shadowlands+)
308                let count = chunk_info.size / 4; // Each index is 4 bytes (u32)
309                for _ in 0..count {
310                    group.extended_vertex_indices.push(reader.read_le()?);
311                }
312            }
313            "MOQG" => {
314                // Read query faces (Dragonflight+)
315                let count = chunk_info.size / 4; // Each face is 4 bytes (u32)
316                for _ in 0..count {
317                    group.query_faces.push(reader.read_le()?);
318                }
319            }
320            "MORI" => {
321                // Read triangle strip indices
322                let count = chunk_info.size / 2; // Each index is 2 bytes (u16)
323                for _ in 0..count {
324                    group.triangle_strip_indices.push(reader.read_le()?);
325                }
326            }
327            "MORB" => {
328                // Read additional render batches
329                let count = chunk_info.size / 10; // Each batch is 10 bytes
330                for _ in 0..count {
331                    group
332                        .additional_render_batches
333                        .push(MorbEntry::read(reader)?);
334                }
335            }
336            "MOTA" => {
337                // Read tangent arrays
338                let count = chunk_info.size / 8; // Each tangent is 8 bytes (4 i16)
339                for _ in 0..count {
340                    group.tangent_arrays.push(MotaEntry::read(reader)?);
341                }
342            }
343            "MOBS" => {
344                // Read shadow batches
345                let count = chunk_info.size / 10; // Each batch is 10 bytes (same as MORB)
346                for _ in 0..count {
347                    group.shadow_batches.push(MobsEntry::read(reader)?);
348                }
349            }
350            _ => {
351                // Skip unknown/unimplemented chunks
352            }
353        }
354    }
355
356    // Calculate triangle and vertex counts from actual data
357    group.n_triangles = (group.vertex_indices.len() / 3) as u32;
358    group.n_vertices = group.vertex_positions.len() as u32;
359
360    Ok(group)
361}
362
363/// Legacy parser for compatibility
364pub struct WmoGroupParser;
365
366impl Default for WmoGroupParser {
367    fn default() -> Self {
368        Self
369    }
370}
371
372impl WmoGroupParser {
373    /// Create a new WMO group parser
374    pub fn new() -> Self {
375        Self
376    }
377
378    /// Parse a WMO group file (legacy interface)
379    pub fn parse_group<R: Read + Seek>(
380        &self,
381        _reader: &mut R,
382        _group_index: u32,
383    ) -> Result<LegacyWmoGroup> {
384        // This is a stub for now - the real implementation will use binrw
385        Err(WmoError::InvalidFormat(
386            "Legacy parser not yet migrated".into(),
387        ))
388    }
389}
390
391/// Read chunk data as bytes
392fn read_chunk_data<R: Read>(
393    reader: &mut R,
394    size: u32,
395) -> std::result::Result<Vec<u8>, Box<dyn std::error::Error>> {
396    let mut data = vec![0u8; size as usize];
397    reader.read_exact(&mut data)?;
398    Ok(data)
399}
400
401/// Parse nested chunks within MOGP data
402fn parse_nested_chunks<R: Read + Seek>(
403    reader: &mut R,
404    group: &mut WmoGroup,
405) -> std::result::Result<(), Box<dyn std::error::Error>> {
406    let end_pos = reader.seek(SeekFrom::End(0))?;
407    reader.seek(SeekFrom::Start(0))?;
408
409    // Parse chunks within the MOGP data
410    while reader.stream_position()? < end_pos {
411        let chunk_start = reader.stream_position()?;
412        let remaining = end_pos - chunk_start;
413
414        if remaining < 8 {
415            break; // Not enough bytes for chunk header
416        }
417
418        let header = match ChunkHeader::read(reader) {
419            Ok(h) => h,
420            Err(_) => break, // Malformed chunk
421        };
422
423        let chunk_id = header.id.as_str();
424        let chunk_size = header.size;
425
426        // Check if we have enough remaining data for this chunk
427        let remaining_data = end_pos - reader.stream_position()?;
428        if chunk_size as u64 > remaining_data {
429            // Chunk claims more data than available - truncated file or corrupted chunk
430            // Skip parsing this chunk and stop
431            break;
432        }
433        match chunk_id {
434            "MOPY" => {
435                // Read material info
436                let count = chunk_size / 2; // Each entry is 2 bytes
437                for _ in 0..count {
438                    group.material_info.push(MopyEntry::read(reader)?);
439                }
440            }
441            "MOVI" => {
442                // Read vertex indices
443                let count = chunk_size / 2; // Each index is 2 bytes
444                for _ in 0..count {
445                    group.vertex_indices.push(reader.read_le()?);
446                }
447            }
448            "MOVT" => {
449                // Read vertex positions
450                let count = chunk_size / 12; // Each position is 3 floats (12 bytes)
451                for _ in 0..count {
452                    group.vertex_positions.push(MovtEntry::read(reader)?);
453                }
454            }
455            "MONR" => {
456                // Read vertex normals
457                let count = chunk_size / 12; // Each normal is 3 floats (12 bytes)
458                for _ in 0..count {
459                    group.vertex_normals.push(MonrEntry::read(reader)?);
460                }
461            }
462            "MOTV" => {
463                // Read texture coordinates
464                let count = chunk_size / 8; // Each coord is 2 floats (8 bytes)
465                for _ in 0..count {
466                    group.texture_coords.push(MotvEntry::read(reader)?);
467                }
468            }
469            "MOBA" => {
470                // Read render batches
471                let count = chunk_size / 24; // Each batch is 24 bytes
472                for _ in 0..count {
473                    group.render_batches.push(MobaEntry::read(reader)?);
474                }
475            }
476            "MOCV" => {
477                // Read vertex colors
478                let count = chunk_size / 4; // Each color is 4 bytes (BGRA)
479                for _ in 0..count {
480                    group.vertex_colors.push(MocvEntry::read(reader)?);
481                }
482            }
483            "MOBN" => {
484                // Read BSP tree nodes
485                let count = chunk_size / 16; // Each node is 16 bytes
486                for _ in 0..count {
487                    group.bsp_nodes.push(MobnEntry::read(reader)?);
488                }
489            }
490            "MOBR" => {
491                // Read BSP face indices
492                let count = chunk_size / 2; // Each index is 2 bytes
493                for _ in 0..count {
494                    group.bsp_face_indices.push(reader.read_le()?);
495                }
496            }
497            "MLIQ" => {
498                // Read liquid header if available
499                if chunk_size >= 32 {
500                    group.liquid_header = Some(MliqHeader::read(reader)?);
501                }
502            }
503            "MOGX" => {
504                // Read query face start (Dragonflight+)
505                if chunk_size >= 4 {
506                    group.query_face_start = Some(reader.read_le()?);
507                }
508            }
509            "MPY2" => {
510                // Read extended material info (Dragonflight+)
511                let count = chunk_size / 4; // Each entry is 4 bytes (2 u16s)
512                for _ in 0..count {
513                    group.extended_materials.push(Mpy2Entry::read(reader)?);
514                }
515            }
516            "MOVX" => {
517                // Read extended vertex indices (Shadowlands+)
518                let count = chunk_size / 4; // Each index is 4 bytes (u32)
519                for _ in 0..count {
520                    group.extended_vertex_indices.push(reader.read_le()?);
521                }
522            }
523            "MOQG" => {
524                // Read query faces (Dragonflight+)
525                let count = chunk_size / 4; // Each face is 4 bytes (u32)
526                for _ in 0..count {
527                    group.query_faces.push(reader.read_le()?);
528                }
529            }
530            "MORI" => {
531                // Read triangle strip indices
532                let count = chunk_size / 2; // Each index is 2 bytes (u16)
533                for _ in 0..count {
534                    group.triangle_strip_indices.push(reader.read_le()?);
535                }
536            }
537            "MORB" => {
538                // Read additional render batches
539                let count = chunk_size / 10; // Each batch is 10 bytes
540                for _ in 0..count {
541                    group
542                        .additional_render_batches
543                        .push(MorbEntry::read(reader)?);
544                }
545            }
546            "MOTA" => {
547                // Read tangent arrays
548                let count = chunk_size / 8; // Each tangent is 8 bytes (4 i16)
549                for _ in 0..count {
550                    group.tangent_arrays.push(MotaEntry::read(reader)?);
551                }
552            }
553            "MOBS" => {
554                // Read shadow batches
555                let count = chunk_size / 10; // Each batch is 10 bytes (same as MORB)
556                for _ in 0..count {
557                    group.shadow_batches.push(MobsEntry::read(reader)?);
558                }
559            }
560            _ => {
561                // Skip unknown chunk
562                reader.seek(SeekFrom::Current(chunk_size as i64))?;
563            }
564        }
565    }
566
567    Ok(())
568}