diff --git a/src/actions.rs b/src/actions.rs new file mode 100644 index 0000000..c8cb2ea --- /dev/null +++ b/src/actions.rs @@ -0,0 +1,11 @@ +mod activate; +mod download; +mod include; +mod install; +mod load_order; + +pub use activate::{ActivationError, activate_instance}; +pub use download::handle_nxm; +pub use include::insert_mod_to_instance; +pub use install::resolve_files_for_install; +pub use load_order::{LoadOrderError, create_loadorder}; diff --git a/src/activator.rs b/src/actions/activate.rs similarity index 98% rename from src/activator.rs rename to src/actions/activate.rs index 4ebcb73..6cb0d7f 100644 --- a/src/activator.rs +++ b/src/actions/activate.rs @@ -7,6 +7,7 @@ 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, diff --git a/src/actions/download.rs b/src/actions/download.rs new file mode 100644 index 0000000..a3449c0 --- /dev/null +++ b/src/actions/download.rs @@ -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(()) +} diff --git a/src/actions/include.rs b/src/actions/include.rs new file mode 100644 index 0000000..b9ad635 --- /dev/null +++ b/src/actions/include.rs @@ -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 { + 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 = 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, +} diff --git a/src/instance.rs b/src/actions/install.rs similarity index 63% rename from src/instance.rs rename to src/actions/install.rs index b6a80b6..c567393 100644 --- a/src/instance.rs +++ b/src/actions/install.rs @@ -1,79 +1,19 @@ use std::{ - collections::HashMap, io, path::{Path, PathBuf}, }; use globset::{Glob, GlobSet, GlobSetBuilder}; -use log::{debug, trace, warn}; -use thiserror::Error; +use log::{debug, trace}; use crate::{ - file_conflict_solver::ConflictSolver, fomod, install_prompt, mod_config_installer::run_fomod_installer, - types::{InstalledMod, ModConfig, ModFile, ModdedInstance, RootConfig}, + types::{ModConfig, ModFile, ModdedInstance, RootConfig}, utils::{resolve_case_insensitive, walk_all_files}, }; -pub fn insert_mod_to_instance( - instance: &mut ModdedInstance, - from_mod: &ModConfig, - files: &[ModFile], - priority: isize, -) -> Result<(), InststanceError> { - 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 { - if let Some(conflict) = solver.add_file(file, &new_mod) { - return Err(InststanceError::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 = 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); - } - - Ok(()) -} - -pub fn files_to_install_mod( +pub fn resolve_files_for_install( root_config: &RootConfig, instance: &ModdedInstance, mod_to_install: &ModConfig, @@ -126,14 +66,11 @@ fn install_fomod( .collect(); trace!("Current loded plugins: {:?}", active_plugins); - let files = run_fomod_installer(module_config, &active_plugins, install_prompt::prompt) - .map_err(|_| InststanceError::FomodRunInstaller)?; + 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).map_err(InststanceError::FomodFinalize) - }) + .map(|f| ModFile::from_installer(f.clone(), &mod_root)) .collect::, _>>()? .into_iter() .flatten() @@ -226,19 +163,3 @@ fn should_be_included(path: impl AsRef) -> bool { ) ) } - -#[derive(Debug, Error)] -pub enum InststanceError { - #[error("Two mods write the same file")] - FileConflict { - lhs_mod_id: String, - rhs_mod_id: String, - path: PathBuf, - }, - - #[error("Failed to run fomod installer")] - FomodRunInstaller, - - #[error("Failed to handle results of fomod installer")] - FomodFinalize(io::Error), -} diff --git a/src/load_order.rs b/src/actions/load_order.rs similarity index 96% rename from src/load_order.rs rename to src/actions/load_order.rs index 769af27..0d066e0 100644 --- a/src/load_order.rs +++ b/src/actions/load_order.rs @@ -10,7 +10,10 @@ use std::{ use thiserror::Error; use walkdir::WalkDir; -use crate::{types::{self, ModdedInstance, RootConfig}, utils::is_plugin_file}; +use crate::{ + types::{self, ModdedInstance, RootConfig}, + utils::is_plugin_file, +}; pub fn create_loadorder( root_config: &RootConfig, diff --git a/src/lib.rs b/src/lib.rs index 021b7de..8215709 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,12 +1,10 @@ -pub mod activator; pub mod cli; pub mod file_conflict_solver; pub mod fomod; pub mod install_prompt; -pub mod instance; -pub mod load_order; pub mod mod_config_installer; pub mod nexus; pub mod types; pub mod unpacker; pub mod utils; +pub mod actions; diff --git a/src/main.rs b/src/main.rs index 2d0efb6..7e222e7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,11 +4,12 @@ use log::{debug, error, info}; use std::{error::Error, path::Path}; use fomod_manager::{ - activator::activate_instance, + actions::{ + activate_instance, create_loadorder, handle_nxm, insert_mod_to_instance, + resolve_files_for_install, + }, cli::{self, Args}, - instance::{self, files_to_install_mod, insert_mod_to_instance}, - load_order, - nexus::{NXMUrl, NexusAPI, download_nxm}, + nexus::NexusAPI, types::RootConfig, unpacker::unpack, }; @@ -29,40 +30,23 @@ fn command_add(root_config: &RootConfig, instance_id: &str, mod_id: &str) -> any .mod_by_id(mod_id) .ok_or(anyhow!("Can't find mod in config"))?; - let files = files_to_install_mod(root_config, &instance, &mod_to_install)?; + let files = resolve_files_for_install(root_config, &instance, &mod_to_install)?; match insert_mod_to_instance(&mut instance, &mod_to_install, &files, 0) { - Ok(_) => { + None => { instance.save_to_file()?; Ok(()) } - Err(err) => { - match &err { - instance::InststanceError::FileConflict { - lhs_mod_id, - rhs_mod_id, - path, - } => { - error!( - "File conflict between {} and {} at {}", - lhs_mod_id, - rhs_mod_id, - path.to_string_lossy() - ); - info!("To resolve file conflicts give one mod a higher priority in the config"); - } - instance::InststanceError::FomodRunInstaller => { - error!("Failed to run FOMod installer"); - } - instance::InststanceError::FomodFinalize(error) => { - error!( - "FOMod installer finished but failed to finalize result: {}", - error - ); - } - }; + Some(conflict) => { + error!( + "File conflict between {} and {} at {}", + conflict.lhs_mod_id, + conflict.rhs_mod_id, + conflict.path.to_string_lossy() + ); + info!("To resolve file conflicts give one mod a higher priority in the config"); - Err(err.into()) + Err(anyhow!("File conflict")) } } } @@ -71,46 +55,15 @@ fn command_order(root_config: &RootConfig, instance_id: &str) -> anyhow::Result< let mut instance = root_config.load_instance_by_id(instance_id)?; let game = root_config.game_by_id(instance.game_id()).unwrap(); - let new_load_order = load_order::create_loadorder(root_config, &game, &instance)?; - + let new_load_order = create_loadorder(root_config, &game, &instance)?; instance.set_load_order(new_load_order); - instance.save_to_file()?; Ok(()) } fn command_download(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(()) + handle_nxm(root_config, raw_url) } fn command_unpack( diff --git a/tests/add_mod_test.rs b/tests/add_mod_test.rs index e09fe4e..c380dbb 100644 --- a/tests/add_mod_test.rs +++ b/tests/add_mod_test.rs @@ -1,7 +1,7 @@ use std::{collections::HashSet, error::Error, path::PathBuf}; use fomod_manager::{ - instance::{files_to_install_mod, insert_mod_to_instance}, + actions::{insert_mod_to_instance, resolve_files_for_install}, types::{Link, RootConfig}, }; @@ -20,9 +20,9 @@ fn add_plain() -> Result<(), Box> { let mod_to_install = root_config .mod_by_id("add_test_plain") .expect("Mod not found"); - let files_to_add = files_to_install_mod(&root_config, &instance, &mod_to_install)?; + let files_to_add = resolve_files_for_install(&root_config, &instance, &mod_to_install)?; - insert_mod_to_instance(&mut instance, &mod_to_install, &files_to_add, 0)?; + insert_mod_to_instance(&mut instance, &mod_to_install, &files_to_add, 0); let installed_mods = instance.mods(); @@ -53,9 +53,9 @@ fn add_nested() -> Result<(), Box> { let mod_to_install = root_config .mod_by_id("add_test_nested") .expect("Mod not found"); - let files_to_add = files_to_install_mod(&root_config, &instance, &mod_to_install)?; + let files_to_add = resolve_files_for_install(&root_config, &instance, &mod_to_install)?; - insert_mod_to_instance(&mut instance, &mod_to_install, &files_to_add, 0)?; + insert_mod_to_instance(&mut instance, &mod_to_install, &files_to_add, 0); let installed_mods = instance.mods(); @@ -86,9 +86,9 @@ fn add_root() -> Result<(), Box> { let mod_to_install = root_config .mod_by_id("add_test_root") .expect("Mod not found"); - let files_to_add = files_to_install_mod(&root_config, &instance, &mod_to_install)?; + let files_to_add = resolve_files_for_install(&root_config, &instance, &mod_to_install)?; - insert_mod_to_instance(&mut instance, &mod_to_install, &files_to_add, 0)?; + insert_mod_to_instance(&mut instance, &mod_to_install, &files_to_add, 0); let installed_mods = instance.mods(); @@ -117,9 +117,9 @@ fn add_filter() -> Result<(), Box> { let mod_to_install = root_config .mod_by_id("add_test_filter") .expect("Mod not found"); - let files_to_add = files_to_install_mod(&root_config, &instance, &mod_to_install)?; + let files_to_add = resolve_files_for_install(&root_config, &instance, &mod_to_install)?; - insert_mod_to_instance(&mut instance, &mod_to_install, &files_to_add, 0)?; + insert_mod_to_instance(&mut instance, &mod_to_install, &files_to_add, 0); let installed_mods = instance.mods();