Skip to main content

modde_games/
lib.rs

1use smallvec::SmallVec;
2
3pub mod bethesda;
4pub mod cyberpunk;
5pub mod detection;
6pub mod generic;
7pub mod launcher;
8pub mod tools;
9pub mod traits;
10pub mod ue4;
11
12pub use detection::{find_detected_game, scan_installed_games, DetectedGame, LauncherSource};
13pub use traits::{
14    GamePlugin, ModClassifyConfig, ModSafety, SaveTracker, classify_mod_by_content,
15    DiscoveredFile, DiscoveredMod, ModScanner, ModSource, ScanContext,
16    walk_files_relative, slug,
17};
18
19/// Build an [`modde_core::installer::InstallProbe`] that delegates to a
20/// game plugin's [`GamePlugin::analyze_mod_archive`] and
21/// [`GamePlugin::recognizes_bare_layout`] hooks.
22///
23/// Only `&'static dyn GamePlugin` is accepted because all registered
24/// plugins are `static` (see [`resolve_game_plugin`]), and the probe's
25/// closures need `'static` captures to cross async task boundaries.
26///
27/// ```ignore
28/// let plugin = resolve_game_plugin("skyrim-se").unwrap();
29/// let probe = game_probe(plugin);
30/// let plan = modde_core::installer::analyze(&extracted, &probe, hash)?;
31/// ```
32pub fn game_probe(
33    plugin: &'static dyn GamePlugin,
34) -> modde_core::installer::InstallProbe {
35    modde_core::installer::InstallProbe::new(
36        move |dir: &std::path::Path| plugin.analyze_mod_archive(dir),
37        move |dir: &std::path::Path| plugin.recognizes_bare_layout(dir),
38    )
39}
40
41/// All recognized game IDs, in the order they appear in the match table.
42pub const SUPPORTED_GAME_IDS: &[&str] = &[
43    "skyrim-se",
44    "skyrim-ae",
45    "fallout4",
46    "fallout76",
47    "starfield",
48    "cyberpunk2077",
49    "stellar-blade",
50];
51
52/// (game_id, display_name) for every supported game, derived from the plugin registry.
53pub fn supported_games() -> SmallVec<[(&'static str, &'static str); 8]> {
54    SUPPORTED_GAME_IDS
55        .iter()
56        .filter_map(|&id| resolve_game_plugin(id).map(|p| (p.game_id(), p.display_name())))
57        .collect()
58}
59
60/// Map a Wabbajack manifest `game` field (e.g. `"Cyberpunk2077"`, `"SkyrimSpecialEdition"`)
61/// to the internal game_id (e.g. `"cyberpunk2077"`, `"skyrim-se"`).
62///
63/// Returns `None` if the name is not recognized.
64pub fn normalize_wabbajack_game(wj_game: &str) -> Option<&'static str> {
65    match wj_game {
66        "Cyberpunk2077" => Some("cyberpunk2077"),
67        "SkyrimSpecialEdition" => Some("skyrim-se"),
68        "Fallout4" => Some("fallout4"),
69        "Fallout76" => Some("fallout76"),
70        "Starfield" => Some("starfield"),
71        _ => None,
72    }
73}
74
75/// Resolve a game_id string to the corresponding `GamePlugin` implementation.
76pub fn resolve_game_plugin(game_id: &str) -> Option<&'static dyn GamePlugin> {
77    match game_id {
78        "skyrim-se" => Some(&bethesda::SKYRIM_SE),
79        "skyrim-ae" => Some(&bethesda::SKYRIM_AE),
80        "fallout4" => Some(&bethesda::FALLOUT4),
81        "fallout76" => Some(&bethesda::FALLOUT76),
82        "starfield" => Some(&bethesda::STARFIELD),
83        "cyberpunk2077" => Some(&cyberpunk::CYBERPUNK2077),
84        "stellar-blade" => Some(&ue4::STELLAR_BLADE),
85        _ => None,
86    }
87}
88
89/// Resolve a game_id to its `ModScanner` implementation, if one exists.
90pub fn resolve_mod_scanner(game_id: &str) -> Option<&'static dyn ModScanner> {
91    match game_id {
92        "cyberpunk2077" => Some(&cyberpunk::scanner::CYBERPUNK_SCANNER),
93        "skyrim-se" | "skyrim-ae" => Some(&bethesda::scanner::SKYRIM_SCANNER),
94        "fallout4" => Some(&bethesda::scanner::FALLOUT4_SCANNER),
95        "starfield" => Some(&bethesda::scanner::STARFIELD_SCANNER),
96        "stellar-blade" => Some(&ue4::scanner::STELLAR_BLADE_SCANNER),
97        _ => None,
98    }
99}
100
101/// Resolve a game_id to its `CollisionClassifier` implementation, if one exists.
102pub fn resolve_collision_classifier(
103    game_id: &str,
104) -> Option<Box<dyn modde_core::collision::CollisionClassifier>> {
105    match game_id {
106        "skyrim-se" | "skyrim-ae" | "fallout4" | "fallout76" | "starfield" => {
107            Some(Box::new(bethesda::collision::BethesdaCollisionClassifier))
108        }
109        "cyberpunk2077" => Some(Box::new(cyberpunk::collision::CyberpunkCollisionClassifier)),
110        _ => None,
111    }
112}
113
114/// Resolve a game_id to its `SaveTracker` implementation, if one exists.
115pub fn resolve_save_tracker(game_id: &str) -> Option<&'static dyn SaveTracker> {
116    match game_id {
117        "skyrim-se" | "skyrim-ae" => Some(&bethesda::saves::SKYRIM_SAVE_TRACKER),
118        "fallout4" => Some(&bethesda::saves::FALLOUT4_SAVE_TRACKER),
119        // FO76 saves are server-side; local cache files are captured with a warning
120        "fallout76" => Some(&bethesda::saves::FALLOUT76_SAVE_TRACKER),
121        // Starfield: reuse Skyrim tracker as placeholder until save format is confirmed
122        "starfield" => Some(&bethesda::saves::SKYRIM_SAVE_TRACKER),
123        "cyberpunk2077" => Some(&cyberpunk::saves::CYBERPUNK_SAVE_TRACKER),
124        _ => None,
125    }
126}