Skip to main content

loadorder/
enums.rs

1/*
2 * This file is part of libloadorder
3 *
4 * Copyright (C) 2017 Oliver Hamlet
5 *
6 * libloadorder is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
10 *
11 * libloadorder is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with libloadorder. If not, see <http://www.gnu.org/licenses/>.
18 */
19
20use std::error;
21use std::ffi::OsString;
22use std::fmt;
23use std::io;
24use std::path::Path;
25use std::path::PathBuf;
26use std::slice::EscapeAscii;
27
28#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
29#[non_exhaustive]
30pub enum LoadOrderMethod {
31    Timestamp,
32    Textfile,
33    Asterisk,
34    OpenMW,
35}
36
37#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
38#[non_exhaustive]
39pub enum GameId {
40    Morrowind = 1,
41    Oblivion,
42    Skyrim,
43    Fallout3,
44    FalloutNV,
45    Fallout4,
46    SkyrimSE,
47    Fallout4VR,
48    SkyrimVR,
49    Starfield,
50    OpenMW,
51    OblivionRemastered,
52}
53
54impl GameId {
55    pub fn to_esplugin_id(self) -> esplugin::GameId {
56        match self {
57            GameId::Morrowind | GameId::OpenMW => esplugin::GameId::Morrowind,
58            GameId::Oblivion | GameId::OblivionRemastered => esplugin::GameId::Oblivion,
59            GameId::Skyrim => esplugin::GameId::Skyrim,
60            GameId::SkyrimSE | GameId::SkyrimVR => esplugin::GameId::SkyrimSE,
61            GameId::Fallout3 => esplugin::GameId::Fallout3,
62            GameId::FalloutNV => esplugin::GameId::FalloutNV,
63            GameId::Fallout4 | GameId::Fallout4VR => esplugin::GameId::Fallout4,
64            GameId::Starfield => esplugin::GameId::Starfield,
65        }
66    }
67
68    pub fn supports_light_plugins(self) -> bool {
69        matches!(
70            self,
71            Self::Fallout4 | Self::Fallout4VR | Self::SkyrimSE | Self::SkyrimVR | Self::Starfield
72        )
73    }
74
75    pub fn supports_medium_plugins(self) -> bool {
76        self == GameId::Starfield
77    }
78
79    pub fn allow_plugin_ghosting(self) -> bool {
80        self != GameId::OpenMW
81    }
82
83    pub(crate) fn treats_master_files_differently(self) -> bool {
84        !matches!(self, GameId::OpenMW | GameId::OblivionRemastered)
85    }
86}
87
88#[expect(clippy::error_impl_error)]
89#[derive(Debug)]
90#[non_exhaustive]
91pub enum Error {
92    InvalidPath(PathBuf),
93    IoError(PathBuf, io::Error),
94    NoFilename(PathBuf),
95    DecodeError(Vec<u8>),
96    EncodeError(String),
97    PluginParsingError(PathBuf, Box<dyn error::Error + Send + Sync + 'static>),
98    PluginNotFound(String),
99    TooManyActivePlugins {
100        light_count: usize,
101        medium_count: usize,
102        full_count: usize,
103    },
104    DuplicatePlugin(String),
105    NonMasterBeforeMaster {
106        master: String,
107        non_master: String,
108    },
109    InvalidEarlyLoadingPluginPosition {
110        name: String,
111        pos: usize,
112        expected_pos: usize,
113    },
114    ImplicitlyActivePlugin(String),
115    BlueprintPluginImplicitlyActiveOnly(String),
116    BlueprintShipsPluginImplicitlyActiveOnly(String),
117    NoLocalAppData,
118    NoDocumentsPath,
119    NoUserConfigPath,
120    NoUserDataPath,
121    NoProgramFilesPath,
122    UnrepresentedHoist {
123        plugin: String,
124        master: String,
125    },
126    InstalledPlugin(String),
127    IniParsingError {
128        path: PathBuf,
129        line: usize,
130        column: usize,
131        message: String,
132    },
133    VdfParsingError(PathBuf, String),
134    SystemError(i32, OsString),
135    InvalidBlueprintPluginPosition {
136        name: String,
137        pos: usize,
138        expected_pos: usize,
139    },
140}
141
142#[cfg(windows)]
143impl From<windows_result::Error> for Error {
144    fn from(error: windows_result::Error) -> Self {
145        Error::SystemError(error.code().0, error.message().into())
146    }
147}
148
149impl fmt::Display for Error {
150    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
151        match self {
152            Error::InvalidPath(path) => write!(f, "The path \"{}\" is invalid", escape_ascii(path)),
153            Error::IoError(path, error) =>
154                write!(f, "I/O error involving the path \"{}\": {error}", escape_ascii(path)),
155            Error::NoFilename(path) =>
156                write!(f, "The plugin path \"{}\" has no filename part", escape_ascii(path)),
157            Error::DecodeError(bytes) => write!(f, "String could not be decoded from Windows-1252: {}", bytes.escape_ascii()),
158            Error::EncodeError(string) => write!(f, "The string \"{string}\" could not be encoded to Windows-1252"),
159            Error::PluginParsingError(path, err) => {
160                write!(f, "An error was encountered while parsing the plugin at \"{}\": {err}", escape_ascii(path))
161            }
162            Error::PluginNotFound(name) => {
163                write!(f, "The plugin \"{name}\" is not in the load order")
164            }
165            Error::TooManyActivePlugins {light_count, medium_count, full_count } =>
166                write!(f, "Maximum number of active plugins exceeded: there are {full_count} active full plugins, {medium_count} active medium plugins and {light_count} active light plugins"),
167            Error::DuplicatePlugin(name) =>
168                write!(f, "The given plugin list contains more than one instance of \"{name}\""),
169            Error::NonMasterBeforeMaster{ master, non_master} =>
170                write!(f, "Attempted to load the non-master plugin \"{non_master}\" before the master plugin \"{master}\""),
171            Error::InvalidEarlyLoadingPluginPosition{ name, pos, expected_pos } =>
172                write!(f, "Attempted to load the early-loading plugin \"{name}\" at position {pos}, its expected position is {expected_pos}"),
173            Error::ImplicitlyActivePlugin(name) =>
174                write!(f, "The implicitly active plugin \"{name}\" cannot be deactivated"),
175            Error::BlueprintPluginImplicitlyActiveOnly(name) =>
176                write!(f, "The blueprint plugin \"{name}\" cannot be explicitly activated because that state will not be recorded when the load order is saved"),
177            Error::BlueprintShipsPluginImplicitlyActiveOnly(name) =>
178                write!(f, "The BlueprintShips plugin \"{name}\" cannot be explicitly activated because that state will not be recorded when the load order is saved"),
179            Error::NoLocalAppData => {
180                write!(f, "The game's local app data folder could not be detected")
181            }
182            Error::NoDocumentsPath => write!(f, "The user's Documents path could not be detected"),
183            Error::NoUserConfigPath => write!(f, "The user's config path could not be detected"),
184            Error::NoUserDataPath => write!(f, "The user's data path could not be detected"),
185            Error::NoProgramFilesPath => write!(f, "The Program Files path could not be obtained"),
186            Error::UnrepresentedHoist { plugin, master } =>
187                write!(f, "The plugin \"{plugin}\" is a master of \"{master}\", which will hoist it"),
188            Error::InstalledPlugin(plugin) =>
189                write!(f, "The plugin \"{plugin}\" is installed, so cannot be removed from the load order"),
190            Error::IniParsingError {
191                path,
192                line,
193                column,
194                message,
195            } => write!(f, "Failed to parse ini file at \"{}\", error at line {line}, column {column}: {message}", escape_ascii(path)),
196            Error::VdfParsingError(path, message) =>
197                write!(f, "Failed to parse VDF file at \"{}\": {message}", escape_ascii(path)),
198            Error::SystemError(code, message) =>
199                write!(f, "Error returned by the operating system, code {code}: \"{}\"", message.as_encoded_bytes().escape_ascii()),
200            Error::InvalidBlueprintPluginPosition{ name, pos, expected_pos } =>
201                write!(f, "Attempted to load the blueprint plugin \"{name}\" at position {pos}, its expected position is {expected_pos}"),
202        }
203    }
204}
205
206impl error::Error for Error {
207    fn cause(&self) -> Option<&dyn error::Error> {
208        match self {
209            Error::IoError(_, x) => Some(x),
210            Error::PluginParsingError(_, x) => Some(x.as_ref()),
211            _ => None,
212        }
213    }
214}
215
216fn escape_ascii(path: &Path) -> EscapeAscii<'_> {
217    path.as_os_str().as_encoded_bytes().escape_ascii()
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223
224    #[test]
225    fn game_id_should_map_to_libespm_id_correctly() {
226        assert_eq!(
227            esplugin::GameId::Morrowind,
228            GameId::Morrowind.to_esplugin_id()
229        );
230        assert_eq!(
231            esplugin::GameId::Oblivion,
232            GameId::Oblivion.to_esplugin_id()
233        );
234        assert_eq!(esplugin::GameId::Skyrim, GameId::Skyrim.to_esplugin_id());
235        assert_eq!(
236            esplugin::GameId::SkyrimSE,
237            GameId::SkyrimSE.to_esplugin_id()
238        );
239        assert_eq!(
240            esplugin::GameId::SkyrimSE,
241            GameId::SkyrimVR.to_esplugin_id()
242        );
243        assert_eq!(
244            esplugin::GameId::Fallout3,
245            GameId::Fallout3.to_esplugin_id()
246        );
247        assert_eq!(
248            esplugin::GameId::FalloutNV,
249            GameId::FalloutNV.to_esplugin_id()
250        );
251        assert_eq!(
252            esplugin::GameId::Fallout4,
253            GameId::Fallout4.to_esplugin_id()
254        );
255        assert_eq!(
256            esplugin::GameId::Fallout4,
257            GameId::Fallout4VR.to_esplugin_id()
258        );
259        assert_eq!(
260            esplugin::GameId::Starfield,
261            GameId::Starfield.to_esplugin_id()
262        );
263    }
264
265    #[test]
266    fn game_id_supports_light_plugins_should_be_false_until_fallout_4() {
267        assert!(!GameId::OpenMW.supports_light_plugins());
268        assert!(!GameId::Morrowind.supports_light_plugins());
269        assert!(!GameId::Oblivion.supports_light_plugins());
270        assert!(!GameId::Skyrim.supports_light_plugins());
271        assert!(GameId::SkyrimSE.supports_light_plugins());
272        assert!(GameId::SkyrimVR.supports_light_plugins());
273        assert!(!GameId::Fallout3.supports_light_plugins());
274        assert!(!GameId::FalloutNV.supports_light_plugins());
275        assert!(GameId::Fallout4.supports_light_plugins());
276        assert!(GameId::Fallout4VR.supports_light_plugins());
277        assert!(GameId::Starfield.supports_light_plugins());
278    }
279
280    #[test]
281    fn game_id_supports_medium_plugins_should_be_false_except_for_starfield() {
282        assert!(!GameId::OpenMW.supports_medium_plugins());
283        assert!(!GameId::Morrowind.supports_medium_plugins());
284        assert!(!GameId::Oblivion.supports_medium_plugins());
285        assert!(!GameId::Skyrim.supports_medium_plugins());
286        assert!(!GameId::SkyrimSE.supports_medium_plugins());
287        assert!(!GameId::SkyrimVR.supports_medium_plugins());
288        assert!(!GameId::Fallout3.supports_medium_plugins());
289        assert!(!GameId::FalloutNV.supports_medium_plugins());
290        assert!(!GameId::Fallout4.supports_medium_plugins());
291        assert!(!GameId::Fallout4VR.supports_medium_plugins());
292        assert!(GameId::Starfield.supports_medium_plugins());
293    }
294
295    #[test]
296    fn game_id_allow_plugin_ghosting_should_be_false_for_openmw_only() {
297        assert!(!GameId::OpenMW.allow_plugin_ghosting());
298        assert!(GameId::Morrowind.allow_plugin_ghosting());
299        assert!(GameId::Oblivion.allow_plugin_ghosting());
300        assert!(GameId::Skyrim.allow_plugin_ghosting());
301        assert!(GameId::SkyrimSE.allow_plugin_ghosting());
302        assert!(GameId::SkyrimVR.allow_plugin_ghosting());
303        assert!(GameId::Fallout3.allow_plugin_ghosting());
304        assert!(GameId::FalloutNV.allow_plugin_ghosting());
305        assert!(GameId::Fallout4.allow_plugin_ghosting());
306        assert!(GameId::Fallout4VR.allow_plugin_ghosting());
307        assert!(GameId::Starfield.allow_plugin_ghosting());
308    }
309
310    #[test]
311    fn game_id_treats_master_files_differently_should_be_false_for_openmw_and_oblivion_remastered_only(
312    ) {
313        assert!(!GameId::OpenMW.treats_master_files_differently());
314        assert!(GameId::Morrowind.treats_master_files_differently());
315        assert!(GameId::Oblivion.treats_master_files_differently());
316        assert!(!GameId::OblivionRemastered.treats_master_files_differently());
317        assert!(GameId::Skyrim.treats_master_files_differently());
318        assert!(GameId::SkyrimSE.treats_master_files_differently());
319        assert!(GameId::SkyrimVR.treats_master_files_differently());
320        assert!(GameId::Fallout3.treats_master_files_differently());
321        assert!(GameId::FalloutNV.treats_master_files_differently());
322        assert!(GameId::Fallout4.treats_master_files_differently());
323        assert!(GameId::Fallout4VR.treats_master_files_differently());
324        assert!(GameId::Starfield.treats_master_files_differently());
325    }
326
327    #[test]
328    fn error_display_should_print_double_quoted_paths() {
329        let string = format!("{}", Error::InvalidPath(PathBuf::from("foo")));
330
331        assert_eq!("The path \"foo\" is invalid", string);
332    }
333
334    #[test]
335    fn error_display_should_print_os_string_as_quoted_string() {
336        let string = format!("{}", Error::SystemError(1, OsString::from("foo")));
337
338        assert_eq!(
339            "Error returned by the operating system, code 1: \"foo\"",
340            string
341        );
342    }
343}