1pub mod archive_index;
7pub mod archives;
8pub mod collision;
9pub mod diagnostics;
10pub mod fomod;
11pub mod ini;
12pub mod ini_profiles;
13pub mod ini_tweaks;
14pub mod loot;
15pub mod plugin_header;
16pub mod plugins_txt;
17pub mod saves;
18pub mod scanner;
19
20use std::path::{Path, PathBuf};
21
22use modde_core::paths;
23
24use crate::policies::{BareLayoutPolicy, ContentPolicy};
25use crate::traits::{ContentCategory, GamePlugin, ModSafety};
26
27pub struct BethesdaGame {
32 game_id: &'static str,
33 display_name: &'static str,
34 steam_app_id: &'static str,
36 my_games_dir: &'static str,
38 ini_files: &'static [&'static str],
40 archive_ext: &'static [&'static str],
42 nexus_domain: &'static str,
44 plugins_txt_folder_name: &'static str,
46 save_profiles: bool,
50}
51
52impl BethesdaGame {
53 #[must_use]
54 pub const fn new(
55 game_id: &'static str,
56 display_name: &'static str,
57 steam_app_id: &'static str,
58 my_games_dir: &'static str,
59 ini_files: &'static [&'static str],
60 archive_ext: &'static [&'static str],
61 nexus_domain: &'static str,
62 plugins_txt_folder_name: &'static str,
63 ) -> Self {
64 Self {
65 game_id,
66 display_name,
67 steam_app_id,
68 my_games_dir,
69 ini_files,
70 archive_ext,
71 nexus_domain,
72 plugins_txt_folder_name,
73 save_profiles: false,
74 }
75 }
76
77 #[must_use]
79 pub const fn with_save_profiles(mut self, enabled: bool) -> Self {
80 self.save_profiles = enabled;
81 self
82 }
83}
84
85const BETHESDA_SAVE_BREAKING_EXT: &[&str] = &["esp", "esm", "esl", "pex", "dll", "psc"];
87
88const BETHESDA_COSMETIC_EXT: &[&str] = &[
90 "nif", "bsa", "ba2", "dds", "png", "tga", "jpg", "hkx", "fuz", "wav", "xwm", "swf", "ini",
91 "json",
92];
93
94const BETHESDA_CONTENT_CATEGORIES: &[(&str, ContentCategory)] = &[
95 ("esp", ContentCategory::Plugin),
96 ("esm", ContentCategory::Plugin),
97 ("esl", ContentCategory::Plugin),
98 ("dds", ContentCategory::Texture),
99 ("png", ContentCategory::Texture),
100 ("tga", ContentCategory::Texture),
101 ("jpg", ContentCategory::Texture),
102 ("nif", ContentCategory::Mesh),
103 ("wav", ContentCategory::Sound),
104 ("xwm", ContentCategory::Sound),
105 ("fuz", ContentCategory::Sound),
106 ("mp3", ContentCategory::Sound),
107 ("ogg", ContentCategory::Sound),
108 ("pex", ContentCategory::Script),
109 ("psc", ContentCategory::Script),
110 ("swf", ContentCategory::Interface),
111 ("bsa", ContentCategory::Archive),
112 ("ba2", ContentCategory::Archive),
113 ("ini", ContentCategory::Config),
114 ("json", ContentCategory::Config),
115 ("yaml", ContentCategory::Config),
116 ("xml", ContentCategory::Config),
117 ("toml", ContentCategory::Config),
118 ("dll", ContentCategory::Binary),
119 ("so", ContentCategory::Binary),
120];
121
122const BETHESDA_CONTENT_POLICY: ContentPolicy = ContentPolicy {
123 save_breaking_ext: BETHESDA_SAVE_BREAKING_EXT,
124 cosmetic_ext: BETHESDA_COSMETIC_EXT,
125 save_breaking_dirs: &[],
126 categories: BETHESDA_CONTENT_CATEGORIES,
127};
128
129const BETHESDA_BARE_LAYOUT_POLICY: BareLayoutPolicy = BareLayoutPolicy {
130 root_dirs: &[
131 "data",
132 "meshes",
133 "textures",
134 "scripts",
135 "interface",
136 "sound",
137 "music",
138 "materials",
139 "seq",
140 "shadersfx",
141 "strings",
142 ],
143 root_file_exts: &["esp", "esm", "esl", "bsa", "ba2"],
144 case_insensitive_dirs: true,
145};
146
147pub const SKYRIM_SE: BethesdaGame = BethesdaGame::new(
148 "skyrim-se",
149 "The Elder Scrolls V: Skyrim Special Edition",
150 "489830",
151 "Skyrim Special Edition",
152 &["Skyrim.ini", "SkyrimPrefs.ini", "SkyrimCustom.ini"],
153 &["bsa", "ba2"],
154 "skyrimspecialedition",
155 "Skyrim Special Edition",
156)
157.with_save_profiles(true);
158
159pub const SKYRIM_AE: BethesdaGame = BethesdaGame::new(
160 "skyrim-ae",
161 "The Elder Scrolls V: Skyrim Anniversary Edition",
162 "489830",
163 "Skyrim Special Edition",
164 &["Skyrim.ini", "SkyrimPrefs.ini", "SkyrimCustom.ini"],
165 &["bsa", "ba2"],
166 "skyrimspecialedition",
167 "Skyrim Special Edition",
168)
169.with_save_profiles(true);
170
171pub const FALLOUT4: BethesdaGame = BethesdaGame::new(
172 "fallout4",
173 "Fallout 4",
174 "377160",
175 "Fallout4",
176 &["Fallout4.ini", "Fallout4Prefs.ini", "Fallout4Custom.ini"],
177 &["ba2"],
178 "fallout4",
179 "Fallout4",
180)
181.with_save_profiles(true);
182
183pub const FALLOUT76: BethesdaGame = BethesdaGame::new(
184 "fallout76",
185 "Fallout 76",
186 "1151340",
187 "Fallout 76",
188 &["Fallout76.ini", "Fallout76Prefs.ini", "Fallout76Custom.ini"],
189 &["ba2"],
190 "fallout76",
191 "Fallout76",
192)
193.with_save_profiles(true);
194
195pub const STARFIELD: BethesdaGame = BethesdaGame::new(
196 "starfield",
197 "Starfield",
198 "1716740",
199 "Starfield",
200 &["StarfieldPrefs.ini", "StarfieldCustom.ini"],
201 &["ba2"],
202 "starfield",
203 "Starfield",
204)
205.with_save_profiles(true);
206
207impl GamePlugin for BethesdaGame {
208 fn game_id(&self) -> &str {
209 self.game_id
210 }
211
212 fn display_name(&self) -> &str {
213 self.display_name
214 }
215
216 fn mod_directory(&self, install: &Path) -> PathBuf {
217 install.join("Data")
218 }
219
220 fn save_directory(&self) -> Option<PathBuf> {
221 let compat = paths::steam_common()
223 .parent()? .join("compatdata")
225 .join(self.steam_app_id)
226 .join("pfx/drive_c/Users/steamuser/Documents/My Games")
227 .join(self.my_games_dir)
228 .join("Saves");
229 if compat.exists() {
230 return Some(compat);
231 }
232 None
233 }
234
235 fn supports_save_profiles(&self) -> bool {
236 self.save_profiles
237 }
238
239 fn classify_mod(&self, mod_dir: &Path) -> ModSafety {
240 BETHESDA_CONTENT_POLICY.classify_mod(mod_dir)
241 }
242
243 fn classify_extension(&self, ext: &str) -> ContentCategory {
244 BETHESDA_CONTENT_POLICY.classify_extension(ext)
245 }
246
247 fn ini_file_names(&self) -> &[&str] {
248 self.ini_files
249 }
250
251 fn archive_extensions(&self) -> &[&str] {
252 self.archive_ext
253 }
254
255 fn has_plugin_system(&self) -> bool {
256 true
257 }
258
259 fn steam_app_id_u32(&self) -> Option<u32> {
260 self.steam_app_id.parse().ok()
261 }
262
263 fn plugins_txt_folder(&self) -> Option<&str> {
264 Some(self.plugins_txt_folder_name)
265 }
266
267 fn nexus_game_domain(&self) -> Option<&str> {
268 Some(self.nexus_domain)
269 }
270
271 fn nexus_game_id_u32(&self) -> Option<u32> {
272 match self.game_id {
275 "skyrim-se" => Some(1704),
276 "skyrim-ae" => Some(1704), "fallout4" => Some(1151),
278 "fallout76" => Some(2590),
279 "starfield" => Some(4187),
280 _ => None,
281 }
282 }
283
284 fn recognizes_bare_layout(&self, extracted_dir: &Path) -> bool {
285 BETHESDA_BARE_LAYOUT_POLICY.recognizes(extracted_dir)
286 }
287}