diff options
Diffstat (limited to 'src-tauri/src/lib.rs')
-rw-r--r-- | src-tauri/src/lib.rs | 276 |
1 files changed, 214 insertions, 62 deletions
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 12c839fe..8ec7e4b1 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,7 +1,6 @@ -use std::env; +use std::{cell::RefCell, env, fs, path::Path, time::Duration, time::Instant}; use anyhow::{anyhow, Context, Result}; -use sentry::{add_breadcrumb, Breadcrumb, Level}; mod northstar; @@ -15,11 +14,14 @@ use platform_specific::linux; use serde::{Deserialize, Serialize}; use sysinfo::SystemExt; +use tokio::time::sleep; use ts_rs::TS; use zip::ZipArchive; use northstar::get_northstar_version_number; +use crate::constants::TITANFALL2_STEAM_ID; + #[derive(Serialize, Deserialize, Debug, Clone)] pub enum InstallType { STEAM, @@ -50,10 +52,26 @@ pub struct NorthstarServer { pub player_count: i32, } +#[derive(Serialize, Deserialize, Debug, Clone, TS)] +#[ts(export)] +pub enum InstallState { + DOWNLOADING, + EXTRACTING, + DONE, +} + +#[derive(Serialize, Deserialize, Debug, Clone, TS)] +#[ts(export)] +struct InstallProgress { + current_downloaded: u64, + total_size: u64, + state: InstallState, +} + /// Check version number of a mod -pub fn check_mod_version_number(path_to_mod_folder: String) -> Result<String, anyhow::Error> { +pub fn check_mod_version_number(path_to_mod_folder: &str) -> Result<String, anyhow::Error> { // println!("{}", format!("{}/mod.json", path_to_mod_folder)); - let data = std::fs::read_to_string(format!("{}/mod.json", path_to_mod_folder))?; + 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)?; // println!("{}", parsed_json); let mod_version_number = match parsed_json.get("Version").and_then(|value| value.as_str()) { @@ -61,7 +79,7 @@ pub fn check_mod_version_number(path_to_mod_folder: String) -> Result<String, an None => return Err(anyhow!("No version number found")), }; - println!("{}", mod_version_number); + log::info!("{}", mod_version_number); Ok(mod_version_number.to_string()) } @@ -81,8 +99,7 @@ pub fn linux_checks_librs() -> Result<(), String> { return Err(format!( "GLIBC is not version {} or greater", min_required_ldd_version - ) - .to_string()); + )); }; // All checks passed @@ -94,7 +111,7 @@ pub fn find_game_install_location() -> Result<GameInstall, String> { // Attempt parsing Steam library directly match steamlocate::SteamDir::locate() { Some(mut steamdir) => { - let titanfall2_steamid = 1237970; + let titanfall2_steamid = TITANFALL2_STEAM_ID.parse().unwrap(); match steamdir.app(&titanfall2_steamid) { Some(app) => { // println!("{:#?}", app); @@ -104,10 +121,10 @@ pub fn find_game_install_location() -> Result<GameInstall, String> { }; return Ok(game_install); } - None => println!("Couldn't locate Titanfall2"), + None => log::info!("Couldn't locate Titanfall2 Steam instal"), } } - None => println!("Couldn't locate Steam on this computer!"), + None => log::info!("Couldn't locate Steam on this computer!"), } // (On Windows only) try parsing Windows registry for Origin install path @@ -115,13 +132,13 @@ pub fn find_game_install_location() -> Result<GameInstall, String> { match windows::origin_install_location_detection() { Ok(game_path) => { let game_install = GameInstall { - game_path: game_path, + game_path, install_type: InstallType::ORIGIN, }; return Ok(game_install); } Err(err) => { - println!("{}", err); + log::info!("{}", err); } }; @@ -130,13 +147,13 @@ pub fn find_game_install_location() -> Result<GameInstall, String> { /// Checks whether the provided path is a valid Titanfall2 gamepath by checking against a certain set of criteria pub fn check_is_valid_game_path(game_install_path: &str) -> Result<(), String> { - let path_to_titanfall2_exe = format!("{}/Titanfall2.exe", game_install_path); + let path_to_titanfall2_exe = format!("{game_install_path}/Titanfall2.exe"); let is_correct_game_path = std::path::Path::new(&path_to_titanfall2_exe).exists(); - println!("Titanfall2.exe exists in path? {}", is_correct_game_path); + log::info!("Titanfall2.exe exists in path? {}", is_correct_game_path); // Exit early if wrong game path if !is_correct_game_path { - return Err(format!("Incorrect game path \"{}\"", game_install_path)); // Return error cause wrong game path + return Err(format!("Incorrect game path \"{game_install_path}\"")); // Return error cause wrong game path } Ok(()) } @@ -159,12 +176,12 @@ fn extract(zip_file: std::fs::File, target: &std::path::Path) -> Result<()> { ); if (*f.name()).ends_with('/') { - println!("Create directory {}", f.name()); + log::info!("Create directory {}", f.name()); std::fs::create_dir_all(target.join(f.name())) .context("Unable to create directory")?; continue; } else if let Some(p) = out.parent() { - std::fs::create_dir_all(&p).context("Unable to create directory")?; + std::fs::create_dir_all(p).context("Unable to create directory")?; } let mut outfile = std::fs::OpenOptions::new() @@ -173,7 +190,7 @@ fn extract(zip_file: std::fs::File, target: &std::path::Path) -> Result<()> { .truncate(true) .open(&out)?; - println!("Write file {}", out.display()); + log::info!("Write file {}", out.display()); std::io::copy(&mut f, &mut outfile).context("Unable to write to file")?; } @@ -186,33 +203,83 @@ fn extract(zip_file: std::fs::File, target: &std::path::Path) -> Result<()> { ///Install N* from the provided mod /// ///Checks cache, else downloads the latest version -async fn do_install(nmod: &thermite::model::ModVersion, game_path: &std::path::Path) -> Result<()> { +async fn do_install( + window: tauri::Window, + nmod: &thermite::model::ModVersion, + game_path: &std::path::Path, +) -> Result<()> { let filename = format!("northstar-{}.zip", nmod.version); let download_directory = format!("{}/___flightcore-temp-download-dir/", game_path.display()); std::fs::create_dir_all(download_directory.clone())?; - let download_path = format!("{}/{}", download_directory.clone(), filename); - println!("{}", download_path); - - let nfile = match thermite::core::manage::download_file(&nmod.url, download_path) { + 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 nfile = match thermite::core::manage::download_file_with_progress( + &nmod.url, + download_path, + |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(); + } + } + }, + ) { Ok(res) => res, Err(err) => return Err(anyhow!("Failed downloading Northstar {}", err)), }; - println!("Extracting Northstar..."); + window + .emit( + "northstar-install-download-progress", + InstallProgress { + current_downloaded: 0, + total_size: 0, + state: InstallState::EXTRACTING, + }, + ) + .unwrap(); + + log::info!("Extracting Northstar..."); extract(nfile, game_path)?; // Delete old copy - println!("Delete temp folder again"); + log::info!("Delete temp folder again"); std::fs::remove_dir_all(download_directory).unwrap(); - println!("Done!"); + 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_path: &str, northstar_package_name: Option<String>, ) -> Result<String, String> { @@ -234,20 +301,30 @@ pub async fn install_northstar( .ok_or_else(|| panic!("Couldn't find Northstar on thunderstore???")) .unwrap(); - // Breadcrumb for sentry to debug crash - add_breadcrumb(Breadcrumb { - // category: Some("auth".into()), - message: Some(format!("Install path \"{}\"", game_path)), - level: Level::Info, - ..Default::default() - }); + log::info!("Install path \"{}\"", game_path); - do_install( + match do_install( + window, nmod.versions.get(&nmod.latest).unwrap(), std::path::Path::new(game_path), ) .await - .unwrap(); + { + 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()) } @@ -258,7 +335,7 @@ pub fn get_host_os() -> String { } pub fn launch_northstar( - game_install: GameInstall, + game_install: &GameInstall, bypass_checks: Option<bool>, ) -> Result<String, String> { dbg!(game_install.clone()); @@ -278,15 +355,12 @@ pub fn launch_northstar( )); } - let bypass_checks = match bypass_checks { - Some(bypass_checks) => bypass_checks, - None => false, - }; + let bypass_checks = bypass_checks.unwrap_or(false); // Only check guards if bypassing checks is not enabled if !bypass_checks { // Some safety checks before, should have more in the future - if get_northstar_version_number(game_install.game_path.clone()).is_err() { + if get_northstar_version_number(&game_install.game_path).is_err() { return Err(anyhow!("Not all checks were met").to_string()); } @@ -314,7 +388,7 @@ pub fn launch_northstar( { let ns_exe_path = format!("{}/NorthstarLauncher.exe", game_install.game_path); let _output = std::process::Command::new("C:\\Windows\\System32\\cmd.exe") - .args(&["/C", "start", "", &ns_exe_path]) + .args(["/C", "start", "", &ns_exe_path]) .spawn() .expect("failed to execute process"); return Ok("Launched game".to_string()); @@ -327,31 +401,109 @@ pub fn launch_northstar( )) } -pub fn check_origin_running() -> bool { - let s = sysinfo::System::new_all(); - for _process in s.processes_by_name("Origin.exe") { - // check here if this is your process - // dbg!(process); - // There's at least one Origin process, so we can launch - return true; +/// Prepare Northstar and Launch through Steam using the Browser Protocol +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()); } - // Alternatively, check for EA Desktop - for _process in s.processes_by_name("EADesktop.exe") { - // There's at least one EADesktop process, so we can launch - return true; + + 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()); + } } - false + + // Switch to Titanfall2 directory to set everything up + if std::env::set_current_dir(game_install.game_path.clone()).is_err() { + // We failed to get to Titanfall2 directory + return Err("Couldn't access Titanfall2 directory".to_string()); + } + + let run_northstar = "run_northstar.txt"; + let run_northstar_bak = "run_northstar.txt.bak"; + + if Path::new(run_northstar).exists() { + // rename should ovewrite existing files + fs::rename(run_northstar, run_northstar_bak).unwrap(); + } + + // Passing arguments gives users a prompt, so we use run_northstar.txt + fs::write(run_northstar, b"1").unwrap(); + + let retval = match open::that(format!("steam://run/{}/", TITANFALL2_STEAM_ID)) { + Ok(()) => Ok("Started game".to_string()), + Err(_err) => Err("Failed to launch Titanfall 2 via Steam".to_string()), + }; + + let is_err = retval.is_err(); + + // Handle the rest in the backround + tauri::async_runtime::spawn(async move { + // Starting the EA app and Titanfall might take a good minute or three + let mut wait_countdown = 60 * 3; + while wait_countdown > 0 && !check_northstar_running() && !is_err { + sleep(Duration::from_millis(1000)).await; + wait_countdown -= 1; + } + + // Northstar may be running, but it may not have loaded the file yet + sleep(Duration::from_millis(2000)).await; + + // intentionally ignore Result + let _ = fs::remove_file(run_northstar); + + if Path::new(run_northstar_bak).exists() { + fs::rename(run_northstar_bak, run_northstar).unwrap(); + } + }); + + retval +} + +pub fn check_origin_running() -> bool { + let s = sysinfo::System::new_all(); + let x = s.processes_by_name("Origin.exe").next().is_some() + || s.processes_by_name("EADesktop.exe").next().is_some(); + x } /// Checks if Northstar process is running pub fn check_northstar_running() -> bool { let s = sysinfo::System::new_all(); - for _process in s.processes_by_name("NorthstarLauncher.exe") { - // check here if this is your process - // dbg!(process); - return true; - } - false + let x = s + .processes_by_name("NorthstarLauncher.exe") + .next() + .is_some() + || s.processes_by_name("Titanfall2.exe").next().is_some(); + x } /// Helps with converting release candidate numbers which are different on Thunderstore @@ -364,7 +516,7 @@ pub fn convert_release_candidate_number(version_number: String) -> String { } /// Returns a serde json object of the parsed `enabledmods.json` file -pub fn get_enabled_mods(game_install: GameInstall) -> Result<serde_json::value::Value, String> { +pub fn get_enabled_mods(game_install: &GameInstall) -> Result<serde_json::value::Value, String> { let enabledmods_json_path = format!("{}/R2Northstar/enabledmods.json", game_install.game_path); // Check for JSON file @@ -381,7 +533,7 @@ pub fn get_enabled_mods(game_install: GameInstall) -> Result<serde_json::value:: // Parse JSON let res: serde_json::Value = match serde_json::from_str(&data) { Ok(result) => result, - Err(err) => return Err(format!("Failed to read JSON due to: {}", err.to_string())), + Err(err) => return Err(format!("Failed to read JSON due to: {}", err)), }; // Return parsed data |