refactored actions to own files
This commit is contained in:
171
src/actions/activate.rs
Normal file
171
src/actions/activate.rs
Normal file
@@ -0,0 +1,171 @@
|
||||
use log::{debug, trace};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::types::{Game, Link, ModdedInstance, RootConfig};
|
||||
use std::collections::HashMap;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::{fs, io, os::unix, path::Path};
|
||||
|
||||
/// Create the symlinks for a instance in a given directory
|
||||
pub fn activate_instance(
|
||||
root_config: &RootConfig,
|
||||
instance: &ModdedInstance,
|
||||
target: impl AsRef<Path>,
|
||||
) -> Result<(), ActivationError> {
|
||||
let game = root_config
|
||||
.game_by_id(instance.game_id())
|
||||
.ok_or(ActivationError::GameNotFound)?;
|
||||
|
||||
check_target_valid(&target)?;
|
||||
|
||||
let resolved_links = resolve_links(root_config, instance, &game)?;
|
||||
|
||||
resolved_links
|
||||
.iter()
|
||||
.try_for_each(|link| apply_link(link, &target))
|
||||
.map_err(ActivationError::Linking)?;
|
||||
create_plugins_txt(instance, target.as_ref()).map_err(ActivationError::PluginsTXT)?;
|
||||
|
||||
debug!("Finished activating instance");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn resolve_links(
|
||||
root_config: &RootConfig,
|
||||
instance: &ModdedInstance,
|
||||
game: &Game,
|
||||
) -> Result<Vec<Link>, ActivationError> {
|
||||
let game_links = game
|
||||
.export_links()
|
||||
.map_err(ActivationError::ExportGameLinks)?;
|
||||
let mod_links = resolve_link_for_instance(root_config, instance)?;
|
||||
let overrides: Vec<Link> = instance.game_file_overrides().to_owned();
|
||||
|
||||
let mut map: HashMap<PathBuf, PathBuf> = HashMap::new();
|
||||
|
||||
for link in game_links.into_iter().chain(mod_links).chain(overrides) {
|
||||
map.insert(link.dst().to_owned(), link.src().to_owned());
|
||||
}
|
||||
|
||||
let final_links: Vec<Link> = map
|
||||
.into_iter()
|
||||
.map(|(dst, src)| Link::new(src, dst))
|
||||
.collect();
|
||||
|
||||
Ok(final_links)
|
||||
}
|
||||
|
||||
fn resolve_link_for_instance(
|
||||
root_config: &RootConfig,
|
||||
instance: &ModdedInstance,
|
||||
) -> Result<Vec<Link>, ActivationError> {
|
||||
debug!("Resolving links for instance");
|
||||
|
||||
let mut links: Vec<Link> = Vec::new();
|
||||
|
||||
for installed_mod in instance.mods() {
|
||||
let mod_config = root_config
|
||||
.mod_by_id(installed_mod.mod_id())
|
||||
.ok_or(ActivationError::ModNotFound)?;
|
||||
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()));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(links)
|
||||
}
|
||||
|
||||
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());
|
||||
|
||||
link_file(link_target, &link_name)
|
||||
}
|
||||
|
||||
fn create_plugins_txt(
|
||||
instance: &ModdedInstance,
|
||||
target: impl AsRef<Path>,
|
||||
) -> Result<(), io::Error> {
|
||||
debug!("Generating Plugins.txt");
|
||||
let mut file = fs::File::create(target.as_ref().join("Plugins.txt"))?;
|
||||
|
||||
writeln!(file, "# Auto generated. DO NOT EDIT MANUALLY!")?;
|
||||
|
||||
for plugin in instance.load_order() {
|
||||
writeln!(file, "*{}", plugin)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn link_file(target: &Path, link_name: &Path) -> Result<(), io::Error> {
|
||||
if let Some(parent) = link_name.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
create_symlink_for_file(target, link_name)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_symlink_for_file(target: &Path, link_name: &Path) -> io::Result<()> {
|
||||
trace!(
|
||||
"Creating symlink at {} with target {}",
|
||||
link_name.to_string_lossy(),
|
||||
target.to_string_lossy()
|
||||
);
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
unix::fs::symlink(target, link_name)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
std::os::windows::fs::symlink_file(target, link_name)
|
||||
}
|
||||
}
|
||||
|
||||
fn check_target_valid(target: impl AsRef<Path>) -> Result<(), ActivationError> {
|
||||
match fs::read_dir(&target) {
|
||||
Ok(entries) => {
|
||||
if entries.count() == 0 {
|
||||
debug!(
|
||||
"Target {} is a valid target",
|
||||
&target.as_ref().to_string_lossy()
|
||||
);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ActivationError::TargetNotEmpty)
|
||||
}
|
||||
}
|
||||
Err(e) => Err(ActivationError::TargetNotValid(e)),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ActivationError {
|
||||
#[error("Could not find game in instance config")]
|
||||
GameNotFound,
|
||||
|
||||
#[error("Could not find mod in instance config")]
|
||||
ModNotFound,
|
||||
|
||||
#[error("The target is not empty")]
|
||||
TargetNotEmpty,
|
||||
|
||||
#[error("The target is not valid")]
|
||||
TargetNotValid(io::Error),
|
||||
|
||||
#[error("Failed to create symlink")]
|
||||
Linking(io::Error),
|
||||
|
||||
#[error("Failed to create Plugins.txt")]
|
||||
PluginsTXT(io::Error),
|
||||
|
||||
#[error("Failed to export files in game installation")]
|
||||
ExportGameLinks(io::Error),
|
||||
}
|
||||
42
src/actions/download.rs
Normal file
42
src/actions/download.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
use anyhow::anyhow;
|
||||
use log::error;
|
||||
|
||||
use crate::{
|
||||
nexus::{NXMUrl, download_nxm},
|
||||
types::RootConfig,
|
||||
unpacker::unpack,
|
||||
};
|
||||
|
||||
/// Handles a nexus mod url. Downloads, unpacks and adds the mod to the config
|
||||
pub fn handle_nxm(root_config: &mut RootConfig, raw_url: &str) -> anyhow::Result<()> {
|
||||
let Some(dl_location) = root_config.download_location() else {
|
||||
return Err(anyhow!("No download location set"));
|
||||
};
|
||||
|
||||
let Some(api_key) = root_config.nexus_api_key() else {
|
||||
return Err(anyhow!("No API key provided"));
|
||||
};
|
||||
|
||||
let Some(nxm_url) = NXMUrl::parse_url(raw_url) else {
|
||||
return Err(anyhow!("Failed to parse URL"));
|
||||
};
|
||||
|
||||
let (dl_file, mod_info) = download_nxm(api_key, &nxm_url, dl_location)?;
|
||||
|
||||
let mod_id = format!("{}-{}", mod_info.generate_id(), nxm_url.file);
|
||||
if root_config.game_by_id(&mod_id).is_some() {
|
||||
error!(
|
||||
"Generated mod id already exists. Pleas install downloaded mod manually. Downloaded at {}",
|
||||
&dl_file.to_string_lossy()
|
||||
);
|
||||
return Err(anyhow!("Mod with generated id already exists"));
|
||||
}
|
||||
|
||||
let new_mod = unpack(root_config, &mod_id, dl_file)?;
|
||||
|
||||
root_config.add_mod(&new_mod);
|
||||
|
||||
root_config.save_to_file()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
70
src/actions/include.rs
Normal file
70
src/actions/include.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
use log::warn;
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
use crate::{
|
||||
file_conflict_solver::ConflictSolver,
|
||||
types::{InstalledMod, ModConfig, ModFile, ModdedInstance},
|
||||
};
|
||||
|
||||
pub fn insert_mod_to_instance(
|
||||
instance: &mut ModdedInstance,
|
||||
from_mod: &ModConfig,
|
||||
files_to_add: &[ModFile],
|
||||
priority: isize,
|
||||
) -> Option<FileConflict> {
|
||||
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_to_add {
|
||||
if let Some(conflict) = solver.add_file(file, &new_mod) {
|
||||
return Some(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);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub struct FileConflict {
|
||||
pub lhs_mod_id: String,
|
||||
pub rhs_mod_id: String,
|
||||
pub path: PathBuf,
|
||||
}
|
||||
165
src/actions/install.rs
Normal file
165
src/actions/install.rs
Normal file
@@ -0,0 +1,165 @@
|
||||
use std::{
|
||||
io,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use globset::{Glob, GlobSet, GlobSetBuilder};
|
||||
use log::{debug, trace};
|
||||
|
||||
use crate::{
|
||||
fomod, install_prompt,
|
||||
mod_config_installer::run_fomod_installer,
|
||||
types::{ModConfig, ModFile, ModdedInstance, RootConfig},
|
||||
utils::{resolve_case_insensitive, walk_all_files},
|
||||
};
|
||||
|
||||
pub fn resolve_files_for_install(
|
||||
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)?;
|
||||
|
||||
let mod_files: Vec<_> = files
|
||||
.iter()
|
||||
.map(|f| ModFile::from_installer(f.clone(), &mod_root))
|
||||
.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"
|
||||
)
|
||||
)
|
||||
}
|
||||
95
src/actions/load_order.rs
Normal file
95
src/actions/load_order.rs
Normal file
@@ -0,0 +1,95 @@
|
||||
use libloot::{
|
||||
Game, GameType,
|
||||
error::{GameHandleCreationError, LoadPluginsError, SortPluginsError},
|
||||
};
|
||||
use log::trace;
|
||||
use std::{
|
||||
io,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use thiserror::Error;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
use crate::{
|
||||
types::{self, ModdedInstance, RootConfig},
|
||||
utils::is_plugin_file,
|
||||
};
|
||||
|
||||
pub fn create_loadorder(
|
||||
root_config: &RootConfig,
|
||||
game: &types::Game,
|
||||
instance: &ModdedInstance,
|
||||
) -> Result<Vec<String>, LoadOrderError> {
|
||||
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"))
|
||||
.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()
|
||||
.iter()
|
||||
.flat_map(|installed_mod| {
|
||||
let mod_config = root_config.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()))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let refs: Vec<_> = instance_plugins.iter().map(|e| e.as_path()).collect();
|
||||
|
||||
trace!("Loading {} plugins to game", refs.len());
|
||||
|
||||
loot_game.load_plugins(&refs)?;
|
||||
|
||||
// Genrate load order
|
||||
let all_plugins = loot_game.loaded_plugins();
|
||||
let plugins_names: Vec<&str> = all_plugins.iter().map(|e| e.name()).collect();
|
||||
|
||||
let sorted = loot_game.sort_plugins(&plugins_names)?;
|
||||
|
||||
Ok(sorted)
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum LoadOrderError {
|
||||
#[error("Failed to read game directory")]
|
||||
Io(#[from] io::Error),
|
||||
|
||||
#[error("Failed to create libloot game")]
|
||||
Creating(#[from] GameHandleCreationError),
|
||||
|
||||
#[error("Failed to add plugins to libloot")]
|
||||
Loading(#[from] LoadPluginsError),
|
||||
|
||||
#[error("libloot failed to sort plugins")]
|
||||
Sorting(#[from] SortPluginsError),
|
||||
}
|
||||
Reference in New Issue
Block a user