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    /// Classic/Vanilla (1.x)
7    Classic,
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::Classic,
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::Classic),
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    pub fn from_header_version(version: u32) -> Option<Self> {
103        match version {
104            // Empirically verified exact versions from format analysis
105            256 => Some(Self::Classic),   // Vanilla 1.12.1
106            260 => Some(Self::TBC),       // The Burning Crusade 2.4.3
107            264 => Some(Self::WotLK),     // Wrath of the Lich King 3.3.5a
108            272 => Some(Self::Cataclysm), // Cataclysm 4.3.4 and MoP 5.4.8
109
110            // Legacy support for version ranges (avoiding overlaps)
111            257..=259 => Some(Self::Classic),
112            261..=263 => Some(Self::TBC),
113            265..=271 => Some(Self::WotLK),
114
115            // MoP uses same version as Cataclysm based on empirical findings
116            // Post-MoP versions (theoretical, not empirically verified)
117            8 => Some(Self::MoP),
118            10 => Some(Self::WoD),
119            11 => Some(Self::Legion),
120            16 => Some(Self::BfA),
121            17 => Some(Self::Shadowlands),
122            18 => Some(Self::Dragonflight),
123            19 => Some(Self::TheWarWithin),
124
125            _ => None,
126        }
127    }
128
129    /// Convert M2Version enum to header version number
130    /// Returns empirically verified version numbers for WoW 1.12.1 through 5.4.8
131    pub fn to_header_version(&self) -> u32 {
132        match self {
133            // Empirically verified versions from format analysis
134            Self::Classic => 256,   // Vanilla 1.12.1
135            Self::TBC => 260,       // The Burning Crusade 2.4.3
136            Self::WotLK => 264,     // Wrath of the Lich King 3.3.5a
137            Self::Cataclysm => 272, // Cataclysm 4.3.4
138            Self::MoP => 272,       // MoP 5.4.8 (same as Cataclysm)
139
140            // Post-MoP versions (theoretical, not empirically verified)
141            Self::WoD => 10,
142            Self::Legion => 11,
143            Self::BfA => 16,
144            Self::Shadowlands => 17,
145            Self::Dragonflight => 18,
146            Self::TheWarWithin => 19,
147        }
148    }
149
150    /// Get the WoW expansion name for this version
151    pub fn expansion_name(&self) -> &'static str {
152        match self {
153            Self::Classic => "Classic",
154            Self::TBC => "The Burning Crusade",
155            Self::WotLK => "Wrath of the Lich King",
156            Self::Cataclysm => "Cataclysm",
157            Self::MoP => "Mists of Pandaria",
158            Self::WoD => "Warlords of Draenor",
159            Self::Legion => "Legion",
160            Self::BfA => "Battle for Azeroth",
161            Self::Shadowlands => "Shadowlands",
162            Self::Dragonflight => "Dragonflight",
163            Self::TheWarWithin => "The War Within",
164        }
165    }
166
167    /// Get common version string representation (e.g., "3.3.5a" for WotLK)
168    pub fn to_version_string(&self) -> &'static str {
169        match self {
170            Self::Classic => "1.12.1",
171            Self::TBC => "2.4.3",
172            Self::WotLK => "3.3.5a",
173            Self::Cataclysm => "4.3.4",
174            Self::MoP => "5.4.8",
175            Self::WoD => "6.2.4",
176            Self::Legion => "7.3.5",
177            Self::BfA => "8.3.7",
178            Self::Shadowlands => "9.2.7",
179            Self::Dragonflight => "10.2.0",
180            Self::TheWarWithin => "11.0.0",
181        }
182    }
183
184    /// Check if a direct conversion path exists between two versions
185    pub fn has_direct_conversion_path(&self, target: &Self) -> bool {
186        // Adjacent versions typically have direct conversion paths
187        let self_ord = *self as usize;
188        let target_ord = *target as usize;
189
190        (self_ord as isize - target_ord as isize).abs() == 1
191    }
192
193    /// Returns true if this version supports chunked format capability
194    /// Based on empirical analysis: chunked format capability introduced in v264 (WotLK)
195    /// but not actually used until post-MoP expansions
196    pub fn supports_chunked_format(&self) -> bool {
197        match self {
198            Self::Classic | Self::TBC => false,
199            Self::WotLK | Self::Cataclysm | Self::MoP => true, // Capability exists but unused
200            _ => true, // Post-MoP versions may use chunked format
201        }
202    }
203
204    /// Returns true if this version uses external chunks
205    /// Based on empirical analysis: no external chunks found through MoP 5.4.8
206    /// All data remains inline in the main M2 file
207    pub fn uses_external_chunks(&self) -> bool {
208        match self {
209            Self::Classic | Self::TBC | Self::WotLK | Self::Cataclysm | Self::MoP => false,
210            _ => false, // Even post-MoP versions may not use external chunks
211        }
212    }
213
214    /// Returns true if this version uses inline data structure
215    /// Based on empirical analysis: 100% inline data through MoP 5.4.8
216    pub fn uses_inline_data(&self) -> bool {
217        match self {
218            Self::Classic | Self::TBC | Self::WotLK | Self::Cataclysm | Self::MoP => true,
219            _ => true, // Assume inline data for newer versions too
220        }
221    }
222
223    /// Get the empirically verified version number for this M2 version
224    /// Returns None if the version was not part of the empirical analysis
225    pub fn empirical_version_number(&self) -> Option<u32> {
226        match self {
227            Self::Classic => Some(256),
228            Self::TBC => Some(260),
229            Self::WotLK => Some(264),
230            Self::Cataclysm => Some(272),
231            Self::MoP => Some(272),
232            _ => None, // Post-MoP versions not empirically verified
233        }
234    }
235}
236
237impl std::fmt::Display for M2Version {
238    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
239        write!(
240            f,
241            "{} ({})",
242            self.expansion_name(),
243            self.to_version_string()
244        )
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251
252    #[test]
253    fn test_version_from_string() {
254        assert_eq!(
255            M2Version::from_string("1.12.1").unwrap(),
256            M2Version::Classic
257        );
258        assert_eq!(M2Version::from_string("2.4.3").unwrap(), M2Version::TBC);
259        assert_eq!(M2Version::from_string("3.3.5a").unwrap(), M2Version::WotLK);
260        assert_eq!(
261            M2Version::from_string("4.3.4").unwrap(),
262            M2Version::Cataclysm
263        );
264        assert_eq!(M2Version::from_string("5.4.8").unwrap(), M2Version::MoP);
265    }
266
267    #[test]
268    fn test_version_from_expansion_name() {
269        assert_eq!(
270            M2Version::from_expansion_name("classic").unwrap(),
271            M2Version::Classic
272        );
273        assert_eq!(
274            M2Version::from_expansion_name("TBC").unwrap(),
275            M2Version::TBC
276        );
277        assert_eq!(
278            M2Version::from_expansion_name("wotlk").unwrap(),
279            M2Version::WotLK
280        );
281        assert_eq!(
282            M2Version::from_expansion_name("cata").unwrap(),
283            M2Version::Cataclysm
284        );
285        assert_eq!(
286            M2Version::from_expansion_name("MoP").unwrap(),
287            M2Version::MoP
288        );
289
290        // Test numeric fallback
291        assert_eq!(
292            M2Version::from_expansion_name("3.3.5a").unwrap(),
293            M2Version::WotLK
294        );
295    }
296
297    #[test]
298    fn test_header_version_conversion() {
299        // Empirically verified versions
300        assert_eq!(
301            M2Version::from_header_version(256),
302            Some(M2Version::Classic)
303        );
304        assert_eq!(M2Version::from_header_version(260), Some(M2Version::TBC));
305        assert_eq!(M2Version::from_header_version(264), Some(M2Version::WotLK));
306        assert_eq!(
307            M2Version::from_header_version(272),
308            Some(M2Version::Cataclysm)
309        );
310
311        // Legacy support ranges
312        assert_eq!(
313            M2Version::from_header_version(257),
314            Some(M2Version::Classic)
315        );
316        assert_eq!(M2Version::from_header_version(261), Some(M2Version::TBC));
317        assert_eq!(M2Version::from_header_version(265), Some(M2Version::WotLK));
318
319        // Later versions
320        assert_eq!(M2Version::from_header_version(8), Some(M2Version::MoP));
321        assert_eq!(M2Version::from_header_version(10), Some(M2Version::WoD));
322        assert_eq!(M2Version::from_header_version(11), Some(M2Version::Legion));
323        assert_eq!(M2Version::from_header_version(16), Some(M2Version::BfA));
324        assert_eq!(
325            M2Version::from_header_version(17),
326            Some(M2Version::Shadowlands)
327        );
328        assert_eq!(
329            M2Version::from_header_version(18),
330            Some(M2Version::Dragonflight)
331        );
332        assert_eq!(
333            M2Version::from_header_version(19),
334            Some(M2Version::TheWarWithin)
335        );
336
337        // Unknown versions
338        assert_eq!(M2Version::from_header_version(1), None);
339        assert_eq!(M2Version::from_header_version(5), None);
340        assert_eq!(M2Version::from_header_version(273), None);
341    }
342
343    #[test]
344    fn test_conversion_paths() {
345        assert!(M2Version::Classic.has_direct_conversion_path(&M2Version::TBC));
346        assert!(M2Version::TBC.has_direct_conversion_path(&M2Version::WotLK));
347        assert!(M2Version::WotLK.has_direct_conversion_path(&M2Version::Cataclysm));
348        assert!(!M2Version::Classic.has_direct_conversion_path(&M2Version::MoP));
349        assert!(!M2Version::Classic.has_direct_conversion_path(&M2Version::TheWarWithin));
350    }
351
352    #[test]
353    fn test_empirical_version_features() {
354        // Test chunked format support
355        assert!(!M2Version::Classic.supports_chunked_format());
356        assert!(!M2Version::TBC.supports_chunked_format());
357        assert!(M2Version::WotLK.supports_chunked_format());
358        assert!(M2Version::Cataclysm.supports_chunked_format());
359        assert!(M2Version::MoP.supports_chunked_format());
360
361        // Test external chunks usage (none found through MoP)
362        assert!(!M2Version::Classic.uses_external_chunks());
363        assert!(!M2Version::TBC.uses_external_chunks());
364        assert!(!M2Version::WotLK.uses_external_chunks());
365        assert!(!M2Version::Cataclysm.uses_external_chunks());
366        assert!(!M2Version::MoP.uses_external_chunks());
367
368        // Test inline data usage (100% through MoP)
369        assert!(M2Version::Classic.uses_inline_data());
370        assert!(M2Version::TBC.uses_inline_data());
371        assert!(M2Version::WotLK.uses_inline_data());
372        assert!(M2Version::Cataclysm.uses_inline_data());
373        assert!(M2Version::MoP.uses_inline_data());
374    }
375
376    #[test]
377    fn test_empirical_version_numbers() {
378        assert_eq!(M2Version::Classic.empirical_version_number(), Some(256));
379        assert_eq!(M2Version::TBC.empirical_version_number(), Some(260));
380        assert_eq!(M2Version::WotLK.empirical_version_number(), Some(264));
381        assert_eq!(M2Version::Cataclysm.empirical_version_number(), Some(272));
382        assert_eq!(M2Version::MoP.empirical_version_number(), Some(272));
383
384        // Post-MoP versions not empirically verified
385        assert_eq!(M2Version::WoD.empirical_version_number(), None);
386        assert_eq!(M2Version::Legion.empirical_version_number(), None);
387    }
388
389    #[test]
390    fn test_header_version_roundtrip() {
391        // Test that empirically verified versions roundtrip correctly
392        assert_eq!(M2Version::Classic.to_header_version(), 256);
393        assert_eq!(M2Version::TBC.to_header_version(), 260);
394        assert_eq!(M2Version::WotLK.to_header_version(), 264);
395        assert_eq!(M2Version::Cataclysm.to_header_version(), 272);
396        assert_eq!(M2Version::MoP.to_header_version(), 272);
397
398        // Verify roundtrip for empirically verified versions
399        assert_eq!(
400            M2Version::from_header_version(M2Version::Classic.to_header_version()),
401            Some(M2Version::Classic)
402        );
403        assert_eq!(
404            M2Version::from_header_version(M2Version::TBC.to_header_version()),
405            Some(M2Version::TBC)
406        );
407        assert_eq!(
408            M2Version::from_header_version(M2Version::WotLK.to_header_version()),
409            Some(M2Version::WotLK)
410        );
411        assert_eq!(
412            M2Version::from_header_version(M2Version::Cataclysm.to_header_version()),
413            Some(M2Version::Cataclysm)
414        );
415    }
416}