genie_scx/
lib.rs

1//! A reader, writer, and converter for all versions of Age of Empires scenarios.
2//!
3//! This crate aims to support every single scenario that exists. If a scenario file from any Age
4//! of Empires 1 or Age of Empires 2 version does not work, please upload it and file an issue!
5
6#![deny(future_incompatible)]
7#![deny(nonstandard_style)]
8#![deny(rust_2018_idioms)]
9#![deny(unsafe_code)]
10#![warn(missing_docs)]
11#![warn(unused)]
12
13mod ai;
14mod bitmap;
15pub mod convert;
16mod format;
17mod header;
18mod map;
19mod player;
20mod triggers;
21mod types;
22mod victory;
23
24use format::SCXFormat;
25use genie_support::{ReadStringError, WriteStringError};
26use std::io::{self, Read, Write};
27
28pub use format::{ScenarioObject, TribeScen};
29pub use genie_support::{DecodeStringError, EncodeStringError};
30pub use genie_support::{StringKey, UnitTypeID};
31pub use header::{DLCOptions, SCXHeader};
32pub use map::{Map, Tile};
33pub use triggers::{Trigger, TriggerCondition, TriggerEffect, TriggerSystem};
34pub use types::*;
35pub use victory::{VictoryConditions, VictoryEntry, VictoryPointEntry};
36
37/// Error type for SCX methods, containing all types of errors that may occur while reading or
38/// writing scenario files.
39#[derive(Debug, thiserror::Error)]
40pub enum Error {
41    /// The scenario that's attempted to be read does not contain a file name.
42    #[error("must have a file name")]
43    MissingFileNameError,
44    /// Attempted to read a scenario with an unsupported format version identifier.
45    #[error("unsupported format version {:?}", .0)]
46    UnsupportedFormatVersionError(SCXVersion),
47    /// Attempted to write a scenario with disabled technologies, to a version that doesn't support
48    /// this many disabled technologies.
49    #[error("too many disabled techs: got {}, but requested version supports up to 20", .0)]
50    TooManyDisabledTechsError(i32),
51    /// Attempted to write a scenario with disabled technologies, to a version that doesn't support
52    /// disabling technologies.
53    #[error("requested version does not support disabling techs")]
54    CannotDisableTechsError,
55    /// Attempted to write a scenario with disabled units, to a version that doesn't support
56    /// disabling units.
57    #[error("requested version does not support disabling units")]
58    CannotDisableUnitsError,
59    /// Attempted to write a scenario with disabled buildings, to a version that doesn't support
60    /// this many disabled buildings.
61    #[error("too many disabled buildings: got {}, but requested version supports up to {}", .0, .1)]
62    TooManyDisabledBuildingsError(i32, i32),
63    /// Attempted to write a scenario with disabled buildings, to a version that doesn't support
64    /// disabling buildings.
65    #[error("requested version does not support disabling buildings")]
66    CannotDisableBuildingsError,
67    /// Failed to decode a string from the scenario file, probably because of a wrong encoding.
68    #[error(transparent)]
69    DecodeStringError(#[from] DecodeStringError),
70    /// Failed to encode a string into the scenario file, probably because of a wrong encoding.
71    #[error(transparent)]
72    EncodeStringError(#[from] EncodeStringError),
73    /// The given ID is not a known diplomatic stance.
74    #[error(transparent)]
75    ParseDiplomaticStanceError(#[from] ParseDiplomaticStanceError),
76    /// The given ID is not a known data set.
77    #[error(transparent)]
78    ParseDataSetError(#[from] ParseDataSetError),
79    /// The given ID is not a known HD Edition DLC.
80    #[error(transparent)]
81    ParseDLCPackageError(#[from] ParseDLCPackageError),
82    /// The given ID is not a known starting age in AoE1 or AoE2.
83    #[error(transparent)]
84    ParseStartingAgeError(#[from] ParseStartingAgeError),
85    /// The given ID is not a known error code.
86    #[error(transparent)]
87    ParseAIErrorCodeError(#[from] num_enum::TryFromPrimitiveError<ai::AIErrorCode>),
88    /// An error occurred while reading or writing.
89    #[error(transparent)]
90    IoError(#[from] io::Error),
91}
92
93impl From<ReadStringError> for Error {
94    fn from(err: ReadStringError) -> Error {
95        match err {
96            ReadStringError::IoError(err) => Error::IoError(err),
97            ReadStringError::DecodeStringError(err) => Error::DecodeStringError(err),
98        }
99    }
100}
101
102impl From<WriteStringError> for Error {
103    fn from(err: WriteStringError) -> Error {
104        match err {
105            WriteStringError::IoError(err) => Error::IoError(err),
106            WriteStringError::EncodeStringError(err) => Error::EncodeStringError(err),
107        }
108    }
109}
110
111/// Result type for SCX methods.
112pub type Result<T> = std::result::Result<T, Error>;
113
114/// A Scenario file.
115#[derive(Debug, Clone)]
116pub struct Scenario {
117    format: SCXFormat,
118    version: VersionBundle,
119}
120
121impl Scenario {
122    /// Read a scenario file.
123    pub fn read_from(input: impl Read) -> Result<Self> {
124        let format = SCXFormat::load_scenario(input)?;
125        let version = format.version();
126
127        Ok(Self { format, version })
128    }
129
130    /// Read a scenario file.
131    #[deprecated = "Use Scenario::read_from instead."]
132    pub fn from<R: Read>(input: &mut R) -> Result<Self> {
133        Self::read_from(input)
134    }
135
136    /// Write the scenario file to an output stream.
137    ///
138    /// Equivalent to `scen.write_to_version(scen.version())`.
139    pub fn write_to(&self, output: impl Write) -> Result<()> {
140        self.format.write_to(output, self.version())
141    }
142
143    /// Write the scenario file to an output stream, targeting specific game versions.
144    pub fn write_to_version(&self, output: impl Write, version: &VersionBundle) -> Result<()> {
145        self.format.write_to(output, version)
146    }
147
148    /// Get the format version of this SCX file.
149    #[inline]
150    pub fn format_version(&self) -> SCXVersion {
151        self.version().format
152    }
153
154    /// Get the header version for this SCX file.
155    #[inline]
156    pub fn header_version(&self) -> u32 {
157        self.version().header
158    }
159
160    /// Get the data version for this SCX file.
161    #[inline]
162    pub fn data_version(&self) -> f32 {
163        self.version().data
164    }
165
166    /// Get the header.
167    #[inline]
168    pub fn header(&self) -> &SCXHeader {
169        &self.format.header
170    }
171
172    /// Get the scenario description.
173    #[inline]
174    pub fn description(&self) -> Option<&str> {
175        self.format.tribe_scen.description()
176    }
177
178    /// Get the scenario filename.
179    #[inline]
180    pub fn filename(&self) -> &str {
181        &self.format.tribe_scen.base.name
182    }
183
184    /// Get data about the game versions this scenario file was made for.
185    #[inline]
186    pub fn version(&self) -> &VersionBundle {
187        &self.version
188    }
189
190    /// Check if this scenario requires the given DLC (for HD Edition scenarios only).
191    #[inline]
192    pub fn requires_dlc(&self, dlc: DLCPackage) -> bool {
193        match &self.header().dlc_options {
194            Some(options) => options.dependencies.iter().any(|dep| *dep == dlc),
195            None => false,
196        }
197    }
198
199    /// Get the UserPatch mod name of the mod that was used to create this scenario.
200    ///
201    /// This returns the short name, like "WK" for WololoKingdoms or "aoc" for Age of Chivalry.
202    #[inline]
203    pub fn mod_name(&self) -> Option<&str> {
204        self.format.mod_name()
205    }
206
207    /// Iterate over all the objects placed in the scenario.
208    #[inline]
209    pub fn objects(&self) -> impl Iterator<Item = &ScenarioObject> {
210        self.format
211            .player_objects
212            .iter()
213            .map(|list| list.iter())
214            .flatten()
215    }
216
217    /// Iterate mutably over all the objects placed in the scenario.
218    #[inline]
219    pub fn objects_mut(&mut self) -> impl Iterator<Item = &mut ScenarioObject> {
220        self.format
221            .player_objects
222            .iter_mut()
223            .map(|list| list.iter_mut())
224            .flatten()
225    }
226
227    /// Get the map/terrain data for this scenario.
228    #[inline]
229    pub fn map(&self) -> &Map {
230        &self.format.map
231    }
232
233    /// Get the (mutable) map/terrain data for this scenario.
234    #[inline]
235    pub fn map_mut(&mut self) -> &mut Map {
236        &mut self.format.map
237    }
238
239    /// Get trigger data for this scenario if it exists.
240    #[inline]
241    pub fn triggers(&self) -> Option<&TriggerSystem> {
242        self.format.triggers.as_ref()
243    }
244
245    /// Get (mutable) trigger data for this scenario if it exists.
246    #[inline]
247    pub fn triggers_mut(&mut self) -> Option<&mut TriggerSystem> {
248        self.format.triggers.as_mut()
249    }
250}