Compare commits

...

2 Commits

Author SHA1 Message Date
c81178567a the great refactor 2026-03-04 22:50:37 +01:00
b6efa0a818 made use of walkdir 2026-03-04 19:56:33 +01:00
17 changed files with 569 additions and 440 deletions

29
Cargo.lock generated
View File

@@ -325,6 +325,7 @@ dependencies = [
"serde",
"thiserror",
"toml",
"walkdir",
]
[[package]]
@@ -725,6 +726,15 @@ version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "saphyr"
version = "0.0.6"
@@ -896,12 +906,31 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "winapi-util"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys",
]
[[package]]
name = "windows-link"
version = "0.2.1"

View File

@@ -14,3 +14,4 @@ quick-xml = { version = "0.39.2", features = ["serde-types", "serialize"] }
serde = { version = "1.0.228", features = ["derive"] }
thiserror = "2.0.18"
toml = "1.0.3"
walkdir = "2.5.0"

View File

@@ -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<PathBuf, PathBuf> = 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<Link> = map
@@ -57,13 +57,13 @@ fn resolve_link_for_instance(
) -> anyhow::Result<Vec<Link>> {
let mut links: Vec<Link> = 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<Path>) -> 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)
}

View File

@@ -1,357 +0,0 @@
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::{
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, 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<Path>, dst: impl AsRef<Path>) -> 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<Link> for (PathBuf, PathBuf) {
fn from(value: Link) -> Self {
(value.src, value.dst)
}
}
impl From<ModFile> 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<Game>,
/// Where all mods are stored
pub mod_location: PathBuf,
#[serde(default)]
pub instances: Vec<InstancePointer>,
/// All available mods
#[serde(default)]
pub mods: Vec<ModConfig>,
}
impl RootConfig {
pub fn load_from_file(path: impl AsRef<Path>) -> Result<Self, ConfigReadWriteError> {
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<ModConfig> {
self.mods.iter().find(|e| e.id == id).cloned()
}
pub fn load_instance_by_id(&self, id: &str) -> Result<ModdedInstance, ConfigReadWriteError> {
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<Vec<Link>, io::Error> {
let links: Vec<Link> = walk_files_recursive(&self.install_location)
.unwrap()
.map(|file| file.path())
.map(|path| 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<String>,
}
impl ModConfig {
pub fn new(id: &str, source: impl AsRef<Path>) -> 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<InstalledMod>,
#[serde(default)]
pub load_order: Vec<String>,
#[serde(default)]
game_file_overrides: Vec<Link>,
}
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<Path>) -> Result<Self, ConfigReadWriteError> {
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<Path>) -> 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<String>) {
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<Link>,
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<Path>, dst: impl AsRef<Path>, 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<Path>,
) -> Result<Vec<Self>, 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,
}

View File

@@ -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<Conflict<'a>> {
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

View File

@@ -1,5 +1,5 @@
use std::{
collections::{HashMap, HashSet},
collections::HashMap,
io,
path::{Path, PathBuf},
};
@@ -8,11 +8,11 @@ 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},
types::{InstalledMod, ModConfig, ModFile, ModdedInstance, RootConfig},
utils::{resolve_case_insensitive, walk_all_files},
};
pub fn insert_mod_to_instance(
@@ -24,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));
}
}
@@ -37,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
@@ -49,14 +49,14 @@ pub fn insert_mod_to_instance(
let mut map: HashMap<String, InstalledMod> = 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);
}
}
}
@@ -71,7 +71,7 @@ pub fn files_to_install_mod(
instance: &ModdedInstance,
mod_to_install: &ModConfig,
) -> anyhow::Result<Vec<ModFile>> {
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)?,
@@ -126,15 +126,16 @@ fn install_fomod(
fn install_from_dir(
mod_config: &ModConfig,
path: impl AsRef<Path>,
mod_location: impl AsRef<Path>,
) -> anyhow::Result<Vec<ModFile>> {
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())
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)
}
@@ -144,13 +145,13 @@ fn install_from_dir_to_data(
) -> anyhow::Result<Vec<ModFile>> {
let glob_filter = create_glob_filter(mod_config.ignore())?;
let data = PathBuf::from("Data");
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| should_be_included(rel_path))
let files: Vec<ModFile> = 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)
}

View File

@@ -3,44 +3,61 @@ use libloot::{
error::{GameHandleCreationError, LoadPluginsError, SortPluginsError},
};
use log::trace;
use std::{io, path::Path};
use thiserror::Error;
use crate::{
basic_types::{self, ModdedInstance, RootConfig},
utils::walk_files_recursive,
use std::{
io,
path::{Path, PathBuf},
};
use thiserror::Error;
use walkdir::WalkDir;
use crate::types::{self, ModdedInstance, RootConfig};
pub fn create_loadorder(
root_config: &RootConfig,
game: &basic_types::Game,
game: &types::Game,
instance: &ModdedInstance,
) -> Result<Vec<String>, 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<_> = 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();
let install_plugins: Vec<PathBuf> = WalkDir::new(game.install_location().join("Data"))
.into_iter()
.map(|entry| {
let entry = entry?;
let path = entry.path();
if is_plugin_file(path) {
Ok(Some(path.to_path_buf()))
} else {
Ok(None)
}
})
.filter_map(|r| r.transpose())
.collect::<Result<_, io::Error>>()?;
// The loaded_plugins function requires &[&Path]
let refs: Vec<&Path> = install_plugins.iter().map(|e| e.as_path()).collect();
trace!("Loading {} plugins to game", refs.len());
loot_game.load_plugins(&refs)?;
// 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();

View File

@@ -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"))?;

33
src/types.rs Normal file
View File

@@ -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,
}

33
src/types/game.rs Normal file
View File

@@ -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<Vec<Link>, io::Error> {
let links: Vec<Link> = 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
}
}

View File

@@ -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<Link>,
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
}
}

55
src/types/link.rs Normal file
View File

@@ -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<Path>, dst: impl AsRef<Path>) -> 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<Link> for (PathBuf, PathBuf) {
fn from(value: Link) -> Self {
(value.src, value.dst)
}
}
impl From<ModFile> for Link {
fn from(value: ModFile) -> Self {
Self::new(value.src(), value.dst())
}
}

49
src/types/mod_config.rs Normal file
View File

@@ -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<String>,
}
impl ModConfig {
pub fn new(id: &str, source: impl AsRef<Path>) -> 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
}
}

80
src/types/mod_file.rs Normal file
View File

@@ -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<Path>, dst: impl AsRef<Path>, 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<Path>,
) -> Result<Vec<Self>, 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
}
}

View File

@@ -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<InstalledMod>,
#[serde(default)]
load_order: Vec<String>,
#[serde(default)]
game_file_overrides: Vec<Link>,
}
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<Path>) -> Result<Self, ConfigReadWriteError> {
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<Path>) -> 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<String>) {
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
}
}

69
src/types/root_config.rs Normal file
View File

@@ -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<Game>,
/// Where all mods are stored
mod_location: PathBuf,
#[serde(default)]
instances: Vec<InstancePointer>,
/// All available mods
#[serde(default)]
mods: Vec<ModConfig>,
}
impl RootConfig {
pub fn load_from_file(path: impl AsRef<Path>) -> Result<Self, ConfigReadWriteError> {
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<ModdedInstance, ConfigReadWriteError> {
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,
}

View File

@@ -1,31 +1,10 @@
use std::{
fs::{self, DirEntry},
fs::{self},
io,
path::{Path, PathBuf},
};
pub fn walk_files_recursive(
root: impl AsRef<Path>,
) -> std::io::Result<impl Iterator<Item = DirEntry>> {
fn visit(dir: &Path, out: &mut Vec<DirEntry>) -> 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())
}
use walkdir::WalkDir;
pub fn path_to_lowercase(path: impl AsRef<Path>) -> PathBuf {
PathBuf::from(path.as_ref().to_string_lossy().to_lowercase())
@@ -63,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<Path>,
) -> Result<impl Iterator<Item = walkdir::DirEntry>, walkdir::Error> {
let a = WalkDir::new(path)
.into_iter()
.collect::<Result<Vec<_>, walkdir::Error>>()?
.into_iter()
.filter(|entry| entry.file_type().is_file());
Ok(a)
}