use crate::chunk_header::ChunkHeader;
use crate::chunk_id::ChunkId;
use binrw::{BinRead, BinResult};
use std::io::{Read, Seek, SeekFrom};
use super::header::McnkHeader;
use super::mcal::McalChunk;
use super::mcbb::McbbChunk;
use super::mccv::MccvChunk;
use super::mcdd::McddChunk;
use super::mclq::MclqChunk;
use super::mclv::MclvChunk;
use super::mcly::MclyChunk;
use super::mcmt::McmtChunk;
use super::mcnr::McnrChunk;
use super::mcrd::McrdChunk;
use super::mcrf::McrfChunk;
use super::mcrw::McrwChunk;
use super::mcse::McseChunk;
use super::mcsh::McshChunk;
use super::mcvt::McvtChunk;
#[derive(Debug, Clone)]
pub struct McnkChunk {
pub header: McnkHeader,
pub heights: Option<McvtChunk>,
pub normals: Option<McnrChunk>,
pub layers: Option<MclyChunk>,
pub materials: Option<McmtChunk>,
pub refs: Option<McrfChunk>,
pub doodad_refs: Option<McrdChunk>,
pub wmo_refs: Option<McrwChunk>,
pub alpha: Option<McalChunk>,
pub shadow: Option<McshChunk>,
pub vertex_colors: Option<MccvChunk>,
pub vertex_lighting: Option<MclvChunk>,
pub sound_emitters: Option<McseChunk>,
pub liquid: Option<MclqChunk>,
pub doodad_disable: Option<McddChunk>,
pub blend_batches: Option<McbbChunk>,
}
impl McnkChunk {
pub fn parse_with_offset<R: Read + Seek>(
reader: &mut R,
mcnk_start_offset: u64,
) -> BinResult<Self> {
Self::parse_with_offset_and_size(reader, mcnk_start_offset, 0x40000)
}
pub fn parse_with_offset_and_size<R: Read + Seek>(
reader: &mut R,
mcnk_start_offset: u64,
mcnk_size: u32,
) -> BinResult<Self> {
let header = McnkHeader::read_le(reader)?;
let heights = if header.has_height() {
let data = read_subchunk(reader, mcnk_start_offset, header.ofs_height(), "MCVT")?;
if !data.is_empty() {
Some(McvtChunk::read_le(&mut std::io::Cursor::new(data))?)
} else {
None
}
} else {
let data = scan_for_subchunk(reader, mcnk_start_offset, mcnk_size, ChunkId::MCVT)?;
if !data.is_empty() {
Some(McvtChunk::read_le(&mut std::io::Cursor::new(data))?)
} else {
None
}
};
let normals = if header.has_normal() {
let data = read_subchunk(reader, mcnk_start_offset, header.ofs_normal(), "MCNR")?;
if !data.is_empty() {
Some(McnrChunk::read_le(&mut std::io::Cursor::new(data))?)
} else {
None
}
} else {
let data = scan_for_subchunk(reader, mcnk_start_offset, mcnk_size, ChunkId::MCNR)?;
if !data.is_empty() {
Some(McnrChunk::read_le(&mut std::io::Cursor::new(data))?)
} else {
None
}
};
let layers = if header.has_layer() {
let data = read_subchunk(reader, mcnk_start_offset, header.ofs_layer, "MCLY")?;
if !data.is_empty() {
Some(MclyChunk::read_le(&mut std::io::Cursor::new(data))?)
} else {
None
}
} else {
None
};
let materials = None;
let refs = if header.has_refs() {
let data = read_subchunk(reader, mcnk_start_offset, header.ofs_refs, "MCRF")?;
if !data.is_empty() {
Some(McrfChunk::read_le(&mut std::io::Cursor::new(data))?)
} else {
None
}
} else {
None
};
let doodad_refs = if header.has_refs() {
let data = read_subchunk(reader, mcnk_start_offset, header.ofs_refs, "MCRF")?;
if !data.is_empty() {
Some(McrdChunk::read_le(&mut std::io::Cursor::new(data))?)
} else {
None
}
} else {
None
};
let wmo_refs = if header.has_refs() {
let data = read_subchunk(reader, mcnk_start_offset, header.ofs_refs, "MCRF")?;
if !data.is_empty() {
Some(McrwChunk::read_le(&mut std::io::Cursor::new(data))?)
} else {
None
}
} else {
None
};
let alpha = if header.has_alpha() {
let data = read_subchunk_with_size(
reader,
mcnk_start_offset,
header.ofs_alpha,
header.size_alpha,
"MCAL",
)?;
if !data.is_empty() {
Some(McalChunk::read_le(&mut std::io::Cursor::new(data))?)
} else {
None
}
} else {
None
};
let shadow = if header.has_shadow() {
let data = read_subchunk_with_size(
reader,
mcnk_start_offset,
header.ofs_shadow,
header.size_shadow,
"MCSH",
)?;
if !data.is_empty() {
Some(McshChunk::read_le(&mut std::io::Cursor::new(data))?)
} else {
None
}
} else {
None
};
let vertex_colors = if header.has_vertex_colors() {
let data = read_subchunk(reader, mcnk_start_offset, header.ofs_mccv, "MCCV")?;
if !data.is_empty() {
Some(MccvChunk::read_le(&mut std::io::Cursor::new(data))?)
} else {
None
}
} else {
None
};
let vertex_lighting = if header.has_baked_lighting() {
let data = read_subchunk(reader, mcnk_start_offset, header.ofs_mclv, "MCLV")?;
if !data.is_empty() {
Some(MclvChunk::read_le(&mut std::io::Cursor::new(data))?)
} else {
None
}
} else {
None
};
let sound_emitters = if header.has_sound_emitters() {
let data = read_subchunk(reader, mcnk_start_offset, header.ofs_snd_emitters, "MCSE")?;
if !data.is_empty() {
Some(McseChunk::read_le(&mut std::io::Cursor::new(data))?)
} else {
None
}
} else {
None
};
let liquid = if header.has_legacy_liquid() {
let mclq_pos = mcnk_start_offset + u64::from(header.ofs_liquid);
reader
.seek(SeekFrom::Start(mclq_pos))
.map_err(|e| binrw::Error::Custom {
pos: mclq_pos,
err: Box::new(format!(
"Failed to seek to MCLQ at offset {}: {}",
header.ofs_liquid, e
)),
})?;
let _chunk_header = ChunkHeader::read_le(reader)?;
let mut data = vec![0u8; header.size_liquid as usize];
reader.read_exact(&mut data)?;
if !data.is_empty() {
MclqChunk::read_le_args(&mut std::io::Cursor::new(data), header.flags.value).ok() } else {
None
}
} else {
None
};
let doodad_disable = None;
Ok(Self {
header,
heights,
normals,
layers,
materials,
refs,
doodad_refs,
wmo_refs,
alpha,
shadow,
vertex_colors,
vertex_lighting,
sound_emitters,
liquid,
doodad_disable,
blend_batches: None, })
}
pub fn has_heights(&self) -> bool {
self.heights.is_some()
}
pub fn has_normals(&self) -> bool {
self.normals.is_some()
}
pub fn has_layers(&self) -> bool {
self.layers.is_some()
}
pub fn has_refs(&self) -> bool {
self.refs.is_some()
}
pub fn has_alpha(&self) -> bool {
self.alpha.is_some()
}
pub fn has_shadow(&self) -> bool {
self.shadow.is_some()
}
pub fn has_vertex_colors(&self) -> bool {
self.vertex_colors.is_some()
}
pub fn has_sound_emitters(&self) -> bool {
self.sound_emitters.is_some()
}
pub fn has_liquid(&self) -> bool {
self.liquid.is_some()
}
pub fn validate_consistency(&self) -> bool {
(self.header.has_height() == self.has_heights())
&& (self.header.has_normal() == self.has_normals())
&& (self.header.has_layer() == self.has_layers())
&& (self.header.has_shadow() == self.has_shadow())
&& (self.header.has_vertex_colors() == self.has_vertex_colors())
&& (self.header.has_legacy_liquid() == self.has_liquid())
}
}
fn read_subchunk<R: Read + Seek>(
reader: &mut R,
mcnk_start_offset: u64,
offset: u32,
name: &str,
) -> BinResult<Vec<u8>> {
if offset == 0 {
return Ok(Vec::new());
}
let subchunk_pos = mcnk_start_offset + u64::from(offset);
reader
.seek(SeekFrom::Start(subchunk_pos))
.map_err(|e| binrw::Error::Custom {
pos: subchunk_pos,
err: Box::new(format!(
"Failed to seek to {} subchunk at offset {}: {}",
name, offset, e
)),
})?;
let subchunk_header = ChunkHeader::read_le(reader).map_err(|e| binrw::Error::Custom {
pos: subchunk_pos,
err: Box::new(format!(
"Failed to read {} subchunk header at file offset {} (MCNK+{}): {:?}",
name, subchunk_pos, offset, e
)),
})?;
let mut data = vec![0u8; subchunk_header.size as usize];
reader.read_exact(&mut data)?;
Ok(data)
}
fn read_subchunk_with_size<R: Read + Seek>(
reader: &mut R,
mcnk_start_offset: u64,
offset: u32,
expected_size: u32,
name: &str,
) -> BinResult<Vec<u8>> {
if offset == 0 || expected_size == 0 {
return Ok(Vec::new());
}
let subchunk_pos = mcnk_start_offset + u64::from(offset);
reader
.seek(SeekFrom::Start(subchunk_pos))
.map_err(|e| binrw::Error::Custom {
pos: subchunk_pos,
err: Box::new(format!(
"Failed to seek to {} subchunk at offset {}: {}",
name, offset, e
)),
})?;
let subchunk_header = ChunkHeader::read_le(reader).map_err(|e| binrw::Error::Custom {
pos: subchunk_pos,
err: Box::new(format!(
"Failed to read {} subchunk header at file offset {} (MCNK+{}): {:?}",
name, subchunk_pos, offset, e
)),
})?;
if subchunk_header.id.as_str() != name {
log::warn!(
"{} subchunk at 0x{:x} has unexpected magic '{}' (expected '{}')",
name,
subchunk_pos,
subchunk_header.id.as_str(),
name
);
}
if subchunk_header.size != expected_size && subchunk_header.size < 0x100000 {
log::debug!(
"{} subchunk size mismatch: header says 0x{:x}, MCNK header says 0x{:x}",
name,
subchunk_header.size,
expected_size
);
}
let mut data = vec![0u8; expected_size as usize];
reader.read_exact(&mut data)?;
Ok(data)
}
fn scan_for_subchunk<R: Read + Seek>(
reader: &mut R,
mcnk_start_offset: u64,
mcnk_size: u32,
target_id: ChunkId,
) -> BinResult<Vec<u8>> {
let scan_start = mcnk_start_offset + 8 + 128; let scan_end = mcnk_start_offset + 8 + u64::from(mcnk_size);
let mut pos = scan_start;
while pos + 8 <= scan_end {
reader
.seek(SeekFrom::Start(pos))
.map_err(|e| binrw::Error::Custom {
pos,
err: Box::new(format!("Failed to seek during subchunk scan: {}", e)),
})?;
let subchunk_header = match ChunkHeader::read_le(reader) {
Ok(h) => h,
Err(_) => {
pos += 1;
continue;
}
};
if subchunk_header.id == target_id {
let mut data = vec![0u8; subchunk_header.size as usize];
reader.read_exact(&mut data)?;
return Ok(data);
}
pos += 8 + u64::from(subchunk_header.size);
}
Ok(Vec::new()) }
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
fn create_test_mcnk_data() -> Vec<u8> {
let mut data = Vec::new();
data.extend_from_slice(b"MCNK"); data.extend_from_slice(&500u32.to_le_bytes());
let header_start = data.len();
data.extend_from_slice(&0x0041u32.to_le_bytes());
data.extend_from_slice(&5u32.to_le_bytes());
data.extend_from_slice(&7u32.to_le_bytes());
data.extend_from_slice(&2u32.to_le_bytes());
data.extend_from_slice(&0u32.to_le_bytes());
data.extend_from_slice(&136u32.to_le_bytes()); data.extend_from_slice(&0u32.to_le_bytes());
data.extend_from_slice(&0u32.to_le_bytes());
data.extend_from_slice(&0u32.to_le_bytes());
data.extend_from_slice(&0u32.to_le_bytes());
data.extend_from_slice(&0u32.to_le_bytes());
data.extend_from_slice(&0u32.to_le_bytes());
data.extend_from_slice(&0u32.to_le_bytes());
data.extend_from_slice(&1234u32.to_le_bytes());
data.extend_from_slice(&0u32.to_le_bytes());
data.extend_from_slice(&0u16.to_le_bytes());
data.extend_from_slice(&1u16.to_le_bytes());
data.extend_from_slice(&[0u8; 16]);
data.extend_from_slice(&0u32.to_le_bytes());
data.extend_from_slice(&0u32.to_le_bytes());
data.extend_from_slice(&0u32.to_le_bytes());
data.extend_from_slice(&0u32.to_le_bytes());
data.extend_from_slice(&0.0f32.to_le_bytes());
data.extend_from_slice(&0.0f32.to_le_bytes());
data.extend_from_slice(&0.0f32.to_le_bytes());
data.extend_from_slice(&0u32.to_le_bytes());
data.extend_from_slice(&0u32.to_le_bytes());
data.extend_from_slice(&0u32.to_le_bytes());
data.extend_from_slice(&[0u8; 8]);
assert_eq!(
data.len() - header_start,
128,
"Header must be exactly 128 bytes"
);
assert_eq!(data.len(), 136, "Data should be at offset 136 before MCVT");
data.extend_from_slice(b"MCVT"); data.extend_from_slice(&(145 * 4u32).to_le_bytes());
for i in 0..145 {
data.extend_from_slice(&(i as f32).to_le_bytes());
}
data
}
#[test]
fn test_mcnk_chunk_parse() {
let data = create_test_mcnk_data();
let mut cursor = Cursor::new(data);
cursor.set_position(8);
let mcnk = McnkChunk::parse_with_offset(&mut cursor, 0).unwrap();
assert_eq!(mcnk.header.index_x, 5);
assert_eq!(mcnk.header.index_y, 7);
assert_eq!(mcnk.header.n_layers, 2);
assert_eq!(mcnk.header.area_id, 1234);
assert!(mcnk.has_heights());
assert_eq!(mcnk.heights.as_ref().unwrap().heights.len(), 145);
assert!(!mcnk.has_normals());
assert!(!mcnk.has_layers());
assert!(!mcnk.has_refs());
assert!(!mcnk.has_alpha());
assert!(!mcnk.has_shadow());
assert!(!mcnk.has_vertex_colors());
assert!(!mcnk.has_sound_emitters());
assert!(!mcnk.has_liquid());
}
#[test]
fn test_mcnk_chunk_validate_consistency() {
let data = create_test_mcnk_data();
let mut cursor = Cursor::new(data);
cursor.set_position(8);
let mcnk = McnkChunk::parse_with_offset(&mut cursor, 0).unwrap();
assert!(mcnk.header.has_height());
assert!(mcnk.has_heights());
}
#[test]
fn test_mcnk_chunk_presence_checks() {
let data = create_test_mcnk_data();
let mut cursor = Cursor::new(data);
cursor.set_position(8);
let mcnk = McnkChunk::parse_with_offset(&mut cursor, 0).unwrap();
assert!(mcnk.has_heights());
assert!(!mcnk.has_normals());
assert!(!mcnk.has_layers());
}
#[test]
fn test_mcnk_chunk_height_access() {
let data = create_test_mcnk_data();
let mut cursor = Cursor::new(data);
cursor.set_position(8);
let mcnk = McnkChunk::parse_with_offset(&mut cursor, 0).unwrap();
if let Some(heights) = &mcnk.heights {
assert_eq!(heights.heights[0], 0.0);
assert_eq!(heights.heights[1], 1.0);
assert_eq!(heights.heights[144], 144.0);
} else {
panic!("Heights should be present");
}
}
}