Compare commits
4 Commits
422c1a67c1
...
44778f91c3
| Author | SHA1 | Date |
|---|---|---|
|
|
44778f91c3 | |
|
|
9c582d1257 | |
|
|
259e867575 | |
|
|
cd905cece8 |
Binary file not shown.
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 179 KiB |
|
|
@ -0,0 +1 @@
|
||||||
|
install -m 755 -o root target/release/munsikka /usr/local/bin/
|
||||||
80
src/lib.rs
80
src/lib.rs
|
|
@ -1,4 +1,5 @@
|
||||||
use std::{
|
use std::{
|
||||||
|
cmp::Ordering::*,
|
||||||
env::var,
|
env::var,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
process::Command,
|
process::Command,
|
||||||
|
|
@ -9,6 +10,8 @@ use yaml_rust2::Yaml;
|
||||||
|
|
||||||
pub const BASE_URL: &str = "http://thunderstore.io";
|
pub const BASE_URL: &str = "http://thunderstore.io";
|
||||||
|
|
||||||
|
pub type DependencyString = String;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ModInfo {
|
pub struct ModInfo {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|
@ -16,7 +19,7 @@ pub struct ModInfo {
|
||||||
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>>,
|
pub dependencies: Vec<DependencyString>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ModInfo {
|
impl ModInfo {
|
||||||
|
|
@ -34,7 +37,7 @@ impl ModInfo {
|
||||||
dependencies: yaml["dependencies"]
|
dependencies: yaml["dependencies"]
|
||||||
.as_vec()?
|
.as_vec()?
|
||||||
.iter()
|
.iter()
|
||||||
.map(|y| y.as_str().map(|str| str.to_owned()))
|
.filter_map(|y| y.as_str().map(|str| str.to_owned()))
|
||||||
.collect(),
|
.collect(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -46,11 +49,30 @@ impl ModInfo {
|
||||||
format!("{url}{}", self.version_string(),)
|
format!("{url}{}", self.version_string(),)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn unique_name(&self) -> String {
|
/// Get a unique identifier for given mod version.
|
||||||
|
/// Used as part of the filepath in archives & extracted files.
|
||||||
|
///
|
||||||
|
/// Returns an error if it contains slashes or if the path contains no filename (such as ..)
|
||||||
|
pub fn unique_name(&self) -> Result<String, String> {
|
||||||
|
let sanitized = self.dependency_string().replace(&[':', '/'], "_");
|
||||||
|
let path = PathBuf::from(&sanitized);
|
||||||
|
if path.file_name() != Some(path.as_os_str()) {
|
||||||
|
return Err(format!(
|
||||||
|
"Mod name {:?} contains invalid characters for a filepath",
|
||||||
|
sanitized
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(sanitized)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the dependecy string for this mod that can be compared to other mods dependencies.
|
||||||
|
///
|
||||||
|
/// This follows the mod naming format from profile
|
||||||
|
pub fn dependency_string(&self) -> DependencyString {
|
||||||
format!(
|
format!(
|
||||||
"{name}_{author}_{version}",
|
"{author}-{name}-{version}",
|
||||||
name = self.name,
|
|
||||||
author = self.author,
|
author = self.author,
|
||||||
|
name = self.name,
|
||||||
version = self.version_string(),
|
version = self.version_string(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -72,25 +94,39 @@ impl FetchExtractor {
|
||||||
.join(PathBuf::from(format!("{}.zip", unique_name)))
|
.join(PathBuf::from(format!("{}.zip", unique_name)))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn extracted_path(&self, unique_name: &str) -> PathBuf {
|
pub fn extract_path(&self, unique_name: &str) -> PathBuf {
|
||||||
self.base_dir
|
self.base_dir
|
||||||
.join(PathBuf::from(format!("{}", unique_name)))
|
.join(PathBuf::from(format!("{}", unique_name)))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_fetched(&self, unique_name: &str) -> Option<PathBuf> {
|
/// Returns `Some(path)` if the item is fetched (archive path exists), or `None` otherwise
|
||||||
match self.archive_path(unique_name).exists() {
|
pub fn try_get_fetched(&self, unique_name: &str) -> Option<PathBuf> {
|
||||||
true => Some(self.archive_path(unique_name)),
|
let path = self.archive_path(unique_name);
|
||||||
|
match path.exists() {
|
||||||
|
true => Some(path),
|
||||||
false => None,
|
false => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_extracted(&self, unique_name: &str) -> Option<PathBuf> {
|
/// Returns if the item in fetched
|
||||||
match self.extracted_path(unique_name).exists() {
|
pub fn is_fetched(&self, unique_name: &str) -> bool {
|
||||||
true => Some(self.extracted_path(unique_name)),
|
self.try_get_fetched(unique_name).is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `Some(path)` if the item is extracted (extraction path exists), or `None` otherwise
|
||||||
|
pub fn try_get_extracted(&self, unique_name: &str) -> Option<PathBuf> {
|
||||||
|
let path = self.extract_path(unique_name);
|
||||||
|
match path.exists() {
|
||||||
|
true => Some(path),
|
||||||
false => None,
|
false => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns if the item is extracted
|
||||||
|
pub fn is_extracted(&self, unique_name: &str) -> bool {
|
||||||
|
self.try_get_extracted(unique_name).is_some()
|
||||||
|
}
|
||||||
|
|
||||||
/// Removes the archive and extracted files
|
/// Removes the archive and extracted files
|
||||||
pub fn clean(&self, unique_name: &str) -> std::io::Result<()> {
|
pub fn clean(&self, unique_name: &str) -> std::io::Result<()> {
|
||||||
if unique_name.is_empty() {
|
if unique_name.is_empty() {
|
||||||
|
|
@ -104,11 +140,11 @@ impl FetchExtractor {
|
||||||
std::fs::create_dir_all(parent_dir)?;
|
std::fs::create_dir_all(parent_dir)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(parent_dir) = self.extracted_path(unique_name).parent() {
|
if let Some(parent_dir) = self.extract_path(unique_name).parent() {
|
||||||
std::fs::create_dir_all(parent_dir)?;
|
std::fs::create_dir_all(parent_dir)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(archive_path) = self.is_fetched(unique_name) {
|
if let Some(archive_path) = self.try_get_fetched(unique_name) {
|
||||||
if archive_path.is_file() {
|
if archive_path.is_file() {
|
||||||
std::fs::remove_file(self.archive_path(unique_name))?;
|
std::fs::remove_file(self.archive_path(unique_name))?;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -119,9 +155,9 @@ impl FetchExtractor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(extract_path) = self.is_extracted(unique_name) {
|
if let Some(extract_path) = self.try_get_extracted(unique_name) {
|
||||||
if extract_path.is_dir() {
|
if extract_path.is_dir() {
|
||||||
std::fs::remove_dir_all(self.extracted_path(unique_name))?;
|
std::fs::remove_dir_all(self.extract_path(unique_name))?;
|
||||||
} else {
|
} else {
|
||||||
return Err(std::io::Error::new(
|
return Err(std::io::Error::new(
|
||||||
std::io::ErrorKind::Other,
|
std::io::ErrorKind::Other,
|
||||||
|
|
@ -136,7 +172,7 @@ impl FetchExtractor {
|
||||||
/// Fetches the archive and writes it to `self.archive_path()`
|
/// Fetches the archive and writes it to `self.archive_path()`
|
||||||
pub fn fetch(&self, url: &str, unique_name: &str) -> reqwest::Result<()> {
|
pub fn fetch(&self, url: &str, unique_name: &str) -> reqwest::Result<()> {
|
||||||
// Don't re-download if it already exists
|
// Don't re-download if it already exists
|
||||||
if self.is_fetched(unique_name).is_some() {
|
if self.is_fetched(unique_name) {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -153,19 +189,19 @@ impl FetchExtractor {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extracts the archive to `self.extracted_path()`
|
/// Extracts the archive to `self.extract_path()`
|
||||||
pub fn extract(&self, unique_name: &str) -> std::io::Result<()> {
|
pub fn extract(&self, unique_name: &str) -> std::io::Result<()> {
|
||||||
// Don't re-extract if it already exists
|
// Don't re-extract if it already exists
|
||||||
if self.is_extracted(unique_name).is_some() {
|
if self.is_extracted(unique_name) {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(parent_dir) = self.extracted_path(unique_name).parent() {
|
if let Some(parent_dir) = self.extract_path(unique_name).parent() {
|
||||||
std::fs::create_dir_all(parent_dir)?;
|
std::fs::create_dir_all(parent_dir)?;
|
||||||
}
|
}
|
||||||
unzip_to(
|
unzip_to(
|
||||||
&self.archive_path(unique_name),
|
&self.archive_path(unique_name),
|
||||||
&self.extracted_path(unique_name),
|
&self.extract_path(unique_name),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -191,6 +227,6 @@ pub fn game_dir() -> PathBuf {
|
||||||
match var("GAME_DIR").ok() {
|
match var("GAME_DIR").ok() {
|
||||||
Some(game_dir) => game_dir.into(),
|
Some(game_dir) => game_dir.into(),
|
||||||
None => PathBuf::from(var("HOME").unwrap())
|
None => PathBuf::from(var("HOME").unwrap())
|
||||||
.join(".steam/steam/steamapps/common/Lethal Company"),
|
.join("steam/SteamLibrary/steamapps/common/Lethal Company"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
88
src/main.rs
88
src/main.rs
|
|
@ -32,6 +32,7 @@ fn main() {
|
||||||
};
|
};
|
||||||
|
|
||||||
let mod_list = fetch_mod_list(&profile_fetcher, &profile_uuid).unwrap();
|
let mod_list = fetch_mod_list(&profile_fetcher, &profile_uuid).unwrap();
|
||||||
|
let mod_list = sort_by_dependencies(&mod_list);
|
||||||
fetch_mods(&mod_fetcher, &mod_list).unwrap();
|
fetch_mods(&mod_fetcher, &mod_list).unwrap();
|
||||||
|
|
||||||
println!("");
|
println!("");
|
||||||
|
|
@ -39,7 +40,7 @@ fn main() {
|
||||||
println!("Installing mods");
|
println!("Installing mods");
|
||||||
for mod_info in mod_list.iter() {
|
for mod_info in mod_list.iter() {
|
||||||
installer::install(
|
installer::install(
|
||||||
&mod_fetcher.extracted_path(&mod_info.unique_name()),
|
&mod_fetcher.extract_path(&mod_info.unique_name().unwrap()),
|
||||||
&game_dir(),
|
&game_dir(),
|
||||||
&mod_info,
|
&mod_info,
|
||||||
)
|
)
|
||||||
|
|
@ -73,33 +74,77 @@ fn fetch_mod_list(
|
||||||
fetcher.extract(profile_uuid)?;
|
fetcher.extract(profile_uuid)?;
|
||||||
|
|
||||||
let mods_yaml_path = fetcher
|
let mods_yaml_path = fetcher
|
||||||
.extracted_path(profile_uuid)
|
.extract_path(profile_uuid)
|
||||||
.join(PathBuf::from("mods.yml"));
|
.join(PathBuf::from("mods.yml"));
|
||||||
let mods_yaml = std::fs::read_to_string(&mods_yaml_path)?;
|
let mods_yaml = std::fs::read_to_string(&mods_yaml_path)?;
|
||||||
|
|
||||||
// Handle yaml
|
// Handle yaml
|
||||||
let mods = &YamlLoader::load_from_str(&mods_yaml)?[0];
|
let mods = &YamlLoader::load_from_str(&mods_yaml)?[0];
|
||||||
|
|
||||||
Ok(sort_mods(
|
Ok(mods
|
||||||
&mods
|
.as_vec()
|
||||||
.as_vec()
|
.ok_or(std::io::Error::new(
|
||||||
.ok_or(std::io::Error::new(
|
std::io::ErrorKind::Other,
|
||||||
std::io::ErrorKind::Other,
|
format!("YAML file is not a list: {:?}", mods_yaml_path),
|
||||||
format!("YAML file is not a list: {:?}", mods_yaml_path),
|
))?
|
||||||
))?
|
.iter()
|
||||||
.iter()
|
.filter(|yaml| yaml["enabled"] == Yaml::Boolean(true))
|
||||||
.filter(|yaml| yaml["enabled"] == Yaml::Boolean(true))
|
.map(|yaml| ModInfo::from_yaml(yaml).unwrap())
|
||||||
.map(|yaml| ModInfo::from_yaml(yaml).unwrap())
|
.collect())
|
||||||
.collect(),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sort mods by dependency order
|
/// Sort mods so that dependencies are installed before dependants
|
||||||
fn sort_mods(mods: &Vec<ModInfo>) -> Vec<ModInfo> {
|
fn sort_by_dependencies(mods: &Vec<ModInfo>) -> Vec<ModInfo> {
|
||||||
// TODO: Actual dependecy sorting
|
let mut resolved_mods: Vec<ModInfo> = Vec::with_capacity(mods.len());
|
||||||
let mut mods = mods.clone();
|
let mut mods = mods.clone();
|
||||||
|
// Sort mods initially by their dependency count, so we are more likely to hit them in order
|
||||||
mods.sort_unstable_by_key(|mod_info| mod_info.dependencies.len());
|
mods.sort_unstable_by_key(|mod_info| mod_info.dependencies.len());
|
||||||
mods
|
|
||||||
|
let mut unsatisfied_iterations: Vec<Vec<ModInfo>> = vec![];
|
||||||
|
|
||||||
|
while mods.len() > 0 {
|
||||||
|
// Find a mod with satisfied dependencies
|
||||||
|
let index = mods
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.find(|(_, mod_info)| {
|
||||||
|
mod_info.dependencies.iter().all(|dependency| {
|
||||||
|
resolved_mods
|
||||||
|
.iter()
|
||||||
|
.any(|resolved| resolved.satisfies(dependency))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.map(|(index, _)| index);
|
||||||
|
if index.is_none() {
|
||||||
|
unsatisfied_iterations.push(mods.clone());
|
||||||
|
}
|
||||||
|
// Even if no mods were satisfied, just pick one.
|
||||||
|
resolved_mods.push(mods.remove(index.unwrap_or(0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if unsatisfied_iterations.len() > 0 {
|
||||||
|
eprintln!(
|
||||||
|
"{} unsatisfied dependencies:\n{:?}\n",
|
||||||
|
unsatisfied_iterations[0].len(),
|
||||||
|
unsatisfied_iterations[0]
|
||||||
|
);
|
||||||
|
|
||||||
|
eprintln!(
|
||||||
|
"Unsatisfied iterations:\n{:?}\n",
|
||||||
|
unsatisfied_iterations
|
||||||
|
.iter()
|
||||||
|
.map(|list| {
|
||||||
|
let list = list
|
||||||
|
.iter()
|
||||||
|
.map(|mod_info| mod_info.name.clone())
|
||||||
|
.collect::<Vec<String>>();
|
||||||
|
format!("({} unsatisfied) {:?}", list.len(), list)
|
||||||
|
})
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolved_mods
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Download and extract mods
|
/// Download and extract mods
|
||||||
|
|
@ -113,10 +158,11 @@ fn fetch_mods(fetcher: &FetchExtractor, mod_list: &Vec<ModInfo>) -> Result<(), B
|
||||||
author = mod_info.author,
|
author = mod_info.author,
|
||||||
version = mod_info.version_string(),
|
version = mod_info.version_string(),
|
||||||
);
|
);
|
||||||
|
let unique_name = mod_info.unique_name().unwrap();
|
||||||
// Skip download & extract if the mod is already extracted
|
// Skip download & extract if the mod is already extracted
|
||||||
if fetcher.is_extracted(&mod_info.unique_name()).is_none() {
|
if !fetcher.is_extracted(&unique_name) {
|
||||||
fetcher.fetch(&mod_info.download_url(), &mod_info.unique_name())?;
|
fetcher.fetch(&mod_info.download_url(), &unique_name)?;
|
||||||
fetcher.extract(&mod_info.unique_name())?;
|
fetcher.extract(&unique_name)?;
|
||||||
println!("✅");
|
println!("✅");
|
||||||
} else {
|
} else {
|
||||||
println!("📦");
|
println!("📦");
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue