Added mod installation. First working version.

master
hheik 2024-08-24 21:53:09 +03:00
parent 897857fc98
commit c1015f10fd
3 changed files with 188 additions and 12 deletions

135
src/installer.rs Normal file
View File

@ -0,0 +1,135 @@
use std::{
fs::{create_dir_all, read_dir, remove_dir_all, remove_file, DirEntry},
path::{Path, PathBuf},
};
use munsikka::*;
const IGNORED_INSTALL: [&str; 7] = [
"icon.png",
"README.md",
"manifest.json",
"LICENSE",
"LICENSE.md",
"LICENSE.txt",
"CHANGELOG.md",
];
// TODO: Remove hardcoding
const MOD_LOADER_FILES: [&str; 3] = ["BepInEx", "doorstop_config.ini", "winhttp.dll"];
/// Removes any mod files from the game directory
pub fn clean(game_dir: &Path) -> std::io::Result<()> {
println!("Cleaning old installation...");
MOD_LOADER_FILES
.iter()
.map(|item| game_dir.join(PathBuf::from(item)))
.for_each(|removal_target| {
// TODO: Only suppress NotFound errors
if removal_target.is_dir() {
remove_dir_all(removal_target).ok();
} else {
remove_file(removal_target).ok();
}
});
Ok(())
}
pub fn install(from: &Path, game_dir: &Path, mod_info: &ModInfo) -> std::io::Result<()> {
for path in [from, game_dir].iter() {
if !path.exists() {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("Couldn't find {path:?}"),
));
}
}
print!(
" {version: >10} | {name: <30} | {author: <30} ",
name = mod_info.name,
author = mod_info.author,
version = mod_info.version_string(),
);
let mut flavour_string = String::new();
for entry in read_dir(from)? {
let entry = entry.unwrap();
if IGNORED_INSTALL
.iter()
.any(|ignored| *ignored == entry.file_name())
{
continue;
}
match choose_install_strategy(&entry) {
InstallStrategy::PluginFile => {
recursive_install(
&entry.path(),
&game_dir.join(PathBuf::from("BepInEx/plugins")),
)
.unwrap();
flavour_string.push_str("📦");
}
InstallStrategy::LoaderSubDirectory => {
recursive_install(&entry.path(), &game_dir.join(PathBuf::from("BepInEx"))).unwrap();
flavour_string.push_str("📂");
}
InstallStrategy::LoaderDirectory => {
recursive_install(&entry.path(), &game_dir).unwrap();
flavour_string.push_str("📂");
}
InstallStrategy::GameDirectory => {
for item in MOD_LOADER_FILES.iter() {
recursive_install(&entry.path().join(PathBuf::from(item)), &game_dir).unwrap();
}
flavour_string.push_str("🛠️");
}
}
}
println!("{flavour_string}");
Ok(())
}
enum InstallStrategy {
/// Simple .dll files and other resources
PluginFile,
/// Directory contained within the mod loader directory
LoaderSubDirectory,
/// The directory is the top-level directory
LoaderDirectory,
/// Contents should be installed to the game directory, used by the mod loader.
GameDirectory,
}
fn choose_install_strategy(top_level_file: &DirEntry) -> InstallStrategy {
if top_level_file.file_name() == "BepInExPack" {
return InstallStrategy::GameDirectory;
}
if top_level_file.file_name() == "BepInEx" {
return InstallStrategy::LoaderDirectory;
}
if top_level_file.path().is_dir() {
return InstallStrategy::LoaderSubDirectory;
}
InstallStrategy::PluginFile
}
fn recursive_install(src: &Path, dest: &Path) -> std::io::Result<()> {
if src.is_dir() {
for entry in read_dir(src)? {
let entry = entry?;
recursive_install(
&src.join(entry.file_name()),
&dest.join(src.file_name().unwrap()),
)?;
}
} else {
let dest = dest.join(src.file_name().unwrap());
create_dir_all(dest.parent().unwrap())?;
std::fs::copy(src, dest)?;
}
Ok(())
}

View File

@ -9,13 +9,14 @@ use yaml_rust2::Yaml;
pub const BASE_URL: &str = "http://thunderstore.io"; pub const BASE_URL: &str = "http://thunderstore.io";
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct ModInfo { pub struct ModInfo {
pub name: String, pub name: String,
pub author: String, pub author: String,
pub version: (i64, i64, i64), pub version: (i64, i64, i64),
pub website_url: String, pub website_url: String,
pub enabled: bool, pub enabled: bool,
pub dependencies: Vec<Option<String>>,
} }
impl ModInfo { impl ModInfo {
@ -30,6 +31,11 @@ impl ModInfo {
), ),
website_url: yaml["websiteUrl"].as_str()?.to_owned(), website_url: yaml["websiteUrl"].as_str()?.to_owned(),
enabled: yaml["enabled"].as_bool()?, enabled: yaml["enabled"].as_bool()?,
dependencies: yaml["dependencies"]
.as_vec()?
.iter()
.map(|y| y.as_str().map(|str| str.to_owned()))
.collect(),
}) })
} }
@ -167,7 +173,7 @@ impl FetchExtractor {
} }
fn unzip_to(from: &Path, to: &Path) -> Result<(), ()> { fn unzip_to(from: &Path, to: &Path) -> Result<(), ()> {
// TODO: use the zip library // TODO: use a zip library
Command::new("unzip") Command::new("unzip")
.arg(from) .arg(from)
.arg("-d") .arg("-d")
@ -180,3 +186,11 @@ fn unzip_to(from: &Path, to: &Path) -> Result<(), ()> {
pub fn data_dir() -> PathBuf { pub fn data_dir() -> PathBuf {
PathBuf::from(var("HOME").unwrap()).join(".local/share/munsikka") PathBuf::from(var("HOME").unwrap()).join(".local/share/munsikka")
} }
pub fn game_dir() -> PathBuf {
match var("GAME_DIR").ok() {
Some(game_dir) => game_dir.into(),
None => PathBuf::from(var("HOME").unwrap())
.join(".steam/steam/steamapps/common/Lethal Company"),
}
}

View File

@ -5,6 +5,8 @@ use munsikka::*;
use base64::prelude::*; use base64::prelude::*;
use yaml_rust2::{Yaml, YamlLoader}; use yaml_rust2::{Yaml, YamlLoader};
pub mod installer;
fn main() { fn main() {
// First and only argument is the (legacy) profile UUID from a mod manager user // First and only argument is the (legacy) profile UUID from a mod manager user
let profile_uuid = std::env::args() let profile_uuid = std::env::args()
@ -31,6 +33,18 @@ fn main() {
let mod_list = fetch_mod_list(&profile_fetcher, &profile_uuid).unwrap(); let mod_list = fetch_mod_list(&profile_fetcher, &profile_uuid).unwrap();
fetch_mods(&mod_fetcher, &mod_list).unwrap(); fetch_mods(&mod_fetcher, &mod_list).unwrap();
println!("");
installer::clean(&game_dir()).unwrap();
println!("Installing mods");
for mod_info in mod_list.iter() {
installer::install(
&mod_fetcher.extracted_path(&mod_info.unique_name()),
&game_dir(),
&mod_info,
)
.unwrap();
}
} }
/// Map the legacy profile response into a zip binary /// Map the legacy profile response into a zip binary
@ -47,6 +61,7 @@ fn profile_preprocessor(profile_response: Vec<u8>) -> Vec<u8> {
BASE64_STANDARD.decode(base64_zip).unwrap() BASE64_STANDARD.decode(base64_zip).unwrap()
} }
/// Gets the list of required mods from a profile uuid
fn fetch_mod_list( fn fetch_mod_list(
fetcher: &FetchExtractor, fetcher: &FetchExtractor,
profile_uuid: &str, profile_uuid: &str,
@ -64,18 +79,30 @@ fn fetch_mod_list(
// Handle yaml // Handle yaml
let mods = &YamlLoader::load_from_str(&mods_yaml)?[0]; let mods = &YamlLoader::load_from_str(&mods_yaml)?[0];
Ok(mods
.as_vec() Ok(sort_mods(
.ok_or(std::io::Error::new( &mods
std::io::ErrorKind::Other, .as_vec()
format!("YAML file is not a list: {:?}", mods_yaml_path), .ok_or(std::io::Error::new(
))? std::io::ErrorKind::Other,
.iter() format!("YAML file is not a list: {:?}", mods_yaml_path),
.filter(|yaml| yaml["enabled"] == Yaml::Boolean(true)) ))?
.map(|yaml| ModInfo::from_yaml(yaml).unwrap()) .iter()
.collect()) .filter(|yaml| yaml["enabled"] == Yaml::Boolean(true))
.map(|yaml| ModInfo::from_yaml(yaml).unwrap())
.collect(),
))
} }
/// Sort mods by dependency order
fn sort_mods(mods: &Vec<ModInfo>) -> Vec<ModInfo> {
// TODO: Actual dependecy sorting
let mut mods = mods.clone();
mods.sort_unstable_by_key(|mod_info| mod_info.dependencies.len());
mods
}
/// Download and extract mods
fn fetch_mods(fetcher: &FetchExtractor, mod_list: &Vec<ModInfo>) -> Result<(), Box<dyn Error>> { fn fetch_mods(fetcher: &FetchExtractor, mod_list: &Vec<ModInfo>) -> Result<(), Box<dyn Error>> {
println!("Fetching {} mods...", mod_list.len()); println!("Fetching {} mods...", mod_list.len());