1pub mod chunks;
40pub mod conversion;
41pub mod error;
42pub mod version;
43
44use crate::chunks::{Chunk, MaidChunk, MainChunk, ModfChunk, MphdChunk, MverChunk, MwmoChunk};
45use crate::error::{Error, Result};
46use crate::version::{VersionConfig, WowVersion};
47use std::io::{Read, Seek, SeekFrom, Write};
48
49#[derive(Debug, Clone, PartialEq)]
51pub struct WdtFile {
52 pub mver: MverChunk,
54
55 pub mphd: MphdChunk,
57
58 pub main: MainChunk,
60
61 pub maid: Option<MaidChunk>,
63
64 pub mwmo: Option<MwmoChunk>,
66
67 pub modf: Option<ModfChunk>,
69
70 pub version_config: VersionConfig,
72}
73
74impl WdtFile {
75 pub fn new(version: WowVersion) -> Self {
77 Self {
78 mver: MverChunk::new(),
79 mphd: MphdChunk::new(),
80 main: MainChunk::new(),
81 maid: None,
82 mwmo: None,
83 modf: None,
84 version_config: VersionConfig::new(version),
85 }
86 }
87
88 pub fn is_wmo_only(&self) -> bool {
90 self.mphd.is_wmo_only()
91 }
92
93 pub fn count_existing_tiles(&self) -> usize {
95 if let Some(ref maid) = self.maid {
96 maid.count_existing_tiles()
97 } else {
98 self.main.count_existing_tiles()
99 }
100 }
101
102 pub fn get_tile(&self, x: usize, y: usize) -> Option<TileInfo> {
104 let main_entry = self.main.get(x, y)?;
105
106 let has_adt = if let Some(ref maid) = self.maid {
107 maid.has_tile(x, y)
108 } else {
109 main_entry.has_adt()
110 };
111
112 Some(TileInfo {
113 x,
114 y,
115 has_adt,
116 area_id: main_entry.area_id,
117 flags: main_entry.flags,
118 })
119 }
120
121 pub fn version(&self) -> WowVersion {
123 self.version_config.version
124 }
125
126 pub fn validate(&self) -> Vec<String> {
128 let mut warnings = Vec::new();
129
130 if self.mver.version != chunks::WDT_VERSION {
132 warnings.push(format!(
133 "Invalid WDT version: expected {}, found {}",
134 chunks::WDT_VERSION,
135 self.mver.version
136 ));
137 }
138
139 warnings.extend(
141 self.version_config
142 .validate_mphd_flags(self.mphd.flags.bits()),
143 );
144
145 if self.is_wmo_only() {
147 if self.mwmo.is_none() {
148 warnings.push("WMO-only map missing MWMO chunk".to_string());
149 }
150 if self.modf.is_none() {
151 warnings.push("WMO-only map missing MODF chunk".to_string());
152 }
153 } else {
154 if self.modf.is_some() {
156 warnings.push("Terrain map should not have MODF chunk".to_string());
157 }
158
159 let should_have_mwmo = self.version_config.should_have_chunk("MWMO", false);
161 let has_mwmo = self.mwmo.is_some();
162
163 if should_have_mwmo && !has_mwmo {
164 warnings
165 .push("Terrain map missing expected MWMO chunk for this version".to_string());
166 } else if !should_have_mwmo && has_mwmo {
167 warnings.push("Terrain map has unexpected MWMO chunk for this version".to_string());
168 }
169 }
170
171 if self.mphd.has_maid() && self.maid.is_none() {
173 warnings
174 .push("MPHD indicates MAID chunk should be present but it's missing".to_string());
175 } else if !self.mphd.has_maid() && self.maid.is_some() {
176 warnings.push("MAID chunk present but not indicated in MPHD flags".to_string());
177 }
178
179 warnings
180 }
181}
182
183#[derive(Debug, Clone, Copy, PartialEq, Eq)]
185pub struct TileInfo {
186 pub x: usize,
187 pub y: usize,
188 pub has_adt: bool,
189 pub area_id: u32,
190 pub flags: u32,
191}
192
193pub struct WdtReader<R: Read + Seek> {
195 reader: R,
196 version: WowVersion,
197}
198
199impl<R: Read + Seek> WdtReader<R> {
200 pub fn new(reader: R, version: WowVersion) -> Self {
202 Self { reader, version }
203 }
204
205 pub fn read(&mut self) -> Result<WdtFile> {
207 let mut wdt = WdtFile::new(self.version);
208
209 let mut has_mver = false;
211 let mut has_mphd = false;
212 let mut has_main = false;
213
214 loop {
216 match self.read_chunk_header() {
217 Ok((magic, size)) => {
218 match &magic {
219 b"REVM" => {
220 wdt.mver = MverChunk::read(&mut self.reader, size)?;
221 has_mver = true;
222 }
223 b"DHPM" => {
224 wdt.mphd = MphdChunk::read(&mut self.reader, size)?;
225 has_mphd = true;
226 }
227 b"NIAM" => {
228 wdt.main = MainChunk::read(&mut self.reader, size)?;
229 has_main = true;
230 }
231 b"DIAM" => {
232 wdt.maid = Some(MaidChunk::read(&mut self.reader, size)?);
233 }
234 b"OMWM" => {
235 wdt.mwmo = Some(MwmoChunk::read(&mut self.reader, size)?);
236 }
237 b"FDOM" => {
238 wdt.modf = Some(ModfChunk::read(&mut self.reader, size)?);
239 }
240 _ => {
241 self.reader.seek(SeekFrom::Current(size as i64))?;
243 }
244 }
245 }
246 Err(e) => {
247 if let Error::Io(ref io_err) = e
249 && io_err.kind() == std::io::ErrorKind::UnexpectedEof
250 {
251 break;
252 }
253 return Err(e);
254 }
255 }
256 }
257
258 if !has_mver {
260 return Err(Error::MissingChunk("MVER".to_string()));
261 }
262 if !has_mphd {
263 return Err(Error::MissingChunk("MPHD".to_string()));
264 }
265 if !has_main {
266 return Err(Error::MissingChunk("MAIN".to_string()));
267 }
268
269 let detected_version = self.detect_version(&wdt);
271
272 wdt.version_config = VersionConfig::new(detected_version);
274
275 Ok(wdt)
276 }
277
278 fn detect_version(&self, wdt: &WdtFile) -> WowVersion {
280 if wdt.maid.is_some() {
287 return WowVersion::BfA;
288 }
289
290 let has_mwmo = wdt.mwmo.is_some();
292 let is_wmo_only = wdt.is_wmo_only();
293
294 if !is_wmo_only && !has_mwmo {
297 return WowVersion::Cataclysm;
300 } else if !is_wmo_only && has_mwmo {
301 let flags = wdt.mphd.flags.bits();
304
305 if (flags & 0x0002) != 0 || (flags & 0x0004) != 0 || (flags & 0x0008) != 0 {
307 return WowVersion::WotLK;
309 } else if (flags & 0x0001) != 0 || flags > 1 {
310 return WowVersion::TBC;
312 } else {
313 return WowVersion::Classic;
315 }
316 }
317
318 if is_wmo_only && wdt.modf.is_some() {
320 let flags = wdt.mphd.flags.bits();
322 if flags > 0x000F {
323 return WowVersion::WotLK;
324 } else if flags > 1 {
325 return WowVersion::TBC;
326 } else {
327 return WowVersion::Classic;
328 }
329 }
330
331 self.version
333 }
334
335 fn read_chunk_header(&mut self) -> Result<([u8; 4], usize)> {
337 let mut magic = [0u8; 4];
338 self.reader.read_exact(&mut magic)?;
339
340 let mut buf = [0u8; 4];
341 self.reader.read_exact(&mut buf)?;
342 let size = u32::from_le_bytes(buf) as usize;
343
344 Ok((magic, size))
345 }
346}
347
348pub struct WdtWriter<W: Write> {
350 writer: W,
351}
352
353impl<W: Write> WdtWriter<W> {
354 pub fn new(writer: W) -> Self {
356 Self { writer }
357 }
358
359 pub fn write(&mut self, wdt: &WdtFile) -> Result<()> {
361 wdt.mver.write_chunk(&mut self.writer)?;
363 wdt.mphd.write_chunk(&mut self.writer)?;
364 wdt.main.write_chunk(&mut self.writer)?;
365
366 if let Some(ref maid) = wdt.maid {
368 maid.write_chunk(&mut self.writer)?;
369 }
370
371 if let Some(ref mwmo) = wdt.mwmo {
373 let should_write = wdt
374 .version_config
375 .should_have_chunk("MWMO", wdt.is_wmo_only());
376 if should_write {
377 mwmo.write_chunk(&mut self.writer)?;
378 }
379 }
380
381 if let Some(ref modf) = wdt.modf {
382 modf.write_chunk(&mut self.writer)?;
383 }
384
385 Ok(())
386 }
387}
388
389pub fn tile_to_world(tile_x: u32, tile_y: u32) -> (f32, f32) {
391 const MAP_SIZE: f32 = 533.333_3;
392 const MAP_OFFSET: f32 = 32.0 * MAP_SIZE;
393
394 let world_x = MAP_OFFSET - (tile_y as f32 * MAP_SIZE);
395 let world_y = MAP_OFFSET - (tile_x as f32 * MAP_SIZE);
396
397 (world_x, world_y)
398}
399
400pub fn world_to_tile(world_x: f32, world_y: f32) -> (u32, u32) {
402 const MAP_SIZE: f32 = 533.333_3;
403 const MAP_OFFSET: f32 = 32.0 * MAP_SIZE;
404
405 let tile_x = ((MAP_OFFSET - world_y) / MAP_SIZE) as u32;
406 let tile_y = ((MAP_OFFSET - world_x) / MAP_SIZE) as u32;
407
408 (tile_x.min(63), tile_y.min(63))
409}
410
411#[cfg(test)]
412mod tests {
413 use super::*;
414 use std::io::Cursor;
415
416 #[test]
417 fn test_empty_wdt() {
418 let wdt = WdtFile::new(WowVersion::Classic);
419 assert_eq!(wdt.count_existing_tiles(), 0);
420 assert!(!wdt.is_wmo_only());
421 }
422
423 #[test]
424 fn test_wdt_read_write() {
425 let mut wdt = WdtFile::new(WowVersion::BfA);
426
427 wdt.mphd.flags |= chunks::MphdFlags::ADT_HAS_HEIGHT_TEXTURING;
429 wdt.main.get_mut(10, 20).unwrap().set_has_adt(true);
430 wdt.main.get_mut(10, 20).unwrap().area_id = 1234;
431
432 let mut buffer = Vec::new();
434 let mut writer = WdtWriter::new(&mut buffer);
435 writer.write(&wdt).unwrap();
436
437 let mut reader = WdtReader::new(Cursor::new(buffer), WowVersion::BfA);
439 let read_wdt = reader.read().unwrap();
440
441 assert_eq!(read_wdt.mphd.flags, wdt.mphd.flags);
443 assert_eq!(read_wdt.main.get(10, 20).unwrap().area_id, 1234);
444 assert!(read_wdt.main.get(10, 20).unwrap().has_adt());
445 }
446
447 #[test]
448 fn test_coordinate_conversion() {
449 let (wx, wy) = tile_to_world(32, 32);
451 assert!((wx - 0.0).abs() < 0.1);
452 assert!((wy - 0.0).abs() < 0.1);
453
454 let (tx, ty) = world_to_tile(wx, wy);
456 assert_eq!(tx, 32);
457 assert_eq!(ty, 32);
458 }
459}