diff options
Diffstat (limited to 'src-tauri/src')
-rw-r--r-- | src-tauri/src/github/release_notes.rs | 15 | ||||
-rw-r--r-- | src-tauri/src/main.rs | 113 | ||||
-rw-r--r-- | src-tauri/src/mod_management/legacy.rs | 101 | ||||
-rw-r--r-- | src-tauri/src/mod_management/mod.rs | 380 | ||||
-rw-r--r-- | src-tauri/src/northstar/install.rs | 16 | ||||
-rw-r--r-- | src-tauri/src/northstar/mod.rs | 78 | ||||
-rw-r--r-- | src-tauri/src/platform_specific/linux.rs | 75 |
7 files changed, 634 insertions, 144 deletions
diff --git a/src-tauri/src/github/release_notes.rs b/src-tauri/src/github/release_notes.rs index 6db5b617..16b65183 100644 --- a/src-tauri/src/github/release_notes.rs +++ b/src-tauri/src/github/release_notes.rs @@ -58,14 +58,17 @@ pub async fn get_newest_flightcore_version() -> Result<FlightCoreVersion, String #[tauri::command] pub async fn check_is_flightcore_outdated() -> Result<bool, String> { let newest_flightcore_release = get_newest_flightcore_version().await?; + // Parse version number excluding leading `v` + let newest_version = semver::Version::parse(&newest_flightcore_release.tag_name[1..]).unwrap(); - // Get version of installed FlightCore... - let version = env!("CARGO_PKG_VERSION"); - // ...and format it - let version = format!("v{}", version); + // Get version of installed FlightCore + let current_version = env!("CARGO_PKG_VERSION"); + let current_version = semver::Version::parse(current_version).unwrap(); - // TODO: This shouldn't be a string compare but promper semver compare - let is_outdated = version != newest_flightcore_release.tag_name; + #[cfg(debug_assertions)] + let is_outdated = current_version < newest_version; + #[cfg(not(debug_assertions))] + let is_outdated = current_version != newest_version; // If outdated, check how new the update is if is_outdated { diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 6553ed4e..21408ff8 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -9,11 +9,6 @@ use std::{ time::Duration, }; -#[cfg(target_os = "windows")] -use std::ptr::null_mut; -#[cfg(target_os = "windows")] -use winapi::um::winuser::{MessageBoxW, MB_ICONERROR, MB_OK, MB_USERICON}; - use crate::constants::REFRESH_DELAY; mod development; @@ -26,6 +21,10 @@ mod util; use semver::Version; use serde::{Deserialize, Serialize}; +#[cfg(target_os = "windows")] +use tauri::api::dialog::blocking::MessageDialogBuilder; +#[cfg(target_os = "windows")] +use tauri::api::dialog::{MessageDialogButtons, MessageDialogKind}; use tauri::{Manager, Runtime}; use tokio::time::sleep; use ts_rs::TS; @@ -128,7 +127,7 @@ fn main() { install_northstar_caller, update_northstar, northstar::launch_northstar, - launch_northstar_steam, + northstar::launch_northstar_steam, github::release_notes::check_is_flightcore_outdated, repair_and_verify::get_log_list, repair_and_verify::verify_game_files, @@ -144,6 +143,9 @@ fn main() { mod_management::delete_northstar_mod, util::get_server_player_count, mod_management::delete_thunderstore_mod, + install_northstar_proton_wrapper, + uninstall_northstar_proton_wrapper, + get_local_northstar_proton_wrapper_version, open_repair_window, thunderstore::query_thunderstore_packages_api, github::get_list_of_tags, @@ -172,23 +174,16 @@ fn main() { #[cfg(target_os = "windows")] { log::error!("WebView2 not installed: {err}"); - // Display a message box to the user with a button to open the installation instructions - let title = "WebView2 not found" - .encode_utf16() - .chain(Some(0)) - .collect::<Vec<_>>(); - let message = "FlightCore requires WebView2 to run.\n\nClick OK to open installation instructions.".encode_utf16().chain(Some(0)).collect::<Vec<_>>(); - unsafe { - let result = MessageBoxW( - null_mut(), - message.as_ptr(), - title.as_ptr(), - MB_OK | MB_ICONERROR | MB_USERICON, - ); - if result == 1 { - // Open the installation instructions URL in the user's default web browser - open::that("https://github.com/R2NorthstarTools/FlightCore/blob/main/docs/TROUBLESHOOTING.md#flightcore-wont-launch").unwrap(); - } + let dialog = MessageDialogBuilder::new( + "WebView2 not found", + "FlightCore requires WebView2 to run.\n\nClick OK to open installation instructions." + ) + .kind(MessageDialogKind::Error) + .buttons(MessageDialogButtons::Ok); + + if dialog.show() { + // Open the installation instructions URL in the user's default web browser + open::that("https://github.com/R2NorthstarTools/FlightCore/blob/main/docs/TROUBLESHOOTING.md#flightcore-wont-launch").unwrap(); } } } @@ -461,8 +456,6 @@ mod platform_specific; #[cfg(target_os = "linux")] use platform_specific::linux; -use crate::constants::TITANFALL2_STEAM_ID; - #[derive(Serialize, Deserialize, Debug, Clone)] pub enum InstallType { STEAM, @@ -529,55 +522,31 @@ fn get_host_os() -> String { env::consts::OS.to_string() } -/// Prepare Northstar and Launch through Steam using the Browser Protocol +/// On Linux attempts to install NorthstarProton +/// On Windows simply returns an error message #[tauri::command] -fn launch_northstar_steam( - game_install: GameInstall, - _bypass_checks: Option<bool>, -) -> Result<String, String> { - if !matches!(game_install.install_type, InstallType::STEAM) { - return Err("Titanfall2 was not installed via Steam".to_string()); - } +async fn install_northstar_proton_wrapper() -> Result<(), String> { + #[cfg(target_os = "linux")] + return linux::install_ns_proton().map_err(|err| err.to_string()); - match steamlocate::SteamDir::locate() { - Some(mut steamdir) => { - if get_host_os() != "windows" { - let titanfall2_steamid: u32 = TITANFALL2_STEAM_ID.parse().unwrap(); - match steamdir.compat_tool(&titanfall2_steamid) { - Some(compat) => { - if !compat - .name - .clone() - .unwrap() - .to_ascii_lowercase() - .contains("northstarproton") - { - return Err( - "Titanfall2 was not configured to use NorthstarProton".to_string() - ); - } - } - None => { - return Err( - "Titanfall2 was not configured to use a compatibility tool".to_string() - ); - } - } - } - } - None => { - return Err("Couldn't access Titanfall2 directory".to_string()); - } - } + #[cfg(target_os = "windows")] + Err("Not supported on Windows".to_string()) +} - // Switch to Titanfall2 directory to set everything up - if std::env::set_current_dir(game_install.game_path).is_err() { - // We failed to get to Titanfall2 directory - return Err("Couldn't access Titanfall2 directory".to_string()); - } +#[tauri::command] +async fn uninstall_northstar_proton_wrapper() -> Result<(), String> { + #[cfg(target_os = "linux")] + return linux::uninstall_ns_proton(); - match open::that(format!("steam://run/{}//--northstar {}/", TITANFALL2_STEAM_ID, game_install.launch_parameters)) { - Ok(()) => Ok("Started game".to_string()), - Err(_err) => Err("Failed to launch Titanfall 2 via Steam".to_string()), - } + #[cfg(target_os = "windows")] + Err("Not supported on Windows".to_string()) +} + +#[tauri::command] +async fn get_local_northstar_proton_wrapper_version() -> Result<String, String> { + #[cfg(target_os = "linux")] + return linux::get_local_ns_proton_version(); + + #[cfg(target_os = "windows")] + Err("Not supported on Windows".to_string()) } diff --git a/src-tauri/src/mod_management/legacy.rs b/src-tauri/src/mod_management/legacy.rs index 0f9074d2..91463250 100644 --- a/src-tauri/src/mod_management/legacy.rs +++ b/src-tauri/src/mod_management/legacy.rs @@ -1,3 +1,7 @@ +use crate::constants::BLACKLISTED_MODS; +use crate::mod_management::{ + delete_mod_folder, get_installed_mods_and_properties, ParsedThunderstoreModString, +}; use crate::GameInstall; use crate::NorthstarMod; use anyhow::{anyhow, Result}; @@ -110,3 +114,100 @@ pub fn parse_installed_mods( // Return found mod names 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, + thunderstore_mod_string: String, +) -> Result<(), String> { + // Prevent deleting core mod + for core_ts_mod in BLACKLISTED_MODS { + if thunderstore_mod_string == core_ts_mod { + return Err(format!("Cannot remove core mod {thunderstore_mod_string}")); + } + } + + let parsed_ts_mod_string: ParsedThunderstoreModString = + thunderstore_mod_string.parse().unwrap(); + + // Get installed mods + let installed_ns_mods = get_installed_mods_and_properties(game_install)?; + + // List of mod folders to remove + let mut mod_folders_to_remove: Vec<String> = Vec::new(); + + // Get folder name based on Thundestore mod string + for installed_ns_mod in installed_ns_mods { + if installed_ns_mod.thunderstore_mod_string.is_none() { + // Not a Thunderstore mod + continue; + } + + let installed_ns_mod_ts_string: ParsedThunderstoreModString = installed_ns_mod + .thunderstore_mod_string + .unwrap() + .parse() + .unwrap(); + + // Installed mod matches specified Thunderstore mod string + if parsed_ts_mod_string.author_name == installed_ns_mod_ts_string.author_name + && parsed_ts_mod_string.mod_name == installed_ns_mod_ts_string.mod_name + { + // Add folder to list of folder to remove + mod_folders_to_remove.push(installed_ns_mod.directory); + } + } + + if mod_folders_to_remove.is_empty() { + return Err(format!( + "No mods removed as no Northstar mods matching {thunderstore_mod_string} were found to be installed." + )); + } + + // Delete given folders + for mod_folder in mod_folders_to_remove { + delete_mod_folder(&mod_folder)?; + } + + Ok(()) +} diff --git a/src-tauri/src/mod_management/mod.rs b/src-tauri/src/mod_management/mod.rs index a2aca85a..0d4edd87 100644 --- a/src-tauri/src/mod_management/mod.rs +++ b/src-tauri/src/mod_management/mod.rs @@ -2,17 +2,20 @@ use crate::constants::{BLACKLISTED_MODS, CORE_MODS}; use async_recursion::async_recursion; +use thermite::prelude::ThermiteError; use crate::NorthstarMod; -use anyhow::Result; +use anyhow::{anyhow, Result}; use serde::{Deserialize, Serialize}; +use std::str::FromStr; +use std::string::ToString; use std::{fs, path::PathBuf}; mod legacy; use crate::GameInstall; #[derive(Debug, Clone)] -struct ParsedThunderstoreModString { +pub struct ParsedThunderstoreModString { author_name: String, mod_name: String, version: String, @@ -22,6 +25,12 @@ impl std::str::FromStr for ParsedThunderstoreModString { type Err = &'static str; // todo use an better error management fn from_str(s: &str) -> Result<Self, Self::Err> { + // Check whether Thunderstore string passse reges + let re = regex::Regex::new(r"^[a-zA-Z0-9_]+-[a-zA-Z0-9_]+-\d+\.\d+\.\d++$").unwrap(); + if !re.is_match(s) { + return Err("Incorrect format"); + } + let mut parts = s.split('-'); let author_name = parts.next().ok_or("None value on author_name")?.to_string(); @@ -36,6 +45,12 @@ impl std::str::FromStr for ParsedThunderstoreModString { } } +impl ToString for ParsedThunderstoreModString { + fn to_string(&self) -> String { + format!("{}-{}-{}", self.author_name, self.mod_name, self.version) + } +} + #[derive(Serialize, Deserialize, Debug, Clone)] pub struct ThunderstoreManifest { name: String, @@ -170,6 +185,132 @@ pub fn set_mod_enabled_status( Ok(()) } +/// Resembles the bare minimum keys in Northstar `mods.json` +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct ModJson { + #[serde(rename = "Name")] + name: String, + #[serde(rename = "Version")] + version: Option<String>, +} + +/// Parse `mods` folder for installed mods. +pub fn parse_mods_in_package( + package_mods_path: PathBuf, + thunderstore_mod_string: ParsedThunderstoreModString, +) -> Result<Vec<NorthstarMod>, anyhow::Error> { + let paths = match std::fs::read_dir(package_mods_path) { + Ok(paths) => paths, + Err(_err) => return Err(anyhow!("No mods folder found")), + }; + + let mut directories: Vec<PathBuf> = Vec::new(); + let mut mods: Vec<NorthstarMod> = 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); + } + } + + // Iterate over folders and check if they are Northstar mods + for directory in directories { + let directory_str = directory.to_str().unwrap().to_string(); + // Check if mod.json exists + let mod_json_path = format!("{}/mod.json", directory_str); + if !std::path::Path::new(&mod_json_path).exists() { + continue; + } + + // Read file into string and parse it + let data = std::fs::read_to_string(mod_json_path.clone())?; + let parsed_mod_json: ModJson = match json5::from_str(&data) { + Ok(parsed_json) => parsed_json, + Err(err) => { + log::warn!("Failed parsing {} with {}", mod_json_path, err.to_string()); + continue; + } + }; + + // Get directory path + let mod_directory = directory.to_str().unwrap().to_string(); + + let ns_mod = NorthstarMod { + name: parsed_mod_json.name, + version: parsed_mod_json.version, + thunderstore_mod_string: Some(thunderstore_mod_string.to_string()), + enabled: false, // Placeholder + directory: mod_directory, + }; + + mods.push(ns_mod); + } + + // Return found mod names + Ok(mods) +} + +/// Parse `packages` folder for installed mods. +pub fn parse_installed_package_mods( + game_install: &GameInstall, +) -> Result<Vec<NorthstarMod>, anyhow::Error> { + let mut collected_mods: Vec<NorthstarMod> = Vec::new(); + + let packages_folder = format!("{}/R2Northstar/packages/", game_install.game_path); + + let packages_dir = match fs::read_dir(packages_folder) { + Ok(res) => res, + Err(err) => { + // We couldn't read directory, probably cause it doesn't exist yet. + // In that case we just say no package mods installed. + log::warn!("{err}"); + return Ok(vec![]); + } + }; + + // Iteratore over folders in `packages` dir + for entry in packages_dir { + let entry_path = entry?.path(); + let entry_str = entry_path.file_name().unwrap().to_str().unwrap(); + + // Use the struct's from_str function to verify format + if entry_path.is_dir() { + let package_thunderstore_string = match ParsedThunderstoreModString::from_str(entry_str) + { + Ok(res) => res, + Err(err) => { + log::warn!( + "Not a Thunderstore mod string \"{}\" cause: {}", + entry_path.display(), + err + ); + continue; + } + }; + let manifest_path = entry_path.join("manifest.json"); + let mods_path = entry_path.join("mods"); + + // Ensure `manifest.json` and `mods/` dir exist + if manifest_path.exists() && mods_path.is_dir() { + let mods = + match parse_mods_in_package(mods_path, package_thunderstore_string.clone()) { + Ok(res) => res, + Err(err) => { + log::warn!("Failed parsing cause: {err}"); + continue; + } + }; + collected_mods.extend(mods); + } + } + } + + Ok(collected_mods) +} + /// Gets list of installed mods and their properties /// - name /// - is enabled? @@ -177,12 +318,20 @@ pub fn set_mod_enabled_status( pub fn get_installed_mods_and_properties( game_install: GameInstall, ) -> Result<Vec<NorthstarMod>, String> { - // Get actually installed mods - let found_installed_mods = match legacy::parse_installed_mods(&game_install) { + // Get installed mods from packages + let mut found_installed_mods = match parse_installed_package_mods(&game_install) { + Ok(res) => res, + Err(err) => return Err(err.to_string()), + }; + // Get installed legacy mods + let found_installed_legacy_mods = match legacy::parse_installed_mods(&game_install) { Ok(res) => res, Err(err) => return Err(err.to_string()), }; + // Combine list of package and legacy mods + found_installed_mods.extend(found_installed_legacy_mods); + // Get enabled mods as JSON let enabled_mods: serde_json::Value = match get_enabled_mods(&game_install) { Ok(enabled_mods) => enabled_mods, @@ -259,6 +408,106 @@ 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; + } + }; + // 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; + } + } + } + } + + if file_path.starts_with("plugins/") { + if let Some(name) = file_path.file_name() { + if name.to_str().unwrap().contains(".dll") { + log::warn!("Plugin detected, skipping"); + return false; // We disallow plugins for now + } + } + } + } + + 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. @@ -273,7 +522,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() { @@ -338,19 +586,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); } }; @@ -400,60 +672,58 @@ pub fn delete_northstar_mod(game_install: GameInstall, nsmod_name: String) -> Re Err(format!("Mod {nsmod_name} not found to be installed")) } +/// Deletes a given Thunderstore package +fn delete_package_folder(ts_package_directory: &str) -> Result<(), String> { + let ns_mod_dir_path = std::path::Path::new(&ts_package_directory); + + // Safety check: Check whether `manifest.json` exists and exit early if not + // If it does not exist, we might not be dealing with a Thunderstore package + let mod_json_path = ns_mod_dir_path.join("manifest.json"); + if !mod_json_path.exists() { + // If it doesn't exist, return an error + return Err(format!( + "manifest.json does not exist in {}", + ts_package_directory + )); + } + + match std::fs::remove_dir_all(ts_package_directory) { + Ok(()) => Ok(()), + Err(err) => Err(format!("Failed deleting package: {err}")), + } +} + /// Deletes all NorthstarMods related to a Thunderstore mod #[tauri::command] pub fn delete_thunderstore_mod( game_install: GameInstall, thunderstore_mod_string: String, ) -> Result<(), String> { - // Prevent deleting core mod - for core_ts_mod in BLACKLISTED_MODS { - if thunderstore_mod_string == core_ts_mod { - return Err(format!("Cannot remove core mod {thunderstore_mod_string}")); - } - } - - let parsed_ts_mod_string: ParsedThunderstoreModString = - thunderstore_mod_string.parse().unwrap(); - - // Get installed mods - let installed_ns_mods = get_installed_mods_and_properties(game_install)?; - - // List of mod folders to remove - let mut mod_folders_to_remove: Vec<String> = Vec::new(); + // Check packages + let packages_folder = format!("{}/R2Northstar/packages", game_install.game_path); + if std::path::Path::new(&packages_folder).exists() { + for entry in fs::read_dir(packages_folder).unwrap() { + let entry = entry.unwrap(); + + // Check if it's a folder and skip if otherwise + if !entry.file_type().unwrap().is_dir() { + log::warn!("Skipping \"{}\", not a file", entry.path().display()); + continue; + } - // Get folder name based on Thundestore mod string - for installed_ns_mod in installed_ns_mods { - if installed_ns_mod.thunderstore_mod_string.is_none() { - // Not a Thunderstore mod - continue; - } + let entry_path = entry.path(); + let package_folder_ts_string = entry_path.file_name().unwrap().to_string_lossy(); - let installed_ns_mod_ts_string: ParsedThunderstoreModString = installed_ns_mod - .thunderstore_mod_string - .unwrap() - .parse() - .unwrap(); + if package_folder_ts_string != thunderstore_mod_string { + // Not the mod folder we are looking for, try the next one\ + continue; + } - // Installed mod matches specified Thunderstore mod string - if parsed_ts_mod_string.author_name == installed_ns_mod_ts_string.author_name - && parsed_ts_mod_string.mod_name == installed_ns_mod_ts_string.mod_name - { - // Add folder to list of folder to remove - mod_folders_to_remove.push(installed_ns_mod.directory); + // All checks passed, this is the matching mod + return delete_package_folder(&entry.path().display().to_string()); } } - if mod_folders_to_remove.is_empty() { - return Err(format!( - "No mods removed as no Northstar mods matching {thunderstore_mod_string} were found to be installed." - )); - } - - // Delete given folders - for mod_folder in mod_folders_to_remove { - delete_mod_folder(&mod_folder)?; - } - - Ok(()) + // Try legacy mod installs as fallback + legacy::delete_thunderstore_mod(game_install, thunderstore_mod_string) } diff --git a/src-tauri/src/northstar/install.rs b/src-tauri/src/northstar/install.rs index 71da515d..28366738 100644 --- a/src-tauri/src/northstar/install.rs +++ b/src-tauri/src/northstar/install.rs @@ -168,6 +168,22 @@ pub fn find_game_install_location() -> Result<GameInstall, String> { // Attempt parsing Steam library directly match steamlocate::SteamDir::locate() { Some(mut steamdir) => { + #[cfg(target_os = "linux")] + { + let snap_dir = match std::env::var("SNAP_USER_DATA") { + Ok(snap_dir) => std::path::PathBuf::from(snap_dir), + Err(_) => match dirs::home_dir() { + Some(path) => path, + None => std::path::PathBuf::new(), + } + .join("snap"), + }; + + if steamdir.path.starts_with(snap_dir) { + log::warn!("Found Steam installed via Snap, you may encounter issues"); + } + } + let titanfall2_steamid = TITANFALL2_STEAM_ID.parse().unwrap(); match steamdir.app(&titanfall2_steamid) { Some(app) => { diff --git a/src-tauri/src/northstar/mod.rs b/src-tauri/src/northstar/mod.rs index 2630ff1f..6d0e26d2 100644 --- a/src-tauri/src/northstar/mod.rs +++ b/src-tauri/src/northstar/mod.rs @@ -3,7 +3,10 @@ pub mod install; use crate::util::check_ea_app_or_origin_running; -use crate::{constants::CORE_MODS, get_host_os, GameInstall, InstallType}; +use crate::{ + constants::{CORE_MODS, TITANFALL2_STEAM_ID}, + get_host_os, GameInstall, InstallType, +}; use anyhow::anyhow; /// Check version number of a mod @@ -65,16 +68,16 @@ pub fn launch_northstar( let host_os = get_host_os(); // Explicitly fail early certain (currently) unsupported install setups - if host_os != "windows" - || !(matches!(game_install.install_type, InstallType::STEAM) - || matches!(game_install.install_type, InstallType::ORIGIN) - || matches!(game_install.install_type, InstallType::UNKNOWN)) - { - return Err(format!( - "Not yet implemented for \"{}\" with Titanfall2 installed via \"{:?}\"", - get_host_os(), - game_install.install_type - )); + if host_os != "windows" { + if !matches!(game_install.install_type, InstallType::STEAM) { + return Err(format!( + "Not yet implemented for \"{}\" with Titanfall2 installed via \"{:?}\"", + get_host_os(), + game_install.install_type + )); + } + + return launch_northstar_steam(game_install, bypass_checks); } let bypass_checks = bypass_checks.unwrap_or(false); @@ -128,3 +131,56 @@ pub fn launch_northstar( get_host_os() )) } + +/// Prepare Northstar and Launch through Steam using the Browser Protocol +#[tauri::command] +pub fn launch_northstar_steam( + game_install: GameInstall, + _bypass_checks: Option<bool>, +) -> Result<String, String> { + if !matches!(game_install.install_type, InstallType::STEAM) { + return Err("Titanfall2 was not installed via Steam".to_string()); + } + + match steamlocate::SteamDir::locate() { + Some(mut steamdir) => { + if get_host_os() != "windows" { + let titanfall2_steamid: u32 = TITANFALL2_STEAM_ID.parse().unwrap(); + match steamdir.compat_tool(&titanfall2_steamid) { + Some(compat) => { + if !compat + .name + .clone() + .unwrap() + .to_ascii_lowercase() + .contains("northstarproton") + { + return Err( + "Titanfall2 was not configured to use NorthstarProton".to_string() + ); + } + } + None => { + return Err( + "Titanfall2 was not configured to use a compatibility tool".to_string() + ); + } + } + } + } + None => { + return Err("Couldn't access Titanfall2 directory".to_string()); + } + } + + // Switch to Titanfall2 directory to set everything up + if std::env::set_current_dir(game_install.game_path).is_err() { + // We failed to get to Titanfall2 directory + return Err("Couldn't access Titanfall2 directory".to_string()); + } + + match open::that(format!("steam://run/{}//--northstar {}/", TITANFALL2_STEAM_ID, game_install.launch_parameters)) { + Ok(()) => Ok("Started game".to_string()), + Err(_err) => Err("Failed to launch Titanfall 2 via Steam".to_string()), + } +} diff --git a/src-tauri/src/platform_specific/linux.rs b/src-tauri/src/platform_specific/linux.rs index 4b9964e9..674f384b 100644 --- a/src-tauri/src/platform_specific/linux.rs +++ b/src-tauri/src/platform_specific/linux.rs @@ -3,6 +3,81 @@ use regex::Regex; use std::process::Command; +fn get_proton_dir() -> Option<String> { + let steam_dir = steamlocate::SteamDir::locate()?; + let compat_dir = format!("{}/compatibilitytools.d/", steam_dir.path.display()); + + Some(compat_dir) +} + +/// Downloads and installs NS proton +/// Assumes Steam install +pub fn install_ns_proton() -> Result<(), thermite::prelude::ThermiteError> { + // Get latest NorthstarProton release + let latest = thermite::core::latest_release()?; + + let temp_dir = std::env::temp_dir(); + let path = format!("{}/nsproton-{}.tar.gz", temp_dir.display(), latest); + let archive = std::fs::File::create(path.clone())?; + + // Download the latest Proton release + log::info!("Downloading NorthstarProton to {}", path); + thermite::core::download_ns_proton(latest, archive)?; + log::info!("Finished Download"); + + let compat_dir = get_proton_dir().unwrap(); + std::fs::create_dir_all(compat_dir.clone())?; + + let finished = std::fs::File::open(path.clone())?; + + // Extract to Proton dir + log::info!("Installing NorthstarProton to {}", compat_dir); + thermite::core::install_ns_proton(&finished, compat_dir)?; + log::info!("Finished Installation"); + drop(finished); + + std::fs::remove_file(path)?; + + Ok(()) +} + +/// Remove NS Proton +pub fn uninstall_ns_proton() -> Result<(), String> { + let compat_dir = get_proton_dir().unwrap(); + let pattern = format!("{}/NorthstarProton-*", compat_dir); + for e in glob::glob(&pattern).expect("Failed to read glob pattern") { + std::fs::remove_dir_all(e.unwrap()).unwrap(); + } + + Ok(()) +} + +/// Get the latest installed NS Proton version +pub fn get_local_ns_proton_version() -> Result<String, String> { + let compat_dir = get_proton_dir().unwrap(); + let ns_prefix = "NorthstarProton-"; + let pattern = format!("{}/{}*/version", compat_dir, ns_prefix); + + let mut version: String = "".to_string(); + + for e in glob::glob(&pattern).expect("Failed to read glob pattern") { + let version_content = std::fs::read_to_string(e.unwrap()).unwrap(); + let version_string = version_content.split(' ').nth(1).unwrap(); + + if version_string.starts_with(ns_prefix) { + version = version_string[ns_prefix.len()..version_string.len() - 1] + .to_string() + .clone(); + } + } + + if version.is_empty() { + return Err("Northstar Proton is not installed".to_string()); + } + + Ok(version) +} + pub fn check_glibc_v() -> f32 { let out = Command::new("/bin/ldd") .arg("--version") |