created base config & a lot of refactoring

This commit is contained in:
2026-03-01 21:36:43 +01:00
parent 86b2139759
commit fe0659ea14
3 changed files with 282 additions and 132 deletions

231
src/basic_types.rs Normal file
View File

@@ -0,0 +1,231 @@
use quick_xml::se;
use serde::{Deserialize, Serialize};
use std::{
error::Error,
fs::{self, read_to_string},
io::{self, Write},
path::{Path, PathBuf},
};
use crate::{
conflict_resolver::{Conflict, ConflictSolver},
fomod::{FileType, FileTypeEnum},
utils::{path_to_lowercase, 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,
/// All available mods
pub mods: Vec<ModConfig>,
}
impl RootConfig {
pub fn load_from_file(path: impl AsRef<Path>) -> Result<Self, Box<dyn Error>> {
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.source.clone())
}
pub fn get_mod_by_id(&self, id: &str) -> Option<ModConfig> {
self.mods.iter().find(|e| e.id == id).cloned()
}
}
/// Available game
#[derive(Debug, Clone, Deserialize, PartialEq)]
pub struct Game {
pub install_location: 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 source: PathBuf,
}
impl ModConfig {
pub fn new(id: &str, source: impl AsRef<Path>) -> Self {
Self {
id: id.to_owned(),
source: 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,
pub mods: Vec<InstalledMod>,
}
impl ModdedInstance {
pub fn new(name: &str) -> Self {
Self {
name: name.to_owned(),
mods: Vec::new(),
}
}
pub fn load_from_file(path: impl AsRef<Path>) -> Result<Self, Box<dyn Error>> {
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<(), Box<dyn Error>> {
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]) {
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));
}
}
for (present_file, present_mod) in &already_installed_files {
solver.add_file_unchecked(present_file, present_mod);
}
// 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")
}
}
// No conflicts. Add files.
for file in files {
new_mod.add_file(file);
}
self.mods.push(new_mod);
}
}
#[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()));
}
/// 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
}
}

View File

@@ -1,15 +1,18 @@
use std::{collections::HashMap, path::PathBuf}; use std::{
collections::HashMap,
path::{Path, PathBuf},
};
use crate::{Mod, ModFile}; use crate::basic_types::{InstalledMod, ModFile};
pub struct ConflictSolver<'a> { pub struct ConflictSolver<'a> {
files: HashMap<PathBuf, (&'a ModFile, &'a Mod)>, files: HashMap<PathBuf, (&'a ModFile, &'a InstalledMod)>,
} }
#[derive(Debug)] #[derive(Debug)]
pub struct Conflict<'a> { pub struct Conflict<'a> {
rhs_mod: &'a Mod, rhs_mod: &'a InstalledMod,
lhs_mod: &'a Mod, lhs_mod: &'a InstalledMod,
rhs_file: &'a ModFile, rhs_file: &'a ModFile,
lhs_file: &'a ModFile, lhs_file: &'a ModFile,
} }
@@ -21,19 +24,27 @@ impl<'a> ConflictSolver<'a> {
} }
} }
pub fn add_file(&mut self, file: &'a ModFile, from_mod: &'a Mod) -> Option<Conflict<'a>> { pub fn add_file_unchecked(&mut self, file: &'a ModFile, from_mod: &'a InstalledMod) {
let path = &file.dest; 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();
match self.files.get(path) { match self.files.get(path) {
Some((current_file, current_file_mod)) => { Some((current_file, current_file_mod)) => {
if from_mod == *current_file_mod { if from_mod == *current_file_mod {
// File from the same mod // File from the same mod
// Check internal priority // Check internal priority
if file.internal_priority > current_file.internal_priority { if file.internal_priority() > current_file.internal_priority() {
self.files.insert(path.to_owned(), (file, from_mod)); self.files.insert(path.to_owned(), (file, from_mod));
return None; return None;
} }
if file.internal_priority == current_file.internal_priority { if file.internal_priority() == current_file.internal_priority() {
// Same prio. We got a conflict. // Same prio. We got a conflict.
return Some(Conflict { return Some(Conflict {
@@ -45,12 +56,12 @@ impl<'a> ConflictSolver<'a> {
} }
} }
if from_mod.priority > current_file_mod.priority { if from_mod.priority() > current_file_mod.priority() {
self.files.insert(path.to_owned(), (file, from_mod)); self.files.insert(path.to_owned(), (file, from_mod));
return None; return None;
} }
if from_mod.priority == current_file_mod.priority { if from_mod.priority() == current_file_mod.priority() {
// Different mod but priority the same. We got a conflict. // Different mod but priority the same. We got a conflict.
return Some(Conflict { return Some(Conflict {
rhs_mod: from_mod, rhs_mod: from_mod,
@@ -67,8 +78,4 @@ impl<'a> ConflictSolver<'a> {
None None
} }
pub fn export_files(&self) -> Vec<(&ModFile, &Mod)> {
self.files.values().copied().collect()
}
} }

View File

@@ -1,145 +1,57 @@
use std::{ use std::{error::Error, path::Path};
error::Error,
fs::{self, DirEntry},
path::{Path, PathBuf},
};
use crate::{ use crate::{
conflict_resolver::ConflictSolver, basic_types::{ModConfig, ModFile, ModdedInstance, RootConfig},
fomod::{FileType, FileTypeEnum}, fomod::Config,
linker::Linker,
mod_config_installer::FomodInstaller, mod_config_installer::FomodInstaller,
}; };
mod basic_types;
mod conflict_resolver; mod conflict_resolver;
mod fomod; mod fomod;
mod install_prompt; mod install_prompt;
mod linker;
mod mod_config_installer; mod mod_config_installer;
mod utils;
#[derive(Debug, Clone, PartialEq, Eq)] pub fn load_mod_config(mod_root: impl AsRef<Path>) -> Result<fomod::Config, Box<dyn Error>> {
struct ModFile { let path = mod_root.as_ref().join("FOMod/ModuleConfig.xml");
source: PathBuf, let mod_config = Config::load_from_file(path)?;
dest: PathBuf, Ok(mod_config)
/// Internal priority inside the mod itself
internal_priority: isize,
} }
impl ModFile { pub fn gen_filelist_for_mod(
#[inline] root_config: &RootConfig,
pub fn new_from_installer(file: FileType) -> Self { instance: &ModdedInstance,
let dest: PathBuf = file.destination.unwrap_or_default().into(); mod_config: &ModConfig,
) -> Result<Vec<ModFile>, Box<dyn Error>> {
let mod_location = root_config.get_mod_location(mod_config);
let module_config = load_mod_config(&mod_location)?;
ModFile { // TODO: add active plugins from instance config
source: file.source.into(), let installer = FomodInstaller::new(module_config, vec![], install_prompt::prompt);
dest: Self::path_to_lowercase(&dest),
internal_priority: file.priority.unwrap_or(0),
}
}
pub fn from_installer(
entry: FileTypeEnum,
from_mod: &Mod,
) -> 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 = from_mod.source.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(&from_mod.source)
.unwrap()
.to_owned(),
dest: dest_base.join(file.path().strip_prefix(&source_root).unwrap()),
})
.collect())
}
}
}
fn path_to_lowercase(path: &Path) -> PathBuf {
PathBuf::from(path.to_string_lossy().to_lowercase())
}
}
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())
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct Mod {
name: String,
source: PathBuf,
priority: isize,
}
fn main() -> Result<(), Box<dyn Error>> {
const XML_PATH: &str = "./data/xml/ineed.xml";
let xml = fs::read_to_string(XML_PATH)?;
let loaded_mod = Mod {
name: "INeed".to_owned(),
source: Path::new("./data/mods/iNeed v1").to_owned(),
priority: 0,
};
let config: fomod::Config = quick_xml::de::from_str(&xml)?;
let installer = FomodInstaller::new(config, vec![], install_prompt::prompt);
let files = installer.run(); let files = installer.run();
let converted_files: Vec<_> = files let converted_files: Vec<_> = files
.iter() .iter()
.flat_map(|f| ModFile::from_installer(f.clone(), &loaded_mod).unwrap()) .flat_map(|f| ModFile::from_installer(f.clone(), &mod_location).unwrap())
.collect(); .collect();
let mut solver = ConflictSolver::new(); Ok(converted_files)
}
for file in &converted_files { fn main() -> Result<(), Box<dyn Error>> {
if let Some(conflict) = solver.add_file(file, &loaded_mod) { let root_config = RootConfig::load_from_file("./data/example.toml")?;
println!("Conflict deteced: {:?}", conflict);
return Ok(());
}
}
let files_to_link = solver.export_files(); let mut new_instance = ModdedInstance::new("My Instance");
let linker = Linker::new(Path::new("./data/target"), Path::new("./data/install")); let mod_to_install = root_config.get_mod_by_id("ineed").unwrap();
linker.link_install_to_target()?; let new_files = gen_filelist_for_mod(&root_config, &new_instance, &mod_to_install)?;
for (file, from_mod) in files_to_link { new_instance.add_mod(&mod_to_install, 0, &new_files);
linker.link_mod_file(file, from_mod)?;
} new_instance.save_to_file("./data/my_instance.toml")?;
Ok(()) Ok(())
} }