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}