From 672ee8e0df804caa4c746dbc9ce7efc5c3674647 Mon Sep 17 00:00:00 2001 From: GeckoEidechse <40122905+GeckoEidechse@users.noreply.github.com> Date: Fri, 21 Jul 2023 12:43:11 +0200 Subject: feat: Support installing Thunderstore packages (#426) Support for installing mods from Thunderstore directly into the `packages` folder. Adds some basic sanity check during installation. After successful installation, the old version of the mod is removed. This includes both from `packages` and from `mods`. --- src-tauri/Cargo.lock | 47 +++++++++--- src-tauri/Cargo.toml | 2 +- src-tauri/src/mod_management/legacy.rs | 40 ++++++++++ src-tauri/src/mod_management/mod.rs | 129 +++++++++++++++++++++++++++++++-- 4 files changed, 201 insertions(+), 17 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index cd380a27..f01d299b 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -44,6 +44,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "aho-corasick" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +dependencies = [ + "memchr", +] + [[package]] name = "alloc-no-stdlib" version = "2.0.4" @@ -1454,7 +1463,7 @@ version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc" dependencies = [ - "aho-corasick", + "aho-corasick 0.7.20", "bstr", "fnv", "log", @@ -1984,12 +1993,14 @@ checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" [[package]] name = "libthermite" -version = "0.6.5" +version = "0.7.0-beta" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27cd844bbc25676cd14fa9ad04cc40e0f3c4d5c66107ef3a99896db1f81324c0" +checksum = "07d01e44ad3cd57ad026987a2d69a71d7a6d49e5ecdbbe64c2f940477b2a3a2d" dependencies = [ "flate2", "json5", + "lazy_static", + "regex", "serde", "serde_json", "tar", @@ -2117,7 +2128,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" dependencies = [ - "regex-automata", + "regex-automata 0.1.10", ] [[package]] @@ -3058,13 +3069,14 @@ dependencies = [ [[package]] name = "regex" -version = "1.7.1" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" +checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" dependencies = [ - "aho-corasick", + "aho-corasick 1.0.2", "memchr", - "regex-syntax", + "regex-automata 0.3.3", + "regex-syntax 0.7.4", ] [[package]] @@ -3073,7 +3085,18 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" dependencies = [ - "regex-syntax", + "regex-syntax 0.6.28", +] + +[[package]] +name = "regex-automata" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39354c10dd07468c2e73926b23bb9c2caca74c5501e38a35da70406f1d923310" +dependencies = [ + "aho-corasick 1.0.2", + "memchr", + "regex-syntax 0.7.4", ] [[package]] @@ -3082,6 +3105,12 @@ version = "0.6.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" +[[package]] +name = "regex-syntax" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" + [[package]] name = "reqwest" version = "0.11.14" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 03eb8e9e..bba82afc 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -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 diff --git a/src-tauri/src/mod_management/legacy.rs b/src-tauri/src/mod_management/legacy.rs index f24f44b6..91463250 100644 --- a/src-tauri/src/mod_management/legacy.rs +++ b/src-tauri/src/mod_management/legacy.rs @@ -115,6 +115,46 @@ pub fn parse_installed_mods( Ok(mods) } +/// Deletes all legacy packages that match in author and mod name +/// 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, diff --git a/src-tauri/src/mod_management/mod.rs b/src-tauri/src/mod_management/mod.rs index b28671c8..c049fe07 100644 --- a/src-tauri/src/mod_management/mod.rs +++ b/src-tauri/src/mod_management/mod.rs @@ -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}; @@ -407,6 +408,97 @@ async fn get_mod_dependencies(thunderstore_mod_string: &str) -> Result::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 = 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; + } + }; + // 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. @@ -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() { @@ -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); } }; -- cgit v1.2.3