pub mod chunks;
pub mod conversion;
pub mod error;
pub mod version;
use crate::chunks::{Chunk, MaidChunk, MainChunk, ModfChunk, MphdChunk, MverChunk, MwmoChunk};
use crate::error::{Error, Result};
use crate::version::{VersionConfig, WowVersion};
use std::io::{Read, Seek, SeekFrom, Write};
#[derive(Debug, Clone, PartialEq)]
pub struct WdtFile {
pub mver: MverChunk,
pub mphd: MphdChunk,
pub main: MainChunk,
pub maid: Option<MaidChunk>,
pub mwmo: Option<MwmoChunk>,
pub modf: Option<ModfChunk>,
pub version_config: VersionConfig,
}
impl WdtFile {
pub fn new(version: WowVersion) -> Self {
Self {
mver: MverChunk::new(),
mphd: MphdChunk::new(),
main: MainChunk::new(),
maid: None,
mwmo: None,
modf: None,
version_config: VersionConfig::new(version),
}
}
pub fn is_wmo_only(&self) -> bool {
self.mphd.is_wmo_only()
}
pub fn count_existing_tiles(&self) -> usize {
if let Some(ref maid) = self.maid {
maid.count_existing_tiles()
} else {
self.main.count_existing_tiles()
}
}
pub fn get_tile(&self, x: usize, y: usize) -> Option<TileInfo> {
let main_entry = self.main.get(x, y)?;
let has_adt = if let Some(ref maid) = self.maid {
maid.has_tile(x, y)
} else {
main_entry.has_adt()
};
Some(TileInfo {
x,
y,
has_adt,
area_id: main_entry.area_id,
flags: main_entry.flags,
})
}
pub fn version(&self) -> WowVersion {
self.version_config.version
}
pub fn validate(&self) -> Vec<String> {
let mut warnings = Vec::new();
if self.mver.version != chunks::WDT_VERSION {
warnings.push(format!(
"Invalid WDT version: expected {}, found {}",
chunks::WDT_VERSION,
self.mver.version
));
}
warnings.extend(
self.version_config
.validate_mphd_flags(self.mphd.flags.bits()),
);
if self.is_wmo_only() {
if self.mwmo.is_none() {
warnings.push("WMO-only map missing MWMO chunk".to_string());
}
if self.modf.is_none() {
warnings.push("WMO-only map missing MODF chunk".to_string());
}
} else {
if self.modf.is_some() {
warnings.push("Terrain map should not have MODF chunk".to_string());
}
let should_have_mwmo = self.version_config.should_have_chunk("MWMO", false);
let has_mwmo = self.mwmo.is_some();
if should_have_mwmo && !has_mwmo {
warnings
.push("Terrain map missing expected MWMO chunk for this version".to_string());
} else if !should_have_mwmo && has_mwmo {
warnings.push("Terrain map has unexpected MWMO chunk for this version".to_string());
}
}
if self.mphd.has_maid() && self.maid.is_none() {
warnings
.push("MPHD indicates MAID chunk should be present but it's missing".to_string());
} else if !self.mphd.has_maid() && self.maid.is_some() {
warnings.push("MAID chunk present but not indicated in MPHD flags".to_string());
}
warnings
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TileInfo {
pub x: usize,
pub y: usize,
pub has_adt: bool,
pub area_id: u32,
pub flags: u32,
}
pub struct WdtReader<R: Read + Seek> {
reader: R,
version: WowVersion,
}
impl<R: Read + Seek> WdtReader<R> {
pub fn new(reader: R, version: WowVersion) -> Self {
Self { reader, version }
}
pub fn read(&mut self) -> Result<WdtFile> {
let mut wdt = WdtFile::new(self.version);
let mut has_mver = false;
let mut has_mphd = false;
let mut has_main = false;
loop {
match self.read_chunk_header() {
Ok((magic, size)) => {
match &magic {
b"REVM" => {
wdt.mver = MverChunk::read(&mut self.reader, size)?;
has_mver = true;
}
b"DHPM" => {
wdt.mphd = MphdChunk::read(&mut self.reader, size)?;
has_mphd = true;
}
b"NIAM" => {
wdt.main = MainChunk::read(&mut self.reader, size)?;
has_main = true;
}
b"DIAM" => {
wdt.maid = Some(MaidChunk::read(&mut self.reader, size)?);
}
b"OMWM" => {
wdt.mwmo = Some(MwmoChunk::read(&mut self.reader, size)?);
}
b"FDOM" => {
wdt.modf = Some(ModfChunk::read(&mut self.reader, size)?);
}
_ => {
self.reader.seek(SeekFrom::Current(size as i64))?;
}
}
}
Err(e) => {
if let Error::Io(ref io_err) = e
&& io_err.kind() == std::io::ErrorKind::UnexpectedEof
{
break;
}
return Err(e);
}
}
}
if !has_mver {
return Err(Error::MissingChunk("MVER".to_string()));
}
if !has_mphd {
return Err(Error::MissingChunk("MPHD".to_string()));
}
if !has_main {
return Err(Error::MissingChunk("MAIN".to_string()));
}
let detected_version = self.detect_version(&wdt);
wdt.version_config = VersionConfig::new(detected_version);
Ok(wdt)
}
fn detect_version(&self, wdt: &WdtFile) -> WowVersion {
if wdt.maid.is_some() {
return WowVersion::BfA;
}
let has_mwmo = wdt.mwmo.is_some();
let is_wmo_only = wdt.is_wmo_only();
if !is_wmo_only && !has_mwmo {
return WowVersion::Cataclysm;
} else if !is_wmo_only && has_mwmo {
let flags = wdt.mphd.flags.bits();
if (flags & 0x0002) != 0 || (flags & 0x0004) != 0 || (flags & 0x0008) != 0 {
return WowVersion::WotLK;
} else if (flags & 0x0001) != 0 || flags > 1 {
return WowVersion::TBC;
} else {
return WowVersion::Classic;
}
}
if is_wmo_only && wdt.modf.is_some() {
let flags = wdt.mphd.flags.bits();
if flags > 0x000F {
return WowVersion::WotLK;
} else if flags > 1 {
return WowVersion::TBC;
} else {
return WowVersion::Classic;
}
}
self.version
}
fn read_chunk_header(&mut self) -> Result<([u8; 4], usize)> {
let mut magic = [0u8; 4];
self.reader.read_exact(&mut magic)?;
let mut buf = [0u8; 4];
self.reader.read_exact(&mut buf)?;
let size = u32::from_le_bytes(buf) as usize;
Ok((magic, size))
}
}
pub struct WdtWriter<W: Write> {
writer: W,
}
impl<W: Write> WdtWriter<W> {
pub fn new(writer: W) -> Self {
Self { writer }
}
pub fn write(&mut self, wdt: &WdtFile) -> Result<()> {
wdt.mver.write_chunk(&mut self.writer)?;
wdt.mphd.write_chunk(&mut self.writer)?;
wdt.main.write_chunk(&mut self.writer)?;
if let Some(ref maid) = wdt.maid {
maid.write_chunk(&mut self.writer)?;
}
if let Some(ref mwmo) = wdt.mwmo {
let should_write = wdt
.version_config
.should_have_chunk("MWMO", wdt.is_wmo_only());
if should_write {
mwmo.write_chunk(&mut self.writer)?;
}
}
if let Some(ref modf) = wdt.modf {
modf.write_chunk(&mut self.writer)?;
}
Ok(())
}
}
pub fn tile_to_world(tile_x: u32, tile_y: u32) -> (f32, f32) {
const MAP_SIZE: f32 = 533.333_3;
const MAP_OFFSET: f32 = 32.0 * MAP_SIZE;
let world_x = MAP_OFFSET - (tile_y as f32 * MAP_SIZE);
let world_y = MAP_OFFSET - (tile_x as f32 * MAP_SIZE);
(world_x, world_y)
}
pub fn world_to_tile(world_x: f32, world_y: f32) -> (u32, u32) {
const MAP_SIZE: f32 = 533.333_3;
const MAP_OFFSET: f32 = 32.0 * MAP_SIZE;
let tile_x = ((MAP_OFFSET - world_y) / MAP_SIZE) as u32;
let tile_y = ((MAP_OFFSET - world_x) / MAP_SIZE) as u32;
(tile_x.min(63), tile_y.min(63))
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
#[test]
fn test_empty_wdt() {
let wdt = WdtFile::new(WowVersion::Classic);
assert_eq!(wdt.count_existing_tiles(), 0);
assert!(!wdt.is_wmo_only());
}
#[test]
fn test_wdt_read_write() {
let mut wdt = WdtFile::new(WowVersion::BfA);
wdt.mphd.flags |= chunks::MphdFlags::ADT_HAS_HEIGHT_TEXTURING;
wdt.main.get_mut(10, 20).unwrap().set_has_adt(true);
wdt.main.get_mut(10, 20).unwrap().area_id = 1234;
let mut buffer = Vec::new();
let mut writer = WdtWriter::new(&mut buffer);
writer.write(&wdt).unwrap();
let mut reader = WdtReader::new(Cursor::new(buffer), WowVersion::BfA);
let read_wdt = reader.read().unwrap();
assert_eq!(read_wdt.mphd.flags, wdt.mphd.flags);
assert_eq!(read_wdt.main.get(10, 20).unwrap().area_id, 1234);
assert!(read_wdt.main.get(10, 20).unwrap().has_adt());
}
#[test]
fn test_coordinate_conversion() {
let (wx, wy) = tile_to_world(32, 32);
assert!((wx - 0.0).abs() < 0.1);
assert!((wy - 0.0).abs() < 0.1);
let (tx, ty) = world_to_tile(wx, wy);
assert_eq!(tx, 32);
assert_eq!(ty, 32);
}
}