use anyhow::{Context, Result};
use smallvec::SmallVec;
pub mod bannerlord;
pub mod bethesda;
pub mod bg3;
pub mod cyberpunk;
pub mod detection;
pub mod gamebryo;
pub mod generic;
pub mod launcher;
pub mod oblivion_remastered;
pub mod optiscaler;
pub mod policies;
pub mod registry;
pub mod save_patterns;
pub mod scanner_patterns;
pub mod stardew;
pub mod tools;
pub mod traits;
pub mod ue4;
pub mod witcher3;
pub use detection::{DetectedGame, LauncherSource, find_detected_game, scan_installed_games};
pub use generic::loader::{load_user_games, reload_user_games};
pub use generic::manage::{
AddUserGameResult, DetectCandidateDir, add_user_game, detect_candidates, read_user_game_spec,
remove_user_game,
};
pub use optiscaler::{
OptiScalerIniOverride, OptiScalerProfile, default_optiscaler_profile,
resolve_optiscaler_profiles,
};
pub use registry::{
EngineFamily, GameRegistration, LauncherIds, all_games, resolve_game, supported_game_ids,
};
pub use traits::{
DeployTarget, DeployTargetKind, DiscoveredFile, DiscoveredMod, GamePlugin, ModClassifyConfig,
ModSafety, ModScanner, ModSource, SaveTracker, ScanContext, classify_mod_by_content, slug,
walk_files_relative,
};
pub fn game_probe(plugin: &'static dyn GamePlugin) -> modde_core::installer::InstallProbe {
let mut probe = modde_core::installer::InstallProbe::new(
move |dir: &std::path::Path| plugin.analyze_mod_archive(dir),
move |dir: &std::path::Path| plugin.recognizes_bare_layout(dir),
);
if let Some(target) = plugin
.deploy_targets()
.iter()
.find(|t| t.kind == crate::traits::DeployTargetKind::UserConfig)
{
probe = probe.with_user_config_target(target.id);
}
probe
}
pub const SUPPORTED_GAME_IDS: &[&str] = registry::SUPPORTED_GAME_IDS;
#[must_use]
pub fn supported_games() -> SmallVec<[(&'static str, &'static str); 8]> {
registry::all_games()
.iter()
.map(|game| (game.game_id, game.display_name))
.collect()
}
#[must_use]
pub fn normalize_wabbajack_game(wj_game: &str) -> Option<&'static str> {
let key: String = wj_game
.chars()
.filter(char::is_ascii_alphanumeric)
.flat_map(char::to_lowercase)
.collect();
registry::all_games()
.iter()
.find(|game| game.normalized_wabbajack_names().any(|name| name == key))
.map(|game| game.game_id)
}
#[must_use]
pub fn resolve_game_plugin(game_id: &str) -> Option<&'static dyn GamePlugin> {
registry::resolve_game(game_id).map(|game| game.plugin)
}
#[must_use]
pub fn resolve_game_plugin_by_nexus_domain(domain: &str) -> Option<&'static dyn GamePlugin> {
registry::resolve_game_by_nexus_domain(domain).map(|game| game.plugin)
}
#[must_use]
pub fn resolve_mod_scanner(game_id: &str) -> Option<&'static dyn ModScanner> {
registry::resolve_game(game_id).and_then(|game| game.scanner)
}
#[must_use]
pub fn resolve_collision_classifier(
game_id: &str,
) -> Option<Box<dyn modde_core::collision::CollisionClassifier>> {
registry::resolve_game(game_id).and_then(|game| game.collision_classifier.map(|build| build()))
}
#[must_use]
pub fn resolve_save_tracker(game_id: &str) -> Option<&'static dyn SaveTracker> {
registry::resolve_game(game_id).and_then(|game| game.save_tracker)
}
#[must_use]
pub fn supports_save_profiles(game_id: &str) -> bool {
registry::resolve_game(game_id).is_some_and(|game| game.supports_save_profiles)
}
pub fn read_native_plugin_order(game_id: &str) -> Result<Vec<modde_core::PluginEntry>> {
let plugin = resolve_game_plugin(game_id)
.ok_or_else(|| anyhow::anyhow!("unsupported game '{game_id}'"))?;
let app_id = plugin
.steam_app_id_u32()
.ok_or_else(|| anyhow::anyhow!("game '{game_id}' does not expose plugins.txt"))?;
let folder = plugin
.plugins_txt_folder()
.ok_or_else(|| anyhow::anyhow!("game '{game_id}' does not expose plugins.txt"))?;
let entries = bethesda::plugins_txt::read_plugins_txt(app_id, folder)
.with_context(|| format!("failed to read plugins.txt for '{game_id}'"))?;
Ok(entries
.into_iter()
.enumerate()
.map(|(sort_index, entry)| modde_core::PluginEntry {
plugin_name: entry.name,
sort_index: sort_index as i64,
enabled: entry.enabled,
})
.collect())
}
pub fn write_native_plugin_order(game_id: &str, plugins: &[modde_core::PluginEntry]) -> Result<()> {
let plugin = resolve_game_plugin(game_id)
.ok_or_else(|| anyhow::anyhow!("unsupported game '{game_id}'"))?;
let app_id = plugin
.steam_app_id_u32()
.ok_or_else(|| anyhow::anyhow!("game '{game_id}' does not expose plugins.txt"))?;
let folder = plugin
.plugins_txt_folder()
.ok_or_else(|| anyhow::anyhow!("game '{game_id}' does not expose plugins.txt"))?;
let entries: Vec<bethesda::plugins_txt::PluginEntry> = plugins
.iter()
.map(|plugin| bethesda::plugins_txt::PluginEntry {
name: plugin.plugin_name.clone(),
enabled: plugin.enabled,
})
.collect();
bethesda::plugins_txt::write_plugins_txt(app_id, folder, &entries)
.with_context(|| format!("failed to write plugins.txt for '{game_id}'"))
}