use std::env; use anyhow::{anyhow, Context, Result}; mod northstar; pub mod constants; mod platform_specific; #[cfg(target_os = "windows")] use platform_specific::windows; #[cfg(target_os = "linux")] use platform_specific::linux; use serde::{Deserialize, Serialize}; use sysinfo::SystemExt; use ts_rs::TS; use zip::ZipArchive; use northstar::get_northstar_version_number; #[derive(Serialize, Deserialize, Debug, Clone)] pub enum InstallType { STEAM, ORIGIN, EAPLAY, UNKNOWN, } #[derive(Serialize, Deserialize, Debug, Clone)] pub struct GameInstall { pub game_path: String, pub install_type: InstallType, } #[derive(Serialize, Deserialize, Debug, Clone, TS)] #[ts(export)] pub struct NorthstarMod { pub name: String, pub version: Option, pub thunderstore_mod_string: Option, pub enabled: bool, pub directory: String, } #[derive(Serialize, Deserialize, Debug, Clone)] pub struct NorthstarServer { #[serde(rename = "playerCount")] pub player_count: i32, } /// Check version number of a mod pub fn check_mod_version_number(path_to_mod_folder: String) -> Result { // println!("{}", 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()) { Some(version_number) => version_number, None => return Err(anyhow!("No version number found")), }; log::info!("{}", mod_version_number); Ok(mod_version_number.to_string()) } // I intend to add more linux related stuff to check here, so making a func // for now tho it only checks `ldd --version` // - salmon #[cfg(target_os = "linux")] pub fn linux_checks_librs() -> Result<(), String> { // Perform various checks in terms of Linux compatibility // Return early with error message if a check fails // check `ldd --version` to see if glibc is up to date for northstar proton let min_required_ldd_version = 2.33; let lddv = linux::check_glibc_v(); if lddv < min_required_ldd_version { return Err(format!( "GLIBC is not version {} or greater", min_required_ldd_version )); }; // All checks passed Ok(()) } /// Attempts to find the game install location pub fn find_game_install_location() -> Result { // Attempt parsing Steam library directly match steamlocate::SteamDir::locate() { Some(mut steamdir) => { let titanfall2_steamid = 1237970; match steamdir.app(&titanfall2_steamid) { Some(app) => { // println!("{:#?}", app); let game_install = GameInstall { game_path: app.path.to_str().unwrap().to_string(), install_type: InstallType::STEAM, }; return Ok(game_install); } None => println!("Couldn't locate Titanfall2"), } } None => println!("Couldn't locate Steam on this computer!"), } // (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: game_path, install_type: InstallType::ORIGIN, }; return Ok(game_install); } Err(err) => { println!("{}", err); } }; Err("Could not auto-detect game install location! Please enter it manually.".to_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!("{game_install_path}/Titanfall2.exe"); let is_correct_game_path = std::path::Path::new(&path_to_titanfall2_exe).exists(); 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 } 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<()> { 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('/') { println!("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)?; println!("Write file {}", out.display()); std::io::copy(&mut f, &mut outfile).context("Unable to write to file")?; } } Ok(()) } /// Copied from `papa` source code and modified ///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<()> { 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 = thermite::core::manage::download_file(&nmod.url, download_path).unwrap(); println!("Extracting Northstar..."); extract(nfile, game_path)?; // Delete old copy println!("Delete temp folder again"); std::fs::remove_dir_all(download_directory).unwrap(); println!("Done!"); Ok(()) } pub async fn install_northstar( game_path: &str, northstar_package_name: Option, ) -> Result { 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 = thermite::api::get_package_index().unwrap().to_vec(); 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(); log::info!("Install path \"{}\"", game_path); match do_install( nmod.versions.get(&nmod.latest).unwrap(), std::path::Path::new(game_path), ) .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()) } /// Returns identifier of host OS FlightCore is running on pub fn get_host_os() -> String { env::consts::OS.to_string() } pub fn launch_northstar( game_install: GameInstall, bypass_checks: Option, ) -> Result { dbg!(game_install.clone()); 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 )); } let bypass_checks = match bypass_checks { Some(bypass_checks) => bypass_checks, None => 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() { return Err(anyhow!("Not all checks were met").to_string()); } // Require Origin to be running to launch Northstar let origin_is_running = check_origin_running(); if !origin_is_running { return Err( anyhow!("Origin not running, start Origin 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 _output = std::process::Command::new("C:\\Windows\\System32\\cmd.exe") .args(["/C", "start", "", &ns_exe_path]) .spawn() .expect("failed to execute process"); return Ok("Launched game".to_string()); } Err(format!( "Not yet implemented for {:?} on {}", game_install.install_type, get_host_os() )) } pub fn check_origin_running() -> bool { let s = sysinfo::System::new_all(); s.processes_by_name("Origin.exe").next().is_some() || s.processes_by_name("EADesktop.exe").next().is_some() } /// Checks if Northstar process is running pub fn check_northstar_running() -> bool { sysinfo::System::new_all() .processes_by_name("NorthstarLauncher.exe") .next() .is_some() } /// 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", "") } /// Returns a serde json object of the parsed `enabledmods.json` file pub fn get_enabled_mods(game_install: GameInstall) -> Result { let enabledmods_json_path = format!("{}/R2Northstar/enabledmods.json", game_install.game_path); // Check for JSON file if !std::path::Path::new(&enabledmods_json_path).exists() { return Err("enabledmods.json not found".to_string()); } // Read file let data = match std::fs::read_to_string(enabledmods_json_path) { Ok(data) => data, Err(err) => return Err(err.to_string()), }; // 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)), }; // Return parsed data Ok(res) }