aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGeckoEidechse <40122905+GeckoEidechse@users.noreply.github.com>2022-11-19 10:04:43 +0100
committerGitHub <noreply@github.com>2022-11-19 10:04:43 +0100
commit38e12489a517662157d85ec6efec00f225cccc9c (patch)
tree16d8218fffaba26f8e44d6dff56ef4e3e1f29233
parentd3c190bd4d3461fc4c310f64eb74a8e2425baaad (diff)
downloadFlightCore-38e12489a517662157d85ec6efec00f225cccc9c.tar.gz
FlightCore-38e12489a517662157d85ec6efec00f225cccc9c.zip
feat: Initial support for installing mods from TS (#32)
* feat: Initial support for installing mods from TS This is the basic code needed to install a mod from Thunderstore * refactor: Remove console log, show msg in notif Instead of console logging result message, show it in notification instead. * refactor: Rename function to indicate behaviour Function not only installs but also downloads mod first. Although it does remove downloaded zip post installation. * refactor: Move install logic to dedicated module `mod_management` module didn't exist when this PR was created * chore: Trim single leftover newline * fix: Update code for newer `libthermite` version * feat: Allow installing older versions of mods Installs the given version number instead of only allowing latest. * fix: Explicit error msg for installing NS as mod While it would fail during install anyway, having explicit error message is nicer * feat: Write TS mod string to mod.json Write Thunderstore mod string of installed mod to its `mod.json` This way we can later check whether a mod is outdated based on the Thunderstore mod string * fix: Early return on empty string Prevent trying to install the first mod that matches an early string. We should never pass an empty string in the first place but better safe then sorry. * build: Add dependency for recursive async Needed for recursive mod dependency install * feat: Recursively install mod dependencies * fix: Early catch installing R2modman as mod Just in case to prevent someone trying to install R2modman as a mod. * refactor: Remove debug prints * fix: Allow installing mods having NS as dependency They would previously error out as Northstar cannot be installed as dependency. We now catch that specific error and return Ok(()) * fix: Delete download folder after mod install Deletes download folder after mod install if non-empty. * fix: Do not early leave when dependency is NS Logic error, instead of skipping installing Northstar as dependency it would previously just return early with success. * chore: Remove leftover commented out code
-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'
+ });
+ });
}
}
});