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";
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct ModInfo {
pub name: String,
pub author: String,
pub version: (i64, i64, i64),
pub website_url: String,
pub enabled: bool,
pub dependencies: Vec<Option<String>>,
}
impl ModInfo {
@ -30,6 +31,11 @@ impl ModInfo {
),
website_url: yaml["websiteUrl"].as_str()?.to_owned(),
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<(), ()> {
// TODO: use the zip library
// TODO: use a zip library
Command::new("unzip")
.arg(from)
.arg("-d")
@ -180,3 +186,11 @@ fn unzip_to(from: &Path, to: &Path) -> Result<(), ()> {
pub fn data_dir() -> PathBuf {
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 yaml_rust2::{Yaml, YamlLoader};
pub mod installer;
fn main() {
// First and only argument is the (legacy) profile UUID from a mod manager user
let profile_uuid = std::env::args()
@ -31,6 +33,18 @@ fn main() {
let mod_list = fetch_mod_list(&profile_fetcher, &profile_uuid).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
@ -47,6 +61,7 @@ fn profile_preprocessor(profile_response: Vec<u8>) -> Vec<u8> {
BASE64_STANDARD.decode(base64_zip).unwrap()
}
/// Gets the list of required mods from a profile uuid
fn fetch_mod_list(
fetcher: &FetchExtractor,
profile_uuid: &str,
@ -64,18 +79,30 @@ fn fetch_mod_list(
// Handle yaml
let mods = &YamlLoader::load_from_str(&mods_yaml)?[0];
Ok(mods
.as_vec()
.ok_or(std::io::Error::new(
std::io::ErrorKind::Other,
format!("YAML file is not a list: {:?}", mods_yaml_path),
))?
.iter()
.filter(|yaml| yaml["enabled"] == Yaml::Boolean(true))
.map(|yaml| ModInfo::from_yaml(yaml).unwrap())
.collect())
Ok(sort_mods(
&mods
.as_vec()
.ok_or(std::io::Error::new(
std::io::ErrorKind::Other,
format!("YAML file is not a list: {:?}", mods_yaml_path),
))?
.iter()
.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>> {
println!("Fetching {} mods...", mod_list.len());