diff options
Diffstat (limited to 'src-tauri/src/northstar')
-rw-r--r-- | src-tauri/src/northstar/install.rs | 358 | ||||
-rw-r--r-- | src-tauri/src/northstar/mod.rs | 276 | ||||
-rw-r--r-- | src-tauri/src/northstar/profile.rs | 121 |
3 files changed, 0 insertions, 755 deletions
diff --git a/src-tauri/src/northstar/install.rs b/src-tauri/src/northstar/install.rs deleted file mode 100644 index 0953fa38..00000000 --- a/src-tauri/src/northstar/install.rs +++ /dev/null @@ -1,358 +0,0 @@ -use anyhow::Result; -use serde::{Deserialize, Serialize}; -use std::time::Duration; -use std::{cell::RefCell, time::Instant}; -use ts_rs::TS; - -use crate::constants::{CORE_MODS, NORTHSTAR_DEFAULT_PROFILE, NORTHSTAR_DLL}; -use crate::{ - util::{extract, move_dir_all}, - GameInstall, InstallType, -}; - -#[cfg(target_os = "windows")] -use crate::platform_specific::windows; - -#[derive(Serialize, Deserialize, Debug, Clone, TS)] -#[ts(export)] -enum InstallState { - Downloading, - Extracting, - Done, -} - -#[derive(Serialize, Deserialize, Debug, Clone, TS)] -#[ts(export)] -struct InstallProgress { - current_downloaded: u64, - total_size: u64, - state: InstallState, -} - -/// Installs Northstar to the given path -#[tauri::command] -pub async fn install_northstar_wrapper( - window: tauri::Window, - game_install: GameInstall, - northstar_package_name: Option<String>, - version_number: Option<String>, -) -> Result<bool, String> { - log::info!("Running Northstar install"); - - // Get Northstar package name (`Northstar` vs `NorthstarReleaseCandidate`) - let northstar_package_name = northstar_package_name - .map(|name| { - if name.len() <= 1 { - "Northstar".to_string() - } else { - name - } - }) - .unwrap_or("Northstar".to_string()); - - match install_northstar(window, game_install, northstar_package_name, version_number).await { - Ok(_) => Ok(true), - Err(err) => { - log::error!("{}", err); - Err(err) - } - } -} - -/// Update Northstar install in the given path -#[tauri::command] -pub async fn update_northstar( - window: tauri::Window, - game_install: GameInstall, - northstar_package_name: Option<String>, -) -> Result<bool, String> { - log::info!("Updating Northstar"); - - // Simply re-run install with up-to-date version for upate - install_northstar_wrapper(window, game_install, northstar_package_name, None).await -} - -/// Copied from `papa` source code and modified -///Install N* from the provided mod -/// -///Checks cache, else downloads the latest version -async fn do_install( - window: tauri::Window, - nmod: &thermite::model::ModVersion, - game_install: GameInstall, -) -> Result<()> { - let filename = format!("northstar-{}.zip", nmod.version); - let temp_dir = format!("{}/___flightcore-temp", game_install.game_path); - let download_directory = format!("{}/download-dir", temp_dir); - let extract_directory = format!("{}/extract-dir", temp_dir); - - log::info!("Attempting to create temporary directory {}", temp_dir); - std::fs::create_dir_all(download_directory.clone())?; - std::fs::create_dir_all(extract_directory.clone())?; - - let download_path = format!("{}/{}", download_directory, filename); - log::info!("Download path: {download_path}"); - - let last_emit = RefCell::new(Instant::now()); // Keep track of the last time a signal was emitted - let mut nfile = std::fs::File::options() - .read(true) - .write(true) - .truncate(true) - .create(true) - .open(download_path)?; - thermite::core::manage::download_with_progress( - &mut nfile, - &nmod.url, - |delta, current, total| { - if delta != 0 { - // Only emit a signal once every 100ms - // This way we don't bombard the frontend with events on fast download speeds - let time_since_last_emit = Instant::now().duration_since(*last_emit.borrow()); - if time_since_last_emit >= Duration::from_millis(100) { - window - .emit( - "northstar-install-download-progress", - InstallProgress { - current_downloaded: current, - total_size: total, - state: InstallState::Downloading, - }, - ) - .unwrap(); - *last_emit.borrow_mut() = Instant::now(); - } - } - }, - )?; - - window - .emit( - "northstar-install-download-progress", - InstallProgress { - current_downloaded: 0, - total_size: 0, - state: InstallState::Extracting, - }, - ) - .unwrap(); - - log::info!("Extracting Northstar..."); - extract(nfile, std::path::Path::new(&extract_directory))?; - - // Prepare Northstar for Installation - log::info!("Preparing Northstar..."); - if game_install.profile != NORTHSTAR_DEFAULT_PROFILE { - // We are using a non standard Profile, we must: - // - move the DLL - // - rename the Profile - - // Move DLL into the default R2Northstar Profile - let old_dll_path = format!("{}/{}", extract_directory, NORTHSTAR_DLL); - let new_dll_path = format!( - "{}/{}/{}", - extract_directory, NORTHSTAR_DEFAULT_PROFILE, NORTHSTAR_DLL - ); - std::fs::rename(old_dll_path, new_dll_path)?; - - // rename default R2Northstar Profile to the profile we want to use - let old_profile_path = format!("{}/{}/", extract_directory, NORTHSTAR_DEFAULT_PROFILE); - let new_profile_path = format!("{}/{}/", extract_directory, game_install.profile); - std::fs::rename(old_profile_path, new_profile_path)?; - } - - log::info!("Installing Northstar..."); - - // Delete previous version here - for core_mod in CORE_MODS { - let path_to_delete_string = format!( - "{}/{}/mods/{}/", - game_install.game_path, game_install.profile, core_mod - ); - log::info!("Preparing to remove {}", path_to_delete_string); - - // Check if folder exists - let path_to_delete = std::path::Path::new(&path_to_delete_string); - - // Check if path even exists before we attempt to remove - if !path_to_delete.exists() { - log::info!("{} does not exist. Skipping", path_to_delete_string); - continue; - } - - if !path_to_delete.is_dir() { - log::error!( - "{} exists but is a file? This should never happen", - path_to_delete_string - ); - continue; - } - - // Safety check for mod.json - // Just so that we won't ever have a https://github.com/ValveSoftware/steam-for-linux/issues/3671 moment - let mod_json_path = format!("{}/mod.json", path_to_delete_string); - let mod_json_path = std::path::Path::new(&mod_json_path); - - if !mod_json_path.exists() { - log::error!("Missing mod.json for {path_to_delete_string} this shouldn't happen"); - continue; - } - - // Finally delete file - match std::fs::remove_dir_all(path_to_delete) { - Ok(()) => { - log::info!("Succesfully removed") - } - Err(err) => { - log::error!("Failed removing {} due to {}", path_to_delete_string, err) - } - }; - } - - for entry in std::fs::read_dir(extract_directory).unwrap() { - let entry = entry.unwrap(); - let destination = format!( - "{}/{}", - game_install.game_path, - entry.path().file_name().unwrap().to_str().unwrap() - ); - - log::info!("Installing {}", entry.path().display()); - if !entry.file_type().unwrap().is_dir() { - std::fs::rename(entry.path(), destination)?; - } else { - move_dir_all(entry.path(), destination)?; - } - } - - // Delete old copy - log::info!("Delete temporary directory"); - std::fs::remove_dir_all(temp_dir).unwrap(); - - log::info!("Done installing Northstar!"); - window - .emit( - "northstar-install-download-progress", - InstallProgress { - current_downloaded: 0, - total_size: 0, - state: InstallState::Done, - }, - ) - .unwrap(); - - Ok(()) -} - -pub async fn install_northstar( - window: tauri::Window, - game_install: GameInstall, - northstar_package_name: String, - version_number: Option<String>, -) -> Result<String, String> { - let index = match thermite::api::get_package_index() { - Ok(res) => res.to_vec(), - Err(err) => { - log::warn!("Failed fetching package index due to: {err}"); - return Err("Failed to connect to Thunderstore.".to_string()); - } - }; - let nmod = index - .iter() - .find(|f| f.name.to_lowercase() == northstar_package_name.to_lowercase()) - .ok_or_else(|| panic!("Couldn't find Northstar on thunderstore???")) - .unwrap(); - - // Use passed version or latest if no version was passed - let version = version_number.as_ref().unwrap_or(&nmod.latest); - - let game_path = game_install.game_path.clone(); - log::info!("Install path \"{}\"", game_path); - - match do_install(window, nmod.versions.get(version).unwrap(), game_install).await { - Ok(_) => (), - Err(err) => { - if game_path - .to_lowercase() - .contains(&r"C:\Program Files\".to_lowercase()) - // default is `C:\Program Files\EA Games\Titanfall2` - { - return Err( - "Cannot install to default EA App install path, please move Titanfall2 to a different install location.".to_string(), - ); - } else { - return Err(err.to_string()); - } - } - } - - Ok(nmod.latest.clone()) -} - -/// Attempts to find the game install location -#[tauri::command] -pub fn find_game_install_location() -> Result<GameInstall, String> { - // Attempt parsing Steam library directly - match steamlocate::SteamDir::locate() { - Ok(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"); - } - } - - match steamdir.find_app(thermite::TITANFALL2_STEAM_ID) { - Ok(Some((app, library))) => { - let app_path = library - .path() - .join("steamapps") - .join("common") - .join(app.install_dir) - .into_os_string() - .into_string() - .unwrap(); - - let game_install = GameInstall { - game_path: app_path, - profile: "R2Northstar".to_string(), - install_type: InstallType::STEAM, - }; - return Ok(game_install); - } - Ok(None) => log::info!("Couldn't locate your Titanfall 2 Steam install."), - Err(err) => log::info!( - "Something went wrong while trying to find Titanfall 2 {}", - err - ), - } - } - Err(err) => log::info!("Couldn't locate Steam on this computer! {}", err), - } - - // (On Windows only) try parsing Windows registry for Origin install path - #[cfg(target_os = "windows")] - match windows::origin_install_location_detection() { - Ok(game_path) => { - let game_install = GameInstall { - game_path, - profile: "R2Northstar".to_string(), - install_type: InstallType::ORIGIN, - }; - return Ok(game_install); - } - Err(err) => { - log::info!("{}", err); - } - }; - - Err("Could not auto-detect game install location! Please enter it manually.".to_string()) -} diff --git a/src-tauri/src/northstar/mod.rs b/src-tauri/src/northstar/mod.rs deleted file mode 100644 index 9953d742..00000000 --- a/src-tauri/src/northstar/mod.rs +++ /dev/null @@ -1,276 +0,0 @@ -//! This module deals with handling things around Northstar such as -//! - getting version number -pub mod install; -pub mod profile; - -use crate::util::check_ea_app_or_origin_running; -use crate::{constants::CORE_MODS, platform_specific::get_host_os, GameInstall, InstallType}; -use crate::{NorthstarThunderstoreRelease, NorthstarThunderstoreReleaseWrapper}; -use anyhow::anyhow; -use serde::{Deserialize, Serialize}; -use ts_rs::TS; - -#[derive(Serialize, Deserialize, Debug, Clone, TS)] -#[ts(export)] -pub struct NorthstarLaunchOptions { - launch_via_steam: bool, - bypass_checks: bool, -} - -/// Gets list of available Northstar versions from Thunderstore -#[tauri::command] -pub async fn get_available_northstar_versions( -) -> Result<Vec<NorthstarThunderstoreReleaseWrapper>, ()> { - let northstar_package_name = "Northstar"; - let index = thermite::api::get_package_index().unwrap().to_vec(); - let nsmod = index - .iter() - .find(|f| f.name.to_lowercase() == northstar_package_name.to_lowercase()) - .ok_or_else(|| panic!("Couldn't find Northstar on thunderstore???")) - .unwrap(); - - let mut releases: Vec<NorthstarThunderstoreReleaseWrapper> = vec![]; - for (_version_string, nsmod_version_obj) in nsmod.versions.iter() { - let current_elem = NorthstarThunderstoreRelease { - package: nsmod_version_obj.name.clone(), - version: nsmod_version_obj.version.clone(), - }; - let current_elem_wrapped = NorthstarThunderstoreReleaseWrapper { - label: format!( - "{} v{}", - nsmod_version_obj.name.clone(), - nsmod_version_obj.version.clone() - ), - value: current_elem, - }; - - releases.push(current_elem_wrapped); - } - - releases.sort_by(|a, b| { - // Parse version number - let a_ver = semver::Version::parse(&a.value.version).unwrap(); - let b_ver = semver::Version::parse(&b.value.version).unwrap(); - b_ver.partial_cmp(&a_ver).unwrap() // Sort newest first - }); - - Ok(releases) -} - -/// Checks if installed Northstar version is up-to-date -/// false -> Northstar install is up-to-date -/// true -> Northstar install is outdated -#[tauri::command] -pub async fn check_is_northstar_outdated( - game_install: GameInstall, - northstar_package_name: Option<String>, -) -> Result<bool, String> { - let northstar_package_name = match northstar_package_name { - Some(northstar_package_name) => { - if northstar_package_name.len() <= 1 { - "Northstar".to_string() - } else { - northstar_package_name - } - } - None => "Northstar".to_string(), - }; - - let index = match thermite::api::get_package_index() { - Ok(res) => res.to_vec(), - Err(err) => return Err(format!("Couldn't check if Northstar up-to-date: {err}")), - }; - let nmod = index - .iter() - .find(|f| f.name.to_lowercase() == northstar_package_name.to_lowercase()) - .expect("Couldn't find Northstar on thunderstore???"); - // .ok_or_else(|| anyhow!("Couldn't find Northstar on thunderstore???"))?; - - let version_number = match get_northstar_version_number(game_install) { - Ok(version_number) => version_number, - Err(err) => { - log::warn!("{}", err); - // If we fail to get new version just assume we are up-to-date - return Err(err); - } - }; - - // Release candidate version numbers are different between `mods.json` and Thunderstore - let version_number = crate::util::convert_release_candidate_number(version_number); - - if version_number != nmod.latest { - log::info!("Installed Northstar version outdated"); - Ok(true) - } else { - log::info!("Installed Northstar version up-to-date"); - Ok(false) - } -} - -/// Check version number of a mod -pub fn check_mod_version_number(path_to_mod_folder: &str) -> Result<String, anyhow::Error> { - let data = std::fs::read_to_string(format!("{path_to_mod_folder}/mod.json"))?; - let parsed_json: serde_json::Value = serde_json::from_str(&data)?; - - let mod_version_number = match parsed_json.get("Version").and_then(|value| value.as_str()) { - Some(version_number) => version_number, - None => return Err(anyhow!("No version number found")), - }; - - log::info!("{}", mod_version_number); - - Ok(mod_version_number.to_string()) -} - -/// Returns the current Northstar version number as a string -#[tauri::command] -pub fn get_northstar_version_number(game_install: GameInstall) -> Result<String, String> { - log::info!("{}", game_install.game_path); - - // TODO: - // Check if NorthstarLauncher.exe exists and check its version number - let initial_version_number = match check_mod_version_number(&format!( - "{}/{}/mods/{}", - game_install.game_path, game_install.profile, CORE_MODS[0] - )) { - Ok(version_number) => version_number, - Err(err) => return Err(err.to_string()), - }; - - for core_mod in CORE_MODS { - let current_version_number = match check_mod_version_number(&format!( - "{}/{}/mods/{}", - game_install.game_path, game_install.profile, core_mod - )) { - Ok(version_number) => version_number, - Err(err) => return Err(err.to_string()), - }; - if current_version_number != initial_version_number { - // We have a version number mismatch - return Err("Found version number mismatch".to_string()); - } - } - log::info!("All mods same version"); - - Ok(initial_version_number) -} - -/// Launches Northstar -#[tauri::command] -pub fn launch_northstar( - game_install: GameInstall, - launch_options: NorthstarLaunchOptions, -) -> Result<String, String> { - dbg!(game_install.clone()); - - if launch_options.launch_via_steam { - return launch_northstar_steam(game_install); - } - - let host_os = get_host_os(); - - // Explicitly fail early certain (currently) unsupported install setups - 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); - } - - // Only check guards if bypassing checks is not enabled - if !launch_options.bypass_checks { - // Some safety checks before, should have more in the future - if get_northstar_version_number(game_install.clone()).is_err() { - return Err(anyhow!("Not all checks were met").to_string()); - } - - // Require EA App or Origin to be running to launch Northstar - let ea_app_is_running = check_ea_app_or_origin_running(); - if !ea_app_is_running { - return Err( - anyhow!("EA App not running, start EA App before launching Northstar").to_string(), - ); - } - } - - // Switch to Titanfall2 directory for launching - // NorthstarLauncher.exe expects to be run from that folder - if std::env::set_current_dir(game_install.game_path.clone()).is_err() { - // We failed to get to Titanfall2 directory - return Err(anyhow!("Couldn't access Titanfall2 directory").to_string()); - } - - // Only Windows with Steam or Origin are supported at the moment - 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)) - { - let ns_exe_path = format!("{}/NorthstarLauncher.exe", game_install.game_path); - let ns_profile_arg = format!("-profile={}", game_install.profile); - - let mut output = std::process::Command::new("C:\\Windows\\System32\\cmd.exe") - .args(["/C", "start", "", &ns_exe_path, &ns_profile_arg]) - .spawn() - .expect("failed to execute process"); - output.wait().expect("failed waiting on child process"); - return Ok("Launched game".to_string()); - } - - Err(format!( - "Not yet implemented for {:?} on {}", - game_install.install_type, - get_host_os() - )) -} - -/// Prepare Northstar and Launch through Steam using the Browser Protocol -pub fn launch_northstar_steam(game_install: GameInstall) -> 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() { - Ok(steamdir) => { - if get_host_os() != "windows" { - match steamdir.compat_tool_mapping() { - Ok(map) => match map.get(&thermite::TITANFALL2_STEAM_ID) { - Some(_) => {} - None => { - return Err( - "Titanfall2 was not configured to use a compatibility tool" - .to_string(), - ); - } - }, - Err(_) => { - return Err("Could not get compatibility tool mapping".to_string()); - } - } - } - } - Err(_) => { - 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/{}//-profile={} --northstar/", - thermite::TITANFALL2_STEAM_ID, - game_install.profile - )) { - Ok(()) => Ok("Started game".to_string()), - Err(_err) => Err("Failed to launch Titanfall 2 via Steam".to_string()), - } -} diff --git a/src-tauri/src/northstar/profile.rs b/src-tauri/src/northstar/profile.rs deleted file mode 100644 index 26a32d6b..00000000 --- a/src-tauri/src/northstar/profile.rs +++ /dev/null @@ -1,121 +0,0 @@ -use crate::util::copy_dir_all; -use crate::GameInstall; - -// These folders are part of Titanfall 2 and -// should NEVER be used as a Profile -const SKIP_PATHS: [&str; 8] = [ - "___flightcore-temp", - "__overlay", - "bin", - "Core", - "r2", - "vpk", - "platform", - "Support", -]; - -// A profile may have one of these to be detected -const MAY_CONTAIN: [&str; 10] = [ - "mods/", - "plugins/", - "packages/", - "logs/", - "runtime/", - "save_data/", - "Northstar.dll", - "enabledmods.json", - "placeholder.playerdata.pdata", - "LEGAL.txt", -]; - -/// Returns a list of Profile names -/// All the returned Profiles can be found relative to the game path -#[tauri::command] -pub fn fetch_profiles(game_install: GameInstall) -> Result<Vec<String>, String> { - let mut profiles: Vec<String> = Vec::new(); - - for content in MAY_CONTAIN { - let pattern = format!("{}/*/{}", game_install.game_path, content); - for e in glob::glob(&pattern).expect("Failed to read glob pattern") { - let path = e.unwrap(); - let mut ancestors = path.ancestors(); - - ancestors.next(); - - let profile_path = std::path::Path::new(ancestors.next().unwrap()); - let profile_name = profile_path - .file_name() - .unwrap() - .to_os_string() - .into_string() - .unwrap(); - - if !profiles.contains(&profile_name) { - profiles.push(profile_name); - } - } - } - - Ok(profiles) -} - -/// Validates if a given profile is actually a valid profile -#[tauri::command] -pub fn validate_profile(game_install: GameInstall, profile: String) -> bool { - // Game files are never a valid profile - // Prevent users with messed up installs from making it even worse - if SKIP_PATHS.contains(&profile.as_str()) { - return false; - } - - log::info!("Validating Profile {}", profile); - - let profile_path = format!("{}/{}", game_install.game_path, profile); - let profile_dir = std::path::Path::new(profile_path.as_str()); - - profile_dir.is_dir() -} - -#[tauri::command] -pub fn delete_profile(game_install: GameInstall, profile: String) -> Result<(), String> { - // Check if the Profile actually exists - if !validate_profile(game_install.clone(), profile.clone()) { - return Err(format!("{} is not a valid Profile", profile)); - } - - log::info!("Deleting Profile {}", profile); - - let profile_path = format!("{}/{}", game_install.game_path, profile); - - match std::fs::remove_dir_all(profile_path) { - Ok(()) => Ok(()), - Err(err) => Err(format!("Failed to delete Profile: {}", err)), - } -} - -/// Clones a profile by simply duplicating the folder under a new name -#[tauri::command] -pub fn clone_profile( - game_install: GameInstall, - old_profile: String, - new_profile: String, -) -> Result<(), String> { - // Check if the old Profile already exists - if !validate_profile(game_install.clone(), old_profile.clone()) { - return Err(format!("{} is not a valid Profile", old_profile)); - } - - // Check that new Profile does not already exist - if validate_profile(game_install.clone(), new_profile.clone()) { - return Err(format!("{} already exists", new_profile)); - } - - log::info!("Cloning Profile {} to {}", old_profile, new_profile); - - let old_profile_path = format!("{}/{}", game_install.game_path, old_profile); - let new_profile_path = format!("{}/{}", game_install.game_path, new_profile); - - copy_dir_all(old_profile_path, new_profile_path).unwrap(); - - Ok(()) -} |