1use std::sync::{OnceLock, RwLock};
6
7use crate::generic::loader::load_user_games;
8use crate::optiscaler::OptiScalerProfile;
9use crate::policies::{CollisionPolicy, PolicyCollisionClassifier};
10use crate::traits::{GamePlugin, ModScanner, SaveTracker};
11
12pub type CollisionClassifierFactory = fn() -> Box<dyn modde_core::collision::CollisionClassifier>;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum EngineFamily {
18 Bethesda,
19 Bannerlord,
20 CyberpunkRedEngine,
21 Gamebryo,
22 Generic,
23 Larian,
24 Smapi,
25 Unreal4,
26 Witcher,
27}
28
29#[derive(Debug, Clone, Copy, Default)]
31pub struct LauncherIds {
32 pub steam_app_id: Option<&'static str>,
33 pub steam_dir: Option<&'static str>,
34 pub heroic_gog_app_id: Option<&'static str>,
35 pub heroic_epic_app_id: Option<&'static str>,
36}
37
38#[derive(Clone, Copy)]
42pub struct GameRegistration {
43 pub game_id: &'static str,
44 pub display_name: &'static str,
45 pub engine: EngineFamily,
46 pub launcher: LauncherIds,
47 pub wabbajack_names: &'static [&'static str],
48 pub nexus_domain: Option<&'static str>,
49 pub nexus_game_id: Option<u32>,
50 pub supports_save_profiles: bool,
51 pub plugin: &'static dyn GamePlugin,
52 pub scanner: Option<&'static dyn ModScanner>,
53 pub save_tracker: Option<&'static dyn SaveTracker>,
54 pub collision_classifier: Option<CollisionClassifierFactory>,
55 pub optiscaler_profiles: &'static [OptiScalerProfile],
56}
57
58impl GameRegistration {
59 pub fn normalized_wabbajack_names(self) -> impl Iterator<Item = String> {
62 self.wabbajack_names.iter().map(|name| {
63 name.chars()
64 .filter(char::is_ascii_alphanumeric)
65 .flat_map(char::to_lowercase)
66 .collect()
67 })
68 }
69}
70
71fn bethesda_collision_classifier() -> Box<dyn modde_core::collision::CollisionClassifier> {
72 Box::new(crate::bethesda::collision::BethesdaCollisionClassifier)
73}
74
75fn cyberpunk_collision_classifier() -> Box<dyn modde_core::collision::CollisionClassifier> {
76 Box::new(crate::cyberpunk::collision::CyberpunkCollisionClassifier)
77}
78
79fn ue4_collision_classifier() -> Box<dyn modde_core::collision::CollisionClassifier> {
80 Box::new(crate::policies::PolicyCollisionClassifier {
81 policy: crate::ue4::UE4_COLLISION_POLICY,
82 })
83}
84
85fn policy_collision_classifier(
86 archive_extensions: &'static [&'static str],
87) -> Box<dyn modde_core::collision::CollisionClassifier> {
88 Box::new(PolicyCollisionClassifier {
89 policy: CollisionPolicy {
90 archive_extensions,
91 severities: DEFAULT_SEVERITIES,
92 },
93 })
94}
95
96const DEFAULT_ARCHIVE_EXTENSIONS: &[&str] = &[];
97const BSA_ARCHIVE_EXTENSIONS: &[&str] = &["bsa"];
98const PAK_ARCHIVE_EXTENSIONS: &[&str] = &["pak", "ucas", "utoc"];
99const WITCHER_ARCHIVE_EXTENSIONS: &[&str] = &["bundle", "cache"];
100
101const DEFAULT_SEVERITIES: &[(&str, modde_core::collision::CollisionSeverity)] = &[
102 ("dds", modde_core::collision::CollisionSeverity::Cosmetic),
103 ("png", modde_core::collision::CollisionSeverity::Cosmetic),
104 ("jpg", modde_core::collision::CollisionSeverity::Cosmetic),
105 ("tga", modde_core::collision::CollisionSeverity::Cosmetic),
106 ("nif", modde_core::collision::CollisionSeverity::Cosmetic),
107 ("ini", modde_core::collision::CollisionSeverity::Config),
108 ("json", modde_core::collision::CollisionSeverity::Config),
109 ("xml", modde_core::collision::CollisionSeverity::Config),
110 ("esp", modde_core::collision::CollisionSeverity::Dangerous),
111 ("esm", modde_core::collision::CollisionSeverity::Dangerous),
112 ("dll", modde_core::collision::CollisionSeverity::Dangerous),
113 ("lua", modde_core::collision::CollisionSeverity::Dangerous),
114 ("ws", modde_core::collision::CollisionSeverity::Dangerous),
115];
116
117pub(crate) fn generic_collision_classifier() -> Box<dyn modde_core::collision::CollisionClassifier>
118{
119 policy_collision_classifier(DEFAULT_ARCHIVE_EXTENSIONS)
120}
121
122fn gamebryo_collision_classifier() -> Box<dyn modde_core::collision::CollisionClassifier> {
123 policy_collision_classifier(BSA_ARCHIVE_EXTENSIONS)
124}
125
126fn pak_collision_classifier() -> Box<dyn modde_core::collision::CollisionClassifier> {
127 policy_collision_classifier(PAK_ARCHIVE_EXTENSIONS)
128}
129
130fn witcher_collision_classifier() -> Box<dyn modde_core::collision::CollisionClassifier> {
131 policy_collision_classifier(WITCHER_ARCHIVE_EXTENSIONS)
132}
133
134pub const SUPPORTED_GAME_IDS: &[&str] = &[
135 "skyrim-se",
136 "skyrim-ae",
137 "fallout4",
138 "fallout76",
139 "starfield",
140 "cyberpunk2077",
141 "stellar-blade",
142 "baldurs-gate3",
143 "stardew-valley",
144 "fallout-new-vegas",
145 "oblivion",
146 "oblivion-remastered",
147 "bannerlord",
148 "witcher3",
149 "subnautica2",
150];
151
152pub static GAME_REGISTRY: &[GameRegistration] = &[
153 GameRegistration {
154 game_id: "skyrim-se",
155 display_name: "The Elder Scrolls V: Skyrim Special Edition",
156 engine: EngineFamily::Bethesda,
157 launcher: LauncherIds {
158 steam_app_id: Some("489830"),
159 steam_dir: Some("Skyrim Special Edition"),
160 heroic_gog_app_id: None,
161 heroic_epic_app_id: None,
162 },
163 wabbajack_names: &["SkyrimSpecialEdition", "SkyrimSE"],
164 nexus_domain: Some("skyrimspecialedition"),
165 nexus_game_id: Some(1704),
166 supports_save_profiles: true,
167 plugin: &crate::bethesda::SKYRIM_SE,
168 scanner: Some(&crate::bethesda::scanner::SKYRIM_SCANNER),
169 save_tracker: Some(&crate::bethesda::saves::SKYRIM_SAVE_TRACKER),
170 collision_classifier: Some(bethesda_collision_classifier),
171 optiscaler_profiles: &[],
172 },
173 GameRegistration {
174 game_id: "skyrim-ae",
175 display_name: "The Elder Scrolls V: Skyrim Anniversary Edition",
176 engine: EngineFamily::Bethesda,
177 launcher: LauncherIds {
178 steam_app_id: None,
181 steam_dir: None,
182 heroic_gog_app_id: None,
183 heroic_epic_app_id: None,
184 },
185 wabbajack_names: &["SkyrimAnniversaryEdition", "SkyrimAE"],
186 nexus_domain: Some("skyrimspecialedition"),
187 nexus_game_id: Some(1704),
188 supports_save_profiles: true,
189 plugin: &crate::bethesda::SKYRIM_AE,
190 scanner: Some(&crate::bethesda::scanner::SKYRIM_SCANNER),
191 save_tracker: Some(&crate::bethesda::saves::SKYRIM_SAVE_TRACKER),
192 collision_classifier: Some(bethesda_collision_classifier),
193 optiscaler_profiles: &[],
194 },
195 GameRegistration {
196 game_id: "fallout4",
197 display_name: "Fallout 4",
198 engine: EngineFamily::Bethesda,
199 launcher: LauncherIds {
200 steam_app_id: Some("377160"),
201 steam_dir: Some("Fallout 4"),
202 heroic_gog_app_id: Some("1998527297"),
203 heroic_epic_app_id: None,
204 },
205 wabbajack_names: &["Fallout4"],
206 nexus_domain: Some("fallout4"),
207 nexus_game_id: Some(1151),
208 supports_save_profiles: true,
209 plugin: &crate::bethesda::FALLOUT4,
210 scanner: Some(&crate::bethesda::scanner::FALLOUT4_SCANNER),
211 save_tracker: Some(&crate::bethesda::saves::FALLOUT4_SAVE_TRACKER),
212 collision_classifier: Some(bethesda_collision_classifier),
213 optiscaler_profiles: &[],
214 },
215 GameRegistration {
216 game_id: "fallout76",
217 display_name: "Fallout 76",
218 engine: EngineFamily::Bethesda,
219 launcher: LauncherIds {
220 steam_app_id: Some("1151340"),
221 steam_dir: Some("Fallout76"),
222 heroic_gog_app_id: None,
223 heroic_epic_app_id: None,
224 },
225 wabbajack_names: &["Fallout76"],
226 nexus_domain: Some("fallout76"),
227 nexus_game_id: Some(2590),
228 supports_save_profiles: true,
229 plugin: &crate::bethesda::FALLOUT76,
230 scanner: Some(&crate::bethesda::scanner::FALLOUT76_SCANNER),
231 save_tracker: Some(&crate::bethesda::saves::FALLOUT76_SAVE_TRACKER),
232 collision_classifier: Some(bethesda_collision_classifier),
233 optiscaler_profiles: &[],
234 },
235 GameRegistration {
236 game_id: "starfield",
237 display_name: "Starfield",
238 engine: EngineFamily::Bethesda,
239 launcher: LauncherIds {
240 steam_app_id: Some("1716740"),
241 steam_dir: Some("Starfield"),
242 heroic_gog_app_id: None,
243 heroic_epic_app_id: None,
244 },
245 wabbajack_names: &["Starfield"],
246 nexus_domain: Some("starfield"),
247 nexus_game_id: Some(4187),
248 supports_save_profiles: true,
249 plugin: &crate::bethesda::STARFIELD,
250 scanner: Some(&crate::bethesda::scanner::STARFIELD_SCANNER),
251 save_tracker: Some(&crate::bethesda::saves::STARFIELD_SAVE_TRACKER),
252 collision_classifier: Some(bethesda_collision_classifier),
253 optiscaler_profiles: &[],
254 },
255 GameRegistration {
256 game_id: "cyberpunk2077",
257 display_name: "Cyberpunk 2077",
258 engine: EngineFamily::CyberpunkRedEngine,
259 launcher: LauncherIds {
260 steam_app_id: Some("1091500"),
261 steam_dir: Some("Cyberpunk 2077"),
262 heroic_gog_app_id: Some("1423049311"),
263 heroic_epic_app_id: Some("Ginger"),
264 },
265 wabbajack_names: &["Cyberpunk2077"],
266 nexus_domain: Some("cyberpunk2077"),
267 nexus_game_id: Some(3333),
268 supports_save_profiles: true,
269 plugin: &crate::cyberpunk::CYBERPUNK2077,
270 scanner: Some(&crate::cyberpunk::scanner::CYBERPUNK_SCANNER),
271 save_tracker: Some(&crate::cyberpunk::saves::CYBERPUNK_SAVE_TRACKER),
272 collision_classifier: Some(cyberpunk_collision_classifier),
273 optiscaler_profiles: &[],
274 },
275 GameRegistration {
276 game_id: "stellar-blade",
277 display_name: "Stellar Blade",
278 engine: EngineFamily::Unreal4,
279 launcher: LauncherIds {
280 steam_app_id: Some("3489700"),
281 steam_dir: Some("Stellar Blade"),
282 heroic_gog_app_id: None,
283 heroic_epic_app_id: None,
284 },
285 wabbajack_names: &["StellarBlade"],
286 nexus_domain: Some("stellarblade"),
287 nexus_game_id: None,
293 supports_save_profiles: true,
294 plugin: &crate::ue4::STELLAR_BLADE,
295 scanner: Some(&crate::ue4::scanner::STELLAR_BLADE_SCANNER),
296 save_tracker: Some(&crate::ue4::saves::STELLAR_BLADE_SAVE_TRACKER),
297 collision_classifier: Some(ue4_collision_classifier),
298 optiscaler_profiles: crate::ue4::STELLAR_BLADE_OPTISCALER_PROFILES,
299 },
300 GameRegistration {
301 game_id: "baldurs-gate3",
302 display_name: "Baldur's Gate 3",
303 engine: EngineFamily::Larian,
304 launcher: LauncherIds {
305 steam_app_id: Some("1086940"),
306 steam_dir: Some("Baldurs Gate 3"),
307 heroic_gog_app_id: None,
308 heroic_epic_app_id: None,
309 },
310 wabbajack_names: &[],
311 nexus_domain: Some("baldursgate3"),
312 nexus_game_id: None,
313 supports_save_profiles: true,
314 plugin: &crate::bg3::BALDURS_GATE3,
315 scanner: Some(&crate::bg3::scanner::BG3_SCANNER),
316 save_tracker: Some(&crate::bg3::saves::BG3_SAVE_TRACKER),
317 collision_classifier: Some(pak_collision_classifier),
318 optiscaler_profiles: &[],
319 },
320 GameRegistration {
321 game_id: "stardew-valley",
322 display_name: "Stardew Valley",
323 engine: EngineFamily::Smapi,
324 launcher: LauncherIds {
325 steam_app_id: Some("413150"),
326 steam_dir: Some("Stardew Valley"),
327 heroic_gog_app_id: None,
328 heroic_epic_app_id: None,
329 },
330 wabbajack_names: &[],
331 nexus_domain: Some("stardewvalley"),
332 nexus_game_id: None,
333 supports_save_profiles: true,
334 plugin: &crate::stardew::STARDEW_VALLEY,
335 scanner: Some(&crate::stardew::scanner::STARDEW_SCANNER),
336 save_tracker: Some(&crate::stardew::saves::STARDEW_SAVE_TRACKER),
337 collision_classifier: Some(generic_collision_classifier),
338 optiscaler_profiles: &[],
339 },
340 GameRegistration {
341 game_id: "fallout-new-vegas",
342 display_name: "Fallout: New Vegas",
343 engine: EngineFamily::Gamebryo,
344 launcher: LauncherIds {
345 steam_app_id: Some("22380"),
346 steam_dir: Some("Fallout New Vegas"),
347 heroic_gog_app_id: None,
348 heroic_epic_app_id: None,
349 },
350 wabbajack_names: &["FalloutNewVegas", "FalloutNV"],
351 nexus_domain: Some("newvegas"),
352 nexus_game_id: None,
353 supports_save_profiles: true,
354 plugin: &crate::gamebryo::FALLOUT_NEW_VEGAS,
355 scanner: Some(&crate::gamebryo::scanner::FALLOUT_NEW_VEGAS_SCANNER),
356 save_tracker: Some(&crate::gamebryo::saves::GAMEBRYO_SAVE_TRACKER),
357 collision_classifier: Some(gamebryo_collision_classifier),
358 optiscaler_profiles: &[],
359 },
360 GameRegistration {
361 game_id: "oblivion",
362 display_name: "The Elder Scrolls IV: Oblivion",
363 engine: EngineFamily::Gamebryo,
364 launcher: LauncherIds {
365 steam_app_id: Some("22330"),
366 steam_dir: Some("Oblivion"),
367 heroic_gog_app_id: None,
368 heroic_epic_app_id: None,
369 },
370 wabbajack_names: &["Oblivion"],
371 nexus_domain: Some("oblivion"),
372 nexus_game_id: None,
373 supports_save_profiles: true,
374 plugin: &crate::gamebryo::OBLIVION,
375 scanner: Some(&crate::gamebryo::scanner::OBLIVION_SCANNER),
376 save_tracker: Some(&crate::gamebryo::saves::GAMEBRYO_SAVE_TRACKER),
377 collision_classifier: Some(gamebryo_collision_classifier),
378 optiscaler_profiles: &[],
379 },
380 GameRegistration {
381 game_id: "oblivion-remastered",
382 display_name: "The Elder Scrolls IV: Oblivion Remastered",
383 engine: EngineFamily::Unreal4,
384 launcher: LauncherIds {
385 steam_app_id: Some("2623190"),
386 steam_dir: Some("Oblivion Remastered"),
387 heroic_gog_app_id: None,
388 heroic_epic_app_id: None,
389 },
390 wabbajack_names: &["OblivionRemastered"],
391 nexus_domain: Some("oblivionremastered"),
392 nexus_game_id: None,
393 supports_save_profiles: true,
394 plugin: &crate::oblivion_remastered::OBLIVION_REMASTERED,
395 scanner: Some(&crate::oblivion_remastered::scanner::OBLIVION_REMASTERED_SCANNER),
396 save_tracker: Some(&crate::oblivion_remastered::saves::OBLIVION_REMASTERED_SAVE_TRACKER),
397 collision_classifier: Some(pak_collision_classifier),
398 optiscaler_profiles: &[],
399 },
400 GameRegistration {
401 game_id: "bannerlord",
402 display_name: "Mount & Blade II: Bannerlord",
403 engine: EngineFamily::Bannerlord,
404 launcher: LauncherIds {
405 steam_app_id: Some("261550"),
406 steam_dir: Some("Mount & Blade II Bannerlord"),
407 heroic_gog_app_id: None,
408 heroic_epic_app_id: None,
409 },
410 wabbajack_names: &[],
411 nexus_domain: Some("mountandblade2bannerlord"),
412 nexus_game_id: None,
413 supports_save_profiles: true,
414 plugin: &crate::bannerlord::BANNERLORD,
415 scanner: Some(&crate::bannerlord::scanner::BANNERLORD_SCANNER),
416 save_tracker: Some(&crate::bannerlord::saves::BANNERLORD_SAVE_TRACKER),
417 collision_classifier: Some(generic_collision_classifier),
418 optiscaler_profiles: &[],
419 },
420 GameRegistration {
421 game_id: "witcher3",
422 display_name: "The Witcher 3: Wild Hunt",
423 engine: EngineFamily::Witcher,
424 launcher: LauncherIds {
425 steam_app_id: Some("292030"),
426 steam_dir: Some("The Witcher 3"),
427 heroic_gog_app_id: None,
428 heroic_epic_app_id: None,
429 },
430 wabbajack_names: &[],
431 nexus_domain: Some("witcher3"),
432 nexus_game_id: None,
433 supports_save_profiles: true,
434 plugin: &crate::witcher3::WITCHER3,
435 scanner: Some(&crate::witcher3::scanner::WITCHER3_SCANNER),
436 save_tracker: Some(&crate::witcher3::saves::WITCHER3_SAVE_TRACKER),
437 collision_classifier: Some(witcher_collision_classifier),
438 optiscaler_profiles: &[],
439 },
440 GameRegistration {
441 game_id: "subnautica2",
442 display_name: "Subnautica 2",
443 engine: EngineFamily::Unreal4,
444 launcher: LauncherIds {
445 steam_app_id: Some("1962700"),
446 steam_dir: Some("Subnautica2"),
447 heroic_gog_app_id: None,
448 heroic_epic_app_id: None,
449 },
450 wabbajack_names: &[],
451 nexus_domain: Some("subnautica2"),
452 nexus_game_id: None,
453 supports_save_profiles: true,
454 plugin: &crate::ue4::SUBNAUTICA2,
455 scanner: Some(&crate::ue4::scanner::SUBNAUTICA2_SCANNER),
456 save_tracker: Some(&crate::ue4::saves::SUBNAUTICA2_SAVE_TRACKER),
457 collision_classifier: Some(ue4_collision_classifier),
458 optiscaler_profiles: &[],
459 },
460];
461
462static REGISTRY: OnceLock<RwLock<&'static [GameRegistration]>> = OnceLock::new();
463
464fn build_registry_snapshot() -> &'static [GameRegistration] {
465 Box::leak(
466 GAME_REGISTRY
467 .iter()
468 .copied()
469 .chain(load_user_games())
470 .collect::<Vec<_>>()
471 .into_boxed_slice(),
472 )
473}
474
475#[must_use]
477pub fn all_games() -> &'static [GameRegistration] {
478 *REGISTRY
479 .get_or_init(|| RwLock::new(build_registry_snapshot()))
480 .read()
481 .unwrap_or_else(std::sync::PoisonError::into_inner)
482}
483
484pub fn reload_registry() {
486 let registry = REGISTRY.get_or_init(|| RwLock::new(build_registry_snapshot()));
487 *registry
488 .write()
489 .unwrap_or_else(std::sync::PoisonError::into_inner) = build_registry_snapshot();
490}
491
492#[must_use]
494pub fn supported_game_ids() -> Vec<&'static str> {
495 all_games().iter().map(|game| game.game_id).collect()
496}
497
498#[must_use]
500pub fn resolve_game(game_id: &str) -> Option<&'static GameRegistration> {
501 all_games().iter().find(|game| game.game_id == game_id)
502}
503
504#[must_use]
510pub fn resolve_game_by_nexus_domain(domain: &str) -> Option<&'static GameRegistration> {
511 all_games()
512 .iter()
513 .find(|game| game.nexus_domain == Some(domain))
514}
515
516pub fn launcher_games() -> impl Iterator<Item = &'static GameRegistration> {
518 all_games().iter().filter(|game| {
519 game.launcher.steam_app_id.is_some()
520 || game.launcher.heroic_gog_app_id.is_some()
521 || game.launcher.heroic_epic_app_id.is_some()
522 })
523}