use crate::error::Result;
use crate::version::WmoVersion;
use crate::wmo_group_types::WmoGroup;
use crate::wmo_types::{WmoFlags, WmoRoot};
use crate::wmo_group_types::WmoGroupFlags;
pub struct WmoValidator;
impl Default for WmoValidator {
fn default() -> Self {
Self::new()
}
}
impl WmoValidator {
pub fn new() -> Self {
Self
}
pub fn validate_root(&self, wmo: &WmoRoot) -> Result<ValidationReport> {
let mut report = ValidationReport::new();
if wmo.version < WmoVersion::min_supported() || wmo.version > WmoVersion::max_supported() {
report.add_error(ValidationError::UnsupportedVersion(wmo.version.to_raw()));
}
if wmo.materials.len() != wmo.header.n_materials as usize {
report.add_error(ValidationError::CountMismatch {
field: "materials".to_string(),
expected: wmo.header.n_materials,
actual: wmo.materials.len() as u32,
});
}
if wmo.groups.len() != wmo.header.n_groups as usize {
report.add_error(ValidationError::CountMismatch {
field: "groups".to_string(),
expected: wmo.header.n_groups,
actual: wmo.groups.len() as u32,
});
}
if wmo.portals.len() != wmo.header.n_portals as usize {
report.add_error(ValidationError::CountMismatch {
field: "portals".to_string(),
expected: wmo.header.n_portals,
actual: wmo.portals.len() as u32,
});
}
if wmo.lights.len() != wmo.header.n_lights as usize {
report.add_error(ValidationError::CountMismatch {
field: "lights".to_string(),
expected: wmo.header.n_lights,
actual: wmo.lights.len() as u32,
});
}
if wmo.doodad_defs.len() != wmo.header.n_doodad_defs as usize {
report.add_error(ValidationError::CountMismatch {
field: "doodad_defs".to_string(),
expected: wmo.header.n_doodad_defs,
actual: wmo.doodad_defs.len() as u32,
});
}
if wmo.doodad_sets.len() != wmo.header.n_doodad_sets as usize {
report.add_error(ValidationError::CountMismatch {
field: "doodad_sets".to_string(),
expected: wmo.header.n_doodad_sets,
actual: wmo.doodad_sets.len() as u32,
});
}
for (i, material) in wmo.materials.iter().enumerate() {
const SPECIAL_TEXTURE_THRESHOLD: u32 = 0xFF000000;
if material.texture1 != 0
&& material.texture1 < SPECIAL_TEXTURE_THRESHOLD
&& material.texture1 as usize >= wmo.textures.len()
{
report.add_error(ValidationError::InvalidReference {
field: format!("material[{i}].texture1"),
value: material.texture1,
max: wmo.textures.len() as u32 - 1,
});
}
if material.texture2 != 0
&& material.texture2 < SPECIAL_TEXTURE_THRESHOLD
&& material.texture2 as usize >= wmo.textures.len()
{
report.add_error(ValidationError::InvalidReference {
field: format!("material[{i}].texture2"),
value: material.texture2,
max: wmo.textures.len() as u32 - 1,
});
}
if material.shader > 20 && material.shader < SPECIAL_TEXTURE_THRESHOLD {
report.add_warning(ValidationWarning::UnusualValue {
field: format!("material[{i}].shader"),
value: material.shader,
explanation: "Shader ID is unusually high".to_string(),
});
}
}
for (i, set) in wmo.doodad_sets.iter().enumerate() {
let end_index = set.start_doodad + set.n_doodads;
if end_index > wmo.doodad_defs.len() as u32 {
report.add_error(ValidationError::InvalidReference {
field: format!("doodad_set[{i}]"),
value: end_index,
max: wmo.doodad_defs.len() as u32,
});
}
}
for (i, portal_ref) in wmo.portal_references.iter().enumerate() {
if portal_ref.portal_index as usize >= wmo.portals.len() {
report.add_error(ValidationError::InvalidReference {
field: format!("portal_reference[{i}].portal_index"),
value: portal_ref.portal_index as u32,
max: wmo.portals.len() as u32 - 1,
});
}
if portal_ref.group_index as usize >= wmo.groups.len() {
report.add_error(ValidationError::InvalidReference {
field: format!("portal_reference[{i}].group_index"),
value: portal_ref.group_index as u32,
max: wmo.groups.len() as u32 - 1,
});
}
if portal_ref.side > 1 {
report.add_error(ValidationError::InvalidValue {
field: format!("portal_reference[{i}].side"),
value: portal_ref.side as u32,
explanation: "Portal side must be 0 or 1".to_string(),
});
}
}
if wmo.header.flags.contains(WmoFlags::HAS_SKYBOX) && wmo.skybox.is_none() {
report.add_warning(ValidationWarning::FlagInconsistency {
flag: "HAS_SKYBOX".to_string(),
field: "skybox".to_string(),
explanation: "HAS_SKYBOX flag is set but no skybox model is defined".to_string(),
});
}
if !wmo.header.flags.contains(WmoFlags::HAS_SKYBOX) && wmo.skybox.is_some() {
report.add_warning(ValidationWarning::FlagInconsistency {
flag: "HAS_SKYBOX".to_string(),
field: "skybox".to_string(),
explanation: "Skybox model is defined but HAS_SKYBOX flag is not set".to_string(),
});
}
for (i, portal) in wmo.portals.iter().enumerate() {
if portal.vertices.is_empty() {
report.add_warning(ValidationWarning::UnusualStructure {
field: format!("portal[{i}]"),
explanation: "Portal has no vertices".to_string(),
});
}
}
if wmo.visible_block_lists.is_empty() && !wmo.portals.is_empty() {
report.add_warning(ValidationWarning::MissingData {
field: "visible_block_lists".to_string(),
explanation: "No visible block lists defined but portals exist".to_string(),
});
}
for (i, portal) in wmo.portals.iter().enumerate() {
let normal = &portal.normal;
let length_squared = normal.x * normal.x + normal.y * normal.y + normal.z * normal.z;
if (length_squared - 1.0).abs() > 0.01 {
report.add_warning(ValidationWarning::UnusualValue {
field: format!("portal[{i}].normal"),
value: length_squared as u32,
explanation: "Portal normal is not normalized".to_string(),
});
}
}
if wmo.bounding_box.min.x > wmo.bounding_box.max.x
|| wmo.bounding_box.min.y > wmo.bounding_box.max.y
|| wmo.bounding_box.min.z > wmo.bounding_box.max.z
{
report.add_error(ValidationError::InvalidBoundingBox {
min: format!(
"({}, {}, {})",
wmo.bounding_box.min.x, wmo.bounding_box.min.y, wmo.bounding_box.min.z
),
max: format!(
"({}, {}, {})",
wmo.bounding_box.max.x, wmo.bounding_box.max.y, wmo.bounding_box.max.z
),
});
}
Ok(report)
}
pub fn validate_group(&self, group: &WmoGroup) -> Result<ValidationReport> {
let mut report = ValidationReport::new();
if group.vertices.is_empty() {
report.add_error(ValidationError::EmptyData {
field: "vertices".to_string(),
explanation: "Group has no vertices".to_string(),
});
}
if group.indices.is_empty() {
report.add_error(ValidationError::EmptyData {
field: "indices".to_string(),
explanation: "Group has no indices".to_string(),
});
}
if group.batches.is_empty() {
report.add_warning(ValidationWarning::EmptyData {
field: "batches".to_string(),
explanation: "Group has no batches".to_string(),
});
}
for (i, batch) in group.batches.iter().enumerate() {
let end_index = batch.start_index + batch.count as u32;
if end_index > group.indices.len() as u32 {
report.add_error(ValidationError::InvalidReference {
field: format!("batch[{i}].indices"),
value: end_index,
max: group.indices.len() as u32,
});
}
if batch.end_vertex as usize > group.vertices.len() {
report.add_error(ValidationError::InvalidReference {
field: format!("batch[{i}].end_vertex"),
value: batch.end_vertex as u32,
max: group.vertices.len() as u32,
});
}
}
for (i, &index) in group.indices.iter().enumerate() {
if index as usize >= group.vertices.len() {
report.add_error(ValidationError::InvalidReference {
field: format!("indices[{i}]"),
value: index as u32,
max: group.vertices.len() as u32 - 1,
});
}
}
if !group.normals.is_empty() && group.normals.len() != group.vertices.len() {
report.add_error(ValidationError::CountMismatch {
field: "normals".to_string(),
expected: group.vertices.len() as u32,
actual: group.normals.len() as u32,
});
}
if !group.tex_coords.is_empty() && group.tex_coords.len() != group.vertices.len() {
report.add_error(ValidationError::CountMismatch {
field: "tex_coords".to_string(),
expected: group.vertices.len() as u32,
actual: group.tex_coords.len() as u32,
});
}
if let Some(colors) = &group.vertex_colors
&& colors.len() != group.vertices.len()
{
report.add_error(ValidationError::CountMismatch {
field: "vertex_colors".to_string(),
expected: group.vertices.len() as u32,
actual: colors.len() as u32,
});
}
if group.header.flags.contains(WmoGroupFlags::HAS_NORMALS) && group.normals.is_empty() {
report.add_warning(ValidationWarning::FlagInconsistency {
flag: "HAS_NORMALS".to_string(),
field: "normals".to_string(),
explanation: "HAS_NORMALS flag is set but no normals are present".to_string(),
});
}
if !group.header.flags.contains(WmoGroupFlags::HAS_NORMALS) && !group.normals.is_empty() {
report.add_warning(ValidationWarning::FlagInconsistency {
flag: "HAS_NORMALS".to_string(),
field: "normals".to_string(),
explanation: "Normals are present but HAS_NORMALS flag is not set".to_string(),
});
}
if group
.header
.flags
.contains(WmoGroupFlags::HAS_VERTEX_COLORS)
&& group.vertex_colors.is_none()
{
report.add_warning(ValidationWarning::FlagInconsistency {
flag: "HAS_VERTEX_COLORS".to_string(),
field: "vertex_colors".to_string(),
explanation: "HAS_VERTEX_COLORS flag is set but no vertex colors are present"
.to_string(),
});
}
if !group
.header
.flags
.contains(WmoGroupFlags::HAS_VERTEX_COLORS)
&& group.vertex_colors.is_some()
{
report.add_warning(ValidationWarning::FlagInconsistency {
flag: "HAS_VERTEX_COLORS".to_string(),
field: "vertex_colors".to_string(),
explanation: "Vertex colors are present but HAS_VERTEX_COLORS flag is not set"
.to_string(),
});
}
if group.header.flags.contains(WmoGroupFlags::HAS_DOODADS) && group.doodad_refs.is_none() {
report.add_warning(ValidationWarning::FlagInconsistency {
flag: "HAS_DOODADS".to_string(),
field: "doodad_refs".to_string(),
explanation: "HAS_DOODADS flag is set but no doodad references are present"
.to_string(),
});
}
if !group.header.flags.contains(WmoGroupFlags::HAS_DOODADS) && group.doodad_refs.is_some() {
report.add_warning(ValidationWarning::FlagInconsistency {
flag: "HAS_DOODADS".to_string(),
field: "doodad_refs".to_string(),
explanation: "Doodad references are present but HAS_DOODADS flag is not set"
.to_string(),
});
}
if group.header.flags.contains(WmoGroupFlags::HAS_WATER) && group.liquid.is_none() {
report.add_warning(ValidationWarning::FlagInconsistency {
flag: "HAS_WATER".to_string(),
field: "liquid".to_string(),
explanation: "HAS_WATER flag is set but no liquid data is present".to_string(),
});
}
if !group.header.flags.contains(WmoGroupFlags::HAS_WATER) && group.liquid.is_some() {
report.add_warning(ValidationWarning::FlagInconsistency {
flag: "HAS_WATER".to_string(),
field: "liquid".to_string(),
explanation: "Liquid data is present but HAS_WATER flag is not set".to_string(),
});
}
if group.header.bounding_box.min.x > group.header.bounding_box.max.x
|| group.header.bounding_box.min.y > group.header.bounding_box.max.y
|| group.header.bounding_box.min.z > group.header.bounding_box.max.z
{
report.add_error(ValidationError::InvalidBoundingBox {
min: format!(
"({}, {}, {})",
group.header.bounding_box.min.x,
group.header.bounding_box.min.y,
group.header.bounding_box.min.z
),
max: format!(
"({}, {}, {})",
group.header.bounding_box.max.x,
group.header.bounding_box.max.y,
group.header.bounding_box.max.z
),
});
}
for (i, vertex) in group.vertices.iter().enumerate() {
if vertex.x < group.header.bounding_box.min.x
|| vertex.x > group.header.bounding_box.max.x
|| vertex.y < group.header.bounding_box.min.y
|| vertex.y > group.header.bounding_box.max.y
|| vertex.z < group.header.bounding_box.min.z
|| vertex.z > group.header.bounding_box.max.z
{
report.add_warning(ValidationWarning::OutOfBounds {
field: format!("vertex[{i}]"),
value: format!("({}, {}, {})", vertex.x, vertex.y, vertex.z),
bounds: format!(
"({}, {}, {}) - ({}, {}, {})",
group.header.bounding_box.min.x,
group.header.bounding_box.min.y,
group.header.bounding_box.min.z,
group.header.bounding_box.max.x,
group.header.bounding_box.max.y,
group.header.bounding_box.max.z
),
});
}
}
Ok(report)
}
}
#[derive(Debug)]
pub struct ValidationReport {
pub errors: Vec<ValidationError>,
pub warnings: Vec<ValidationWarning>,
}
impl Default for ValidationReport {
fn default() -> Self {
Self::new()
}
}
impl ValidationReport {
pub fn new() -> Self {
Self {
errors: Vec::new(),
warnings: Vec::new(),
}
}
pub fn add_error(&mut self, error: ValidationError) {
self.errors.push(error);
}
pub fn add_warning(&mut self, warning: ValidationWarning) {
self.warnings.push(warning);
}
pub fn has_errors(&self) -> bool {
!self.errors.is_empty()
}
pub fn has_warnings(&self) -> bool {
!self.warnings.is_empty()
}
pub fn error_count(&self) -> usize {
self.errors.len()
}
pub fn warning_count(&self) -> usize {
self.warnings.len()
}
pub fn print(&self) {
if self.has_errors() {
println!("Validation Errors:");
for error in &self.errors {
println!(" - {error}");
}
}
if self.has_warnings() {
println!("Validation Warnings:");
for warning in &self.warnings {
println!(" - {warning}");
}
}
if !self.has_errors() && !self.has_warnings() {
println!("No validation issues found.");
}
}
}
#[derive(Debug)]
pub enum ValidationError {
UnsupportedVersion(u32),
CountMismatch {
field: String,
expected: u32,
actual: u32,
},
InvalidReference { field: String, value: u32, max: u32 },
InvalidValue {
field: String,
value: u32,
explanation: String,
},
InvalidBoundingBox { min: String, max: String },
EmptyData { field: String, explanation: String },
}
impl std::fmt::Display for ValidationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UnsupportedVersion(version) => write!(f, "Unsupported WMO version: {version}"),
Self::CountMismatch {
field,
expected,
actual,
} => {
write!(
f,
"Count mismatch for {field}: expected {expected}, found {actual}"
)
}
Self::InvalidReference { field, value, max } => {
write!(
f,
"Invalid reference in {field}: {value}, max allowed: {max}"
)
}
Self::InvalidValue {
field,
value,
explanation,
} => {
write!(f, "Invalid value in {field}: {value} ({explanation})")
}
Self::InvalidBoundingBox { min, max } => {
write!(f, "Invalid bounding box: min {min} exceeds max {max}")
}
Self::EmptyData { field, explanation } => {
write!(f, "Empty data for {field}: {explanation}")
}
}
}
}
#[derive(Debug)]
pub enum ValidationWarning {
FlagInconsistency {
flag: String,
field: String,
explanation: String,
},
UnusualValue {
field: String,
value: u32,
explanation: String,
},
UnusualStructure { field: String, explanation: String },
MissingData { field: String, explanation: String },
EmptyData { field: String, explanation: String },
OutOfBounds {
field: String,
value: String,
bounds: String,
},
}
impl std::fmt::Display for ValidationWarning {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::FlagInconsistency {
flag,
field,
explanation,
} => {
write!(f, "Flag inconsistency with {flag}: {field} ({explanation})")
}
Self::UnusualValue {
field,
value,
explanation,
} => {
write!(f, "Unusual value in {field}: {value} ({explanation})")
}
Self::UnusualStructure { field, explanation } => {
write!(f, "Unusual structure in {field}: {explanation}")
}
Self::MissingData { field, explanation } => {
write!(f, "Missing data for {field}: {explanation}")
}
Self::EmptyData { field, explanation } => {
write!(f, "Empty data for {field}: {explanation}")
}
Self::OutOfBounds {
field,
value,
bounds,
} => {
write!(f, "{field} is out of bounds: {value} not within {bounds}")
}
}
}
}