Skip to main content

modde_games/generic/
mod.rs

1//! User-defined "generic" games: a configurable [`GamePlugin`] driven by a
2//! TOML [`spec::GameSpec`], plus loading/managing those user specs from disk.
3
4pub(crate) mod leak;
5pub mod loader;
6pub mod manage;
7pub mod spec;
8
9use std::path::{Path, PathBuf};
10
11use smallvec::SmallVec;
12
13use crate::traits::GamePlugin;
14
15use self::spec::GameSpec;
16
17/// A generic game with loose file drop support.
18pub struct GenericGame {
19    id: String,
20    name: String,
21    install_path_override: Option<PathBuf>,
22    install_dir_name: Option<String>,
23    mod_dir: Option<PathBuf>,
24    executable_dir: PathBuf,
25    proxy_dlls: Vec<String>,
26    steam_app_id: Option<String>,
27    nexus_domain: Option<String>,
28}
29
30impl GenericGame {
31    /// Build a generic game from its core fields, leaving optional metadata empty.
32    pub fn new(
33        id: impl Into<String>,
34        name: impl Into<String>,
35        install_path_override: Option<PathBuf>,
36        mod_dir: impl Into<PathBuf>,
37    ) -> Self {
38        Self {
39            id: id.into(),
40            name: name.into(),
41            install_path_override,
42            install_dir_name: None,
43            mod_dir: Some(mod_dir.into()),
44            executable_dir: PathBuf::new(),
45            proxy_dlls: Vec::new(),
46            steam_app_id: None,
47            nexus_domain: None,
48        }
49    }
50
51    /// Build a generic game from a deserialized [`GameSpec`].
52    pub fn from_spec(spec: GameSpec) -> Self {
53        Self {
54            id: spec.id,
55            name: spec.display_name,
56            install_path_override: spec.install_path_override,
57            install_dir_name: spec.install_dir_name,
58            mod_dir: spec.mod_dir,
59            executable_dir: spec.executable_dir,
60            proxy_dlls: spec.proxy_dlls,
61            steam_app_id: spec.steam_app_id,
62            nexus_domain: spec.nexus_domain,
63        }
64    }
65
66    fn install_from_steam_dir_name(dir_name: &str) -> Option<PathBuf> {
67        modde_core::paths::steam_library_folders()
68            .into_iter()
69            .map(|library| library.join("steamapps/common").join(dir_name))
70            .find(|path| path.is_dir())
71    }
72}
73
74impl GamePlugin for GenericGame {
75    fn game_id(&self) -> &str {
76        &self.id
77    }
78
79    fn display_name(&self) -> &str {
80        &self.name
81    }
82
83    fn detect_install(&self) -> Option<PathBuf> {
84        if let Some(path) = self
85            .install_path_override
86            .as_ref()
87            .filter(|path| path.is_dir())
88        {
89            return Some(path.clone());
90        }
91
92        if let Some(dir_name) = self.install_dir_name.as_deref()
93            && let Some(path) = Self::install_from_steam_dir_name(dir_name)
94        {
95            return Some(path);
96        }
97
98        crate::detection::find_game_install(&modde_core::GameId::from(self.game_id()))
99    }
100
101    fn mod_directory(&self, install: &Path) -> PathBuf {
102        self.mod_dir
103            .as_ref()
104            .map_or_else(|| install.to_path_buf(), |dir| install.join(dir))
105    }
106
107    fn executable_dir(&self, install: &Path) -> PathBuf {
108        install.join(&self.executable_dir)
109    }
110
111    fn wine_dll_overrides(&self, install: &Path) -> SmallVec<[String; 4]> {
112        let executable_dir = self.executable_dir(install);
113        self.proxy_dlls
114            .iter()
115            .filter(|name| executable_dir.join(format!("{name}.dll")).exists())
116            .cloned()
117            .collect()
118    }
119
120    fn steam_app_id_u32(&self) -> Option<u32> {
121        self.steam_app_id.as_deref()?.parse().ok()
122    }
123
124    fn nexus_game_domain(&self) -> Option<&str> {
125        self.nexus_domain.as_deref()
126    }
127}