wow_wmo/
root_parser.rs

1use crate::chunk_discovery::ChunkDiscovery;
2use crate::chunks::{
3    GfidEntry, McvpEntry, MfogEntry, ModdEntry, ModiEntry, Modn, ModsEntry, MogiEntry, Mogn,
4    MoltEntry, MolvEntry, Mom3Entry, MomtEntry, MopeEntry, MoprEntry, MoptEntry, MopvEntry, Mosb,
5    Motx, MouvEntry, MovbEntry, MovvEntry,
6};
7use binrw::{BinRead, BinReaderExt};
8use std::collections::HashMap;
9use std::io::{Read, Seek, SeekFrom};
10
11/// WMO Root file structure with extended chunk support
12#[derive(Debug, Clone)]
13pub struct WmoRoot {
14    /// Version (always 17 for supported versions)
15    pub version: u32,
16    /// Number of materials (from MOHD)
17    pub n_materials: u32,
18    /// Number of groups (from MOHD)
19    pub n_groups: u32,
20    /// Number of portals (from MOHD)
21    pub n_portals: u32,
22    /// Number of lights (from MOHD)
23    pub n_lights: u32,
24    /// Number of doodad names (from MOHD)
25    pub n_doodad_names: u32,
26    /// Number of doodad definitions (from MOHD)
27    pub n_doodad_defs: u32,
28    /// Number of doodad sets (from MOHD)
29    pub n_doodad_sets: u32,
30    /// Ambient color as BGRA (from MOHD) - base/ambient lighting color
31    pub ambient_color: [u8; 4],
32    /// WMO ID (foreign key to WMOAreaTable.dbc)
33    pub wmo_id: u32,
34    /// Bounding box minimum corner
35    pub bounding_box_min: [f32; 3],
36    /// Bounding box maximum corner
37    pub bounding_box_max: [f32; 3],
38    /// WMO flags
39    pub flags: u16,
40    /// Number of LOD levels
41    pub num_lod: u16,
42
43    // Extended chunk data
44    /// Texture filenames (MOTX)
45    pub textures: Vec<String>,
46    pub texture_offset_index_map: HashMap<u32, u32>,
47    /// Materials (MOMT)
48    pub materials: Vec<MomtEntry>,
49    /// Group names (MOGN)
50    pub group_names: Vec<String>,
51    /// Group information (MOGI)
52    pub group_info: Vec<MogiEntry>,
53    /// Skybox name (MOSB)
54    pub skybox: Option<String>,
55    /// Portal vertices (MOPV)
56    pub portal_vertices: Vec<MopvEntry>,
57    /// Portal information (MOPT)
58    pub portals: Vec<MoptEntry>,
59    /// Portal references (MOPR)
60    pub portal_refs: Vec<MoprEntry>,
61    /// Visible block vertices (MOVV)
62    pub visible_vertices: Vec<MovvEntry>,
63    /// Visible block list (MOVB)
64    pub visible_blocks: Vec<MovbEntry>,
65    /// Lights (MOLT)
66    pub lights: Vec<MoltEntry>,
67    /// Doodad sets (MODS)
68    pub doodad_sets: Vec<ModsEntry>,
69    /// Doodad names (MODN)
70    pub doodad_names: Vec<String>,
71    /// Doodad definitions (MODD)
72    pub doodad_defs: Vec<ModdEntry>,
73    /// Fog definitions (MFOG)
74    pub fogs: Vec<MfogEntry>,
75    /// Convex volume planes (MCVP - Cataclysm+)
76    pub convex_volume_planes: Vec<McvpEntry>,
77    /// UV transformations (MOUV - Legion+)
78    pub uv_transforms: Vec<MouvEntry>,
79    /// Portal extra information (MOPE - WarWithin+)
80    pub portal_extras: Vec<MopeEntry>,
81    /// Light extensions (MOLV - Shadowlands+)
82    pub light_extensions: Vec<MolvEntry>,
83    /// Doodad file IDs (MODI - Battle for Azeroth+)
84    pub doodad_ids: Vec<ModiEntry>,
85    /// New materials (MOM3 - WarWithin+)
86    pub new_materials: Vec<Mom3Entry>,
87    /// Group file IDs (GFID - modern WoW versions)
88    pub group_file_ids: Vec<GfidEntry>,
89}
90
91/// MOHD chunk structure (WMO Header)
92/// Reference: <https://wowdev.wiki/WMO#MOHD_chunk>
93#[derive(Debug, Clone, BinRead)]
94#[br(little)]
95struct Mohd {
96    /// Number of textures (0x00)
97    n_materials: u32,
98    /// Number of groups (0x04)
99    n_groups: u32,
100    /// Number of portals (0x08)
101    n_portals: u32,
102    /// Number of lights (0x0C)
103    n_lights: u32,
104    /// Number of doodad names (0x10)
105    n_doodad_names: u32,
106    /// Number of doodad definitions (0x14)
107    n_doodad_defs: u32,
108    /// Number of doodad sets (0x18)
109    n_doodad_sets: u32,
110    /// Ambient color as BGRA (0x1C) - base/ambient lighting color for the WMO
111    ambient_color: [u8; 4],
112    /// WMO ID (foreign key to WMOAreaTable.dbc) (0x20)
113    wmo_id: u32,
114    /// Bounding box min (0x24)
115    bounding_box_min: [f32; 3],
116    /// Bounding box max (0x30)
117    bounding_box_max: [f32; 3],
118    /// Flags (0x3C)
119    flags: u16,
120    /// Number of LOD levels (0x3E)
121    num_lod: u16,
122}
123
124/// Parse a WMO root file using discovered chunks
125pub fn parse_root_file<R: Read + Seek>(
126    reader: &mut R,
127    discovery: ChunkDiscovery,
128) -> Result<WmoRoot, Box<dyn std::error::Error>> {
129    let mut root = WmoRoot {
130        version: 0,
131        n_materials: 0,
132        n_groups: 0,
133        n_portals: 0,
134        n_lights: 0,
135        n_doodad_names: 0,
136        n_doodad_defs: 0,
137        n_doodad_sets: 0,
138        ambient_color: [128, 128, 128, 255], // Default to mid-gray
139        wmo_id: 0,
140        bounding_box_min: [0.0; 3],
141        bounding_box_max: [0.0; 3],
142        flags: 0,
143        num_lod: 0,
144        textures: Vec::new(),
145        texture_offset_index_map: HashMap::new(),
146        materials: Vec::new(),
147        group_names: Vec::new(),
148        group_info: Vec::new(),
149        skybox: None,
150        portal_vertices: Vec::new(),
151        portals: Vec::new(),
152        portal_refs: Vec::new(),
153        visible_vertices: Vec::new(),
154        visible_blocks: Vec::new(),
155        lights: Vec::new(),
156        doodad_sets: Vec::new(),
157        doodad_names: Vec::new(),
158        doodad_defs: Vec::new(),
159        fogs: Vec::new(),
160        convex_volume_planes: Vec::new(),
161        uv_transforms: Vec::new(),
162        portal_extras: Vec::new(),
163        light_extensions: Vec::new(),
164        doodad_ids: Vec::new(),
165        new_materials: Vec::new(),
166        group_file_ids: Vec::new(),
167    };
168
169    // Process chunks in order
170    for chunk_info in &discovery.chunks {
171        // Seek to chunk data (skip header)
172        reader.seek(SeekFrom::Start(chunk_info.offset + 8))?;
173
174        match chunk_info.id.as_str() {
175            "MVER" => {
176                // Read version
177                root.version = reader.read_le()?;
178            }
179            "MOHD" => {
180                // Read header and populate all fields
181                let mohd = Mohd::read(reader)?;
182                root.n_materials = mohd.n_materials;
183                root.n_groups = mohd.n_groups;
184                root.n_portals = mohd.n_portals;
185                root.n_lights = mohd.n_lights;
186                root.n_doodad_names = mohd.n_doodad_names;
187                root.n_doodad_defs = mohd.n_doodad_defs;
188                root.n_doodad_sets = mohd.n_doodad_sets;
189                root.ambient_color = mohd.ambient_color;
190                root.wmo_id = mohd.wmo_id;
191                root.bounding_box_min = mohd.bounding_box_min;
192                root.bounding_box_max = mohd.bounding_box_max;
193                root.flags = mohd.flags;
194                root.num_lod = mohd.num_lod;
195            }
196            "MOTX" => {
197                // Read texture filenames
198                let mut data = vec![0u8; chunk_info.size as usize];
199                reader.read_exact(&mut data)?;
200                let motx = Motx::parse(&data)?;
201                root.textures = motx.textures;
202                root.texture_offset_index_map = motx.texture_offset_index_map;
203            }
204            "MOMT" => {
205                // Read materials
206                let count = chunk_info.size / 64; // Each material is 64 bytes
207                for _ in 0..count {
208                    root.materials.push(MomtEntry::read(reader)?);
209                }
210            }
211            "MOGN" => {
212                // Read group names
213                let mut data = vec![0u8; chunk_info.size as usize];
214                reader.read_exact(&mut data)?;
215                let mogn = Mogn::parse(&data)?;
216                root.group_names = mogn.names;
217            }
218            "MOGI" => {
219                // Read group information
220                let count = chunk_info.size / 32; // Each entry is 32 bytes
221                for _ in 0..count {
222                    root.group_info.push(MogiEntry::read(reader)?);
223                }
224            }
225            "MOSB" => {
226                // Read skybox name
227                let mut data = vec![0u8; chunk_info.size as usize];
228                reader.read_exact(&mut data)?;
229                let mosb = Mosb::parse(&data)?;
230                root.skybox = mosb.skybox;
231            }
232            "MOPV" => {
233                // Read portal vertices
234                let count = chunk_info.size / 12; // Each vertex is 3 floats (12 bytes)
235                for _ in 0..count {
236                    root.portal_vertices.push(MopvEntry::read(reader)?);
237                }
238            }
239            "MOPT" => {
240                // Read portal information
241                let count = chunk_info.size / 20; // Each portal is 20 bytes
242                for _ in 0..count {
243                    root.portals.push(MoptEntry::read(reader)?);
244                }
245            }
246            "MOPR" => {
247                // Read portal references
248                let count = chunk_info.size / 8; // Each reference is 8 bytes
249                for _ in 0..count {
250                    root.portal_refs.push(MoprEntry::read(reader)?);
251                }
252            }
253            "MOVV" => {
254                // Read visible block vertices
255                let count = chunk_info.size / 12; // Each vertex is 3 floats (12 bytes)
256                for _ in 0..count {
257                    root.visible_vertices.push(MovvEntry::read(reader)?);
258                }
259            }
260            "MOVB" => {
261                // Read visible block list
262                let count = chunk_info.size / 4; // Each entry is 4 bytes
263                for _ in 0..count {
264                    root.visible_blocks.push(MovbEntry::read(reader)?);
265                }
266            }
267            "MOLT" => {
268                // Read lights
269                let count = chunk_info.size / 48; // Each light is 48 bytes
270                for _ in 0..count {
271                    root.lights.push(MoltEntry::read(reader)?);
272                }
273            }
274            "MODS" => {
275                // Read doodad sets
276                let count = chunk_info.size / 32; // Each set is 32 bytes
277                for _ in 0..count {
278                    root.doodad_sets.push(ModsEntry::read(reader)?);
279                }
280            }
281            "MODN" => {
282                // Read doodad names
283                let mut data = vec![0u8; chunk_info.size as usize];
284                reader.read_exact(&mut data)?;
285                let modn = Modn::parse(&data)?;
286                root.doodad_names = modn.names;
287            }
288            "MODD" => {
289                // Read doodad definitions
290                let count = chunk_info.size / 40; // Each def is 40 bytes
291                for _ in 0..count {
292                    root.doodad_defs.push(ModdEntry::read(reader)?);
293                }
294            }
295            "MFOG" => {
296                // Read fog definitions
297                let count = chunk_info.size / 48; // Each fog is 48 bytes
298                for _ in 0..count {
299                    root.fogs.push(MfogEntry::read(reader)?);
300                }
301            }
302            "MCVP" => {
303                // Read convex volume planes (Cataclysm+)
304                let count = chunk_info.size / 16; // Each plane is 16 bytes (4 floats)
305                for _ in 0..count {
306                    root.convex_volume_planes.push(McvpEntry::read(reader)?);
307                }
308            }
309            "MOUV" => {
310                // Read UV transformations (Legion+)
311                let count = chunk_info.size / 16; // Each UV transform is 16 bytes (4 floats)
312                for _ in 0..count {
313                    root.uv_transforms.push(MouvEntry::read(reader)?);
314                }
315            }
316            "MOPE" => {
317                // Read portal extra information (WarWithin+)
318                let count = chunk_info.size / 16; // Each entry is 16 bytes (4 u32s)
319                for _ in 0..count {
320                    root.portal_extras.push(MopeEntry::read(reader)?);
321                }
322            }
323            "MOLV" => {
324                // Read light extensions (Shadowlands+)
325                let count = chunk_info.size / 100; // Each entry is 100 bytes
326                for _ in 0..count {
327                    root.light_extensions.push(MolvEntry::read(reader)?);
328                }
329            }
330            "MODI" => {
331                // Read doodad file IDs (Battle for Azeroth+)
332                let count = chunk_info.size / 4; // Each ID is 4 bytes (u32)
333                for _ in 0..count {
334                    root.doodad_ids.push(reader.read_le()?);
335                }
336            }
337            "MOM3" => {
338                // Read new materials (WarWithin+)
339                // Structure is variable, read as opaque data for now
340                let mut data = vec![0u8; chunk_info.size as usize];
341                reader.read_exact(&mut data)?;
342                root.new_materials.push(Mom3Entry { data });
343            }
344            "MOMO" => {
345                // Alpha version container chunk (version 14 only)
346                // Skip - this is a container chunk with no data
347            }
348            "GFID" => {
349                // Read group file IDs (modern WoW versions)
350                let count = chunk_info.size / 4; // Each ID is 4 bytes (u32)
351                for _ in 0..count {
352                    root.group_file_ids.push(reader.read_le()?);
353                }
354            }
355            _ => {
356                // Skip unknown/unimplemented chunks
357            }
358        }
359    }
360
361    Ok(root)
362}