From dc41f93ecb8bf48bfa9cb0f3bd9d2d45b0fe6248 Mon Sep 17 00:00:00 2001 From: Niklas Kapelle Date: Wed, 4 Mar 2026 15:17:55 +0100 Subject: [PATCH] the big refactor --- Cargo.lock | 7 + Cargo.toml | 1 + src/activator.rs | 123 ++++++++ src/basic_types.rs | 66 ++--- ...ct_resolver.rs => file_conflict_solver.rs} | 22 +- src/instance.rs | 178 ++++++++++++ src/linker.rs | 71 ----- src/load_order.rs | 102 +++---- src/main.rs | 268 +++--------------- src/mod_config_installer.rs | 104 +++---- 10 files changed, 472 insertions(+), 470 deletions(-) create mode 100644 src/activator.rs rename src/{conflict_resolver.rs => file_conflict_solver.rs} (86%) create mode 100644 src/instance.rs delete mode 100644 src/linker.rs diff --git a/Cargo.lock b/Cargo.lock index c24edb1..4f45f2b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -67,6 +67,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + [[package]] name = "arraydeque" version = "0.5.1" @@ -309,6 +315,7 @@ checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" name = "fomod-manager" version = "0.1.0" dependencies = [ + "anyhow", "clap", "env_logger", "globset", diff --git a/Cargo.toml b/Cargo.toml index 9d46d0e..c6cf405 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] +anyhow = "1.0.102" clap = { version = "4.5.60", features = ["derive"] } env_logger = "0.11.9" globset = "0.4.18" diff --git a/src/activator.rs b/src/activator.rs new file mode 100644 index 0000000..3fdb8ad --- /dev/null +++ b/src/activator.rs @@ -0,0 +1,123 @@ +use anyhow::{Result, anyhow}; +use log::{debug, trace}; + +use crate::basic_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}; + +pub fn activate_instance( + root_config: &RootConfig, + instance: &ModdedInstance, + target: impl AsRef, +) -> Result<()> { + // TODO: Resolve game for instance config + let game = root_config + .games + .first() + .ok_or(anyhow!("TODO: resolve game from config"))?; + + let resolved_links = resolve_links(root_config, instance, game)?; + + resolved_links + .iter() + .try_for_each(|link| apply_link(link, target.as_ref())); + create_plugins_txt(&instance, target.as_ref())?; + + todo!() +} + +fn resolve_links( + root_config: &RootConfig, + instance: &ModdedInstance, + game: &Game, +) -> anyhow::Result> { + let game_links = game.export_links()?; + let mod_links = resolve_link_for_instance(root_config, instance)?; + let overrides = instance.game_file_overrides().to_owned(); + + let mut map: HashMap = HashMap::new(); + + for link in game_links.into_iter().chain(mod_links).chain(overrides) { + map.insert(link.dst, link.src); + } + + let final_links: Vec = map + .into_iter() + .map(|(dst, src)| Link::new(src, dst)) + .collect(); + + Ok(final_links) +} + +fn resolve_link_for_instance( + root_config: &RootConfig, + instance: &ModdedInstance, +) -> anyhow::Result> { + let mut links: Vec = Vec::new(); + + for installed_mod in &instance.mods { + let mod_config = root_config.get_mod_by_id(&installed_mod.mod_id()).unwrap(); + let mod_source_root = root_config.get_mod_location(&mod_config); + + 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) -> 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, +) -> 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<()> { + let absolute_path = fs::canonicalize(target)?; + + trace!( + "Creating symlink at {} with target {}", + link_name.to_string_lossy(), + target.to_string_lossy() + ); + + #[cfg(unix)] + { + unix::fs::symlink(absolute_path, link_name) + } + + #[cfg(windows)] + { + std::os::windows::fs::symlink_file(target, link_name) + } +} diff --git a/src/basic_types.rs b/src/basic_types.rs index 0151555..b337d59 100644 --- a/src/basic_types.rs +++ b/src/basic_types.rs @@ -8,13 +8,12 @@ use std::{ use thiserror::Error; use crate::{ - conflict_resolver::ConflictSolver, fomod::{FileType, FileTypeEnum}, utils::walk_files_recursive, }; /// A link between a file from a mod and a destination in a ModdedInstance -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)] #[serde(from = "(PathBuf, PathBuf)", into = "(PathBuf,PathBuf)")] pub struct Link { pub src: PathBuf, @@ -49,6 +48,12 @@ impl From for (PathBuf, PathBuf) { } } +impl From for Link { + fn from(value: ModFile) -> Self { + Self::new(value.source, value.dest) + } +} + #[derive(Debug, Clone, Deserialize, PartialEq)] pub struct RootConfig { /// Available games @@ -209,46 +214,6 @@ impl ModdedInstance { Ok(()) } - pub fn add_mod(&mut self, from_mod: &ModConfig, priority: isize, files: &[ModFile]) { - trace!("Adding mod to instance"); - - let mut new_mod = InstalledMod::new(from_mod, priority); - let mut solver = ConflictSolver::new(); - - // Add all the files form the instance. Unchecked because it is already checked. - let mut already_installed_files: Vec<(ModFile, &InstalledMod)> = Vec::new(); - - for installed_mod in &self.mods { - for link in &installed_mod.files { - already_installed_files - .push((ModFile::new(&link.src, &link.dst, 1), installed_mod)); - } - } - - trace!("Adding already present files to confict solver"); - for (present_file, present_mod) in &already_installed_files { - solver.add_file_unchecked(present_file, present_mod); - } - - trace!("Adding file from mod to confict solver"); - // Now add the new files and check for conflicts - for file in files { - if let Some(conflict) = solver.add_file(file, &new_mod) { - // FIXME: Find a way to display conflict to user - println!("{:?}", conflict); - panic!("Conflict") - } - } - - trace!("No conflicts where found"); - // No conflicts. Add files. - for file in files { - new_mod.add_file(file); - } - - self.mods.push(new_mod); - } - pub fn set_load_order(&mut self, order: Vec) { self.load_order = order; } @@ -260,9 +225,20 @@ impl ModdedInstance { pub fn game_file_overrides(&self) -> &[Link] { &self.game_file_overrides } + + pub fn update_or_create_mod(&mut self, installed_mod: &InstalledMod) { + match self.mods.iter_mut().find(|e| e.id == installed_mod.id) { + Some(existing) => { + *existing = installed_mod.to_owned(); + } + None => { + self.mods.push(installed_mod.to_owned()); + } + } + } } -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)] pub struct InstalledMod { id: String, files: Vec, @@ -270,9 +246,9 @@ pub struct InstalledMod { } impl InstalledMod { - pub fn new(from_mod: &ModConfig, priority: isize) -> Self { + pub fn new(root_mod_id: &str, priority: isize) -> Self { Self { - id: from_mod.id.clone(), + id: root_mod_id.to_owned(), files: Vec::new(), priority, } diff --git a/src/conflict_resolver.rs b/src/file_conflict_solver.rs similarity index 86% rename from src/conflict_resolver.rs rename to src/file_conflict_solver.rs index 415a48a..ef224ce 100644 --- a/src/conflict_resolver.rs +++ b/src/file_conflict_solver.rs @@ -1,12 +1,8 @@ use std::{collections::HashMap, path::PathBuf}; -use log::debug; +use log::{debug, trace}; -use crate::basic_types::{InstalledMod, ModFile}; - -pub struct ConflictSolver<'a> { - files: HashMap, -} +use crate::basic_types::{InstalledMod, ModConfig, ModFile, ModdedInstance}; #[derive(Debug)] pub struct Conflict<'a> { @@ -16,6 +12,10 @@ pub struct Conflict<'a> { lhs_file: &'a ModFile, } +pub struct ConflictSolver<'a> { + files: HashMap, +} + impl<'a> ConflictSolver<'a> { pub fn new() -> Self { Self { @@ -23,9 +23,9 @@ impl<'a> ConflictSolver<'a> { } } - pub fn add_file_unchecked(&mut self, file: &'a ModFile, from_mod: &'a InstalledMod) { - self.files.insert(file.destination(), (file, from_mod)); - } + // fn add_file_unchecked(&mut self, file: &'a ModFile, from_mod: &'a InstalledMod) { + // self.files.insert(file.destination(), (file, from_mod)); + // } pub fn add_file( &mut self, @@ -84,4 +84,8 @@ impl<'a> ConflictSolver<'a> { None } + + pub fn export_files(&self) -> Vec<(&'a ModFile, &'a InstalledMod)> { + self.files.iter().map(|e| e.1.to_owned()).collect() + } } diff --git a/src/instance.rs b/src/instance.rs new file mode 100644 index 0000000..b0de8ce --- /dev/null +++ b/src/instance.rs @@ -0,0 +1,178 @@ +use std::{ + collections::{HashMap, HashSet}, + io, + path::{Path, PathBuf}, +}; + +use globset::{Glob, GlobSet, GlobSetBuilder}; +use log::warn; + +use crate::{ + basic_types::{InstalledMod, Link, ModConfig, ModFile, ModdedInstance, RootConfig}, + file_conflict_solver::ConflictSolver, + fomod, install_prompt, + mod_config_installer::run_fomod_installer, + utils::{resolve_case_insensitive, walk_files_recursive}, +}; + +pub fn insert_mod_to_instance( + instance: &mut ModdedInstance, + from_mod: &ModConfig, + files: &[ModFile], + priority: isize, +) { + 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!("Got 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) { + // TODO: Return conflict + } + } + + 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(), new_mod); + } + } + } + + for (_, installed_mod) in map { + instance.update_or_create_mod(&installed_mod); + } +} + +pub fn files_to_install_mod( + root_config: &RootConfig, + instance: &ModdedInstance, + mod_to_install: &ModConfig, +) -> anyhow::Result> { + let mod_location = root_config.get_mod_location(mod_to_install); + + 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 => todo!(), + }; + + 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> { + let module_config = fomod::Config::load_from_file(module_config_path)?; + + // TODO: add active plugins from instance config + let files = run_fomod_installer(module_config, &[], install_prompt::prompt)?; + + let mod_files: Vec<_> = files + .iter() + .flat_map(|f| ModFile::from_installer(f.clone(), &mod_root).unwrap()) + .collect(); + + Ok(mod_files) +} + +fn install_from_dir( + mod_config: &ModConfig, + path: impl AsRef, +) -> anyhow::Result> { + let glob_filter = create_glob_filter(mod_config.ignore())?; + let files: Vec<_> = walk_files_recursive(&path)? + .map(|entry| entry.path()) + .map(|file_path| file_path.strip_prefix(&path).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 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" + ) + ) +} diff --git a/src/linker.rs b/src/linker.rs deleted file mode 100644 index 5f5ea02..0000000 --- a/src/linker.rs +++ /dev/null @@ -1,71 +0,0 @@ -use log::debug; - -use crate::basic_types::{Game, Link, ModdedInstance, RootConfig}; -use std::io::Write; -use std::{fs, io, os::unix, path::Path}; - -pub fn link_instance_to_target( - root_config: &RootConfig, - instance: &ModdedInstance, - target: impl AsRef, -) -> Result<(), io::Error> { - debug!("Linking instance to {}", target.as_ref().to_string_lossy()); - for installed_mod in &instance.mods { - let mod_config = root_config.get_mod_by_id(&installed_mod.mod_id()).unwrap(); - let mod_source_root = root_config.get_mod_location(&mod_config); - - for link in installed_mod.files() { - let link_target = mod_source_root.join(&link.src); - let link_name = target.as_ref().join(&link.dst); - link_file(&link_target, &link_name)?; - } - } - - Ok(()) -} - -pub fn apply_link(link: &Link, target: impl AsRef) -> Result<(), io::Error> { - let link_target = &link.src; - let link_name = target.as_ref().join(&link.dst); - - link_file(&link_target, &link_name) -} - -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(()) -} - -pub fn create_plugins_txt( - instance: &ModdedInstance, - target: impl AsRef, -) -> 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 create_symlink_for_file(target: &Path, link_name: &Path) -> io::Result<()> { - let absolute_path = fs::canonicalize(target)?; - - #[cfg(unix)] - { - unix::fs::symlink(absolute_path, link_name) - } - - #[cfg(windows)] - { - std::os::windows::fs::symlink_file(target, link_name) - } -} diff --git a/src/load_order.rs b/src/load_order.rs index 826be0d..a11c23e 100644 --- a/src/load_order.rs +++ b/src/load_order.rs @@ -1,87 +1,69 @@ -use std::{ - io, - path::{Path, PathBuf}, -}; - use libloot::{ Game, GameType, error::{GameHandleCreationError, LoadPluginsError, SortPluginsError}, }; use log::trace; +use std::{io, path::Path}; use thiserror::Error; use crate::{ - basic_types::{ModdedInstance, RootConfig}, + basic_types::{self, ModdedInstance, RootConfig}, utils::walk_files_recursive, }; -pub struct LoadOrder { - game: libloot::Game, - target: PathBuf, -} +pub fn create_loadorder( + root_config: &RootConfig, + game: &basic_types::Game, + instance: &ModdedInstance, +) -> Result, LoadOrderError> { + let mut loot_game = Game::new(GameType::SkyrimSE, &game.install_location)?; -impl LoadOrder { - pub fn new(install_dir: impl AsRef, game_type: GameType) -> Result { - Ok(Self { - game: Game::with_local_path( - game_type, - install_dir.as_ref(), - &install_dir.as_ref().join("appdata"), - )?, - target: install_dir.as_ref().to_owned(), - }) - } + // Add plugins files from the game install + let install_plugins: Vec<_> = walk_files_recursive(game.install_location.join("Data"))? + .filter(|f| is_plugin_file(f.path())) + .map(|f| f.path()) + .collect(); + let refs: Vec<_> = install_plugins.iter().map(|e| e.as_path()).collect(); - pub fn add_plugins_from_instance( - &mut self, - root_config: &RootConfig, - instance: &ModdedInstance, - ) -> Result<(), LoadOrderError> { - for installed_mod in &instance.mods { + 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.get_mod_by_id(&installed_mod.mod_id()).unwrap(); let mod_source_root = root_config.get_mod_location(&mod_config); - let mod_plugins: Vec = installed_mod + installed_mod .files() .iter() - .filter(|f| Self::is_plugin_file(&f.dst)) - .map(|link| mod_source_root.join(&link.src)) - .collect(); + .filter(|f| is_plugin_file(&f.dst)) + .map(move |link| mod_source_root.join(&link.src)) + }) + .collect(); - let refs: Vec<_> = mod_plugins.iter().map(|e| e.as_path()).collect(); - trace!("Loading {} plugins to game", refs.len()); - self.game.load_plugins(&refs)?; - } - Ok(()) - } + let refs: Vec<_> = instance_plugins.iter().map(|e| e.as_path()).collect(); - pub fn add_plugins_from_install(&mut self) -> Result<(), LoadOrderError> { - let plugins: Vec<_> = walk_files_recursive(self.target.join("Data"))? - .filter(|f| Self::is_plugin_file(f.path())) - .map(|f| f.path()) - .collect(); - let refs: Vec<_> = plugins.iter().map(|e| e.as_path()).collect(); - trace!("Loading {} plugins to game", refs.len()); - self.game.load_plugins(&refs)?; - Ok(()) - } + trace!("Loading {} plugins to game", refs.len()); - fn is_plugin_file(filename: impl AsRef) -> bool { - filename - .as_ref() - .extension() - .is_some_and(|ext| ext == "esp" || ext == "esm" || ext == "esl") - } + loot_game.load_plugins(&refs)?; - pub fn load_order(&self) -> Result, LoadOrderError> { - trace!("Generating new load order"); - let all_plugins = self.game.loaded_plugins(); - let plugins_names: Vec<&str> = all_plugins.iter().map(|e| e.name()).collect(); + // 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 = self.game.sort_plugins(&plugins_names)?; + let sorted = loot_game.sort_plugins(&plugins_names)?; - Ok(sorted) - } + Ok(sorted) +} + +fn is_plugin_file(filename: impl AsRef) -> bool { + filename + .as_ref() + .extension() + .is_some_and(|ext| ext == "esp" || ext == "esm" || ext == "esl") } #[derive(Error, Debug)] diff --git a/src/main.rs b/src/main.rs index 8d947ec..7dfbcce 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,253 +1,71 @@ +use anyhow::anyhow; use clap::Parser; -use globset::{Glob, GlobSet, GlobSetBuilder}; use log::debug; -use std::{ - collections::HashMap, - error::Error, - path::{Path, PathBuf}, -}; +use std::{error::Error, path::Path}; use crate::{ - basic_types::{Game, Link, ModConfig, ModFile, ModdedInstance, RootConfig}, + activator::activate_instance, + basic_types::RootConfig, cli::Args, - linker::{apply_link, create_plugins_txt}, - load_order::LoadOrder, - mod_config_installer::FomodInstaller, - utils::{resolve_case_insensitive, walk_files_recursive}, + instance::{files_to_install_mod, insert_mod_to_instance}, }; +mod activator; mod basic_types; mod cli; -mod conflict_resolver; +mod file_conflict_solver; mod fomod; mod install_prompt; -mod linker; +mod instance; mod load_order; mod mod_config_installer; mod utils; -pub fn gen_filelist_for_mod( - root_config: &RootConfig, - instance: &ModdedInstance, - mod_config: &ModConfig, -) -> Result, Box> { - let mod_location = root_config.get_mod_location(mod_config); - - if mod_config.is_root_mod() { - return gen_filelist_for_root_mod(mod_config, mod_location); - } - - let module_config_path = resolve_case_insensitive(&mod_location, "fomod/ModuleConfig.xml")?; - - let files: Vec = match module_config_path { - Some(path) => { - debug!("Found a ModuleConfig.xml"); - gen_filelist_from_fomod(instance, path, &mod_location)? - } - None => { - debug!("Did not find a ModuleConfig.xml"); - gen_filelist_from_mod_dir(&mod_location)? - } - }; - - Ok(files) -} - -fn create_glob_filter(rules: &[String]) -> Result> { - let mut builder = GlobSetBuilder::new(); - - for p in rules { - builder.add(Glob::new(p)?); - } - - let set = builder.build()?; - Ok(set) -} - -fn gen_filelist_for_root_mod( - mod_config: &ModConfig, - mod_location: impl AsRef, -) -> Result, Box> { - let glob_filter = create_glob_filter(mod_config.ignore())?; - let files: Vec<_> = walk_files_recursive(&mod_location)? - .map(|entry| entry.path()) - .map(|path| 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) -} - -pub fn gen_filelist_from_mod_dir( - mod_location: impl AsRef, -) -> Result, Box> { - // Check for Data dir in root - let files = match resolve_case_insensitive(&mod_location, "data")? { - Some(data_path) => gen_filelist_from_dir(data_path), - None => gen_filelist_from_dir(&mod_location), - }?; - - let mod_files: Vec<_> = files - .iter() - .filter(|e| should_be_included(e)) - .map(|e| ModFile::new(e, PathBuf::from("Data").join(e), 0)) - .collect(); - - Ok(mod_files) -} - -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" - ) - ) -} - -pub fn gen_filelist_from_dir(path: impl AsRef) -> Result, Box> { - let files: Vec<_> = walk_files_recursive(&path)? - .map(|file| file.path().strip_prefix(&path).unwrap().to_owned()) - .collect(); - - Ok(files) -} - -pub fn gen_filelist_from_fomod( - instance: &ModdedInstance, - xml_path: impl AsRef, - mod_location: impl AsRef, -) -> Result, Box> { - let module_config = fomod::Config::load_from_file(xml_path)?; - // TODO: add active plugins from instance config - let installer = FomodInstaller::new(module_config, vec![], install_prompt::prompt); - let files = installer.run(); - - let mod_files: Vec<_> = files - .iter() - .flat_map(|f| ModFile::from_installer(f.clone(), &mod_location).unwrap()) - .collect(); - - Ok(mod_files) -} - -pub fn activate_instance( +fn command_activate( root_config: &RootConfig, instance_id: &str, - target: &Path, -) -> Result<(), Box> { - let instance_config = root_config.load_instance_by_id(instance_id)?; + target: impl AsRef, +) -> anyhow::Result<()> { + let instance = root_config.load_instance_by_id(instance_id)?; + activate_instance(root_config, &instance, target)?; + Ok(()) +} - let links = gen_links_for_activation( - root_config, - &instance_config, - root_config.games.first().unwrap(), - )?; +fn command_add(root_config: &RootConfig, instance_id: &str, mod_id: &str) -> anyhow::Result<()> { + let mut instance = root_config.load_instance_by_id(instance_id)?; + let mod_to_install = root_config + .get_mod_by_id(mod_id) + .ok_or(anyhow!("Can't find mod in config"))?; - links.iter().try_for_each(|link| apply_link(link, target)); - create_plugins_txt(&instance_config, target)?; + let files = files_to_install_mod(root_config, &instance, &mod_to_install)?; + + insert_mod_to_instance(&mut instance, &mod_to_install, &files, 0); + + let path = &root_config.get_instance_config(instance_id).unwrap().path; + instance.save_to_file(path)?; Ok(()) } -pub fn gen_links_for_activation( - root_config: &RootConfig, - instance: &ModdedInstance, - game: &Game, -) -> Result, Box> { - let game_links = game.export_links()?; - let mod_links = gen_links_for_instance(root_config, instance)?; - let overrides = instance.game_file_overrides().to_owned(); +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 + .games + .first() + .ok_or(anyhow!("TODO: get game from instance"))?; - let mut map: HashMap = HashMap::new(); + let new_load_order = load_order::create_loadorder(root_config, game, &instance)?; - for link in game_links.into_iter().chain(mod_links).chain(overrides) { - map.insert(link.dst, link.src); - } + instance.set_load_order(new_load_order); - let final_links: Vec = map - .into_iter() - .map(|(dst, src)| Link::new(src, dst)) - .collect(); - - Ok(final_links) -} - -fn gen_links_for_instance( - root_config: &RootConfig, - instance: &ModdedInstance, -) -> Result, Box> { - let mut links: Vec = Vec::new(); - - for installed_mod in &instance.mods { - let mod_config = root_config.get_mod_by_id(&installed_mod.mod_id()).unwrap(); - let mod_source_root = root_config.get_mod_location(&mod_config); - - 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) -} - -pub fn add_mod_to_instance( - root_config: &RootConfig, - instance_id: &str, - mod_id: &str, -) -> Result<(), Box> { - let mut instance_config = root_config.load_instance_by_id(instance_id)?.clone(); - let mod_config = root_config.get_mod_by_id(mod_id).ok_or("Mod not found")?; - - let new_files = gen_filelist_for_mod(root_config, &instance_config, &mod_config)?; - - instance_config.add_mod(&mod_config, 0, &new_files); - - let path = &root_config - .get_instance_config(instance_id) - .ok_or("Mod not found")? - .path; - instance_config.save_to_file(path)?; - - Ok(()) -} - -pub fn order_plugins(root_config: &RootConfig, instance_id: &str) -> Result<(), Box> { - let mut instance_config = root_config.load_instance_by_id(instance_id)?.clone(); - - let mut orderer = LoadOrder::new( - &root_config.games.first().unwrap().install_location, - libloot::GameType::SkyrimSE, + instance.save_to_file( + root_config + .get_instance_config(instance_id) + .ok_or(anyhow!("Failed to find instance"))? + .path + .clone(), )?; - orderer.add_plugins_from_install()?; - orderer.add_plugins_from_instance(root_config, &instance_config)?; - - let load_order = orderer.load_order()?; - - instance_config.set_load_order(load_order); - - let path = &root_config - .get_instance_config(instance_id) - .ok_or("Mod not found")? - .path; - - instance_config.save_to_file(path)?; - Ok(()) } @@ -267,13 +85,13 @@ fn main() -> Result<(), Box> { match args.command { cli::Commands::Activate { instance, target } => { - activate_instance(&root_config, &instance, &target)?; + command_activate(&root_config, &instance, &target)?; } cli::Commands::Add { instance, mod_id } => { - add_mod_to_instance(&root_config, &instance, &mod_id)?; + command_add(&root_config, &instance, &mod_id)?; } cli::Commands::LoadOrder { instance } => { - order_plugins(&root_config, &instance)?; + command_order(&root_config, &instance)?; } } diff --git a/src/mod_config_installer.rs b/src/mod_config_installer.rs index 3cff8df..3eddf38 100644 --- a/src/mod_config_installer.rs +++ b/src/mod_config_installer.rs @@ -8,7 +8,7 @@ use crate::fomod::{ }; #[derive(Debug)] -pub struct InstallerState { +struct InstallerState { flags: HashMap, selected_files: Vec, } @@ -39,7 +39,7 @@ impl InstallerState { self.selected_files.extend_from_slice(list); } - pub fn into_file_list(self) -> Vec { + fn into_file_list(self) -> Vec { self.selected_files } } @@ -171,82 +171,66 @@ fn resolve_plugin_type( } } -pub struct FomodInstaller { - config: Config, - installed_plugins: Vec, +pub fn run_fomod_installer( + fomod_config: Config, + installed_plugins: &[String], group_prompt: fn(GroupPrompt) -> Vec, -} +) -> anyhow::Result> { + let mut state = InstallerState::new(); -impl FomodInstaller { - pub fn new( - config: Config, - installed_plugins: Vec, - group_promt: fn(GroupPrompt) -> Vec, - ) -> Self { - Self { - config, - installed_plugins, - group_prompt: group_promt, - } + // Always-installed files first + if let Some(required) = &fomod_config.required_install_files { + state.add_files(required); } - pub fn run(self) -> Vec { - let mut state = InstallerState::new(); + if let Some(install_steps) = fomod_config.install_steps { + let steps = &install_steps.install_step; - // Always-installed files first - if let Some(required) = &self.config.required_install_files { - state.add_files(required); - } + for step in steps { + // Check if the step should be visible + if step + .visible + .as_ref() + .is_some_and(|v| !evaluate_dependency(v, &state, installed_plugins)) + { + // Dependency to show the step not meet. Skipping. + continue; + } - if let Some(install_steps) = &self.config.install_steps { - let steps = &install_steps.install_step; + for group in &step.optional_file_groups.group { + // TODO: Skip groups where all plugins are NotUsable - for step in steps { - // Check if the step should be visible - if step - .visible - .as_ref() - .is_some_and(|v| !evaluate_dependency(v, &state, &self.installed_plugins)) - { - // Dependency to show the step not meet. Skipping. - continue; - } + let prompt = GroupPrompt::new(group, &state, installed_plugins); - for group in &step.optional_file_groups.group { - // TODO: Skip groups where all plugins are NotUsable + let selected_plugins = (group_prompt)(prompt); - let prompt = GroupPrompt::new(group, &state, &self.installed_plugins); + for i in selected_plugins { + let plugin = &group.plugins.plugin[i]; - let selected_plugins = (self.group_prompt)(prompt); + // Add files from selected plugin + if let Some(files) = &plugin.files { + state.add_files(files); + } - for i in selected_plugins { - let plugin = &group.plugins.plugin[i]; - - // Add files from selected plugin - if let Some(files) = &plugin.files { - state.add_files(files); - } - - // Set condition flags - if let Some(condition_flags) = &plugin.condition_flags { - for flag in &condition_flags.flag { - state.set_flag(&flag.name, &flag.flag_value); - } + // Set condition flags + if let Some(condition_flags) = &plugin.condition_flags { + for flag in &condition_flags.flag { + state.set_flag(&flag.name, &flag.flag_value); } } } } } + } - // Evaluate conditional file installs based on final flag state - if let Some(conditional) = &self.config.conditional_file_installs { - for pattern in &conditional.patterns.pattern { - if evaluate_dependency(&pattern.dependencies, &state, &self.installed_plugins) { - state.add_files(&pattern.files); - } + // Evaluate conditional file installs based on final flag state + if let Some(conditional) = &fomod_config.conditional_file_installs { + for pattern in &conditional.patterns.pattern { + if evaluate_dependency(&pattern.dependencies, &state, installed_plugins) { + state.add_files(&pattern.files); } } - - state.into_file_list() } + + Ok(state.into_file_list()) }