Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support installing Thunderstore packages #426

Merged
merged 11 commits into from
Jul 21, 2023
47 changes: 38 additions & 9 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ steamlocate = "1.2"
# Error messages
anyhow = "1.0"
# libthermite for Northstar/mod install handling
libthermite = { version = "0.6.5", features = ["proton"] }
libthermite = { version = "0.7.0-beta", features = ["proton"] }
# zip stuff
zip = "0.6.2"
# Regex
Expand Down
40 changes: 40 additions & 0 deletions src-tauri/src/mod_management/legacy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,46 @@ pub fn parse_installed_mods(
Ok(mods)
}

/// Deletes all legacy packages that match in author and mod name
GeckoEidechse marked this conversation as resolved.
Show resolved Hide resolved
/// regardless of version
///
/// "legacy package" refers to a Thunderstore package installed into the `mods` folder
/// by extracting Northstar mods contained inside and then adding `manifest.json` and `thunderstore_author.txt`
/// to indicate which Thunderstore package they are part of
pub fn delete_legacy_package_install(
thunderstore_mod_string: &str,
game_install: &GameInstall,
) -> Result<(), String> {
let thunderstore_mod_string: ParsedThunderstoreModString =
thunderstore_mod_string.parse().unwrap();
let found_installed_legacy_mods = match parse_installed_mods(game_install) {
Ok(res) => res,
Err(err) => return Err(err.to_string()),
};

for legacy_mod in found_installed_legacy_mods {
if legacy_mod.thunderstore_mod_string.is_none() {
continue; // Not a thunderstore mod
}

let current_mod_ts_string: ParsedThunderstoreModString = legacy_mod
.clone()
.thunderstore_mod_string
.unwrap()
.parse()
.unwrap();

if thunderstore_mod_string.author_name == current_mod_ts_string.author_name
&& thunderstore_mod_string.mod_name == current_mod_ts_string.mod_name
{
// They match, delete
delete_mod_folder(&legacy_mod.directory)?;
}
}

Ok(())
}

/// Deletes all NorthstarMods related to a Thunderstore mod
pub fn delete_thunderstore_mod(
game_install: GameInstall,
Expand Down
129 changes: 122 additions & 7 deletions src-tauri/src/mod_management/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

use crate::constants::{BLACKLISTED_MODS, CORE_MODS};
use async_recursion::async_recursion;
use thermite::prelude::ThermiteError;

use crate::NorthstarMod;
use anyhow::{anyhow, Result};
Expand Down Expand Up @@ -407,6 +408,97 @@ async fn get_mod_dependencies(thunderstore_mod_string: &str) -> Result<Vec<Strin
Ok(Vec::<String>::new())
}

/// Deletes all versions of Thunderstore package except the specified one
fn delete_older_versions(
thunderstore_mod_string: &str,
game_install: &GameInstall,
) -> Result<(), String> {
let thunderstore_mod_string: ParsedThunderstoreModString =
thunderstore_mod_string.parse().unwrap();
log::info!(
"Deleting other versions of {}",
thunderstore_mod_string.to_string()
);
let packages_folder = format!("{}/R2Northstar/packages", game_install.game_path);

// Get folders in packages dir
let paths = match std::fs::read_dir(&packages_folder) {
Ok(paths) => paths,
Err(_err) => return Err(format!("Failed to read directory {}", &packages_folder)),
};

let mut directories: Vec<PathBuf> = Vec::new();

// Get list of folders in `mods` directory
for path in paths {
let my_path = path.unwrap().path();

let md = std::fs::metadata(my_path.clone()).unwrap();
if md.is_dir() {
directories.push(my_path);
}
}

for directory in directories {
let folder_name = directory.file_name().unwrap().to_str().unwrap();
let ts_mod_string_from_folder: ParsedThunderstoreModString = match folder_name.parse() {
Ok(res) => res,
Err(err) => {
// Failed parsing folder name as Thunderstore mod string
// This means it doesn't follow the `AUTHOR-MOD-VERSION` naming structure
// This folder could've been manually created by the user or another application
// As parsing failed we cannot determine the Thunderstore package it is part of hence we skip it
log::warn!("{err}");
continue;
GeckoEidechse marked this conversation as resolved.
Show resolved Hide resolved
}
};
// Check which match `AUTHOR-MOD` and do NOT match `AUTHOR-MOD-VERSION`
if ts_mod_string_from_folder.author_name == thunderstore_mod_string.author_name
&& ts_mod_string_from_folder.mod_name == thunderstore_mod_string.mod_name
&& ts_mod_string_from_folder.version != thunderstore_mod_string.version
{
delete_package_folder(&directory.display().to_string())?;
}
}

Ok(())
}

/// Checks whether some mod is correctly formatted
/// Currently checks whether
/// - Some `mod.json` exists under `mods/*/mod.json`
fn fc_sanity_check(input: &&fs::File) -> bool {
let mut archive = match zip::read::ZipArchive::new(*input) {
Ok(archive) => archive,
Err(_) => return false,
};

let mut has_mods = false;
let mut mod_json_exists = false;

// Checks for `mods/*/mod.json`
for i in 0..archive.len() {
let file = match archive.by_index(i) {
Ok(file) => file,
Err(_) => continue,
};
let file_path = file.mangled_name();
if file_path.starts_with("mods/") {
has_mods = true;
if let Some(name) = file_path.file_name() {
if name == "mod.json" {
let parent_path = file_path.parent().unwrap();
if parent_path.parent().unwrap().to_str().unwrap() == "mods" {
mod_json_exists = true;
}
}
}
}
}

has_mods && mod_json_exists
}

// Copied from `libtermite` source code and modified
// Should be replaced with a library call to libthermite in the future
/// Download and install mod to the specified target.
Expand All @@ -421,7 +513,6 @@ pub async fn fc_download_mod_and_install(
"{}/___flightcore-temp-download-dir/",
game_install.game_path
);
let mods_directory = format!("{}/R2Northstar/mods/", game_install.game_path);

// Early return on empty string
if thunderstore_mod_string.is_empty() {
Expand Down Expand Up @@ -486,19 +577,43 @@ pub async fn fc_download_mod_and_install(
Err(err) => return Err(err.to_string()),
};

// Get Thunderstore mod author
let author = thunderstore_mod_string.split('-').next().unwrap();
// Get directory to install to made up of packages directory and Thunderstore mod string
let install_directory = format!("{}/R2Northstar/packages/", game_install.game_path);

// Extract the mod to the mods directory
match thermite::core::manage::install_mod(
author,
match thermite::core::manage::install_with_sanity(
thunderstore_mod_string,
temp_file.file(),
std::path::Path::new(&mods_directory),
std::path::Path::new(&install_directory),
fc_sanity_check,
) {
Ok(_) => (),
Err(err) => {
log::warn!("libthermite couldn't install mod {thunderstore_mod_string} due to {err:?}",);
return Err(err.to_string());
return match err {
ThermiteError::SanityError => Err(
"Mod failed sanity check during install. It's probably not correctly formatted"
.to_string(),
),
_ => Err(err.to_string()),
};
}
};

// Successful package install
match legacy::delete_legacy_package_install(thunderstore_mod_string, game_install) {
Ok(()) => (),
Err(err) => {
// Catch error but ignore
log::warn!("Failed deleting legacy versions due to: {}", err);
}
};

match delete_older_versions(thunderstore_mod_string, game_install) {
Ok(()) => (),
Err(err) => {
// Catch error but ignore
log::warn!("Failed deleting older versions due to: {}", err);
}
};

Expand Down