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
31use 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#[derive(Debug, Clone)]
60pub struct Adt {
61 pub version: AdtVersion,
63 pub mver: MverChunk,
65 pub mhdr: Option<MhdrChunk>,
67 pub mcnk_chunks: Vec<McnkChunk>,
69 pub mcin: Option<McinChunk>,
71 pub mtex: Option<MtexChunk>,
73 pub mmdx: Option<MmdxChunk>,
75 pub mmid: Option<MmidChunk>,
77 pub mwmo: Option<MwmoChunk>,
79 pub mwid: Option<MwidChunk>,
81 pub mddf: Option<MddfChunk>,
83 pub modf: Option<ModfChunk>,
85
86 pub mfbo: Option<MfboChunk>,
89 pub mh2o: Option<AdvancedMh2oChunk>,
91 pub mtfx: Option<MtfxChunk>,
93 pub mamp: Option<MampChunk>,
95 pub mtxp: Option<MtxpChunk>,
97}
98
99impl Adt {
100 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 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 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 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 let file = File::open(path)?;
184 Self::from_reader(file)
185 }
186 }
187 }
188
189 pub fn from_reader<R: Read + Seek>(mut reader: R) -> Result<Self> {
191 let file_size = reader.seek(SeekFrom::End(0))?;
193 reader.seek(SeekFrom::Start(0))?;
194
195 const MIN_ADT_SIZE: u64 = 12 + 8 + 64 + 8; 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 let mver = MverChunk::read(&mut reader)?;
205 let version = AdtVersion::from_mver(mver.version)?;
206
207 reader.seek(SeekFrom::Start(0))?;
209
210 let mut context = ParserContext {
212 reader: &mut reader,
213 version,
214 position: 0,
215 };
216
217 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 let chunk_pos = current_pos - 8; chunks.mcnk_positions.push((chunk_pos, header.size));
269 context.reader.seek(SeekFrom::Current(header.size as i64))?;
271 }
272 b"MFBO" => {
274 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 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 let chunk_data_start = context.reader.stream_position()?;
297 let chunk_start = chunk_data_start - 8; 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 context
307 .reader
308 .seek(SeekFrom::Start(chunk_data_start + header.size as u64))?;
309 chunks.mh2o = Some(AdvancedMh2oChunk { chunks: Vec::new() });
311 }
312 }
313 }
314 b"MTFX" => {
315 let chunk = MtfxChunk::read_with_header(header.clone(), &mut context)?;
317 chunks.mtfx = Some(chunk);
318 }
319 b"MAMP" => {
320 let chunk = MampChunk::read_with_header(header.clone(), &mut context)?;
322 chunks.mamp = Some(chunk);
323 }
324 b"MTXP" => {
325 let chunk = MtxpChunk::read_with_header(header.clone(), &mut context)?;
327 chunks.mtxp = Some(chunk);
328 }
329 _ => {
330 context.reader.seek(SeekFrom::Current(header.size as i64))?;
332 }
333 }
334
335 context.position = current_pos as usize + header.size as usize;
337 }
338
339 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 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 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 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 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;
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 if chunks.mcin.is_none() && !chunks.mcnk_positions.is_empty() {
396 for (chunk_pos, _chunk_size) in chunks.mcnk_positions.iter() {
397 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 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 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 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 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 pub fn version(&self) -> AdtVersion {
477 self.version
478 }
479
480 pub fn mcnk_chunks(&self) -> &[McnkChunk] {
482 &self.mcnk_chunks
483 }
484
485 pub fn mh2o(&self) -> Option<&AdvancedMh2oChunk> {
487 self.mh2o.as_ref()
488 }
489
490 pub fn to_version(&self, target_version: AdtVersion) -> Result<Self> {
492 if self.version == target_version {
493 return Ok(self.clone());
495 }
496
497 convert_adt(self, target_version)
498 }
499
500 pub fn validate(&self) -> Result<()> {
502 validator::validate_adt(self, ValidationLevel::Basic)?;
503 Ok(())
504 }
505
506 pub fn validate_with_report(&self, level: ValidationLevel) -> Result<ValidationReport> {
508 validator::validate_adt(self, level)
509 }
510
511 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#[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)>, 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
549pub(crate) struct ParserContext<'a, R: Read + Seek> {
551 pub reader: &'a mut R,
552 pub version: AdtVersion,
553 pub position: usize,
554}