wow_adt/api.rs
1//! High-level parser API for ADT terrain files.
2//!
3//! This module provides type-safe, version-aware parsing of World of Warcraft ADT
4//! (A Dungeon Terrain) files using the two-pass architecture:
5//!
6//! **Pass 1 (Discovery)**: Fast chunk enumeration to detect version and file type
7//! **Pass 2 (Parse)**: Type-safe extraction of chunk data into structured formats
8//!
9//! # Quick Start
10//!
11//! ```no_run
12//! use std::fs::File;
13//! use std::io::BufReader;
14//! use wow_adt::api::parse_adt;
15//!
16//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
17//! let file = File::open("world/maps/azeroth/azeroth_32_32.adt")?;
18//! let mut reader = BufReader::new(file);
19//! let adt = parse_adt(&mut reader)?;
20//!
21//! match adt {
22//! wow_adt::api::ParsedAdt::Root(root) => {
23//! println!("Version: {:?}", root.version);
24//! println!("Terrain chunks: {}", root.mcnk_chunks.len());
25//! println!("Textures: {}", root.textures.len());
26//!
27//! if let Some(water) = &root.water_data {
28//! println!("Has WotLK+ water: {} chunks with water",
29//! water.liquid_chunk_count());
30//! }
31//! }
32//! _ => {}
33//! }
34//! # Ok(())
35//! # }
36//! ```
37//!
38//! # Performance
39//!
40//! - Discovery phase: <10ms for typical ADT files (5-15 MB)
41//! - Full parse: 100 ADT files in <5 seconds (50ms per file average)
42//! - Memory: ≤2× raw file size peak
43
44use std::io::{Read, Seek};
45use std::time::{Duration, Instant};
46
47use crate::chunk_discovery::{ChunkDiscovery, discover_chunks};
48use crate::chunks::mh2o::Mh2oChunk;
49use crate::chunks::{
50 DoodadPlacement, MampChunk, MbbbChunk, MbmhChunk, MbmiChunk, MbnvChunk, McalChunk, McinChunk,
51 MclyChunk, McnkChunk, MfboChunk, MhdrChunk, MtxfChunk, MtxpChunk, WmoPlacement,
52};
53use crate::error::Result;
54use crate::file_type::AdtFileType;
55use crate::version::AdtVersion;
56
57/// Parsed root ADT file (main terrain file for all versions).
58///
59/// Contains all terrain data for a single 16×16 yard area:
60/// - Heightmaps and vertex normals
61/// - Texture layers and alpha blending
62/// - Object placements (M2 models and WMOs)
63/// - Version-specific features (water, flight bounds, etc.)
64///
65/// # Guarantees
66///
67/// - `version` correctly identifies format version
68/// - All required chunks present (MHDR, MCIN, MTEX, MCNK)
69/// - Version-specific chunks only present for compatible versions
70/// - All indices valid within their respective arrays
71/// - MCNK count ∈ [1, 256]
72///
73/// # Example
74///
75/// ```no_run
76/// use wow_adt::api::{parse_adt, ParsedAdt};
77/// use std::fs::File;
78///
79/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
80/// let mut file = File::open("terrain.adt")?;
81/// let adt = parse_adt(&mut file)?;
82///
83/// if let ParsedAdt::Root(root) = adt {
84/// println!("Version: {:?}", root.version);
85/// println!("Terrain chunks: {}", root.mcnk_chunks.len());
86///
87/// if let Some(water) = &root.water_data {
88/// // Access water data by chunk index
89/// for (idx, entry) in water.entries.iter().enumerate() {
90/// if entry.header.has_liquid() {
91/// let row = idx / 16;
92/// let col = idx % 16;
93/// println!("Chunk ({}, {}) has {} water layer(s)",
94/// row, col, entry.instances.len());
95/// }
96/// }
97/// }
98/// }
99/// # Ok(())
100/// # }
101/// ```
102#[derive(Debug, Clone)]
103pub struct RootAdt {
104 /// Detected ADT version
105 pub version: AdtVersion,
106
107 /// MHDR - Header with chunk offsets
108 pub mhdr: MhdrChunk,
109
110 /// MCIN - MCNK chunk index (256 entries)
111 pub mcin: McinChunk,
112
113 /// Texture filenames from MTEX chunk
114 pub textures: Vec<String>,
115
116 /// M2 model filenames from MMDX chunk
117 pub models: Vec<String>,
118
119 /// M2 model filename offsets from MMID chunk
120 pub model_indices: Vec<u32>,
121
122 /// WMO filenames from MWMO chunk
123 pub wmos: Vec<String>,
124
125 /// WMO filename offsets from MWID chunk
126 pub wmo_indices: Vec<u32>,
127
128 /// M2 model placements from MDDF chunk
129 pub doodad_placements: Vec<DoodadPlacement>,
130
131 /// WMO placements from MODF chunk
132 pub wmo_placements: Vec<WmoPlacement>,
133
134 /// MCNK terrain chunks (1-256 chunks)
135 pub mcnk_chunks: Vec<McnkChunk>,
136
137 /// MFBO - Flight boundaries (TBC+)
138 pub flight_bounds: Option<MfboChunk>,
139
140 /// MH2O - Advanced water system (WotLK+)
141 ///
142 /// Contains 256 entries (one per MCNK chunk) with liquid layer data.
143 /// Each entry has header, instances, and optional attributes.
144 pub water_data: Option<Mh2oChunk>,
145
146 /// MTXF - Texture flags (WotLK 3.x+)
147 ///
148 /// Rendering flags for each texture controlling specularity,
149 /// environment mapping, and animation.
150 pub texture_flags: Option<MtxfChunk>,
151
152 /// MAMP - Texture amplifier (Cataclysm+)
153 pub texture_amplifier: Option<MampChunk>,
154
155 /// MTXP - Texture parameters (MoP+)
156 pub texture_params: Option<MtxpChunk>,
157
158 /// MBMH - Blend mesh headers (MoP 5.x+)
159 ///
160 /// Headers describing blend mesh batches for smooth texture transitions.
161 /// Each entry contains map_object_id, texture_id, and index/vertex ranges.
162 pub blend_mesh_headers: Option<MbmhChunk>,
163
164 /// MBBB - Blend mesh bounding boxes (MoP 5.x+)
165 ///
166 /// Bounding boxes for visibility culling of blend meshes.
167 /// Each entry has map_object_id and min/max coordinates.
168 pub blend_mesh_bounds: Option<MbbbChunk>,
169
170 /// MBNV - Blend mesh vertices (MoP 5.x+)
171 ///
172 /// Vertex data for blend mesh system with position, normal, UV coordinates,
173 /// and 3 RGBA color channels for texture blending.
174 pub blend_mesh_vertices: Option<MbnvChunk>,
175
176 /// MBMI - Blend mesh indices (MoP 5.x+)
177 ///
178 /// Triangle indices (u16) referencing MBNV vertex array.
179 /// MCBB chunks in MCNK reference ranges within this array.
180 pub blend_mesh_indices: Option<MbmiChunk>,
181}
182
183impl RootAdt {
184 /// Check if this ADT has water data.
185 #[must_use]
186 pub fn has_water(&self) -> bool {
187 self.water_data.is_some()
188 }
189
190 /// Check if this ADT has flight boundaries.
191 #[must_use]
192 pub fn has_flight_bounds(&self) -> bool {
193 self.flight_bounds.is_some()
194 }
195
196 /// Get number of terrain chunks.
197 #[must_use]
198 pub fn terrain_chunk_count(&self) -> usize {
199 self.mcnk_chunks.len()
200 }
201
202 /// Get number of textures.
203 #[must_use]
204 pub fn texture_count(&self) -> usize {
205 self.textures.len()
206 }
207
208 /// Get number of M2 models.
209 #[must_use]
210 pub fn model_count(&self) -> usize {
211 self.models.len()
212 }
213
214 /// Get number of WMO objects.
215 #[must_use]
216 pub fn wmo_count(&self) -> usize {
217 self.wmos.len()
218 }
219
220 // ========================================================================
221 // Mutable Access Methods (for modify workflows)
222 // ========================================================================
223
224 /// Get mutable access to textures for replacement workflows.
225 ///
226 /// Use this to modify texture filenames in place. After modification,
227 /// serialize using [`AdtBuilder::from_parsed()`](crate::builder::AdtBuilder::from_parsed).
228 ///
229 /// # Example
230 ///
231 /// ```rust,no_run
232 /// use wow_adt::api::parse_adt;
233 /// # use std::fs::File;
234 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
235 /// # let mut file = File::open("terrain.adt")?;
236 /// # let adt = parse_adt(&mut file)?;
237 /// # if let wow_adt::api::ParsedAdt::Root(mut root) = adt {
238 /// root.textures_mut()[0] = "terrain/grass_new.blp".to_string();
239 /// # }
240 /// # Ok(())
241 /// # }
242 /// ```
243 pub fn textures_mut(&mut self) -> &mut Vec<String> {
244 &mut self.textures
245 }
246
247 /// Get mutable access to M2 model filenames.
248 pub fn models_mut(&mut self) -> &mut Vec<String> {
249 &mut self.models
250 }
251
252 /// Get mutable access to WMO filenames.
253 pub fn wmos_mut(&mut self) -> &mut Vec<String> {
254 &mut self.wmos
255 }
256
257 /// Get mutable access to M2 model placements.
258 ///
259 /// Use this to add, remove, or modify doodad placements.
260 ///
261 /// # Example
262 ///
263 /// ```rust,no_run
264 /// use wow_adt::api::parse_adt;
265 /// # use std::fs::File;
266 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
267 /// # let mut file = File::open("terrain.adt")?;
268 /// # let adt = parse_adt(&mut file)?;
269 /// # if let wow_adt::api::ParsedAdt::Root(mut root) = adt {
270 /// root.doodad_placements_mut()[0].position[2] += 10.0; // Z coordinate
271 /// # }
272 /// # Ok(())
273 /// # }
274 /// ```
275 pub fn doodad_placements_mut(&mut self) -> &mut Vec<DoodadPlacement> {
276 &mut self.doodad_placements
277 }
278
279 /// Get mutable access to WMO placements.
280 pub fn wmo_placements_mut(&mut self) -> &mut Vec<WmoPlacement> {
281 &mut self.wmo_placements
282 }
283
284 /// Get mutable access to MCNK terrain chunks.
285 ///
286 /// Use this to modify terrain geometry, heights, textures, etc.
287 ///
288 /// # Example
289 ///
290 /// ```rust,no_run
291 /// use wow_adt::api::parse_adt;
292 /// # use std::fs::File;
293 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
294 /// # let mut file = File::open("terrain.adt")?;
295 /// # let adt = parse_adt(&mut file)?;
296 /// # if let wow_adt::api::ParsedAdt::Root(mut root) = adt {
297 /// if let Some(heights) = &mut root.mcnk_chunks_mut()[0].heights {
298 /// for height in &mut heights.heights {
299 /// *height += 5.0;
300 /// }
301 /// }
302 /// # }
303 /// # Ok(())
304 /// # }
305 /// ```
306 pub fn mcnk_chunks_mut(&mut self) -> &mut Vec<McnkChunk> {
307 &mut self.mcnk_chunks
308 }
309
310 /// Get mutable access to water data (WotLK+).
311 ///
312 /// Returns `None` if this ADT has no water data.
313 pub fn water_data_mut(&mut self) -> Option<&mut Mh2oChunk> {
314 self.water_data.as_mut()
315 }
316
317 /// Get mutable access to flight boundaries (TBC+).
318 ///
319 /// Returns `None` if this ADT has no flight bounds.
320 pub fn flight_bounds_mut(&mut self) -> Option<&mut MfboChunk> {
321 self.flight_bounds.as_mut()
322 }
323
324 /// Get mutable access to texture flags (WotLK+).
325 pub fn texture_flags_mut(&mut self) -> Option<&mut MtxfChunk> {
326 self.texture_flags.as_mut()
327 }
328
329 /// Get mutable access to texture amplifier (Cataclysm+).
330 pub fn texture_amplifier_mut(&mut self) -> Option<&mut MampChunk> {
331 self.texture_amplifier.as_mut()
332 }
333
334 /// Get mutable access to texture parameters (MoP+).
335 pub fn texture_params_mut(&mut self) -> Option<&mut MtxpChunk> {
336 self.texture_params.as_mut()
337 }
338}
339
340/// MCNK texture data container for split texture files.
341///
342/// In Cataclysm+ split architecture, texture files contain simplified MCNK chunks
343/// that only hold texture layer and alpha map data. This structure represents the
344/// per-chunk texture information.
345///
346/// # Format
347///
348/// Each texture file has 256 MCNK containers (16x16 grid), one for each terrain chunk.
349/// Unlike root file MCNK chunks, these do not contain full headers or geometry data.
350///
351/// # References
352///
353/// - wowdev.wiki: ADT/v18#Texture Files (_tex0.adt)
354#[derive(Debug, Clone)]
355pub struct McnkChunkTexture {
356 /// Chunk index in 16x16 grid (0-255)
357 pub index: usize,
358
359 /// MCLY - Texture layer definitions (up to 4 layers per chunk)
360 pub layers: Option<MclyChunk>,
361
362 /// MCAL - Alpha maps for texture blending
363 pub alpha_maps: Option<McalChunk>,
364}
365
366/// MCNK object data container for split object files.
367///
368/// In Cataclysm+ split architecture, object files contain simplified MCNK chunks
369/// that only hold references to M2 models and WMO objects placed within that chunk.
370///
371/// # Format
372///
373/// Each object file has 256 MCNK containers (16x16 grid), one for each terrain chunk.
374/// The MCRD/MCRW chunks contain indices into the global MDDF/MODF arrays.
375///
376/// # References
377///
378/// - wowdev.wiki: ADT/v18#Object Files (_obj0.adt)
379#[derive(Debug, Clone)]
380pub struct McnkChunkObject {
381 /// Chunk index in 16x16 grid (0-255)
382 pub index: usize,
383
384 /// MCRD - M2 doodad reference indices (Cataclysm+)
385 ///
386 /// Indices into the global MDDF array for M2 models placed in this chunk.
387 pub doodad_refs: Vec<u32>,
388
389 /// MCRW - WMO reference indices (Cataclysm+)
390 ///
391 /// Indices into the global MODF array for WMO objects placed in this chunk.
392 pub wmo_refs: Vec<u32>,
393}
394
395/// Parsed texture file (Cataclysm+ `_tex0.adt`).
396///
397/// Contains texture-related data split from root ADT in Cataclysm+:
398/// - Texture filenames
399/// - Texture layer definitions (per-chunk)
400/// - Alpha maps for blending (per-chunk)
401/// - Texture parameters (MoP+)
402///
403/// # Format
404///
405/// Texture files contain MTEX (global texture list) and 256 MCNK containers
406/// with texture layer and alpha map data for each terrain chunk.
407///
408/// # Example
409///
410/// ```no_run
411/// use wow_adt::api::{parse_adt, ParsedAdt};
412/// use std::fs::File;
413///
414/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
415/// let mut file = File::open("terrain_tex0.adt")?;
416/// let adt = parse_adt(&mut file)?;
417///
418/// if let ParsedAdt::Tex0(tex) = adt {
419/// println!("Textures: {}", tex.textures.len());
420/// println!("Chunks with texture data: {}", tex.mcnk_textures.len());
421/// }
422/// # Ok(())
423/// # }
424/// ```
425#[derive(Debug, Clone)]
426pub struct Tex0Adt {
427 /// Detected ADT version
428 pub version: AdtVersion,
429
430 /// Texture filenames from MTEX chunk
431 pub textures: Vec<String>,
432
433 /// MTXP - Texture parameters (MoP+)
434 pub texture_params: Option<MtxpChunk>,
435
436 /// Per-chunk texture data (256 chunks in 16x16 grid)
437 pub mcnk_textures: Vec<McnkChunkTexture>,
438}
439
440/// Parsed object file (Cataclysm+ `_obj0.adt`).
441///
442/// Contains object placement data split from root ADT in Cataclysm+:
443/// - M2 model filenames and placements
444/// - WMO filenames and placements
445/// - Per-chunk object references (MCRD/MCRW)
446///
447/// # Format
448///
449/// Object files contain global object lists (MMDX/MWMO) and placement arrays
450/// (MDDF/MODF), plus 256 MCNK containers with per-chunk object references.
451///
452/// # Example
453///
454/// ```no_run
455/// use wow_adt::api::{parse_adt, ParsedAdt};
456/// use std::fs::File;
457///
458/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
459/// let mut file = File::open("terrain_obj0.adt")?;
460/// let adt = parse_adt(&mut file)?;
461///
462/// if let ParsedAdt::Obj0(obj) = adt {
463/// println!("M2 models: {}", obj.models.len());
464/// println!("WMO objects: {}", obj.wmos.len());
465/// println!("Chunks with objects: {}", obj.mcnk_objects.len());
466/// }
467/// # Ok(())
468/// # }
469/// ```
470#[derive(Debug, Clone)]
471pub struct Obj0Adt {
472 /// Detected ADT version
473 pub version: AdtVersion,
474
475 /// M2 model filenames from MMDX chunk
476 pub models: Vec<String>,
477
478 /// M2 model filename offsets from MMID chunk
479 pub model_indices: Vec<u32>,
480
481 /// WMO filenames from MWMO chunk
482 pub wmos: Vec<String>,
483
484 /// WMO filename offsets from MWID chunk
485 pub wmo_indices: Vec<u32>,
486
487 /// M2 model placements from MDDF chunk
488 pub doodad_placements: Vec<DoodadPlacement>,
489
490 /// WMO placements from MODF chunk
491 pub wmo_placements: Vec<WmoPlacement>,
492
493 /// Per-chunk object references (256 chunks in 16x16 grid)
494 pub mcnk_objects: Vec<McnkChunkObject>,
495}
496
497/// Parsed LOD file (Cataclysm+ `_lod.adt`).
498///
499/// Contains level-of-detail data for distant terrain rendering.
500/// This format is minimally documented and currently a stub implementation.
501#[derive(Debug, Clone)]
502pub struct LodAdt {
503 /// Detected ADT version
504 pub version: AdtVersion,
505}
506
507// ============================================================================
508// Type Aliases for Spec Compliance
509// ============================================================================
510//
511// The split file specification uses names like `TextureAdt` and `ObjectAdt`,
512// while our implementation uses `Tex0Adt` and `Obj0Adt` to reflect the actual
513// file naming convention (`_tex0.adt`, `_obj0.adt`).
514//
515// These type aliases provide compatibility with the specification naming
516// while preserving the more accurate implementation names.
517
518/// Type alias for texture file structure (spec-compliant naming).
519///
520/// This is an alias for [`Tex0Adt`], providing compatibility with the
521/// split file specification which uses `TextureAdt` as the canonical name.
522///
523/// # Example
524///
525/// ```rust
526/// use wow_adt::api::TextureAdt;
527/// // Equivalent to using Tex0Adt
528/// ```
529pub type TextureAdt = Tex0Adt;
530
531/// Type alias for object file structure (spec-compliant naming).
532///
533/// This is an alias for [`Obj0Adt`], providing compatibility with the
534/// split file specification which uses `ObjectAdt` as the canonical name.
535///
536/// # Example
537///
538/// ```rust
539/// use wow_adt::api::ObjectAdt;
540/// // Equivalent to using Obj0Adt
541/// ```
542pub type ObjectAdt = Obj0Adt;
543
544/// Parsed ADT file with type-safe variant access.
545///
546/// Different ADT file types are represented as enum variants, allowing
547/// type-safe access to file-specific data.
548///
549/// # Variants
550///
551/// - [`Root`](ParsedAdt::Root) - Main terrain file (all versions)
552/// - [`Tex0`](ParsedAdt::Tex0) - Texture data (Cataclysm+)
553/// - [`Tex1`](ParsedAdt::Tex1) - Additional textures (Cataclysm+)
554/// - [`Obj0`](ParsedAdt::Obj0) - Object placements (Cataclysm+)
555/// - [`Obj1`](ParsedAdt::Obj1) - Additional objects (Cataclysm+)
556/// - [`Lod`](ParsedAdt::Lod) - Level-of-detail (Cataclysm+)
557///
558/// # Example
559///
560/// ```no_run
561/// use wow_adt::api::{parse_adt, ParsedAdt};
562/// use std::fs::File;
563///
564/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
565/// let mut file = File::open("terrain.adt")?;
566/// let adt = parse_adt(&mut file)?;
567///
568/// match adt {
569/// ParsedAdt::Root(root) => {
570/// println!("Root ADT with {} chunks", root.mcnk_chunks.len());
571/// }
572/// ParsedAdt::Tex0(tex) => {
573/// println!("Texture file with {} textures", tex.textures.len());
574/// }
575/// ParsedAdt::Obj0(obj) => {
576/// println!("Object file with {} models", obj.models.len());
577/// }
578/// _ => {}
579/// }
580/// # Ok(())
581/// # }
582/// ```
583#[derive(Debug, Clone)]
584pub enum ParsedAdt {
585 /// Root ADT file (main terrain)
586 Root(Box<RootAdt>),
587
588 /// Texture file 0 (Cataclysm+)
589 Tex0(Tex0Adt),
590
591 /// Texture file 1 (Cataclysm+)
592 Tex1(Tex0Adt),
593
594 /// Object file 0 (Cataclysm+)
595 Obj0(Obj0Adt),
596
597 /// Object file 1 (Cataclysm+)
598 Obj1(Obj0Adt),
599
600 /// Level-of-detail file (Cataclysm+)
601 Lod(LodAdt),
602}
603
604impl ParsedAdt {
605 /// Check if this is a root ADT file.
606 #[must_use]
607 pub const fn is_root(&self) -> bool {
608 matches!(self, Self::Root(_))
609 }
610
611 /// Check if this is a split file (Cataclysm+).
612 #[must_use]
613 pub const fn is_split(&self) -> bool {
614 !self.is_root()
615 }
616
617 /// Get the file type.
618 #[must_use]
619 pub const fn file_type(&self) -> AdtFileType {
620 match self {
621 Self::Root(_) => AdtFileType::Root,
622 Self::Tex0(_) => AdtFileType::Tex0,
623 Self::Tex1(_) => AdtFileType::Tex1,
624 Self::Obj0(_) => AdtFileType::Obj0,
625 Self::Obj1(_) => AdtFileType::Obj1,
626 Self::Lod(_) => AdtFileType::Lod,
627 }
628 }
629
630 /// Get the ADT version.
631 #[must_use]
632 pub fn version(&self) -> AdtVersion {
633 match self {
634 Self::Root(r) => r.version,
635 Self::Tex0(t) | Self::Tex1(t) => t.version,
636 Self::Obj0(o) | Self::Obj1(o) => o.version,
637 Self::Lod(l) => l.version,
638 }
639 }
640}
641
642/// Diagnostic metadata for format research and debugging.
643///
644/// Contains performance metrics and warnings collected during parsing.
645/// Useful for format analysis, debugging, and performance profiling.
646///
647/// # Example
648///
649/// ```no_run
650/// use wow_adt::api::parse_adt_with_metadata;
651/// use std::fs::File;
652///
653/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
654/// let mut file = File::open("terrain.adt")?;
655/// let (adt, metadata) = parse_adt_with_metadata(&mut file)?;
656///
657/// println!("Version: {:?}", metadata.version);
658/// println!("Discovery: {:?}", metadata.discovery_duration);
659/// println!("Parse: {:?}", metadata.parse_duration);
660/// println!("Chunks: {}", metadata.chunk_count);
661///
662/// for warning in &metadata.warnings {
663/// eprintln!("Warning: {}", warning);
664/// }
665/// # Ok(())
666/// # }
667/// ```
668#[derive(Debug, Clone)]
669pub struct AdtMetadata {
670 /// Detected ADT version
671 pub version: AdtVersion,
672
673 /// Detected file type
674 pub file_type: AdtFileType,
675
676 /// Total number of chunks discovered
677 pub chunk_count: usize,
678
679 /// Time spent in discovery phase
680 pub discovery_duration: Duration,
681
682 /// Time spent in parse phase
683 pub parse_duration: Duration,
684
685 /// Warning messages collected during parsing
686 pub warnings: Vec<String>,
687
688 /// Full chunk discovery results
689 pub discovery: ChunkDiscovery,
690}
691
692/// Parse ADT file with automatic version detection and type-safe chunk extraction.
693///
694/// This is the primary entry point for parsing ADT files. It automatically detects
695/// the file type and version, then parses all chunks into type-safe structures.
696///
697/// # Arguments
698///
699/// * `reader` - Seekable input stream (File, Cursor, etc.)
700///
701/// # Returns
702///
703/// - `Ok(ParsedAdt)` - Successfully parsed ADT with appropriate variant
704/// - `Err(AdtError)` - Parsing failed with detailed error context
705///
706/// # Behavior
707///
708/// 1. **Discovery Phase**: Enumerate chunks, detect version and file type
709/// 2. **Parse Phase**: Extract chunk data into type-safe structures
710/// 3. **Validation**: Verify required chunks and structural integrity
711/// 4. **Error Handling**: Fail-fast on critical errors
712///
713/// # Example
714///
715/// ```no_run
716/// use wow_adt::api::{parse_adt, ParsedAdt};
717/// use std::fs::File;
718/// use std::io::BufReader;
719///
720/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
721/// let file = File::open("world/maps/azeroth/azeroth_32_32.adt")?;
722/// let mut reader = BufReader::new(file);
723/// let adt = parse_adt(&mut reader)?;
724///
725/// match adt {
726/// ParsedAdt::Root(root) => {
727/// println!("Loaded {} terrain chunks", root.mcnk_chunks.len());
728/// println!("Textures: {:?}", root.textures);
729/// }
730/// ParsedAdt::Tex0(tex) => {
731/// println!("Texture file with {} entries", tex.textures.len());
732/// }
733/// _ => {}
734/// }
735/// # Ok(())
736/// # }
737/// ```
738///
739/// # Performance
740///
741/// - Discovery: <10ms for typical files
742/// - Full parse: ~50ms average per file
743/// - Memory: ≤2× raw file size peak
744pub fn parse_adt<R: Read + Seek>(reader: &mut R) -> Result<ParsedAdt> {
745 let (adt, _metadata) = parse_adt_with_metadata(reader)?;
746 Ok(adt)
747}
748
749/// Parse ADT file with diagnostic metadata for debugging and format research.
750///
751/// Similar to [`parse_adt`] but also returns metadata containing:
752/// - Chunk discovery results
753/// - Detected version and reasoning
754/// - File type identification
755/// - Parse timing statistics
756/// - Warning messages
757///
758/// # Arguments
759///
760/// * `reader` - Seekable input stream
761///
762/// # Returns
763///
764/// - `Ok((ParsedAdt, AdtMetadata))` - Parsed ADT with metadata
765/// - `Err(AdtError)` - Parsing failed
766///
767/// # Example
768///
769/// ```no_run
770/// use wow_adt::api::parse_adt_with_metadata;
771/// use std::fs::File;
772///
773/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
774/// let mut file = File::open("terrain.adt")?;
775/// let (adt, metadata) = parse_adt_with_metadata(&mut file)?;
776///
777/// println!("Version: {:?}", metadata.version);
778/// println!("File type: {:?}", metadata.file_type);
779/// println!("Chunks discovered: {}", metadata.chunk_count);
780/// println!("Discovery time: {:?}", metadata.discovery_duration);
781/// println!("Parse time: {:?}", metadata.parse_duration);
782///
783/// for warning in metadata.warnings {
784/// eprintln!("Warning: {}", warning);
785/// }
786/// # Ok(())
787/// # }
788/// ```
789pub fn parse_adt_with_metadata<R: Read + Seek>(reader: &mut R) -> Result<(ParsedAdt, AdtMetadata)> {
790 // Discovery phase
791 let discovery_start = Instant::now();
792 let discovery = discover_chunks(reader)?;
793 let discovery_duration = discovery_start.elapsed();
794
795 // Detect version and file type
796 let version = AdtVersion::from_discovery(&discovery);
797 let file_type = AdtFileType::from_discovery(&discovery);
798
799 log::debug!(
800 "Discovered {} chunks, version: {:?}, file type: {:?}",
801 discovery.total_chunks,
802 version,
803 file_type
804 );
805
806 // Parse phase - route to appropriate parser
807 let parse_start = Instant::now();
808 let (adt, warnings) = match file_type {
809 AdtFileType::Root => {
810 let (root, warnings) = crate::root_parser::parse_root_adt(reader, &discovery, version)?;
811 (ParsedAdt::Root(Box::new(root)), warnings)
812 }
813 AdtFileType::Tex0 | AdtFileType::Tex1 => {
814 let (tex, warnings) = crate::split_parser::parse_tex_adt(reader, &discovery, version)?;
815 let adt = if file_type == AdtFileType::Tex0 {
816 ParsedAdt::Tex0(tex)
817 } else {
818 ParsedAdt::Tex1(tex)
819 };
820 (adt, warnings)
821 }
822 AdtFileType::Obj0 | AdtFileType::Obj1 => {
823 let (obj, warnings) = crate::split_parser::parse_obj_adt(reader, &discovery, version)?;
824 let adt = if file_type == AdtFileType::Obj0 {
825 ParsedAdt::Obj0(obj)
826 } else {
827 ParsedAdt::Obj1(obj)
828 };
829 (adt, warnings)
830 }
831 AdtFileType::Lod => {
832 let (lod, warnings) = crate::split_parser::parse_lod_adt(reader, &discovery, version)?;
833 (ParsedAdt::Lod(lod), warnings)
834 }
835 };
836 let parse_duration = parse_start.elapsed();
837
838 let metadata = AdtMetadata {
839 version,
840 file_type,
841 chunk_count: discovery.total_chunks,
842 discovery_duration,
843 parse_duration,
844 warnings,
845 discovery,
846 };
847
848 Ok((adt, metadata))
849}
850
851#[cfg(test)]
852mod tests {
853 use super::*;
854
855 #[test]
856 fn test_parsed_adt_is_root() {
857 let root = RootAdt {
858 version: AdtVersion::WotLK,
859 mhdr: MhdrChunk::default(),
860 mcin: McinChunk {
861 entries: McinChunk::default().entries,
862 },
863 textures: vec![],
864 models: vec![],
865 model_indices: vec![],
866 wmos: vec![],
867 wmo_indices: vec![],
868 doodad_placements: vec![],
869 wmo_placements: vec![],
870 mcnk_chunks: vec![],
871 flight_bounds: None,
872 water_data: None,
873 texture_flags: None,
874 texture_amplifier: None,
875 texture_params: None,
876 blend_mesh_headers: None,
877 blend_mesh_bounds: None,
878 blend_mesh_vertices: None,
879 blend_mesh_indices: None,
880 };
881
882 let parsed = ParsedAdt::Root(Box::new(root));
883 assert!(parsed.is_root());
884 assert!(!parsed.is_split());
885 assert_eq!(parsed.file_type(), AdtFileType::Root);
886 }
887
888 #[test]
889 fn test_parsed_adt_is_split() {
890 let tex = Tex0Adt {
891 version: AdtVersion::Cataclysm,
892 textures: vec![],
893 texture_params: None,
894 mcnk_textures: vec![],
895 };
896
897 let parsed = ParsedAdt::Tex0(tex);
898 assert!(!parsed.is_root());
899 assert!(parsed.is_split());
900 assert_eq!(parsed.file_type(), AdtFileType::Tex0);
901 }
902
903 #[test]
904 fn test_root_adt_helpers() {
905 let root = RootAdt {
906 version: AdtVersion::WotLK,
907 mhdr: MhdrChunk::default(),
908 mcin: McinChunk {
909 entries: McinChunk::default().entries,
910 },
911 textures: vec!["texture1.blp".into(), "texture2.blp".into()],
912 models: vec!["model1.m2".into()],
913 model_indices: vec![0],
914 wmos: vec![],
915 wmo_indices: vec![],
916 doodad_placements: vec![],
917 wmo_placements: vec![],
918 mcnk_chunks: vec![],
919 flight_bounds: None,
920 water_data: None,
921 texture_flags: None,
922 texture_amplifier: None,
923 texture_params: None,
924 blend_mesh_headers: None,
925 blend_mesh_bounds: None,
926 blend_mesh_vertices: None,
927 blend_mesh_indices: None,
928 };
929
930 assert_eq!(root.texture_count(), 2);
931 assert_eq!(root.model_count(), 1);
932 assert_eq!(root.wmo_count(), 0);
933 assert_eq!(root.terrain_chunk_count(), 0);
934 assert!(!root.has_water());
935 assert!(!root.has_flight_bounds());
936 }
937}