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    pub fn from_header_version(version: u32) -> Option<Self> {
102        match version {
103            // Classic through WotLK use versions 256-264
104            256..=264 => Some(Self::Classic), // Covers all pre-Cata versions
105
106            // Cataclysm uses 272 (actual files), but internally referenced as 4
107            272 | 4 => Some(Self::Cataclysm),
108
109            // MoP and later use sequential version numbers
110            8 => Some(Self::MoP),
111            10 => Some(Self::WoD),
112            11 => Some(Self::Legion),
113            16 => Some(Self::BfA),
114            17 => Some(Self::Shadowlands),
115            18 => Some(Self::Dragonflight),
116            19..=u32::MAX => Some(Self::TheWarWithin),
117
118            _ => None,
119        }
120    }
121
122    /// Convert M2Version enum to header version number
123    pub fn to_header_version(&self) -> u32 {
124        match self {
125            // For compatibility, we use the newer simplified version numbers
126            Self::Classic | Self::TBC | Self::WotLK => 264, // Use the highest pre-Cata version
127            Self::Cataclysm => 272,                         // Use the actual file version, not 4
128            Self::MoP => 8,
129            Self::WoD => 10,
130            Self::Legion => 11,
131            Self::BfA => 16,
132            Self::Shadowlands => 17,
133            Self::Dragonflight => 18,
134            Self::TheWarWithin => 19,
135        }
136    }
137
138    /// Get the WoW expansion name for this version
139    pub fn expansion_name(&self) -> &'static str {
140        match self {
141            Self::Classic => "Classic",
142            Self::TBC => "The Burning Crusade",
143            Self::WotLK => "Wrath of the Lich King",
144            Self::Cataclysm => "Cataclysm",
145            Self::MoP => "Mists of Pandaria",
146            Self::WoD => "Warlords of Draenor",
147            Self::Legion => "Legion",
148            Self::BfA => "Battle for Azeroth",
149            Self::Shadowlands => "Shadowlands",
150            Self::Dragonflight => "Dragonflight",
151            Self::TheWarWithin => "The War Within",
152        }
153    }
154
155    /// Get common version string representation (e.g., "3.3.5a" for WotLK)
156    pub fn to_version_string(&self) -> &'static str {
157        match self {
158            Self::Classic => "1.12.1",
159            Self::TBC => "2.4.3",
160            Self::WotLK => "3.3.5a",
161            Self::Cataclysm => "4.3.4",
162            Self::MoP => "5.4.8",
163            Self::WoD => "6.2.4",
164            Self::Legion => "7.3.5",
165            Self::BfA => "8.3.7",
166            Self::Shadowlands => "9.2.7",
167            Self::Dragonflight => "10.2.0",
168            Self::TheWarWithin => "11.0.0",
169        }
170    }
171
172    /// Check if a direct conversion path exists between two versions
173    pub fn has_direct_conversion_path(&self, target: &Self) -> bool {
174        // Adjacent versions typically have direct conversion paths
175        let self_ord = *self as usize;
176        let target_ord = *target as usize;
177
178        (self_ord as isize - target_ord as isize).abs() == 1
179    }
180}
181
182impl std::fmt::Display for M2Version {
183    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
184        write!(
185            f,
186            "{} ({})",
187            self.expansion_name(),
188            self.to_version_string()
189        )
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    #[test]
198    fn test_version_from_string() {
199        assert_eq!(
200            M2Version::from_string("1.12.1").unwrap(),
201            M2Version::Classic
202        );
203        assert_eq!(M2Version::from_string("2.4.3").unwrap(), M2Version::TBC);
204        assert_eq!(M2Version::from_string("3.3.5a").unwrap(), M2Version::WotLK);
205        assert_eq!(
206            M2Version::from_string("4.3.4").unwrap(),
207            M2Version::Cataclysm
208        );
209        assert_eq!(M2Version::from_string("5.4.8").unwrap(), M2Version::MoP);
210    }
211
212    #[test]
213    fn test_version_from_expansion_name() {
214        assert_eq!(
215            M2Version::from_expansion_name("classic").unwrap(),
216            M2Version::Classic
217        );
218        assert_eq!(
219            M2Version::from_expansion_name("TBC").unwrap(),
220            M2Version::TBC
221        );
222        assert_eq!(
223            M2Version::from_expansion_name("wotlk").unwrap(),
224            M2Version::WotLK
225        );
226        assert_eq!(
227            M2Version::from_expansion_name("cata").unwrap(),
228            M2Version::Cataclysm
229        );
230        assert_eq!(
231            M2Version::from_expansion_name("MoP").unwrap(),
232            M2Version::MoP
233        );
234
235        // Test numeric fallback
236        assert_eq!(
237            M2Version::from_expansion_name("3.3.5a").unwrap(),
238            M2Version::WotLK
239        );
240    }
241
242    #[test]
243    fn test_header_version_conversion() {
244        // Classic versions
245        assert_eq!(
246            M2Version::from_header_version(256),
247            Some(M2Version::Classic)
248        );
249        assert_eq!(
250            M2Version::from_header_version(264),
251            Some(M2Version::Classic)
252        );
253
254        // Cataclysm
255        assert_eq!(
256            M2Version::from_header_version(272),
257            Some(M2Version::Cataclysm)
258        );
259        assert_eq!(
260            M2Version::from_header_version(4),
261            Some(M2Version::Cataclysm)
262        );
263
264        // Later versions
265        assert_eq!(M2Version::from_header_version(8), Some(M2Version::MoP));
266        assert_eq!(M2Version::from_header_version(10), Some(M2Version::WoD));
267        assert_eq!(M2Version::from_header_version(11), Some(M2Version::Legion));
268        assert_eq!(M2Version::from_header_version(16), Some(M2Version::BfA));
269        assert_eq!(
270            M2Version::from_header_version(17),
271            Some(M2Version::Shadowlands)
272        );
273        assert_eq!(
274            M2Version::from_header_version(18),
275            Some(M2Version::Dragonflight)
276        );
277        assert_eq!(
278            M2Version::from_header_version(19),
279            Some(M2Version::TheWarWithin)
280        );
281
282        // Unknown versions
283        assert_eq!(M2Version::from_header_version(1), None);
284        assert_eq!(M2Version::from_header_version(5), None);
285    }
286
287    #[test]
288    fn test_conversion_paths() {
289        assert!(M2Version::Classic.has_direct_conversion_path(&M2Version::TBC));
290        assert!(M2Version::TBC.has_direct_conversion_path(&M2Version::WotLK));
291        assert!(M2Version::WotLK.has_direct_conversion_path(&M2Version::Cataclysm));
292        assert!(!M2Version::Classic.has_direct_conversion_path(&M2Version::MoP));
293        assert!(!M2Version::Classic.has_direct_conversion_path(&M2Version::TheWarWithin));
294    }
295}