From fe0659ea1478e2e078b20984fc03c255fc70cca9 Mon Sep 17 00:00:00 2001 From: Niklas Kapelle Date: Sun, 1 Mar 2026 21:36:43 +0100 Subject: [PATCH] created base config & a lot of refactoring --- src/basic_types.rs | 231 +++++++++++++++++++++++++++++++++++++++ src/conflict_resolver.rs | 37 ++++--- src/main.rs | 146 +++++-------------------- 3 files changed, 282 insertions(+), 132 deletions(-) create mode 100644 src/basic_types.rs diff --git a/src/basic_types.rs b/src/basic_types.rs new file mode 100644 index 0000000..48f6d71 --- /dev/null +++ b/src/basic_types.rs @@ -0,0 +1,231 @@ +use quick_xml::se; +use serde::{Deserialize, Serialize}; +use std::{ + error::Error, + fs::{self, read_to_string}, + io::{self, Write}, + path::{Path, PathBuf}, +}; + +use crate::{ + conflict_resolver::{Conflict, ConflictSolver}, + fomod::{FileType, FileTypeEnum}, + utils::{path_to_lowercase, walk_files_recursive}, +}; + +#[derive(Debug, Clone, Deserialize, PartialEq)] +pub struct RootConfig { + /// Available games + pub games: Vec, + + /// Where all mods are stored + pub mod_location: PathBuf, + + /// All available mods + pub mods: Vec, +} + +impl RootConfig { + pub fn load_from_file(path: impl AsRef) -> Result> { + let data = read_to_string(path)?; + let config = toml::from_str(&data)?; + + Ok(config) + } + + #[inline] + pub fn get_mod_location(&self, mod_config: &ModConfig) -> PathBuf { + self.mod_location.join(mod_config.source.clone()) + } + + pub fn get_mod_by_id(&self, id: &str) -> Option { + self.mods.iter().find(|e| e.id == id).cloned() + } +} + +/// Available game +#[derive(Debug, Clone, Deserialize, PartialEq)] +pub struct Game { + pub install_location: PathBuf, +} + +/// Config for an available mod +#[derive(Debug, Clone, Deserialize, PartialEq)] +pub struct ModConfig { + /// ID of the mod + pub id: String, + + /// Relative to the mod_location from root config + pub source: PathBuf, +} + +impl ModConfig { + pub fn new(id: &str, source: impl AsRef) -> Self { + Self { + id: id.to_owned(), + source: source.as_ref().to_owned(), + } + } +} + +/// An modded game with all plugins and files +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +pub struct ModdedInstance { + pub name: String, + pub mods: Vec, +} + +impl ModdedInstance { + pub fn new(name: &str) -> Self { + Self { + name: name.to_owned(), + mods: Vec::new(), + } + } + + pub fn load_from_file(path: impl AsRef) -> Result> { + let data = read_to_string(path)?; + let config = toml::from_str(&data)?; + + Ok(config) + } + + pub fn save_to_file(&self, path: impl AsRef) -> Result<(), Box> { + let content = toml::to_string_pretty(self)?; + let mut file = fs::File::create(path)?; + write!(file, "{}", content)?; + Ok(()) + } + + pub fn add_mod(&mut self, from_mod: &ModConfig, priority: isize, files: &[ModFile]) { + 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 (src, dst) in &installed_mod.files { + already_installed_files.push((ModFile::new(src, dst, 0), installed_mod)); + } + } + + for (present_file, present_mod) in &already_installed_files { + solver.add_file_unchecked(present_file, present_mod); + } + + // 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") + } + } + + // No conflicts. Add files. + for file in files { + new_mod.add_file(file); + } + + self.mods.push(new_mod); + } +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +pub struct InstalledMod { + id: String, + files: Vec<(PathBuf, PathBuf)>, + priority: isize, +} + +impl InstalledMod { + pub fn new(from_mod: &ModConfig, priority: isize) -> Self { + Self { + id: from_mod.id.clone(), + files: Vec::new(), + priority, + } + } + + pub fn add_file(&mut self, file: &ModFile) { + self.files.push((file.source.clone(), file.dest.clone())); + } + + /// The priority over other mods. Only used when 2 files conflict. + pub fn priority(&self) -> isize { + self.priority + } + + /// The selected files + pub fn files(&self) -> Vec<(PathBuf, PathBuf)> { + self.files.clone() + } +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +pub struct ModFile { + /// Relative path in the mod + source: PathBuf, + + /// Relative path on where to install file in game dir + dest: PathBuf, + + /// Internal priority inside the mod itself. In case the mod overwrites internal files. + internal_priority: isize, +} + +impl ModFile { + pub fn new(src: impl AsRef, dst: impl AsRef, prio: isize) -> Self { + Self { + source: src.as_ref().to_owned(), + dest: dst.as_ref().to_owned(), + internal_priority: prio, + } + } + + pub fn new_from_installer(file: FileType) -> Self { + let dest: PathBuf = file.destination.unwrap_or_default().into(); + + ModFile { + source: file.source.into(), + dest: dest.to_owned(), + internal_priority: file.priority.unwrap_or(0), + } + } + + pub fn from_installer( + entry: FileTypeEnum, + source: impl AsRef, + ) -> Result, std::io::Error> { + match entry { + FileTypeEnum::File(file_type) => Ok(vec![Self::new_from_installer(file_type)]), + FileTypeEnum::Folder(dir_type) => { + let source_root = source.as_ref().join(&dir_type.source); + let priority = dir_type.priority.unwrap_or(0); + let dest_base: PathBuf = + Path::new("Data").join(PathBuf::from(dir_type.destination.unwrap_or_default())); + + Ok(walk_files_recursive(&source_root)? + .map(|file| Self { + internal_priority: priority, + source: file.path().strip_prefix(&source).unwrap().to_owned(), + dest: dest_base.join(file.path().strip_prefix(&source_root).unwrap()), + }) + .collect()) + } + } + } + + /// Get the realtive path this file should be installed + #[inline] + pub fn destination(&self) -> PathBuf { + self.dest.clone() + } + + /// Get the iternal priority. Only used when 2 files conflict. + #[inline] + pub fn internal_priority(&self) -> isize { + self.internal_priority + } +} diff --git a/src/conflict_resolver.rs b/src/conflict_resolver.rs index 2d04a49..1fe9a57 100644 --- a/src/conflict_resolver.rs +++ b/src/conflict_resolver.rs @@ -1,15 +1,18 @@ -use std::{collections::HashMap, path::PathBuf}; +use std::{ + collections::HashMap, + path::{Path, PathBuf}, +}; -use crate::{Mod, ModFile}; +use crate::basic_types::{InstalledMod, ModFile}; pub struct ConflictSolver<'a> { - files: HashMap, + files: HashMap, } #[derive(Debug)] pub struct Conflict<'a> { - rhs_mod: &'a Mod, - lhs_mod: &'a Mod, + rhs_mod: &'a InstalledMod, + lhs_mod: &'a InstalledMod, rhs_file: &'a ModFile, lhs_file: &'a ModFile, } @@ -21,19 +24,27 @@ impl<'a> ConflictSolver<'a> { } } - pub fn add_file(&mut self, file: &'a ModFile, from_mod: &'a Mod) -> Option> { - let path = &file.dest; + pub 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, + file: &'a ModFile, + from_mod: &'a InstalledMod, + ) -> Option> { + let path = &file.destination(); match self.files.get(path) { Some((current_file, current_file_mod)) => { if from_mod == *current_file_mod { // File from the same mod // Check internal priority - if file.internal_priority > current_file.internal_priority { + if file.internal_priority() > current_file.internal_priority() { self.files.insert(path.to_owned(), (file, from_mod)); return None; } - if file.internal_priority == current_file.internal_priority { + if file.internal_priority() == current_file.internal_priority() { // Same prio. We got a conflict. return Some(Conflict { @@ -45,12 +56,12 @@ impl<'a> ConflictSolver<'a> { } } - if from_mod.priority > current_file_mod.priority { + if from_mod.priority() > current_file_mod.priority() { self.files.insert(path.to_owned(), (file, from_mod)); return None; } - if from_mod.priority == current_file_mod.priority { + if from_mod.priority() == current_file_mod.priority() { // Different mod but priority the same. We got a conflict. return Some(Conflict { rhs_mod: from_mod, @@ -67,8 +78,4 @@ impl<'a> ConflictSolver<'a> { None } - - pub fn export_files(&self) -> Vec<(&ModFile, &Mod)> { - self.files.values().copied().collect() - } } diff --git a/src/main.rs b/src/main.rs index 7852064..f5ba661 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,145 +1,57 @@ -use std::{ - error::Error, - fs::{self, DirEntry}, - path::{Path, PathBuf}, -}; +use std::{error::Error, path::Path}; use crate::{ - conflict_resolver::ConflictSolver, - fomod::{FileType, FileTypeEnum}, - linker::Linker, + basic_types::{ModConfig, ModFile, ModdedInstance, RootConfig}, + fomod::Config, mod_config_installer::FomodInstaller, }; +mod basic_types; mod conflict_resolver; mod fomod; mod install_prompt; -mod linker; mod mod_config_installer; +mod utils; -#[derive(Debug, Clone, PartialEq, Eq)] -struct ModFile { - source: PathBuf, - dest: PathBuf, - /// Internal priority inside the mod itself - internal_priority: isize, +pub fn load_mod_config(mod_root: impl AsRef) -> Result> { + let path = mod_root.as_ref().join("FOMod/ModuleConfig.xml"); + let mod_config = Config::load_from_file(path)?; + Ok(mod_config) } -impl ModFile { - #[inline] - pub fn new_from_installer(file: FileType) -> Self { - let dest: PathBuf = file.destination.unwrap_or_default().into(); +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); + let module_config = load_mod_config(&mod_location)?; - ModFile { - source: file.source.into(), - dest: Self::path_to_lowercase(&dest), - internal_priority: file.priority.unwrap_or(0), - } - } - - pub fn from_installer( - entry: FileTypeEnum, - from_mod: &Mod, - ) -> Result, std::io::Error> { - match entry { - FileTypeEnum::File(file_type) => Ok(vec![Self::new_from_installer(file_type)]), - FileTypeEnum::Folder(dir_type) => { - let source_root = from_mod.source.join(&dir_type.source); - let priority = dir_type.priority.unwrap_or(0); - let dest_base: PathBuf = - Path::new("Data").join(PathBuf::from(dir_type.destination.unwrap_or_default())); - - Ok(walk_files_recursive(&source_root)? - .map(|file| Self { - internal_priority: priority, - source: file - .path() - .strip_prefix(&from_mod.source) - .unwrap() - .to_owned(), - dest: dest_base.join(file.path().strip_prefix(&source_root).unwrap()), - }) - .collect()) - } - } - } - - fn path_to_lowercase(path: &Path) -> PathBuf { - PathBuf::from(path.to_string_lossy().to_lowercase()) - } -} - -pub fn walk_files_recursive( - root: impl AsRef, -) -> std::io::Result> { - fn visit(dir: &Path, out: &mut Vec) -> std::io::Result<()> { - for entry in fs::read_dir(dir)? { - let entry = entry?; - let path = entry.path(); - let file_type = entry.file_type()?; - - if file_type.is_dir() { - visit(&path, out)?; - } else if file_type.is_file() { - out.push(entry); - } - } - Ok(()) - } - - let mut files = Vec::new(); - visit(root.as_ref(), &mut files)?; - Ok(files.into_iter()) -} - -#[derive(Debug, Clone, PartialEq, Eq)] -struct Mod { - name: String, - - source: PathBuf, - - priority: isize, -} - -fn main() -> Result<(), Box> { - const XML_PATH: &str = "./data/xml/ineed.xml"; - let xml = fs::read_to_string(XML_PATH)?; - - let loaded_mod = Mod { - name: "INeed".to_owned(), - source: Path::new("./data/mods/iNeed v1").to_owned(), - priority: 0, - }; - - let config: fomod::Config = quick_xml::de::from_str(&xml)?; - - let installer = FomodInstaller::new(config, vec![], install_prompt::prompt); + // TODO: add active plugins from instance config + let installer = FomodInstaller::new(module_config, vec![], install_prompt::prompt); let files = installer.run(); let converted_files: Vec<_> = files .iter() - .flat_map(|f| ModFile::from_installer(f.clone(), &loaded_mod).unwrap()) + .flat_map(|f| ModFile::from_installer(f.clone(), &mod_location).unwrap()) .collect(); - let mut solver = ConflictSolver::new(); + Ok(converted_files) +} - for file in &converted_files { - if let Some(conflict) = solver.add_file(file, &loaded_mod) { - println!("Conflict deteced: {:?}", conflict); - return Ok(()); - } - } +fn main() -> Result<(), Box> { + let root_config = RootConfig::load_from_file("./data/example.toml")?; - let files_to_link = solver.export_files(); + let mut new_instance = ModdedInstance::new("My Instance"); - let linker = Linker::new(Path::new("./data/target"), Path::new("./data/install")); + let mod_to_install = root_config.get_mod_by_id("ineed").unwrap(); - linker.link_install_to_target()?; + let new_files = gen_filelist_for_mod(&root_config, &new_instance, &mod_to_install)?; - for (file, from_mod) in files_to_link { - linker.link_mod_file(file, from_mod)?; - } + new_instance.add_mod(&mod_to_install, 0, &new_files); + + new_instance.save_to_file("./data/my_instance.toml")?; Ok(()) }