Skip to main content

wow_wdl/
types.rs

1//! Core types for the WDL file format
2
3use std::collections::HashMap;
4use std::io::{self, Read, Seek, Write};
5
6use crate::error::{Result, WdlError};
7use crate::version::WdlVersion;
8
9/// Magic number for the MVER chunk (Version)
10pub const MVER_MAGIC: [u8; 4] = [b'R', b'E', b'V', b'M'];
11/// Magic number for the MWMO chunk (WMO Filenames)
12pub const MWMO_MAGIC: [u8; 4] = [b'O', b'M', b'W', b'M'];
13/// Magic number for the MWID chunk (WMO Filename Offsets)
14pub const MWID_MAGIC: [u8; 4] = [b'D', b'I', b'W', b'M'];
15/// Magic number for the MODF chunk (WMO Placement Data)
16pub const MODF_MAGIC: [u8; 4] = [b'F', b'D', b'O', b'M'];
17/// Magic number for the MAOF chunk (Map Area Offset Table)
18pub const MAOF_MAGIC: [u8; 4] = [b'F', b'O', b'A', b'M'];
19/// Magic number for the MARE chunk (Map Area Low-Resolution Heights)
20pub const MARE_MAGIC: [u8; 4] = [b'E', b'R', b'A', b'M'];
21/// Magic number for the MAHO chunk (Map Area Holes)
22pub const MAHO_MAGIC: [u8; 4] = [b'O', b'H', b'A', b'M'];
23/// Magic number for the MLDD chunk (M2 doodad placement, Legion+)
24pub const MLDD_MAGIC: [u8; 4] = [b'D', b'D', b'L', b'M'];
25/// Magic number for the MLDX chunk (M2 doodad visibility, Legion+)
26pub const MLDX_MAGIC: [u8; 4] = [b'X', b'D', b'L', b'M'];
27/// Magic number for the MLMD chunk (WMO placement, Legion+)
28pub const MLMD_MAGIC: [u8; 4] = [b'D', b'M', b'L', b'M'];
29/// Magic number for the MLMX chunk (WMO visibility, Legion+)
30pub const MLMX_MAGIC: [u8; 4] = [b'X', b'M', b'L', b'M'];
31
32/// Vector 3D type used in WoW files
33#[derive(Debug, Clone, PartialEq)]
34pub struct Vec3d {
35    /// X coordinate
36    pub x: f32,
37    /// Y coordinate
38    pub y: f32,
39    /// Z coordinate
40    pub z: f32,
41}
42
43impl Vec3d {
44    /// Creates a new 3D vector
45    pub fn new(x: f32, y: f32, z: f32) -> Self {
46        Self { x, y, z }
47    }
48
49    /// Creates a vector at origin (0, 0, 0)
50    pub fn origin() -> Self {
51        Self::new(0.0, 0.0, 0.0)
52    }
53
54    /// Reads a Vec3d from a reader
55    pub fn read<R: Read>(reader: &mut R) -> io::Result<Self> {
56        let mut buf = [0u8; 4];
57        reader.read_exact(&mut buf)?;
58        let x = f32::from_le_bytes(buf);
59        reader.read_exact(&mut buf)?;
60        let y = f32::from_le_bytes(buf);
61        reader.read_exact(&mut buf)?;
62        let z = f32::from_le_bytes(buf);
63        Ok(Self::new(x, y, z))
64    }
65
66    /// Writes a Vec3d to a writer
67    pub fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
68        writer.write_all(&self.x.to_le_bytes())?;
69        writer.write_all(&self.y.to_le_bytes())?;
70        writer.write_all(&self.z.to_le_bytes())?;
71        Ok(())
72    }
73}
74
75/// Bounding box used in WoW files
76#[derive(Debug, Clone, PartialEq)]
77pub struct BoundingBox {
78    /// Minimum corner of the bounding box
79    pub min: Vec3d,
80    /// Maximum corner of the bounding box
81    pub max: Vec3d,
82}
83
84impl BoundingBox {
85    /// Creates a new bounding box
86    pub fn new(min: Vec3d, max: Vec3d) -> Self {
87        Self { min, max }
88    }
89
90    /// Creates a default bounding box at origin with no size
91    pub fn zero() -> Self {
92        Self::new(Vec3d::origin(), Vec3d::origin())
93    }
94
95    /// Reads a BoundingBox from a reader
96    pub fn read<R: Read>(reader: &mut R) -> io::Result<Self> {
97        let min = Vec3d::read(reader)?;
98        let max = Vec3d::read(reader)?;
99        Ok(Self::new(min, max))
100    }
101
102    /// Writes a BoundingBox to a writer
103    pub fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
104        self.min.write(writer)?;
105        self.max.write(writer)?;
106        Ok(())
107    }
108}
109
110/// A chunk in a WDL file
111#[derive(Debug, Clone)]
112pub struct Chunk {
113    /// The four-character identifier for this chunk
114    pub magic: [u8; 4],
115    /// The size of the data in this chunk
116    pub size: u32,
117    /// The data contained in this chunk
118    pub data: Vec<u8>,
119}
120
121impl Chunk {
122    /// Creates a new chunk with the specified magic, size, and data
123    pub fn new(magic: [u8; 4], data: Vec<u8>) -> Self {
124        let size = data.len() as u32;
125        Self { magic, size, data }
126    }
127
128    /// Reads a chunk from a reader
129    pub fn read<R: Read + Seek>(reader: &mut R) -> io::Result<Self> {
130        let mut magic = [0u8; 4];
131        if reader.read_exact(&mut magic).is_err() {
132            // We've reached EOF
133            return Err(io::Error::new(
134                io::ErrorKind::UnexpectedEof,
135                "unexpected end of file",
136            ));
137        }
138
139        let mut size_buf = [0u8; 4];
140        reader.read_exact(&mut size_buf)?;
141        let size = u32::from_le_bytes(size_buf);
142        let mut data = vec![0u8; size as usize];
143        reader.read_exact(&mut data)?;
144
145        Ok(Self { magic, size, data })
146    }
147
148    /// Writes a chunk to a writer
149    pub fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
150        writer.write_all(&self.magic)?;
151        writer.write_all(&self.size.to_le_bytes())?;
152        writer.write_all(&self.data)?;
153        Ok(())
154    }
155
156    /// Returns a string representation of the magic value
157    pub fn magic_str(&self) -> String {
158        String::from_utf8_lossy(&self.magic).to_string()
159    }
160}
161
162/// Model placement information (MODF chunk data)
163#[derive(Debug, Clone)]
164pub struct ModelPlacement {
165    /// Unique ID for this instance
166    pub id: u32,
167    /// Referenced WMO ID
168    pub wmo_id: u32,
169    /// Position in world
170    pub position: Vec3d,
171    /// Rotation vector (radians)
172    pub rotation: Vec3d,
173    /// Bounds information
174    pub bounds: BoundingBox,
175    /// Flags
176    pub flags: u16,
177    /// Doodad set
178    pub doodad_set: u16,
179    /// Name set
180    pub name_set: u16,
181    /// Padding/reserved value
182    pub padding: u16,
183}
184
185impl ModelPlacement {
186    /// Reads a ModelPlacement from a reader
187    pub fn read<R: Read>(reader: &mut R) -> io::Result<Self> {
188        let mut buf4 = [0u8; 4];
189        let mut buf2 = [0u8; 2];
190
191        reader.read_exact(&mut buf4)?;
192        let wmo_id = u32::from_le_bytes(buf4);
193        reader.read_exact(&mut buf4)?;
194        let id = u32::from_le_bytes(buf4);
195        let position = Vec3d::read(reader)?;
196        let rotation = Vec3d::read(reader)?;
197        let bounds = BoundingBox::read(reader)?;
198        reader.read_exact(&mut buf2)?;
199        let flags = u16::from_le_bytes(buf2);
200        reader.read_exact(&mut buf2)?;
201        let doodad_set = u16::from_le_bytes(buf2);
202        reader.read_exact(&mut buf2)?;
203        let name_set = u16::from_le_bytes(buf2);
204
205        reader.read_exact(&mut buf2)?;
206        let padding = u16::from_le_bytes(buf2);
207
208        Ok(Self {
209            id,
210            wmo_id,
211            position,
212            rotation,
213            bounds,
214            flags,
215            doodad_set,
216            name_set,
217            padding,
218        })
219    }
220
221    /// Writes a ModelPlacement to a writer
222    pub fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
223        writer.write_all(&self.wmo_id.to_le_bytes())?;
224        writer.write_all(&self.id.to_le_bytes())?;
225        self.position.write(writer)?;
226        self.rotation.write(writer)?;
227        self.bounds.write(writer)?;
228        writer.write_all(&self.flags.to_le_bytes())?;
229        writer.write_all(&self.doodad_set.to_le_bytes())?;
230        writer.write_all(&self.name_set.to_le_bytes())?;
231
232        writer.write_all(&self.padding.to_le_bytes())?;
233
234        Ok(())
235    }
236}
237
238/// M2 Model placement information (MLDD chunk data in Legion+)
239#[derive(Debug, Clone)]
240pub struct M2Placement {
241    /// Unique ID for this instance
242    pub id: u32,
243    /// Referenced M2 file data ID
244    pub m2_id: u32,
245    /// Position in world
246    pub position: Vec3d,
247    /// Rotation vector (radians)
248    pub rotation: Vec3d,
249    /// Scale factor
250    pub scale: f32,
251    /// Flags
252    pub flags: u32,
253}
254
255impl M2Placement {
256    /// Reads an M2Placement from a reader
257    pub fn read<R: Read>(reader: &mut R) -> io::Result<Self> {
258        let mut buf = [0u8; 4];
259
260        reader.read_exact(&mut buf)?;
261        let id = u32::from_le_bytes(buf);
262        reader.read_exact(&mut buf)?;
263        let m2_id = u32::from_le_bytes(buf);
264        let position = Vec3d::read(reader)?;
265        let rotation = Vec3d::read(reader)?;
266        reader.read_exact(&mut buf)?;
267        let scale = f32::from_le_bytes(buf);
268        reader.read_exact(&mut buf)?;
269        let flags = u32::from_le_bytes(buf);
270
271        Ok(Self {
272            id,
273            m2_id,
274            position,
275            rotation,
276            scale,
277            flags,
278        })
279    }
280
281    /// Writes an M2Placement to a writer
282    pub fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
283        writer.write_all(&self.id.to_le_bytes())?;
284        writer.write_all(&self.m2_id.to_le_bytes())?;
285        self.position.write(writer)?;
286        self.rotation.write(writer)?;
287        writer.write_all(&self.scale.to_le_bytes())?;
288        writer.write_all(&self.flags.to_le_bytes())?;
289
290        Ok(())
291    }
292}
293
294/// WMO Model visibility info (MLDX chunk data in Legion+)
295#[derive(Debug, Clone)]
296pub struct M2VisibilityInfo {
297    /// Bounding box for visibility check
298    pub bounds: BoundingBox,
299    /// Visibility radius
300    pub radius: f32,
301}
302
303impl M2VisibilityInfo {
304    /// Reads an M2VisibilityInfo from a reader
305    pub fn read<R: Read>(reader: &mut R) -> io::Result<Self> {
306        let bounds = BoundingBox::read(reader)?;
307        let mut buf = [0u8; 4];
308        reader.read_exact(&mut buf)?;
309        let radius = f32::from_le_bytes(buf);
310
311        Ok(Self { bounds, radius })
312    }
313
314    /// Writes an M2VisibilityInfo to a writer
315    pub fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
316        self.bounds.write(writer)?;
317        writer.write_all(&self.radius.to_le_bytes())?;
318
319        Ok(())
320    }
321}
322
323/// Height map data for a single map tile (MARE chunk)
324///
325/// WDL height data provides low-resolution terrain heights for each ADT tile.
326/// The data consists of 545 signed 16-bit integers total:
327/// - 17x17 outer grid (289 values) - vertices at chunk corners
328/// - 16x16 inner grid (256 values) - vertices at chunk centers
329///
330/// This matches the vertex layout of full ADT heightmaps but at lower resolution.
331#[derive(Debug, Clone)]
332pub struct HeightMapTile {
333    /// Outer heightmap values (17x17 grid)
334    /// These represent the height values at the corners of each chunk
335    pub outer_values: Vec<i16>,
336    /// Inner heightmap values (16x16 grid)
337    /// These represent the height values at the centers of each chunk
338    pub inner_values: Vec<i16>,
339}
340
341impl Default for HeightMapTile {
342    fn default() -> Self {
343        Self::new()
344    }
345}
346
347impl HeightMapTile {
348    /// Number of outer height map values (17x17)
349    pub const OUTER_COUNT: usize = 17 * 17;
350    /// Number of inner height map values (16x16)
351    pub const INNER_COUNT: usize = 16 * 16;
352    /// Total count of height map values (545)
353    pub const TOTAL_COUNT: usize = Self::OUTER_COUNT + Self::INNER_COUNT;
354
355    /// Creates a new HeightMapTile with default values (all zeroes)
356    pub fn new() -> Self {
357        Self {
358            outer_values: vec![0; Self::OUTER_COUNT],
359            inner_values: vec![0; Self::INNER_COUNT],
360        }
361    }
362
363    /// Reads a HeightMapTile from a reader
364    pub fn read<R: Read>(reader: &mut R) -> io::Result<Self> {
365        let mut outer_values = Vec::with_capacity(Self::OUTER_COUNT);
366        let mut buf = [0u8; 2];
367        for _ in 0..Self::OUTER_COUNT {
368            reader.read_exact(&mut buf)?;
369            outer_values.push(i16::from_le_bytes(buf));
370        }
371
372        let mut inner_values = Vec::with_capacity(Self::INNER_COUNT);
373        for _ in 0..Self::INNER_COUNT {
374            reader.read_exact(&mut buf)?;
375            inner_values.push(i16::from_le_bytes(buf));
376        }
377
378        Ok(Self {
379            outer_values,
380            inner_values,
381        })
382    }
383
384    /// Writes a HeightMapTile to a writer
385    pub fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
386        for value in &self.outer_values {
387            writer.write_all(&value.to_le_bytes())?;
388        }
389
390        for value in &self.inner_values {
391            writer.write_all(&value.to_le_bytes())?;
392        }
393
394        Ok(())
395    }
396}
397
398/// Holes data for a map tile (MAHO chunk)
399///
400/// Represents terrain holes in a 16x16 chunk grid for an ADT tile.
401/// Each bit in the bitmask represents whether a chunk has a hole:
402/// - 0 = hole present
403/// - 1 = no hole (solid terrain)
404#[derive(Debug, Clone)]
405pub struct HolesData {
406    /// Bitmasks for holes (16 uint16 values, one per row)
407    /// Each uint16 represents 16 chunks in a row (bits 0-15 = chunks 0-15)
408    pub hole_masks: [u16; 16],
409}
410
411impl Default for HolesData {
412    fn default() -> Self {
413        Self::new()
414    }
415}
416
417impl HolesData {
418    /// Number of hole mask values
419    pub const MASK_COUNT: usize = 16;
420
421    /// Creates a new HolesData with no holes (all 1s)
422    pub fn new() -> Self {
423        Self {
424            hole_masks: [0xFFFF; Self::MASK_COUNT],
425        }
426    }
427
428    /// Creates a new HolesData with all holes (all 0s)
429    pub fn all_holes() -> Self {
430        Self {
431            hole_masks: [0; Self::MASK_COUNT],
432        }
433    }
434
435    /// Reads HolesData from a reader
436    pub fn read<R: Read>(reader: &mut R) -> io::Result<Self> {
437        let mut hole_masks = [0u16; Self::MASK_COUNT];
438        let mut buf = [0u8; 2];
439        for mask in &mut hole_masks {
440            reader.read_exact(&mut buf)?;
441            *mask = u16::from_le_bytes(buf);
442        }
443
444        Ok(Self { hole_masks })
445    }
446
447    /// Writes HolesData to a writer
448    pub fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
449        for mask in &self.hole_masks {
450            writer.write_all(&mask.to_le_bytes())?;
451        }
452
453        Ok(())
454    }
455
456    /// Checks if a specific chunk has a hole
457    ///
458    /// # Arguments
459    ///
460    /// * `x` - X coordinate (0-15)
461    /// * `y` - Y coordinate (0-15)
462    ///
463    /// # Returns
464    ///
465    /// `true` if there is a hole, `false` if there is no hole
466    pub fn has_hole(&self, x: usize, y: usize) -> bool {
467        if x >= 16 || y >= 16 {
468            return false;
469        }
470
471        let mask = self.hole_masks[y];
472        (mask & (1 << x)) == 0
473    }
474
475    /// Sets whether a specific chunk has a hole
476    ///
477    /// # Arguments
478    ///
479    /// * `x` - X coordinate (0-15)
480    /// * `y` - Y coordinate (0-15)
481    /// * `has_hole` - Whether the chunk should have a hole
482    pub fn set_hole(&mut self, x: usize, y: usize, has_hole: bool) {
483        if x >= 16 || y >= 16 {
484            return;
485        }
486
487        if has_hole {
488            // Clear the bit to create a hole
489            self.hole_masks[y] &= !(1 << x);
490        } else {
491            // Set the bit to remove a hole
492            self.hole_masks[y] |= 1 << x;
493        }
494    }
495}
496
497/// Main WDL file representation
498#[derive(Debug)]
499pub struct WdlFile {
500    /// Version information
501    pub version: WdlVersion,
502    /// Version number from MVER chunk
503    pub version_number: u32,
504    /// Map tile offsets (MAOF chunk)
505    /// Contains 4096 (64x64) absolute file offsets to MapAreaLow entries
506    /// Zero values indicate tiles without low-resolution data
507    pub map_tile_offsets: [u32; 64 * 64],
508    /// Heightmap tiles (MARE chunks)
509    pub heightmap_tiles: HashMap<(u32, u32), HeightMapTile>,
510    /// Holes data (MAHO chunks)
511    pub holes_data: HashMap<(u32, u32), HolesData>,
512    /// WMO filenames (MWMO chunk)
513    pub wmo_filenames: Vec<String>,
514    /// WMO filename offsets into MWMO data (MWID chunk)
515    pub wmo_indices: Vec<u32>,
516    /// WMO placements (MODF chunk)
517    pub wmo_placements: Vec<ModelPlacement>,
518    /// M2 placements (MLDD chunk, Legion+)
519    pub m2_placements: Vec<M2Placement>,
520    /// M2 visibility info (MLDX chunk, Legion+)
521    pub m2_visibility: Vec<M2VisibilityInfo>,
522    /// WMO placements (MLMD chunk, Legion+)
523    pub wmo_legion_placements: Vec<M2Placement>,
524    /// WMO visibility info (MLMX chunk, Legion+)
525    pub wmo_legion_visibility: Vec<M2VisibilityInfo>,
526    /// All chunks in the file, in order
527    pub chunks: Vec<Chunk>,
528}
529
530impl WdlFile {
531    /// Creates a new empty WDL file
532    pub fn new() -> Self {
533        Self {
534            version: WdlVersion::default(),
535            version_number: WdlVersion::default().version_number(),
536            map_tile_offsets: [0; 64 * 64],
537            heightmap_tiles: HashMap::new(),
538            holes_data: HashMap::new(),
539            wmo_filenames: Vec::new(),
540            wmo_indices: Vec::new(),
541            wmo_placements: Vec::new(),
542            m2_placements: Vec::new(),
543            m2_visibility: Vec::new(),
544            wmo_legion_placements: Vec::new(),
545            wmo_legion_visibility: Vec::new(),
546            chunks: Vec::new(),
547        }
548    }
549
550    /// Creates a new WDL file with the specified version
551    pub fn with_version(version: WdlVersion) -> Self {
552        let mut file = Self::new();
553        file.version = version;
554        file.version_number = version.version_number();
555        file
556    }
557
558    /// Validates the WDL file
559    pub fn validate(&self) -> Result<()> {
560        // Check version
561        if self.version_number != self.version.version_number() {
562            return Err(WdlError::ValidationError(format!(
563                "Version number mismatch: file has {}, expected {}",
564                self.version_number,
565                self.version.version_number()
566            )));
567        }
568
569        // MWID contains offsets into MWMO data, not indices into the filename array
570        // So we don't validate them here
571
572        // TODO: Add more validation as needed
573
574        Ok(())
575    }
576
577    /// Converts the WDL file to another version
578    pub fn convert_to(&self, target_version: WdlVersion) -> Result<Self> {
579        // Create a new file with the target version
580        let mut new_file = WdlFile::with_version(target_version);
581
582        // Copy basic data
583        new_file.map_tile_offsets = self.map_tile_offsets;
584        new_file.heightmap_tiles = self.heightmap_tiles.clone();
585        new_file.holes_data = self.holes_data.clone();
586
587        // Handle version-specific chunks
588        if target_version.has_wmo_chunks() {
589            new_file.wmo_filenames = self.wmo_filenames.clone();
590            new_file.wmo_indices = self.wmo_indices.clone();
591            new_file.wmo_placements = self.wmo_placements.clone();
592        }
593
594        if target_version.has_ml_chunks() {
595            new_file.m2_placements = self.m2_placements.clone();
596            new_file.m2_visibility = self.m2_visibility.clone();
597            new_file.wmo_legion_placements = self.wmo_legion_placements.clone();
598            new_file.wmo_legion_visibility = self.wmo_legion_visibility.clone();
599
600            // If we're converting from a pre-Legion format to Legion+,
601            // we need to convert the WMO data to Legion format
602            if !self.version.has_ml_chunks() && self.version.has_wmo_chunks() {
603                // TODO: Implement conversion from WMO to Legion format
604            }
605        }
606
607        Ok(new_file)
608    }
609}
610
611impl Default for WdlFile {
612    fn default() -> Self {
613        Self::new()
614    }
615}
616
617#[cfg(test)]
618mod tests {
619    use super::*;
620
621    #[test]
622    fn test_vec3d() {
623        let vec = Vec3d::new(1.0, 2.0, 3.0);
624        assert_eq!(vec.x, 1.0);
625        assert_eq!(vec.y, 2.0);
626        assert_eq!(vec.z, 3.0);
627
628        let origin = Vec3d::origin();
629        assert_eq!(origin.x, 0.0);
630        assert_eq!(origin.y, 0.0);
631        assert_eq!(origin.z, 0.0);
632    }
633
634    #[test]
635    fn test_bounding_box() {
636        let min = Vec3d::new(1.0, 2.0, 3.0);
637        let max = Vec3d::new(4.0, 5.0, 6.0);
638        let bbox = BoundingBox::new(min.clone(), max.clone());
639
640        assert_eq!(bbox.min, min);
641        assert_eq!(bbox.max, max);
642
643        let zero = BoundingBox::zero();
644        assert_eq!(zero.min, Vec3d::origin());
645        assert_eq!(zero.max, Vec3d::origin());
646    }
647
648    #[test]
649    fn test_holes_data() {
650        let mut holes = HolesData::new();
651
652        // By default, no holes
653        for x in 0..16 {
654            for y in 0..16 {
655                assert!(!holes.has_hole(x, y));
656            }
657        }
658
659        // Set a hole
660        holes.set_hole(5, 7, true);
661        assert!(holes.has_hole(5, 7));
662
663        // Remove the hole
664        holes.set_hole(5, 7, false);
665        assert!(!holes.has_hole(5, 7));
666
667        // Test all holes
668        let all_holes = HolesData::all_holes();
669        for x in 0..16 {
670            for y in 0..16 {
671                assert!(all_holes.has_hole(x, y));
672            }
673        }
674    }
675}