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";
|
||||
|
||||
#[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"),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
31
src/main.rs
31
src/main.rs
|
|
@ -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,7 +79,9 @@ fn fetch_mod_list(
|
|||
|
||||
// Handle yaml
|
||||
let mods = &YamlLoader::load_from_str(&mods_yaml)?[0];
|
||||
Ok(mods
|
||||
|
||||
Ok(sort_mods(
|
||||
&mods
|
||||
.as_vec()
|
||||
.ok_or(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
|
|
@ -73,9 +90,19 @@ fn fetch_mod_list(
|
|||
.iter()
|
||||
.filter(|yaml| yaml["enabled"] == Yaml::Boolean(true))
|
||||
.map(|yaml| ModInfo::from_yaml(yaml).unwrap())
|
||||
.collect())
|
||||
.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());
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue