use std::{ collections::HashMap, io, path::{Path, PathBuf}, }; use globset::{Glob, GlobSet, GlobSetBuilder}; use log::{debug, trace, warn}; use thiserror::Error; use crate::{ file_conflict_solver::ConflictSolver, fomod, install_prompt, mod_config_installer::run_fomod_installer, types::{InstalledMod, 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( root_config: &RootConfig, instance: &ModdedInstance, mod_to_install: &ModConfig, ) -> anyhow::Result> { 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.join(data_path))? } ModKind::Root => install_from_dir(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, ) -> Result { 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, mod_root: impl AsRef, ) -> anyhow::Result> { 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) .map_err(|_| InststanceError::FomodRunInstaller)?; let mod_files: Vec<_> = files .iter() .map(|f| { ModFile::from_installer(f.clone(), &mod_root).map_err(InststanceError::FomodFinalize) }) .collect::, _>>()? .into_iter() .flatten() .collect(); Ok(mod_files) } fn install_from_dir( mod_config: &ModConfig, mod_location: impl AsRef, ) -> anyhow::Result> { 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, ) -> anyhow::Result> { let glob_filter = create_glob_filter(mod_config.ignore())?; let data = PathBuf::from("Data"); let files: Vec = 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 { 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) -> 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" ) ) } #[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), }