use binrw::{BinRead, BinWrite};
use std::io::{Read, Seek, Write};
#[derive(Debug, Clone, Default)]
pub struct MtexChunk {
pub filenames: Vec<String>,
}
impl BinRead for MtexChunk {
type Args<'a> = ();
fn read_options<R: Read + Seek>(
reader: &mut R,
_endian: binrw::Endian,
_args: Self::Args<'_>,
) -> binrw::BinResult<Self> {
let filenames = parse_null_terminated_strings(reader)?;
Ok(Self { filenames })
}
}
impl BinWrite for MtexChunk {
type Args<'a> = ();
fn write_options<W: Write + Seek>(
&self,
writer: &mut W,
_endian: binrw::Endian,
_args: Self::Args<'_>,
) -> binrw::BinResult<()> {
for filename in &self.filenames {
writer.write_all(filename.as_bytes())?;
writer.write_all(&[0u8])?; }
Ok(())
}
}
#[derive(Debug, Clone, Default)]
pub struct MmdxChunk {
pub filenames: Vec<String>,
}
impl BinRead for MmdxChunk {
type Args<'a> = ();
fn read_options<R: Read + Seek>(
reader: &mut R,
_endian: binrw::Endian,
_args: Self::Args<'_>,
) -> binrw::BinResult<Self> {
let filenames = parse_null_terminated_strings(reader)?;
Ok(Self { filenames })
}
}
impl BinWrite for MmdxChunk {
type Args<'a> = ();
fn write_options<W: Write + Seek>(
&self,
writer: &mut W,
_endian: binrw::Endian,
_args: Self::Args<'_>,
) -> binrw::BinResult<()> {
for filename in &self.filenames {
writer.write_all(filename.as_bytes())?;
writer.write_all(&[0u8])?; }
Ok(())
}
}
#[derive(Debug, Clone, Default)]
pub struct MwmoChunk {
pub filenames: Vec<String>,
}
impl BinRead for MwmoChunk {
type Args<'a> = ();
fn read_options<R: Read + Seek>(
reader: &mut R,
_endian: binrw::Endian,
_args: Self::Args<'_>,
) -> binrw::BinResult<Self> {
let filenames = parse_null_terminated_strings(reader)?;
Ok(Self { filenames })
}
}
impl BinWrite for MwmoChunk {
type Args<'a> = ();
fn write_options<W: Write + Seek>(
&self,
writer: &mut W,
_endian: binrw::Endian,
_args: Self::Args<'_>,
) -> binrw::BinResult<()> {
for filename in &self.filenames {
writer.write_all(filename.as_bytes())?;
writer.write_all(&[0u8])?; }
Ok(())
}
}
#[derive(Debug, Clone, Default, BinRead, BinWrite)]
#[brw(little)]
pub struct MmidChunk {
#[br(parse_with = binrw::helpers::until_eof)]
pub offsets: Vec<u32>,
}
impl MmidChunk {
pub fn validate_offsets(&self, mmdx_size: usize) -> bool {
self.offsets
.iter()
.all(|&offset| (offset as usize) < mmdx_size)
}
pub fn get_filename_index(&self, mmdx: &MmdxChunk, offset: u32) -> Option<usize> {
let mut current_offset = 0;
for (idx, filename) in mmdx.filenames.iter().enumerate() {
if current_offset == offset {
return Some(idx);
}
current_offset += filename.len() as u32 + 1; }
None
}
}
#[derive(Debug, Clone, Default, BinRead, BinWrite)]
#[brw(little)]
pub struct MwidChunk {
#[br(parse_with = binrw::helpers::until_eof)]
pub offsets: Vec<u32>,
}
impl MwidChunk {
pub fn validate_offsets(&self, mwmo_size: usize) -> bool {
self.offsets
.iter()
.all(|&offset| (offset as usize) < mwmo_size)
}
pub fn get_filename_index(&self, mwmo: &MwmoChunk, offset: u32) -> Option<usize> {
let mut current_offset = 0;
for (idx, filename) in mwmo.filenames.iter().enumerate() {
if current_offset == offset {
return Some(idx);
}
current_offset += filename.len() as u32 + 1; }
None
}
}
fn parse_null_terminated_strings<R: Read>(reader: &mut R) -> binrw::BinResult<Vec<String>> {
let mut strings = Vec::new();
loop {
let mut string_bytes = Vec::new();
loop {
let mut byte = [0u8; 1];
match reader.read_exact(&mut byte) {
Ok(()) => {
if byte[0] == 0 {
break;
}
string_bytes.push(byte[0]);
}
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
if !string_bytes.is_empty() {
let string = String::from_utf8_lossy(&string_bytes).into_owned();
strings.push(string);
}
return Ok(strings);
}
Err(e) => {
return Err(binrw::Error::Io(e));
}
}
}
if !string_bytes.is_empty() {
let string = String::from_utf8_lossy(&string_bytes).into_owned();
strings.push(string);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
#[test]
fn test_mtex_chunk_parse() {
let data = b"test.blp\0world.blp\0";
let mut cursor = Cursor::new(data.to_vec());
let mtex = MtexChunk::read_le(&mut cursor).unwrap();
assert_eq!(mtex.filenames.len(), 2);
assert_eq!(mtex.filenames[0], "test.blp");
assert_eq!(mtex.filenames[1], "world.blp");
}
#[test]
fn test_mtex_chunk_single_file() {
let data = b"terrain.blp\0";
let mut cursor = Cursor::new(data.to_vec());
let mtex = MtexChunk::read_le(&mut cursor).unwrap();
assert_eq!(mtex.filenames.len(), 1);
assert_eq!(mtex.filenames[0], "terrain.blp");
}
#[test]
fn test_mtex_chunk_empty() {
let data = b"";
let mut cursor = Cursor::new(data.to_vec());
let mtex = MtexChunk::read_le(&mut cursor).unwrap();
assert_eq!(mtex.filenames.len(), 0);
}
#[test]
fn test_mtex_chunk_utf8_lossy() {
let data = b"test\xFF.blp\0"; let mut cursor = Cursor::new(data.to_vec());
let mtex = MtexChunk::read_le(&mut cursor).unwrap();
assert_eq!(mtex.filenames.len(), 1);
assert!(mtex.filenames[0].contains("test"));
assert!(mtex.filenames[0].contains(".blp"));
}
#[test]
fn test_mtex_chunk_backslash_path() {
let data = b"Tileset\\Generic\\CityTile.blp\0";
let mut cursor = Cursor::new(data.to_vec());
let mtex = MtexChunk::read_le(&mut cursor).unwrap();
assert_eq!(mtex.filenames.len(), 1);
assert_eq!(mtex.filenames[0], "Tileset\\Generic\\CityTile.blp");
}
#[test]
fn test_mtex_chunk_multiple_paths() {
let data = b"Tileset\\Ground.blp\0Tileset\\Detail.blp\0Tileset\\Alpha.blp\0";
let mut cursor = Cursor::new(data.to_vec());
let mtex = MtexChunk::read_le(&mut cursor).unwrap();
assert_eq!(mtex.filenames.len(), 3);
assert_eq!(mtex.filenames[0], "Tileset\\Ground.blp");
assert_eq!(mtex.filenames[1], "Tileset\\Detail.blp");
assert_eq!(mtex.filenames[2], "Tileset\\Alpha.blp");
}
#[test]
fn test_mtex_chunk_round_trip() {
let original = MtexChunk {
filenames: vec![
"texture1.blp".to_string(),
"texture2.blp".to_string(),
"texture3.blp".to_string(),
],
};
let mut buffer = Cursor::new(Vec::new());
original.write_le(&mut buffer).unwrap();
let data = buffer.into_inner();
let mut cursor = Cursor::new(data);
let parsed = MtexChunk::read_le(&mut cursor).unwrap();
assert_eq!(original.filenames.len(), parsed.filenames.len());
assert_eq!(original.filenames, parsed.filenames);
}
#[test]
fn test_mtex_chunk_round_trip_with_paths() {
let original = MtexChunk {
filenames: vec![
"Tileset\\Ground\\Grass.blp".to_string(),
"Tileset\\Detail\\Rock.blp".to_string(),
],
};
let mut buffer = Cursor::new(Vec::new());
original.write_le(&mut buffer).unwrap();
let data = buffer.into_inner();
let mut cursor = Cursor::new(data);
let parsed = MtexChunk::read_le(&mut cursor).unwrap();
assert_eq!(original.filenames, parsed.filenames);
}
#[test]
fn test_mtex_chunk_default() {
let mtex = MtexChunk::default();
assert_eq!(mtex.filenames.len(), 0);
}
#[test]
fn test_mtex_chunk_no_trailing_null() {
let data = b"test.blp\0world.blp";
let mut cursor = Cursor::new(data.to_vec());
let mtex = MtexChunk::read_le(&mut cursor).unwrap();
assert_eq!(mtex.filenames.len(), 2);
assert_eq!(mtex.filenames[0], "test.blp");
assert_eq!(mtex.filenames[1], "world.blp");
}
#[test]
fn test_mwmo_chunk() {
let data = b"building.wmo\0castle.wmo\0";
let mut cursor = Cursor::new(data.to_vec());
let mwmo = MwmoChunk::read_le(&mut cursor).unwrap();
assert_eq!(mwmo.filenames.len(), 2);
assert_eq!(mwmo.filenames[0], "building.wmo");
assert_eq!(mwmo.filenames[1], "castle.wmo");
}
#[test]
fn test_mwmo_chunk_single_file() {
let data = b"dungeon.wmo\0";
let mut cursor = Cursor::new(data.to_vec());
let mwmo = MwmoChunk::read_le(&mut cursor).unwrap();
assert_eq!(mwmo.filenames.len(), 1);
assert_eq!(mwmo.filenames[0], "dungeon.wmo");
}
#[test]
fn test_mwmo_chunk_empty() {
let data = b"";
let mut cursor = Cursor::new(data.to_vec());
let mwmo = MwmoChunk::read_le(&mut cursor).unwrap();
assert_eq!(mwmo.filenames.len(), 0);
}
#[test]
fn test_mwmo_chunk_with_path() {
let data = b"World\\wmo\\Dungeon\\Dungeon.wmo\0";
let mut cursor = Cursor::new(data.to_vec());
let mwmo = MwmoChunk::read_le(&mut cursor).unwrap();
assert_eq!(mwmo.filenames.len(), 1);
assert_eq!(mwmo.filenames[0], "World\\wmo\\Dungeon\\Dungeon.wmo");
}
#[test]
fn test_mwmo_chunk_round_trip() {
let original = MwmoChunk {
filenames: vec!["building.wmo".to_string(), "castle.wmo".to_string()],
};
let mut buffer = Cursor::new(Vec::new());
original.write_le(&mut buffer).unwrap();
let data = buffer.into_inner();
let mut cursor = Cursor::new(data);
let parsed = MwmoChunk::read_le(&mut cursor).unwrap();
assert_eq!(original.filenames, parsed.filenames);
}
#[test]
fn test_mmdx_chunk() {
let data = b"model1.m2\0model2.m2\0";
let mut cursor = Cursor::new(data.to_vec());
let mmdx = MmdxChunk::read_le(&mut cursor).unwrap();
assert_eq!(mmdx.filenames.len(), 2);
assert_eq!(mmdx.filenames[0], "model1.m2");
assert_eq!(mmdx.filenames[1], "model2.m2");
}
#[test]
fn test_mmdx_chunk_with_path() {
let data = b"World\\Doodad\\Tree.m2\0";
let mut cursor = Cursor::new(data.to_vec());
let mmdx = MmdxChunk::read_le(&mut cursor).unwrap();
assert_eq!(mmdx.filenames.len(), 1);
assert_eq!(mmdx.filenames[0], "World\\Doodad\\Tree.m2");
}
#[test]
fn test_mmdx_chunk_round_trip() {
let original = MmdxChunk {
filenames: vec!["model1.m2".to_string(), "model2.m2".to_string()],
};
let mut buffer = Cursor::new(Vec::new());
original.write_le(&mut buffer).unwrap();
let data = buffer.into_inner();
let mut cursor = Cursor::new(data);
let parsed = MmdxChunk::read_le(&mut cursor).unwrap();
assert_eq!(original.filenames, parsed.filenames);
}
#[test]
fn test_mmid_chunk() {
let data = vec![
0x00, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, ];
let mut cursor = Cursor::new(data);
let mmid = MmidChunk::read_le(&mut cursor).unwrap();
assert_eq!(mmid.offsets.len(), 3);
assert_eq!(mmid.offsets[0], 0);
assert_eq!(mmid.offsets[1], 10);
assert_eq!(mmid.offsets[2], 20);
}
#[test]
fn test_mmid_validate_offsets() {
let mmid = MmidChunk {
offsets: vec![0, 10, 20],
};
assert!(mmid.validate_offsets(30));
assert!(!mmid.validate_offsets(20));
}
#[test]
fn test_mmid_get_filename_index() {
let mmdx = MmdxChunk {
filenames: vec![
"model1.m2".to_string(), "model2.m2".to_string(), "model3.m2".to_string(), ],
};
let mmid = MmidChunk {
offsets: vec![0, 10, 20],
};
assert_eq!(mmid.get_filename_index(&mmdx, 0), Some(0));
assert_eq!(mmid.get_filename_index(&mmdx, 10), Some(1));
assert_eq!(mmid.get_filename_index(&mmdx, 20), Some(2));
assert_eq!(mmid.get_filename_index(&mmdx, 999), None);
}
#[test]
fn test_mwid_chunk() {
let data = vec![
0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, ];
let mut cursor = Cursor::new(data);
let mwid = MwidChunk::read_le(&mut cursor).unwrap();
assert_eq!(mwid.offsets.len(), 2);
assert_eq!(mwid.offsets[0], 0);
assert_eq!(mwid.offsets[1], 15);
}
#[test]
fn test_mwid_validate_offsets() {
let mwid = MwidChunk {
offsets: vec![0, 15, 30],
};
assert!(mwid.validate_offsets(40));
assert!(!mwid.validate_offsets(30));
}
#[test]
fn test_mwid_get_filename_index() {
let mwmo = MwmoChunk {
filenames: vec![
"building.wmo".to_string(), "castle.wmo".to_string(), ],
};
let mwid = MwidChunk {
offsets: vec![0, 13],
};
assert_eq!(mwid.get_filename_index(&mwmo, 0), Some(0));
assert_eq!(mwid.get_filename_index(&mwmo, 13), Some(1));
assert_eq!(mwid.get_filename_index(&mwmo, 999), None);
}
}