wow_alchemy_adt/
lib.rs

1use std::fs::File;
2use std::io::{Read, Seek, SeekFrom};
3use std::path::Path;
4
5mod adt_builder;
6mod chunk;
7mod converter;
8mod error;
9mod io_helpers;
10mod liquid_converter;
11mod mcnk_converter;
12mod mcnk_subchunks;
13mod mcnk_writer;
14mod merge;
15mod mh2o;
16mod model_export;
17mod normal_map;
18pub mod split_adt;
19mod streaming;
20mod texture_converter;
21mod validator;
22mod version;
23mod writer;
24
25#[cfg(feature = "parallel")]
26mod parallel;
27
28#[cfg(feature = "extract")]
29pub mod extract;
30
31// Import advanced water chunk type
32use crate::mh2o::Mh2oChunk as AdvancedMh2oChunk;
33pub use mh2o::{Mh2oEntry, Mh2oInstance, WaterLevelData, WaterVertex, WaterVertexData};
34
35pub use adt_builder::{AdtBuilder, create_flat_terrain};
36pub use chunk::*;
37pub use converter::convert_adt;
38pub use error::{AdtError, Result};
39pub use mcnk_converter::{convert_mcnk, convert_mcnk_chunks};
40pub use mcnk_subchunks::*;
41pub use merge::{MergeOptions, extract_portion, merge_adts, merge_chunk};
42pub use model_export::{ModelExportOptions, ModelFormat, export_to_3d};
43pub use normal_map::{
44    NormalChannelEncoding, NormalMapFormat, NormalMapOptions, extract_normal_map,
45};
46pub use streaming::{
47    AdtStreamer, StreamedChunk, count_matching_chunks, iterate_mcnk_chunks, open_adt_stream,
48};
49pub use texture_converter::{convert_alpha_maps, convert_area_id, convert_texture_layers};
50pub use validator::{ValidationLevel, ValidationReport, validate_adt};
51pub use version::AdtVersion;
52
53#[cfg(feature = "parallel")]
54pub use parallel::{ParallelOptions, batch_convert, batch_validate, process_parallel};
55
56// Export split ADT functionality for Cataclysm+ support
57
58/// Main ADT structure that holds all the parsed data for a terrain file
59#[derive(Debug, Clone)]
60pub struct Adt {
61    /// Version of the ADT file
62    pub version: AdtVersion,
63    /// MVER chunk - file version
64    pub mver: MverChunk,
65    /// MHDR chunk - header with offsets to other chunks
66    pub mhdr: Option<MhdrChunk>,
67    /// MCNK chunks - map chunk data (terrain height, texturing, etc.)
68    pub mcnk_chunks: Vec<McnkChunk>,
69    /// MCIN chunk - map chunk index
70    pub mcin: Option<McinChunk>,
71    /// MTEX chunk - texture filenames
72    pub mtex: Option<MtexChunk>,
73    /// MMDX chunk - model filenames
74    pub mmdx: Option<MmdxChunk>,
75    /// MMID chunk - model indices
76    pub mmid: Option<MmidChunk>,
77    /// MWMO chunk - WMO filenames
78    pub mwmo: Option<MwmoChunk>,
79    /// MWID chunk - WMO indices
80    pub mwid: Option<MwidChunk>,
81    /// MDDF chunk - doodad placement information
82    pub mddf: Option<MddfChunk>,
83    /// MODF chunk - model placement information
84    pub modf: Option<ModfChunk>,
85
86    // Version-specific data
87    /// TBC and later - flight boundaries
88    pub mfbo: Option<MfboChunk>,
89    /// WotLK and later - water data
90    pub mh2o: Option<AdvancedMh2oChunk>,
91    /// Cataclysm and later - texture effects
92    pub mtfx: Option<MtfxChunk>,
93    /// Cataclysm and later - texture amplifier
94    pub mamp: Option<MampChunk>,
95    /// MoP and later - texture parameters
96    pub mtxp: Option<MtxpChunk>,
97}
98
99impl Adt {
100    /// Parse an ADT file from a path
101    ///
102    /// For split files (_tex0, _obj0, etc.), this returns a minimal ADT structure
103    /// with only the chunks present in that specific file type.
104    pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self> {
105        let path_str = path.as_ref().to_string_lossy();
106        let file_type = split_adt::SplitAdtType::from_filename(&path_str);
107
108        match file_type {
109            split_adt::SplitAdtType::Obj0 => {
110                // Parse obj0 file
111                let mut file = File::open(path)?;
112                let obj_data = split_adt::SplitAdtParser::parse_obj0(&mut file)?;
113
114                Ok(Adt {
115                    version: AdtVersion::Cataclysm,
116                    mver: MverChunk { version: 18 },
117                    mhdr: Some(MhdrChunk::default()),
118                    mcin: None,
119                    mtex: None,
120                    mmdx: obj_data.mmdx,
121                    mmid: obj_data.mmid,
122                    mwmo: obj_data.mwmo,
123                    mwid: obj_data.mwid,
124                    mddf: obj_data.mddf,
125                    modf: obj_data.modf,
126                    mcnk_chunks: Vec::new(),
127                    mfbo: None,
128                    mh2o: None,
129                    mtfx: None,
130                    mamp: None,
131                    mtxp: None,
132                })
133            }
134            split_adt::SplitAdtType::Tex0 | split_adt::SplitAdtType::Tex1 => {
135                // Parse tex file
136                let mut file = File::open(path)?;
137                let tex_data = split_adt::SplitAdtParser::parse_tex0(&mut file)?;
138
139                Ok(Adt {
140                    version: AdtVersion::Cataclysm,
141                    mver: MverChunk { version: 18 },
142                    mhdr: Some(MhdrChunk::default()),
143                    mcin: None,
144                    mtex: tex_data.mtex,
145                    mmdx: None,
146                    mmid: None,
147                    mwmo: None,
148                    mwid: None,
149                    mddf: None,
150                    modf: None,
151                    mcnk_chunks: Vec::new(),
152                    mfbo: None,
153                    mh2o: None,
154                    mtfx: None,
155                    mamp: None,
156                    mtxp: None,
157                })
158            }
159            split_adt::SplitAdtType::Obj1 | split_adt::SplitAdtType::Lod => {
160                // Return minimal ADT for unsupported split file types
161                Ok(Adt {
162                    version: AdtVersion::Cataclysm,
163                    mver: MverChunk { version: 18 },
164                    mhdr: Some(MhdrChunk::default()),
165                    mcin: None,
166                    mtex: None,
167                    mmdx: None,
168                    mmid: None,
169                    mwmo: None,
170                    mwid: None,
171                    mddf: None,
172                    modf: None,
173                    mcnk_chunks: Vec::new(),
174                    mfbo: None,
175                    mh2o: None,
176                    mtfx: None,
177                    mamp: None,
178                    mtxp: None,
179                })
180            }
181            split_adt::SplitAdtType::Root => {
182                // Parse normal ADT file
183                let file = File::open(path)?;
184                Self::from_reader(file)
185            }
186        }
187    }
188
189    /// Parse an ADT file from any reader that implements Read + Seek
190    pub fn from_reader<R: Read + Seek>(mut reader: R) -> Result<Self> {
191        // Get file size for bounds checking
192        let file_size = reader.seek(SeekFrom::End(0))?;
193        reader.seek(SeekFrom::Start(0))?;
194
195        // Minimum ADT file size check (at least MVER + MHDR chunks)
196        const MIN_ADT_SIZE: u64 = 12 + 8 + 64 + 8; // MVER header + data + MHDR header + data
197        if file_size < MIN_ADT_SIZE {
198            return Err(AdtError::InvalidFileSize(format!(
199                "File too small to be a valid ADT: {file_size} bytes"
200            )));
201        }
202
203        // First, read the MVER chunk to determine the file version
204        let mver = MverChunk::read(&mut reader)?;
205        let version = AdtVersion::from_mver(mver.version)?;
206
207        // Seek back to the beginning for full parsing
208        reader.seek(SeekFrom::Start(0))?;
209
210        // Create a parser context to track our state during parsing
211        let mut context = ParserContext {
212            reader: &mut reader,
213            version,
214            position: 0,
215        };
216
217        // Read the full file
218        let mut chunks = ChunkMap::new();
219
220        while let Ok(header) = ChunkHeader::read(&mut context.reader) {
221            let current_pos = context.reader.stream_position()?;
222
223            match &header.magic {
224                b"MVER" => {
225                    let chunk = MverChunk::read_with_header(header.clone(), &mut context)?;
226                    chunks.mver = Some(chunk);
227                }
228                b"MHDR" => {
229                    let chunk = MhdrChunk::read_with_header(header.clone(), &mut context)?;
230                    chunks.mhdr = Some(chunk);
231                }
232                b"MCIN" => {
233                    let chunk = McinChunk::read_with_header(header.clone(), &mut context)?;
234                    chunks.mcin = Some(chunk);
235                }
236                b"MTEX" => {
237                    let chunk = MtexChunk::read_with_header(header.clone(), &mut context)?;
238                    chunks.mtex = Some(chunk);
239                }
240                b"MMDX" => {
241                    let chunk = MmdxChunk::read_with_header(header.clone(), &mut context)?;
242                    chunks.mmdx = Some(chunk);
243                }
244                b"MMID" => {
245                    let chunk = MmidChunk::read_with_header(header.clone(), &mut context)?;
246                    chunks.mmid = Some(chunk);
247                }
248                b"MWMO" => {
249                    let chunk = MwmoChunk::read_with_header(header.clone(), &mut context)?;
250                    chunks.mwmo = Some(chunk);
251                }
252                b"MWID" => {
253                    let chunk = MwidChunk::read_with_header(header.clone(), &mut context)?;
254                    chunks.mwid = Some(chunk);
255                }
256                b"MDDF" => {
257                    let chunk = MddfChunk::read_with_header(header.clone(), &mut context)?;
258                    chunks.mddf = Some(chunk);
259                }
260                b"MODF" => {
261                    let chunk = ModfChunk::read_with_header(header.clone(), &mut context)?;
262                    chunks.modf = Some(chunk);
263                }
264                b"MCNK" => {
265                    // In Cataclysm+, ADT files may not have MCIN and MCNK chunks appear directly
266                    // Store the position and size for later processing
267                    let chunk_pos = current_pos - 8; // Subtract header size to get chunk start
268                    chunks.mcnk_positions.push((chunk_pos, header.size));
269                    // Skip the chunk data for now
270                    context.reader.seek(SeekFrom::Current(header.size as i64))?;
271                }
272                // Version-specific chunks
273                b"MFBO" => {
274                    // Parse MFBO regardless of initial version - version will be detected later
275                    match MfboChunk::read_with_header(header.clone(), &mut context) {
276                        Ok(chunk) => {
277                            chunks.mfbo = Some(chunk);
278                        }
279                        Err(e) => {
280                            eprintln!(
281                                "Warning: Failed to parse MFBO chunk ({e}), marking as present for version detection"
282                            );
283                            // For version detection purposes, just mark that we found MFBO
284                            // Use default values if parsing fails
285                            chunks.mfbo = Some(MfboChunk {
286                                max: [0; 9],
287                                min: [0; 9],
288                            });
289                            context.reader.seek(SeekFrom::Current(header.size as i64))?;
290                        }
291                    }
292                }
293                b"MH2O" => {
294                    // MH2O is used for water data in WotLK and later
295                    // Get the current position (after reading header)
296                    let chunk_data_start = context.reader.stream_position()?;
297                    let chunk_start = chunk_data_start - 8; // Subtract header size
298
299                    match AdvancedMh2oChunk::read_full(&mut context, chunk_start, header.size) {
300                        Ok(chunk) => {
301                            chunks.mh2o = Some(chunk);
302                        }
303                        Err(e) => {
304                            eprintln!("Warning: Failed to parse MH2O chunk: {e}");
305                            // Skip the chunk data on error
306                            context
307                                .reader
308                                .seek(SeekFrom::Start(chunk_data_start + header.size as u64))?;
309                            // Mark as present for version detection
310                            chunks.mh2o = Some(AdvancedMh2oChunk { chunks: Vec::new() });
311                        }
312                    }
313                }
314                b"MTFX" => {
315                    // Parse MTFX regardless of initial version - version will be detected later
316                    let chunk = MtfxChunk::read_with_header(header.clone(), &mut context)?;
317                    chunks.mtfx = Some(chunk);
318                }
319                b"MAMP" => {
320                    // Parse MAMP regardless of initial version - version will be detected later
321                    let chunk = MampChunk::read_with_header(header.clone(), &mut context)?;
322                    chunks.mamp = Some(chunk);
323                }
324                b"MTXP" => {
325                    // Parse MTXP regardless of initial version - version will be detected later
326                    let chunk = MtxpChunk::read_with_header(header.clone(), &mut context)?;
327                    chunks.mtxp = Some(chunk);
328                }
329                _ => {
330                    // Unknown chunk, skip it
331                    context.reader.seek(SeekFrom::Current(header.size as i64))?;
332                }
333            }
334
335            // Update our position
336            context.position = current_pos as usize + header.size as usize;
337        }
338
339        // Phase 2: Read MCNK chunks using MCIN offsets
340        if let Some(ref mcin) = chunks.mcin {
341            for (i, entry) in mcin.entries.iter().enumerate() {
342                if entry.offset > 0 && entry.size > 0 {
343                    // Validate offset is within file bounds
344                    if entry.offset as u64 + entry.size as u64 > file_size {
345                        eprintln!(
346                            "MCNK chunk {} at offset {} exceeds file size {}",
347                            i, entry.offset, file_size
348                        );
349                        continue;
350                    }
351
352                    // Seek to the MCNK chunk offset
353                    match context.reader.seek(SeekFrom::Start(entry.offset as u64)) {
354                        Ok(_) => {}
355                        Err(e) => {
356                            eprintln!(
357                                "Error seeking to MCNK chunk {} at offset {}: {}",
358                                i, entry.offset, e
359                            );
360                            continue;
361                        }
362                    }
363
364                    // Read the MCNK chunk header
365                    let header = match ChunkHeader::read(&mut context.reader) {
366                        Ok(h) => h,
367                        Err(e) => {
368                            eprintln!("Error reading MCNK chunk {i} header: {e}");
369                            continue;
370                        }
371                    };
372
373                    // Verify it's actually an MCNK chunk
374                    if &header.magic == b"MCNK" {
375                        match McnkChunk::read_with_header(header, &mut context) {
376                            Ok(chunk) => chunks.mcnk.push(chunk),
377                            Err(e) => {
378                                eprintln!("Error reading MCNK chunk {i} content: {e}");
379                                // Continue with other chunks instead of failing completely
380                                continue;
381                            }
382                        }
383                    } else {
384                        eprintln!(
385                            "Expected MCNK at offset {}, found {:?}",
386                            entry.offset,
387                            header.magic_as_string()
388                        );
389                    }
390                }
391            }
392        }
393
394        // Phase 3: If no MCIN but we found direct MCNK chunks, parse them now
395        if chunks.mcin.is_none() && !chunks.mcnk_positions.is_empty() {
396            for (chunk_pos, _chunk_size) in chunks.mcnk_positions.iter() {
397                // Seek to the MCNK chunk
398                match context.reader.seek(SeekFrom::Start(*chunk_pos)) {
399                    Ok(_) => {}
400                    Err(e) => {
401                        eprintln!("Error seeking to direct MCNK chunk at offset {chunk_pos}: {e}");
402                        continue;
403                    }
404                }
405
406                // Read the MCNK chunk header
407                let header = match ChunkHeader::read(&mut context.reader) {
408                    Ok(h) => h,
409                    Err(e) => {
410                        eprintln!(
411                            "Error reading direct MCNK chunk header at offset {chunk_pos}: {e}"
412                        );
413                        continue;
414                    }
415                };
416
417                // Verify it's actually an MCNK chunk
418                if &header.magic == b"MCNK" {
419                    match McnkChunk::read_with_header(header, &mut context) {
420                        Ok(chunk) => {
421                            chunks.mcnk.push(chunk);
422                        }
423                        Err(e) => {
424                            eprintln!(
425                                "Warning: Error reading MCNK chunk at offset {chunk_pos}: {e}"
426                            );
427                            continue;
428                        }
429                    }
430                } else {
431                    eprintln!(
432                        "Warning: Expected MCNK at offset {}, found {:?}",
433                        chunk_pos,
434                        header.magic_as_string()
435                    );
436                }
437            }
438        }
439
440        // Detect version based on chunk presence
441        let has_mcnk_with_mccv = chunks.mcnk.iter().any(|mcnk| mcnk.mccv_offset > 0);
442        let detected_version = AdtVersion::detect_from_chunks_extended(
443            chunks.mfbo.is_some(),
444            chunks.mh2o.is_some(),
445            chunks.mtfx.is_some(),
446            has_mcnk_with_mccv,
447            chunks.mtxp.is_some(),
448            chunks.mamp.is_some(),
449        );
450
451        // Construct the ADT from the parsed chunks
452        let adt = Adt {
453            version: detected_version,
454            mver: chunks.mver.unwrap_or(MverChunk { version: 18 }),
455            mhdr: chunks.mhdr,
456            mcnk_chunks: chunks.mcnk,
457            mcin: chunks.mcin,
458            mtex: chunks.mtex,
459            mmdx: chunks.mmdx,
460            mmid: chunks.mmid,
461            mwmo: chunks.mwmo,
462            mwid: chunks.mwid,
463            mddf: chunks.mddf,
464            modf: chunks.modf,
465            mfbo: chunks.mfbo,
466            mh2o: chunks.mh2o,
467            mtfx: chunks.mtfx,
468            mamp: chunks.mamp,
469            mtxp: chunks.mtxp,
470        };
471
472        Ok(adt)
473    }
474
475    /// Get the version of this ADT file
476    pub fn version(&self) -> AdtVersion {
477        self.version
478    }
479
480    /// Get the MCNK chunks
481    pub fn mcnk_chunks(&self) -> &[McnkChunk] {
482        &self.mcnk_chunks
483    }
484
485    /// Get the MH2O water chunk (WotLK+)
486    pub fn mh2o(&self) -> Option<&AdvancedMh2oChunk> {
487        self.mh2o.as_ref()
488    }
489
490    /// Convert to a specific version
491    pub fn to_version(&self, target_version: AdtVersion) -> Result<Self> {
492        if self.version == target_version {
493            // No conversion needed
494            return Ok(self.clone());
495        }
496
497        convert_adt(self, target_version)
498    }
499
500    /// Validate the ADT data
501    pub fn validate(&self) -> Result<()> {
502        validator::validate_adt(self, ValidationLevel::Basic)?;
503        Ok(())
504    }
505
506    /// Perform comprehensive validation with detailed report
507    pub fn validate_with_report(&self, level: ValidationLevel) -> Result<ValidationReport> {
508        validator::validate_adt(self, level)
509    }
510
511    /// Validate with detailed report and file context
512    pub fn validate_with_report_and_context<P: AsRef<Path>>(
513        &self,
514        level: ValidationLevel,
515        file_path: P,
516    ) -> Result<ValidationReport> {
517        validator::validate_adt_with_context(self, level, Some(file_path))
518    }
519}
520
521/// Helper structure to collect parsed chunks during reading
522#[derive(Default)]
523struct ChunkMap {
524    mver: Option<MverChunk>,
525    mhdr: Option<MhdrChunk>,
526    mcin: Option<McinChunk>,
527    mtex: Option<MtexChunk>,
528    mmdx: Option<MmdxChunk>,
529    mmid: Option<MmidChunk>,
530    mwmo: Option<MwmoChunk>,
531    mwid: Option<MwidChunk>,
532    mddf: Option<MddfChunk>,
533    modf: Option<ModfChunk>,
534    mcnk: Vec<McnkChunk>,
535    mcnk_positions: Vec<(u64, u32)>, // (position, size) for direct MCNK chunks
536    mfbo: Option<MfboChunk>,
537    mh2o: Option<AdvancedMh2oChunk>,
538    mtfx: Option<MtfxChunk>,
539    mamp: Option<MampChunk>,
540    mtxp: Option<MtxpChunk>,
541}
542
543impl ChunkMap {
544    fn new() -> Self {
545        Self::default()
546    }
547}
548
549/// Context for parsing chunks
550pub(crate) struct ParserContext<'a, R: Read + Seek> {
551    pub reader: &'a mut R,
552    pub version: AdtVersion,
553    pub position: usize,
554}