Compare commits
2 Commits
a5999f28eb
...
dc41f93ecb
| Author | SHA1 | Date | |
|---|---|---|---|
|
dc41f93ecb
|
|||
|
7e6de5c73c
|
7
Cargo.lock
generated
7
Cargo.lock
generated
@@ -67,6 +67,12 @@ dependencies = [
|
|||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyhow"
|
||||||
|
version = "1.0.102"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arraydeque"
|
name = "arraydeque"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
@@ -309,6 +315,7 @@ checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
|||||||
name = "fomod-manager"
|
name = "fomod-manager"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"globset",
|
"globset",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ version = "0.1.0"
|
|||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
anyhow = "1.0.102"
|
||||||
clap = { version = "4.5.60", features = ["derive"] }
|
clap = { version = "4.5.60", features = ["derive"] }
|
||||||
env_logger = "0.11.9"
|
env_logger = "0.11.9"
|
||||||
globset = "0.4.18"
|
globset = "0.4.18"
|
||||||
|
|||||||
123
src/activator.rs
Normal file
123
src/activator.rs
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
use anyhow::{Result, anyhow};
|
||||||
|
use log::{debug, trace};
|
||||||
|
|
||||||
|
use crate::basic_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};
|
||||||
|
|
||||||
|
pub fn activate_instance(
|
||||||
|
root_config: &RootConfig,
|
||||||
|
instance: &ModdedInstance,
|
||||||
|
target: impl AsRef<Path>,
|
||||||
|
) -> Result<()> {
|
||||||
|
// TODO: Resolve game for instance config
|
||||||
|
let game = root_config
|
||||||
|
.games
|
||||||
|
.first()
|
||||||
|
.ok_or(anyhow!("TODO: resolve game from config"))?;
|
||||||
|
|
||||||
|
let resolved_links = resolve_links(root_config, instance, game)?;
|
||||||
|
|
||||||
|
resolved_links
|
||||||
|
.iter()
|
||||||
|
.try_for_each(|link| apply_link(link, target.as_ref()));
|
||||||
|
create_plugins_txt(&instance, target.as_ref())?;
|
||||||
|
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_links(
|
||||||
|
root_config: &RootConfig,
|
||||||
|
instance: &ModdedInstance,
|
||||||
|
game: &Game,
|
||||||
|
) -> anyhow::Result<Vec<Link>> {
|
||||||
|
let game_links = game.export_links()?;
|
||||||
|
let mod_links = resolve_link_for_instance(root_config, instance)?;
|
||||||
|
let overrides = 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, link.src);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
) -> 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 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<()> {
|
||||||
|
let absolute_path = fs::canonicalize(target)?;
|
||||||
|
|
||||||
|
trace!(
|
||||||
|
"Creating symlink at {} with target {}",
|
||||||
|
link_name.to_string_lossy(),
|
||||||
|
target.to_string_lossy()
|
||||||
|
);
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
unix::fs::symlink(absolute_path, link_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
std::os::windows::fs::symlink_file(target, link_name)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,13 +8,12 @@ use std::{
|
|||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
conflict_resolver::ConflictSolver,
|
|
||||||
fomod::{FileType, FileTypeEnum},
|
fomod::{FileType, FileTypeEnum},
|
||||||
utils::walk_files_recursive,
|
utils::walk_files_recursive,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A link between a file from a mod and a destination in a ModdedInstance
|
/// A link between a file from a mod and a destination in a ModdedInstance
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)]
|
||||||
#[serde(from = "(PathBuf, PathBuf)", into = "(PathBuf,PathBuf)")]
|
#[serde(from = "(PathBuf, PathBuf)", into = "(PathBuf,PathBuf)")]
|
||||||
pub struct Link {
|
pub struct Link {
|
||||||
pub src: PathBuf,
|
pub src: PathBuf,
|
||||||
@@ -49,6 +48,12 @@ impl From<Link> for (PathBuf, PathBuf) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<ModFile> for Link {
|
||||||
|
fn from(value: ModFile) -> Self {
|
||||||
|
Self::new(value.source, value.dest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, PartialEq)]
|
#[derive(Debug, Clone, Deserialize, PartialEq)]
|
||||||
pub struct RootConfig {
|
pub struct RootConfig {
|
||||||
/// Available games
|
/// Available games
|
||||||
@@ -170,6 +175,9 @@ pub struct ModdedInstance {
|
|||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub load_order: Vec<String>,
|
pub load_order: Vec<String>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
game_file_overrides: Vec<Link>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ModdedInstance {
|
impl ModdedInstance {
|
||||||
@@ -178,6 +186,7 @@ impl ModdedInstance {
|
|||||||
name: name.to_owned(),
|
name: name.to_owned(),
|
||||||
mods: Vec::new(),
|
mods: Vec::new(),
|
||||||
load_order: Vec::new(),
|
load_order: Vec::new(),
|
||||||
|
game_file_overrides: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,46 +214,6 @@ impl ModdedInstance {
|
|||||||
Ok(())
|
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 link in &installed_mod.files {
|
|
||||||
already_installed_files
|
|
||||||
.push((ModFile::new(&link.src, &link.dst, 1), 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>) {
|
pub fn set_load_order(&mut self, order: Vec<String>) {
|
||||||
self.load_order = order;
|
self.load_order = order;
|
||||||
}
|
}
|
||||||
@@ -252,9 +221,24 @@ impl ModdedInstance {
|
|||||||
pub fn load_order(&self) -> &[String] {
|
pub fn load_order(&self) -> &[String] {
|
||||||
&self.load_order
|
&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)]
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)]
|
||||||
pub struct InstalledMod {
|
pub struct InstalledMod {
|
||||||
id: String,
|
id: String,
|
||||||
files: Vec<Link>,
|
files: Vec<Link>,
|
||||||
@@ -262,9 +246,9 @@ pub struct InstalledMod {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl InstalledMod {
|
impl InstalledMod {
|
||||||
pub fn new(from_mod: &ModConfig, priority: isize) -> Self {
|
pub fn new(root_mod_id: &str, priority: isize) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: from_mod.id.clone(),
|
id: root_mod_id.to_owned(),
|
||||||
files: Vec::new(),
|
files: Vec::new(),
|
||||||
priority,
|
priority,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
use std::{collections::HashMap, path::PathBuf};
|
use std::{collections::HashMap, path::PathBuf};
|
||||||
|
|
||||||
use log::debug;
|
use log::{debug, trace};
|
||||||
|
|
||||||
use crate::basic_types::{InstalledMod, ModFile};
|
use crate::basic_types::{InstalledMod, ModConfig, ModFile, ModdedInstance};
|
||||||
|
|
||||||
pub struct ConflictSolver<'a> {
|
|
||||||
files: HashMap<PathBuf, (&'a ModFile, &'a InstalledMod)>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Conflict<'a> {
|
pub struct Conflict<'a> {
|
||||||
@@ -16,6 +12,10 @@ pub struct Conflict<'a> {
|
|||||||
lhs_file: &'a ModFile,
|
lhs_file: &'a ModFile,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct ConflictSolver<'a> {
|
||||||
|
files: HashMap<PathBuf, (&'a ModFile, &'a InstalledMod)>,
|
||||||
|
}
|
||||||
|
|
||||||
impl<'a> ConflictSolver<'a> {
|
impl<'a> ConflictSolver<'a> {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -23,9 +23,9 @@ impl<'a> ConflictSolver<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_file_unchecked(&mut self, file: &'a ModFile, from_mod: &'a InstalledMod) {
|
// fn add_file_unchecked(&mut self, file: &'a ModFile, from_mod: &'a InstalledMod) {
|
||||||
self.files.insert(file.destination(), (file, from_mod));
|
// self.files.insert(file.destination(), (file, from_mod));
|
||||||
}
|
// }
|
||||||
|
|
||||||
pub fn add_file(
|
pub fn add_file(
|
||||||
&mut self,
|
&mut self,
|
||||||
@@ -84,4 +84,8 @@ impl<'a> ConflictSolver<'a> {
|
|||||||
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn export_files(&self) -> Vec<(&'a ModFile, &'a InstalledMod)> {
|
||||||
|
self.files.iter().map(|e| e.1.to_owned()).collect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
178
src/instance.rs
Normal file
178
src/instance.rs
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
use std::{
|
||||||
|
collections::{HashMap, HashSet},
|
||||||
|
io,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
use globset::{Glob, GlobSet, GlobSetBuilder};
|
||||||
|
use log::warn;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
basic_types::{InstalledMod, Link, ModConfig, ModFile, ModdedInstance, RootConfig},
|
||||||
|
file_conflict_solver::ConflictSolver,
|
||||||
|
fomod, install_prompt,
|
||||||
|
mod_config_installer::run_fomod_installer,
|
||||||
|
utils::{resolve_case_insensitive, walk_files_recursive},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn insert_mod_to_instance(
|
||||||
|
instance: &mut ModdedInstance,
|
||||||
|
from_mod: &ModConfig,
|
||||||
|
files: &[ModFile],
|
||||||
|
priority: isize,
|
||||||
|
) {
|
||||||
|
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!("Got 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) {
|
||||||
|
// TODO: Return conflict
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(), new_mod);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (_, installed_mod) in map {
|
||||||
|
instance.update_or_create_mod(&installed_mod);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn files_to_install_mod(
|
||||||
|
root_config: &RootConfig,
|
||||||
|
instance: &ModdedInstance,
|
||||||
|
mod_to_install: &ModConfig,
|
||||||
|
) -> anyhow::Result<Vec<ModFile>> {
|
||||||
|
let mod_location = root_config.get_mod_location(mod_to_install);
|
||||||
|
|
||||||
|
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.join(data_path))?
|
||||||
|
}
|
||||||
|
ModKind::Root => install_from_dir(mod_to_install, mod_location)?,
|
||||||
|
ModKind::Unkown => todo!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
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>> {
|
||||||
|
let module_config = fomod::Config::load_from_file(module_config_path)?;
|
||||||
|
|
||||||
|
// TODO: add active plugins from instance config
|
||||||
|
let files = run_fomod_installer(module_config, &[], install_prompt::prompt)?;
|
||||||
|
|
||||||
|
let mod_files: Vec<_> = files
|
||||||
|
.iter()
|
||||||
|
.flat_map(|f| ModFile::from_installer(f.clone(), &mod_root).unwrap())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(mod_files)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn install_from_dir(
|
||||||
|
mod_config: &ModConfig,
|
||||||
|
path: impl AsRef<Path>,
|
||||||
|
) -> anyhow::Result<Vec<ModFile>> {
|
||||||
|
let glob_filter = create_glob_filter(mod_config.ignore())?;
|
||||||
|
let files: Vec<_> = walk_files_recursive(&path)?
|
||||||
|
.map(|entry| entry.path())
|
||||||
|
.map(|file_path| file_path.strip_prefix(&path).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 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"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
use log::debug;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
basic_types::{Game, Link, ModdedInstance, RootConfig},
|
|
||||||
utils::walk_files_recursive,
|
|
||||||
};
|
|
||||||
use std::io::Write;
|
|
||||||
use std::{fs, io, os::unix, path::Path};
|
|
||||||
|
|
||||||
pub fn link_instance_to_target(
|
|
||||||
root_config: &RootConfig,
|
|
||||||
instance: &ModdedInstance,
|
|
||||||
target: impl AsRef<Path>,
|
|
||||||
) -> Result<(), io::Error> {
|
|
||||||
debug!("Linking instance to {}", target.as_ref().to_string_lossy());
|
|
||||||
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 link in installed_mod.files() {
|
|
||||||
let link_target = mod_source_root.join(&link.src);
|
|
||||||
let link_name = target.as_ref().join(&link.dst);
|
|
||||||
link_file(&link_target, &link_name)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn link_game_to_target(game: &Game, target: impl AsRef<Path>) -> Result<(), io::Error> {
|
|
||||||
for link in game.export_links()? {
|
|
||||||
link_file(&link.src, &target.as_ref().join(&link.dst))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
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(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub 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 create_symlink_for_file(target: &Path, link_name: &Path) -> io::Result<()> {
|
|
||||||
let absolute_path = fs::canonicalize(target)?;
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
unix::fs::symlink(absolute_path, link_name)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
{
|
|
||||||
std::os::windows::fs::symlink_file(target, link_name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,87 +1,69 @@
|
|||||||
use std::{
|
|
||||||
io,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
};
|
|
||||||
|
|
||||||
use libloot::{
|
use libloot::{
|
||||||
Game, GameType,
|
Game, GameType,
|
||||||
error::{GameHandleCreationError, LoadPluginsError, SortPluginsError},
|
error::{GameHandleCreationError, LoadPluginsError, SortPluginsError},
|
||||||
};
|
};
|
||||||
use log::trace;
|
use log::trace;
|
||||||
|
use std::{io, path::Path};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
basic_types::{ModdedInstance, RootConfig},
|
basic_types::{self, ModdedInstance, RootConfig},
|
||||||
utils::walk_files_recursive,
|
utils::walk_files_recursive,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct LoadOrder {
|
pub fn create_loadorder(
|
||||||
game: libloot::Game,
|
|
||||||
target: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LoadOrder {
|
|
||||||
pub fn new(install_dir: impl AsRef<Path>, game_type: GameType) -> Result<Self, LoadOrderError> {
|
|
||||||
Ok(Self {
|
|
||||||
game: Game::with_local_path(
|
|
||||||
game_type,
|
|
||||||
install_dir.as_ref(),
|
|
||||||
&install_dir.as_ref().join("appdata"),
|
|
||||||
)?,
|
|
||||||
target: install_dir.as_ref().to_owned(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_plugins_from_instance(
|
|
||||||
&mut self,
|
|
||||||
root_config: &RootConfig,
|
root_config: &RootConfig,
|
||||||
|
game: &basic_types::Game,
|
||||||
instance: &ModdedInstance,
|
instance: &ModdedInstance,
|
||||||
) -> Result<(), LoadOrderError> {
|
) -> Result<Vec<String>, LoadOrderError> {
|
||||||
for installed_mod in &instance.mods {
|
let mut loot_game = Game::new(GameType::SkyrimSE, &game.install_location)?;
|
||||||
|
|
||||||
|
// Add plugins files from the game install
|
||||||
|
let install_plugins: Vec<_> = walk_files_recursive(game.install_location.join("Data"))?
|
||||||
|
.filter(|f| is_plugin_file(f.path()))
|
||||||
|
.map(|f| f.path())
|
||||||
|
.collect();
|
||||||
|
let refs: Vec<_> = 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.get_mod_by_id(&installed_mod.mod_id()).unwrap();
|
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_source_root = root_config.get_mod_location(&mod_config);
|
||||||
|
|
||||||
let mod_plugins: Vec<PathBuf> = installed_mod
|
installed_mod
|
||||||
.files()
|
.files()
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|f| Self::is_plugin_file(&f.dst))
|
.filter(|f| is_plugin_file(&f.dst))
|
||||||
.map(|link| mod_source_root.join(&link.src))
|
.map(move |link| mod_source_root.join(&link.src))
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let refs: Vec<_> = mod_plugins.iter().map(|e| e.as_path()).collect();
|
let refs: Vec<_> = instance_plugins.iter().map(|e| e.as_path()).collect();
|
||||||
trace!("Loading {} plugins to game", refs.len());
|
|
||||||
self.game.load_plugins(&refs)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_plugins_from_install(&mut self) -> Result<(), LoadOrderError> {
|
|
||||||
let plugins: Vec<_> = walk_files_recursive(self.target.join("Data"))?
|
|
||||||
.filter(|f| Self::is_plugin_file(f.path()))
|
|
||||||
.map(|f| f.path())
|
|
||||||
.collect();
|
|
||||||
let refs: Vec<_> = plugins.iter().map(|e| e.as_path()).collect();
|
|
||||||
trace!("Loading {} plugins to game", refs.len());
|
trace!("Loading {} plugins to game", refs.len());
|
||||||
self.game.load_plugins(&refs)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_plugin_file(filename: impl AsRef<Path>) -> bool {
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_plugin_file(filename: impl AsRef<Path>) -> bool {
|
||||||
filename
|
filename
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.extension()
|
.extension()
|
||||||
.is_some_and(|ext| ext == "esp" || ext == "esm" || ext == "esl")
|
.is_some_and(|ext| ext == "esp" || ext == "esm" || ext == "esl")
|
||||||
}
|
|
||||||
|
|
||||||
pub fn load_order(&self) -> Result<Vec<String>, LoadOrderError> {
|
|
||||||
trace!("Generating new load order");
|
|
||||||
let all_plugins = self.game.loaded_plugins();
|
|
||||||
let plugins_names: Vec<&str> = all_plugins.iter().map(|e| e.name()).collect();
|
|
||||||
|
|
||||||
let sorted = self.game.sort_plugins(&plugins_names)?;
|
|
||||||
|
|
||||||
Ok(sorted)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
|
|||||||
220
src/main.rs
220
src/main.rs
@@ -1,205 +1,71 @@
|
|||||||
|
use anyhow::anyhow;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use globset::{Glob, GlobSet, GlobSetBuilder};
|
|
||||||
use log::debug;
|
use log::debug;
|
||||||
use std::{
|
use std::{error::Error, path::Path};
|
||||||
error::Error,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
basic_types::{ModConfig, ModFile, ModdedInstance, RootConfig},
|
activator::activate_instance,
|
||||||
|
basic_types::RootConfig,
|
||||||
cli::Args,
|
cli::Args,
|
||||||
linker::{create_plugins_txt, link_game_to_target, link_instance_to_target},
|
instance::{files_to_install_mod, insert_mod_to_instance},
|
||||||
load_order::LoadOrder,
|
|
||||||
mod_config_installer::FomodInstaller,
|
|
||||||
utils::{resolve_case_insensitive, walk_files_recursive},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
mod activator;
|
||||||
mod basic_types;
|
mod basic_types;
|
||||||
mod cli;
|
mod cli;
|
||||||
mod conflict_resolver;
|
mod file_conflict_solver;
|
||||||
mod fomod;
|
mod fomod;
|
||||||
mod install_prompt;
|
mod install_prompt;
|
||||||
mod linker;
|
mod instance;
|
||||||
mod load_order;
|
mod load_order;
|
||||||
mod mod_config_installer;
|
mod mod_config_installer;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
pub fn gen_filelist_for_mod(
|
fn command_activate(
|
||||||
root_config: &RootConfig,
|
|
||||||
instance: &ModdedInstance,
|
|
||||||
mod_config: &ModConfig,
|
|
||||||
) -> Result<Vec<ModFile>, Box<dyn Error>> {
|
|
||||||
let mod_location = root_config.get_mod_location(mod_config);
|
|
||||||
|
|
||||||
if mod_config.is_root_mod() {
|
|
||||||
return gen_filelist_for_root_mod(mod_config, mod_location);
|
|
||||||
}
|
|
||||||
|
|
||||||
let module_config_path = resolve_case_insensitive(&mod_location, "fomod/ModuleConfig.xml")?;
|
|
||||||
|
|
||||||
let files: Vec<ModFile> = match module_config_path {
|
|
||||||
Some(path) => {
|
|
||||||
debug!("Found a ModuleConfig.xml");
|
|
||||||
gen_filelist_from_fomod(instance, path, &mod_location)?
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
debug!("Did not find a ModuleConfig.xml");
|
|
||||||
gen_filelist_from_mod_dir(&mod_location)?
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(files)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_glob_filter(rules: &[String]) -> Result<GlobSet, Box<dyn Error>> {
|
|
||||||
let mut builder = GlobSetBuilder::new();
|
|
||||||
|
|
||||||
for p in rules {
|
|
||||||
builder.add(Glob::new(p)?);
|
|
||||||
}
|
|
||||||
|
|
||||||
let set = builder.build()?;
|
|
||||||
Ok(set)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn gen_filelist_for_root_mod(
|
|
||||||
mod_config: &ModConfig,
|
|
||||||
mod_location: impl AsRef<Path>,
|
|
||||||
) -> Result<Vec<ModFile>, Box<dyn Error>> {
|
|
||||||
let glob_filter = create_glob_filter(mod_config.ignore())?;
|
|
||||||
let files: Vec<_> = walk_files_recursive(&mod_location)?
|
|
||||||
.map(|entry| entry.path())
|
|
||||||
.map(|path| 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn gen_filelist_from_mod_dir(
|
|
||||||
mod_location: impl AsRef<Path>,
|
|
||||||
) -> Result<Vec<ModFile>, Box<dyn Error>> {
|
|
||||||
// Check for Data dir in root
|
|
||||||
let files = match resolve_case_insensitive(&mod_location, "data")? {
|
|
||||||
Some(data_path) => gen_filelist_from_dir(data_path),
|
|
||||||
None => gen_filelist_from_dir(&mod_location),
|
|
||||||
}?;
|
|
||||||
|
|
||||||
let mod_files: Vec<_> = files
|
|
||||||
.iter()
|
|
||||||
.filter(|e| should_be_included(e))
|
|
||||||
.map(|e| ModFile::new(e, PathBuf::from("Data").join(e), 0))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Ok(mod_files)
|
|
||||||
}
|
|
||||||
|
|
||||||
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"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn gen_filelist_from_dir(path: impl AsRef<Path>) -> Result<Vec<PathBuf>, Box<dyn Error>> {
|
|
||||||
let files: Vec<_> = walk_files_recursive(&path)?
|
|
||||||
.map(|file| file.path().strip_prefix(&path).unwrap().to_owned())
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Ok(files)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn gen_filelist_from_fomod(
|
|
||||||
instance: &ModdedInstance,
|
|
||||||
xml_path: impl AsRef<Path>,
|
|
||||||
mod_location: impl AsRef<Path>,
|
|
||||||
) -> Result<Vec<ModFile>, Box<dyn Error>> {
|
|
||||||
let module_config = fomod::Config::load_from_file(xml_path)?;
|
|
||||||
// TODO: add active plugins from instance config
|
|
||||||
let installer = FomodInstaller::new(module_config, vec![], install_prompt::prompt);
|
|
||||||
let files = installer.run();
|
|
||||||
|
|
||||||
let mod_files: Vec<_> = files
|
|
||||||
.iter()
|
|
||||||
.flat_map(|f| ModFile::from_installer(f.clone(), &mod_location).unwrap())
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Ok(mod_files)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn activate_instance(
|
|
||||||
root_config: &RootConfig,
|
root_config: &RootConfig,
|
||||||
instance_id: &str,
|
instance_id: &str,
|
||||||
target: &Path,
|
target: impl AsRef<Path>,
|
||||||
) -> Result<(), Box<dyn Error>> {
|
) -> anyhow::Result<()> {
|
||||||
let instance_config = root_config.load_instance_by_id(instance_id)?;
|
let instance = root_config.load_instance_by_id(instance_id)?;
|
||||||
|
activate_instance(root_config, &instance, target)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
link_game_to_target(root_config.games.first().unwrap(), target)?;
|
fn command_add(root_config: &RootConfig, instance_id: &str, mod_id: &str) -> anyhow::Result<()> {
|
||||||
link_instance_to_target(root_config, &instance_config, target)?;
|
let mut instance = root_config.load_instance_by_id(instance_id)?;
|
||||||
create_plugins_txt(&instance_config, target)?;
|
let mod_to_install = root_config
|
||||||
|
.get_mod_by_id(mod_id)
|
||||||
|
.ok_or(anyhow!("Can't find mod in config"))?;
|
||||||
|
|
||||||
|
let files = files_to_install_mod(root_config, &instance, &mod_to_install)?;
|
||||||
|
|
||||||
|
insert_mod_to_instance(&mut instance, &mod_to_install, &files, 0);
|
||||||
|
|
||||||
|
let path = &root_config.get_instance_config(instance_id).unwrap().path;
|
||||||
|
instance.save_to_file(path)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_mod_to_instance(
|
fn command_order(root_config: &RootConfig, instance_id: &str) -> anyhow::Result<()> {
|
||||||
root_config: &RootConfig,
|
let mut instance = root_config.load_instance_by_id(instance_id)?;
|
||||||
instance_id: &str,
|
let game = root_config
|
||||||
mod_id: &str,
|
.games
|
||||||
) -> Result<(), Box<dyn Error>> {
|
.first()
|
||||||
let mut instance_config = root_config.load_instance_by_id(instance_id)?.clone();
|
.ok_or(anyhow!("TODO: get game from instance"))?;
|
||||||
let mod_config = root_config.get_mod_by_id(mod_id).ok_or("Mod not found")?;
|
|
||||||
|
|
||||||
let new_files = gen_filelist_for_mod(root_config, &instance_config, &mod_config)?;
|
let new_load_order = load_order::create_loadorder(root_config, game, &instance)?;
|
||||||
|
|
||||||
instance_config.add_mod(&mod_config, 0, &new_files);
|
instance.set_load_order(new_load_order);
|
||||||
|
|
||||||
let path = &root_config
|
instance.save_to_file(
|
||||||
|
root_config
|
||||||
.get_instance_config(instance_id)
|
.get_instance_config(instance_id)
|
||||||
.ok_or("Mod not found")?
|
.ok_or(anyhow!("Failed to find instance"))?
|
||||||
.path;
|
.path
|
||||||
instance_config.save_to_file(path)?;
|
.clone(),
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn order_plugins(root_config: &RootConfig, instance_id: &str) -> Result<(), Box<dyn Error>> {
|
|
||||||
let mut instance_config = root_config.load_instance_by_id(instance_id)?.clone();
|
|
||||||
|
|
||||||
let mut orderer = LoadOrder::new(
|
|
||||||
&root_config.games.first().unwrap().install_location,
|
|
||||||
libloot::GameType::SkyrimSE,
|
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
orderer.add_plugins_from_install()?;
|
|
||||||
orderer.add_plugins_from_instance(root_config, &instance_config)?;
|
|
||||||
|
|
||||||
let load_order = orderer.load_order()?;
|
|
||||||
|
|
||||||
instance_config.set_load_order(load_order);
|
|
||||||
|
|
||||||
let path = &root_config
|
|
||||||
.get_instance_config(instance_id)
|
|
||||||
.ok_or("Mod not found")?
|
|
||||||
.path;
|
|
||||||
|
|
||||||
instance_config.save_to_file(path)?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,13 +85,13 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
|
|
||||||
match args.command {
|
match args.command {
|
||||||
cli::Commands::Activate { instance, target } => {
|
cli::Commands::Activate { instance, target } => {
|
||||||
activate_instance(&root_config, &instance, &target)?;
|
command_activate(&root_config, &instance, &target)?;
|
||||||
}
|
}
|
||||||
cli::Commands::Add { instance, mod_id } => {
|
cli::Commands::Add { instance, mod_id } => {
|
||||||
add_mod_to_instance(&root_config, &instance, &mod_id)?;
|
command_add(&root_config, &instance, &mod_id)?;
|
||||||
}
|
}
|
||||||
cli::Commands::LoadOrder { instance } => {
|
cli::Commands::LoadOrder { instance } => {
|
||||||
order_plugins(&root_config, &instance)?;
|
command_order(&root_config, &instance)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use crate::fomod::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct InstallerState {
|
struct InstallerState {
|
||||||
flags: HashMap<String, String>,
|
flags: HashMap<String, String>,
|
||||||
selected_files: Vec<FileTypeEnum>,
|
selected_files: Vec<FileTypeEnum>,
|
||||||
}
|
}
|
||||||
@@ -39,7 +39,7 @@ impl InstallerState {
|
|||||||
self.selected_files.extend_from_slice(list);
|
self.selected_files.extend_from_slice(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn into_file_list(self) -> Vec<FileTypeEnum> {
|
fn into_file_list(self) -> Vec<FileTypeEnum> {
|
||||||
self.selected_files
|
self.selected_files
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -171,34 +171,19 @@ fn resolve_plugin_type(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct FomodInstaller {
|
pub fn run_fomod_installer(
|
||||||
config: Config,
|
fomod_config: Config,
|
||||||
installed_plugins: Vec<String>,
|
installed_plugins: &[String],
|
||||||
group_prompt: fn(GroupPrompt) -> Vec<usize>,
|
group_prompt: fn(GroupPrompt) -> Vec<usize>,
|
||||||
}
|
) -> anyhow::Result<Vec<FileTypeEnum>> {
|
||||||
|
|
||||||
impl FomodInstaller {
|
|
||||||
pub fn new(
|
|
||||||
config: Config,
|
|
||||||
installed_plugins: Vec<String>,
|
|
||||||
group_promt: fn(GroupPrompt) -> Vec<usize>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
config,
|
|
||||||
installed_plugins,
|
|
||||||
group_prompt: group_promt,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run(self) -> Vec<FileTypeEnum> {
|
|
||||||
let mut state = InstallerState::new();
|
let mut state = InstallerState::new();
|
||||||
|
|
||||||
// Always-installed files first
|
// Always-installed files first
|
||||||
if let Some(required) = &self.config.required_install_files {
|
if let Some(required) = &fomod_config.required_install_files {
|
||||||
state.add_files(required);
|
state.add_files(required);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(install_steps) = &self.config.install_steps {
|
if let Some(install_steps) = fomod_config.install_steps {
|
||||||
let steps = &install_steps.install_step;
|
let steps = &install_steps.install_step;
|
||||||
|
|
||||||
for step in steps {
|
for step in steps {
|
||||||
@@ -206,7 +191,7 @@ impl FomodInstaller {
|
|||||||
if step
|
if step
|
||||||
.visible
|
.visible
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.is_some_and(|v| !evaluate_dependency(v, &state, &self.installed_plugins))
|
.is_some_and(|v| !evaluate_dependency(v, &state, installed_plugins))
|
||||||
{
|
{
|
||||||
// Dependency to show the step not meet. Skipping.
|
// Dependency to show the step not meet. Skipping.
|
||||||
continue;
|
continue;
|
||||||
@@ -215,9 +200,9 @@ impl FomodInstaller {
|
|||||||
for group in &step.optional_file_groups.group {
|
for group in &step.optional_file_groups.group {
|
||||||
// TODO: Skip groups where all plugins are NotUsable
|
// TODO: Skip groups where all plugins are NotUsable
|
||||||
|
|
||||||
let prompt = GroupPrompt::new(group, &state, &self.installed_plugins);
|
let prompt = GroupPrompt::new(group, &state, installed_plugins);
|
||||||
|
|
||||||
let selected_plugins = (self.group_prompt)(prompt);
|
let selected_plugins = (group_prompt)(prompt);
|
||||||
|
|
||||||
for i in selected_plugins {
|
for i in selected_plugins {
|
||||||
let plugin = &group.plugins.plugin[i];
|
let plugin = &group.plugins.plugin[i];
|
||||||
@@ -239,14 +224,13 @@ impl FomodInstaller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Evaluate conditional file installs based on final flag state
|
// Evaluate conditional file installs based on final flag state
|
||||||
if let Some(conditional) = &self.config.conditional_file_installs {
|
if let Some(conditional) = &fomod_config.conditional_file_installs {
|
||||||
for pattern in &conditional.patterns.pattern {
|
for pattern in &conditional.patterns.pattern {
|
||||||
if evaluate_dependency(&pattern.dependencies, &state, &self.installed_plugins) {
|
if evaluate_dependency(&pattern.dependencies, &state, installed_plugins) {
|
||||||
state.add_files(&pattern.files);
|
state.add_files(&pattern.files);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
state.into_file_list()
|
Ok(state.into_file_list())
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user