1use crate::error::Result;
2use crate::version::WmoVersion;
3use crate::wmo_group_types::WmoGroup;
4use crate::wmo_types::{WmoFlags, WmoRoot};
5
6use crate::wmo_group_types::WmoGroupFlags;
8
9pub struct WmoValidator;
11
12impl Default for WmoValidator {
13 fn default() -> Self {
14 Self::new()
15 }
16}
17
18impl WmoValidator {
19 pub fn new() -> Self {
21 Self
22 }
23
24 pub fn validate_root(&self, wmo: &WmoRoot) -> Result<ValidationReport> {
26 let mut report = ValidationReport::new();
27
28 if wmo.version < WmoVersion::min_supported() || wmo.version > WmoVersion::max_supported() {
30 report.add_error(ValidationError::UnsupportedVersion(wmo.version.to_raw()));
31 }
32
33 if wmo.materials.len() != wmo.header.n_materials as usize {
35 report.add_error(ValidationError::CountMismatch {
36 field: "materials".to_string(),
37 expected: wmo.header.n_materials,
38 actual: wmo.materials.len() as u32,
39 });
40 }
41
42 if wmo.groups.len() != wmo.header.n_groups as usize {
43 report.add_error(ValidationError::CountMismatch {
44 field: "groups".to_string(),
45 expected: wmo.header.n_groups,
46 actual: wmo.groups.len() as u32,
47 });
48 }
49
50 if wmo.portals.len() != wmo.header.n_portals as usize {
51 report.add_error(ValidationError::CountMismatch {
52 field: "portals".to_string(),
53 expected: wmo.header.n_portals,
54 actual: wmo.portals.len() as u32,
55 });
56 }
57
58 if wmo.lights.len() != wmo.header.n_lights as usize {
59 report.add_error(ValidationError::CountMismatch {
60 field: "lights".to_string(),
61 expected: wmo.header.n_lights,
62 actual: wmo.lights.len() as u32,
63 });
64 }
65
66 if wmo.doodad_defs.len() != wmo.header.n_doodad_defs as usize {
67 report.add_error(ValidationError::CountMismatch {
68 field: "doodad_defs".to_string(),
69 expected: wmo.header.n_doodad_defs,
70 actual: wmo.doodad_defs.len() as u32,
71 });
72 }
73
74 if wmo.doodad_sets.len() != wmo.header.n_doodad_sets as usize {
75 report.add_error(ValidationError::CountMismatch {
76 field: "doodad_sets".to_string(),
77 expected: wmo.header.n_doodad_sets,
78 actual: wmo.doodad_sets.len() as u32,
79 });
80 }
81
82 for (i, material) in wmo.materials.iter().enumerate() {
84 const SPECIAL_TEXTURE_THRESHOLD: u32 = 0xFF000000;
87
88 if material.texture1 != 0
89 && material.texture1 < SPECIAL_TEXTURE_THRESHOLD
90 && material.texture1 as usize >= wmo.textures.len()
91 {
92 report.add_error(ValidationError::InvalidReference {
93 field: format!("material[{i}].texture1"),
94 value: material.texture1,
95 max: wmo.textures.len() as u32 - 1,
96 });
97 }
98
99 if material.texture2 != 0
100 && material.texture2 < SPECIAL_TEXTURE_THRESHOLD
101 && material.texture2 as usize >= wmo.textures.len()
102 {
103 report.add_error(ValidationError::InvalidReference {
104 field: format!("material[{i}].texture2"),
105 value: material.texture2,
106 max: wmo.textures.len() as u32 - 1,
107 });
108 }
109
110 if material.shader > 20 && material.shader < SPECIAL_TEXTURE_THRESHOLD {
113 report.add_warning(ValidationWarning::UnusualValue {
114 field: format!("material[{i}].shader"),
115 value: material.shader,
116 explanation: "Shader ID is unusually high".to_string(),
117 });
118 }
119 }
120
121 for (i, set) in wmo.doodad_sets.iter().enumerate() {
123 let end_index = set.start_doodad + set.n_doodads;
124 if end_index > wmo.doodad_defs.len() as u32 {
125 report.add_error(ValidationError::InvalidReference {
126 field: format!("doodad_set[{i}]"),
127 value: end_index,
128 max: wmo.doodad_defs.len() as u32,
129 });
130 }
131 }
132
133 for (i, portal_ref) in wmo.portal_references.iter().enumerate() {
135 if portal_ref.portal_index as usize >= wmo.portals.len() {
136 report.add_error(ValidationError::InvalidReference {
137 field: format!("portal_reference[{i}].portal_index"),
138 value: portal_ref.portal_index as u32,
139 max: wmo.portals.len() as u32 - 1,
140 });
141 }
142
143 if portal_ref.group_index as usize >= wmo.groups.len() {
144 report.add_error(ValidationError::InvalidReference {
145 field: format!("portal_reference[{i}].group_index"),
146 value: portal_ref.group_index as u32,
147 max: wmo.groups.len() as u32 - 1,
148 });
149 }
150
151 if portal_ref.side > 1 {
152 report.add_error(ValidationError::InvalidValue {
153 field: format!("portal_reference[{i}].side"),
154 value: portal_ref.side as u32,
155 explanation: "Portal side must be 0 or 1".to_string(),
156 });
157 }
158 }
159
160 if wmo.header.flags.contains(WmoFlags::HAS_SKYBOX) && wmo.skybox.is_none() {
162 report.add_warning(ValidationWarning::FlagInconsistency {
163 flag: "HAS_SKYBOX".to_string(),
164 field: "skybox".to_string(),
165 explanation: "HAS_SKYBOX flag is set but no skybox model is defined".to_string(),
166 });
167 }
168
169 if !wmo.header.flags.contains(WmoFlags::HAS_SKYBOX) && wmo.skybox.is_some() {
170 report.add_warning(ValidationWarning::FlagInconsistency {
171 flag: "HAS_SKYBOX".to_string(),
172 field: "skybox".to_string(),
173 explanation: "Skybox model is defined but HAS_SKYBOX flag is not set".to_string(),
174 });
175 }
176
177 for (i, portal) in wmo.portals.iter().enumerate() {
179 if portal.vertices.is_empty() {
180 report.add_warning(ValidationWarning::UnusualStructure {
181 field: format!("portal[{i}]"),
182 explanation: "Portal has no vertices".to_string(),
183 });
184 }
185 }
186
187 if wmo.visible_block_lists.is_empty() && !wmo.portals.is_empty() {
189 report.add_warning(ValidationWarning::MissingData {
190 field: "visible_block_lists".to_string(),
191 explanation: "No visible block lists defined but portals exist".to_string(),
192 });
193 }
194
195 for (i, portal) in wmo.portals.iter().enumerate() {
197 let normal = &portal.normal;
198 let length_squared = normal.x * normal.x + normal.y * normal.y + normal.z * normal.z;
199
200 if (length_squared - 1.0).abs() > 0.01 {
202 report.add_warning(ValidationWarning::UnusualValue {
203 field: format!("portal[{i}].normal"),
204 value: length_squared as u32,
205 explanation: "Portal normal is not normalized".to_string(),
206 });
207 }
208 }
209
210 if wmo.bounding_box.min.x > wmo.bounding_box.max.x
212 || wmo.bounding_box.min.y > wmo.bounding_box.max.y
213 || wmo.bounding_box.min.z > wmo.bounding_box.max.z
214 {
215 report.add_error(ValidationError::InvalidBoundingBox {
216 min: format!(
217 "({}, {}, {})",
218 wmo.bounding_box.min.x, wmo.bounding_box.min.y, wmo.bounding_box.min.z
219 ),
220 max: format!(
221 "({}, {}, {})",
222 wmo.bounding_box.max.x, wmo.bounding_box.max.y, wmo.bounding_box.max.z
223 ),
224 });
225 }
226
227 Ok(report)
228 }
229
230 pub fn validate_group(&self, group: &WmoGroup) -> Result<ValidationReport> {
232 let mut report = ValidationReport::new();
233
234 if group.vertices.is_empty() {
236 report.add_error(ValidationError::EmptyData {
237 field: "vertices".to_string(),
238 explanation: "Group has no vertices".to_string(),
239 });
240 }
241
242 if group.indices.is_empty() {
244 report.add_error(ValidationError::EmptyData {
245 field: "indices".to_string(),
246 explanation: "Group has no indices".to_string(),
247 });
248 }
249
250 if group.batches.is_empty() {
252 report.add_warning(ValidationWarning::EmptyData {
253 field: "batches".to_string(),
254 explanation: "Group has no batches".to_string(),
255 });
256 }
257
258 for (i, batch) in group.batches.iter().enumerate() {
260 let end_index = batch.start_index + batch.count as u32;
261 if end_index > group.indices.len() as u32 {
262 report.add_error(ValidationError::InvalidReference {
263 field: format!("batch[{i}].indices"),
264 value: end_index,
265 max: group.indices.len() as u32,
266 });
267 }
268
269 if batch.end_vertex as usize > group.vertices.len() {
270 report.add_error(ValidationError::InvalidReference {
271 field: format!("batch[{i}].end_vertex"),
272 value: batch.end_vertex as u32,
273 max: group.vertices.len() as u32,
274 });
275 }
276 }
277
278 for (i, &index) in group.indices.iter().enumerate() {
280 if index as usize >= group.vertices.len() {
281 report.add_error(ValidationError::InvalidReference {
282 field: format!("indices[{i}]"),
283 value: index as u32,
284 max: group.vertices.len() as u32 - 1,
285 });
286 }
287 }
288
289 if !group.normals.is_empty() && group.normals.len() != group.vertices.len() {
291 report.add_error(ValidationError::CountMismatch {
292 field: "normals".to_string(),
293 expected: group.vertices.len() as u32,
294 actual: group.normals.len() as u32,
295 });
296 }
297
298 if !group.tex_coords.is_empty() && group.tex_coords.len() != group.vertices.len() {
300 report.add_error(ValidationError::CountMismatch {
301 field: "tex_coords".to_string(),
302 expected: group.vertices.len() as u32,
303 actual: group.tex_coords.len() as u32,
304 });
305 }
306
307 if let Some(colors) = &group.vertex_colors
309 && colors.len() != group.vertices.len()
310 {
311 report.add_error(ValidationError::CountMismatch {
312 field: "vertex_colors".to_string(),
313 expected: group.vertices.len() as u32,
314 actual: colors.len() as u32,
315 });
316 }
317
318 if group.header.flags.contains(WmoGroupFlags::HAS_NORMALS) && group.normals.is_empty() {
320 report.add_warning(ValidationWarning::FlagInconsistency {
321 flag: "HAS_NORMALS".to_string(),
322 field: "normals".to_string(),
323 explanation: "HAS_NORMALS flag is set but no normals are present".to_string(),
324 });
325 }
326
327 if !group.header.flags.contains(WmoGroupFlags::HAS_NORMALS) && !group.normals.is_empty() {
328 report.add_warning(ValidationWarning::FlagInconsistency {
329 flag: "HAS_NORMALS".to_string(),
330 field: "normals".to_string(),
331 explanation: "Normals are present but HAS_NORMALS flag is not set".to_string(),
332 });
333 }
334
335 if group
337 .header
338 .flags
339 .contains(WmoGroupFlags::HAS_VERTEX_COLORS)
340 && group.vertex_colors.is_none()
341 {
342 report.add_warning(ValidationWarning::FlagInconsistency {
343 flag: "HAS_VERTEX_COLORS".to_string(),
344 field: "vertex_colors".to_string(),
345 explanation: "HAS_VERTEX_COLORS flag is set but no vertex colors are present"
346 .to_string(),
347 });
348 }
349
350 if !group
351 .header
352 .flags
353 .contains(WmoGroupFlags::HAS_VERTEX_COLORS)
354 && group.vertex_colors.is_some()
355 {
356 report.add_warning(ValidationWarning::FlagInconsistency {
357 flag: "HAS_VERTEX_COLORS".to_string(),
358 field: "vertex_colors".to_string(),
359 explanation: "Vertex colors are present but HAS_VERTEX_COLORS flag is not set"
360 .to_string(),
361 });
362 }
363
364 if group.header.flags.contains(WmoGroupFlags::HAS_DOODADS) && group.doodad_refs.is_none() {
366 report.add_warning(ValidationWarning::FlagInconsistency {
367 flag: "HAS_DOODADS".to_string(),
368 field: "doodad_refs".to_string(),
369 explanation: "HAS_DOODADS flag is set but no doodad references are present"
370 .to_string(),
371 });
372 }
373
374 if !group.header.flags.contains(WmoGroupFlags::HAS_DOODADS) && group.doodad_refs.is_some() {
375 report.add_warning(ValidationWarning::FlagInconsistency {
376 flag: "HAS_DOODADS".to_string(),
377 field: "doodad_refs".to_string(),
378 explanation: "Doodad references are present but HAS_DOODADS flag is not set"
379 .to_string(),
380 });
381 }
382
383 if group.header.flags.contains(WmoGroupFlags::HAS_WATER) && group.liquid.is_none() {
385 report.add_warning(ValidationWarning::FlagInconsistency {
386 flag: "HAS_WATER".to_string(),
387 field: "liquid".to_string(),
388 explanation: "HAS_WATER flag is set but no liquid data is present".to_string(),
389 });
390 }
391
392 if !group.header.flags.contains(WmoGroupFlags::HAS_WATER) && group.liquid.is_some() {
393 report.add_warning(ValidationWarning::FlagInconsistency {
394 flag: "HAS_WATER".to_string(),
395 field: "liquid".to_string(),
396 explanation: "Liquid data is present but HAS_WATER flag is not set".to_string(),
397 });
398 }
399
400 if group.header.bounding_box.min.x > group.header.bounding_box.max.x
402 || group.header.bounding_box.min.y > group.header.bounding_box.max.y
403 || group.header.bounding_box.min.z > group.header.bounding_box.max.z
404 {
405 report.add_error(ValidationError::InvalidBoundingBox {
406 min: format!(
407 "({}, {}, {})",
408 group.header.bounding_box.min.x,
409 group.header.bounding_box.min.y,
410 group.header.bounding_box.min.z
411 ),
412 max: format!(
413 "({}, {}, {})",
414 group.header.bounding_box.max.x,
415 group.header.bounding_box.max.y,
416 group.header.bounding_box.max.z
417 ),
418 });
419 }
420
421 for (i, vertex) in group.vertices.iter().enumerate() {
423 if vertex.x < group.header.bounding_box.min.x
424 || vertex.x > group.header.bounding_box.max.x
425 || vertex.y < group.header.bounding_box.min.y
426 || vertex.y > group.header.bounding_box.max.y
427 || vertex.z < group.header.bounding_box.min.z
428 || vertex.z > group.header.bounding_box.max.z
429 {
430 report.add_warning(ValidationWarning::OutOfBounds {
431 field: format!("vertex[{i}]"),
432 value: format!("({}, {}, {})", vertex.x, vertex.y, vertex.z),
433 bounds: format!(
434 "({}, {}, {}) - ({}, {}, {})",
435 group.header.bounding_box.min.x,
436 group.header.bounding_box.min.y,
437 group.header.bounding_box.min.z,
438 group.header.bounding_box.max.x,
439 group.header.bounding_box.max.y,
440 group.header.bounding_box.max.z
441 ),
442 });
443 }
444 }
445
446 Ok(report)
447 }
448}
449
450#[derive(Debug)]
452pub struct ValidationReport {
453 pub errors: Vec<ValidationError>,
455
456 pub warnings: Vec<ValidationWarning>,
458}
459
460impl Default for ValidationReport {
461 fn default() -> Self {
462 Self::new()
463 }
464}
465
466impl ValidationReport {
467 pub fn new() -> Self {
469 Self {
470 errors: Vec::new(),
471 warnings: Vec::new(),
472 }
473 }
474
475 pub fn add_error(&mut self, error: ValidationError) {
477 self.errors.push(error);
478 }
479
480 pub fn add_warning(&mut self, warning: ValidationWarning) {
482 self.warnings.push(warning);
483 }
484
485 pub fn has_errors(&self) -> bool {
487 !self.errors.is_empty()
488 }
489
490 pub fn has_warnings(&self) -> bool {
492 !self.warnings.is_empty()
493 }
494
495 pub fn error_count(&self) -> usize {
497 self.errors.len()
498 }
499
500 pub fn warning_count(&self) -> usize {
502 self.warnings.len()
503 }
504
505 pub fn print(&self) {
507 if self.has_errors() {
508 println!("Validation Errors:");
509 for error in &self.errors {
510 println!(" - {error}");
511 }
512 }
513
514 if self.has_warnings() {
515 println!("Validation Warnings:");
516 for warning in &self.warnings {
517 println!(" - {warning}");
518 }
519 }
520
521 if !self.has_errors() && !self.has_warnings() {
522 println!("No validation issues found.");
523 }
524 }
525}
526
527#[derive(Debug)]
529pub enum ValidationError {
530 UnsupportedVersion(u32),
532
533 CountMismatch {
535 field: String,
536 expected: u32,
537 actual: u32,
538 },
539
540 InvalidReference { field: String, value: u32, max: u32 },
542
543 InvalidValue {
545 field: String,
546 value: u32,
547 explanation: String,
548 },
549
550 InvalidBoundingBox { min: String, max: String },
552
553 EmptyData { field: String, explanation: String },
555}
556
557impl std::fmt::Display for ValidationError {
558 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
559 match self {
560 Self::UnsupportedVersion(version) => write!(f, "Unsupported WMO version: {version}"),
561 Self::CountMismatch {
562 field,
563 expected,
564 actual,
565 } => {
566 write!(
567 f,
568 "Count mismatch for {field}: expected {expected}, found {actual}"
569 )
570 }
571 Self::InvalidReference { field, value, max } => {
572 write!(
573 f,
574 "Invalid reference in {field}: {value}, max allowed: {max}"
575 )
576 }
577 Self::InvalidValue {
578 field,
579 value,
580 explanation,
581 } => {
582 write!(f, "Invalid value in {field}: {value} ({explanation})")
583 }
584 Self::InvalidBoundingBox { min, max } => {
585 write!(f, "Invalid bounding box: min {min} exceeds max {max}")
586 }
587 Self::EmptyData { field, explanation } => {
588 write!(f, "Empty data for {field}: {explanation}")
589 }
590 }
591 }
592}
593
594#[derive(Debug)]
596pub enum ValidationWarning {
597 FlagInconsistency {
599 flag: String,
600 field: String,
601 explanation: String,
602 },
603
604 UnusualValue {
606 field: String,
607 value: u32,
608 explanation: String,
609 },
610
611 UnusualStructure { field: String, explanation: String },
613
614 MissingData { field: String, explanation: String },
616
617 EmptyData { field: String, explanation: String },
619
620 OutOfBounds {
622 field: String,
623 value: String,
624 bounds: String,
625 },
626}
627
628impl std::fmt::Display for ValidationWarning {
629 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
630 match self {
631 Self::FlagInconsistency {
632 flag,
633 field,
634 explanation,
635 } => {
636 write!(f, "Flag inconsistency with {flag}: {field} ({explanation})")
637 }
638 Self::UnusualValue {
639 field,
640 value,
641 explanation,
642 } => {
643 write!(f, "Unusual value in {field}: {value} ({explanation})")
644 }
645 Self::UnusualStructure { field, explanation } => {
646 write!(f, "Unusual structure in {field}: {explanation}")
647 }
648 Self::MissingData { field, explanation } => {
649 write!(f, "Missing data for {field}: {explanation}")
650 }
651 Self::EmptyData { field, explanation } => {
652 write!(f, "Empty data for {field}: {explanation}")
653 }
654 Self::OutOfBounds {
655 field,
656 value,
657 bounds,
658 } => {
659 write!(f, "{field} is out of bounds: {value} not within {bounds}")
660 }
661 }
662 }
663}