245 lines
7.1 KiB
Rust
245 lines
7.1 KiB
Rust
use std::{
|
|
collections::HashMap,
|
|
io,
|
|
path::{Path, PathBuf},
|
|
};
|
|
|
|
use globset::{Glob, GlobSet, GlobSetBuilder};
|
|
use log::{debug, trace, warn};
|
|
use thiserror::Error;
|
|
|
|
use crate::{
|
|
file_conflict_solver::ConflictSolver,
|
|
fomod, install_prompt,
|
|
mod_config_installer::run_fomod_installer,
|
|
types::{InstalledMod, ModConfig, ModFile, ModdedInstance, RootConfig},
|
|
utils::{resolve_case_insensitive, walk_all_files},
|
|
};
|
|
|
|
pub fn insert_mod_to_instance(
|
|
instance: &mut ModdedInstance,
|
|
from_mod: &ModConfig,
|
|
files: &[ModFile],
|
|
priority: isize,
|
|
) -> Result<(), InststanceError> {
|
|
let mut solver = ConflictSolver::new();
|
|
|
|
let mut installed_files: Vec<(ModFile, &InstalledMod)> = Vec::new();
|
|
for installed_mod in instance.mods() {
|
|
for link in installed_mod.files() {
|
|
let recreated_mod_file = ModFile::new(link.src(), link.dst(), 0);
|
|
installed_files.push((recreated_mod_file, installed_mod));
|
|
}
|
|
}
|
|
|
|
for (file, from_mod) in &installed_files {
|
|
if let Some(conflict) = solver.add_file(file, from_mod) {
|
|
warn!("File conflict on already added file: {:?}", conflict);
|
|
}
|
|
}
|
|
|
|
let new_mod = InstalledMod::new(from_mod.id(), priority);
|
|
for file in files {
|
|
if let Some(conflict) = solver.add_file(file, &new_mod) {
|
|
return Err(InststanceError::FileConflict {
|
|
lhs_mod_id: conflict.lhs_mod.mod_id().to_owned(),
|
|
rhs_mod_id: conflict.rhs_mod.mod_id().to_owned(),
|
|
path: conflict.rhs_file.dst().to_owned(),
|
|
});
|
|
}
|
|
}
|
|
|
|
let new_link_tree = solver.export_files();
|
|
|
|
let mut map: HashMap<String, InstalledMod> = HashMap::new();
|
|
|
|
for (file, from_mod) in new_link_tree {
|
|
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());
|
|
new_mod.add_file(file);
|
|
map.insert(new_mod.mod_id().to_owned(), new_mod);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (_, installed_mod) in map {
|
|
instance.update_or_create_mod(&installed_mod);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn files_to_install_mod(
|
|
root_config: &RootConfig,
|
|
instance: &ModdedInstance,
|
|
mod_to_install: &ModConfig,
|
|
) -> anyhow::Result<Vec<ModFile>> {
|
|
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)?,
|
|
ModKind::EmbeddedData(_data_path) => install_from_dir(mod_to_install, mod_location)?,
|
|
ModKind::Root => install_root(mod_to_install, mod_location)?,
|
|
ModKind::Unkown => install_from_dir_to_data(mod_to_install, mod_location)?,
|
|
};
|
|
|
|
Ok(files)
|
|
}
|
|
|
|
fn determain_mod_kind(
|
|
mod_config: &ModConfig,
|
|
mod_location: impl AsRef<Path>,
|
|
) -> Result<ModKind, io::Error> {
|
|
if mod_config.is_root_mod() {
|
|
return Ok(ModKind::Root);
|
|
}
|
|
|
|
// Check for moduleconfig.xml
|
|
let module_config_path = resolve_case_insensitive(&mod_location, "fomod/ModuleConfig.xml")?;
|
|
|
|
if let Some(path) = module_config_path {
|
|
return Ok(ModKind::Fomod(path));
|
|
};
|
|
|
|
match resolve_case_insensitive(&mod_location, "data")? {
|
|
Some(data_path) => Ok(ModKind::EmbeddedData(data_path)),
|
|
None => Ok(ModKind::Unkown),
|
|
}
|
|
}
|
|
|
|
fn install_fomod(
|
|
instance: &ModdedInstance,
|
|
module_config_path: impl AsRef<Path>,
|
|
mod_root: impl AsRef<Path>,
|
|
) -> anyhow::Result<Vec<ModFile>> {
|
|
debug!("Running FOmod installer");
|
|
let module_config = fomod::Config::load_from_file(module_config_path)?;
|
|
|
|
let active_plugins: Vec<_> = instance
|
|
.active_plugins()
|
|
.map(|e| e.to_string_lossy())
|
|
.map(|e| e.to_string())
|
|
.collect();
|
|
|
|
trace!("Current loded plugins: {:?}", active_plugins);
|
|
let files = run_fomod_installer(module_config, &active_plugins, install_prompt::prompt)
|
|
.map_err(|_| InststanceError::FomodRunInstaller)?;
|
|
|
|
let mod_files: Vec<_> = files
|
|
.iter()
|
|
.map(|f| {
|
|
ModFile::from_installer(f.clone(), &mod_root).map_err(InststanceError::FomodFinalize)
|
|
})
|
|
.collect::<Result<Vec<_>, _>>()?
|
|
.into_iter()
|
|
.flatten()
|
|
.collect();
|
|
Ok(mod_files)
|
|
}
|
|
|
|
fn install_from_dir(
|
|
mod_config: &ModConfig,
|
|
mod_location: impl AsRef<Path>,
|
|
) -> anyhow::Result<Vec<ModFile>> {
|
|
let glob_filter = create_glob_filter(mod_config.ignore())?;
|
|
|
|
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))
|
|
.filter(|rel_path| should_be_included(rel_path))
|
|
.map(|rel_path| ModFile::new(&rel_path, &rel_path, 0))
|
|
.collect();
|
|
|
|
Ok(files)
|
|
}
|
|
|
|
fn install_root(
|
|
mod_config: &ModConfig,
|
|
mod_location: impl AsRef<Path>,
|
|
) -> anyhow::Result<Vec<ModFile>> {
|
|
let glob_filter = create_glob_filter(mod_config.ignore())?;
|
|
|
|
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)
|
|
}
|
|
|
|
fn install_from_dir_to_data(
|
|
mod_config: &ModConfig,
|
|
path: impl AsRef<Path>,
|
|
) -> anyhow::Result<Vec<ModFile>> {
|
|
let glob_filter = create_glob_filter(mod_config.ignore())?;
|
|
let data = PathBuf::from("Data");
|
|
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)
|
|
}
|
|
|
|
fn create_glob_filter(rules: &[String]) -> anyhow::Result<GlobSet> {
|
|
let mut builder = GlobSetBuilder::new();
|
|
|
|
for p in rules {
|
|
builder.add(Glob::new(p)?);
|
|
}
|
|
|
|
let set = builder.build()?;
|
|
Ok(set)
|
|
}
|
|
|
|
enum ModKind {
|
|
Fomod(PathBuf),
|
|
EmbeddedData(PathBuf),
|
|
Root,
|
|
Unkown,
|
|
}
|
|
|
|
fn should_be_included(path: impl AsRef<Path>) -> bool {
|
|
matches!(
|
|
path.as_ref().extension().and_then(|e| e.to_str()),
|
|
Some(
|
|
"esp"
|
|
| "esm"
|
|
| "esl"
|
|
| "bsa"
|
|
| "ba2"
|
|
| "bsl"
|
|
| "ini"
|
|
| "pex"
|
|
| "psc"
|
|
| "strings"
|
|
| "ilstrings"
|
|
| "dlstrings"
|
|
| "dll"
|
|
)
|
|
)
|
|
}
|
|
|
|
#[derive(Debug, Error)]
|
|
pub enum InststanceError {
|
|
#[error("Two mods write the same file")]
|
|
FileConflict {
|
|
lhs_mod_id: String,
|
|
rhs_mod_id: String,
|
|
path: PathBuf,
|
|
},
|
|
|
|
#[error("Failed to run fomod installer")]
|
|
FomodRunInstaller,
|
|
|
|
#[error("Failed to handle results of fomod installer")]
|
|
FomodFinalize(io::Error),
|
|
}
|