the great refactor

This commit is contained in:
2026-03-04 22:50:37 +01:00
parent b6efa0a818
commit c81178567a
15 changed files with 519 additions and 448 deletions

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,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<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> = 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::<Result<_, io::Error>>()?;
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()));
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::<Result<_, io::Error>>()?;
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,
}

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

@@ -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<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);
}
}
}
@@ -72,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)?,
@@ -127,25 +126,15 @@ 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<_> = 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::<Result<_, io::Error>>()?;
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<Vec<ModFile>> {
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::<Result<_, io::Error>>()?;
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

@@ -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<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<PathBuf> = WalkDir::new(game.install_location.join("Data"))
let install_plugins: Vec<PathBuf> = 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();

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

@@ -4,6 +4,8 @@ use std::{
path::{Path, PathBuf},
};
use walkdir::WalkDir;
pub fn path_to_lowercase(path: impl AsRef<Path>) -> 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<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)
}