//! This module contains various utility/helper functions that do not fit into any other module use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; use sysinfo::{ProcessExt, SystemExt}; use zip::ZipArchive; use crate::constants::{APP_USER_AGENT, MASTER_SERVER_URL, SERVER_BROWSER_ENDPOINT}; #[derive(Serialize, Deserialize, Debug, Clone)] pub struct NorthstarServer { #[serde(rename = "playerCount")] pub player_count: i32, } /// This function's only use is to force a `panic!()` // This must NOT be async to ensure crashing whole application. #[tauri::command] pub fn force_panic() { panic!("Force panicked!"); } /// Returns true if built in debug mode #[tauri::command] pub async fn is_debug_mode() -> bool { cfg!(debug_assertions) } /// Returns the current version number as a string #[tauri::command] pub async fn get_flightcore_version_number() -> String { let version = env!("CARGO_PKG_VERSION"); if cfg!(debug_assertions) { // Debugging enabled format!("v{} (debug mode)", version) } else { // Debugging disabled format!("v{}", version) } } /// Spawns repair window #[tauri::command] pub async fn open_repair_window(handle: tauri::AppHandle) -> Result<(), String> { // Spawn new window let repair_window = match tauri::WindowBuilder::new( &handle, "RepairWindow", tauri::WindowUrl::App("/#/repair".into()), ) .build() { Ok(res) => res, Err(err) => return Err(err.to_string()), }; // Set window title match repair_window.set_title("FlightCore Repair Window") { Ok(()) => (), Err(err) => return Err(err.to_string()), }; Ok(()) } /// Closes all windows and exits application #[tauri::command] pub async fn close_application(app: tauri::AppHandle) -> Result<(), String> { app.exit(0); // Close application Ok(()) } /// Fetches `/client/servers` endpoint from master server async fn fetch_server_list() -> Result { let url = format!("{MASTER_SERVER_URL}{SERVER_BROWSER_ENDPOINT}"); let client = reqwest::Client::new(); let res = client .get(url) .header(reqwest::header::USER_AGENT, APP_USER_AGENT) .send() .await? .text() .await?; Ok(res) } /// Gets server and playercount from master server API #[tauri::command] pub async fn get_server_player_count() -> Result<(i32, usize), String> { let res = match fetch_server_list().await { Ok(res) => res, Err(err) => return Err(err.to_string()), }; let ns_servers: Vec = serde_json::from_str(&res).expect("JSON was not well-formatted"); // Get server count let server_count = ns_servers.len(); // Sum up player count let total_player_count: i32 = ns_servers.iter().map(|server| server.player_count).sum(); log::info!("total_player_count: {}", total_player_count); log::info!("server_count: {}", server_count); Ok((total_player_count, server_count)) } #[tauri::command] pub async fn kill_northstar() -> Result<(), String> { if !check_northstar_running() { return Err("Northstar is not running".to_string()); } let s = sysinfo::System::new_all(); for process in s.processes_by_exact_name("Titanfall2.exe") { log::info!("Killing Process {}", process.pid()); process.kill(); } for process in s.processes_by_exact_name("NorthstarLauncher.exe") { log::info!("Killing Process {}", process.pid()); process.kill(); } Ok(()) } /// Copied from `papa` source code and modified ///Extract N* zip file to target game path // fn extract(ctx: &Ctx, zip_file: File, target: &Path) -> Result<()> { pub fn extract(zip_file: std::fs::File, target: &std::path::Path) -> Result<()> { let mut archive = ZipArchive::new(&zip_file).context("Unable to open zip archive")?; for i in 0..archive.len() { let mut f = archive.by_index(i).unwrap(); //This should work fine for N* because the dir structure *should* always be the same if f.enclosed_name().unwrap().starts_with("Northstar") { let out = target.join( f.enclosed_name() .unwrap() .strip_prefix("Northstar") .unwrap(), ); if (*f.name()).ends_with('/') { 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")?; } let mut outfile = std::fs::OpenOptions::new() .create(true) .write(true) .truncate(true) .open(&out)?; log::info!("Write file {}", out.display()); std::io::copy(&mut f, &mut outfile).context("Unable to write to file")?; } } Ok(()) } pub fn check_ea_app_or_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(); let x = s .processes_by_name("NorthstarLauncher.exe") .next() .is_some() || s.processes_by_name("Titanfall2.exe").next().is_some(); x } /// Copies a folder and all its contents to a new location #[allow(dead_code)] pub fn copy_dir_all( src: impl AsRef, dst: impl AsRef, ) -> std::io::Result<()> { std::fs::create_dir_all(&dst)?; for entry in std::fs::read_dir(src)? { let entry = entry?; let ty = entry.file_type()?; if ty.is_dir() { copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?; } else { std::fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?; } } Ok(()) } /// Moves a folders file structure to a new location /// Old folders are not removed pub fn move_dir_all( src: impl AsRef, dst: impl AsRef, ) -> std::io::Result<()> { std::fs::create_dir_all(&dst)?; for entry in std::fs::read_dir(src)? { let entry = entry?; let ty = entry.file_type()?; if ty.is_dir() { move_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?; std::fs::remove_dir(entry.path())?; } else { std::fs::rename(entry.path(), dst.as_ref().join(entry.file_name()))?; } } Ok(()) } /// Helps with converting release candidate numbers which are different on Thunderstore /// due to restrictions imposed by the platform pub fn convert_release_candidate_number(version_number: String) -> String { // This simply converts `-rc` to `0` // Works as intended for RCs < 10, e.g. `v1.9.2-rc1` -> `v1.9.201` // Doesn't work for larger numbers, e.g. `v1.9.2-rc11` -> `v1.9.2011` (should be `v1.9.211`) version_number.replace("-rc", "0").replace(".00", ".") } #[cfg(test)] mod tests { use super::*; #[test] fn test_basic_release_candidate_number_conversion() { let input = "v1.2.3-rc4".to_string(); let output = convert_release_candidate_number(input); let expected_output = "v1.2.304"; assert_eq!(output, expected_output); } #[test] fn test_leading_zero_release_candidate_number_conversion() { let input = "v1.2.0-rc3".to_string(); let output = convert_release_candidate_number(input); let expected_output = "v1.2.3"; assert_eq!(output, expected_output); } #[test] fn test_double_patch_digit_release_candidate_number_conversion() { // let input = "v1.2.34-rc5".to_string(); // let output = convert_release_candidate_number(input); // let expected_output = "v1.2.3405"; let input = "v1.19.10-rc1".to_string(); let output = convert_release_candidate_number(input); let expected_output = "v1.19.1001"; assert_eq!(output, expected_output); } }