wow_m2/
version.rs

1use crate::error::Result;
2
3/// M2 format versions across WoW expansions
4#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
5pub enum M2Version {
6    /// Vanilla (1.x)
7    Vanilla,
8
9    /// The Burning Crusade (2.x)
10    TBC,
11
12    /// Wrath of the Lich King (3.x)
13    WotLK,
14
15    /// Cataclysm (4.x)
16    Cataclysm,
17
18    /// Mists of Pandaria (5.x)
19    MoP,
20
21    /// Warlords of Draenor (6.x)
22    WoD,
23
24    /// Legion (7.x)
25    Legion,
26
27    /// Battle for Azeroth (8.x)
28    BfA,
29
30    /// Shadowlands (9.x)
31    Shadowlands,
32
33    /// Dragonflight (10.x)
34    Dragonflight,
35
36    /// The War Within (11.x+)
37    TheWarWithin,
38}
39
40impl M2Version {
41    /// Parse version from a string (e.g., "1.12.1", "3.3.5a", "4.3.4")
42    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    /// Parse version from expansion short names or numeric versions
78    /// Supports both numeric versions (e.g., "3.3.5a") and short names (e.g., "WotLK", "TBC")
79    pub fn from_expansion_name(s: &str) -> Result<Self> {
80        // First try parsing as a short name
81        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                // If it's not a short name, try parsing as a numeric version
95                Self::from_string(s)
96            }
97        }
98    }
99
100    /// Convert header version number to M2Version enum
101    /// Based on empirical analysis of WoW versions 1.12.1 through 5.4.8
102    /// Updated to handle Legion+ chunked format versions (272+)
103    pub fn from_header_version(version: u32) -> Option<Self> {
104        match version {
105            // Empirically verified exact versions from format analysis
106            256 => Some(Self::Vanilla),   // Vanilla 1.12.1
107            260 => Some(Self::TBC),       // The Burning Crusade 2.4.3
108            264 => Some(Self::WotLK),     // Wrath of the Lich King 3.3.5a
109            272 => Some(Self::Cataclysm), // Cataclysm 4.3.4 and MoP 5.4.8
110
111            // Legacy support for version ranges (avoiding overlaps)
112            257..=259 => Some(Self::Vanilla),
113            261..=263 => Some(Self::TBC),
114            265..=271 => Some(Self::WotLK),
115
116            // Legion+ versions (272+ with chunked format support)
117            273..=279 => Some(Self::Legion), // Legion 7.x versions
118            280..=289 => Some(Self::BfA),    // Battle for Azeroth 8.x versions
119            290..=299 => Some(Self::Shadowlands), // Shadowlands 9.x versions
120            300..=309 => Some(Self::Dragonflight), // Dragonflight 10.x versions
121            310..=399 => Some(Self::TheWarWithin), // The War Within 11.x versions
122
123            // MoP uses same version as Cataclysm based on empirical findings
124            // Alternative version numbering for post-MoP (legacy compatibility)
125            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    /// Convert M2Version enum to header version number
138    /// Returns empirically verified version numbers for WoW 1.12.1 through 5.4.8
139    /// Updated to handle Legion+ chunked format versions
140    pub fn to_header_version(&self) -> u32 {
141        match self {
142            // Empirically verified versions from format analysis
143            Self::Vanilla => 256,   // Vanilla 1.12.1
144            Self::TBC => 260,       // The Burning Crusade 2.4.3
145            Self::WotLK => 264,     // Wrath of the Lich King 3.3.5a
146            Self::Cataclysm => 272, // Cataclysm 4.3.4
147            Self::MoP => 272,       // MoP 5.4.8 (same as Cataclysm)
148
149            // Post-MoP versions with chunked format support
150            Self::WoD => 275,          // Theoretical WoD chunked version
151            Self::Legion => 276,       // Legion chunked format base version
152            Self::BfA => 280,          // Battle for Azeroth chunked version
153            Self::Shadowlands => 290,  // Shadowlands chunked version
154            Self::Dragonflight => 300, // Dragonflight chunked version
155            Self::TheWarWithin => 310, // The War Within chunked version
156        }
157    }
158
159    /// Get the WoW expansion name for this version
160    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    /// Get common version string representation (e.g., "3.3.5a" for WotLK)
177    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    /// Check if a direct conversion path exists between two versions
194    pub fn has_direct_conversion_path(&self, target: &Self) -> bool {
195        // Adjacent versions typically have direct conversion paths
196        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    /// Returns true if this version supports chunked format capability
203    /// Based on empirical analysis: chunked format capability introduced in v264 (WotLK)
204    /// but not actually used until Legion (versions 272+)
205    pub fn supports_chunked_format(&self) -> bool {
206        match self {
207            Self::Vanilla | Self::TBC => false,
208            Self::WotLK | Self::Cataclysm | Self::MoP => true, // Capability exists but unused
209            Self::WoD
210            | Self::Legion
211            | Self::BfA
212            | Self::Shadowlands
213            | Self::Dragonflight
214            | Self::TheWarWithin => true,
215        }
216    }
217
218    /// Returns true if this version uses external chunks
219    /// Based on empirical analysis: no external chunks found through MoP 5.4.8
220    /// External chunks introduced with Legion+ (versions 272+)
221    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    /// Returns true if this version uses inline data structure
235    /// Based on empirical analysis: 100% inline data through MoP 5.4.8
236    /// Legion+ versions use chunked data with FileDataID references
237    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    /// Returns true if this version uses the new skin format (with version field)
251    ///
252    /// WotLK introduced external .skin files but used the old format (no version field).
253    /// Cataclysm introduced the new skin format with a version field.
254    ///
255    /// - Old format: magic + arrays (no version field) - used by WotLK and earlier
256    /// - New format: magic + version + name + vertex_count + arrays - used by Cataclysm+
257    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    /// Get the empirically verified version number for this M2 version
272    /// Returns None if the version was not part of the empirical analysis
273    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, // Post-MoP versions not empirically verified but have theoretical versions
281        }
282    }
283
284    /// Returns true if this version requires chunked format parsing (MD21)
285    /// Legion+ versions (272+) use chunked format exclusively
286    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    /// Detect expansion from version number including Legion+ support
294    /// Updated to handle versions 272+ as Legion+
295    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, // Could also be MoP
301            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, // Default fallback
307        }
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        // Test numeric fallback
365        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        // Empirically verified versions
374        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        // Legacy support ranges
386        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        // Legion+ versions (chunked format)
394        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        // Legacy alternative versions
411        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        // Unknown versions
429        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); // Use a version clearly outside any range
432    }
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        // Test chunked format support
446        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        // Test external chunks usage (introduced in Legion+)
453        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        // Test inline data usage (100% through WoD, chunked after)
463        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        // Post-MoP versions not empirically verified
482        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        // Test that empirically verified versions roundtrip correctly
489        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        // Test Legion+ versions
496        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        // Verify roundtrip for empirically verified versions
501        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        // Test Legion+ roundtrip
519        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        // Test chunked format requirement detection
528        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}