genie_scx/
types.rs

1//! Contains pure types, no IO.
2//!
3//! Most of these are more descriptive wrappers around integers.
4use std::cmp::Ordering;
5use std::convert::TryFrom;
6use std::fmt::{self, Debug, Display};
7
8/// The SCX Format version string. In practice, this does not really reflect the game version.
9#[derive(Clone, Copy, PartialEq, Eq)]
10pub struct SCXVersion(pub(crate) [u8; 4]);
11
12impl SCXVersion {
13    /// Get the raw bytes representing this scx format version.
14    pub fn as_bytes(&self) -> &[u8] {
15        &self.0
16    }
17
18    pub(crate) fn to_player_version(self) -> Option<f32> {
19        match self.as_bytes() {
20            b"1.07" => Some(1.07),
21            b"1.09" | b"1.10" | b"1.11" => Some(1.11),
22            b"1.12" | b"1.13" | b"1.14" | b"1.15" | b"1.16" => Some(1.12),
23            b"1.18" | b"1.19" => Some(1.13),
24            b"1.20" | b"1.21" | b"1.32" | b"1.36" | b"1.37" => Some(1.14),
25            _ => None,
26        }
27    }
28}
29
30impl Default for SCXVersion {
31    fn default() -> Self {
32        Self(*b"1.21")
33    }
34}
35
36impl Debug for SCXVersion {
37    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38        write!(f, "{:?}", std::str::from_utf8(&self.0).unwrap())
39    }
40}
41
42impl Display for SCXVersion {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        write!(f, "{}", std::str::from_utf8(&self.0).unwrap())
45    }
46}
47
48impl PartialEq<[u8; 4]> for SCXVersion {
49    fn eq(&self, other: &[u8; 4]) -> bool {
50        other[0] == self.0[0] && other[1] == b'.' && other[2] == self.0[2] && other[3] == self.0[3]
51    }
52}
53
54impl PartialEq<SCXVersion> for [u8; 4] {
55    fn eq(&self, other: &SCXVersion) -> bool {
56        other == self
57    }
58}
59
60impl Ord for SCXVersion {
61    fn cmp(&self, other: &SCXVersion) -> Ordering {
62        match self.0[0].cmp(&other.0[0]) {
63            Ordering::Equal => {}
64            ord => return ord,
65        }
66        match self.0[2].cmp(&other.0[2]) {
67            Ordering::Equal => {}
68            ord => return ord,
69        }
70        self.0[3].cmp(&other.0[3])
71    }
72}
73
74impl PartialOrd for SCXVersion {
75    fn partial_cmp(&self, other: &SCXVersion) -> Option<Ordering> {
76        Some(self.cmp(other))
77    }
78}
79
80/// Could not parse a diplomatic stance because given number is an unknown stance ID.
81#[derive(Debug, Clone, Copy, thiserror::Error)]
82#[error("invalid diplomatic stance {} (must be 0/1/3)", .0)]
83pub struct ParseDiplomaticStanceError(i32);
84
85/// A player's diplomatic stance toward another player.
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub enum DiplomaticStance {
88    /// The other player is an ally.
89    Ally = 0,
90    /// This player is neutral toward the other player.
91    Neutral = 1,
92    /// The other player is an enemy.
93    Enemy = 3,
94}
95
96impl TryFrom<i32> for DiplomaticStance {
97    type Error = ParseDiplomaticStanceError;
98
99    fn try_from(n: i32) -> Result<Self, Self::Error> {
100        match n {
101            0 => Ok(DiplomaticStance::Ally),
102            1 => Ok(DiplomaticStance::Neutral),
103            3 => Ok(DiplomaticStance::Enemy),
104            n => Err(ParseDiplomaticStanceError(n)),
105        }
106    }
107}
108
109impl From<DiplomaticStance> for i32 {
110    fn from(stance: DiplomaticStance) -> i32 {
111        match stance {
112            DiplomaticStance::Ally => 0,
113            DiplomaticStance::Neutral => 1,
114            DiplomaticStance::Enemy => 3,
115        }
116    }
117}
118
119/// Could not parse a data set because given number is an unknown data set ID.
120#[derive(Debug, Clone, Copy, thiserror::Error)]
121#[error("invalid data set {} (must be 0/1)", .0)]
122pub struct ParseDataSetError(i32);
123
124/// The data set used by a scenario, HD Edition only.
125#[derive(Debug, Clone, Copy, PartialEq, Eq)]
126pub enum DataSet {
127    /// The "base" data set, containing Age of Kings and the Age of Conquerors expansion.
128    BaseGame,
129    /// The "expansions" data set, containing the HD Edition expansions.
130    Expansions,
131}
132
133impl TryFrom<i32> for DataSet {
134    type Error = ParseDataSetError;
135    fn try_from(n: i32) -> Result<Self, Self::Error> {
136        match n {
137            0 => Ok(DataSet::BaseGame),
138            1 => Ok(DataSet::Expansions),
139            n => Err(ParseDataSetError(n)),
140        }
141    }
142}
143
144impl From<DataSet> for i32 {
145    fn from(id: DataSet) -> i32 {
146        match id {
147            DataSet::BaseGame => 0,
148            DataSet::Expansions => 1,
149        }
150    }
151}
152
153/// Could not parse a DLC package identifier because given number is an unknown DLC ID.
154#[derive(Debug, Clone, Copy, thiserror::Error)]
155#[error("unknown dlc package {}", .0)]
156pub struct ParseDLCPackageError(i32);
157
158/// An HD Edition DLC identifier.
159#[derive(Debug, Clone, Copy, PartialEq, Eq)]
160pub enum DLCPackage {
161    /// The Age of Kings base game.
162    AgeOfKings,
163    /// The Age of Conquerors expansion.
164    AgeOfConquerors,
165    /// The Forgotten expansion.
166    TheForgotten,
167    /// The African Kingdoms expansion.
168    AfricanKingdoms,
169    /// The Rise of the Rajas expansion.
170    RiseOfTheRajas,
171    /// The Last Khans expansion.
172    LastKhans,
173}
174
175impl TryFrom<i32> for DLCPackage {
176    type Error = ParseDLCPackageError;
177    fn try_from(n: i32) -> Result<Self, Self::Error> {
178        match n {
179            2 => Ok(DLCPackage::AgeOfKings),
180            3 => Ok(DLCPackage::AgeOfConquerors),
181            4 => Ok(DLCPackage::TheForgotten),
182            5 => Ok(DLCPackage::AfricanKingdoms),
183            6 => Ok(DLCPackage::RiseOfTheRajas),
184            7 => Ok(DLCPackage::LastKhans),
185            n => Err(ParseDLCPackageError(n)),
186        }
187    }
188}
189
190impl From<DLCPackage> for i32 {
191    fn from(dlc_id: DLCPackage) -> i32 {
192        match dlc_id {
193            DLCPackage::AgeOfKings => 2,
194            DLCPackage::AgeOfConquerors => 3,
195            DLCPackage::TheForgotten => 4,
196            DLCPackage::AfricanKingdoms => 5,
197            DLCPackage::RiseOfTheRajas => 6,
198            DLCPackage::LastKhans => 7,
199        }
200    }
201}
202
203fn expected_range(version: f32) -> &'static str {
204    if version < 1.25 {
205        "-1-4"
206    } else {
207        "-1-6"
208    }
209}
210
211/// Could not parse a starting age because given number refers to an unknown age.
212#[derive(Debug, Clone, Copy, thiserror::Error)]
213#[error("invalid starting age {} (must be {})", .found, expected_range(*.version))]
214pub struct ParseStartingAgeError {
215    version: f32,
216    found: i32,
217}
218
219/// The starting age.
220#[derive(Debug, PartialEq, Eq, Clone, Copy)]
221pub enum StartingAge {
222    /// Use the game default.
223    Default = -1,
224    /// Start in the Dark Age with Nomad resources.
225    Nomad = -2,
226    /// Start in the Dark Age.
227    DarkAge = 0,
228    /// Start in the Feudal Age.
229    FeudalAge = 1,
230    /// Start in the Castle Age.
231    CastleAge = 2,
232    /// Start in the Imperial Age.
233    ImperialAge = 3,
234    /// Start in the Imperial Age with all technologies researched.
235    PostImperialAge = 4,
236}
237
238impl StartingAge {
239    /// Convert a starting age number to the appropriate enum for a particular
240    /// data version.
241    pub fn try_from(n: i32, version: f32) -> Result<Self, ParseStartingAgeError> {
242        if version < 1.25 {
243            match n {
244                -1 => Ok(StartingAge::Default),
245                0 => Ok(StartingAge::DarkAge),
246                1 => Ok(StartingAge::FeudalAge),
247                2 => Ok(StartingAge::CastleAge),
248                3 => Ok(StartingAge::ImperialAge),
249                4 => Ok(StartingAge::PostImperialAge),
250                _ => Err(ParseStartingAgeError { version, found: n }),
251            }
252        } else {
253            match n {
254                -1 | 0 => Ok(StartingAge::Default),
255                1 => Ok(StartingAge::Nomad),
256                2 => Ok(StartingAge::DarkAge),
257                3 => Ok(StartingAge::FeudalAge),
258                4 => Ok(StartingAge::CastleAge),
259                5 => Ok(StartingAge::ImperialAge),
260                6 => Ok(StartingAge::PostImperialAge),
261                _ => Err(ParseStartingAgeError { version, found: n }),
262            }
263        }
264    }
265
266    /// Serialize the age identifier to an integer that is understood by the given game version.
267    pub fn to_i32(self, version: f32) -> i32 {
268        if version < 1.25 {
269            match self {
270                StartingAge::Default => -1,
271                StartingAge::Nomad | StartingAge::DarkAge => 0,
272                StartingAge::FeudalAge => 1,
273                StartingAge::CastleAge => 2,
274                StartingAge::ImperialAge => 3,
275                StartingAge::PostImperialAge => 4,
276            }
277        } else {
278            match self {
279                StartingAge::Default => 0,
280                StartingAge::Nomad => 1,
281                StartingAge::DarkAge => 2,
282                StartingAge::FeudalAge => 3,
283                StartingAge::CastleAge => 4,
284                StartingAge::ImperialAge => 5,
285                StartingAge::PostImperialAge => 6,
286            }
287        }
288    }
289}
290
291#[derive(Debug, Clone, Copy, PartialEq, Eq)]
292pub enum VictoryCondition {
293    Capture,
294    Create,
295    Destroy,
296    DestroyMultiple,
297    BringToArea,
298    BringToObject,
299    Attribute,
300    Explore,
301    CreateInArea,
302    DestroyAll,
303    DestroyPlayer,
304    Points,
305    Other(u8),
306}
307
308impl From<u8> for VictoryCondition {
309    fn from(n: u8) -> Self {
310        match n {
311            0 => VictoryCondition::Capture,
312            1 => VictoryCondition::Create,
313            2 => VictoryCondition::Destroy,
314            3 => VictoryCondition::DestroyMultiple,
315            4 => VictoryCondition::BringToArea,
316            5 => VictoryCondition::BringToObject,
317            6 => VictoryCondition::Attribute,
318            7 => VictoryCondition::Explore,
319            8 => VictoryCondition::CreateInArea,
320            9 => VictoryCondition::DestroyAll,
321            10 => VictoryCondition::DestroyPlayer,
322            11 => VictoryCondition::Points,
323            n => VictoryCondition::Other(n),
324        }
325    }
326}
327
328impl From<VictoryCondition> for u8 {
329    fn from(condition: VictoryCondition) -> Self {
330        match condition {
331            VictoryCondition::Capture => 0,
332            VictoryCondition::Create => 1,
333            VictoryCondition::Destroy => 2,
334            VictoryCondition::DestroyMultiple => 3,
335            VictoryCondition::BringToArea => 4,
336            VictoryCondition::BringToObject => 5,
337            VictoryCondition::Attribute => 6,
338            VictoryCondition::Explore => 7,
339            VictoryCondition::CreateInArea => 8,
340            VictoryCondition::DestroyAll => 9,
341            VictoryCondition::DestroyPlayer => 10,
342            VictoryCondition::Points => 11,
343            VictoryCondition::Other(n) => n,
344        }
345    }
346}
347
348/// All the versions an SCX file uses in a single struct.
349#[derive(Debug, Clone, PartialEq)]
350pub struct VersionBundle {
351    /// The version of the 'container' file format.
352    pub format: SCXVersion,
353    /// The version of the header.
354    pub header: u32,
355    /// The version of the HD Edition DLC Options, only if `header` >= 3.
356    pub dlc_options: Option<i32>,
357    /// The compressed data version.
358    pub data: f32,
359    /// The version of embedded bitmaps.
360    pub picture: u32,
361    /// The version of the victory conditions data.
362    pub victory: f32,
363    /// The version of the trigger system.
364    pub triggers: Option<f64>,
365    /// The version of the map data.
366    pub map: u32,
367}
368
369impl VersionBundle {
370    /// A version bundle with the parameters AoE1 uses by default.
371    pub fn aoe() -> Self {
372        unimplemented!()
373    }
374
375    /// A version bundle with the parameters AoE1: Rise of Rome uses by default.
376    pub fn ror() -> Self {
377        Self {
378            format: SCXVersion(*b"1.11"),
379            header: 2,
380            dlc_options: None,
381            data: 1.15,
382            picture: 1,
383            victory: 2.0,
384            triggers: None,
385            map: 0,
386        }
387    }
388
389    /// A version bundle with the parameters AoK uses by default.
390    pub fn aok() -> Self {
391        Self {
392            format: SCXVersion(*b"1.18"),
393            header: 2,
394            dlc_options: None,
395            data: 1.2,
396            picture: 1,
397            victory: 2.0,
398            triggers: Some(1.6),
399            map: 0,
400        }
401    }
402
403    /// A version bundle with the parameters AoC uses by default
404    pub fn aoc() -> Self {
405        Self {
406            format: SCXVersion(*b"1.21"),
407            header: 2,
408            dlc_options: None,
409            data: 1.22,
410            picture: 1,
411            victory: 2.0,
412            triggers: Some(1.6),
413            map: 0,
414        }
415    }
416
417    /// A version bundle with the parameters UserPatch 1.4 uses by default.
418    pub fn userpatch_14() -> Self {
419        Self::aoc()
420    }
421
422    /// A version bundle with the parameters UserPatch 1.5 uses by default.
423    pub fn userpatch_15() -> Self {
424        Self::userpatch_14()
425    }
426
427    /// A version bundle with the parameters HD Edition uses by default.
428    pub fn hd_edition() -> Self {
429        Self {
430            format: SCXVersion(*b"1.21"),
431            header: 3,
432            dlc_options: Some(1000),
433            data: 1.26,
434            picture: 3,
435            victory: 2.0,
436            triggers: Some(1.6),
437            map: 0,
438        }
439    }
440
441    /// A version bundle with parameters Age of Empires 2: Definitive Edition uses by default.
442    ///
443    /// This will be updated along with DE2 patches.
444    pub fn aoe2_de() -> Self {
445        Self {
446            format: SCXVersion(*b"1.37"),
447            header: 5,
448            dlc_options: Some(1000),
449            data: 1.37,
450            picture: 3,
451            victory: 2.0,
452            triggers: Some(2.2),
453            map: 2,
454        }
455    }
456
457    /// Returns whether this version is (likely) for an AoK scenario.
458    pub fn is_aok(&self) -> bool {
459        match self.format.as_bytes() {
460            b"1.18" | b"1.19" | b"1.20" => true,
461            _ => false,
462        }
463    }
464
465    /// Returns whether this version is (likely) for an AoC scenario.
466    pub fn is_aoc(&self) -> bool {
467        self.format == *b"1.21" && self.data <= 1.22
468    }
469
470    /// Returns whether this version is (likely) for an HD Edition scenario.
471    pub fn is_hd_edition(&self) -> bool {
472        self.format == *b"1.21" || self.format == *b"1.22" && self.data > 1.22
473    }
474
475    /// Returns whether this version is (likely) for an AoE2: Definitive Edition scenario.
476    pub fn is_age2_de(&self) -> bool {
477        self.data >= 1.28
478    }
479}