Compare commits
4 Commits
bc2ed1d2e3
...
afb53e9022
| Author | SHA1 | Date | |
|---|---|---|---|
|
afb53e9022
|
|||
|
8fc5480243
|
|||
|
50151d30df
|
|||
|
2bf59a17f8
|
@@ -152,6 +152,11 @@ impl InstalledMod {
|
|||||||
self.files.push((file.source.clone(), file.dest.clone()));
|
self.files.push((file.source.clone(), file.dest.clone()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the id of the mod
|
||||||
|
pub fn mod_id(&self) -> String {
|
||||||
|
self.id.clone()
|
||||||
|
}
|
||||||
|
|
||||||
/// The priority over other mods. Only used when 2 files conflict.
|
/// The priority over other mods. Only used when 2 files conflict.
|
||||||
pub fn priority(&self) -> isize {
|
pub fn priority(&self) -> isize {
|
||||||
self.priority
|
self.priority
|
||||||
|
|||||||
165
src/linker.rs
165
src/linker.rs
@@ -1,148 +1,61 @@
|
|||||||
use std::{
|
use std::{fs, io, os::unix, path::Path};
|
||||||
fs, io,
|
|
||||||
os::unix,
|
use crate::{
|
||||||
path::{Path, PathBuf},
|
basic_types::{ModdedInstance, RootConfig},
|
||||||
|
utils::walk_files_recursive,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{Mod, ModFile, fomod::FileTypeEnum};
|
pub fn link_instance_to_target(
|
||||||
|
root_config: &RootConfig,
|
||||||
|
instance: &ModdedInstance,
|
||||||
|
target: impl AsRef<Path>,
|
||||||
|
) -> Result<(), io::Error> {
|
||||||
|
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);
|
||||||
|
|
||||||
pub struct Linker {
|
for (src, dst) in installed_mod.files() {
|
||||||
target: PathBuf,
|
let link_target = mod_source_root.join(src);
|
||||||
game_dir: PathBuf,
|
let link_name = target.as_ref().join(dst);
|
||||||
}
|
link_file(&link_target, &link_name)?;
|
||||||
|
|
||||||
impl Linker {
|
|
||||||
pub fn new(target_path: &Path, game_dir: &Path) -> Self {
|
|
||||||
Self {
|
|
||||||
target: target_path.to_owned(),
|
|
||||||
game_dir: game_dir.to_owned(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn link_file(&self, from: &Path, to: &Path) -> Result<(), LinkerError> {
|
Ok(())
|
||||||
let target = self.target.join(to);
|
}
|
||||||
|
|
||||||
if let Some(parent) = target.parent() {
|
pub fn link_game_to_target(
|
||||||
|
game_dir: impl AsRef<Path>,
|
||||||
|
target: impl AsRef<Path>,
|
||||||
|
) -> Result<(), io::Error> {
|
||||||
|
walk_files_recursive(&game_dir)?.try_for_each(|file| {
|
||||||
|
let link_target = file.path();
|
||||||
|
let link_name = target
|
||||||
|
.as_ref()
|
||||||
|
.join(file.path().strip_prefix(&game_dir).unwrap());
|
||||||
|
link_file(&link_target, &link_name)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn link_file(target: &Path, link_name: &Path) -> Result<(), io::Error> {
|
||||||
|
if let Some(parent) = link_name.parent() {
|
||||||
fs::create_dir_all(parent)?;
|
fs::create_dir_all(parent)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
create_symlink_for_file(from, &target)?;
|
create_symlink_for_file(target, link_name)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn link_recursive(&self, from: &Path, to: &Path) -> Result<(), LinkerError> {
|
fn create_symlink_for_file(target: &Path, link_name: &Path) -> io::Result<()> {
|
||||||
for entry in fs::read_dir(from)? {
|
let absolute_path = fs::canonicalize(target)?;
|
||||||
let entry = entry?;
|
|
||||||
let entry_path = entry.path();
|
|
||||||
let relative = entry_path
|
|
||||||
.strip_prefix(from)
|
|
||||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
|
|
||||||
let target_path = to.join(relative);
|
|
||||||
|
|
||||||
if entry_path.is_dir() {
|
|
||||||
self.link_recursive(&entry_path, &target_path)?;
|
|
||||||
} else {
|
|
||||||
self.link_file(&entry_path, &target_path)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn link_mod_file(&self, file: &ModFile, from_mod: &Mod) -> Result<(), LinkerError> {
|
|
||||||
let src = from_mod.source.join(&file.source);
|
|
||||||
|
|
||||||
self.link_file(&src, &file.dest)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn link_plugin_files(
|
|
||||||
&self,
|
|
||||||
entries: &[FileTypeEnum],
|
|
||||||
mod_dir: &Path,
|
|
||||||
) -> Result<(), LinkerError> {
|
|
||||||
let mut sorted_entries = entries.to_owned();
|
|
||||||
sorted_entries.sort_by_cached_key(|e| match e {
|
|
||||||
FileTypeEnum::File(file_type) => file_type.priority.unwrap_or(0),
|
|
||||||
FileTypeEnum::Folder(file_type) => file_type.priority.unwrap_or(0),
|
|
||||||
});
|
|
||||||
|
|
||||||
for entry in sorted_entries {
|
|
||||||
match entry {
|
|
||||||
FileTypeEnum::File(file) => {
|
|
||||||
let from = mod_dir.join(file.source);
|
|
||||||
let to = Path::new("Data").join(file.destination.unwrap_or("".to_owned()));
|
|
||||||
self.link_file(&from, &to)?;
|
|
||||||
}
|
|
||||||
FileTypeEnum::Folder(folder) => {
|
|
||||||
let from = mod_dir.join(folder.source);
|
|
||||||
let to = Path::new("Data").join(folder.destination.unwrap_or("".to_owned()));
|
|
||||||
self.link_recursive(&from, &to)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn link_install_to_target(&self) -> Result<(), LinkerError> {
|
|
||||||
fn symlink_tree(src: &Path, dst: &Path) -> Result<(), LinkerError> {
|
|
||||||
if !dst.exists() {
|
|
||||||
fs::create_dir_all(dst)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
for entry in fs::read_dir(src)? {
|
|
||||||
let entry = entry?;
|
|
||||||
let src_path = entry.path();
|
|
||||||
let dst_path = dst.join(entry.path().file_name().unwrap());
|
|
||||||
|
|
||||||
let meta = entry.metadata()?;
|
|
||||||
if meta.is_dir() {
|
|
||||||
symlink_tree(&src_path, &dst_path)?;
|
|
||||||
} else if meta.is_file() {
|
|
||||||
create_symlink_for_file(&src_path, &dst_path)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
symlink_tree(&self.game_dir, &self.target)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum LinkerError {
|
|
||||||
Io(io::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for LinkerError {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
Self::Io(e) => write!(f, "IO error: {}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::error::Error for LinkerError {}
|
|
||||||
|
|
||||||
impl From<io::Error> for LinkerError {
|
|
||||||
fn from(e: io::Error) -> Self {
|
|
||||||
Self::Io(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_symlink_for_file(src: &Path, dst: &Path) -> io::Result<()> {
|
|
||||||
let absolute_path = fs::canonicalize(src)?;
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
unix::fs::symlink(absolute_path, dst)
|
unix::fs::symlink(absolute_path, link_name)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
std::os::windows::fs::symlink_file(src, dst)
|
std::os::windows::fs::symlink_file(target, link_name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
78
src/load_order.rs
Normal file
78
src/load_order.rs
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
use std::{
|
||||||
|
error::Error,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
use libloot::{Game, GameType};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
basic_types::{ModdedInstance, RootConfig},
|
||||||
|
utils::walk_files_recursive,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct LoadOrder {
|
||||||
|
game: libloot::Game,
|
||||||
|
target: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LoadOrder {
|
||||||
|
pub fn new(install_dir: impl AsRef<Path>, game_type: GameType) -> Result<Self, Box<dyn Error>> {
|
||||||
|
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,
|
||||||
|
instance: &ModdedInstance,
|
||||||
|
) -> Result<(), Box<dyn Error>> {
|
||||||
|
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);
|
||||||
|
|
||||||
|
let mod_plugins: Vec<PathBuf> = installed_mod
|
||||||
|
.files()
|
||||||
|
.iter()
|
||||||
|
.filter(|f| Self::is_plugin_file(&f.0))
|
||||||
|
.map(|(from, _)| mod_source_root.join(from))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let refs: Vec<_> = mod_plugins.iter().map(|e| e.as_path()).collect();
|
||||||
|
self.game.load_plugins(&refs)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_plugins_from_install(&mut self) -> Result<(), Box<dyn Error>> {
|
||||||
|
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();
|
||||||
|
dbg!(&refs);
|
||||||
|
self.game.load_plugins(&refs)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_plugin_file(filename: impl AsRef<Path>) -> bool {
|
||||||
|
filename
|
||||||
|
.as_ref()
|
||||||
|
.extension()
|
||||||
|
.is_some_and(|ext| ext == "esp" || ext == "esm" || ext == "esl")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_order(&self) -> Result<Vec<String>, Box<dyn Error>> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
use std::{
|
|
||||||
fmt::Display,
|
|
||||||
fs,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
};
|
|
||||||
|
|
||||||
use libloot::{
|
|
||||||
Game, GameType,
|
|
||||||
error::{GameHandleCreationError, LoadPluginsError, SortPluginsError},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{Mod, ModFile};
|
|
||||||
|
|
||||||
pub struct LootOrder {
|
|
||||||
game: libloot::Game,
|
|
||||||
target: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LootOrder {
|
|
||||||
pub fn new(install_dir: &Path, game_type: GameType) -> Result<Self, LootOrderError> {
|
|
||||||
Ok(Self {
|
|
||||||
game: Game::with_local_path(game_type, install_dir, &install_dir.join("appdata"))?,
|
|
||||||
target: install_dir.to_owned(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_plugin(&mut self, file: &ModFile) -> Result<(), LootOrderError> {
|
|
||||||
if !Self::is_plugin_file(file.dest.to_str().unwrap_or_default()) {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let rel_path = self.target.join(&file.dest);
|
|
||||||
let absolute_path = fs::canonicalize(rel_path).unwrap();
|
|
||||||
|
|
||||||
self.game.load_plugins(&[&absolute_path])?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_plugin_file(filename: &str) -> bool {
|
|
||||||
filename.ends_with(".esp") || filename.ends_with(".esm") || filename.ends_with(".esl")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn load_order(&self) -> Result<(), LootOrderError> {
|
|
||||||
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)?;
|
|
||||||
|
|
||||||
dbg!(sorted);
|
|
||||||
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum LootOrderError {
|
|
||||||
Create(GameHandleCreationError),
|
|
||||||
Load(LoadPluginsError),
|
|
||||||
Sort(SortPluginsError),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::error::Error for LootOrderError {}
|
|
||||||
|
|
||||||
impl Display for LootOrderError {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
LootOrderError::Create(game_handle_creation_error) => write!(
|
|
||||||
f,
|
|
||||||
"Failed to create LootOrder: {}",
|
|
||||||
game_handle_creation_error
|
|
||||||
),
|
|
||||||
LootOrderError::Load(load_plugins_error) => {
|
|
||||||
write!(f, "Failed to load plugin: {}", load_plugins_error)
|
|
||||||
}
|
|
||||||
LootOrderError::Sort(sort_plugins_error) => {
|
|
||||||
write!(f, "Failed to sort: {}", sort_plugins_error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<GameHandleCreationError> for LootOrderError {
|
|
||||||
fn from(e: GameHandleCreationError) -> Self {
|
|
||||||
Self::Create(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<LoadPluginsError> for LootOrderError {
|
|
||||||
fn from(e: LoadPluginsError) -> Self {
|
|
||||||
Self::Load(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<SortPluginsError> for LootOrderError {
|
|
||||||
fn from(e: SortPluginsError) -> Self {
|
|
||||||
Self::Sort(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
28
src/main.rs
28
src/main.rs
@@ -3,6 +3,8 @@ use std::{error::Error, path::Path};
|
|||||||
use crate::{
|
use crate::{
|
||||||
basic_types::{ModConfig, ModFile, ModdedInstance, RootConfig},
|
basic_types::{ModConfig, ModFile, ModdedInstance, RootConfig},
|
||||||
fomod::Config,
|
fomod::Config,
|
||||||
|
linker::{link_game_to_target, link_instance_to_target},
|
||||||
|
load_order::LoadOrder,
|
||||||
mod_config_installer::FomodInstaller,
|
mod_config_installer::FomodInstaller,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -10,6 +12,8 @@ mod basic_types;
|
|||||||
mod conflict_resolver;
|
mod conflict_resolver;
|
||||||
mod fomod;
|
mod fomod;
|
||||||
mod install_prompt;
|
mod install_prompt;
|
||||||
|
mod linker;
|
||||||
|
mod load_order;
|
||||||
mod mod_config_installer;
|
mod mod_config_installer;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
@@ -43,15 +47,29 @@ pub fn gen_filelist_for_mod(
|
|||||||
fn main() -> Result<(), Box<dyn Error>> {
|
fn main() -> Result<(), Box<dyn Error>> {
|
||||||
let root_config = RootConfig::load_from_file("./data/example.toml")?;
|
let root_config = RootConfig::load_from_file("./data/example.toml")?;
|
||||||
|
|
||||||
let mut new_instance = ModdedInstance::new("My Instance");
|
// let mut new_instance = ModdedInstance::new("My Instance");
|
||||||
|
// let mod_to_install = root_config.get_mod_by_id("ineed").unwrap();
|
||||||
|
// let new_files = gen_filelist_for_mod(&root_config, &new_instance, &mod_to_install)?;
|
||||||
|
// new_instance.add_mod(&mod_to_install, 0, &new_files);
|
||||||
|
// new_instance.save_to_file("./data/my_instance.toml")?;
|
||||||
|
|
||||||
let mod_to_install = root_config.get_mod_by_id("ineed").unwrap();
|
let modded_instance = ModdedInstance::load_from_file("./data/my_instance.toml")?;
|
||||||
|
|
||||||
let new_files = gen_filelist_for_mod(&root_config, &new_instance, &mod_to_install)?;
|
link_game_to_target(
|
||||||
|
&root_config.games.first().unwrap().install_location,
|
||||||
|
"./data/target",
|
||||||
|
)?;
|
||||||
|
link_instance_to_target(&root_config, &modded_instance, "./data/target")?;
|
||||||
|
|
||||||
new_instance.add_mod(&mod_to_install, 0, &new_files);
|
let mut orderer = LoadOrder::new(
|
||||||
|
&root_config.games.first().unwrap().install_location,
|
||||||
|
libloot::GameType::SkyrimSE,
|
||||||
|
)?;
|
||||||
|
|
||||||
new_instance.save_to_file("./data/my_instance.toml")?;
|
orderer.add_plugins_from_install()?;
|
||||||
|
orderer.add_plugins_from_instance(&root_config, &modded_instance)?;
|
||||||
|
|
||||||
|
orderer.load_order()?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user