aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src-tauri/Cargo.lock12
-rw-r--r--src-tauri/Cargo.toml2
-rw-r--r--src-tauri/src/main.rs42
-rw-r--r--src-tauri/src/mod_management/mod.rs176
-rw-r--r--src-tauri/src/repair_and_verify/mod.rs31
-rw-r--r--src-vue/src/views/DeveloperView.vue65
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'
+ });
+ });
}
}
});