use memchr::memchr;
use std::collections::HashMap;
use std::io::{self, Cursor, Read, Seek, SeekFrom, Write};
use crate::error::{Result, WdlError};
use crate::types::*;
use crate::version::WdlVersion;
#[derive(Debug, Default)]
pub struct WdlParser {
version: WdlVersion,
}
impl WdlParser {
pub fn new() -> Self {
Self {
version: WdlVersion::Latest,
}
}
pub fn with_version(version: WdlVersion) -> Self {
Self { version }
}
pub fn set_version(&mut self, version: WdlVersion) {
self.version = version;
}
pub fn version(&self) -> WdlVersion {
self.version
}
pub fn parse<R: Read + Seek>(&self, reader: &mut R) -> Result<WdlFile> {
let mut file = WdlFile::new();
file.version = self.version;
let mut mver_found = false;
let mut mwmo_index = None;
let mut mwid_index = None;
let mut modf_index = None;
let mut maof_index = None;
let mut mldd_index = None;
let mut mldx_index = None;
let mut mlmd_index = None;
let mut mlmx_index = None;
let mut chunk_index = 0;
loop {
let chunk = match Chunk::read(reader) {
Ok(chunk) => chunk,
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => break,
Err(e) => return Err(WdlError::Io(e)),
};
match chunk.magic {
MVER_MAGIC => {
mver_found = true;
let mut cursor = Cursor::new(&chunk.data);
let mut buf = [0u8; 4];
cursor.read_exact(&mut buf).map_err(WdlError::Io)?;
file.version_number = u32::from_le_bytes(buf);
}
MWMO_MAGIC => mwmo_index = Some(chunk_index),
MWID_MAGIC => mwid_index = Some(chunk_index),
MODF_MAGIC => modf_index = Some(chunk_index),
MAOF_MAGIC => maof_index = Some(chunk_index),
MLDD_MAGIC => mldd_index = Some(chunk_index),
MLDX_MAGIC => mldx_index = Some(chunk_index),
MLMD_MAGIC => mlmd_index = Some(chunk_index),
MLMX_MAGIC => mlmx_index = Some(chunk_index),
_ => {}
}
file.chunks.push(chunk);
chunk_index += 1;
}
if !mver_found {
return Err(WdlError::InvalidMagic {
expected: String::from_utf8_lossy(&MVER_MAGIC).to_string(),
found: "Not found".to_string(),
});
}
if self.version == WdlVersion::Latest {
if mldd_index.is_some()
|| mldx_index.is_some()
|| mlmd_index.is_some()
|| mlmx_index.is_some()
{
file.version = WdlVersion::Legion;
}
else if mwmo_index.is_some() || mwid_index.is_some() || modf_index.is_some() {
if file.chunks.iter().any(|c| c.magic == MAHO_MAGIC) {
file.version = WdlVersion::Wotlk;
} else {
file.version = WdlVersion::Vanilla;
}
}
}
if let Some(index) = mwmo_index {
let chunk = &file.chunks[index];
file.wmo_filenames = self.parse_zero_terminated_strings(&chunk.data)?;
}
if let Some(index) = mwid_index {
let chunk = &file.chunks[index];
let mut cursor = Cursor::new(&chunk.data);
while cursor.position() < chunk.data.len() as u64 {
let mut buf = [0u8; 4];
match cursor.read_exact(&mut buf) {
Ok(_) => file.wmo_indices.push(u32::from_le_bytes(buf)),
Err(_) => break,
}
}
}
if let Some(index) = modf_index {
let chunk = &file.chunks[index];
let mut cursor = Cursor::new(&chunk.data);
while cursor.position() < chunk.data.len() as u64 {
match ModelPlacement::read(&mut cursor) {
Ok(placement) => file.wmo_placements.push(placement),
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => break,
Err(e) => return Err(WdlError::Io(e)),
}
}
}
if let Some(index) = maof_index {
let chunk = &file.chunks[index];
let mut cursor = Cursor::new(&chunk.data);
for i in 0..64 * 64 {
let mut buf = [0u8; 4];
cursor.read_exact(&mut buf).map_err(WdlError::Io)?;
file.map_tile_offsets[i] = u32::from_le_bytes(buf);
}
self.parse_map_tiles(reader, &mut file)?;
}
if file.version.has_ml_chunks() {
if let Some(index) = mldd_index {
let chunk = &file.chunks[index];
let mut cursor = Cursor::new(&chunk.data);
while cursor.position() < chunk.data.len() as u64 {
match M2Placement::read(&mut cursor) {
Ok(placement) => file.m2_placements.push(placement),
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => break,
Err(e) => return Err(WdlError::Io(e)),
}
}
}
if let Some(index) = mldx_index {
let chunk = &file.chunks[index];
let mut cursor = Cursor::new(&chunk.data);
while cursor.position() < chunk.data.len() as u64 {
match M2VisibilityInfo::read(&mut cursor) {
Ok(info) => file.m2_visibility.push(info),
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => break,
Err(e) => return Err(WdlError::Io(e)),
}
}
}
if let Some(index) = mlmd_index {
let chunk = &file.chunks[index];
let mut cursor = Cursor::new(&chunk.data);
while cursor.position() < chunk.data.len() as u64 {
match M2Placement::read(&mut cursor) {
Ok(placement) => file.wmo_legion_placements.push(placement),
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => break,
Err(e) => return Err(WdlError::Io(e)),
}
}
}
if let Some(index) = mlmx_index {
let chunk = &file.chunks[index];
let mut cursor = Cursor::new(&chunk.data);
while cursor.position() < chunk.data.len() as u64 {
match M2VisibilityInfo::read(&mut cursor) {
Ok(info) => file.wmo_legion_visibility.push(info),
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => break,
Err(e) => return Err(WdlError::Io(e)),
}
}
}
}
file.validate()?;
Ok(file)
}
pub fn write<W: Write + Seek>(&self, writer: &mut W, file: &WdlFile) -> Result<()> {
let mut chunks = Vec::new();
let mut mver_data = Vec::new();
mver_data
.write_all(&file.version.version_number().to_le_bytes())
.map_err(WdlError::Io)?;
chunks.push(Chunk::new(MVER_MAGIC, mver_data));
if file.version.has_wmo_chunks() && !file.wmo_filenames.is_empty() {
let mut mwmo_data = Vec::new();
for name in &file.wmo_filenames {
mwmo_data.extend_from_slice(name.as_bytes());
mwmo_data.push(0); }
chunks.push(Chunk::new(MWMO_MAGIC, mwmo_data));
let mut mwid_data = Vec::new();
for &idx in &file.wmo_indices {
mwid_data
.write_all(&idx.to_le_bytes())
.map_err(WdlError::Io)?;
}
chunks.push(Chunk::new(MWID_MAGIC, mwid_data));
let mut modf_data = Vec::new();
for placement in &file.wmo_placements {
placement.write(&mut modf_data).map_err(WdlError::Io)?;
}
chunks.push(Chunk::new(MODF_MAGIC, modf_data));
}
if file.version.has_ml_chunks() {
if !file.m2_placements.is_empty() {
let mut mldd_data = Vec::new();
for placement in &file.m2_placements {
placement.write(&mut mldd_data).map_err(WdlError::Io)?;
}
chunks.push(Chunk::new(MLDD_MAGIC, mldd_data));
}
if !file.m2_visibility.is_empty() {
let mut mldx_data = Vec::new();
for info in &file.m2_visibility {
info.write(&mut mldx_data).map_err(WdlError::Io)?;
}
chunks.push(Chunk::new(MLDX_MAGIC, mldx_data));
}
if !file.wmo_legion_placements.is_empty() {
let mut mlmd_data = Vec::new();
for placement in &file.wmo_legion_placements {
placement.write(&mut mlmd_data).map_err(WdlError::Io)?;
}
chunks.push(Chunk::new(MLMD_MAGIC, mlmd_data));
}
if !file.wmo_legion_visibility.is_empty() {
let mut mlmx_data = Vec::new();
for info in &file.wmo_legion_visibility {
info.write(&mut mlmx_data).map_err(WdlError::Io)?;
}
chunks.push(Chunk::new(MLMX_MAGIC, mlmx_data));
}
}
let mut map_tile_offsets = [0u32; 64 * 64];
let mut mare_chunks = HashMap::new();
let mut maho_chunks = HashMap::new();
let mut current_offset = 0;
for chunk in &chunks {
current_offset += 8 + chunk.size; }
current_offset += 8 + (64 * 64 * 4);
for y in 0..64 {
for x in 0..64 {
let index = y * 64 + x;
let key = (x as u32, y as u32);
if !file.heightmap_tiles.contains_key(&key) {
map_tile_offsets[index] = 0;
continue;
}
map_tile_offsets[index] = current_offset;
let heightmap = file.heightmap_tiles.get(&key).unwrap();
let mut mare_data = Vec::new();
heightmap.write(&mut mare_data).map_err(WdlError::Io)?;
mare_chunks.insert(key, Chunk::new(MARE_MAGIC, mare_data));
current_offset += 8 + (HeightMapTile::TOTAL_COUNT * 2) as u32;
if file.version.has_maho_chunk()
&& let Some(holes) = file.holes_data.get(&key)
{
let mut maho_data = Vec::new();
holes.write(&mut maho_data).map_err(WdlError::Io)?;
maho_chunks.insert(key, Chunk::new(MAHO_MAGIC, maho_data));
current_offset += 8 + (HolesData::MASK_COUNT * 2) as u32;
}
}
}
let mut maof_data = Vec::new();
for &offset in &map_tile_offsets {
maof_data
.write_all(&offset.to_le_bytes())
.map_err(WdlError::Io)?;
}
chunks.push(Chunk::new(MAOF_MAGIC, maof_data));
for chunk in &chunks {
chunk.write(writer).map_err(WdlError::Io)?;
}
for y in 0..64 {
for x in 0..64 {
let key = (x as u32, y as u32);
if !mare_chunks.contains_key(&key) {
continue;
}
mare_chunks
.get(&key)
.unwrap()
.write(writer)
.map_err(WdlError::Io)?;
if let Some(maho_chunk) = maho_chunks.get(&key) {
maho_chunk.write(writer).map_err(WdlError::Io)?;
}
}
}
Ok(())
}
fn parse_zero_terminated_strings(&self, data: &[u8]) -> Result<Vec<String>> {
let mut strings = Vec::new();
let mut start = 0;
while start < data.len() {
match memchr(0, &data[start..]) {
Some(end) => {
match String::from_utf8(data[start..start + end].to_vec()) {
Ok(s) => strings.push(s),
Err(_) => {
return Err(WdlError::ParseError(
"Invalid UTF-8 in string".to_string(),
));
}
}
start += end + 1; }
None => break,
}
}
Ok(strings)
}
fn parse_map_tiles<R: Read + Seek>(&self, reader: &mut R, file: &mut WdlFile) -> Result<()> {
for y in 0..64 {
for x in 0..64 {
let index = y * 64 + x;
let offset = file.map_tile_offsets[index];
if offset == 0 {
continue; }
reader
.seek(SeekFrom::Start(offset as u64))
.map_err(WdlError::Io)?;
let chunk = Chunk::read(reader).map_err(WdlError::Io)?;
if chunk.magic != MARE_MAGIC {
return Err(WdlError::UnexpectedChunk(
String::from_utf8_lossy(&chunk.magic).to_string(),
));
}
let mut cursor = Cursor::new(&chunk.data);
let heightmap = HeightMapTile::read(&mut cursor).map_err(WdlError::Io)?;
file.heightmap_tiles.insert((x as u32, y as u32), heightmap);
if self.version.has_maho_chunk() {
match Chunk::read(reader) {
Ok(chunk) => {
if chunk.magic == MAHO_MAGIC {
let mut cursor = Cursor::new(&chunk.data);
let holes = HolesData::read(&mut cursor).map_err(WdlError::Io)?;
file.holes_data.insert((x as u32, y as u32), holes);
} else {
reader
.seek(SeekFrom::Current(-(8 + chunk.size as i64)))
.map_err(WdlError::Io)?;
}
}
Err(_) => {
}
}
}
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
#[test]
fn test_parse_zero_terminated_strings() {
let parser = WdlParser::new();
let data = b"test1\0test2\0test3\0";
let strings = parser.parse_zero_terminated_strings(data).unwrap();
assert_eq!(strings.len(), 3);
assert_eq!(strings[0], "test1");
assert_eq!(strings[1], "test2");
assert_eq!(strings[2], "test3");
}
#[test]
fn test_empty_write_read() {
let parser = WdlParser::new();
let file = WdlFile::new();
let mut buffer = Vec::new();
let mut cursor = Cursor::new(&mut buffer);
parser.write(&mut cursor, &file).unwrap();
let mut cursor = Cursor::new(buffer);
let parsed_file = parser.parse(&mut cursor).unwrap();
assert_eq!(parsed_file.version, file.version);
assert_eq!(parsed_file.version_number, file.version_number);
}
}