pub mod backup;
pub mod collisions;
pub mod deploy;
pub mod detect;
pub mod diagnostics;
pub mod export;
pub mod fomod;
pub mod game;
pub mod import;
pub mod install;
pub mod instance;
pub mod loot;
pub mod nexus;
pub mod nix_schema;
pub mod nxm;
pub mod play;
pub mod profile;
pub mod rollback;
pub mod save;
pub mod scan;
pub mod skill;
pub mod stock;
pub mod tool;
pub mod uninstall;
pub mod update;
pub mod verify;
pub mod wabbajack;
use std::path::PathBuf;
use anyhow::Result;
use modde_core::PluginEntry;
use modde_core::profile::{Profile, ProfileManager};
use modde_core::resolver::GameId;
use modde_core::save::SaveFingerprint;
pub fn resolve_save_dir(game_id: &str) -> Option<PathBuf> {
let plugin = modde_games::resolve_game_plugin(game_id)?;
plugin
.supports_save_profiles()
.then(|| plugin.save_directory())
.flatten()
}
pub fn supports_save_profiles(game_id: &str) -> Result<bool> {
let plugin = modde_games::resolve_game_plugin(game_id).ok_or_else(|| {
anyhow::anyhow!(
"unknown game '{}'. Supported games: {}",
game_id,
modde_games::supported_game_ids().join(", ")
)
})?;
Ok(plugin.supports_save_profiles())
}
pub fn require_save_dir(game_id: &str) -> Result<PathBuf> {
if modde_games::resolve_game_plugin(game_id).is_none() {
anyhow::bail!(
"unknown game '{}'. Supported games: {}",
game_id,
modde_games::supported_game_ids().join(", ")
);
}
if !supports_save_profiles(game_id)? {
anyhow::bail!(
"save profiles are not supported for game '{game_id}'. \
This title does not use modde's per-profile save layer."
);
}
resolve_save_dir(game_id).ok_or_else(|| {
anyhow::anyhow!(
"save directory not found for game '{game_id}'. \
The game may not be installed, or save tracking is not supported for this title."
)
})
}
pub fn compute_fingerprint(
pm: &ProfileManager,
name: &str,
game_id: &str,
) -> Option<SaveFingerprint> {
if !supports_save_profiles(game_id).ok()? {
return None;
}
let profile = pm.load(name, Some(&GameId::from(game_id))).ok()?;
let game_plugin = modde_games::resolve_game_plugin(game_id)?;
let staging_dir = ProfileManager::staging_dir(&profile.name);
Some(SaveFingerprint::compute(&profile.mods, |mod_id| {
let mod_path = staging_dir.join(mod_id);
game_plugin.classify_mod(&mod_path).affects_saves()
}))
}
pub fn load_profile_or_default(
pm: &ProfileManager,
name: Option<&str>,
game_id: Option<&str>,
) -> Result<Profile> {
if let Some(name) = name {
Ok(pm.load(name, game_id.map(GameId::from).as_ref())?)
} else {
let profiles = pm.list()?;
let first = profiles.first().ok_or_else(|| {
anyhow::anyhow!(
"no profiles found. Create one with: modde profile create <name> --game <id>"
)
})?;
Ok(pm.load(&first.name, Some(&first.game_id))?)
}
}
pub fn load_plugin_order(pm: &ProfileManager, profile: &Profile) -> Result<Vec<PluginEntry>> {
let mut plugins = profile
.id
.map(|profile_id| pm.db().get_plugin_order(profile_id))
.transpose()?
.unwrap_or_default();
if plugins.is_empty() {
plugins =
modde_games::read_native_plugin_order(profile.game_id.as_str()).unwrap_or_default();
if !plugins.is_empty()
&& let Some(profile_id) = profile.id
{
pm.db().set_plugin_order(profile_id, &plugins)?;
}
}
Ok(plugins)
}
pub fn persist_plugin_order(
pm: &ProfileManager,
profile: &Profile,
plugins: &[PluginEntry],
) -> Result<()> {
if let Some(profile_id) = profile.id {
pm.db().set_plugin_order(profile_id, plugins)?;
}
if modde_games::resolve_game_plugin(profile.game_id.as_str())
.is_some_and(modde_games::GamePlugin::has_plugin_system)
{
modde_games::write_native_plugin_order(profile.game_id.as_str(), plugins)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn supports_save_profiles_errors_for_unknown_game() {
let err = supports_save_profiles("not-a-game").unwrap_err();
assert!(err.to_string().contains("unknown game 'not-a-game'"));
}
#[test]
fn stellar_blade_without_save_dir_reports_missing_directory() {
assert!(supports_save_profiles("stellar-blade").unwrap());
assert!(resolve_save_dir("stellar-blade").is_none());
let err = require_save_dir("stellar-blade").unwrap_err();
assert!(
err.to_string()
.contains("save directory not found for game 'stellar-blade'")
);
}
#[test]
fn enabled_game_without_save_dir_reports_missing_directory() {
assert!(supports_save_profiles("skyrim-se").unwrap());
if resolve_save_dir("skyrim-se").is_none() {
let err = require_save_dir("skyrim-se").unwrap_err();
assert!(
err.to_string()
.contains("save directory not found for game 'skyrim-se'")
);
}
}
}