1use anyhow::{Context, Result};
6use smallvec::SmallVec;
7
8pub mod bannerlord;
9pub mod bethesda;
10pub mod bg3;
11pub mod cyberpunk;
12pub mod detection;
13pub mod gamebryo;
14pub mod generic;
15pub mod launcher;
16pub mod oblivion_remastered;
17pub mod optiscaler;
18pub mod policies;
19pub mod registry;
20pub mod save_patterns;
21pub mod scanner_patterns;
22pub mod stardew;
23pub mod tools;
24pub mod traits;
25pub mod ue4;
26pub mod witcher3;
27
28pub use detection::{DetectedGame, LauncherSource, find_detected_game, scan_installed_games};
29pub use generic::loader::{load_user_games, reload_user_games};
30pub use generic::manage::{
31 AddUserGameResult, DetectCandidateDir, add_user_game, detect_candidates, read_user_game_spec,
32 remove_user_game,
33};
34pub use optiscaler::{
35 OptiScalerIniOverride, OptiScalerProfile, default_optiscaler_profile,
36 resolve_optiscaler_profiles,
37};
38pub use registry::{
39 EngineFamily, GameRegistration, LauncherIds, all_games, resolve_game, supported_game_ids,
40};
41pub use traits::{
42 DeployTarget, DeployTargetKind, DiscoveredFile, DiscoveredMod, GamePlugin, ModClassifyConfig,
43 ModSafety, ModScanner, ModSource, SaveTracker, ScanContext, classify_mod_by_content, slug,
44 walk_files_relative,
45};
46
47pub fn game_probe(plugin: &'static dyn GamePlugin) -> modde_core::installer::InstallProbe {
61 let mut probe = modde_core::installer::InstallProbe::new(
62 move |dir: &std::path::Path| plugin.analyze_mod_archive(dir),
63 move |dir: &std::path::Path| plugin.recognizes_bare_layout(dir),
64 );
65 if let Some(target) = plugin
68 .deploy_targets()
69 .iter()
70 .find(|t| t.kind == crate::traits::DeployTargetKind::UserConfig)
71 {
72 probe = probe.with_user_config_target(target.id);
73 }
74 probe
75}
76
77pub const SUPPORTED_GAME_IDS: &[&str] = registry::SUPPORTED_GAME_IDS;
79
80#[must_use]
82pub fn supported_games() -> SmallVec<[(&'static str, &'static str); 8]> {
83 registry::all_games()
84 .iter()
85 .map(|game| (game.game_id, game.display_name))
86 .collect()
87}
88
89#[must_use]
94pub fn normalize_wabbajack_game(wj_game: &str) -> Option<&'static str> {
95 let key: String = wj_game
96 .chars()
97 .filter(char::is_ascii_alphanumeric)
98 .flat_map(char::to_lowercase)
99 .collect();
100
101 registry::all_games()
102 .iter()
103 .find(|game| game.normalized_wabbajack_names().any(|name| name == key))
104 .map(|game| game.game_id)
105}
106
107#[must_use]
109pub fn resolve_game_plugin(game_id: &str) -> Option<&'static dyn GamePlugin> {
110 registry::resolve_game(game_id).map(|game| game.plugin)
111}
112
113#[must_use]
120pub fn resolve_game_plugin_by_nexus_domain(domain: &str) -> Option<&'static dyn GamePlugin> {
121 registry::resolve_game_by_nexus_domain(domain).map(|game| game.plugin)
122}
123
124#[must_use]
126pub fn resolve_mod_scanner(game_id: &str) -> Option<&'static dyn ModScanner> {
127 registry::resolve_game(game_id).and_then(|game| game.scanner)
128}
129
130#[must_use]
132pub fn resolve_collision_classifier(
133 game_id: &str,
134) -> Option<Box<dyn modde_core::collision::CollisionClassifier>> {
135 registry::resolve_game(game_id).and_then(|game| game.collision_classifier.map(|build| build()))
136}
137
138#[must_use]
140pub fn resolve_save_tracker(game_id: &str) -> Option<&'static dyn SaveTracker> {
141 registry::resolve_game(game_id).and_then(|game| game.save_tracker)
142}
143
144#[must_use]
146pub fn supports_save_profiles(game_id: &str) -> bool {
147 registry::resolve_game(game_id).is_some_and(|game| game.supports_save_profiles)
148}
149
150pub fn read_native_plugin_order(game_id: &str) -> Result<Vec<modde_core::PluginEntry>> {
152 let plugin = resolve_game_plugin(game_id)
153 .ok_or_else(|| anyhow::anyhow!("unsupported game '{game_id}'"))?;
154 let app_id = plugin
155 .steam_app_id_u32()
156 .ok_or_else(|| anyhow::anyhow!("game '{game_id}' does not expose plugins.txt"))?;
157 let folder = plugin
158 .plugins_txt_folder()
159 .ok_or_else(|| anyhow::anyhow!("game '{game_id}' does not expose plugins.txt"))?;
160
161 let entries = bethesda::plugins_txt::read_plugins_txt(app_id, folder)
162 .with_context(|| format!("failed to read plugins.txt for '{game_id}'"))?;
163
164 Ok(entries
165 .into_iter()
166 .enumerate()
167 .map(|(sort_index, entry)| modde_core::PluginEntry {
168 plugin_name: entry.name,
169 sort_index: sort_index as i64,
170 enabled: entry.enabled,
171 })
172 .collect())
173}
174
175pub fn write_native_plugin_order(game_id: &str, plugins: &[modde_core::PluginEntry]) -> Result<()> {
177 let plugin = resolve_game_plugin(game_id)
178 .ok_or_else(|| anyhow::anyhow!("unsupported game '{game_id}'"))?;
179 let app_id = plugin
180 .steam_app_id_u32()
181 .ok_or_else(|| anyhow::anyhow!("game '{game_id}' does not expose plugins.txt"))?;
182 let folder = plugin
183 .plugins_txt_folder()
184 .ok_or_else(|| anyhow::anyhow!("game '{game_id}' does not expose plugins.txt"))?;
185
186 let entries: Vec<bethesda::plugins_txt::PluginEntry> = plugins
187 .iter()
188 .map(|plugin| bethesda::plugins_txt::PluginEntry {
189 name: plugin.plugin_name.clone(),
190 enabled: plugin.enabled,
191 })
192 .collect();
193
194 bethesda::plugins_txt::write_plugins_txt(app_id, folder, &entries)
195 .with_context(|| format!("failed to write plugins.txt for '{game_id}'"))
196}