Compare commits

..

4 Commits

Author SHA1 Message Date
afb53e9022 updated wip main 2026-03-01 23:19:26 +01:00
8fc5480243 fixed load_order 2026-03-01 23:18:59 +01:00
50151d30df refactored linker 2026-03-01 22:21:00 +01:00
2bf59a17f8 added mod_id to InstalledMod 2026-03-01 22:20:26 +01:00
5 changed files with 148 additions and 232 deletions

View File

@@ -152,6 +152,11 @@ impl InstalledMod {
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.
pub fn priority(&self) -> isize {
self.priority

View File

@@ -1,148 +1,61 @@
use std::{
fs, io,
os::unix,
path::{Path, PathBuf},
use std::{fs, io, os::unix, path::Path};
use crate::{
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 {
target: PathBuf,
game_dir: PathBuf,
}
impl Linker {
pub fn new(target_path: &Path, game_dir: &Path) -> Self {
Self {
target: target_path.to_owned(),
game_dir: game_dir.to_owned(),
for (src, dst) in installed_mod.files() {
let link_target = mod_source_root.join(src);
let link_name = target.as_ref().join(dst);
link_file(&link_target, &link_name)?;
}
}
fn link_file(&self, from: &Path, to: &Path) -> Result<(), LinkerError> {
let target = self.target.join(to);
Ok(())
}
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)?;
}
create_symlink_for_file(from, &target)?;
create_symlink_for_file(target, link_name)?;
Ok(())
}
fn link_recursive(&self, from: &Path, to: &Path) -> Result<(), LinkerError> {
for entry in fs::read_dir(from)? {
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)?;
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, dst)
unix::fs::symlink(absolute_path, link_name)
}
#[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
View 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)
}
}

View File

@@ -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)
}
}

View File

@@ -3,6 +3,8 @@ use std::{error::Error, path::Path};
use crate::{
basic_types::{ModConfig, ModFile, ModdedInstance, RootConfig},
fomod::Config,
linker::{link_game_to_target, link_instance_to_target},
load_order::LoadOrder,
mod_config_installer::FomodInstaller,
};
@@ -10,6 +12,8 @@ mod basic_types;
mod conflict_resolver;
mod fomod;
mod install_prompt;
mod linker;
mod load_order;
mod mod_config_installer;
mod utils;
@@ -43,15 +47,29 @@ pub fn gen_filelist_for_mod(
fn main() -> Result<(), Box<dyn Error>> {
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(())
}