1use crate::error::Result;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
5pub enum M2Version {
6 Vanilla,
8
9 TBC,
11
12 WotLK,
14
15 Cataclysm,
17
18 MoP,
20
21 WoD,
23
24 Legion,
26
27 BfA,
29
30 Shadowlands,
32
33 Dragonflight,
35
36 TheWarWithin,
38}
39
40impl M2Version {
41 pub fn from_string(s: &str) -> Result<Self> {
43 let parts: Vec<&str> = s.split('.').collect();
44 if parts.is_empty() {
45 return Err(crate::error::M2Error::UnsupportedVersion(format!(
46 "Invalid version string: {s}"
47 )));
48 }
49
50 let major = parts[0].parse::<u32>().map_err(|_| {
51 crate::error::M2Error::UnsupportedVersion(format!(
52 "Invalid major version: {}",
53 parts[0]
54 ))
55 })?;
56
57 Ok(match major {
58 1 => M2Version::Vanilla,
59 2 => M2Version::TBC,
60 3 => M2Version::WotLK,
61 4 => M2Version::Cataclysm,
62 5 => M2Version::MoP,
63 6 => M2Version::WoD,
64 7 => M2Version::Legion,
65 8 => M2Version::BfA,
66 9 => M2Version::Shadowlands,
67 10 => M2Version::Dragonflight,
68 11 => M2Version::TheWarWithin,
69 _ => {
70 return Err(crate::error::M2Error::UnsupportedVersion(format!(
71 "Unknown WoW version: {major}"
72 )));
73 }
74 })
75 }
76
77 pub fn from_expansion_name(s: &str) -> Result<Self> {
80 match s.to_lowercase().as_str() {
82 "vanilla" | "classic" => Ok(M2Version::Vanilla),
83 "tbc" | "bc" | "burningcrusade" | "burning_crusade" => Ok(M2Version::TBC),
84 "wotlk" | "wrath" | "lichking" | "lich_king" | "wlk" => Ok(M2Version::WotLK),
85 "cata" | "cataclysm" => Ok(M2Version::Cataclysm),
86 "mop" | "pandaria" | "mists" | "mists_of_pandaria" => Ok(M2Version::MoP),
87 "wod" | "draenor" | "warlords" | "warlords_of_draenor" => Ok(M2Version::WoD),
88 "legion" => Ok(M2Version::Legion),
89 "bfa" | "bfazeroth" | "battle_for_azeroth" | "battleforazeroth" => Ok(M2Version::BfA),
90 "sl" | "shadowlands" => Ok(M2Version::Shadowlands),
91 "df" | "dragonflight" => Ok(M2Version::Dragonflight),
92 "tww" | "warwithin" | "the_war_within" | "thewarwithin" => Ok(M2Version::TheWarWithin),
93 _ => {
94 Self::from_string(s)
96 }
97 }
98 }
99
100 pub fn from_header_version(version: u32) -> Option<Self> {
104 match version {
105 256 => Some(Self::Vanilla), 260 => Some(Self::TBC), 264 => Some(Self::WotLK), 272 => Some(Self::Cataclysm), 257..=259 => Some(Self::Vanilla),
113 261..=263 => Some(Self::TBC),
114 265..=271 => Some(Self::WotLK),
115
116 273..=279 => Some(Self::Legion), 280..=289 => Some(Self::BfA), 290..=299 => Some(Self::Shadowlands), 300..=309 => Some(Self::Dragonflight), 310..=399 => Some(Self::TheWarWithin), 8 => Some(Self::MoP),
126 10 => Some(Self::WoD),
127 11 => Some(Self::Legion),
128 16 => Some(Self::BfA),
129 17 => Some(Self::Shadowlands),
130 18 => Some(Self::Dragonflight),
131 19 => Some(Self::TheWarWithin),
132
133 _ => None,
134 }
135 }
136
137 pub fn to_header_version(&self) -> u32 {
141 match self {
142 Self::Vanilla => 256, Self::TBC => 260, Self::WotLK => 264, Self::Cataclysm => 272, Self::MoP => 272, Self::WoD => 275, Self::Legion => 276, Self::BfA => 280, Self::Shadowlands => 290, Self::Dragonflight => 300, Self::TheWarWithin => 310, }
157 }
158
159 pub fn expansion_name(&self) -> &'static str {
161 match self {
162 Self::Vanilla => "Vanilla",
163 Self::TBC => "The Burning Crusade",
164 Self::WotLK => "Wrath of the Lich King",
165 Self::Cataclysm => "Cataclysm",
166 Self::MoP => "Mists of Pandaria",
167 Self::WoD => "Warlords of Draenor",
168 Self::Legion => "Legion",
169 Self::BfA => "Battle for Azeroth",
170 Self::Shadowlands => "Shadowlands",
171 Self::Dragonflight => "Dragonflight",
172 Self::TheWarWithin => "The War Within",
173 }
174 }
175
176 pub fn to_version_string(&self) -> &'static str {
178 match self {
179 Self::Vanilla => "1.12.1",
180 Self::TBC => "2.4.3",
181 Self::WotLK => "3.3.5a",
182 Self::Cataclysm => "4.3.4",
183 Self::MoP => "5.4.8",
184 Self::WoD => "6.2.4",
185 Self::Legion => "7.3.5",
186 Self::BfA => "8.3.7",
187 Self::Shadowlands => "9.2.7",
188 Self::Dragonflight => "10.2.0",
189 Self::TheWarWithin => "11.0.0",
190 }
191 }
192
193 pub fn has_direct_conversion_path(&self, target: &Self) -> bool {
195 let self_ord = *self as usize;
197 let target_ord = *target as usize;
198
199 (self_ord as isize - target_ord as isize).abs() == 1
200 }
201
202 pub fn supports_chunked_format(&self) -> bool {
206 match self {
207 Self::Vanilla | Self::TBC => false,
208 Self::WotLK | Self::Cataclysm | Self::MoP => true, Self::WoD
210 | Self::Legion
211 | Self::BfA
212 | Self::Shadowlands
213 | Self::Dragonflight
214 | Self::TheWarWithin => true,
215 }
216 }
217
218 pub fn uses_external_chunks(&self) -> bool {
222 match self {
223 Self::Vanilla | Self::TBC | Self::WotLK | Self::Cataclysm | Self::MoP | Self::WoD => {
224 false
225 }
226 Self::Legion
227 | Self::BfA
228 | Self::Shadowlands
229 | Self::Dragonflight
230 | Self::TheWarWithin => true,
231 }
232 }
233
234 pub fn uses_inline_data(&self) -> bool {
238 match self {
239 Self::Vanilla | Self::TBC | Self::WotLK | Self::Cataclysm | Self::MoP | Self::WoD => {
240 true
241 }
242 Self::Legion
243 | Self::BfA
244 | Self::Shadowlands
245 | Self::Dragonflight
246 | Self::TheWarWithin => false,
247 }
248 }
249
250 pub fn uses_new_skin_format(&self) -> bool {
258 match self {
259 Self::Vanilla | Self::TBC | Self::WotLK => false,
260 Self::Cataclysm
261 | Self::MoP
262 | Self::WoD
263 | Self::Legion
264 | Self::BfA
265 | Self::Shadowlands
266 | Self::Dragonflight
267 | Self::TheWarWithin => true,
268 }
269 }
270
271 pub fn empirical_version_number(&self) -> Option<u32> {
274 match self {
275 Self::Vanilla => Some(256),
276 Self::TBC => Some(260),
277 Self::WotLK => Some(264),
278 Self::Cataclysm => Some(272),
279 Self::MoP => Some(272),
280 _ => None, }
282 }
283
284 pub fn requires_chunked_format(&self) -> bool {
287 matches!(
288 self,
289 Self::Legion | Self::BfA | Self::Shadowlands | Self::Dragonflight | Self::TheWarWithin
290 )
291 }
292
293 pub fn detect_expansion(version: u32) -> M2Version {
296 match version {
297 256..=259 => M2Version::Vanilla,
298 260..=263 => M2Version::TBC,
299 264..=271 => M2Version::WotLK,
300 272 => M2Version::Cataclysm, 273..=279 => M2Version::Legion,
302 280..=289 => M2Version::BfA,
303 290..=299 => M2Version::Shadowlands,
304 300..=309 => M2Version::Dragonflight,
305 310.. => M2Version::TheWarWithin,
306 _ => M2Version::Vanilla, }
308 }
309}
310
311impl std::fmt::Display for M2Version {
312 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
313 write!(
314 f,
315 "{} ({})",
316 self.expansion_name(),
317 self.to_version_string()
318 )
319 }
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325
326 #[test]
327 fn test_version_from_string() {
328 assert_eq!(
329 M2Version::from_string("1.12.1").unwrap(),
330 M2Version::Vanilla
331 );
332 assert_eq!(M2Version::from_string("2.4.3").unwrap(), M2Version::TBC);
333 assert_eq!(M2Version::from_string("3.3.5a").unwrap(), M2Version::WotLK);
334 assert_eq!(
335 M2Version::from_string("4.3.4").unwrap(),
336 M2Version::Cataclysm
337 );
338 assert_eq!(M2Version::from_string("5.4.8").unwrap(), M2Version::MoP);
339 }
340
341 #[test]
342 fn test_version_from_expansion_name() {
343 assert_eq!(
344 M2Version::from_expansion_name("classic").unwrap(),
345 M2Version::Vanilla
346 );
347 assert_eq!(
348 M2Version::from_expansion_name("TBC").unwrap(),
349 M2Version::TBC
350 );
351 assert_eq!(
352 M2Version::from_expansion_name("wotlk").unwrap(),
353 M2Version::WotLK
354 );
355 assert_eq!(
356 M2Version::from_expansion_name("cata").unwrap(),
357 M2Version::Cataclysm
358 );
359 assert_eq!(
360 M2Version::from_expansion_name("MoP").unwrap(),
361 M2Version::MoP
362 );
363
364 assert_eq!(
366 M2Version::from_expansion_name("3.3.5a").unwrap(),
367 M2Version::WotLK
368 );
369 }
370
371 #[test]
372 fn test_header_version_conversion() {
373 assert_eq!(
375 M2Version::from_header_version(256),
376 Some(M2Version::Vanilla)
377 );
378 assert_eq!(M2Version::from_header_version(260), Some(M2Version::TBC));
379 assert_eq!(M2Version::from_header_version(264), Some(M2Version::WotLK));
380 assert_eq!(
381 M2Version::from_header_version(272),
382 Some(M2Version::Cataclysm)
383 );
384
385 assert_eq!(
387 M2Version::from_header_version(257),
388 Some(M2Version::Vanilla)
389 );
390 assert_eq!(M2Version::from_header_version(261), Some(M2Version::TBC));
391 assert_eq!(M2Version::from_header_version(265), Some(M2Version::WotLK));
392
393 assert_eq!(M2Version::from_header_version(273), Some(M2Version::Legion));
395 assert_eq!(M2Version::from_header_version(276), Some(M2Version::Legion));
396 assert_eq!(M2Version::from_header_version(280), Some(M2Version::BfA));
397 assert_eq!(
398 M2Version::from_header_version(290),
399 Some(M2Version::Shadowlands)
400 );
401 assert_eq!(
402 M2Version::from_header_version(300),
403 Some(M2Version::Dragonflight)
404 );
405 assert_eq!(
406 M2Version::from_header_version(310),
407 Some(M2Version::TheWarWithin)
408 );
409
410 assert_eq!(M2Version::from_header_version(8), Some(M2Version::MoP));
412 assert_eq!(M2Version::from_header_version(10), Some(M2Version::WoD));
413 assert_eq!(M2Version::from_header_version(11), Some(M2Version::Legion));
414 assert_eq!(M2Version::from_header_version(16), Some(M2Version::BfA));
415 assert_eq!(
416 M2Version::from_header_version(17),
417 Some(M2Version::Shadowlands)
418 );
419 assert_eq!(
420 M2Version::from_header_version(18),
421 Some(M2Version::Dragonflight)
422 );
423 assert_eq!(
424 M2Version::from_header_version(19),
425 Some(M2Version::TheWarWithin)
426 );
427
428 assert_eq!(M2Version::from_header_version(1), None);
430 assert_eq!(M2Version::from_header_version(5), None);
431 assert_eq!(M2Version::from_header_version(999), None); }
433
434 #[test]
435 fn test_conversion_paths() {
436 assert!(M2Version::Vanilla.has_direct_conversion_path(&M2Version::TBC));
437 assert!(M2Version::TBC.has_direct_conversion_path(&M2Version::WotLK));
438 assert!(M2Version::WotLK.has_direct_conversion_path(&M2Version::Cataclysm));
439 assert!(!M2Version::Vanilla.has_direct_conversion_path(&M2Version::MoP));
440 assert!(!M2Version::Vanilla.has_direct_conversion_path(&M2Version::TheWarWithin));
441 }
442
443 #[test]
444 fn test_empirical_version_features() {
445 assert!(!M2Version::Vanilla.supports_chunked_format());
447 assert!(!M2Version::TBC.supports_chunked_format());
448 assert!(M2Version::WotLK.supports_chunked_format());
449 assert!(M2Version::Cataclysm.supports_chunked_format());
450 assert!(M2Version::MoP.supports_chunked_format());
451
452 assert!(!M2Version::Vanilla.uses_external_chunks());
454 assert!(!M2Version::TBC.uses_external_chunks());
455 assert!(!M2Version::WotLK.uses_external_chunks());
456 assert!(!M2Version::Cataclysm.uses_external_chunks());
457 assert!(!M2Version::MoP.uses_external_chunks());
458 assert!(!M2Version::WoD.uses_external_chunks());
459 assert!(M2Version::Legion.uses_external_chunks());
460 assert!(M2Version::BfA.uses_external_chunks());
461
462 assert!(M2Version::Vanilla.uses_inline_data());
464 assert!(M2Version::TBC.uses_inline_data());
465 assert!(M2Version::WotLK.uses_inline_data());
466 assert!(M2Version::Cataclysm.uses_inline_data());
467 assert!(M2Version::MoP.uses_inline_data());
468 assert!(M2Version::WoD.uses_inline_data());
469 assert!(!M2Version::Legion.uses_inline_data());
470 assert!(!M2Version::BfA.uses_inline_data());
471 }
472
473 #[test]
474 fn test_empirical_version_numbers() {
475 assert_eq!(M2Version::Vanilla.empirical_version_number(), Some(256));
476 assert_eq!(M2Version::TBC.empirical_version_number(), Some(260));
477 assert_eq!(M2Version::WotLK.empirical_version_number(), Some(264));
478 assert_eq!(M2Version::Cataclysm.empirical_version_number(), Some(272));
479 assert_eq!(M2Version::MoP.empirical_version_number(), Some(272));
480
481 assert_eq!(M2Version::WoD.empirical_version_number(), None);
483 assert_eq!(M2Version::Legion.empirical_version_number(), None);
484 }
485
486 #[test]
487 fn test_header_version_roundtrip() {
488 assert_eq!(M2Version::Vanilla.to_header_version(), 256);
490 assert_eq!(M2Version::TBC.to_header_version(), 260);
491 assert_eq!(M2Version::WotLK.to_header_version(), 264);
492 assert_eq!(M2Version::Cataclysm.to_header_version(), 272);
493 assert_eq!(M2Version::MoP.to_header_version(), 272);
494
495 assert_eq!(M2Version::Legion.to_header_version(), 276);
497 assert_eq!(M2Version::BfA.to_header_version(), 280);
498 assert_eq!(M2Version::Shadowlands.to_header_version(), 290);
499
500 assert_eq!(
502 M2Version::from_header_version(M2Version::Vanilla.to_header_version()),
503 Some(M2Version::Vanilla)
504 );
505 assert_eq!(
506 M2Version::from_header_version(M2Version::TBC.to_header_version()),
507 Some(M2Version::TBC)
508 );
509 assert_eq!(
510 M2Version::from_header_version(M2Version::WotLK.to_header_version()),
511 Some(M2Version::WotLK)
512 );
513 assert_eq!(
514 M2Version::from_header_version(M2Version::Cataclysm.to_header_version()),
515 Some(M2Version::Cataclysm)
516 );
517
518 assert_eq!(
520 M2Version::from_header_version(M2Version::Legion.to_header_version()),
521 Some(M2Version::Legion)
522 );
523 }
524
525 #[test]
526 fn test_chunked_format_detection() {
527 assert!(!M2Version::Vanilla.requires_chunked_format());
529 assert!(!M2Version::TBC.requires_chunked_format());
530 assert!(!M2Version::WotLK.requires_chunked_format());
531 assert!(!M2Version::Cataclysm.requires_chunked_format());
532 assert!(!M2Version::MoP.requires_chunked_format());
533 assert!(!M2Version::WoD.requires_chunked_format());
534 assert!(M2Version::Legion.requires_chunked_format());
535 assert!(M2Version::BfA.requires_chunked_format());
536 }
537
538 #[test]
539 fn test_expansion_detection() {
540 assert_eq!(M2Version::detect_expansion(256), M2Version::Vanilla);
541 assert_eq!(M2Version::detect_expansion(260), M2Version::TBC);
542 assert_eq!(M2Version::detect_expansion(264), M2Version::WotLK);
543 assert_eq!(M2Version::detect_expansion(272), M2Version::Cataclysm);
544 assert_eq!(M2Version::detect_expansion(273), M2Version::Legion);
545 assert_eq!(M2Version::detect_expansion(276), M2Version::Legion);
546 assert_eq!(M2Version::detect_expansion(280), M2Version::BfA);
547 assert_eq!(M2Version::detect_expansion(310), M2Version::TheWarWithin);
548 }
549}