wow_adt/
file_type.rs

1//! ADT file type detection and classification.
2//!
3//! Starting with Cataclysm (4.x), Blizzard split ADT files into multiple components
4//! for improved loading performance and data organization. This module provides
5//! detection logic to identify which type of ADT file is being processed.
6//!
7//! # Cataclysm+ Split File Architecture
8//!
9//! Pre-Cataclysm (1.x - 3.x):
10//! - Single monolithic ADT file containing all terrain, texture, and object data
11//!
12//! Cataclysm+ (4.x - 5.x):
13//! - **Root ADT**: Core terrain data (heightmaps, normals, holes)
14//! - **_tex0.adt**: Primary texture definitions and layer data
15//! - **_tex1.adt**: Additional texture data (heightmaps, shadows)
16//! - **_obj0.adt**: M2 model and WMO placement data
17//! - **_obj1.adt**: Additional object placement data
18//! - **_lod.adt**: Level-of-detail rendering data
19//!
20//! # Detection Strategy
21//!
22//! File type is determined by analyzing chunk presence patterns:
23//!
24//! | File Type | Key Chunks | Detection Logic |
25//! |-----------|-----------|----------------|
26//! | Root | MCNK | Has terrain chunks |
27//! | Tex0/Tex1 | MTEX, MCLY | Has textures but no MCNK |
28//! | Obj0/Obj1 | MDDF, MODF | Has object refs but no MCNK |
29//! | Lod | Minimal chunks | Fallback for LOD data |
30//!
31//! Filename-based detection provides faster classification when file path is available.
32
33use crate::chunk_discovery::{ChunkDiscovery, ChunkLocation};
34use crate::chunk_id::ChunkId;
35use std::collections::HashMap;
36
37/// ADT file type in Cataclysm+ split file architecture.
38///
39/// Starting with World of Warcraft 4.0 (Cataclysm), ADT files were split into
40/// multiple specialized files to improve loading performance and enable streaming.
41///
42/// # File Type Responsibilities
43///
44/// - **Root**: Core terrain geometry (heightmaps, vertex normals, hole masks)
45/// - **Tex0**: Texture filenames, layer definitions, alpha maps
46/// - **Tex1**: Height textures, shadow maps, additional texture data
47/// - **Obj0**: M2 model placements (MDDF), WMO placements (MODF)
48/// - **Obj1**: Additional object placement data
49/// - **Lod**: Level-of-detail data for distant terrain rendering
50///
51/// # Version Compatibility
52///
53/// - **1.x - 3.x**: All data in single root file (detected as `Root`)
54/// - **4.x - 5.x**: Split file architecture with specialized types
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
56pub enum AdtFileType {
57    /// Root ADT file containing core terrain data.
58    ///
59    /// Contains MCNK chunks with heightmaps, normals, and terrain structure.
60    /// Present in all WoW versions.
61    Root,
62
63    /// Texture file 0 containing primary texture data.
64    ///
65    /// Contains MTEX (texture filenames) and MCLY (layer definitions).
66    /// Cataclysm+ only.
67    Tex0,
68
69    /// Texture file 1 containing additional texture data.
70    ///
71    /// Contains height textures, shadow maps, and auxiliary texture information.
72    /// Cataclysm+ only.
73    Tex1,
74
75    /// Object file 0 containing M2 and WMO placement data.
76    ///
77    /// Contains MDDF (M2 doodad placements) and MODF (WMO placements).
78    /// Cataclysm+ only.
79    Obj0,
80
81    /// Object file 1 containing additional object data.
82    ///
83    /// Contains supplementary object placement information.
84    /// Cataclysm+ only.
85    Obj1,
86
87    /// Level-of-detail file for distant terrain rendering.
88    ///
89    /// Contains simplified geometry for rendering distant terrain tiles.
90    /// Cataclysm+ only.
91    Lod,
92}
93
94impl AdtFileType {
95    /// Detect file type from chunk location map.
96    ///
97    /// Analyzes which chunks are present in the file to determine its type.
98    /// This method works without knowing the filename.
99    ///
100    /// # Detection Logic
101    ///
102    /// 1. **Has MCNK chunks** → Root file (terrain data)
103    /// 2. **Has MTEX but no MCNK** → Tex0/Tex1 file (texture data)
104    /// 3. **Has MDDF/MODF but no MCNK** → Obj0/Obj1 file (object placements)
105    /// 4. **Minimal chunks** → Lod file (level-of-detail data)
106    ///
107    /// # Arguments
108    ///
109    /// * `chunks` - Map of chunk IDs to their locations
110    ///
111    /// # Returns
112    ///
113    /// Detected file type based on chunk presence patterns.
114    ///
115    /// # Examples
116    ///
117    /// ```rust
118    /// use wow_adt::file_type::AdtFileType;
119    /// use wow_adt::chunk_id::ChunkId;
120    /// use wow_adt::chunk_discovery::ChunkLocation;
121    /// use std::collections::HashMap;
122    ///
123    /// let mut chunks = HashMap::new();
124    /// chunks.insert(ChunkId::MCNK, vec![ChunkLocation { offset: 1024, size: 500 }]);
125    ///
126    /// let file_type = AdtFileType::detect_from_chunks(&chunks);
127    /// assert_eq!(file_type, AdtFileType::Root);
128    /// ```
129    pub fn detect_from_chunks(chunks: &HashMap<ChunkId, Vec<ChunkLocation>>) -> Self {
130        let has_mcnk = chunks.contains_key(&ChunkId::MCNK);
131        let has_mhdr = chunks.contains_key(&ChunkId::MHDR);
132        let has_mtex = chunks.contains_key(&ChunkId::MTEX);
133        let has_mddf = chunks.contains_key(&ChunkId::MDDF);
134        let has_modf = chunks.contains_key(&ChunkId::MODF);
135        let has_mmdx = chunks.contains_key(&ChunkId::MMDX);
136        let has_mwmo = chunks.contains_key(&ChunkId::MWMO);
137
138        // Distinguish between Root and split files (Cataclysm+):
139        // - Root files: MHDR + MCNK (may or may not have MTEX/MDDF/MODF depending on version)
140        // - Tex0 files: MTEX + MCNK (texture-specific sub-chunks) but NO MHDR
141        // - Obj0 files: MDDF/MODF/MMDX/MWMO + MCNK (object sub-chunks) but NO MHDR
142
143        if has_mhdr && has_mcnk {
144            // Root file - has header and terrain chunks
145            Self::Root
146        } else if has_mtex && !has_mhdr {
147            // Texture file - has textures but no header (split file)
148            Self::Tex0
149        } else if (has_mddf || has_modf || has_mmdx || has_mwmo) && !has_mhdr {
150            // Object file - has placements but no header (split file)
151            Self::Obj0
152        } else if has_mcnk {
153            // Has MCNK but no MHDR - could be legacy or unusual format
154            // Default to Root for backward compatibility
155            Self::Root
156        } else {
157            // LOD files or other minimal chunk inventory
158            Self::Lod
159        }
160    }
161
162    /// Detect file type from chunk discovery result.
163    ///
164    /// Convenience method that extracts chunk locations from `ChunkDiscovery`
165    /// and performs file type detection.
166    ///
167    /// # Arguments
168    ///
169    /// * `discovery` - Chunk discovery result from Phase 1 parsing
170    ///
171    /// # Returns
172    ///
173    /// Detected file type based on discovered chunks.
174    ///
175    /// # Examples
176    ///
177    /// ```rust
178    /// use wow_adt::file_type::AdtFileType;
179    /// use wow_adt::chunk_discovery::{ChunkDiscovery, ChunkLocation};
180    /// use wow_adt::chunk_id::ChunkId;
181    /// use std::collections::HashMap;
182    ///
183    /// let mut discovery = ChunkDiscovery::new(1000);
184    /// let mut chunks = HashMap::new();
185    /// chunks.insert(ChunkId::MCNK, vec![ChunkLocation { offset: 100, size: 500 }]);
186    /// discovery.chunks = chunks;
187    ///
188    /// let file_type = AdtFileType::from_discovery(&discovery);
189    /// assert_eq!(file_type, AdtFileType::Root);
190    /// ```
191    pub fn from_discovery(discovery: &ChunkDiscovery) -> Self {
192        Self::detect_from_chunks(&discovery.chunks)
193    }
194
195    /// Detect file type from filename pattern.
196    ///
197    /// Faster detection method when filename is available. Recognizes standard
198    /// Cataclysm+ naming conventions.
199    ///
200    /// # Filename Patterns
201    ///
202    /// - `MapName_XX_YY.adt` → Root
203    /// - `MapName_XX_YY_tex0.adt` → Tex0
204    /// - `MapName_XX_YY_tex1.adt` → Tex1
205    /// - `MapName_XX_YY_obj0.adt` → Obj0
206    /// - `MapName_XX_YY_obj1.adt` → Obj1
207    /// - `MapName_XX_YY_lod.adt` → Lod
208    ///
209    /// # Arguments
210    ///
211    /// * `filename` - ADT filename (case-insensitive)
212    ///
213    /// # Returns
214    ///
215    /// File type based on filename pattern. Defaults to `Root` if no pattern matches.
216    ///
217    /// # Examples
218    ///
219    /// ```rust
220    /// use wow_adt::file_type::AdtFileType;
221    ///
222    /// assert_eq!(
223    ///     AdtFileType::from_filename("Azeroth_32_48_tex0.adt"),
224    ///     AdtFileType::Tex0
225    /// );
226    ///
227    /// assert_eq!(
228    ///     AdtFileType::from_filename("Kalimdor_16_32.adt"),
229    ///     AdtFileType::Root
230    /// );
231    /// ```
232    pub fn from_filename(filename: &str) -> Self {
233        let lower = filename.to_lowercase();
234
235        if lower.ends_with("_tex0.adt") {
236            Self::Tex0
237        } else if lower.ends_with("_tex1.adt") {
238            Self::Tex1
239        } else if lower.ends_with("_obj0.adt") {
240            Self::Obj0
241        } else if lower.ends_with("_obj1.adt") {
242            Self::Obj1
243        } else if lower.ends_with("_lod.adt") {
244            Self::Lod
245        } else {
246            // Default to root for standard .adt files
247            Self::Root
248        }
249    }
250
251    /// Get human-readable description of file type.
252    ///
253    /// # Returns
254    ///
255    /// Static string describing the file type's purpose.
256    ///
257    /// # Examples
258    ///
259    /// ```rust
260    /// use wow_adt::file_type::AdtFileType;
261    ///
262    /// assert_eq!(
263    ///     AdtFileType::Root.description(),
264    ///     "Root ADT (terrain data)"
265    /// );
266    /// ```
267    #[must_use]
268    pub const fn description(&self) -> &'static str {
269        match self {
270            Self::Root => "Root ADT (terrain data)",
271            Self::Tex0 => "Texture file 0 (primary textures)",
272            Self::Tex1 => "Texture file 1 (additional textures)",
273            Self::Obj0 => "Object file 0 (M2/WMO placements)",
274            Self::Obj1 => "Object file 1 (additional objects)",
275            Self::Lod => "Level-of-detail file",
276        }
277    }
278
279    /// Check if file type is part of split file architecture.
280    ///
281    /// # Returns
282    ///
283    /// `true` for Cataclysm+ split files (Tex0, Tex1, Obj0, Obj1, Lod).
284    /// `false` for root files (present in all versions).
285    #[must_use]
286    pub const fn is_split_file(&self) -> bool {
287        !matches!(self, Self::Root)
288    }
289
290    /// Check if file type contains terrain geometry.
291    ///
292    /// # Returns
293    ///
294    /// `true` for Root and Lod files containing heightmap data.
295    #[must_use]
296    pub const fn has_terrain_geometry(&self) -> bool {
297        matches!(self, Self::Root | Self::Lod)
298    }
299
300    /// Check if file type contains texture data.
301    ///
302    /// # Returns
303    ///
304    /// `true` for Tex0 and Tex1 files containing texture definitions.
305    #[must_use]
306    pub const fn has_texture_data(&self) -> bool {
307        matches!(self, Self::Tex0 | Self::Tex1)
308    }
309
310    /// Check if file type contains object placement data.
311    ///
312    /// # Returns
313    ///
314    /// `true` for Obj0 and Obj1 files containing M2/WMO placements.
315    #[must_use]
316    pub const fn has_object_data(&self) -> bool {
317        matches!(self, Self::Obj0 | Self::Obj1)
318    }
319}
320
321impl std::fmt::Display for AdtFileType {
322    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
323        write!(f, "{}", self.description())
324    }
325}
326
327#[cfg(test)]
328mod tests {
329    use super::*;
330
331    #[test]
332    fn detect_root_file_from_chunks() {
333        let mut chunks = HashMap::new();
334        chunks.insert(
335            ChunkId::MCNK,
336            vec![ChunkLocation {
337                offset: 1024,
338                size: 500,
339            }],
340        );
341
342        assert_eq!(AdtFileType::detect_from_chunks(&chunks), AdtFileType::Root);
343    }
344
345    #[test]
346    fn detect_texture_file_from_chunks() {
347        let mut chunks = HashMap::new();
348        chunks.insert(
349            ChunkId::MTEX,
350            vec![ChunkLocation {
351                offset: 512,
352                size: 200,
353            }],
354        );
355
356        assert_eq!(AdtFileType::detect_from_chunks(&chunks), AdtFileType::Tex0);
357    }
358
359    #[test]
360    fn detect_object_file_from_chunks() {
361        let mut chunks = HashMap::new();
362        chunks.insert(
363            ChunkId::MDDF,
364            vec![ChunkLocation {
365                offset: 2048,
366                size: 100,
367            }],
368        );
369
370        assert_eq!(AdtFileType::detect_from_chunks(&chunks), AdtFileType::Obj0);
371    }
372
373    #[test]
374    fn detect_lod_file_from_chunks() {
375        let chunks = HashMap::new();
376
377        assert_eq!(AdtFileType::detect_from_chunks(&chunks), AdtFileType::Lod);
378    }
379
380    #[test]
381    fn test_detect_from_discovery() {
382        let mut discovery = ChunkDiscovery::new(1000);
383        let mut chunks = HashMap::new();
384        chunks.insert(
385            ChunkId::MCNK,
386            vec![ChunkLocation {
387                offset: 100,
388                size: 500,
389            }],
390        );
391        discovery.chunks = chunks;
392
393        let file_type = AdtFileType::from_discovery(&discovery);
394        assert_eq!(file_type, AdtFileType::Root);
395    }
396
397    #[test]
398    fn test_detect_tex0_from_discovery() {
399        let mut discovery = ChunkDiscovery::new(1000);
400        let mut chunks = HashMap::new();
401        chunks.insert(
402            ChunkId::MTEX,
403            vec![ChunkLocation {
404                offset: 100,
405                size: 200,
406            }],
407        );
408        discovery.chunks = chunks;
409
410        let file_type = AdtFileType::from_discovery(&discovery);
411        assert_eq!(file_type, AdtFileType::Tex0);
412    }
413
414    #[test]
415    fn test_detect_obj0_from_discovery() {
416        let mut discovery = ChunkDiscovery::new(1000);
417        let mut chunks = HashMap::new();
418        chunks.insert(
419            ChunkId::MODF,
420            vec![ChunkLocation {
421                offset: 100,
422                size: 300,
423            }],
424        );
425        discovery.chunks = chunks;
426
427        let file_type = AdtFileType::from_discovery(&discovery);
428        assert_eq!(file_type, AdtFileType::Obj0);
429    }
430
431    #[test]
432    fn test_detect_lod_from_discovery() {
433        let discovery = ChunkDiscovery::new(1000);
434
435        let file_type = AdtFileType::from_discovery(&discovery);
436        assert_eq!(file_type, AdtFileType::Lod);
437    }
438
439    #[test]
440    fn detect_from_filename_tex0() {
441        assert_eq!(
442            AdtFileType::from_filename("Azeroth_32_48_tex0.adt"),
443            AdtFileType::Tex0
444        );
445    }
446
447    #[test]
448    fn detect_from_filename_case_insensitive() {
449        assert_eq!(
450            AdtFileType::from_filename("AZEROTH_32_48_TEX0.ADT"),
451            AdtFileType::Tex0
452        );
453    }
454
455    #[test]
456    fn detect_from_filename_root() {
457        assert_eq!(
458            AdtFileType::from_filename("Kalimdor_16_32.adt"),
459            AdtFileType::Root
460        );
461    }
462
463    #[test]
464    fn file_type_descriptions() {
465        assert_eq!(AdtFileType::Root.description(), "Root ADT (terrain data)");
466        assert_eq!(
467            AdtFileType::Tex0.description(),
468            "Texture file 0 (primary textures)"
469        );
470    }
471
472    #[test]
473    fn file_type_display() {
474        assert_eq!(format!("{}", AdtFileType::Root), "Root ADT (terrain data)");
475    }
476
477    #[test]
478    fn is_split_file() {
479        assert!(!AdtFileType::Root.is_split_file());
480        assert!(AdtFileType::Tex0.is_split_file());
481        assert!(AdtFileType::Obj0.is_split_file());
482        assert!(AdtFileType::Lod.is_split_file());
483    }
484
485    #[test]
486    fn has_terrain_geometry() {
487        assert!(AdtFileType::Root.has_terrain_geometry());
488        assert!(AdtFileType::Lod.has_terrain_geometry());
489        assert!(!AdtFileType::Tex0.has_terrain_geometry());
490        assert!(!AdtFileType::Obj0.has_terrain_geometry());
491    }
492
493    #[test]
494    fn has_texture_data() {
495        assert!(AdtFileType::Tex0.has_texture_data());
496        assert!(AdtFileType::Tex1.has_texture_data());
497        assert!(!AdtFileType::Root.has_texture_data());
498        assert!(!AdtFileType::Obj0.has_texture_data());
499    }
500
501    #[test]
502    fn has_object_data() {
503        assert!(AdtFileType::Obj0.has_object_data());
504        assert!(AdtFileType::Obj1.has_object_data());
505        assert!(!AdtFileType::Root.has_object_data());
506        assert!(!AdtFileType::Tex0.has_object_data());
507    }
508}