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