diff --git a/src/activator.rs b/src/activator.rs index 9e23a3b..980a93b 100644 --- a/src/activator.rs +++ b/src/activator.rs @@ -1,7 +1,7 @@ use anyhow::{Result, anyhow}; use log::{debug, trace}; -use crate::basic_types::{Game, Link, ModdedInstance, RootConfig}; +use crate::types::{Game, Link, ModdedInstance, RootConfig}; use std::collections::HashMap; use std::io::Write; use std::path::PathBuf; @@ -14,7 +14,7 @@ pub fn activate_instance( ) -> Result<()> { // TODO: Resolve game for instance config let game = root_config - .games + .games() .first() .ok_or(anyhow!("TODO: resolve game from config"))?; @@ -40,7 +40,7 @@ fn resolve_links( let mut map: HashMap = HashMap::new(); for link in game_links.into_iter().chain(mod_links).chain(overrides) { - map.insert(link.dst, link.src); + map.insert(link.dst().to_owned(), link.src().to_owned()); } let final_links: Vec = map @@ -57,13 +57,13 @@ fn resolve_link_for_instance( ) -> 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 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.mod_location().join(mod_config.path()); for link in installed_mod.files() { - let link_target = mod_source_root.join(&link.src); - links.push(Link::new(link_target, &link.dst)); + let link_target = mod_source_root.join(link.src()); + links.push(Link::new(link_target, link.dst())); } } @@ -71,8 +71,8 @@ fn resolve_link_for_instance( } 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); + let link_target = &link.src(); + let link_name = target.as_ref().join(link.dst()); link_file(link_target, &link_name) } diff --git a/src/basic_types.rs b/src/basic_types.rs deleted file mode 100644 index 41e9f27..0000000 --- a/src/basic_types.rs +++ /dev/null @@ -1,370 +0,0 @@ -use anyhow::Result; -use log::trace; -use serde::{Deserialize, Serialize}; -use std::{ - fs::{self, read_to_string}, - io::{self, Write}, - path::{Path, PathBuf}, -}; -use thiserror::Error; -use walkdir::WalkDir; - -use crate::fomod::{FileType, FileTypeEnum}; - -/// A link between a file from a mod and a destination in a ModdedInstance -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)] -#[serde(from = "(PathBuf, PathBuf)", into = "(PathBuf,PathBuf)")] -pub struct Link { - pub src: PathBuf, - pub dst: PathBuf, -} - -impl Link { - pub fn new(src: impl AsRef, dst: impl AsRef) -> Self { - Self { - src: src.as_ref().to_owned(), - dst: dst.as_ref().to_owned(), - } - } - - pub fn from_mod_file(file: &ModFile) -> Self { - Self::new(&file.source, &file.dest) - } -} - -impl From<(PathBuf, PathBuf)> for Link { - fn from(value: (PathBuf, PathBuf)) -> Self { - Self { - src: value.0, - dst: value.1, - } - } -} - -impl From for (PathBuf, PathBuf) { - fn from(value: Link) -> Self { - (value.src, value.dst) - } -} - -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 - 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, -} - -impl Game { - pub fn export_links(&self) -> Result, io::Error> { - let links: Vec = WalkDir::new(&self.install_location) - .into_iter() - .map(|entry| { - let entry = entry?; - let path = entry.path(); - - Ok(Link::new( - &path, - path.strip_prefix(&self.install_location).unwrap(), - )) - }) - .collect::>()?; - - Ok(links) - } -} - -#[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, - - /// If the files should be included on the root - #[serde(default)] - root_mod: bool, - - /// Globs of what files to ignore - #[serde(default)] - ignore: Vec, -} - -impl ModConfig { - pub fn new(id: &str, source: impl AsRef) -> Self { - Self { - id: id.to_owned(), - path: source.as_ref().to_owned(), - root_mod: false, - ignore: Vec::new(), - } - } - - pub fn is_root_mod(&self) -> bool { - self.root_mod - } - - pub fn ignore(&self) -> &[String] { - &self.ignore - } -} - -/// 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, - - #[serde(default)] - game_file_overrides: Vec, -} - -impl ModdedInstance { - pub fn new(name: &str) -> Self { - Self { - name: name.to_owned(), - mods: Vec::new(), - load_order: Vec::new(), - game_file_overrides: 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 set_load_order(&mut self, order: Vec) { - self.load_order = order; - } - - pub fn load_order(&self) -> &[String] { - &self.load_order - } - - 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, Eq, Hash)] -pub struct InstalledMod { - id: String, - files: Vec, - priority: isize, -} - -impl InstalledMod { - pub fn new(root_mod_id: &str, priority: isize) -> Self { - Self { - id: root_mod_id.to_owned(), - files: Vec::new(), - priority, - } - } - - pub fn add_file(&mut self, file: &ModFile) { - self.files.push(Link::from_mod_file(file)); - } - - /// 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) -> &[Link] { - &self.files - } -} - -#[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())); - - let files = WalkDir::new(&source_root) - .into_iter() - .map(|entry| { - let entry = entry?; - Ok(Self { - internal_priority: priority, - source: entry.path().strip_prefix(&source).unwrap().to_owned(), - dest: dest_base.join(entry.path().strip_prefix(&source_root).unwrap()), - }) - }) - .collect::>()?; - - Ok(files) - } - } - } - - /// 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, -} diff --git a/src/file_conflict_solver.rs b/src/file_conflict_solver.rs index ef224ce..6619a23 100644 --- a/src/file_conflict_solver.rs +++ b/src/file_conflict_solver.rs @@ -1,8 +1,8 @@ use std::{collections::HashMap, path::PathBuf}; -use log::{debug, trace}; +use log::debug; -use crate::basic_types::{InstalledMod, ModConfig, ModFile, ModdedInstance}; +use crate::types::{InstalledMod, ModFile}; #[derive(Debug)] pub struct Conflict<'a> { @@ -23,22 +23,18 @@ impl<'a> ConflictSolver<'a> { } } - // 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(); + let path = &file.dst().to_owned(); match self.files.get(path) { Some((current_file, current_file_mod)) => { - debug!( - "Trying to resolve file conflict between at {}", - path.to_string_lossy() - ); + // debug!( + // "Trying to resolve file conflict between at {}", + // path.to_string_lossy() + // ); if from_mod == *current_file_mod { // File from the same mod diff --git a/src/instance.rs b/src/instance.rs index 88a6950..321db95 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -6,14 +6,13 @@ use std::{ use globset::{Glob, GlobSet, GlobSetBuilder}; use log::warn; -use walkdir::WalkDir; use crate::{ - basic_types::{InstalledMod, ModConfig, ModFile, ModdedInstance, RootConfig}, file_conflict_solver::ConflictSolver, fomod, install_prompt, mod_config_installer::run_fomod_installer, - utils::resolve_case_insensitive, + types::{InstalledMod, ModConfig, ModFile, ModdedInstance, RootConfig}, + utils::{resolve_case_insensitive, walk_all_files}, }; pub fn insert_mod_to_instance( @@ -25,9 +24,9 @@ pub fn insert_mod_to_instance( let mut solver = ConflictSolver::new(); let mut installed_files: Vec<(ModFile, &InstalledMod)> = Vec::new(); - for installed_mod in &instance.mods { + for installed_mod in instance.mods() { for link in installed_mod.files() { - let recreated_mod_file = ModFile::new(&link.src, &link.dst, 0); + let recreated_mod_file = ModFile::new(link.src(), link.dst(), 0); installed_files.push((recreated_mod_file, installed_mod)); } } @@ -38,7 +37,7 @@ pub fn insert_mod_to_instance( } } - let new_mod = InstalledMod::new(&from_mod.id, priority); + 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 @@ -50,14 +49,14 @@ pub fn insert_mod_to_instance( let mut map: HashMap = HashMap::new(); for (file, from_mod) in new_link_tree { - match map.get_mut(&from_mod.mod_id()) { + 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()); + 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); + map.insert(new_mod.mod_id().to_owned(), new_mod); } } } @@ -72,7 +71,7 @@ pub fn files_to_install_mod( instance: &ModdedInstance, mod_to_install: &ModConfig, ) -> anyhow::Result> { - let mod_location = root_config.get_mod_location(mod_to_install); + 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)?, @@ -127,25 +126,15 @@ fn install_fomod( fn install_from_dir( mod_config: &ModConfig, - path: impl AsRef, + mod_location: impl AsRef, ) -> anyhow::Result> { let glob_filter = create_glob_filter(mod_config.ignore())?; - let files: Vec<_> = WalkDir::new(path) - .into_iter() - .map(|entry| { - let entry = entry?; - let path = entry.path(); - let rel_path = path.strip_prefix(&path).unwrap(); - - if !glob_filter.is_match(rel_path) { - Ok(Some(ModFile::new(&rel_path, &rel_path, 0))) - } else { - Ok(None) - } - }) - .filter_map(|r| r.transpose()) - .collect::>()?; + 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) } @@ -156,25 +145,12 @@ fn install_from_dir_to_data( ) -> anyhow::Result> { let glob_filter = create_glob_filter(mod_config.ignore())?; let data = PathBuf::from("Data"); - let files: Vec<_> = WalkDir::new(&path) - .into_iter() - .map(|entry| { - let entry = entry?; - let path = entry.path(); - - let rel_path = path.strip_prefix(&path).unwrap(); - if !should_be_included(rel_path) { - return Ok(None); - } - - if glob_filter.is_match(rel_path) { - return Ok(None); - } - - Ok(Some(ModFile::new(rel_path, data.join(rel_path), 0))) - }) - .filter_map(|r| r.transpose()) - .collect::>()?; + 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) } diff --git a/src/load_order.rs b/src/load_order.rs index aa3cb94..f9d3cf5 100644 --- a/src/load_order.rs +++ b/src/load_order.rs @@ -10,17 +10,21 @@ use std::{ use thiserror::Error; use walkdir::WalkDir; -use crate::basic_types::{self, ModdedInstance, RootConfig}; +use crate::types::{self, ModdedInstance, RootConfig}; pub fn create_loadorder( root_config: &RootConfig, - game: &basic_types::Game, + game: &types::Game, instance: &ModdedInstance, ) -> Result, LoadOrderError> { - let mut loot_game = Game::new(GameType::SkyrimSE, &game.install_location)?; + let mut loot_game = Game::with_local_path( + GameType::SkyrimSE, + game.install_location(), + &game.install_location().join(PathBuf::from("appdata")), + )?; // Add plugins files from the game install - let install_plugins: Vec = WalkDir::new(game.install_location.join("Data")) + let install_plugins: Vec = WalkDir::new(game.install_location().join("Data")) .into_iter() .map(|entry| { let entry = entry?; @@ -43,17 +47,17 @@ pub fn create_loadorder( // Add plugins from the instance let instance_plugins: Vec<_> = instance - .mods + .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_config = root_config.get_mod_by_id(installed_mod.mod_id()).unwrap(); + let mod_source_root = root_config.mod_location().join(mod_config.path()); installed_mod .files() .iter() - .filter(|f| is_plugin_file(&f.dst)) - .map(move |link| mod_source_root.join(&link.src)) + .filter(|f| is_plugin_file(f.dst())) + .map(move |link| mod_source_root.join(link.src())) }) .collect(); diff --git a/src/main.rs b/src/main.rs index 7dfbcce..e70909d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,13 +5,12 @@ use std::{error::Error, path::Path}; use crate::{ activator::activate_instance, - basic_types::RootConfig, cli::Args, instance::{files_to_install_mod, insert_mod_to_instance}, + types::RootConfig, }; mod activator; -mod basic_types; mod cli; mod file_conflict_solver; mod fomod; @@ -19,6 +18,7 @@ mod install_prompt; mod instance; mod load_order; mod mod_config_installer; +mod types; mod utils; fn command_activate( @@ -50,7 +50,7 @@ fn command_add(root_config: &RootConfig, instance_id: &str, mod_id: &str) -> any 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 + .games() .first() .ok_or(anyhow!("TODO: get game from instance"))?; diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..3727a0a --- /dev/null +++ b/src/types.rs @@ -0,0 +1,33 @@ +use thiserror::Error; + +mod game; +mod installed_mod; +mod link; +mod mod_config; +mod mod_file; +mod modded_instance; +mod root_config; + + +pub use game::*; +pub use installed_mod::*; +pub use link::*; +pub use mod_config::*; +pub use mod_file::*; +pub use modded_instance::*; +pub use root_config::*; + +#[derive(Error, Debug)] +pub enum ConfigReadWriteError { + #[error("IO failure")] + Io(#[from] std::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, +} diff --git a/src/types/game.rs b/src/types/game.rs new file mode 100644 index 0000000..f76e869 --- /dev/null +++ b/src/types/game.rs @@ -0,0 +1,33 @@ +use std::{ + io, + path::{Path, PathBuf}, +}; + +use serde::Deserialize; + +use crate::{types::link::Link, utils::walk_all_files}; + +/// Available game +#[derive(Debug, Clone, Deserialize, PartialEq)] +pub struct Game { + install_location: PathBuf, +} + +impl Game { + pub fn export_links(&self) -> Result, io::Error> { + let links: Vec = walk_all_files(&self.install_location)? + .map(|entry| { + Link::new( + entry.path(), + entry.path().strip_prefix(&self.install_location).unwrap(), + ) + }) + .collect(); + + Ok(links) + } + + pub fn install_location(&self) -> &Path { + &self.install_location + } +} diff --git a/src/types/installed_mod.rs b/src/types/installed_mod.rs new file mode 100644 index 0000000..5598d40 --- /dev/null +++ b/src/types/installed_mod.rs @@ -0,0 +1,39 @@ +use serde::{Deserialize, Serialize}; + +use crate::types::{link::Link, mod_file::ModFile}; + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)] +pub struct InstalledMod { + id: String, + files: Vec, + priority: isize, +} + +impl InstalledMod { + pub fn new(root_mod_id: &str, priority: isize) -> Self { + Self { + id: root_mod_id.to_owned(), + files: Vec::new(), + priority, + } + } + + pub fn add_file(&mut self, file: &ModFile) { + self.files.push(Link::from_mod_file(file)); + } + + /// Get the id of the mod + pub fn mod_id(&self) -> &str { + &self.id + } + + /// 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) -> &[Link] { + &self.files + } +} diff --git a/src/types/link.rs b/src/types/link.rs new file mode 100644 index 0000000..6f51afc --- /dev/null +++ b/src/types/link.rs @@ -0,0 +1,55 @@ +use std::path::{Path, PathBuf}; + +use serde::{Deserialize, Serialize}; + +use crate::types::mod_file::ModFile; + +/// A link between a file from a mod and a destination in a ModdedInstance +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)] +#[serde(from = "(PathBuf, PathBuf)", into = "(PathBuf,PathBuf)")] +pub struct Link { + src: PathBuf, + dst: PathBuf, +} + +impl Link { + pub fn new(src: impl AsRef, dst: impl AsRef) -> Self { + Self { + src: src.as_ref().to_owned(), + dst: dst.as_ref().to_owned(), + } + } + + pub fn from_mod_file(file: &ModFile) -> Self { + Self::new(file.src(), file.dst()) + } + + pub fn src(&self) -> &Path { + &self.src + } + + pub fn dst(&self) -> &Path { + &self.dst + } +} + +impl From<(PathBuf, PathBuf)> for Link { + fn from(value: (PathBuf, PathBuf)) -> Self { + Self { + src: value.0, + dst: value.1, + } + } +} + +impl From for (PathBuf, PathBuf) { + fn from(value: Link) -> Self { + (value.src, value.dst) + } +} + +impl From for Link { + fn from(value: ModFile) -> Self { + Self::new(value.src(), value.dst()) + } +} diff --git a/src/types/mod_config.rs b/src/types/mod_config.rs new file mode 100644 index 0000000..015179a --- /dev/null +++ b/src/types/mod_config.rs @@ -0,0 +1,49 @@ +use std::path::{Path, PathBuf}; + +use serde::Deserialize; + +/// Config for an available mod +#[derive(Debug, Clone, Deserialize, PartialEq)] +pub struct ModConfig { + /// ID of the mod + id: String, + + /// Relative to the mod_location from root config + path: PathBuf, + + /// If the files should be included on the root + #[serde(default)] + root_mod: bool, + + /// Globs of what files to ignore + #[serde(default)] + ignore: Vec, +} + +impl ModConfig { + pub fn new(id: &str, source: impl AsRef) -> Self { + Self { + id: id.to_owned(), + path: source.as_ref().to_owned(), + root_mod: false, + ignore: Vec::new(), + } + } + + pub fn id(&self) -> &str { + &self.id + } + + /// Get the relative path to the mod from the mod directory + pub fn path(&self) -> &Path { + &self.path + } + + pub fn is_root_mod(&self) -> bool { + self.root_mod + } + + pub fn ignore(&self) -> &[String] { + &self.ignore + } +} diff --git a/src/types/mod_file.rs b/src/types/mod_file.rs new file mode 100644 index 0000000..7568f27 --- /dev/null +++ b/src/types/mod_file.rs @@ -0,0 +1,80 @@ +use std::path::{Path, PathBuf}; + +use serde::Deserialize; + +use crate::{ + fomod::{FileType, FileTypeEnum}, + utils::walk_all_files, +}; + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +pub struct ModFile { + /// Relative path in the mod + src: PathBuf, + + /// Relative path on where to install file in game dir + dst: 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 { + src: src.as_ref().to_owned(), + dst: 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 { + src: file.source.into(), + dst: 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())); + + let files = walk_all_files(&source_root)? + .map(|entry| Self { + src: entry.path().strip_prefix(&source).unwrap().to_owned(), + dst: dest_base.join(entry.path().strip_prefix(&source_root).unwrap()), + internal_priority: priority, + }) + .collect(); + + Ok(files) + } + } + } + + pub fn src(&self) -> &Path { + &self.src + } + + /// Get the realtive path this file should be installed + pub fn dst(&self) -> &Path { + &self.dst + } + + /// Get the iternal priority. Only used when 2 files conflict. + #[inline] + pub fn internal_priority(&self) -> isize { + self.internal_priority + } +} diff --git a/src/types/modded_instance.rs b/src/types/modded_instance.rs new file mode 100644 index 0000000..93791bf --- /dev/null +++ b/src/types/modded_instance.rs @@ -0,0 +1,91 @@ +use std::{ + fs::{self, read_to_string}, + io::Write, + path::Path, +}; + +use log::trace; +use serde::{Deserialize, Serialize}; + +use crate::types::{ConfigReadWriteError, installed_mod::InstalledMod, link::Link}; + +/// An modded game with all plugins and files +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +pub struct ModdedInstance { + name: String, + + #[serde(default)] + mods: Vec, + + #[serde(default)] + load_order: Vec, + + #[serde(default)] + game_file_overrides: Vec, +} + +impl ModdedInstance { + pub fn new(name: &str) -> Self { + Self { + name: name.to_owned(), + mods: Vec::new(), + load_order: Vec::new(), + game_file_overrides: 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 set_load_order(&mut self, order: Vec) { + self.load_order = order; + } + + pub fn load_order(&self) -> &[String] { + &self.load_order + } + + 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.mod_id() == installed_mod.mod_id()) + { + Some(existing) => { + *existing = installed_mod.to_owned(); + } + None => { + self.mods.push(installed_mod.to_owned()); + } + } + } + + pub fn mods(&self) -> &[InstalledMod] { + &self.mods + } +} diff --git a/src/types/root_config.rs b/src/types/root_config.rs new file mode 100644 index 0000000..68522a8 --- /dev/null +++ b/src/types/root_config.rs @@ -0,0 +1,69 @@ +use std::{ + fs::read_to_string, + path::{Path, PathBuf}, +}; + +use log::trace; +use serde::Deserialize; + +use crate::types::{ConfigReadWriteError, ModConfig, game::Game, modded_instance::ModdedInstance}; + +#[derive(Debug, Clone, Deserialize, PartialEq)] +pub struct RootConfig { + /// Available games + games: Vec, + + /// Where all mods are stored + mod_location: PathBuf, + + #[serde(default)] + instances: Vec, + + /// All available mods + #[serde(default)] + 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) + } + + pub fn games(&self) -> &[Game] { + &self.games + } + + pub fn get_mod_by_id(&self, id: &str) -> Option<&ModConfig> { + self.mods.iter().find(|e| e.id() == id) + } + + 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) + } + + pub fn mod_location(&self) -> &Path { + &self.mod_location + } +} + +#[derive(Debug, Clone, Deserialize, PartialEq)] +pub struct InstancePointer { + pub id: String, + pub path: PathBuf, +} diff --git a/src/utils.rs b/src/utils.rs index 6874537..3a49d98 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -4,6 +4,8 @@ use std::{ path::{Path, PathBuf}, }; +use walkdir::WalkDir; + pub fn path_to_lowercase(path: impl AsRef) -> PathBuf { PathBuf::from(path.as_ref().to_string_lossy().to_lowercase()) } @@ -40,3 +42,17 @@ pub fn resolve_case_insensitive( Ok(Some(current)) } + +/// Use walkdir to walk all actual files in a dir +/// Returns early id any error occurs +pub fn walk_all_files( + path: impl AsRef, +) -> Result, walkdir::Error> { + let a = WalkDir::new(path) + .into_iter() + .collect::, walkdir::Error>>()? + .into_iter() + .filter(|entry| entry.file_type().is_file()); + + Ok(a) +}