use std::io::{Seek, SeekFrom, Write};
use binrw::BinWrite;
use crate::chunks::mcnk::{
MccvChunk, MclyChunk, MclyLayer, McnkChunk, McnkFlags, McnkHeader, McnrChunk, McvtChunk,
};
use crate::chunks::mh2o::{Mh2oChunk, Mh2oHeader};
use crate::chunks::{
McinChunk, McinEntry, MddfChunk, MhdrChunk, MmdxChunk, MmidChunk, ModfChunk, MtexChunk,
MverChunk, MwidChunk, MwmoChunk,
};
use crate::error::{AdtError, Result};
use crate::{BuiltAdt, ChunkId};
pub fn serialize_to_writer<W: Write + Seek>(adt: &BuiltAdt, writer: &mut W) -> Result<()> {
let mut chunk_positions = ChunkPositions::default();
let mver = MverChunk { version: 18 };
write_chunk(writer, ChunkId::MVER, &mver)?;
chunk_positions.mhdr_data_start = writer.stream_position()? + 8; let mhdr_placeholder = MhdrChunk::default();
write_chunk(writer, ChunkId::MHDR, &mhdr_placeholder)?;
chunk_positions.mcin_data_start = writer.stream_position()? + 8;
let mcin_placeholder = McinChunk::default();
write_chunk(writer, ChunkId::MCIN, &mcin_placeholder)?;
chunk_positions.mtex = writer.stream_position()?;
let mtex = create_mtex_chunk(adt.textures());
write_chunk(writer, ChunkId::MTEX, &mtex)?;
chunk_positions.mmdx = writer.stream_position()?;
let mmdx = create_mmdx_chunk(adt.models());
write_chunk(writer, ChunkId::MMDX, &mmdx)?;
chunk_positions.mmid = writer.stream_position()?;
let mmid = create_mmid_chunk(adt.models());
write_chunk(writer, ChunkId::MMID, &mmid)?;
chunk_positions.mwmo = writer.stream_position()?;
let mwmo = create_mwmo_chunk(adt.wmos());
write_chunk(writer, ChunkId::MWMO, &mwmo)?;
chunk_positions.mwid = writer.stream_position()?;
let mwid = create_mwid_chunk(adt.wmos());
write_chunk(writer, ChunkId::MWID, &mwid)?;
chunk_positions.mddf = writer.stream_position()?;
let mddf = create_mddf_chunk(adt.doodad_placements());
write_chunk(writer, ChunkId::MDDF, &mddf)?;
chunk_positions.modf = writer.stream_position()?;
let modf = create_modf_chunk(adt.wmo_placements());
write_chunk(writer, ChunkId::MODF, &modf)?;
if let Some(mfbo) = adt.flight_bounds() {
chunk_positions.mfbo = Some(writer.stream_position()?);
write_chunk(writer, ChunkId::MFBO, mfbo)?;
}
if let Some(mh2o) = adt.water_data() {
chunk_positions.mh2o = Some(writer.stream_position()?);
write_mh2o_chunk(writer, mh2o)?;
}
if matches!(
adt.version(),
crate::AdtVersion::WotLK | crate::AdtVersion::Cataclysm | crate::AdtVersion::MoP
) {
chunk_positions.mtxf = Some(writer.stream_position()?);
let mtxf = adt.texture_flags().cloned().unwrap_or_else(|| {
crate::chunks::MtxfChunk {
flags: vec![0; adt.textures().len()],
}
});
write_chunk(writer, ChunkId::MTXF, &mtxf)?;
}
if let Some(mamp) = adt.texture_amplifier() {
chunk_positions.mamp = Some(writer.stream_position()?);
write_chunk(writer, ChunkId::MAMP, mamp)?;
}
if let Some(mtxp) = adt.texture_params() {
chunk_positions.mtxp = Some(writer.stream_position()?);
write_chunk(writer, ChunkId::MTXP, mtxp)?;
}
if let Some(mbmh) = adt.blend_mesh_headers() {
chunk_positions.mbmh = Some(writer.stream_position()?);
write_chunk(writer, ChunkId::MBMH, mbmh)?;
}
if let Some(mbbb) = adt.blend_mesh_bounds() {
chunk_positions.mbbb = Some(writer.stream_position()?);
write_chunk(writer, ChunkId::MBBB, mbbb)?;
}
if let Some(mbnv) = adt.blend_mesh_vertices() {
chunk_positions.mbnv = Some(writer.stream_position()?);
write_chunk(writer, ChunkId::MBNV, mbnv)?;
}
if let Some(mbmi) = adt.blend_mesh_indices() {
chunk_positions.mbmi = Some(writer.stream_position()?);
write_chunk(writer, ChunkId::MBMI, mbmi)?;
}
chunk_positions.mcnk_start = writer.stream_position()?;
if adt.mcnk_chunks().is_empty() {
for y in 0..16 {
for x in 0..16 {
let mcnk_start = writer.stream_position()?;
write_minimal_mcnk_chunk(writer, x, y, adt.version())?;
let mcnk_end = writer.stream_position()?;
let mcnk_size = (mcnk_end - mcnk_start - 8) as u32;
chunk_positions.mcnk_entries.push((mcnk_start, mcnk_size));
}
}
} else {
for mcnk in adt.mcnk_chunks() {
let mcnk_start = writer.stream_position()?;
write_mcnk_chunk(writer, mcnk)?;
let mcnk_end = writer.stream_position()?;
let mcnk_size = (mcnk_end - mcnk_start - 8) as u32;
chunk_positions.mcnk_entries.push((mcnk_start, mcnk_size));
}
while chunk_positions.mcnk_entries.len() < 256 {
chunk_positions.mcnk_entries.push((0, 0));
}
}
let mhdr = calculate_mhdr_offsets(&chunk_positions);
writer.seek(SeekFrom::Start(chunk_positions.mhdr_data_start))?;
mhdr.write_le(writer)?;
let mcin = calculate_mcin_entries(&chunk_positions);
writer.seek(SeekFrom::Start(chunk_positions.mcin_data_start))?;
mcin.write_le(writer)?;
writer.flush()?;
Ok(())
}
fn write_minimal_mcnk_chunk<W: Write + Seek>(
writer: &mut W,
x: u32,
y: u32,
version: crate::AdtVersion,
) -> Result<()> {
let tile_size = 533.33 / 16.0;
let pos_x = x as f32 * tile_size;
let pos_y = y as f32 * tile_size;
let pos_z = 0.0;
let mcnk_start = writer.stream_position()?;
let header_start = writer.stream_position()?;
let placeholder = vec![0u8; 8 + 136]; writer.write_all(&placeholder)?;
let mcvt_offset = (writer.stream_position()? - mcnk_start) as u32;
let mcvt = McvtChunk {
heights: vec![0.0; 145], };
writer.write_all(&ChunkId::MCVT.0)?;
let data_size = (mcvt.heights.len() * 4) as u32;
writer.write_all(&data_size.to_le_bytes())?;
for height in &mcvt.heights {
writer.write_all(&height.to_le_bytes())?;
}
let mcnr_offset = (writer.stream_position()? - mcnk_start) as u32;
let mcnr = McnrChunk::default(); writer.write_all(&ChunkId::MCNR.0)?;
let data_size = (mcnr.normals.len() * 3 + 13) as u32; writer.write_all(&data_size.to_le_bytes())?;
for normal in &mcnr.normals {
writer.write_all(&[normal.x as u8, normal.z as u8, normal.y as u8])?;
}
writer.write_all(&[0u8; 13])?;
let mcly_offset = (writer.stream_position()? - mcnk_start) as u32;
let mcly = MclyChunk {
layers: vec![MclyLayer {
texture_id: 0, flags: Default::default(), offset_in_mcal: 0, effect_id: 0, }],
};
write_chunk(writer, ChunkId::MCLY, &mcly)?;
let mccv_offset = if matches!(
version,
crate::AdtVersion::VanillaLate
| crate::AdtVersion::TBC
| crate::AdtVersion::WotLK
| crate::AdtVersion::Cataclysm
| crate::AdtVersion::MoP
) {
let offset = (writer.stream_position()? - mcnk_start) as u32;
let mccv = MccvChunk::default(); writer.write_all(&ChunkId::MCCV.0)?;
let data_size = (mccv.colors.len() * 4) as u32; writer.write_all(&data_size.to_le_bytes())?;
for color in &mccv.colors {
writer.write_all(&[color.b, color.g, color.r, color.a])?;
}
offset
} else {
0 };
let mcnk_end = writer.stream_position()?;
let mcnk_size = (mcnk_end - mcnk_start - 8) as u32;
let header = McnkHeader {
flags: McnkFlags { value: 0 },
index_x: x,
index_y: y,
n_layers: 1, n_doodad_refs: 0,
multipurpose_field: McnkHeader::multipurpose_from_offsets(mcvt_offset, mcnr_offset),
ofs_layer: mcly_offset,
ofs_refs: 0, ofs_alpha: 0, size_alpha: 0,
ofs_shadow: 0, size_shadow: 0,
area_id: 0,
n_map_obj_refs: 0,
holes_low_res: 0,
unknown_but_used: 1, pred_tex: [0; 8],
no_effect_doodad: [0; 8],
unknown_8bytes: [0; 8], ofs_snd_emitters: 0, n_snd_emitters: 0,
ofs_liquid: 0, size_liquid: 0,
position: [pos_x, pos_y, pos_z],
ofs_mccv: mccv_offset,
ofs_mclv: 0, unused: 0,
_padding: [0; 8],
};
writer.seek(SeekFrom::Start(header_start))?;
ChunkId::MCNK.write_le(writer)?;
writer.write_all(&mcnk_size.to_le_bytes())?;
header.write_le(writer)?;
writer.seek(SeekFrom::Start(mcnk_end))?;
Ok(())
}
fn write_mcnk_chunk<W: Write + Seek>(writer: &mut W, mcnk: &McnkChunk) -> Result<()> {
let mcnk_start = writer.stream_position()?;
let header_start = writer.stream_position()?;
let placeholder = vec![0u8; 8 + 136];
writer.write_all(&placeholder)?;
let mut header = mcnk.header.clone();
header.ofs_layer = 0;
header.n_layers = 0;
header.ofs_refs = 0;
header.ofs_alpha = 0;
header.size_alpha = 0;
header.ofs_shadow = 0;
header.size_shadow = 0;
header.ofs_liquid = 0;
header.size_liquid = 0;
header.ofs_mccv = 0;
header.ofs_mclv = 0;
header.ofs_snd_emitters = 0;
header.n_snd_emitters = 0;
header.multipurpose_field = [0u8; 8];
if let Some(mcvt) = &mcnk.heights {
let offset = (writer.stream_position()? - mcnk_start) as u32;
header.multipurpose_field[0..4].copy_from_slice(&offset.to_le_bytes());
writer.write_all(&ChunkId::MCVT.0)?; let data_size = (mcvt.heights.len() * 4) as u32;
writer.write_all(&data_size.to_le_bytes())?;
for height in &mcvt.heights {
writer.write_all(&height.to_le_bytes())?;
}
}
if let Some(mcnr) = &mcnk.normals {
let offset = (writer.stream_position()? - mcnk_start) as u32;
header.multipurpose_field[4..8].copy_from_slice(&offset.to_le_bytes());
writer.write_all(&ChunkId::MCNR.0)?;
let data_size = (mcnr.normals.len() * 3 + 13) as u32; writer.write_all(&data_size.to_le_bytes())?;
for normal in &mcnr.normals {
writer.write_all(&[normal.x as u8, normal.z as u8, normal.y as u8])?;
}
writer.write_all(&[0u8; 13])?; }
if let Some(mcly) = &mcnk.layers {
header.ofs_layer = (writer.stream_position()? - mcnk_start) as u32;
header.n_layers = mcly.layers.len() as u32;
write_chunk(writer, ChunkId::MCLY, mcly)?;
}
if let Some(mcrf) = &mcnk.refs {
header.ofs_refs = (writer.stream_position()? - mcnk_start) as u32;
write_chunk(writer, ChunkId::MCRF, mcrf)?;
}
if let Some(mcal) = &mcnk.alpha {
header.ofs_alpha = (writer.stream_position()? - mcnk_start) as u32;
let alpha_start = writer.stream_position()?;
write_chunk(writer, ChunkId::MCAL, mcal)?;
let alpha_end = writer.stream_position()?;
header.size_alpha = (alpha_end - alpha_start - 8) as u32;
}
if let Some(mcsh) = &mcnk.shadow {
header.ofs_shadow = (writer.stream_position()? - mcnk_start) as u32;
let shadow_start = writer.stream_position()?;
write_chunk(writer, ChunkId::MCSH, mcsh)?;
let shadow_end = writer.stream_position()?;
header.size_shadow = (shadow_end - shadow_start - 8) as u32;
}
if let Some(mclq) = &mcnk.liquid {
header.ofs_liquid = (writer.stream_position()? - mcnk_start) as u32;
let liquid_start = writer.stream_position()?;
write_chunk(writer, ChunkId::MCLQ, mclq)?;
let liquid_end = writer.stream_position()?;
header.size_liquid = (liquid_end - liquid_start) as u32;
}
if let Some(mccv) = &mcnk.vertex_colors {
header.ofs_mccv = (writer.stream_position()? - mcnk_start) as u32;
writer.write_all(&ChunkId::MCCV.0)?;
let data_size = (mccv.colors.len() * 4) as u32; writer.write_all(&data_size.to_le_bytes())?;
for color in &mccv.colors {
writer.write_all(&[color.b, color.g, color.r, color.a])?;
}
}
if let Some(mcse) = &mcnk.sound_emitters {
header.ofs_snd_emitters = (writer.stream_position()? - mcnk_start) as u32;
header.n_snd_emitters = mcse.emitters.len() as u32;
write_chunk(writer, ChunkId::MCSE, mcse)?;
}
if let Some(mclv) = &mcnk.vertex_lighting {
header.ofs_mclv = (writer.stream_position()? - mcnk_start) as u32;
write_chunk(writer, ChunkId::MCLV, mclv)?;
}
if let Some(mcrd) = &mcnk.doodad_refs {
if header.ofs_refs == 0 {
header.ofs_refs = (writer.stream_position()? - mcnk_start) as u32;
}
write_chunk(writer, ChunkId::MCRD, mcrd)?;
}
if let Some(mcrw) = &mcnk.wmo_refs {
if header.ofs_refs == 0 {
header.ofs_refs = (writer.stream_position()? - mcnk_start) as u32;
}
write_chunk(writer, ChunkId::MCRW, mcrw)?;
}
if let Some(mcmt) = &mcnk.materials {
write_chunk(writer, ChunkId::MCMT, mcmt)?;
}
if let Some(mcdd) = &mcnk.doodad_disable {
write_chunk(writer, ChunkId::MCDD, mcdd)?;
}
if let Some(mcbb) = &mcnk.blend_batches {
write_chunk(writer, ChunkId::MCBB, mcbb)?;
}
let mcnk_end = writer.stream_position()?;
let mcnk_size = (mcnk_end - mcnk_start - 8) as u32;
writer.seek(SeekFrom::Start(header_start))?;
ChunkId::MCNK.write_le(writer)?;
writer.write_all(&mcnk_size.to_le_bytes())?;
header.write_le(writer)?;
writer.seek(SeekFrom::Start(mcnk_end))?;
Ok(())
}
#[derive(Default)]
struct ChunkPositions {
mhdr_data_start: u64,
mcin_data_start: u64,
mtex: u64,
mmdx: u64,
mmid: u64,
mwmo: u64,
mwid: u64,
mddf: u64,
modf: u64,
mfbo: Option<u64>,
mh2o: Option<u64>,
mtxf: Option<u64>,
mamp: Option<u64>,
mtxp: Option<u64>,
mbmh: Option<u64>,
mbbb: Option<u64>,
mbnv: Option<u64>,
mbmi: Option<u64>,
mcnk_start: u64,
mcnk_entries: Vec<(u64, u32)>,
}
fn calculate_mhdr_offsets(positions: &ChunkPositions) -> MhdrChunk {
let base = positions.mhdr_data_start;
let relative_offset = |pos: u64| -> u32 { if pos == 0 { 0 } else { (pos - base) as u32 } };
MhdrChunk {
flags: calculate_mhdr_flags(positions),
mcin_offset: relative_offset(positions.mcin_data_start - 8), mtex_offset: relative_offset(positions.mtex),
mmdx_offset: relative_offset(positions.mmdx),
mmid_offset: relative_offset(positions.mmid),
mwmo_offset: relative_offset(positions.mwmo),
mwid_offset: relative_offset(positions.mwid),
mddf_offset: relative_offset(positions.mddf),
modf_offset: relative_offset(positions.modf),
mfbo_offset: positions.mfbo.map_or(0, relative_offset),
mh2o_offset: positions.mh2o.map_or(0, relative_offset),
mtxf_offset: positions.mtxf.map_or(0, relative_offset),
unused1: 0,
unused2: 0,
unused3: 0,
unused4: 0,
}
}
fn calculate_mhdr_flags(positions: &ChunkPositions) -> u32 {
let mut flags = 0u32;
if positions.mfbo.is_some() {
flags |= 0x01;
}
if positions.mh2o.is_some() {
flags |= 0x02;
}
flags
}
fn calculate_mcin_entries(positions: &ChunkPositions) -> McinChunk {
let mut entries = Vec::with_capacity(256);
for &(offset, size) in &positions.mcnk_entries {
entries.push(McinEntry {
offset: offset as u32,
size,
flags: 0,
async_id: 0,
});
}
while entries.len() < 256 {
entries.push(McinEntry::default());
}
McinChunk { entries }
}
fn write_chunk<W: Write + Seek, T: BinWrite>(
writer: &mut W,
chunk_id: ChunkId,
data: &T,
) -> Result<()>
where
for<'a> <T as BinWrite>::Args<'a>: Default,
{
writer.write_all(&chunk_id.0)?;
let data_start = writer.stream_position()?;
writer.write_all(&[0u8; 4])?;
data.write_le(writer)
.map_err(|e| AdtError::BinrwError(format!("Failed to serialize {chunk_id}: {e}")))?;
let data_end = writer.stream_position()?;
let size = (data_end - data_start - 4) as u32;
writer.seek(SeekFrom::Start(data_start))?;
writer.write_all(&size.to_le_bytes())?;
writer.seek(SeekFrom::Start(data_end))?;
Ok(())
}
fn write_mh2o_chunk<W: Write + Seek>(writer: &mut W, mh2o: &Mh2oChunk) -> Result<()> {
use binrw::BinWrite;
writer.write_all(&ChunkId::MH2O.0)?;
let size_pos = writer.stream_position()?;
writer.write_all(&[0u8; 4])?;
let data_start = writer.stream_position()?;
let headers_start = writer.stream_position()?;
const HEADER_SIZE: usize = 12; const HEADER_COUNT: usize = 256;
writer.write_all(&vec![0u8; HEADER_SIZE * HEADER_COUNT])?;
let mut current_pos = writer.stream_position()?;
let mut final_headers = Vec::with_capacity(HEADER_COUNT);
for entry in &mh2o.entries {
let mut header = entry.header;
if !entry.instances.is_empty() {
header.offset_instances = (current_pos - data_start) as u32;
writer.seek(SeekFrom::Start(current_pos))?;
let mut instances_with_offsets = entry.instances.clone();
let instances_size = instances_with_offsets.len() * 24; let mut vertex_data_offset = current_pos - data_start + instances_size as u64;
for (idx, inst) in instances_with_offsets.iter_mut().enumerate() {
if let Some(_bitmap) = entry.exists_bitmaps.get(idx).and_then(|b| b.as_ref()) {
inst.offset_exists_bitmap = vertex_data_offset as u32;
vertex_data_offset += 8; } else {
inst.offset_exists_bitmap = 0;
}
if let Some(vertex_data) = entry.vertex_data.get(idx).and_then(|v| v.as_ref()) {
inst.offset_vertex_data = vertex_data_offset as u32;
vertex_data_offset += vertex_data.byte_size() as u64;
} else {
inst.offset_vertex_data = 0;
}
}
for instance in &instances_with_offsets {
instance.write_le(writer).map_err(|e| {
AdtError::BinrwError(format!("Failed to write MH2O instance: {e}"))
})?;
}
current_pos = writer.stream_position()?;
for idx in 0..entry.instances.len() {
if let Some(bitmap) = entry.exists_bitmaps.get(idx).and_then(|b| b.as_ref()) {
writer.write_all(&bitmap.to_le_bytes())?;
current_pos = writer.stream_position()?;
}
if let Some(vertex_data) = entry.vertex_data.get(idx).and_then(|v| v.as_ref()) {
use crate::chunks::mh2o::VertexDataArray;
match vertex_data {
VertexDataArray::HeightDepth(vertices) => {
for vertex in vertices.as_ref().iter().filter_map(|v| v.as_ref()) {
vertex.write_le(writer).map_err(|e| {
AdtError::BinrwError(format!(
"Failed to write HeightDepth vertex: {e}"
))
})?;
}
}
VertexDataArray::HeightUv(vertices) => {
for vertex in vertices.as_ref().iter().filter_map(|v| v.as_ref()) {
vertex.write_le(writer).map_err(|e| {
AdtError::BinrwError(format!(
"Failed to write HeightUv vertex: {e}"
))
})?;
}
}
VertexDataArray::DepthOnly(vertices) => {
for vertex in vertices.as_ref().iter().filter_map(|v| v.as_ref()) {
vertex.write_le(writer).map_err(|e| {
AdtError::BinrwError(format!(
"Failed to write DepthOnly vertex: {e}"
))
})?;
}
}
VertexDataArray::HeightUvDepth(vertices) => {
for vertex in vertices.as_ref().iter().filter_map(|v| v.as_ref()) {
vertex.write_le(writer).map_err(|e| {
AdtError::BinrwError(format!(
"Failed to write HeightUvDepth vertex: {e}"
))
})?;
}
}
}
current_pos = writer.stream_position()?;
}
}
header.layer_count = entry.instances.len() as u32;
} else {
header.offset_instances = 0;
header.layer_count = 0;
}
if let Some(attrs) = &entry.attributes {
header.offset_attributes = (current_pos - data_start) as u32;
writer.seek(SeekFrom::Start(current_pos))?;
attrs.write_le(writer).map_err(|e| {
AdtError::BinrwError(format!("Failed to write MH2O attributes: {e}"))
})?;
current_pos = writer.stream_position()?;
} else {
header.offset_attributes = 0;
}
final_headers.push(header);
}
while final_headers.len() < HEADER_COUNT {
final_headers.push(Mh2oHeader::default());
}
writer.seek(SeekFrom::Start(headers_start))?;
for header in &final_headers {
header
.write_le(writer)
.map_err(|e| AdtError::BinrwError(format!("Failed to write MH2O header: {e}")))?;
}
let data_end = current_pos;
let chunk_size = (data_end - data_start) as u32;
writer.seek(SeekFrom::Start(size_pos))?;
writer.write_all(&chunk_size.to_le_bytes())?;
writer.seek(SeekFrom::Start(data_end))?;
Ok(())
}
fn create_mtex_chunk(textures: &[String]) -> MtexChunk {
MtexChunk {
filenames: textures.to_vec(),
}
}
fn create_mmdx_chunk(models: &[String]) -> MmdxChunk {
MmdxChunk {
filenames: models.to_vec(),
}
}
fn create_mmid_chunk(models: &[String]) -> MmidChunk {
let mut offsets = Vec::new();
let mut current_offset = 0u32;
for model in models {
offsets.push(current_offset);
current_offset += model.len() as u32 + 1; }
MmidChunk { offsets }
}
fn create_mwmo_chunk(wmos: &[String]) -> MwmoChunk {
MwmoChunk {
filenames: wmos.to_vec(),
}
}
fn create_mwid_chunk(wmos: &[String]) -> MwidChunk {
let mut offsets = Vec::new();
let mut current_offset = 0u32;
for wmo in wmos {
offsets.push(current_offset);
current_offset += wmo.len() as u32 + 1; }
MwidChunk { offsets }
}
fn create_mddf_chunk(placements: &[crate::chunks::DoodadPlacement]) -> MddfChunk {
MddfChunk {
placements: placements.to_vec(),
}
}
fn create_modf_chunk(placements: &[crate::chunks::WmoPlacement]) -> ModfChunk {
ModfChunk {
placements: placements.to_vec(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::AdtVersion;
use crate::builder::AdtBuilder;
use crate::chunks::{McnkChunk, McnkFlags, McnkHeader};
use std::io::{Cursor, Read};
fn create_minimal_mcnk() -> McnkChunk {
McnkChunk {
header: McnkHeader {
flags: McnkFlags { value: 0 },
index_x: 0,
index_y: 0,
n_layers: 0,
n_doodad_refs: 0,
multipurpose_field: McnkHeader::multipurpose_from_offsets(0, 0),
ofs_layer: 0,
ofs_refs: 0,
ofs_alpha: 0,
size_alpha: 0,
ofs_shadow: 0,
size_shadow: 0,
area_id: 0,
n_map_obj_refs: 0,
holes_low_res: 0,
unknown_but_used: 0,
pred_tex: [0; 8],
no_effect_doodad: [0; 8],
unknown_8bytes: [0; 8],
ofs_snd_emitters: 0,
n_snd_emitters: 0,
ofs_liquid: 0,
size_liquid: 0,
position: [0.0, 0.0, 0.0],
ofs_mccv: 0,
ofs_mclv: 0,
unused: 0,
_padding: [0; 8],
},
heights: None,
normals: None,
layers: None,
materials: None,
refs: None,
doodad_refs: None,
wmo_refs: None,
alpha: None,
shadow: None,
vertex_colors: None,
vertex_lighting: None,
sound_emitters: None,
liquid: None,
doodad_disable: None,
blend_batches: None,
}
}
#[test]
fn test_serialize_minimal_adt() {
let adt = AdtBuilder::new()
.with_version(AdtVersion::VanillaEarly)
.add_texture("terrain/grass.blp")
.add_mcnk_chunk(create_minimal_mcnk())
.build()
.expect("Failed to build ADT");
let mut buffer = Cursor::new(Vec::new());
serialize_to_writer(&adt, &mut buffer).expect("Failed to serialize ADT");
let data = buffer.into_inner();
assert_eq!(&data[0..4], b"REVM"); assert_eq!(u32::from_le_bytes([data[4], data[5], data[6], data[7]]), 4); assert_eq!(
u32::from_le_bytes([data[8], data[9], data[10], data[11]]),
18
); }
#[test]
fn test_chunk_ordering() {
let adt = AdtBuilder::new()
.add_texture("test.blp")
.add_mcnk_chunk(create_minimal_mcnk())
.build()
.expect("Failed to build ADT");
let mut buffer = Cursor::new(Vec::new());
serialize_to_writer(&adt, &mut buffer).expect("Failed to serialize ADT");
let data = buffer.into_inner();
let mut cursor = Cursor::new(&data);
let mut read_magic = || {
let mut magic = [0u8; 4];
cursor.read_exact(&mut magic).unwrap();
let mut size_bytes = [0u8; 4];
cursor.read_exact(&mut size_bytes).unwrap();
let size = u32::from_le_bytes(size_bytes);
cursor.seek(SeekFrom::Current(size as i64)).unwrap();
ChunkId(magic)
};
assert_eq!(read_magic(), ChunkId::MVER);
assert_eq!(read_magic(), ChunkId::MHDR);
assert_eq!(read_magic(), ChunkId::MCIN);
assert_eq!(read_magic(), ChunkId::MTEX);
assert_eq!(read_magic(), ChunkId::MMDX);
assert_eq!(read_magic(), ChunkId::MMID);
assert_eq!(read_magic(), ChunkId::MWMO);
assert_eq!(read_magic(), ChunkId::MWID);
assert_eq!(read_magic(), ChunkId::MDDF);
assert_eq!(read_magic(), ChunkId::MODF);
}
#[test]
fn test_offset_calculation() {
let adt = AdtBuilder::new()
.add_texture("test.blp")
.add_mcnk_chunk(create_minimal_mcnk())
.build()
.expect("Failed to build ADT");
let mut buffer = Cursor::new(Vec::new());
serialize_to_writer(&adt, &mut buffer).expect("Failed to serialize ADT");
let data = buffer.into_inner();
let mhdr_offset = 12; let mhdr_data_offset = mhdr_offset + 8;
let mcin_offset = u32::from_le_bytes([
data[mhdr_data_offset + 4],
data[mhdr_data_offset + 5],
data[mhdr_data_offset + 6],
data[mhdr_data_offset + 7],
]);
let mtex_offset = u32::from_le_bytes([
data[mhdr_data_offset + 8],
data[mhdr_data_offset + 9],
data[mhdr_data_offset + 10],
data[mhdr_data_offset + 11],
]);
assert!(mcin_offset > 0, "MCIN offset should be non-zero");
assert!(mtex_offset > 0, "MTEX offset should be non-zero");
}
#[test]
fn test_mmid_offset_calculation() {
let models = vec!["model1.m2".to_string(), "model2.m2".to_string()];
let mmid = create_mmid_chunk(&models);
assert_eq!(mmid.offsets.len(), 2);
assert_eq!(mmid.offsets[0], 0);
assert_eq!(mmid.offsets[1], 10); }
#[test]
fn test_mwid_offset_calculation() {
let wmos = vec!["building.wmo".to_string(), "castle.wmo".to_string()];
let mwid = create_mwid_chunk(&wmos);
assert_eq!(mwid.offsets.len(), 2);
assert_eq!(mwid.offsets[0], 0);
assert_eq!(mwid.offsets[1], 13); }
#[test]
fn test_write_chunk() {
let mut buffer = Cursor::new(Vec::new());
let mver = MverChunk { version: 18 };
write_chunk(&mut buffer, ChunkId::MVER, &mver).expect("Failed to write chunk");
let data = buffer.into_inner();
assert_eq!(&data[0..4], b"REVM"); assert_eq!(u32::from_le_bytes([data[4], data[5], data[6], data[7]]), 4); assert_eq!(
u32::from_le_bytes([data[8], data[9], data[10], data[11]]),
18
); }
#[test]
fn test_mhdr_flags_no_optional_chunks() {
let positions = ChunkPositions::default();
let flags = calculate_mhdr_flags(&positions);
assert_eq!(flags, 0);
}
#[test]
fn test_mhdr_flags_with_mfbo() {
let positions = ChunkPositions {
mfbo: Some(1000),
..Default::default()
};
let flags = calculate_mhdr_flags(&positions);
assert_eq!(flags, 0x01);
}
#[test]
fn test_mhdr_flags_with_mh2o() {
let positions = ChunkPositions {
mh2o: Some(2000),
..Default::default()
};
let flags = calculate_mhdr_flags(&positions);
assert_eq!(flags, 0x02);
}
#[test]
fn test_mhdr_flags_with_both() {
let positions = ChunkPositions {
mfbo: Some(1000),
mh2o: Some(2000),
..Default::default()
};
let flags = calculate_mhdr_flags(&positions);
assert_eq!(flags, 0x03);
}
#[test]
fn test_mh2o_serialization() {
use crate::chunks::mh2o::{Mh2oAttributes, Mh2oEntry, Mh2oInstance};
let mut entries = vec![Mh2oEntry::default(); 256];
entries[0] = Mh2oEntry {
header: Mh2oHeader {
offset_instances: 0, layer_count: 1,
offset_attributes: 0, },
instances: vec![Mh2oInstance {
liquid_type: 5, liquid_object_or_lvf: 0, min_height_level: 100.0,
max_height_level: 105.0,
x_offset: 0,
y_offset: 0,
width: 8,
height: 8,
offset_exists_bitmap: 0, offset_vertex_data: 0, }],
vertex_data: vec![None], exists_bitmaps: vec![None], attributes: Some(Mh2oAttributes {
fishable: 0xFFFFFFFFFFFFFFFF, deep: 0x0000000000000000, }),
};
let mh2o = Mh2oChunk { entries };
let mut buffer = Cursor::new(Vec::new());
write_mh2o_chunk(&mut buffer, &mh2o).expect("Failed to write MH2O chunk");
let data = buffer.into_inner();
assert_eq!(&data[0..4], b"O2HM"); let chunk_size = u32::from_le_bytes([data[4], data[5], data[6], data[7]]);
assert_eq!(chunk_size, 3112);
let header_offset = 8; let offset_instances = u32::from_le_bytes([
data[header_offset],
data[header_offset + 1],
data[header_offset + 2],
data[header_offset + 3],
]);
let layer_count = u32::from_le_bytes([
data[header_offset + 4],
data[header_offset + 5],
data[header_offset + 6],
data[header_offset + 7],
]);
let offset_attributes = u32::from_le_bytes([
data[header_offset + 8],
data[header_offset + 9],
data[header_offset + 10],
data[header_offset + 11],
]);
assert_eq!(offset_instances, 3072); assert_eq!(layer_count, 1);
assert_eq!(offset_attributes, 3096);
let second_header_offset = header_offset + 12;
let second_offset_instances = u32::from_le_bytes([
data[second_header_offset],
data[second_header_offset + 1],
data[second_header_offset + 2],
data[second_header_offset + 3],
]);
let second_layer_count = u32::from_le_bytes([
data[second_header_offset + 4],
data[second_header_offset + 5],
data[second_header_offset + 6],
data[second_header_offset + 7],
]);
assert_eq!(second_offset_instances, 0);
assert_eq!(second_layer_count, 0);
}
}