use crate::Adt;
use crate::error::{AdtError, Result};
use crate::split_adt::SplitAdtType;
use crate::version::AdtVersion;
use std::collections::HashSet;
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ValidationLevel {
Basic,
#[default]
Standard,
Strict,
}
pub fn validate_adt(adt: &Adt, level: ValidationLevel) -> Result<ValidationReport> {
validate_adt_with_context(adt, level, None::<&Path>)
}
pub fn validate_adt_with_context<P: AsRef<Path>>(
adt: &Adt,
level: ValidationLevel,
file_path: Option<P>,
) -> Result<ValidationReport> {
let mut report = ValidationReport::new();
let file_type = file_path
.as_ref()
.map(|p| SplitAdtType::from_filename(&p.as_ref().to_string_lossy()));
basic_validation(adt, &mut report, file_type)?;
if level == ValidationLevel::Basic {
return Ok(report);
}
xref_validation(adt, &mut report)?;
if level == ValidationLevel::Strict {
strict_validation(adt, &mut report)?;
}
Ok(report)
}
fn basic_validation(
adt: &Adt,
report: &mut ValidationReport,
file_type: Option<SplitAdtType>,
) -> Result<()> {
match file_type {
Some(SplitAdtType::Obj0) | Some(SplitAdtType::Obj1) => {
return Ok(());
}
Some(SplitAdtType::Tex0) | Some(SplitAdtType::Tex1) => {
return Ok(());
}
Some(SplitAdtType::Lod) => {
return Ok(());
}
_ => {
}
}
if adt.mhdr.is_none() {
report.add_error("Missing MHDR chunk".to_string());
return Err(AdtError::MissingChunk("MHDR".to_string()));
}
if adt.mcnk_chunks.is_empty() {
report.add_error("No MCNK chunks found".to_string());
return Err(AdtError::ValidationError(
"No MCNK chunks found".to_string(),
));
}
if adt.mcnk_chunks.len() != 256 {
report.add_warning(format!(
"Expected 256 MCNK chunks for a complete map tile, found {}",
adt.mcnk_chunks.len()
));
}
match adt.version() {
AdtVersion::TBC | AdtVersion::WotLK | AdtVersion::Cataclysm => {
if let Some(ref mhdr) = adt.mhdr {
if adt.version() >= AdtVersion::TBC && mhdr.mfbo_offset.is_none() {
report.add_warning("TBC+ ADT should have MFBO offset in MHDR".to_string());
}
if adt.version() >= AdtVersion::WotLK && mhdr.mh2o_offset.is_none() {
report.add_warning("WotLK+ ADT should have MH2O offset in MHDR".to_string());
}
if adt.version() >= AdtVersion::Cataclysm && mhdr.mtfx_offset.is_none() {
report
.add_warning("Cataclysm+ ADT should have MTFX offset in MHDR".to_string());
}
}
}
_ => {}
}
if let Some(ref mhdr) = adt.mhdr {
if mhdr.mcin_offset > 0 && adt.mcin.is_none() {
report.add_warning("MHDR has MCIN offset but no MCIN chunk found".to_string());
}
if mhdr.mtex_offset > 0 && adt.mtex.is_none() {
report.add_warning("MHDR has MTEX offset but no MTEX chunk found".to_string());
}
if mhdr.mmdx_offset > 0 && adt.mmdx.is_none() {
report.add_warning("MHDR has MMDX offset but no MMDX chunk found".to_string());
}
}
Ok(())
}
fn xref_validation(adt: &Adt, report: &mut ValidationReport) -> Result<()> {
for (i, chunk) in adt.mcnk_chunks.iter().enumerate() {
let expected_x = (i % 16) as u32;
let expected_y = (i / 16) as u32;
if chunk.ix != expected_x || chunk.iy != expected_y {
report.add_warning(format!(
"MCNK chunk at index {} has incorrect indices [{}, {}], expected [{}, {}]",
i, chunk.ix, chunk.iy, expected_x, expected_y
));
}
}
if let Some(ref mcin) = adt.mcin {
for (i, entry) in mcin.entries.iter().enumerate() {
if entry.offset == 0 && entry.size == 0 {
continue;
}
if entry.size < 8 || entry.size > 1024 * 1024 {
report.add_warning(format!(
"MCIN entry {} has suspicious size: {}",
i, entry.size
));
}
}
}
if let Some(ref _mmdx) = adt.mmdx {
if let Some(ref mmid) = adt.mmid {
for (i, &offset) in mmid.offsets.iter().enumerate() {
let found = false;
if !found {
report.add_info(format!("MMID entry {i} references offset {offset} in MMDX"));
}
}
}
}
if let Some(ref _mwmo) = adt.mwmo {
if let Some(ref mwid) = adt.mwid {
for (i, &offset) in mwid.offsets.iter().enumerate() {
let found = false;
if !found {
report.add_info(format!("MWID entry {i} references offset {offset} in MWMO"));
}
}
}
}
if let Some(ref mddf) = adt.mddf {
if let Some(ref mmid) = adt.mmid {
for (i, doodad) in mddf.doodads.iter().enumerate() {
if doodad.name_id as usize >= mmid.offsets.len() {
report.add_error(format!(
"MDDF entry {} references invalid MMID index: {}",
i, doodad.name_id
));
}
}
} else if !mddf.doodads.is_empty() {
report.add_error("MDDF references doodads but no MMID chunk found".to_string());
}
}
if let Some(ref modf) = adt.modf {
if let Some(ref mwid) = adt.mwid {
for (i, model) in modf.models.iter().enumerate() {
if model.name_id as usize >= mwid.offsets.len() {
report.add_error(format!(
"MODF entry {} references invalid MWID index: {}",
i, model.name_id
));
}
}
} else if !modf.models.is_empty() {
report.add_error("MODF references WMOs but no MWID chunk found".to_string());
}
}
if let Some(ref mtex) = adt.mtex {
let texture_count = mtex.filenames.len();
for (i, chunk) in adt.mcnk_chunks.iter().enumerate() {
for (j, layer) in chunk.texture_layers.iter().enumerate() {
if layer.texture_id as usize >= texture_count {
report.add_error(format!(
"MCNK chunk {}, layer {} references invalid texture ID: {}",
i, j, layer.texture_id
));
}
}
}
}
for (i, chunk) in adt.mcnk_chunks.iter().enumerate() {
if !chunk.doodad_refs.is_empty() {
if let Some(ref mmid) = adt.mmid {
for (j, &doodad_ref) in chunk.doodad_refs.iter().enumerate() {
if doodad_ref as usize >= mmid.offsets.len() {
report.add_error(format!(
"MCNK chunk {i}, doodad ref {j} references invalid MMID index: {doodad_ref}"
));
}
}
} else {
report.add_error(format!(
"MCNK chunk {i} references doodads but no MMID chunk found"
));
}
}
if !chunk.map_obj_refs.is_empty() {
if let Some(ref mwid) = adt.mwid {
for (j, &map_obj_ref) in chunk.map_obj_refs.iter().enumerate() {
if map_obj_ref as usize >= mwid.offsets.len() {
report.add_error(format!(
"MCNK chunk {i}, map object ref {j} references invalid MWID index: {map_obj_ref}"
));
}
}
} else {
report.add_error(format!(
"MCNK chunk {i} references map objects but no MWID chunk found"
));
}
}
}
Ok(())
}
fn strict_validation(adt: &Adt, report: &mut ValidationReport) -> Result<()> {
if let Some(ref mddf) = adt.mddf {
let mut doodad_ids = HashSet::new();
for (i, doodad) in mddf.doodads.iter().enumerate() {
if !doodad_ids.insert(doodad.unique_id) {
report.add_warning(format!(
"MDDF entry {} has duplicate unique ID: {}",
i, doodad.unique_id
));
}
}
}
if let Some(ref modf) = adt.modf {
let mut model_ids = HashSet::new();
for (i, model) in modf.models.iter().enumerate() {
if !model_ids.insert(model.unique_id) {
report.add_warning(format!(
"MODF entry {} has duplicate unique ID: {}",
i, model.unique_id
));
}
}
}
for (i, chunk) in adt.mcnk_chunks.iter().enumerate() {
if chunk.holes != 0 {
report.add_info(format!("MCNK chunk {} has holes: {:#06x}", i, chunk.holes));
}
}
match adt.version() {
AdtVersion::WotLK | AdtVersion::Cataclysm => {
if let Some(ref mh2o) = adt.mh2o {
if mh2o.chunks.len() != 256 {
report.add_error(format!(
"MH2O has {} chunks, expected 256",
mh2o.chunks.len()
));
}
}
}
_ => {}
}
Ok(())
}
#[derive(Debug, Clone)]
pub struct ValidationReport {
pub errors: Vec<String>,
pub warnings: Vec<String>,
pub info: Vec<String>,
}
impl Default for ValidationReport {
fn default() -> Self {
Self::new()
}
}
impl ValidationReport {
pub fn new() -> Self {
Self {
errors: Vec::new(),
warnings: Vec::new(),
info: Vec::new(),
}
}
pub fn add_error(&mut self, message: String) {
self.errors.push(message);
}
pub fn add_warning(&mut self, message: String) {
self.warnings.push(message);
}
pub fn add_info(&mut self, message: String) {
self.info.push(message);
}
pub fn is_valid(&self) -> bool {
self.errors.is_empty()
}
pub fn is_clean(&self) -> bool {
self.errors.is_empty() && self.warnings.is_empty()
}
pub fn format(&self) -> String {
let mut result = String::new();
if self.is_valid() {
result.push_str("Validation passed");
if !self.warnings.is_empty() {
result.push_str(" with warnings");
}
result.push_str(".\n\n");
} else {
result.push_str(&format!(
"Validation failed with {} errors.\n\n",
self.errors.len()
));
}
if !self.errors.is_empty() {
result.push_str("Errors:\n");
for (i, error) in self.errors.iter().enumerate() {
result.push_str(&format!(" {}. {}\n", i + 1, error));
}
result.push('\n');
}
if !self.warnings.is_empty() {
result.push_str("Warnings:\n");
for (i, warning) in self.warnings.iter().enumerate() {
result.push_str(&format!(" {}. {}\n", i + 1, warning));
}
result.push('\n');
}
if !self.info.is_empty() {
result.push_str("Info:\n");
for (i, info) in self.info.iter().enumerate() {
result.push_str(&format!(" {}. {}\n", i + 1, info));
}
}
result
}
}