use std::io::{Cursor, Read, Seek, SeekFrom};
use binrw::BinRead;
use crate::api::{LodAdt, McnkChunkObject, McnkChunkTexture, Obj0Adt, Tex0Adt};
use crate::chunk_discovery::ChunkDiscovery;
use crate::chunk_header::ChunkHeader;
use crate::chunk_id::ChunkId;
use crate::chunks::mcnk::{McrdChunk, McrwChunk};
use crate::chunks::{
McalChunk, MclyChunk, MddfChunk, MmdxChunk, MmidChunk, ModfChunk, MtexChunk, MtxpChunk,
MwidChunk, MwmoChunk,
};
use crate::error::Result;
use crate::version::AdtVersion;
pub fn parse_tex_adt<R: Read + Seek>(
reader: &mut R,
discovery: &ChunkDiscovery,
version: AdtVersion,
) -> Result<(Tex0Adt, Vec<String>)> {
let warnings = Vec::new();
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 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 mcnk_textures = parse_mcnk_texture_chunks(reader, discovery)?;
let tex = Tex0Adt {
version,
textures,
texture_params,
mcnk_textures,
};
Ok((tex, warnings))
}
pub fn parse_obj_adt<R: Read + Seek>(
reader: &mut R,
discovery: &ChunkDiscovery,
version: AdtVersion,
) -> Result<(Obj0Adt, Vec<String>)> {
let warnings = 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 = 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 = 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 = 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 = 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 = 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 = Cursor::new(chunk_data);
let modf = ModfChunk::read_le(&mut cursor)?;
modf.placements
} else {
Vec::new()
}
} else {
Vec::new()
};
let mcnk_objects = parse_mcnk_object_chunks(reader, discovery)?;
let obj = Obj0Adt {
version,
models,
model_indices,
wmos,
wmo_indices,
doodad_placements,
wmo_placements,
mcnk_objects,
};
Ok((obj, warnings))
}
pub fn parse_lod_adt<R: Read + Seek>(
_reader: &mut R,
_discovery: &ChunkDiscovery,
version: AdtVersion,
) -> Result<(LodAdt, Vec<String>)> {
let warnings = vec!["LOD file format not yet fully implemented".to_string()];
let lod = LodAdt { version };
Ok((lod, warnings))
}
fn parse_mcnk_texture_chunks<R: Read + Seek>(
reader: &mut R,
discovery: &ChunkDiscovery,
) -> Result<Vec<McnkChunkTexture>> {
let mut mcnk_textures = Vec::new();
let mcnk_chunks = match discovery.get_chunks(ChunkId::MCNK) {
Some(chunks) => chunks,
None => return Ok(mcnk_textures), };
for (index, chunk_info) in mcnk_chunks.iter().enumerate() {
reader.seek(SeekFrom::Start(chunk_info.offset + 8))?;
let mcnk_end = chunk_info.offset + 8 + u64::from(chunk_info.size);
let mut current_pos = chunk_info.offset + 8;
let mut layers = None;
let mut alpha_maps = None;
while current_pos < mcnk_end {
reader.seek(SeekFrom::Start(current_pos))?;
let subchunk_header = match ChunkHeader::read_le(reader) {
Ok(header) => header,
Err(_) => break, };
current_pos += 8;
match subchunk_header.id {
ChunkId::MCLY => {
let mut chunk_data = vec![0u8; subchunk_header.size as usize];
reader.seek(SeekFrom::Start(current_pos))?;
reader.read_exact(&mut chunk_data)?;
let mut cursor = std::io::Cursor::new(chunk_data);
layers = Some(MclyChunk::read_le(&mut cursor)?);
}
ChunkId::MCAL => {
let mut chunk_data = vec![0u8; subchunk_header.size as usize];
reader.seek(SeekFrom::Start(current_pos))?;
reader.read_exact(&mut chunk_data)?;
let mut cursor = std::io::Cursor::new(chunk_data);
alpha_maps = Some(McalChunk::read_le(&mut cursor)?);
}
_ => {
}
}
current_pos += u64::from(subchunk_header.size);
}
mcnk_textures.push(McnkChunkTexture {
index,
layers,
alpha_maps,
});
}
Ok(mcnk_textures)
}
fn parse_mcnk_object_chunks<R: Read + Seek>(
reader: &mut R,
discovery: &ChunkDiscovery,
) -> Result<Vec<McnkChunkObject>> {
let mut mcnk_objects = Vec::new();
let mcnk_chunks = match discovery.get_chunks(ChunkId::MCNK) {
Some(chunks) => chunks,
None => return Ok(mcnk_objects), };
for (index, chunk_info) in mcnk_chunks.iter().enumerate() {
reader.seek(SeekFrom::Start(chunk_info.offset + 8))?;
let mcnk_end = chunk_info.offset + 8 + u64::from(chunk_info.size);
let mut current_pos = chunk_info.offset + 8;
let mut doodad_refs = Vec::new();
let mut wmo_refs = Vec::new();
while current_pos < mcnk_end {
reader.seek(SeekFrom::Start(current_pos))?;
let subchunk_header = match ChunkHeader::read_le(reader) {
Ok(header) => header,
Err(_) => break, };
current_pos += 8;
match subchunk_header.id {
ChunkId::MCRD => {
reader.seek(SeekFrom::Start(current_pos))?;
let mcrd = McrdChunk::read_le(reader)?;
doodad_refs = mcrd.doodad_refs;
}
ChunkId::MCRW => {
reader.seek(SeekFrom::Start(current_pos))?;
let mcrw = McrwChunk::read_le(reader)?;
wmo_refs = mcrw.wmo_refs;
}
_ => {
}
}
current_pos += u64::from(subchunk_header.size);
}
mcnk_objects.push(McnkChunkObject {
index,
doodad_refs,
wmo_refs,
});
}
Ok(mcnk_objects)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::chunk_discovery::discover_chunks;
use std::io::Cursor;
fn create_minimal_tex_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::MTEX.0);
let texture = b"Tileset\\Terrain.blp\0";
data.extend_from_slice(&(texture.len() as u32).to_le_bytes());
data.extend_from_slice(texture);
data
}
fn create_minimal_obj_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::MMDX.0);
let model = b"World\\Doodad\\Model.m2\0";
data.extend_from_slice(&(model.len() as u32).to_le_bytes());
data.extend_from_slice(model);
data.extend_from_slice(&ChunkId::MMID.0);
data.extend_from_slice(&4u32.to_le_bytes());
data.extend_from_slice(&0u32.to_le_bytes());
data.extend_from_slice(&ChunkId::MDDF.0);
data.extend_from_slice(&0u32.to_le_bytes());
data
}
#[test]
fn test_parse_tex_adt() {
let data = create_minimal_tex_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_tex_adt(&mut cursor, &discovery, version);
assert!(result.is_ok());
let (tex, warnings) = result.unwrap();
assert_eq!(tex.version, AdtVersion::VanillaEarly);
assert_eq!(tex.textures.len(), 1);
assert_eq!(tex.textures[0], "Tileset\\Terrain.blp");
assert!(warnings.is_empty());
}
#[test]
fn test_parse_obj_adt() {
let data = create_minimal_obj_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_obj_adt(&mut cursor, &discovery, version);
assert!(result.is_ok());
let (obj, warnings) = result.unwrap();
assert_eq!(obj.version, AdtVersion::VanillaEarly);
assert_eq!(obj.models.len(), 1);
assert_eq!(obj.models[0], "World\\Doodad\\Model.m2");
assert_eq!(obj.model_indices.len(), 1);
assert!(warnings.is_empty());
}
#[test]
fn test_parse_lod_adt() {
let data = Vec::new();
let mut cursor = Cursor::new(data);
let discovery = crate::chunk_discovery::ChunkDiscovery::new(0);
let version = AdtVersion::Cataclysm;
let result = parse_lod_adt(&mut cursor, &discovery, version);
assert!(result.is_ok());
let (lod, warnings) = result.unwrap();
assert_eq!(lod.version, AdtVersion::Cataclysm);
assert!(!warnings.is_empty());
}
fn create_obj_adt_with_mcnk() -> 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::MMDX.0);
let model1 = b"World\\Doodad\\Tree.m2\0";
let model2 = b"World\\Doodad\\Rock.m2\0";
let mmdx_size = model1.len() + model2.len();
data.extend_from_slice(&(mmdx_size as u32).to_le_bytes());
data.extend_from_slice(model1);
data.extend_from_slice(model2);
data.extend_from_slice(&ChunkId::MMID.0);
data.extend_from_slice(&8u32.to_le_bytes());
data.extend_from_slice(&0u32.to_le_bytes());
data.extend_from_slice(&(model1.len() as u32).to_le_bytes());
data.extend_from_slice(&ChunkId::MDDF.0);
data.extend_from_slice(&0u32.to_le_bytes());
data.extend_from_slice(&ChunkId::MCNK.0);
let mcnk_start = data.len();
data.extend_from_slice(&0u32.to_le_bytes());
data.extend_from_slice(&ChunkId::MCRD.0);
data.extend_from_slice(&8u32.to_le_bytes()); data.extend_from_slice(&0u32.to_le_bytes()); data.extend_from_slice(&1u32.to_le_bytes());
let mcnk_size = data.len() - mcnk_start - 4;
let size_bytes = (mcnk_size as u32).to_le_bytes();
data[mcnk_start..mcnk_start + 4].copy_from_slice(&size_bytes);
data
}
#[test]
fn test_parse_obj_adt_with_mcnk() {
let data = create_obj_adt_with_mcnk();
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_obj_adt(&mut cursor, &discovery, version);
assert!(result.is_ok());
let (obj, warnings) = result.unwrap();
assert_eq!(obj.models.len(), 2);
assert_eq!(obj.model_indices.len(), 2);
assert_eq!(obj.mcnk_objects.len(), 1);
assert_eq!(obj.mcnk_objects[0].index, 0);
assert_eq!(obj.mcnk_objects[0].doodad_refs.len(), 2);
assert_eq!(obj.mcnk_objects[0].doodad_refs[0], 0);
assert_eq!(obj.mcnk_objects[0].doodad_refs[1], 1);
assert!(obj.mcnk_objects[0].wmo_refs.is_empty());
assert!(warnings.is_empty());
}
#[test]
fn test_parse_obj_adt_file_type_detection() {
let data = create_minimal_obj_adt();
let mut cursor = Cursor::new(data);
let discovery = discover_chunks(&mut cursor).unwrap();
assert_eq!(
crate::file_type::AdtFileType::from_discovery(&discovery),
crate::file_type::AdtFileType::Obj0
);
}
#[test]
fn test_parse_obj_adt_empty_mcnk() {
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::MMDX.0);
let model = b"World\\Doodad\\Tree.m2\0";
data.extend_from_slice(&(model.len() as u32).to_le_bytes());
data.extend_from_slice(model);
data.extend_from_slice(&ChunkId::MMID.0);
data.extend_from_slice(&4u32.to_le_bytes());
data.extend_from_slice(&0u32.to_le_bytes());
data.extend_from_slice(&ChunkId::MDDF.0);
data.extend_from_slice(&0u32.to_le_bytes());
data.extend_from_slice(&ChunkId::MCNK.0);
data.extend_from_slice(&0u32.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_obj_adt(&mut cursor, &discovery, version);
assert!(result.is_ok());
let (obj, _) = result.unwrap();
assert_eq!(obj.mcnk_objects.len(), 1);
assert_eq!(obj.mcnk_objects[0].index, 0);
assert!(obj.mcnk_objects[0].doodad_refs.is_empty());
assert!(obj.mcnk_objects[0].wmo_refs.is_empty());
}
fn create_tex_adt_with_mcnk() -> 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::MTEX.0);
let texture1 = b"Tileset\\Ground.blp\0";
let texture2 = b"Tileset\\Grass.blp\0";
let mtex_size = texture1.len() + texture2.len();
data.extend_from_slice(&(mtex_size as u32).to_le_bytes());
data.extend_from_slice(texture1);
data.extend_from_slice(texture2);
data.extend_from_slice(&ChunkId::MCNK.0);
let mcnk_start = data.len();
data.extend_from_slice(&0u32.to_le_bytes());
data.extend_from_slice(&ChunkId::MCLY.0);
data.extend_from_slice(&32u32.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(&1u32.to_le_bytes()); data.extend_from_slice(&0u32.to_le_bytes()); data.extend_from_slice(&2048u32.to_le_bytes()); data.extend_from_slice(&0u32.to_le_bytes());
let mcnk_size = data.len() - mcnk_start - 4;
let size_bytes = (mcnk_size as u32).to_le_bytes();
data[mcnk_start..mcnk_start + 4].copy_from_slice(&size_bytes);
data
}
#[test]
fn test_parse_tex_adt_with_mcnk() {
let data = create_tex_adt_with_mcnk();
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_tex_adt(&mut cursor, &discovery, version);
assert!(result.is_ok());
let (tex, warnings) = result.unwrap();
assert_eq!(tex.textures.len(), 2);
assert_eq!(tex.mcnk_textures.len(), 1);
assert_eq!(tex.mcnk_textures[0].index, 0);
assert!(tex.mcnk_textures[0].layers.is_some());
let layers = tex.mcnk_textures[0].layers.as_ref().unwrap();
assert_eq!(layers.layers.len(), 2);
assert_eq!(layers.layers[0].texture_id, 0);
assert_eq!(layers.layers[1].texture_id, 1);
assert!(warnings.is_empty());
}
#[test]
fn test_parse_tex_adt_file_type_detection() {
let data = create_minimal_tex_adt();
let mut cursor = Cursor::new(data);
let discovery = discover_chunks(&mut cursor).unwrap();
assert_eq!(
crate::file_type::AdtFileType::from_discovery(&discovery),
crate::file_type::AdtFileType::Tex0
);
}
#[test]
fn test_parse_tex_adt_empty_mcnk() {
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::MTEX.0);
let texture = b"Tileset\\Terrain.blp\0";
data.extend_from_slice(&(texture.len() as u32).to_le_bytes());
data.extend_from_slice(texture);
data.extend_from_slice(&ChunkId::MCNK.0);
data.extend_from_slice(&0u32.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_tex_adt(&mut cursor, &discovery, version);
assert!(result.is_ok());
let (tex, _) = result.unwrap();
assert_eq!(tex.mcnk_textures.len(), 1);
assert_eq!(tex.mcnk_textures[0].index, 0);
assert!(tex.mcnk_textures[0].layers.is_none());
assert!(tex.mcnk_textures[0].alpha_maps.is_none());
}
}