refactored actions to own files

This commit is contained in:
2026-03-18 13:23:58 +01:00
parent 9e3bdeacc6
commit 281327d69c
9 changed files with 161 additions and 162 deletions

171
src/actions/activate.rs Normal file
View File

@@ -0,0 +1,171 @@
use log::{debug, trace};
use thiserror::Error;
use crate::types::{Game, Link, ModdedInstance, RootConfig};
use std::collections::HashMap;
use std::io::Write;
use std::path::PathBuf;
use std::{fs, io, os::unix, path::Path};
/// Create the symlinks for a instance in a given directory
pub fn activate_instance(
root_config: &RootConfig,
instance: &ModdedInstance,
target: impl AsRef<Path>,
) -> Result<(), ActivationError> {
let game = root_config
.game_by_id(instance.game_id())
.ok_or(ActivationError::GameNotFound)?;
check_target_valid(&target)?;
let resolved_links = resolve_links(root_config, instance, &game)?;
resolved_links
.iter()
.try_for_each(|link| apply_link(link, &target))
.map_err(ActivationError::Linking)?;
create_plugins_txt(instance, target.as_ref()).map_err(ActivationError::PluginsTXT)?;
debug!("Finished activating instance");
Ok(())
}
fn resolve_links(
root_config: &RootConfig,
instance: &ModdedInstance,
game: &Game,
) -> Result<Vec<Link>, ActivationError> {
let game_links = game
.export_links()
.map_err(ActivationError::ExportGameLinks)?;
let mod_links = resolve_link_for_instance(root_config, instance)?;
let overrides: Vec<Link> = instance.game_file_overrides().to_owned();
let mut map: HashMap<PathBuf, PathBuf> = HashMap::new();
for link in game_links.into_iter().chain(mod_links).chain(overrides) {
map.insert(link.dst().to_owned(), link.src().to_owned());
}
let final_links: Vec<Link> = map
.into_iter()
.map(|(dst, src)| Link::new(src, dst))
.collect();
Ok(final_links)
}
fn resolve_link_for_instance(
root_config: &RootConfig,
instance: &ModdedInstance,
) -> Result<Vec<Link>, ActivationError> {
debug!("Resolving links for instance");
let mut links: Vec<Link> = Vec::new();
for installed_mod in instance.mods() {
let mod_config = root_config
.mod_by_id(installed_mod.mod_id())
.ok_or(ActivationError::ModNotFound)?;
let mod_source_root = root_config.mod_location().join(mod_config.path());
for link in installed_mod.files() {
let link_target = mod_source_root.join(link.src());
links.push(Link::new(link_target, link.dst()));
}
}
Ok(links)
}
fn apply_link(link: &Link, target: impl AsRef<Path>) -> Result<(), io::Error> {
let link_target = &link.src();
let link_name = target.as_ref().join(link.dst());
link_file(link_target, &link_name)
}
fn create_plugins_txt(
instance: &ModdedInstance,
target: impl AsRef<Path>,
) -> Result<(), io::Error> {
debug!("Generating Plugins.txt");
let mut file = fs::File::create(target.as_ref().join("Plugins.txt"))?;
writeln!(file, "# Auto generated. DO NOT EDIT MANUALLY!")?;
for plugin in instance.load_order() {
writeln!(file, "*{}", plugin)?;
}
Ok(())
}
fn link_file(target: &Path, link_name: &Path) -> Result<(), io::Error> {
if let Some(parent) = link_name.parent() {
fs::create_dir_all(parent)?;
}
create_symlink_for_file(target, link_name)?;
Ok(())
}
fn create_symlink_for_file(target: &Path, link_name: &Path) -> io::Result<()> {
trace!(
"Creating symlink at {} with target {}",
link_name.to_string_lossy(),
target.to_string_lossy()
);
#[cfg(unix)]
{
unix::fs::symlink(target, link_name)
}
#[cfg(windows)]
{
std::os::windows::fs::symlink_file(target, link_name)
}
}
fn check_target_valid(target: impl AsRef<Path>) -> Result<(), ActivationError> {
match fs::read_dir(&target) {
Ok(entries) => {
if entries.count() == 0 {
debug!(
"Target {} is a valid target",
&target.as_ref().to_string_lossy()
);
Ok(())
} else {
Err(ActivationError::TargetNotEmpty)
}
}
Err(e) => Err(ActivationError::TargetNotValid(e)),
}
}
#[derive(Debug, Error)]
pub enum ActivationError {
#[error("Could not find game in instance config")]
GameNotFound,
#[error("Could not find mod in instance config")]
ModNotFound,
#[error("The target is not empty")]
TargetNotEmpty,
#[error("The target is not valid")]
TargetNotValid(io::Error),
#[error("Failed to create symlink")]
Linking(io::Error),
#[error("Failed to create Plugins.txt")]
PluginsTXT(io::Error),
#[error("Failed to export files in game installation")]
ExportGameLinks(io::Error),
}

42
src/actions/download.rs Normal file
View File

@@ -0,0 +1,42 @@
use anyhow::anyhow;
use log::error;
use crate::{
nexus::{NXMUrl, download_nxm},
types::RootConfig,
unpacker::unpack,
};
/// Handles a nexus mod url. Downloads, unpacks and adds the mod to the config
pub fn handle_nxm(root_config: &mut RootConfig, raw_url: &str) -> anyhow::Result<()> {
let Some(dl_location) = root_config.download_location() else {
return Err(anyhow!("No download location set"));
};
let Some(api_key) = root_config.nexus_api_key() else {
return Err(anyhow!("No API key provided"));
};
let Some(nxm_url) = NXMUrl::parse_url(raw_url) else {
return Err(anyhow!("Failed to parse URL"));
};
let (dl_file, mod_info) = download_nxm(api_key, &nxm_url, dl_location)?;
let mod_id = format!("{}-{}", mod_info.generate_id(), nxm_url.file);
if root_config.game_by_id(&mod_id).is_some() {
error!(
"Generated mod id already exists. Pleas install downloaded mod manually. Downloaded at {}",
&dl_file.to_string_lossy()
);
return Err(anyhow!("Mod with generated id already exists"));
}
let new_mod = unpack(root_config, &mod_id, dl_file)?;
root_config.add_mod(&new_mod);
root_config.save_to_file()?;
Ok(())
}

70
src/actions/include.rs Normal file
View File

@@ -0,0 +1,70 @@
use log::warn;
use std::{collections::HashMap, path::PathBuf};
use crate::{
file_conflict_solver::ConflictSolver,
types::{InstalledMod, ModConfig, ModFile, ModdedInstance},
};
pub fn insert_mod_to_instance(
instance: &mut ModdedInstance,
from_mod: &ModConfig,
files_to_add: &[ModFile],
priority: isize,
) -> Option<FileConflict> {
let mut solver = ConflictSolver::new();
let mut installed_files: Vec<(ModFile, &InstalledMod)> = Vec::new();
for installed_mod in instance.mods() {
for link in installed_mod.files() {
let recreated_mod_file = ModFile::new(link.src(), link.dst(), 0);
installed_files.push((recreated_mod_file, installed_mod));
}
}
for (file, from_mod) in &installed_files {
if let Some(conflict) = solver.add_file(file, from_mod) {
warn!("File conflict on already added file: {:?}", conflict);
}
}
let new_mod = InstalledMod::new(from_mod.id(), priority);
for file in files_to_add {
if let Some(conflict) = solver.add_file(file, &new_mod) {
return Some(FileConflict {
lhs_mod_id: conflict.lhs_mod.mod_id().to_owned(),
rhs_mod_id: conflict.rhs_mod.mod_id().to_owned(),
path: conflict.rhs_file.dst().to_owned(),
});
}
}
let new_link_tree = solver.export_files();
let mut map: HashMap<String, InstalledMod> = HashMap::new();
for (file, from_mod) in new_link_tree {
match map.get_mut(from_mod.mod_id()) {
Some(existing) => {
existing.add_file(file);
}
None => {
let mut new_mod = InstalledMod::new(from_mod.mod_id(), from_mod.priority());
new_mod.add_file(file);
map.insert(new_mod.mod_id().to_owned(), new_mod);
}
}
}
for (_, installed_mod) in map {
instance.update_or_create_mod(&installed_mod);
}
None
}
pub struct FileConflict {
pub lhs_mod_id: String,
pub rhs_mod_id: String,
pub path: PathBuf,
}

165
src/actions/install.rs Normal file
View File

@@ -0,0 +1,165 @@
use std::{
io,
path::{Path, PathBuf},
};
use globset::{Glob, GlobSet, GlobSetBuilder};
use log::{debug, trace};
use crate::{
fomod, install_prompt,
mod_config_installer::run_fomod_installer,
types::{ModConfig, ModFile, ModdedInstance, RootConfig},
utils::{resolve_case_insensitive, walk_all_files},
};
pub fn resolve_files_for_install(
root_config: &RootConfig,
instance: &ModdedInstance,
mod_to_install: &ModConfig,
) -> anyhow::Result<Vec<ModFile>> {
let mod_location = root_config.mod_location().join(mod_to_install.path());
let files = match determain_mod_kind(mod_to_install, &mod_location)? {
ModKind::Fomod(xml_path) => install_fomod(instance, xml_path, &mod_location)?,
ModKind::EmbeddedData(_data_path) => install_from_dir(mod_to_install, mod_location)?,
ModKind::Root => install_root(mod_to_install, mod_location)?,
ModKind::Unkown => install_from_dir_to_data(mod_to_install, mod_location)?,
};
Ok(files)
}
fn determain_mod_kind(
mod_config: &ModConfig,
mod_location: impl AsRef<Path>,
) -> Result<ModKind, io::Error> {
if mod_config.is_root_mod() {
return Ok(ModKind::Root);
}
// Check for moduleconfig.xml
let module_config_path = resolve_case_insensitive(&mod_location, "fomod/ModuleConfig.xml")?;
if let Some(path) = module_config_path {
return Ok(ModKind::Fomod(path));
};
match resolve_case_insensitive(&mod_location, "data")? {
Some(data_path) => Ok(ModKind::EmbeddedData(data_path)),
None => Ok(ModKind::Unkown),
}
}
fn install_fomod(
instance: &ModdedInstance,
module_config_path: impl AsRef<Path>,
mod_root: impl AsRef<Path>,
) -> anyhow::Result<Vec<ModFile>> {
debug!("Running FOmod installer");
let module_config = fomod::Config::load_from_file(module_config_path)?;
let active_plugins: Vec<_> = instance
.active_plugins()
.map(|e| e.to_string_lossy())
.map(|e| e.to_string())
.collect();
trace!("Current loded plugins: {:?}", active_plugins);
let files = run_fomod_installer(module_config, &active_plugins, install_prompt::prompt)?;
let mod_files: Vec<_> = files
.iter()
.map(|f| ModFile::from_installer(f.clone(), &mod_root))
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.flatten()
.collect();
Ok(mod_files)
}
fn install_from_dir(
mod_config: &ModConfig,
mod_location: impl AsRef<Path>,
) -> anyhow::Result<Vec<ModFile>> {
let glob_filter = create_glob_filter(mod_config.ignore())?;
let files: Vec<_> = walk_all_files(&mod_location)?
.map(|entry| entry.path().strip_prefix(&mod_location).unwrap().to_owned())
.filter(|rel_path| !glob_filter.is_match(rel_path))
.filter(|rel_path| should_be_included(rel_path))
.map(|rel_path| ModFile::new(&rel_path, &rel_path, 0))
.collect();
Ok(files)
}
fn install_root(
mod_config: &ModConfig,
mod_location: impl AsRef<Path>,
) -> anyhow::Result<Vec<ModFile>> {
let glob_filter = create_glob_filter(mod_config.ignore())?;
let files: Vec<_> = walk_all_files(&mod_location)?
.map(|entry| entry.path().strip_prefix(&mod_location).unwrap().to_owned())
.filter(|rel_path| !glob_filter.is_match(rel_path))
.map(|rel_path| ModFile::new(&rel_path, &rel_path, 0))
.collect();
Ok(files)
}
fn install_from_dir_to_data(
mod_config: &ModConfig,
path: impl AsRef<Path>,
) -> anyhow::Result<Vec<ModFile>> {
let glob_filter = create_glob_filter(mod_config.ignore())?;
let data = PathBuf::from("Data");
let files: Vec<ModFile> = walk_all_files(&path)?
.map(|entry| entry.path().strip_prefix(&path).unwrap().to_owned())
.filter(|rel_path| !glob_filter.is_match(rel_path))
.filter(|rel_path| should_be_included(rel_path))
.map(|rel_path| ModFile::new(&rel_path, data.join(&rel_path), 0))
.collect();
Ok(files)
}
fn create_glob_filter(rules: &[String]) -> anyhow::Result<GlobSet> {
let mut builder = GlobSetBuilder::new();
for p in rules {
builder.add(Glob::new(p)?);
}
let set = builder.build()?;
Ok(set)
}
enum ModKind {
Fomod(PathBuf),
EmbeddedData(PathBuf),
Root,
Unkown,
}
fn should_be_included(path: impl AsRef<Path>) -> bool {
matches!(
path.as_ref().extension().and_then(|e| e.to_str()),
Some(
"esp"
| "esm"
| "esl"
| "bsa"
| "ba2"
| "bsl"
| "ini"
| "pex"
| "psc"
| "strings"
| "ilstrings"
| "dlstrings"
| "dll"
)
)
}

95
src/actions/load_order.rs Normal file
View File

@@ -0,0 +1,95 @@
use libloot::{
Game, GameType,
error::{GameHandleCreationError, LoadPluginsError, SortPluginsError},
};
use log::trace;
use std::{
io,
path::{Path, PathBuf},
};
use thiserror::Error;
use walkdir::WalkDir;
use crate::{
types::{self, ModdedInstance, RootConfig},
utils::is_plugin_file,
};
pub fn create_loadorder(
root_config: &RootConfig,
game: &types::Game,
instance: &ModdedInstance,
) -> Result<Vec<String>, LoadOrderError> {
let mut loot_game = Game::with_local_path(
GameType::SkyrimSE,
game.install_location(),
&game.install_location().join(PathBuf::from("appdata")),
)?;
// Add plugins files from the game install
let install_plugins: Vec<PathBuf> = WalkDir::new(game.install_location().join("Data"))
.into_iter()
.map(|entry| {
let entry = entry?;
let path = entry.path();
if is_plugin_file(path) {
Ok(Some(path.to_path_buf()))
} else {
Ok(None)
}
})
.filter_map(|r| r.transpose())
.collect::<Result<_, io::Error>>()?;
// The loaded_plugins function requires &[&Path]
let refs: Vec<&Path> = install_plugins.iter().map(|e| e.as_path()).collect();
trace!("Loading {} plugins to game", refs.len());
loot_game.load_plugins(&refs)?;
// Add plugins from the instance
let instance_plugins: Vec<_> = instance
.mods()
.iter()
.flat_map(|installed_mod| {
let mod_config = root_config.mod_by_id(installed_mod.mod_id()).unwrap();
let mod_source_root = root_config.mod_location().join(mod_config.path());
installed_mod
.files()
.iter()
.filter(|f| is_plugin_file(f.dst()))
.map(move |link| mod_source_root.join(link.src()))
})
.collect();
let refs: Vec<_> = instance_plugins.iter().map(|e| e.as_path()).collect();
trace!("Loading {} plugins to game", refs.len());
loot_game.load_plugins(&refs)?;
// Genrate load order
let all_plugins = loot_game.loaded_plugins();
let plugins_names: Vec<&str> = all_plugins.iter().map(|e| e.name()).collect();
let sorted = loot_game.sort_plugins(&plugins_names)?;
Ok(sorted)
}
#[derive(Error, Debug)]
pub enum LoadOrderError {
#[error("Failed to read game directory")]
Io(#[from] io::Error),
#[error("Failed to create libloot game")]
Creating(#[from] GameHandleCreationError),
#[error("Failed to add plugins to libloot")]
Loading(#[from] LoadPluginsError),
#[error("libloot failed to sort plugins")]
Sorting(#[from] SortPluginsError),
}