use std::io::{Read, Seek, SeekFrom};
use binrw::BinRead;
use crate::api::RootAdt;
use crate::chunk_discovery::ChunkDiscovery;
use crate::chunk_id::ChunkId;
use crate::chunks::mh2o::{Mh2oAttributes, Mh2oChunk, Mh2oEntry, Mh2oHeader, Mh2oInstance};
use crate::chunks::{
MampChunk, MbbbChunk, MbmhChunk, MbmiChunk, MbnvChunk, McinChunk, McnkChunk, MddfChunk,
MfboChunk, MhdrChunk, MmdxChunk, MmidChunk, ModfChunk, MtexChunk, MtxfChunk, MtxpChunk,
MwidChunk, MwmoChunk,
};
use crate::error::{AdtError, Result};
use crate::version::AdtVersion;
pub fn parse_root_adt<R: Read + Seek>(
reader: &mut R,
discovery: &ChunkDiscovery,
version: AdtVersion,
) -> Result<(RootAdt, Vec<String>)> {
let warnings = Vec::new();
let is_split_root = !discovery.has_chunk(ChunkId::MCIN) && !discovery.has_chunk(ChunkId::MTEX);
if !discovery.has_chunk(ChunkId::MHDR) {
return Err(AdtError::MissingRequiredChunk(ChunkId::MHDR));
}
if !discovery.has_chunk(ChunkId::MCNK) {
return Err(AdtError::MissingRequiredChunk(ChunkId::MCNK));
}
if !is_split_root {
if !discovery.has_chunk(ChunkId::MCIN) {
return Err(AdtError::MissingRequiredChunk(ChunkId::MCIN));
}
if !discovery.has_chunk(ChunkId::MTEX) {
return Err(AdtError::MissingRequiredChunk(ChunkId::MTEX));
}
}
let mhdr = parse_simple_chunk::<MhdrChunk, _>(reader, discovery, ChunkId::MHDR)?;
let mcin = if is_split_root {
McinChunk { entries: vec![] }
} else {
parse_simple_chunk::<McinChunk, _>(reader, discovery, ChunkId::MCIN)?
};
let textures = if let Some(chunks) = discovery.get_chunks(ChunkId::MTEX) {
if let Some(chunk_info) = chunks.first() {
reader.seek(SeekFrom::Start(chunk_info.offset + 8))?; let mut chunk_data = vec![0u8; chunk_info.size as usize];
reader.read_exact(&mut chunk_data)?;
let mut cursor = std::io::Cursor::new(chunk_data);
let mtex = MtexChunk::read_le(&mut cursor)?;
mtex.filenames
} else {
Vec::new()
}
} else {
Vec::new()
};
let models = if let Some(chunks) = discovery.get_chunks(ChunkId::MMDX) {
if let Some(chunk_info) = chunks.first() {
reader.seek(SeekFrom::Start(chunk_info.offset + 8))?;
let mut chunk_data = vec![0u8; chunk_info.size as usize];
reader.read_exact(&mut chunk_data)?;
let mut cursor = std::io::Cursor::new(chunk_data);
let mmdx = MmdxChunk::read_le(&mut cursor)?;
mmdx.filenames
} else {
Vec::new()
}
} else {
Vec::new()
};
let model_indices = if let Some(chunks) = discovery.get_chunks(ChunkId::MMID) {
if let Some(chunk_info) = chunks.first() {
reader.seek(SeekFrom::Start(chunk_info.offset + 8))?;
let mut chunk_data = vec![0u8; chunk_info.size as usize];
reader.read_exact(&mut chunk_data)?;
let mut cursor = std::io::Cursor::new(chunk_data);
let mmid = MmidChunk::read_le(&mut cursor)?;
mmid.offsets
} else {
Vec::new()
}
} else {
Vec::new()
};
let wmos = if let Some(chunks) = discovery.get_chunks(ChunkId::MWMO) {
if let Some(chunk_info) = chunks.first() {
reader.seek(SeekFrom::Start(chunk_info.offset + 8))?;
let mut chunk_data = vec![0u8; chunk_info.size as usize];
reader.read_exact(&mut chunk_data)?;
let mut cursor = std::io::Cursor::new(chunk_data);
let mwmo = MwmoChunk::read_le(&mut cursor)?;
mwmo.filenames
} else {
Vec::new()
}
} else {
Vec::new()
};
let wmo_indices = if let Some(chunks) = discovery.get_chunks(ChunkId::MWID) {
if let Some(chunk_info) = chunks.first() {
reader.seek(SeekFrom::Start(chunk_info.offset + 8))?;
let mut chunk_data = vec![0u8; chunk_info.size as usize];
reader.read_exact(&mut chunk_data)?;
let mut cursor = std::io::Cursor::new(chunk_data);
let mwid = MwidChunk::read_le(&mut cursor)?;
mwid.offsets
} else {
Vec::new()
}
} else {
Vec::new()
};
let doodad_placements = if let Some(chunks) = discovery.get_chunks(ChunkId::MDDF) {
if let Some(chunk_info) = chunks.first() {
reader.seek(SeekFrom::Start(chunk_info.offset + 8))?;
let mut chunk_data = vec![0u8; chunk_info.size as usize];
reader.read_exact(&mut chunk_data)?;
let mut cursor = std::io::Cursor::new(chunk_data);
let mddf = MddfChunk::read_le(&mut cursor)?;
mddf.placements
} else {
Vec::new()
}
} else {
Vec::new()
};
let wmo_placements = if let Some(chunks) = discovery.get_chunks(ChunkId::MODF) {
if let Some(chunk_info) = chunks.first() {
reader.seek(SeekFrom::Start(chunk_info.offset + 8))?;
let mut chunk_data = vec![0u8; chunk_info.size as usize];
reader.read_exact(&mut chunk_data)?;
let mut cursor = std::io::Cursor::new(chunk_data);
let modf = ModfChunk::read_le(&mut cursor)?;
modf.placements
} else {
Vec::new()
}
} else {
Vec::new()
};
let mcnk_chunks = parse_mcnk_chunks(reader, discovery)?;
let flight_bounds = if matches!(
version,
AdtVersion::TBC | AdtVersion::WotLK | AdtVersion::Cataclysm | AdtVersion::MoP
) {
if let Some(chunks) = discovery.get_chunks(ChunkId::MFBO) {
if let Some(chunk_info) = chunks.first() {
reader.seek(SeekFrom::Start(chunk_info.offset + 8))?;
Some(MfboChunk::read_le(reader)?)
} else {
None
}
} else {
None
}
} else {
None
};
let water_data = if matches!(
version,
AdtVersion::WotLK | AdtVersion::Cataclysm | AdtVersion::MoP
) {
if let Some(chunks) = discovery.get_chunks(ChunkId::MH2O) {
if let Some(chunk_info) = chunks.first() {
parse_mh2o_chunk(reader, chunk_info.offset, chunk_info.size)?
} else {
None
}
} else {
None
}
} else {
None
};
let texture_flags = if matches!(
version,
AdtVersion::WotLK | AdtVersion::Cataclysm | AdtVersion::MoP
) {
if let Some(chunks) = discovery.get_chunks(ChunkId::MTXF) {
if let Some(chunk_info) = chunks.first() {
reader.seek(SeekFrom::Start(chunk_info.offset + 8))?;
Some(MtxfChunk::read_le(reader)?)
} else {
None
}
} else {
None
}
} else {
None
};
let texture_amplifier = if matches!(version, AdtVersion::Cataclysm | AdtVersion::MoP) {
if let Some(chunks) = discovery.get_chunks(ChunkId::MAMP) {
if let Some(chunk_info) = chunks.first() {
reader.seek(SeekFrom::Start(chunk_info.offset + 8))?;
Some(MampChunk::read_le(reader)?)
} else {
None
}
} else {
None
}
} else {
None
};
let texture_params = if matches!(version, AdtVersion::MoP) {
if let Some(chunks) = discovery.get_chunks(ChunkId::MTXP) {
if let Some(chunk_info) = chunks.first() {
reader.seek(SeekFrom::Start(chunk_info.offset + 8))?;
Some(MtxpChunk::read_le(reader)?)
} else {
None
}
} else {
None
}
} else {
None
};
let blend_mesh_headers = if matches!(version, AdtVersion::MoP) {
if let Some(chunks) = discovery.get_chunks(ChunkId::MBMH) {
if let Some(chunk_info) = chunks.first() {
reader.seek(SeekFrom::Start(chunk_info.offset + 8))?;
Some(MbmhChunk::read_le(reader)?)
} else {
None
}
} else {
None
}
} else {
None
};
let blend_mesh_bounds = if matches!(version, AdtVersion::MoP) {
if let Some(chunks) = discovery.get_chunks(ChunkId::MBBB) {
if let Some(chunk_info) = chunks.first() {
reader.seek(SeekFrom::Start(chunk_info.offset + 8))?;
Some(MbbbChunk::read_le(reader)?)
} else {
None
}
} else {
None
}
} else {
None
};
let blend_mesh_vertices = if matches!(version, AdtVersion::MoP) {
if let Some(chunks) = discovery.get_chunks(ChunkId::MBNV) {
if let Some(chunk_info) = chunks.first() {
reader.seek(SeekFrom::Start(chunk_info.offset + 8))?;
Some(MbnvChunk::read_le(reader)?)
} else {
None
}
} else {
None
}
} else {
None
};
let blend_mesh_indices = if matches!(version, AdtVersion::MoP) {
if let Some(chunks) = discovery.get_chunks(ChunkId::MBMI) {
if let Some(chunk_info) = chunks.first() {
reader.seek(SeekFrom::Start(chunk_info.offset + 8))?;
Some(MbmiChunk::read_le(reader)?)
} else {
None
}
} else {
None
}
} else {
None
};
let root = RootAdt {
version,
mhdr,
mcin,
textures,
models,
model_indices,
wmos,
wmo_indices,
doodad_placements,
wmo_placements,
mcnk_chunks,
flight_bounds,
water_data,
texture_flags,
texture_amplifier,
texture_params,
blend_mesh_headers,
blend_mesh_bounds,
blend_mesh_vertices,
blend_mesh_indices,
};
Ok((root, warnings))
}
fn parse_mh2o_chunk<R: Read + Seek>(
reader: &mut R,
chunk_offset: u64,
chunk_size: u32,
) -> Result<Option<Mh2oChunk>> {
let data_start = chunk_offset + 8;
reader.seek(SeekFrom::Start(data_start))?;
let mut headers = Vec::with_capacity(Mh2oChunk::ENTRY_COUNT);
for _ in 0..Mh2oChunk::ENTRY_COUNT {
let header = Mh2oHeader::read_le(reader)?;
headers.push(header);
}
let mut entries = Vec::with_capacity(Mh2oChunk::ENTRY_COUNT);
for header in headers {
let mut instances = Vec::new();
let mut attributes = None;
if header.has_liquid() {
if header.offset_instances < chunk_size {
reader.seek(SeekFrom::Start(
data_start + u64::from(header.offset_instances),
))?;
for _ in 0..header.layer_count {
match Mh2oInstance::read_le(reader) {
Ok(instance) => instances.push(instance),
Err(_) => break, }
}
}
}
if header.has_attributes() && header.offset_attributes < chunk_size {
reader.seek(SeekFrom::Start(
data_start + u64::from(header.offset_attributes),
))?;
if let Ok(attrs) = Mh2oAttributes::read_le(reader) {
attributes = Some(attrs);
}
}
let mut vertex_data = Vec::with_capacity(instances.len());
let mut exists_bitmaps = Vec::with_capacity(instances.len());
for instance in &instances {
let exists_bitmap = if instance.offset_exists_bitmap != 0
&& u64::from(instance.offset_exists_bitmap) < u64::from(chunk_size)
{
reader.seek(SeekFrom::Start(
data_start + u64::from(instance.offset_exists_bitmap),
))?;
let tile_count = (instance.width as usize) * (instance.height as usize);
let byte_count = tile_count.div_ceil(8);
let mut bitmap_bytes = vec![0u8; byte_count];
match reader.read_exact(&mut bitmap_bytes) {
Ok(_) => {
let mut padded = [0u8; 8];
for (i, &byte) in bitmap_bytes.iter().enumerate() {
padded[i] = byte;
}
let bitmap = u64::from_le_bytes(padded);
Some(bitmap)
}
Err(_) => return Ok(None),
}
} else {
None
};
exists_bitmaps.push(exists_bitmap);
let vdata = if instance.offset_vertex_data != 0
&& u64::from(instance.offset_vertex_data) < u64::from(chunk_size)
{
reader.seek(SeekFrom::Start(
data_start + u64::from(instance.offset_vertex_data),
))?;
use crate::chunks::mh2o::{
DepthOnlyVertex, HeightDepthVertex, HeightUvDepthVertex, HeightUvVertex,
VertexDataArray,
};
let lvf_opt = instance.get_lvf_wotlk();
match lvf_opt {
Some(crate::chunks::mh2o::LiquidVertexFormat::HeightDepth) => {
let mut grid: [Option<HeightDepthVertex>; 81] = [None; 81];
let z_end = ((instance.y_offset + instance.height) as usize).min(8);
let x_end = ((instance.x_offset + instance.width) as usize).min(8);
for z in instance.y_offset as usize..=z_end {
for x in instance.x_offset as usize..=x_end {
match HeightDepthVertex::read_le(reader) {
Ok(v) => {
let idx = z * 9 + x;
grid[idx] = Some(v);
}
Err(_) => return Ok(None), }
}
}
Some(VertexDataArray::HeightDepth(Box::new(grid)))
}
Some(crate::chunks::mh2o::LiquidVertexFormat::HeightUv) => {
let mut grid: [Option<HeightUvVertex>; 81] = [None; 81];
let z_end = ((instance.y_offset + instance.height) as usize).min(8);
let x_end = ((instance.x_offset + instance.width) as usize).min(8);
for z in instance.y_offset as usize..=z_end {
for x in instance.x_offset as usize..=x_end {
match HeightUvVertex::read_le(reader) {
Ok(v) => {
let idx = z * 9 + x;
grid[idx] = Some(v);
}
Err(_) => return Ok(None),
}
}
}
Some(VertexDataArray::HeightUv(Box::new(grid)))
}
Some(crate::chunks::mh2o::LiquidVertexFormat::DepthOnly) => {
let mut grid: [Option<DepthOnlyVertex>; 81] = [None; 81];
let z_end = ((instance.y_offset + instance.height) as usize).min(8);
let x_end = ((instance.x_offset + instance.width) as usize).min(8);
for z in instance.y_offset as usize..=z_end {
for x in instance.x_offset as usize..=x_end {
match DepthOnlyVertex::read_le(reader) {
Ok(v) => {
let idx = z * 9 + x;
grid[idx] = Some(v);
}
Err(_) => return Ok(None),
}
}
}
Some(VertexDataArray::DepthOnly(Box::new(grid)))
}
Some(crate::chunks::mh2o::LiquidVertexFormat::HeightUvDepth) => {
let mut grid: [Option<HeightUvDepthVertex>; 81] = [None; 81];
let z_end = ((instance.y_offset + instance.height) as usize).min(8);
let x_end = ((instance.x_offset + instance.width) as usize).min(8);
for z in instance.y_offset as usize..=z_end {
for x in instance.x_offset as usize..=x_end {
match HeightUvDepthVertex::read_le(reader) {
Ok(v) => {
let idx = z * 9 + x;
grid[idx] = Some(v);
}
Err(_) => return Ok(None),
}
}
}
Some(VertexDataArray::HeightUvDepth(Box::new(grid)))
}
None => None, }
} else {
None
};
vertex_data.push(vdata);
}
entries.push(Mh2oEntry {
header,
instances,
vertex_data,
exists_bitmaps,
attributes,
});
}
let chunk = Mh2oChunk { entries };
if chunk.has_any_liquid() {
Ok(Some(chunk))
} else {
Ok(None)
}
}
fn parse_mcnk_chunks<R: Read + Seek>(
reader: &mut R,
discovery: &ChunkDiscovery,
) -> Result<Vec<McnkChunk>> {
let mcnk_locations = discovery
.get_chunks(ChunkId::MCNK)
.ok_or(AdtError::MissingRequiredChunk(ChunkId::MCNK))?;
let mut mcnk_chunks = Vec::with_capacity(mcnk_locations.len());
for (index, chunk_info) in mcnk_locations.iter().enumerate() {
reader.seek(SeekFrom::Start(chunk_info.offset + 8))?;
let mcnk =
McnkChunk::parse_with_offset_and_size(reader, chunk_info.offset, chunk_info.size)
.map_err(|e| {
log::error!(
"Failed to parse MCNK chunk {} at offset {}: {:?}",
index,
chunk_info.offset,
e
);
e
})?;
mcnk_chunks.push(mcnk);
}
log::debug!("Parsed {} MCNK chunks", mcnk_chunks.len());
Ok(mcnk_chunks)
}
fn parse_simple_chunk<T, R>(
reader: &mut R,
discovery: &ChunkDiscovery,
chunk_id: ChunkId,
) -> Result<T>
where
T: for<'a> BinRead<Args<'a> = ()>,
R: Read + Seek,
{
let chunks = discovery
.get_chunks(chunk_id)
.ok_or(AdtError::MissingRequiredChunk(chunk_id))?;
let chunk_info = chunks
.first()
.ok_or(AdtError::MissingRequiredChunk(chunk_id))?;
reader.seek(SeekFrom::Start(chunk_info.offset + 8))?;
T::read_le(reader).map_err(|e| AdtError::ChunkParseError {
chunk: chunk_id,
offset: chunk_info.offset,
details: format!("{e}"),
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::chunk_discovery::discover_chunks;
use std::io::Cursor;
fn create_minimal_root_adt() -> Vec<u8> {
let mut data = Vec::new();
data.extend_from_slice(&ChunkId::MVER.0);
data.extend_from_slice(&4u32.to_le_bytes());
data.extend_from_slice(&18u32.to_le_bytes());
data.extend_from_slice(&ChunkId::MHDR.0);
data.extend_from_slice(&64u32.to_le_bytes());
data.extend_from_slice(&[0u8; 64]);
data.extend_from_slice(&ChunkId::MCIN.0);
data.extend_from_slice(&4096u32.to_le_bytes());
data.extend_from_slice(&[0u8; 4096]);
data.extend_from_slice(&ChunkId::MTEX.0);
data.extend_from_slice(&0u32.to_le_bytes());
data.extend_from_slice(&ChunkId::MCNK.0);
data.extend_from_slice(&136u32.to_le_bytes());
data.extend_from_slice(&[0u8; 136]);
data
}
#[test]
fn test_parse_minimal_root_adt() {
let data = create_minimal_root_adt();
let mut cursor = Cursor::new(data);
let discovery = discover_chunks(&mut cursor).unwrap();
let version = AdtVersion::from_discovery(&discovery);
cursor.set_position(0);
let result = parse_root_adt(&mut cursor, &discovery, version);
assert!(result.is_ok());
let (root, warnings) = result.unwrap();
assert_eq!(root.version, AdtVersion::VanillaEarly);
assert_eq!(root.textures.len(), 0);
assert_eq!(root.mcnk_chunks.len(), 1);
assert!(warnings.is_empty());
}
#[test]
fn test_parse_root_missing_mhdr() {
let mut data = Vec::new();
data.extend_from_slice(&ChunkId::MVER.0);
data.extend_from_slice(&4u32.to_le_bytes());
data.extend_from_slice(&18u32.to_le_bytes());
let mut cursor = Cursor::new(data);
let discovery = discover_chunks(&mut cursor).unwrap();
let version = AdtVersion::from_discovery(&discovery);
cursor.set_position(0);
let result = parse_root_adt(&mut cursor, &discovery, version);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
AdtError::MissingRequiredChunk(_)
));
}
#[test]
fn test_parse_root_missing_mcin() {
let mut data = Vec::new();
data.extend_from_slice(&ChunkId::MVER.0);
data.extend_from_slice(&4u32.to_le_bytes());
data.extend_from_slice(&18u32.to_le_bytes());
data.extend_from_slice(&ChunkId::MHDR.0);
data.extend_from_slice(&64u32.to_le_bytes());
data.extend_from_slice(&[0u8; 64]);
let mut cursor = Cursor::new(data);
let discovery = discover_chunks(&mut cursor).unwrap();
let version = AdtVersion::from_discovery(&discovery);
cursor.set_position(0);
let result = parse_root_adt(&mut cursor, &discovery, version);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
AdtError::MissingRequiredChunk(_)
));
}
fn create_split_root_adt() -> Vec<u8> {
let mut data = Vec::new();
data.extend_from_slice(&ChunkId::MVER.0);
data.extend_from_slice(&4u32.to_le_bytes());
data.extend_from_slice(&18u32.to_le_bytes());
data.extend_from_slice(&ChunkId::MHDR.0);
data.extend_from_slice(&64u32.to_le_bytes());
data.extend_from_slice(&[0u8; 64]);
data.extend_from_slice(&ChunkId::MCNK.0);
data.extend_from_slice(&136u32.to_le_bytes());
data.extend_from_slice(&[0u8; 136]);
data
}
#[test]
fn test_parse_split_root_adt() {
let data = create_split_root_adt();
let mut cursor = Cursor::new(data);
let discovery = discover_chunks(&mut cursor).unwrap();
let version = AdtVersion::from_discovery(&discovery);
cursor.set_position(0);
let result = parse_root_adt(&mut cursor, &discovery, version);
assert!(result.is_ok());
let (root, warnings) = result.unwrap();
assert_eq!(root.mcin.entries.len(), 0);
assert_eq!(root.textures.len(), 0);
assert_eq!(root.mcnk_chunks.len(), 1);
assert!(warnings.is_empty());
}
#[test]
fn test_split_root_detection() {
let data = create_split_root_adt();
let mut cursor = Cursor::new(data);
let discovery = discover_chunks(&mut cursor).unwrap();
assert!(!discovery.has_chunk(ChunkId::MCIN));
assert!(!discovery.has_chunk(ChunkId::MTEX));
assert!(discovery.has_chunk(ChunkId::MHDR));
assert!(discovery.has_chunk(ChunkId::MCNK));
}
#[test]
fn test_split_root_without_mcnk_fails() {
let mut data = Vec::new();
data.extend_from_slice(&ChunkId::MVER.0);
data.extend_from_slice(&4u32.to_le_bytes());
data.extend_from_slice(&18u32.to_le_bytes());
data.extend_from_slice(&ChunkId::MHDR.0);
data.extend_from_slice(&64u32.to_le_bytes());
data.extend_from_slice(&[0u8; 64]);
let mut cursor = Cursor::new(data);
let discovery = discover_chunks(&mut cursor).unwrap();
let version = AdtVersion::from_discovery(&discovery);
cursor.set_position(0);
let result = parse_root_adt(&mut cursor, &discovery, version);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
AdtError::MissingRequiredChunk(ChunkId::MCNK)
));
}
#[test]
fn test_monolithic_root_requires_mcin_and_mtex() {
let mut data = Vec::new();
data.extend_from_slice(&ChunkId::MVER.0);
data.extend_from_slice(&4u32.to_le_bytes());
data.extend_from_slice(&18u32.to_le_bytes());
data.extend_from_slice(&ChunkId::MHDR.0);
data.extend_from_slice(&64u32.to_le_bytes());
data.extend_from_slice(&[0u8; 64]);
data.extend_from_slice(&ChunkId::MCIN.0);
data.extend_from_slice(&4096u32.to_le_bytes());
data.extend_from_slice(&[0u8; 4096]);
data.extend_from_slice(&ChunkId::MCNK.0);
data.extend_from_slice(&128u32.to_le_bytes());
data.extend_from_slice(&[0u8; 128]);
let mut cursor = Cursor::new(data);
let discovery = discover_chunks(&mut cursor).unwrap();
let version = AdtVersion::from_discovery(&discovery);
cursor.set_position(0);
let result = parse_root_adt(&mut cursor, &discovery, version);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
AdtError::MissingRequiredChunk(ChunkId::MTEX)
));
}
}