Compare commits
5 Commits
2a97995469
...
bc2ed1d2e3
| Author | SHA1 | Date | |
|---|---|---|---|
|
bc2ed1d2e3
|
|||
|
fe0659ea14
|
|||
|
86b2139759
|
|||
|
03032586ed
|
|||
|
8d8270ebb0
|
55
Cargo.lock
generated
55
Cargo.lock
generated
@@ -183,6 +183,7 @@ dependencies = [
|
|||||||
"libloot",
|
"libloot",
|
||||||
"quick-xml",
|
"quick-xml",
|
||||||
"serde",
|
"serde",
|
||||||
|
"toml",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -566,6 +567,15 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_spanned"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.117"
|
version = "2.0.117"
|
||||||
@@ -606,6 +616,45 @@ dependencies = [
|
|||||||
"crunchy",
|
"crunchy",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml"
|
||||||
|
version = "1.0.3+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c7614eaf19ad818347db24addfa201729cf2a9b6fdfd9eb0ab870fcacc606c0c"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"serde_core",
|
||||||
|
"serde_spanned",
|
||||||
|
"toml_datetime",
|
||||||
|
"toml_parser",
|
||||||
|
"toml_writer",
|
||||||
|
"winnow",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_datetime"
|
||||||
|
version = "1.0.0+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_parser"
|
||||||
|
version = "1.0.9+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4"
|
||||||
|
dependencies = [
|
||||||
|
"winnow",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_writer"
|
||||||
|
version = "1.0.6+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ucd-trie"
|
name = "ucd-trie"
|
||||||
version = "0.1.7"
|
version = "0.1.7"
|
||||||
@@ -653,3 +702,9 @@ checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winnow"
|
||||||
|
version = "0.7.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
|
||||||
|
|||||||
@@ -7,3 +7,4 @@ edition = "2024"
|
|||||||
libloot = "0.29.0"
|
libloot = "0.29.0"
|
||||||
quick-xml = { version = "0.39.2", features = ["serde-types", "serialize"] }
|
quick-xml = { version = "0.39.2", features = ["serde-types", "serialize"] }
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
|
toml = "1.0.3"
|
||||||
|
|||||||
231
src/basic_types.rs
Normal file
231
src/basic_types.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
11
src/fomod.rs
11
src/fomod.rs
@@ -2,6 +2,8 @@
|
|||||||
// https://github.com/luctius/fomod/
|
// https://github.com/luctius/fomod/
|
||||||
// Original license: MIT / Apache-2.0
|
// Original license: MIT / Apache-2.0
|
||||||
|
|
||||||
|
use std::{error::Error, fs, path::Path};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, PartialEq)]
|
#[derive(Debug, Deserialize, PartialEq)]
|
||||||
@@ -41,6 +43,15 @@ pub struct Config {
|
|||||||
pub conditional_file_installs: Option<ConditionalFileInstallList>,
|
pub conditional_file_installs: Option<ConditionalFileInstallList>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn load_from_file(path: impl AsRef<Path>) -> Result<Self, Box<dyn Error>> {
|
||||||
|
let data = fs::read_to_string(path)?;
|
||||||
|
let config = quick_xml::de::from_str(&data)?;
|
||||||
|
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(
|
#[derive(
|
||||||
Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash, Default,
|
Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash, Default,
|
||||||
)]
|
)]
|
||||||
|
|||||||
146
src/main.rs
146
src/main.rs
@@ -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(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,7 @@ use crate::fomod::{
|
|||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct InstallerState {
|
pub struct InstallerState {
|
||||||
/// Flags set by plugin selections throughout the install
|
|
||||||
flags: HashMap<String, String>,
|
flags: HashMap<String, String>,
|
||||||
|
|
||||||
/// Files to install, keyed by destination path.
|
|
||||||
/// Higher priority value wins when destinations conflict.
|
|
||||||
selected_files: Vec<FileTypeEnum>,
|
selected_files: Vec<FileTypeEnum>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,6 +95,29 @@ pub struct GroupPrompt {
|
|||||||
pub options: Vec<InstallOption>,
|
pub options: Vec<InstallOption>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl GroupPrompt {
|
||||||
|
fn new(group: &Group, state: &InstallerState, installed_plugins: &[String]) -> Self {
|
||||||
|
let options = group
|
||||||
|
.plugins
|
||||||
|
.plugin
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(idx, option)| InstallOption {
|
||||||
|
name: option.name.trim().to_owned(),
|
||||||
|
option_type: resolve_plugin_type(option, state, installed_plugins),
|
||||||
|
description: option.description.trim().to_owned(),
|
||||||
|
idx,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
name: group.name.clone(),
|
||||||
|
select_type: group.typ,
|
||||||
|
options,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct InstallOption {
|
pub struct InstallOption {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub option_type: PluginTypeEnum,
|
pub option_type: PluginTypeEnum,
|
||||||
@@ -112,31 +131,6 @@ impl Display for InstallOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_group_prompt(
|
|
||||||
group: &Group,
|
|
||||||
state: &InstallerState,
|
|
||||||
installed_plugins: &[String],
|
|
||||||
) -> GroupPrompt {
|
|
||||||
let options = group
|
|
||||||
.plugins
|
|
||||||
.plugin
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(idx, option)| InstallOption {
|
|
||||||
name: option.name.trim().to_owned(),
|
|
||||||
option_type: resolve_plugin_type(option, state, installed_plugins),
|
|
||||||
description: option.description.trim().to_owned(),
|
|
||||||
idx,
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
GroupPrompt {
|
|
||||||
name: group.name.clone(),
|
|
||||||
select_type: group.typ,
|
|
||||||
options,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Each plugin in the install steps have a type. e.g.: Optional, Required, Recommended
|
/// Each plugin in the install steps have a type. e.g.: Optional, Required, Recommended
|
||||||
/// But the type depends on dependencies
|
/// But the type depends on dependencies
|
||||||
/// This function eveluates the plugin type
|
/// This function eveluates the plugin type
|
||||||
@@ -207,7 +201,7 @@ 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 = create_group_prompt(group, &state, &self.installed_plugins);
|
let prompt = GroupPrompt::new(group, &state, &self.installed_plugins);
|
||||||
|
|
||||||
let selected_plugins = (self.group_prompt)(prompt);
|
let selected_plugins = (self.group_prompt)(prompt);
|
||||||
|
|
||||||
|
|||||||
31
src/utils.rs
Normal file
31
src/utils.rs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
use std::{
|
||||||
|
fs::{self, DirEntry},
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn path_to_lowercase(path: impl AsRef<Path>) -> PathBuf {
|
||||||
|
PathBuf::from(path.as_ref().to_string_lossy().to_lowercase())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user