diff options
-rw-r--r-- | src-tauri/Cargo.lock | 12 | ||||
-rw-r--r-- | src-tauri/Cargo.toml | 2 | ||||
-rw-r--r-- | src-tauri/src/main.rs | 42 | ||||
-rw-r--r-- | src-tauri/src/mod_management/mod.rs | 176 | ||||
-rw-r--r-- | src-tauri/src/repair_and_verify/mod.rs | 31 | ||||
-rw-r--r-- | src-vue/src/views/DeveloperView.vue | 65 |
6 files changed, 325 insertions, 3 deletions
diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 51c69a18..737078d3 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -79,6 +79,7 @@ name = "app" version = "0.3.1" dependencies = [ "anyhow", + "async-recursion", "json5", "libthermite", "powershell_script", @@ -97,6 +98,17 @@ dependencies = [ ] [[package]] +name = "async-recursion" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cda8f4bcc10624c4e85bc66b3f452cca98cfa5ca002dc83a16aad2367641bea" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] name = "atk" version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index d63daf87..d6da1030 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -40,6 +40,8 @@ reqwest = { version = "0.11", features = ["blocking"] } tauri-plugin-store = { git = "https://github.com/tauri-apps/tauri-plugin-store", branch = "dev" } # JSON5 parsing support (allows comments in JSON) json5 = "0.4.1" +# Async recursion for recursive mod install +async-recursion = "1.0.0" [features] # by default Tauri runs in production mode diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 7db9e089..1ddb4c69 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -15,10 +15,12 @@ mod github; use github::release_notes::{get_northstar_release_notes, check_is_flightcore_outdated}; mod repair_and_verify; -use repair_and_verify::{verify_game_files, disable_all_but_core}; +use repair_and_verify::{clean_up_download_folder, disable_all_but_core, verify_game_files}; mod mod_management; -use mod_management::{set_mod_enabled_status, get_installed_mods_and_properties}; +use mod_management::{ + fc_download_mod_and_install, get_installed_mods_and_properties, set_mod_enabled_status, +}; use tauri::Manager; use tauri_plugin_store::PluginBuilder; @@ -92,6 +94,8 @@ fn main() { get_northstar_release_notes, linux_checks, get_installed_mods_caller, + install_mod_caller, + clean_up_download_folder_caller, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); @@ -275,7 +279,9 @@ async fn verify_game_files_caller(game_install: GameInstall) -> Result<String, S } #[tauri::command] -async fn get_enabled_mods_caller(game_install: GameInstall) -> Result<serde_json::value::Value, String> { +async fn get_enabled_mods_caller( + game_install: GameInstall, +) -> Result<serde_json::value::Value, String> { get_enabled_mods(game_install) } @@ -297,3 +303,33 @@ async fn disable_all_but_core_caller(game_install: GameInstall) -> Result<(), St async fn get_installed_mods_caller(game_install: GameInstall) -> Result<Vec<NorthstarMod>, String> { get_installed_mods_and_properties(game_install) } + +#[tauri::command] +/// Installs the specified mod +async fn install_mod_caller( + game_install: GameInstall, + thunderstore_mod_string: String, +) -> Result<(), String> { + fc_download_mod_and_install(game_install.clone(), thunderstore_mod_string).await?; + match clean_up_download_folder(game_install, false) { + Ok(()) => Ok(()), + Err(err) => { + println!("Failed to delete download folder due to {}", err); + // Failure to delete download folder is not an error in mod install + // As such ignore. User can still force delete if need be + Ok(()) + } + } +} + +#[tauri::command] +/// Installs the specified mod +async fn clean_up_download_folder_caller( + game_install: GameInstall, + force: bool, +) -> Result<(), String> { + match clean_up_download_folder(game_install, force) { + Ok(()) => Ok(()), + Err(err) => Err(err.to_string()), + } +} diff --git a/src-tauri/src/mod_management/mod.rs b/src-tauri/src/mod_management/mod.rs index ecfd3835..96bb1a0f 100644 --- a/src-tauri/src/mod_management/mod.rs +++ b/src-tauri/src/mod_management/mod.rs @@ -1,4 +1,5 @@ // This file contains various mod management functions +use async_recursion::async_recursion; use anyhow::{anyhow, Result}; use app::NorthstarMod; @@ -130,3 +131,178 @@ pub fn get_installed_mods_and_properties( Ok(installed_mods) } + +async fn get_ns_mod_download_url(thunderstore_mod_string: String) -> Result<String, String> { + + // TODO: This will crash the thread if not internet connection exist. `match` should be used instead + let index = thermite::api::get_package_index().await.unwrap().to_vec(); + + // String replace works but more care should be taken in the future + let ts_mod_string_url = thunderstore_mod_string.replace("-", "/"); + + for ns_mod in index { + // Iterate over all versions of a given mod + for (_key, ns_mod) in &ns_mod.versions { + if ns_mod.url.contains(&ts_mod_string_url) { + dbg!(ns_mod.clone()); + return Ok(ns_mod.url.clone()); + } + } + } + + Err("Could not find mod on Thunderstore".to_string()) +} + +/// Adds given Thunderstore mod string to the given `mod.json` +/// This way we can later check whether a mod is outdated based on the TS mod string +fn add_thunderstore_mod_string( + path_to_mod_json: String, + thunderstore_mod_string: String, +) -> Result<(), anyhow::Error> { + + // Read file into string and parse it + let data = std::fs::read_to_string(path_to_mod_json.clone())?; + let parsed_json: serde_json::Value = json5::from_str(&data)?; + + // Insert the Thunderstore mod string + let mut parsed_json = parsed_json.as_object().unwrap().clone(); + parsed_json.insert( + "ThunderstoreModString".to_string(), + serde_json::Value::String(thunderstore_mod_string), + ); + + // And write back to disk + std::fs::write( + path_to_mod_json, + serde_json::to_string_pretty(&parsed_json)?, + )?; + + Ok(()) +} + +/// Returns a vector of modstrings containing the dependencies of a given mod +async fn get_mod_dependencies( + thunderstore_mod_string: String, +) -> Result<Vec<String>, anyhow::Error> { + dbg!(thunderstore_mod_string.clone()); + + // TODO: This will crash the thread if not internet connection exist. `match` should be used instead + let index = thermite::api::get_package_index().await.unwrap().to_vec(); + + // String replace works but more care should be taken in the future + let ts_mod_string_url = thunderstore_mod_string.replace("-", "/"); + + // Iterate over index + for ns_mod in index { + // Iterate over all versions of a given mod + for (_key, ns_mod) in &ns_mod.versions { + if ns_mod.url.contains(&ts_mod_string_url) { + dbg!(ns_mod.clone()); + return Ok(ns_mod.deps.clone()); + } + } + } + Ok(Vec::<String>::new()) +} + +// 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. +#[async_recursion] +pub async fn fc_download_mod_and_install( + game_install: GameInstall, + thunderstore_mod_string: String, +) -> Result<(), String> { + // Get mods and download directories + let download_directory = format!( + "{}/___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.len() == 0 { + return Err("Passed empty string".to_string()); + } + + let deps = match get_mod_dependencies(thunderstore_mod_string.clone()).await { + Ok(deps) => deps, + Err(err) => return Err(err.to_string()), + }; + dbg!(deps.clone()); + + // Recursively install dependencies + for dep in deps { + match fc_download_mod_and_install(game_install.clone(), dep).await { + Ok(()) => (), + Err(err) => { + if err.to_string() == "Cannot install Northstar as a mod!" { + continue; // For Northstar as a dependency, we just skip it + } + else { + return Err(err.to_string()) + } + } + }; + } + + // Prevent installing Northstar as a mod + // While it would fail during install anyway, having explicit error message is nicer + let blacklisted_mods = ["northstar-Northstar", "northstar-NorthstarReleaseCandidate", "ebkr-r2modman"]; + for blacklisted_mod in blacklisted_mods { + if thunderstore_mod_string.contains(blacklisted_mod) { + return Err("Cannot install Northstar as a mod!".to_string()); + } + } + + // Get download URL for the specified mod + let download_url = get_ns_mod_download_url(thunderstore_mod_string.clone()).await?; + + // Create download directory + match std::fs::create_dir_all(download_directory.clone()) { + Ok(()) => (), + Err(err) => return Err(err.to_string()), + }; + + let name = thunderstore_mod_string.clone(); + let path = format!( + "{}/___flightcore-temp-download-dir/{}.zip", + game_install.game_path, name + ); + + // Download the mod + let f = match thermite::core::actions::download_file(&download_url, path.clone()).await { + Ok(f) => f, + Err(e) => return Err(e.to_string()), + }; + + // Extract the mod to the mods directory + let pkg = match thermite::core::actions::install_mod(&f, std::path::Path::new(&mods_directory)) + { + Ok(pkg) => pkg, + Err(err) => return Err(err.to_string()), + }; + dbg!(pkg.clone()); + + // Add Thunderstore mod string to `mod.json` of installed NorthstarMods + for nsmod in pkg.mods { + let path_to_current_mod_json = format!( + "{}/{}/mod.json", + mods_directory, + nsmod.path.to_string_lossy() + ); + match add_thunderstore_mod_string(path_to_current_mod_json, thunderstore_mod_string.clone()) + { + Ok(()) => (), + Err(err) => { + println!("Failed setting modstring for {}", nsmod.name); + println!("{}", err); + } + } + } + + // Delete downloaded zip file + std::fs::remove_file(path).unwrap(); + + Ok(()) +} diff --git a/src-tauri/src/repair_and_verify/mod.rs b/src-tauri/src/repair_and_verify/mod.rs index 8f8fe633..188d3821 100644 --- a/src-tauri/src/repair_and_verify/mod.rs +++ b/src-tauri/src/repair_and_verify/mod.rs @@ -2,6 +2,7 @@ use app::{get_enabled_mods, GameInstall}; use crate::mod_management::set_mod_enabled_status; +use anyhow::anyhow; /// Verifies Titanfall2 game files pub fn verify_game_files(game_install: GameInstall) -> Result<String, String> { @@ -34,3 +35,33 @@ pub fn disable_all_but_core(game_install: GameInstall) -> Result<(), String> { Ok(()) } + +/// Deletes download folder +/// If `force` is FALSE, bails on non-empty folder +/// If `force` is TRUE, deletes folder even if non-empty +pub fn clean_up_download_folder( + game_install: GameInstall, + force: bool, +) -> Result<(), anyhow::Error> { + // Get download directory + let download_directory = format!( + "{}/___flightcore-temp-download-dir/", + game_install.game_path + ); + + // Check if files in folder + let download_dir_contents = std::fs::read_dir(download_directory.clone())?; + // dbg!(download_dir_contents); + + let mut count = 0; + download_dir_contents.inspect(|_| count += 1).for_each(drop); + + if count > 0 && !force { + return Err(anyhow!("Folder not empty, not deleting")); + } + + // Delete folder + std::fs::remove_dir_all(download_directory)?; + + Ok(()) +} diff --git a/src-vue/src/views/DeveloperView.vue b/src-vue/src/views/DeveloperView.vue index 1e9c5e99..2fa5d89e 100644 --- a/src-vue/src/views/DeveloperView.vue +++ b/src-vue/src/views/DeveloperView.vue @@ -22,6 +22,15 @@ Toggle Release Candidate </el-button> + + <h3>Mod install:</h3> + + <el-input v-model="mod_to_install_field_string" placeholder="Please input Thunderstore dependency string" clearable /> + + <el-button type="primary" @click="installMod"> + Install mod + </el-button> + <h3>Repair:</h3> <el-button type="primary" @click="disableAllModsButCore"> @@ -32,6 +41,10 @@ Get installed mods </el-button> + <el-button type="primary" @click="cleanUpDownloadFolder"> + Force delete temp download folder + </el-button> + </div> </template> @@ -46,6 +59,11 @@ const persistentStore = new Store('flight-core-settings.json'); export default defineComponent({ name: "DeveloperView", + data() { + return { + mod_to_install_field_string : "", + } + }, methods: { disableDevMode() { this.$store.commit('toggleDeveloperMode'); @@ -149,6 +167,53 @@ export default defineComponent({ position: 'bottom-right' }); }); + }, + async installMod() { + let game_install = { + game_path: this.$store.state.game_path, + install_type: this.$store.state.install_type + } as GameInstall; + let mod_to_install = this.mod_to_install_field_string; + await invoke("install_mod_caller", { gameInstall: game_install, thunderstoreModString: mod_to_install }).then((message) => { + // Show user notificatio if mod install completed. + ElNotification({ + title: `Installed ${mod_to_install}`, + message: message as string, + type: 'success', + position: 'bottom-right' + }); + }) + .catch((error) => { + ElNotification({ + title: 'Error', + message: error, + type: 'error', + position: 'bottom-right' + }); + }); + }, + async cleanUpDownloadFolder() { + let game_install = { + game_path: this.$store.state.game_path, + install_type: this.$store.state.install_type + } as GameInstall; + await invoke("clean_up_download_folder_caller", { gameInstall: game_install, force: true }).then((message) => { + // Show user notificatio if mod install completed. + ElNotification({ + title: `Done`, + message: `Done`, + type: 'success', + position: 'bottom-right' + }); + }) + .catch((error) => { + ElNotification({ + title: 'Error', + message: error, + type: 'error', + position: 'bottom-right' + }); + }); } } }); |