use crate::ChunkId;
use crate::builder::built_adt::BuiltAdt;
use crate::builder::validation::{
validate_blend_mesh_data, validate_doodad_placement_references, validate_model_filename,
validate_texture_filename, validate_version_chunk_compatibility, validate_wmo_filename,
validate_wmo_placement_references,
};
use crate::chunks::mh2o::Mh2oChunk;
use crate::chunks::{
DoodadPlacement, MampChunk, MbbbChunk, MbmhChunk, MbmiChunk, MbnvChunk, McnkChunk, MfboChunk,
MtxfChunk, MtxpChunk, WmoPlacement,
};
use crate::error::{AdtError, Result};
use crate::version::AdtVersion;
#[derive(Debug, Clone)]
pub struct AdtBuilder {
version: AdtVersion,
textures: Vec<String>,
models: Vec<String>,
wmos: Vec<String>,
doodad_placements: Vec<DoodadPlacement>,
wmo_placements: Vec<WmoPlacement>,
mcnk_chunks: Vec<McnkChunk>,
flight_bounds: Option<MfboChunk>,
water_data: Option<Mh2oChunk>,
texture_flags: Option<MtxfChunk>,
texture_amplifier: Option<MampChunk>,
texture_params: Option<MtxpChunk>,
blend_mesh_headers: Option<MbmhChunk>,
blend_mesh_bounds: Option<MbbbChunk>,
blend_mesh_vertices: Option<MbnvChunk>,
blend_mesh_indices: Option<MbmiChunk>,
}
impl AdtBuilder {
#[must_use]
pub fn new() -> Self {
Self {
version: AdtVersion::VanillaEarly,
textures: Vec::new(),
models: Vec::new(),
wmos: Vec::new(),
doodad_placements: Vec::new(),
wmo_placements: Vec::new(),
mcnk_chunks: Vec::new(),
flight_bounds: None,
water_data: None,
texture_flags: None,
texture_amplifier: None,
texture_params: None,
blend_mesh_headers: None,
blend_mesh_bounds: None,
blend_mesh_vertices: None,
blend_mesh_indices: None,
}
}
#[must_use]
pub fn with_version(mut self, version: AdtVersion) -> Self {
self.version = version;
self
}
#[must_use]
pub fn add_texture<S: Into<String>>(mut self, filename: S) -> Self {
let filename = filename.into();
if let Err(e) = validate_texture_filename(&filename) {
panic!("Invalid texture filename: {}", e);
}
self.textures.push(filename);
self
}
#[must_use]
pub fn add_textures<I, S>(mut self, filenames: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
for filename in filenames {
let filename = filename.into();
if let Err(e) = validate_texture_filename(&filename) {
panic!("Invalid texture filename: {}", e);
}
self.textures.push(filename);
}
self
}
#[must_use]
pub fn add_model<S: Into<String>>(mut self, filename: S) -> Self {
let filename = filename.into();
if let Err(e) = validate_model_filename(&filename) {
panic!("Invalid model filename: {}", e);
}
self.models.push(filename);
self
}
#[must_use]
pub fn add_wmo<S: Into<String>>(mut self, filename: S) -> Self {
let filename = filename.into();
if let Err(e) = validate_wmo_filename(&filename) {
panic!("Invalid WMO filename: {}", e);
}
self.wmos.push(filename);
self
}
#[must_use]
pub fn add_doodad_placement(mut self, placement: DoodadPlacement) -> Self {
if placement.scale == 0 {
panic!("Doodad placement scale must be greater than 0");
}
self.doodad_placements.push(placement);
self
}
#[must_use]
pub fn add_wmo_placement(mut self, placement: WmoPlacement) -> Self {
self.wmo_placements.push(placement);
self
}
#[must_use]
pub fn add_mcnk_chunk(mut self, chunk: McnkChunk) -> Self {
self.mcnk_chunks.push(chunk);
self
}
#[must_use]
pub fn add_flight_bounds(mut self, bounds: MfboChunk) -> Self {
self.flight_bounds = Some(bounds);
self
}
#[must_use]
pub fn add_water_data(mut self, water: Mh2oChunk) -> Self {
self.water_data = Some(water);
self
}
#[must_use]
pub fn add_texture_flags(mut self, flags: MtxfChunk) -> Self {
self.texture_flags = Some(flags);
self
}
#[must_use]
pub fn add_texture_amplifier(mut self, amp: MampChunk) -> Self {
self.texture_amplifier = Some(amp);
self
}
#[must_use]
pub fn add_texture_params(mut self, params: MtxpChunk) -> Self {
self.texture_params = Some(params);
self
}
#[must_use]
pub fn add_blend_mesh_headers(mut self, headers: MbmhChunk) -> Self {
self.blend_mesh_headers = Some(headers);
self
}
#[must_use]
pub fn add_blend_mesh_bounds(mut self, bounds: MbbbChunk) -> Self {
self.blend_mesh_bounds = Some(bounds);
self
}
#[must_use]
pub fn add_blend_mesh_vertices(mut self, vertices: MbnvChunk) -> Self {
self.blend_mesh_vertices = Some(vertices);
self
}
#[must_use]
pub fn add_blend_mesh_indices(mut self, indices: MbmiChunk) -> Self {
self.blend_mesh_indices = Some(indices);
self
}
pub fn from_parsed(root: crate::api::RootAdt) -> Self {
Self {
version: root.version,
textures: root.textures,
models: root.models,
wmos: root.wmos,
doodad_placements: root.doodad_placements,
wmo_placements: root.wmo_placements,
mcnk_chunks: root.mcnk_chunks,
flight_bounds: root.flight_bounds,
water_data: root.water_data,
texture_flags: root.texture_flags,
texture_amplifier: root.texture_amplifier,
texture_params: root.texture_params,
blend_mesh_headers: root.blend_mesh_headers,
blend_mesh_bounds: root.blend_mesh_bounds,
blend_mesh_vertices: root.blend_mesh_vertices,
blend_mesh_indices: root.blend_mesh_indices,
}
}
pub fn build(self) -> Result<BuiltAdt> {
if self.textures.is_empty() {
return Err(AdtError::MissingRequiredChunk(ChunkId::MTEX));
}
if self.mcnk_chunks.len() > 256 {
return Err(AdtError::ChunkParseError {
chunk: ChunkId::MCNK,
offset: 0,
details: format!(
"MCNK count {} exceeds maximum 256 terrain chunks",
self.mcnk_chunks.len()
),
});
}
validate_doodad_placement_references(&self.doodad_placements, self.models.len())?;
validate_wmo_placement_references(&self.wmo_placements, self.wmos.len())?;
if let Some(_bounds) = &self.flight_bounds {
validate_version_chunk_compatibility(self.version, ChunkId::MFBO)?;
}
if let Some(_water) = &self.water_data {
validate_version_chunk_compatibility(self.version, ChunkId::MH2O)?;
}
if let Some(_amp) = &self.texture_amplifier {
validate_version_chunk_compatibility(self.version, ChunkId::MAMP)?;
}
if let Some(_params) = &self.texture_params {
validate_version_chunk_compatibility(self.version, ChunkId::MTXP)?;
}
validate_blend_mesh_data(
&self.blend_mesh_headers,
&self.blend_mesh_bounds,
&self.blend_mesh_vertices,
&self.blend_mesh_indices,
)?;
if self.blend_mesh_headers.is_some() {
validate_version_chunk_compatibility(self.version, ChunkId::MBMH)?;
}
Ok(BuiltAdt::new(
self.version,
self.textures,
self.models,
self.wmos,
self.doodad_placements,
self.wmo_placements,
self.mcnk_chunks,
self.flight_bounds,
self.water_data,
self.texture_flags,
self.texture_amplifier,
self.texture_params,
self.blend_mesh_headers,
self.blend_mesh_bounds,
self.blend_mesh_vertices,
self.blend_mesh_indices,
))
}
}
impl Default for AdtBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_builder_defaults_to_vanilla() {
let builder = AdtBuilder::new();
assert_eq!(builder.version, AdtVersion::VanillaEarly);
}
#[test]
fn test_with_version() {
let builder = AdtBuilder::new().with_version(AdtVersion::WotLK);
assert_eq!(builder.version, AdtVersion::WotLK);
}
#[test]
fn test_add_texture_valid() {
let builder = AdtBuilder::new().add_texture("terrain/grass.blp");
assert_eq!(builder.textures.len(), 1);
assert_eq!(builder.textures[0], "terrain/grass.blp");
}
#[test]
#[should_panic(expected = "Invalid texture filename")]
fn test_add_texture_empty() {
let _builder = AdtBuilder::new().add_texture("");
}
#[test]
#[should_panic(expected = "Invalid texture filename")]
fn test_add_texture_backslash() {
let _builder = AdtBuilder::new().add_texture("terrain\\grass.blp");
}
#[test]
fn test_add_textures() {
let textures = vec!["terrain/grass.blp", "terrain/dirt.blp"];
let builder = AdtBuilder::new().add_textures(textures);
assert_eq!(builder.textures.len(), 2);
}
#[test]
fn test_add_model_valid() {
let builder = AdtBuilder::new().add_model("doodad/tree.m2");
assert_eq!(builder.models.len(), 1);
assert_eq!(builder.models[0], "doodad/tree.m2");
}
#[test]
#[should_panic(expected = "Invalid model filename")]
fn test_add_model_wrong_extension() {
let _builder = AdtBuilder::new().add_model("doodad/tree.mdx");
}
#[test]
fn test_add_wmo_valid() {
let builder = AdtBuilder::new().add_wmo("building/house.wmo");
assert_eq!(builder.wmos.len(), 1);
assert_eq!(builder.wmos[0], "building/house.wmo");
}
#[test]
fn test_add_doodad_placement() {
let placement = DoodadPlacement {
name_id: 0,
unique_id: 1,
position: [0.0, 0.0, 0.0],
rotation: [0.0, 0.0, 0.0],
scale: 1024,
flags: 0,
};
let builder = AdtBuilder::new().add_doodad_placement(placement);
assert_eq!(builder.doodad_placements.len(), 1);
}
#[test]
#[should_panic(expected = "scale must be greater than 0")]
fn test_add_doodad_placement_zero_scale() {
let placement = DoodadPlacement {
name_id: 0,
unique_id: 1,
position: [0.0, 0.0, 0.0],
rotation: [0.0, 0.0, 0.0],
scale: 0,
flags: 0,
};
let _builder = AdtBuilder::new().add_doodad_placement(placement);
}
#[test]
fn test_add_wmo_placement() {
let placement = WmoPlacement {
name_id: 0,
unique_id: 1,
position: [0.0, 0.0, 0.0],
rotation: [0.0, 0.0, 0.0],
extents_min: [-100.0, -100.0, 0.0],
extents_max: [100.0, 100.0, 200.0],
flags: 0,
doodad_set: 0,
name_set: 0,
scale: 1024,
};
let builder = AdtBuilder::new().add_wmo_placement(placement);
assert_eq!(builder.wmo_placements.len(), 1);
}
#[test]
fn test_build_missing_texture() {
let builder = AdtBuilder::new();
let result = builder.build();
assert!(matches!(
result,
Err(AdtError::MissingRequiredChunk(ChunkId::MTEX))
));
}
#[test]
fn test_default() {
let builder = AdtBuilder::default();
assert_eq!(builder.version, AdtVersion::VanillaEarly);
}
}