use log::trace; use serde::{Deserialize, Serialize}; use std::{ fs::{self, read_to_string}, io::{self, Write}, path::{Path, PathBuf}, }; use thiserror::Error; use crate::{ conflict_resolver::ConflictSolver, fomod::{FileType, FileTypeEnum}, utils::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, #[serde(default)] pub instances: Vec, /// All available mods #[serde(default)] pub mods: Vec, } impl RootConfig { pub fn load_from_file(path: impl AsRef) -> Result { trace!( "Loading RootConfig from file: {}", path.as_ref().to_string_lossy() ); 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.path.clone()) } pub fn get_mod_by_id(&self, id: &str) -> Option { self.mods.iter().find(|e| e.id == id).cloned() } pub fn load_instance_by_id(&self, id: &str) -> Result { let conf = self .get_instance_config(id) .ok_or(ConfigReadWriteError::IDNotFound)?; ModdedInstance::load_from_file(&conf.path) } pub fn get_instance_config(&self, id: &str) -> Option<&InstancePointer> { self.instances.iter().find(|e| e.id == id) } } /// Available game #[derive(Debug, Clone, Deserialize, PartialEq)] pub struct Game { pub install_location: PathBuf, } #[derive(Debug, Clone, Deserialize, PartialEq)] pub struct InstancePointer { pub id: String, pub path: 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 path: PathBuf, } impl ModConfig { pub fn new(id: &str, source: impl AsRef) -> Self { Self { id: id.to_owned(), path: 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, #[serde(default)] pub mods: Vec, #[serde(default)] pub load_order: Vec, } impl ModdedInstance { pub fn new(name: &str) -> Self { Self { name: name.to_owned(), mods: Vec::new(), load_order: Vec::new(), } } pub fn load_from_file(path: impl AsRef) -> Result { trace!( "Loading ModdedInstance from file: {}", path.as_ref().to_string_lossy() ); let data = read_to_string(path)?; let config = toml::from_str(&data)?; Ok(config) } pub fn save_to_file(&self, path: impl AsRef) -> Result<(), ConfigReadWriteError> { trace!( "Saving ModdedInstance to: {}", path.as_ref().to_string_lossy() ); 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]) { 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 (src, dst) in &installed_mod.files { already_installed_files.push((ModFile::new(src, dst, 0), 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; } pub fn load_order(&self) -> &[String] { &self.load_order } } #[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())); } /// Get the id of the mod pub fn mod_id(&self) -> String { self.id.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 } } #[derive(Error, Debug)] pub enum ConfigReadWriteError { #[error("IO failure")] Io(#[from] io::Error), #[error("Failed to deserialize toml")] Deserialize(#[from] toml::de::Error), #[error("Failed to serialize to toml")] Serialize(#[from] toml::ser::Error), #[error("The provided ID could not be found")] IDNotFound, }