308 lines
8.3 KiB
Rust
308 lines
8.3 KiB
Rust
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<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,
|
|
}
|
|
|
|
#[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<Path>) -> 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<InstalledMod>,
|
|
|
|
#[serde(default)]
|
|
pub load_order: Vec<String>,
|
|
}
|
|
|
|
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<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 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<String>) {
|
|
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<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,
|
|
}
|