Added mod installation. First working version.
parent
897857fc98
commit
c1015f10fd
|
|
@ -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(())
|
||||||
|
}
|
||||||
18
src/lib.rs
18
src/lib.rs
|
|
@ -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"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
47
src/main.rs
47
src/main.rs
|
|
@ -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());
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue