diff options
Diffstat (limited to 'src-tauri/src')
-rw-r--r-- | src-tauri/src/constants.rs | 60 | ||||
-rw-r--r-- | src-tauri/src/development/mod.rs | 84 | ||||
-rw-r--r-- | src-tauri/src/github/mod.rs | 312 | ||||
-rw-r--r-- | src-tauri/src/github/pull_requests.rs | 398 | ||||
-rw-r--r-- | src-tauri/src/github/release_notes.rs | 244 | ||||
-rw-r--r-- | src-tauri/src/lib.rs | 14 | ||||
-rw-r--r-- | src-tauri/src/main.rs | 221 | ||||
-rw-r--r-- | src-tauri/src/mod_management/legacy.rs | 213 | ||||
-rw-r--r-- | src-tauri/src/mod_management/mod.rs | 797 | ||||
-rw-r--r-- | src-tauri/src/mod_management/plugins.rs | 26 | ||||
-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 | ||||
-rw-r--r-- | src-tauri/src/platform_specific/linux.rs | 98 | ||||
-rw-r--r-- | src-tauri/src/platform_specific/mod.rs | 50 | ||||
-rw-r--r-- | src-tauri/src/platform_specific/windows.rs | 104 | ||||
-rw-r--r-- | src-tauri/src/repair_and_verify/mod.rs | 137 | ||||
-rw-r--r-- | src-tauri/src/thunderstore/mod.rs | 86 | ||||
-rw-r--r-- | src-tauri/src/util.rs | 324 |
19 files changed, 17 insertions, 3906 deletions
diff --git a/src-tauri/src/constants.rs b/src-tauri/src/constants.rs deleted file mode 100644 index 3ad2d6e8..00000000 --- a/src-tauri/src/constants.rs +++ /dev/null @@ -1,60 +0,0 @@ -// This file stores various global constants values -use const_format::concatcp; -use std::time::Duration; - -/// FlightCore user agent for web requests -pub const APP_USER_AGENT: &str = concatcp!("FlightCore/", env!("CARGO_PKG_VERSION")); - -/// URL of the Northstar masterserver -pub const MASTER_SERVER_URL: &str = "https://northstar.tf"; - -/// server list endpoint -pub const SERVER_BROWSER_ENDPOINT: &str = "/client/servers"; - -/// List of core Northstar mods -pub const CORE_MODS: [&str; 3] = [ - "Northstar.Client", - "Northstar.Custom", - "Northstar.CustomServers", -]; - -/// List of Thunderstoremods that shouldn't be installable -/// as they behave different than common Squirrel mods -pub const BLACKLISTED_MODS: [&str; 3] = [ - "northstar-Northstar", - "northstar-NorthstarReleaseCandidate", - "ebkr-r2modman", -]; - -/// List of Thunderstoremods that have some specific install requirements that makes them different from standard mods -pub const MODS_WITH_SPECIAL_REQUIREMENTS: [&str; 1] = ["NanohmProtogen-VanillaPlus"]; - -/// Order in which the sections for release notes should be displayed -pub const SECTION_ORDER: [&str; 11] = [ - "feat", "fix", "docs", "style", "refactor", "build", "test", "i18n", "ci", "chore", "other", -]; - -/// Statistics (players and servers counts) refresh delay -pub const REFRESH_DELAY: Duration = Duration::from_secs(5 * 60); - -/// Flightcore repo name and org name on GitHub -pub const FLIGHTCORE_REPO_NAME: &str = "R2NorthstarTools/FlightCore"; - -/// Northstar release repo name and org name on GitHub -pub const NORTHSTAR_RELEASE_REPO_NAME: &str = "R2Northstar/Northstar"; - -/// NorthstarLauncher repo name on GitHub -pub const NORTHSTAR_LAUNCHER_REPO_NAME: &str = "NorthstarLauncher"; - -/// NorthstarMods repo name on GitHub -pub const NORTHSTAR_MODS_REPO_NAME: &str = "NorthstarMods"; - -/// URL to launcher commits API URL -pub const NS_LAUNCHER_COMMITS_API_URL: &str = - "https://api.github.com/repos/R2Northstar/NorthstarLauncher/commits"; - -/// Filename of DLL that Northstar uses -pub const NORTHSTAR_DLL: &str = "Northstar.dll"; - -/// Profile that Northstar defaults to and ships with -pub const NORTHSTAR_DEFAULT_PROFILE: &str = "R2Northstar"; diff --git a/src-tauri/src/development/mod.rs b/src-tauri/src/development/mod.rs deleted file mode 100644 index 7184904c..00000000 --- a/src-tauri/src/development/mod.rs +++ /dev/null @@ -1,84 +0,0 @@ -use crate::constants::NS_LAUNCHER_COMMITS_API_URL; -use crate::github::{ - pull_requests::{check_github_api, download_zip_into_memory, get_launcher_download_link}, - CommitInfo, -}; - -#[tauri::command] -pub async fn install_git_main(game_install_path: &str) -> Result<String, String> { - // Get list of commits - let commits: Vec<CommitInfo> = serde_json::from_value( - check_github_api(NS_LAUNCHER_COMMITS_API_URL) - .await - .expect("Failed request"), - ) - .unwrap(); - - // Get latest commit... - let latest_commit_sha = commits[0].sha.clone(); - // ...and according artifact download URL - let download_url = get_launcher_download_link(latest_commit_sha.clone()).await?; - - let archive = match download_zip_into_memory(download_url).await { - Ok(archive) => archive, - Err(err) => return Err(err.to_string()), - }; - - let extract_directory = format!( - "{}/___flightcore-temp/download-dir/launcher-pr-{}", - game_install_path, latest_commit_sha - ); - match std::fs::create_dir_all(extract_directory.clone()) { - Ok(_) => (), - Err(err) => { - return Err(format!( - "Failed creating temporary download directory: {}", - err - )) - } - }; - - let target_dir = std::path::PathBuf::from(extract_directory.clone()); // Doesn't need to exist - match zip_extract::extract(std::io::Cursor::new(archive), &target_dir, true) { - Ok(()) => (), - Err(err) => { - return Err(format!("Failed unzip: {}", err)); - } - }; - - // Copy only necessary files from temp dir - // Copy: - // - NorthstarLauncher.exe - // - Northstar.dll - let files_to_copy = vec!["NorthstarLauncher.exe", "Northstar.dll"]; - for file_name in files_to_copy { - let source_file_path = format!("{}/{}", extract_directory, file_name); - let destination_file_path = format!("{}/{}", game_install_path, file_name); - match std::fs::copy(source_file_path, destination_file_path) { - Ok(_result) => (), - Err(err) => { - return Err(format!( - "Failed to copy necessary file {} from temp dir: {}", - file_name, err - )) - } - }; - } - - // delete extract directory - match std::fs::remove_dir_all(&extract_directory) { - Ok(()) => (), - Err(err) => { - return Err(format!( - "Failed to delete temporary download directory: {}", - err - )) - } - } - - log::info!( - "All done with installing launcher from {}", - latest_commit_sha - ); - Ok(latest_commit_sha) -} diff --git a/src-tauri/src/github/mod.rs b/src-tauri/src/github/mod.rs deleted file mode 100644 index 9bc3f834..00000000 --- a/src-tauri/src/github/mod.rs +++ /dev/null @@ -1,312 +0,0 @@ -pub mod pull_requests; -pub mod release_notes; - -use crate::constants::{ - APP_USER_AGENT, FLIGHTCORE_REPO_NAME, NORTHSTAR_RELEASE_REPO_NAME, SECTION_ORDER, -}; -use regex::Regex; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use ts_rs::TS; - -#[derive(Serialize, Deserialize, Debug, Clone, TS)] -#[ts(export)] -pub struct Tag { - name: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] -#[ts(export)] -pub enum Project { - FlightCore, - Northstar, -} - -/// Wrapper type needed for frontend -#[derive(Serialize, Deserialize, Debug, Clone, TS)] -#[ts(export)] -pub struct TagWrapper { - label: String, - value: Tag, -} - -#[derive(Debug, Deserialize)] -pub struct CommitInfo { - pub sha: String, - commit: Commit, - author: Option<CommitAuthor>, -} - -#[derive(Debug, Deserialize)] -struct Commit { - message: String, -} - -#[derive(Debug, Deserialize)] -struct CommitAuthor { - login: String, -} - -#[derive(Debug, Deserialize)] -struct Comparison { - commits: Vec<CommitInfo>, -} - -/// Get a list of tags on the FlightCore repo -#[tauri::command] -pub fn get_list_of_tags(project: Project) -> Result<Vec<TagWrapper>, String> { - // Set the repository name. - - // Create a `reqwest` client with a user agent. - let client = reqwest::blocking::Client::builder() - .user_agent(APP_USER_AGENT) - .build() - .unwrap(); - - // Switch repo to fetch from based on project - let repo_name = match project { - Project::FlightCore => FLIGHTCORE_REPO_NAME, - Project::Northstar => NORTHSTAR_RELEASE_REPO_NAME, - }; - - // Fetch the list of tags for the repository as a `Vec<Tag>`. - let tags_url = format!("https://api.github.com/repos/{}/tags", repo_name); - let tags: Vec<Tag> = client.get(tags_url).send().unwrap().json().unwrap(); - - // Map each `Tag` element to a `TagWrapper` element with the desired label and `Tag` value. - let tag_wrappers: Vec<TagWrapper> = tags - .into_iter() - .map(|tag| TagWrapper { - label: tag.name.clone(), - value: tag, - }) - .collect(); - - Ok(tag_wrappers) -} - -/// Use GitHub API to compare two tags of the same repo against each other and get the resulting changes -#[tauri::command] -pub fn compare_tags(project: Project, first_tag: Tag, second_tag: Tag) -> Result<String, String> { - match project { - Project::FlightCore => compare_tags_flightcore(first_tag, second_tag), - Project::Northstar => compare_tags_northstar(first_tag, second_tag), - } -} - -pub fn compare_tags_flightcore(first_tag: Tag, second_tag: Tag) -> Result<String, String> { - // Fetch the list of commits between the two tags. - - // Create a `reqwest` client with a user agent. - let client = reqwest::blocking::Client::builder() - .user_agent(APP_USER_AGENT) - .build() - .unwrap(); - - let repo = "R2NorthstarTools/FlightCore"; - - let mut full_patch_notes = "".to_string(); - - let mut patch_notes: Vec<String> = [].to_vec(); - println!("{}", repo); - // let repo = "R2Northstar/NorthstarLauncher"; - let comparison_url = format!( - "https://api.github.com/repos/{}/compare/{}...{}", - repo, first_tag.name, second_tag.name - ); - - let comparison: Comparison = client.get(comparison_url).send().unwrap().json().unwrap(); - let commits = comparison.commits; - - // Display the list of commits. - println!( - "Commits between {} and {}:", - first_tag.name, second_tag.name - ); - - // Iterate over all commits in the diff - for commit in commits { - println!( - " * {} : {}", - commit.sha, - commit.commit.message.split('\n').next().unwrap() - ); - patch_notes.push( - commit - .commit - .message - .split('\n') - .next() - .unwrap() - .to_string(), - ); - } - - full_patch_notes += &generate_flightcore_release_notes(patch_notes); - - Ok(full_patch_notes.to_string()) -} - -/// Generate release notes in the format used for FlightCore -fn generate_flightcore_release_notes(commits: Vec<String>) -> String { - let grouped_commits = group_commits_by_type(commits); - let mut release_notes = String::new(); - - // Go over commit types and generate notes - for commit_type in SECTION_ORDER { - if let Some(commit_list) = grouped_commits.get(commit_type) { - if !commit_list.is_empty() { - let section_title = match commit_type { - "feat" => "**Features:**", - "fix" => "**Bug Fixes:**", - "docs" => "**Documentation:**", - "style" => "**Code style changes:**", - "refactor" => "**Code Refactoring:**", - "build" => "**Build:**", - "ci" => "**Continuous integration changes:**", - "test" => "**Tests:**", - "chore" => "**Chores:**", - "i18n" => "**Translations:**", - _ => "**Other:**", - }; - - release_notes.push_str(&format!("{}\n", section_title)); - - for commit_message in commit_list { - release_notes.push_str(&format!("- {}\n", commit_message)); - } - - release_notes.push('\n'); - } - } - } - - let release_notes = release_notes.trim_end_matches('\n').to_string(); - release_notes -} - -/// Group semantic commit messages by type -/// Commmit messages that are not formatted accordingly are marked as "other" -fn group_commits_by_type(commits: Vec<String>) -> HashMap<String, Vec<String>> { - let mut grouped_commits: HashMap<String, Vec<String>> = HashMap::new(); - let mut other_commits: Vec<String> = vec![]; - - for commit in commits { - let commit_parts: Vec<&str> = commit.splitn(2, ':').collect(); - if commit_parts.len() == 2 { - let commit_type = commit_parts[0].to_lowercase(); - let commit_description = commit_parts[1].trim().to_string(); - - // Check if known commit type - if SECTION_ORDER.contains(&commit_type.as_str()) { - let commit_list = grouped_commits.entry(commit_type.to_string()).or_default(); - commit_list.push(commit_description); - } else { - // otherwise add to list of "other" - other_commits.push(commit.to_string()); - } - } else { - other_commits.push(commit.to_string()); - } - } - grouped_commits.insert("other".to_string(), other_commits); - - grouped_commits -} - -/// Compares two tags on Northstar repo and generates release notes over the diff in tags -/// over the 3 major repos (Northstar, NorthstarLauncher, NorthstarMods) -pub fn compare_tags_northstar(first_tag: Tag, second_tag: Tag) -> Result<String, String> { - // Fetch the list of commits between the two tags. - - // Create a `reqwest` client with a user agent. - let client = reqwest::blocking::Client::builder() - .user_agent(APP_USER_AGENT) - .build() - .unwrap(); - - let repos = [ - "R2Northstar/Northstar", - "R2Northstar/NorthstarLauncher", - "R2Northstar/NorthstarMods", - ]; - - let mut full_patch_notes = "".to_string(); - let mut authors_set = std::collections::HashSet::new(); - - for repo in repos { - full_patch_notes += &format!("{}\n\n", repo); - - let mut patch_notes: Vec<String> = [].to_vec(); - println!("{}", repo); - // let repo = "R2Northstar/NorthstarLauncher"; - let comparison_url = format!( - "https://api.github.com/repos/{}/compare/{}...{}", - repo, first_tag.name, second_tag.name - ); - - log::info!("Compare URL: {}", comparison_url.clone()); - let comparison: Comparison = client.get(&comparison_url).send().unwrap().json().unwrap(); - let commits = comparison.commits; - - // Display the list of commits. - println!( - "Commits between {} and {}:", - first_tag.name, second_tag.name - ); - - // - for commit in commits { - println!( - " * {} : {}", - commit.sha, - turn_pr_number_into_link(commit.commit.message.split('\n').next().unwrap(), repo) - ); - patch_notes.push(turn_pr_number_into_link( - commit.commit.message.split('\n').next().unwrap(), - repo, - )); - - // Store authors in set - if commit.author.is_some() { - authors_set.insert(commit.author.unwrap().login); - } - } - - full_patch_notes += &patch_notes.join("\n"); - full_patch_notes += "\n\n\n"; - } - - // Convert the set to a sorted vector. - let mut sorted_vec: Vec<String> = authors_set.into_iter().collect(); - sorted_vec.sort_by_key(|a| a.to_lowercase()); - - // Define a string to prepend to each element. - let prefix = "@"; - - // Create a new list with the prefix prepended to each element. - let prefixed_list: Vec<String> = sorted_vec.iter().map(|s| prefix.to_owned() + s).collect(); - - full_patch_notes += "**Contributors:**\n"; - full_patch_notes += &prefixed_list.join(" "); - - Ok(full_patch_notes.to_string()) -} - -/// Takes the commit title and repo slug and formats it as -/// `[commit title(SHORTENED_REPO#NUMBER)](LINK)` -fn turn_pr_number_into_link(input: &str, repo: &str) -> String { - // Extract `Mods/Launcher` from repo title - let last_line = repo - .split('/') - .next_back() - .unwrap() - .trim_start_matches("Northstar"); - // Extract PR number - let re = Regex::new(r"#(\d+)").unwrap(); - - // Generate pull request link - let pull_link = format!("https://github.com/{}/pull/", repo); - re.replace_all(input, format!("[{}#$1]({}$1)", last_line, pull_link)) - .to_string() -} diff --git a/src-tauri/src/github/pull_requests.rs b/src-tauri/src/github/pull_requests.rs deleted file mode 100644 index de733feb..00000000 --- a/src-tauri/src/github/pull_requests.rs +++ /dev/null @@ -1,398 +0,0 @@ -use crate::constants::{APP_USER_AGENT, NORTHSTAR_LAUNCHER_REPO_NAME, NORTHSTAR_MODS_REPO_NAME}; -use crate::repair_and_verify::check_is_valid_game_path; -use crate::GameInstall; -use anyhow::anyhow; -use serde::{Deserialize, Serialize}; -use std::fs::File; -use std::io; -use std::io::prelude::*; -use std::path::Path; -use ts_rs::TS; - -#[derive(Serialize, Deserialize, Debug, Clone, TS)] -#[ts(export)] -struct Repo { - full_name: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, TS)] -#[ts(export)] -struct CommitHead { - sha: String, - #[serde(rename = "ref")] - gh_ref: String, - repo: Repo, -} - -#[derive(Serialize, Deserialize, Debug, Clone, TS)] -#[ts(export)] -pub struct PullsApiResponseElement { - number: u64, - title: String, - url: String, - head: CommitHead, - html_url: String, - labels: Vec<String>, -} - -// GitHub API response JSON elements as structs -#[derive(Debug, Deserialize, Clone)] -struct WorkflowRun { - id: u64, - head_sha: String, -} -#[derive(Debug, Deserialize, Clone)] -struct ActionsRunsResponse { - workflow_runs: Vec<WorkflowRun>, -} - -#[derive(Debug, Deserialize, Clone)] -struct Artifact { - id: u64, - name: String, - workflow_run: WorkflowRun, -} - -#[derive(Debug, Deserialize, Clone)] -struct ArtifactsResponse { - artifacts: Vec<Artifact>, -} - -#[derive(Serialize, Deserialize, Debug, Clone, TS)] -#[ts(export)] -pub enum PullRequestType { - Mods, - Launcher, -} - -/// Parse pull requests from specified URL -pub async fn get_pull_requests( - repo: PullRequestType, -) -> Result<Vec<PullsApiResponseElement>, anyhow::Error> { - let repo = match repo { - PullRequestType::Mods => NORTHSTAR_MODS_REPO_NAME, - PullRequestType::Launcher => NORTHSTAR_LAUNCHER_REPO_NAME, - }; - - // Grab list of PRs - let octocrab = octocrab::instance(); - let page = octocrab - .pulls("R2Northstar", repo) - .list() - .state(octocrab::params::State::Open) - .per_page(50) // Only grab 50 PRs - .page(1u32) - .send() - .await?; - - // Iterate over pull request elements and insert into struct - let mut all_pull_requests: Vec<PullsApiResponseElement> = vec![]; - for item in page.items { - let repo = Repo { - full_name: item - .head - .repo - .ok_or(anyhow!("repo not found"))? - .full_name - .ok_or(anyhow!("full_name not found"))?, - }; - - let head = CommitHead { - sha: item.head.sha, - gh_ref: item.head.ref_field, - repo, - }; - - // Get labels and their names and put the into vector - let label_names: Vec<String> = item - .labels - .unwrap_or_else(Vec::new) - .into_iter() - .map(|label| label.name) - .collect(); - - // TODO there's probably a way to automatically serialize into the struct but I don't know yet how to - let elem = PullsApiResponseElement { - number: item.number, - title: item.title.ok_or(anyhow!("title not found"))?, - url: item.url, - head, - html_url: item - .html_url - .ok_or(anyhow!("html_url not found"))? - .to_string(), - labels: label_names, - }; - - all_pull_requests.push(elem); - } - - Ok(all_pull_requests) -} - -/// Gets either launcher or mods PRs -#[tauri::command] -pub async fn get_pull_requests_wrapper( - install_type: PullRequestType, -) -> Result<Vec<PullsApiResponseElement>, String> { - match get_pull_requests(install_type).await { - Ok(res) => Ok(res), - Err(err) => Err(err.to_string()), - } -} - -pub async fn check_github_api(url: &str) -> Result<serde_json::Value, Box<dyn std::error::Error>> { - let client = reqwest::Client::new(); - let res = client - .get(url) - .header(reqwest::header::USER_AGENT, APP_USER_AGENT) - .send() - .await - .unwrap() - .text() - .await - .unwrap(); - - let json: serde_json::Value = serde_json::from_str(&res).expect("JSON was not well-formatted"); - - Ok(json) -} - -/// Downloads a file from given URL into an array in memory -pub async fn download_zip_into_memory(download_url: String) -> Result<Vec<u8>, anyhow::Error> { - let client = reqwest::Client::builder() - .user_agent(APP_USER_AGENT) - .build()?; - - let response = client.get(download_url).send().await?; - - if !response.status().is_success() { - return Err(anyhow!("Request unsuccessful: {}", response.status())); - } - - let bytes = response.bytes().await?; - Ok(bytes.to_vec()) -} - -/// Gets GitHub download link of a mods PR -fn get_mods_download_link(pull_request: PullsApiResponseElement) -> Result<String, anyhow::Error> { - // {pr object} -> number == pr_number - // -> head -> ref - // -> repo -> full_name - - // Use repo and branch name to get download link - let download_url = format!( - "https://github.com/{}/archive/refs/heads/{}.zip", - pull_request.head.repo.full_name, // repo name - pull_request.head.gh_ref, // branch name - ); - - Ok(download_url) -} - -/// Gets `nightly.link` artifact download link of a launcher commit -#[tauri::command] -pub async fn get_launcher_download_link(commit_sha: String) -> Result<String, String> { - // Iterate over the first 10 pages of - for i in 1..=10 { - // Crossreference with runs API - let runs_response: ActionsRunsResponse = match check_github_api(&format!( - "https://api.github.com/repos/R2Northstar/NorthstarLauncher/actions/runs?page={}", - i - )) - .await - { - Ok(result) => serde_json::from_value(result).unwrap(), - Err(err) => return Err(format!("{}", err)), - }; - - // Cross-reference commit sha against workflow runs - for workflow_run in &runs_response.workflow_runs { - // If head commit sha of CI run matches the one passed to this function, grab CI output - if workflow_run.head_sha == commit_sha { - // Check artifacts - let api_url = format!("https://api.github.com/repos/R2Northstar/NorthstarLauncher/actions/runs/{}/artifacts", workflow_run.id); - let artifacts_response: ArtifactsResponse = serde_json::from_value( - check_github_api(&api_url).await.expect("Failed request"), - ) - .unwrap(); - - let multiple_artifacts = artifacts_response.artifacts.len() > 1; - - // Iterate over artifacts - for artifact in artifacts_response.artifacts { - if multiple_artifacts && !artifact.name.starts_with("NorthstarLauncher-MSVC") { - continue; - } - - // Make sure artifact and CI run commit head sha match - if artifact.workflow_run.head_sha == workflow_run.head_sha { - // Download artifact - return Ok(format!("https://nightly.link/R2Northstar/NorthstarLauncher/actions/artifacts/{}.zip", artifact.id)); - } - } - } - } - } - - Err(format!( - "Couldn't grab download link for \"{}\". Corresponding PR might be too old and therefore no CI build has been detected. Maybe ask author to update?", - commit_sha - )) -} - -/// Adds a batch file that allows for launching Northstar with mods PR profile -fn add_batch_file(game_install_path: &str) { - let batch_path = format!("{}/r2ns-launch-mod-pr-version.bat", game_install_path); - let path = Path::new(&batch_path); - let display = path.display(); - - // Open a file in write-only mode, returns `io::Result<File>` - let mut file = match File::create(path) { - Err(why) => panic!("couldn't create {}: {}", display, why), - Ok(file) => file, - }; - - // Write the string to `file`, returns `io::Result<()>` - let batch_file_content = - "NorthstarLauncher.exe -profile=R2Northstar-PR-test-managed-folder\r\n"; - - match file.write_all(batch_file_content.as_bytes()) { - Err(why) => panic!("couldn't write to {}: {}", display, why), - Ok(_) => log::info!("successfully wrote to {}", display), - } -} - -/// Downloads selected launcher PR and extracts it into game install path -#[tauri::command] -pub async fn apply_launcher_pr( - pull_request: PullsApiResponseElement, - game_install: GameInstall, -) -> Result<(), String> { - // Exit early if wrong game path - check_is_valid_game_path(&game_install.game_path)?; - - // get download link - let download_url = match get_launcher_download_link(pull_request.head.sha.clone()).await { - Ok(res) => res, - Err(err) => { - return Err(format!( - "Couldn't grab download link for PR \"{}\". {}", - pull_request.number, err - )) - } - }; - - let archive = match download_zip_into_memory(download_url).await { - Ok(archive) => archive, - Err(err) => return Err(err.to_string()), - }; - - let extract_directory = format!( - "{}/___flightcore-temp/download-dir/launcher-pr-{}", - game_install.game_path, pull_request.number - ); - match std::fs::create_dir_all(extract_directory.clone()) { - Ok(_) => (), - Err(err) => { - return Err(format!( - "Failed creating temporary download directory: {}", - err - )) - } - }; - - let target_dir = std::path::PathBuf::from(extract_directory.clone()); // Doesn't need to exist - match zip_extract::extract(io::Cursor::new(archive), &target_dir, true) { - Ok(()) => (), - Err(err) => { - return Err(format!("Failed unzip: {}", err)); - } - }; - - // Copy only necessary files from temp dir - // Copy: - // - NorthstarLauncher.exe - // - Northstar.dll - let files_to_copy = vec!["NorthstarLauncher.exe", "Northstar.dll"]; - for file_name in files_to_copy { - let source_file_path = format!("{}/{}", extract_directory, file_name); - let destination_file_path = format!("{}/{}", game_install.game_path, file_name); - match std::fs::copy(source_file_path, destination_file_path) { - Ok(_result) => (), - Err(err) => { - return Err(format!( - "Failed to copy necessary file {} from temp dir: {}", - file_name, err - )) - } - }; - } - - // delete extract directory - match std::fs::remove_dir_all(&extract_directory) { - Ok(()) => (), - Err(err) => { - return Err(format!( - "Failed to delete temporary download directory: {}", - err - )) - } - } - - log::info!("All done with installing launcher PR"); - Ok(()) -} - -/// Downloads selected mods PR and extracts it into profile in game install path -#[tauri::command] -pub async fn apply_mods_pr( - pull_request: PullsApiResponseElement, - game_install: GameInstall, -) -> Result<(), String> { - // Exit early if wrong game path - check_is_valid_game_path(&game_install.game_path)?; - - let download_url = match get_mods_download_link(pull_request) { - Ok(url) => url, - Err(err) => return Err(err.to_string()), - }; - - let archive = match download_zip_into_memory(download_url).await { - Ok(archive) => archive, - Err(err) => return Err(err.to_string()), - }; - - let profile_folder = format!( - "{}/R2Northstar-PR-test-managed-folder", - game_install.game_path - ); - - // Delete previously managed folder - if std::fs::remove_dir_all(profile_folder.clone()).is_err() { - if std::path::Path::new(&profile_folder).exists() { - log::error!("Failed removing previous dir"); - } else { - log::warn!("Failed removing folder that doesn't exist. Probably cause first run"); - } - }; - - // Create profile folder - match std::fs::create_dir_all(profile_folder.clone()) { - Ok(()) => (), - Err(err) => return Err(err.to_string()), - } - - let target_dir = std::path::PathBuf::from(format!("{}/mods", profile_folder)); // Doesn't need to exist - match zip_extract::extract(io::Cursor::new(archive), &target_dir, true) { - Ok(()) => (), - Err(err) => { - return Err(format!("Failed unzip: {}", err)); - } - }; - // Add batch file to launch right profile - add_batch_file(&game_install.game_path); - - log::info!("All done with installing mods PR"); - Ok(()) -} diff --git a/src-tauri/src/github/release_notes.rs b/src-tauri/src/github/release_notes.rs deleted file mode 100644 index 4adfb24b..00000000 --- a/src-tauri/src/github/release_notes.rs +++ /dev/null @@ -1,244 +0,0 @@ -use rand::prelude::SliceRandom; -use serde::{Deserialize, Serialize}; -use std::vec::Vec; -use ts_rs::TS; - -#[derive(Serialize, Deserialize, Debug, Clone, TS)] -#[ts(export)] -pub struct ReleaseInfo { - pub name: String, - pub published_at: String, - pub body: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, TS)] -#[ts(export)] -pub struct FlightCoreVersion { - tag_name: String, - published_at: String, -} - -/// Gets newest FlighCore version from GitHub -#[tauri::command] -pub async fn get_newest_flightcore_version() -> Result<FlightCoreVersion, String> { - // Get newest version number from GitHub API - log::info!("Checking GitHub API"); - let octocrab = octocrab::instance(); - let page = octocrab - .repos("R2NorthstarTools", "FlightCore") - .releases() - .list() - // Optional Parameters - .per_page(1) - .page(1u32) - // Send the request - .send() - .await - .map_err(|err| err.to_string())?; - - // Get newest element - let latest_release_item = &page.items[0]; - - let flightcore_version = FlightCoreVersion { - tag_name: latest_release_item.tag_name.clone(), - published_at: latest_release_item.published_at.unwrap().to_rfc3339(), - }; - log::info!("Done checking GitHub API"); - - Ok(flightcore_version) -} - -/// Checks if installed FlightCore version is up-to-date -/// false -> FlightCore install is up-to-date -/// true -> FlightCore install is outdated -#[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 current_version = env!("CARGO_PKG_VERSION"); - let current_version = semver::Version::parse(current_version).unwrap(); - - #[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 { - // Time to wait (2h) h * m * s - let threshold_seconds = 2 * 60 * 60; - - // Get current time - let current_time = chrono::Utc::now(); - - // Get latest release time from GitHub API response - let result = chrono::DateTime::parse_from_rfc3339(&newest_flightcore_release.published_at) - .unwrap() - .with_timezone(&chrono::Utc); - - // Check if current time is outside of threshold - let diff = current_time - result; - if diff.num_seconds() < threshold_seconds { - // User would be outdated but the newest release is recent - // therefore we do not wanna show outdated warning. - return Ok(false); - } - return Ok(true); - } - - Ok(is_outdated) -} - -#[tauri::command] -pub async fn get_northstar_release_notes() -> Result<Vec<ReleaseInfo>, String> { - let octocrab = octocrab::instance(); - let page = octocrab - .repos("R2Northstar", "Northstar") - .releases() - .list() - // Optional Parameters - .per_page(25) - .page(1u32) - // Send the request - .send() - .await - .map_err(|err| err.to_string())?; - - // TODO there's probably a way to automatically serialize into the struct but I don't know yet how to - let mut release_info_vector: Vec<ReleaseInfo> = vec![]; - for item in page.items { - let release_info = ReleaseInfo { - name: item.name.ok_or(String::from("Release name not found"))?, - published_at: item - .published_at - .ok_or(String::from("Release date not found"))? - .to_rfc3339(), - body: item.body.ok_or(String::from("Release body not found"))?, - }; - release_info_vector.push(release_info); - } - - log::info!("Done checking GitHub API"); - - Ok(release_info_vector) -} - -/// Checks latest GitHub release and generates a announcement message for Discord based on it -#[tauri::command] -pub async fn generate_release_note_announcement() -> Result<String, String> { - let octocrab = octocrab::instance(); - let page = octocrab - .repos("R2Northstar", "Northstar") - .releases() - .list() - // Optional Parameters - .per_page(1) - .page(1u32) - // Send the request - .send() - .await - .unwrap(); - - // Get newest element - let latest_release_item = &page.items[0]; - - // Extract the URL to the GitHub release note - let github_release_link = latest_release_item.html_url.clone(); - - // Extract release version number - let current_ns_version = &latest_release_item.tag_name; - - // Extract changelog and format it - let changelog = remove_markdown_links::remove_markdown_links( - latest_release_item - .body - .as_ref() - .unwrap() - .split("**Contributors:**") - .next() - .unwrap() - .trim(), - ); - - // Strings to insert for different sections - // Hardcoded for now - let general_info = "REPLACE ME"; - let modders_info = "Mod compatibility should not be impacted"; - let server_hosters_info = "REPLACE ME"; - - let mut rng = rand::thread_rng(); - let attributes = vec![ - "adorable", - "amazing", - "beautiful", - "blithsome", - "brilliant", - "compassionate", - "dazzling", - "delightful", - "distinguished", - "elegant", - "enigmatic", - "enthusiastic", - "fashionable", - "fortuitous", - "friendly", - "generous", - "gleeful", - "gorgeous", - "handsome", - "lively", - "lovely", - "lucky", - "lustrous", - "marvelous", - "merry", - "mirthful", - "phantasmagorical", - "pretty", - "propitious", - "ravishing", - "sincere", - "sophisticated fellow", - "stupendous", - "vivacious", - "wonderful", - "zestful", - ]; - - let selected_attribute = attributes.choose(&mut rng).unwrap(); - - // Build announcement string - let return_string = format!( - r"Hello {selected_attribute} people <3 -**Northstar `{current_ns_version}` is out!** - -{general_info} - -__**Modders:**__ - -{modders_info} - -__**Server hosters:**__ - -{server_hosters_info} - -__**Changelog:**__ -``` -{changelog} -``` -{github_release_link} - -Checkout #installation on how to install/update Northstar -(the process is the same for both, using a Northstar installer like FlightCore, Viper, or VTOL is recommended over manual installation) - -If you do notice any bugs, please open an issue on Github or drop a message in the thread below -" - ); - - // Return built announcement message - Ok(return_string.to_string()) -} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs new file mode 100644 index 00000000..4a277ef3 --- /dev/null +++ b/src-tauri/src/lib.rs @@ -0,0 +1,14 @@ +// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ +#[tauri::command] +fn greet(name: &str) -> String { + format!("Hello, {}! You've been greeted from Rust!", name) +} + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + tauri::Builder::default() + .plugin(tauri_plugin_opener::init()) + .invoke_handler(tauri::generate_handler![greet]) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index a9f484f5..2abccd9e 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1,221 +1,6 @@ -#![cfg_attr( - all(not(debug_assertions), target_os = "windows"), - windows_subsystem = "windows" -)] - -use std::{env, time::Duration}; - -mod constants; -mod development; -mod github; -mod mod_management; -mod northstar; -mod platform_specific; -mod repair_and_verify; -mod thunderstore; -mod util; - -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; -use tokio::time::sleep; -use ts_rs::TS; - -#[derive(Serialize, Deserialize, Debug, Clone, TS)] -#[ts(export)] -struct NorthstarThunderstoreRelease { - package: String, - version: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, TS)] -#[ts(export)] -pub struct NorthstarThunderstoreReleaseWrapper { - label: String, - value: NorthstarThunderstoreRelease, -} +// Prevents additional console window on Windows in release, DO NOT REMOVE!! +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] fn main() { - // Setup logger - let mut log_builder = pretty_env_logger::formatted_builder(); - log_builder.parse_filters("info"); - let logger = sentry_log::SentryLogger::with_dest(log_builder.build()); - - log::set_boxed_logger(Box::new(logger)).unwrap(); - log::set_max_level(log::LevelFilter::Info); - - // Only enable Sentry crash logs on release - #[cfg(not(debug_assertions))] - let _guard = sentry::init(( - "https://f833732deb2240b0b2dc4abce97d0f1d@o1374052.ingest.sentry.io/6692177", - sentry::ClientOptions { - release: sentry::release_name!(), - attach_stacktrace: true, - ..Default::default() - }, - )); - - let tauri_builder_res = tauri::Builder::default() - .plugin(tauri_plugin_store::Builder::default().build()) - .setup(|app| { - let app_handle = app.app_handle(); - tauri::async_runtime::spawn(async move { - loop { - sleep(Duration::from_millis(2000)).await; - // println!("sending backend ping"); - app_handle.emit_all("backend-ping", "ping").unwrap(); - } - }); - let app_handle = app.app_handle(); - tauri::async_runtime::spawn(async move { - loop { - sleep(Duration::from_millis(2000)).await; - app_handle - .emit_all( - "ea-app-running-ping", - util::check_ea_app_or_origin_running(), - ) - .unwrap(); - } - }); - let app_handle = app.app_handle(); - tauri::async_runtime::spawn(async move { - loop { - sleep(Duration::from_millis(2000)).await; - app_handle - .emit_all("northstar-running-ping", util::check_northstar_running()) - .unwrap(); - } - }); - - // Emit updated player and server count to GUI - let app_handle = app.app_handle(); - tauri::async_runtime::spawn(async move { - loop { - sleep(constants::REFRESH_DELAY).await; - app_handle - .emit_all( - "northstar-statistics", - util::get_server_player_count().await, - ) - .unwrap(); - } - }); - - Ok(()) - }) - .manage(()) - .invoke_handler(tauri::generate_handler![ - development::install_git_main, - github::compare_tags, - github::get_list_of_tags, - github::pull_requests::apply_launcher_pr, - github::pull_requests::apply_mods_pr, - github::pull_requests::get_launcher_download_link, - github::pull_requests::get_pull_requests_wrapper, - github::release_notes::check_is_flightcore_outdated, - github::release_notes::generate_release_note_announcement, - github::release_notes::get_newest_flightcore_version, - github::release_notes::get_northstar_release_notes, - mod_management::delete_northstar_mod, - mod_management::delete_thunderstore_mod, - mod_management::get_installed_mods_and_properties, - mod_management::install_mod_wrapper, - mod_management::set_mod_enabled_status, - northstar::check_is_northstar_outdated, - northstar::get_available_northstar_versions, - northstar::get_northstar_version_number, - northstar::install::find_game_install_location, - northstar::install::install_northstar_wrapper, - northstar::install::update_northstar, - northstar::launch_northstar, - northstar::profile::clone_profile, - northstar::profile::delete_profile, - northstar::profile::fetch_profiles, - northstar::profile::validate_profile, - platform_specific::check_cgnat, - platform_specific::get_host_os, - platform_specific::get_local_northstar_proton_wrapper_version, - platform_specific::install_northstar_proton_wrapper, - platform_specific::uninstall_northstar_proton_wrapper, - repair_and_verify::clean_up_download_folder_wrapper, - repair_and_verify::disable_all_but_core, - repair_and_verify::get_log_list, - repair_and_verify::verify_game_files, - repair_and_verify::verify_install_location, - thunderstore::query_thunderstore_packages_api, - util::close_application, - util::force_panic, - util::get_flightcore_version_number, - util::get_server_player_count, - util::is_debug_mode, - util::kill_northstar, - util::open_repair_window, - ]) - .run(tauri::generate_context!()); - - match tauri_builder_res { - Ok(()) => (), - Err(err) => { - // Failed to launch system native web view - - // Log error on Linux - #[cfg(not(target_os = "windows"))] - { - log::error!("{err}"); - } - - // On Windows we can show an error window using Windows API to show how to install WebView2 - #[cfg(target_os = "windows")] - { - log::error!("WebView2 not installed: {err}"); - 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(); - } - } - } - }; -} - -/// Defines how Titanfall2 was installed (Steam, Origin, ...) -#[derive(Serialize, Deserialize, Debug, Clone, TS)] -#[ts(export)] -pub enum InstallType { - STEAM, - ORIGIN, - EAPLAY, - UNKNOWN, -} - -/// Object holding information of the Titanfall2 install, including -/// - Install path -/// - Active profile -/// - Type of installation (Steam, Origin, ...) -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct GameInstall { - pub game_path: String, - pub profile: String, - pub install_type: InstallType, -} - -/// Object holding various information about a Northstar mod -#[derive(Serialize, Deserialize, Debug, Clone, TS)] -#[ts(export)] -pub struct NorthstarMod { - pub name: String, - pub version: Option<String>, - pub thunderstore_mod_string: Option<String>, - pub enabled: bool, - pub directory: String, + tauri_app_lib::run() } diff --git a/src-tauri/src/mod_management/legacy.rs b/src-tauri/src/mod_management/legacy.rs deleted file mode 100644 index 1e9f90f5..00000000 --- a/src-tauri/src/mod_management/legacy.rs +++ /dev/null @@ -1,213 +0,0 @@ -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}; -use serde::{Deserialize, Serialize}; -use std::{io::Read, path::PathBuf}; - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct ModJson { - #[serde(rename = "Name")] - name: String, - #[serde(rename = "ThunderstoreModString")] - thunderstore_mod_string: Option<String>, - #[serde(rename = "Version")] - version: Option<String>, -} - -/// Parses `manifest.json` for Thunderstore mod string -fn parse_for_thunderstore_mod_string(nsmod_path: &str) -> Result<String, anyhow::Error> { - let manifest_json_path = format!("{}/manifest.json", nsmod_path); - let ts_author_txt_path = format!("{}/thunderstore_author.txt", nsmod_path); - - // Check if `manifest.json` exists and parse - let data = std::fs::read_to_string(manifest_json_path)?; - let thunderstore_manifest: super::ThunderstoreManifest = json5::from_str(&data)?; - - // Check if `thunderstore_author.txt` exists and parse - let mut file = std::fs::File::open(ts_author_txt_path)?; - let mut thunderstore_author = String::new(); - file.read_to_string(&mut thunderstore_author)?; - - // Build mod string - let thunderstore_mod_string = format!( - "{}-{}-{}", - thunderstore_author, thunderstore_manifest.name, thunderstore_manifest.version_number - ); - - Ok(thunderstore_mod_string) -} - -/// Parse `mods` folder for installed mods. -pub fn parse_installed_mods( - game_install: &GameInstall, -) -> Result<Vec<NorthstarMod>, anyhow::Error> { - let ns_mods_folder = format!("{}/{}/mods/", game_install.game_path, game_install.profile); - - let paths = match std::fs::read_dir(ns_mods_folder) { - 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 { - log::info!("{path:?}"); - let my_path = path.unwrap().path(); - log::info!("{my_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; - } - - // Parse mod.json and get mod name - - // 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 Thunderstore mod string if it exists - let thunderstore_mod_string = match parsed_mod_json.thunderstore_mod_string { - // Attempt legacy method for getting Thunderstore string first - Some(ts_mod_string) => Some(ts_mod_string), - // Legacy method failed - None => match parse_for_thunderstore_mod_string(&directory_str) { - Ok(thunderstore_mod_string) => Some(thunderstore_mod_string), - Err(_err) => None, - }, - }; - // 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, - enabled: false, // Placeholder - directory: mod_directory, - }; - - mods.push(ns_mod); - } - - // 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 deleted file mode 100644 index 52ef1180..00000000 --- a/src-tauri/src/mod_management/mod.rs +++ /dev/null @@ -1,797 +0,0 @@ -// This file contains various mod management functions - -use crate::constants::{BLACKLISTED_MODS, CORE_MODS, MODS_WITH_SPECIAL_REQUIREMENTS}; -use async_recursion::async_recursion; -use thermite::prelude::ThermiteError; - -use crate::NorthstarMod; -use anyhow::{anyhow, Result}; -use serde::{Deserialize, Serialize}; -use std::error::Error; -use std::str::FromStr; -use std::string::ToString; -use std::{fs, path::PathBuf}; - -mod legacy; -mod plugins; -use crate::GameInstall; - -#[derive(Debug, Clone)] -pub struct ParsedThunderstoreModString { - author_name: String, - mod_name: String, - version: String, -} - -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 passes regex - 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(); - let mod_name = parts.next().ok_or("None value on mod_name")?.to_string(); - let version = parts.next().ok_or("None value on version")?.to_string(); - - Ok(ParsedThunderstoreModString { - author_name, - mod_name, - version, - }) - } -} - -impl std::fmt::Display for ParsedThunderstoreModString { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}-{}-{}", self.author_name, self.mod_name, self.version) - } -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct ThunderstoreManifest { - name: String, - version_number: String, -} - -/// A wrapper around a temporary file handle and its path. -/// -/// This struct is designed to be used for temporary files that should be automatically deleted -/// when the `TempFile` instance goes out of scope. -#[derive(Debug)] -pub struct TempFile(fs::File, PathBuf); - -impl TempFile { - pub fn new(file: fs::File, path: PathBuf) -> Self { - Self(file, path) - } - - pub fn file(&self) -> &fs::File { - &self.0 - } -} - -impl Drop for TempFile { - fn drop(&mut self) { - _ = fs::remove_file(&self.1) - } -} - -impl std::ops::Deref for TempFile { - type Target = fs::File; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -/// Installs the specified mod -#[tauri::command] -pub async fn install_mod_wrapper( - game_install: GameInstall, - thunderstore_mod_string: String, -) -> Result<(), String> { - match fc_download_mod_and_install(&game_install, &thunderstore_mod_string).await { - Ok(()) => (), - Err(err) => { - log::warn!("{err}"); - return Err(err); - } - }; - match crate::repair_and_verify::clean_up_download_folder(&game_install, false) { - Ok(()) => Ok(()), - Err(err) => { - log::info!("Failed to delete download folder due to {}", err); - // Failure to delete download folder is not an error in mod install - // As such ignore. User can still force delete if need be - Ok(()) - } - } -} - -/// 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> { - let enabledmods_json_path = format!( - "{}/{}/enabledmods.json", - game_install.game_path, game_install.profile - ); - - // 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) -} - -/// Gets all currently installed and enabled/disabled mods to rebuild `enabledmods.json` -pub fn rebuild_enabled_mods_json(game_install: &GameInstall) -> Result<(), String> { - let enabledmods_json_path = format!( - "{}/{}/enabledmods.json", - game_install.game_path, game_install.profile - ); - let mods_and_properties = get_installed_mods_and_properties(game_install.clone())?; - - // Create new mapping - let mut my_map = serde_json::Map::new(); - - // Build mapping - for ns_mod in mods_and_properties.into_iter() { - my_map.insert(ns_mod.name, serde_json::Value::Bool(ns_mod.enabled)); - } - - // Turn into serde object - let obj = serde_json::Value::Object(my_map); - - // Write to file - std::fs::write( - enabledmods_json_path, - serde_json::to_string_pretty(&obj).unwrap(), - ) - .unwrap(); - - Ok(()) -} - -/// Set the status of a passed mod to enabled/disabled -#[tauri::command] -pub fn set_mod_enabled_status( - game_install: GameInstall, - mod_name: String, - is_enabled: bool, -) -> Result<(), String> { - let enabledmods_json_path = format!( - "{}/{}/enabledmods.json", - game_install.game_path, game_install.profile - ); - - // Parse JSON - let mut res: serde_json::Value = match get_enabled_mods(&game_install) { - Ok(res) => res, - Err(err) => { - log::warn!("Couldn't parse `enabledmod.json`: {}", err); - log::warn!("Rebuilding file."); - - rebuild_enabled_mods_json(&game_install)?; - - // Then try again - get_enabled_mods(&game_install)? - } - }; - - // Check if key exists - if res.get(mod_name.clone()).is_none() { - // If it doesn't exist, rebuild `enabledmod.json` - log::info!("Value not found in `enabledmod.json`. Rebuilding file"); - rebuild_enabled_mods_json(&game_install)?; - - // Then try again - res = get_enabled_mods(&game_install)?; - } - - // Update value - res[mod_name] = serde_json::Value::Bool(is_enabled); - - // Save the JSON structure into the output file - std::fs::write( - enabledmods_json_path, - serde_json::to_string_pretty(&res).unwrap(), - ) - .unwrap(); - - 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?.path(); - let md = std::fs::metadata(my_path.clone())?; - 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!( - "{}/{}/packages/", - game_install.game_path, game_install.profile - ); - - 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? -#[tauri::command] -pub fn get_installed_mods_and_properties( - game_install: GameInstall, -) -> Result<Vec<NorthstarMod>, String> { - // 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, - Err(_) => serde_json::from_str("{}").unwrap(), // `enabledmods.json` not found, create empty object - }; - - let mut installed_mods = Vec::new(); - let binding = serde_json::Map::new(); // Empty map in case treating as object fails - let mapping = enabled_mods.as_object().unwrap_or(&binding); - - // Use list of installed mods and set enabled based on `enabledmods.json` - for mut current_mod in found_installed_mods { - let current_mod_enabled = match mapping.get(¤t_mod.name) { - Some(enabled) => enabled.as_bool().unwrap(), - None => true, // Northstar considers mods not in mapping as enabled. - }; - current_mod.enabled = current_mod_enabled; - installed_mods.push(current_mod); - } - - Ok(installed_mods) -} - -async fn get_ns_mod_download_url(thunderstore_mod_string: &str) -> Result<String, String> { - // TODO: This will crash the thread if not internet connection exist. `match` should be used instead - let index = thermite::api::get_package_index().unwrap().to_vec(); - - // Parse mod string - let parsed_ts_mod_string: ParsedThunderstoreModString = match thunderstore_mod_string.parse() { - Ok(res) => res, - Err(_) => return Err("Failed to parse mod string".to_string()), - }; - - // Encode as URL - let ts_mod_string_url = format!( - "{}/{}/{}", - parsed_ts_mod_string.author_name, - parsed_ts_mod_string.mod_name, - parsed_ts_mod_string.version - ); - - for ns_mod in index { - // Iterate over all versions of a given mod - for ns_mod in ns_mod.versions.values() { - if ns_mod.url.contains(&ts_mod_string_url) { - return Ok(ns_mod.url.clone()); - } - } - } - - Err("Could not find mod on Thunderstore".to_string()) -} - -/// Returns a vector of modstrings containing the dependencies of a given mod -async fn get_mod_dependencies(thunderstore_mod_string: &str) -> Result<Vec<String>, anyhow::Error> { - log::info!("Attempting to get dependencies for: {thunderstore_mod_string}"); - - let index = thermite::api::get_package_index()?.to_vec(); - - // String replace works but more care should be taken in the future - let ts_mod_string_url = thunderstore_mod_string.replace('-', "/"); - - // Iterate over index - for ns_mod in index { - // Iterate over all versions of a given mod - for ns_mod in ns_mod.versions.values() { - if ns_mod.url.contains(&ts_mod_string_url) { - return Ok(ns_mod.deps.clone()); - } - } - } - 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!( - "{}/{}/packages", - game_install.game_path, game_install.profile - ); - - // 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) -> Result<(), Box<dyn Error + Send + Sync + 'static>> { - let mut archive = match zip::read::ZipArchive::new(*input) { - Ok(archive) => archive, - Err(_) => { - return Err(Box::new(ThermiteError::UnknownError( - "Failed reading zip file".into(), - ))) - } - }; - - 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, prompting user"); - if !plugins::plugin_prompt() { - return Err(Box::new(ThermiteError::UnknownError( - "Plugin detected and install denied".into(), - ))); - } - } - } - } - } - - if has_mods && mod_json_exists { - Ok(()) - } else { - Err(Box::new(ThermiteError::UnknownError( - "Mod not correctly formatted".into(), - ))) - } -} - -// 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. -#[async_recursion] -pub async fn fc_download_mod_and_install( - game_install: &GameInstall, - thunderstore_mod_string: &str, -) -> Result<(), String> { - log::info!("Attempting to install \"{thunderstore_mod_string}\" to {game_install:?}"); - // Get mods and download directories - let download_directory = format!( - "{}/___flightcore-temp/download-dir/", - game_install.game_path - ); - - // Early return on empty string - if thunderstore_mod_string.is_empty() { - return Err("Passed empty string".to_string()); - } - - let deps = match get_mod_dependencies(thunderstore_mod_string).await { - Ok(deps) => deps, - Err(err) => return Err(err.to_string()), - }; - log::info!("Mod dependencies: {deps:?}"); - - // Recursively install dependencies - for dep in deps { - match fc_download_mod_and_install(game_install, &dep).await { - Ok(()) => (), - Err(err) => { - if err == "Cannot install Northstar as a mod!" { - continue; // For Northstar as a dependency, we just skip it - } else { - return Err(err); - } - } - }; - } - - // Prevent installing Northstar as a mod - // While it would fail during install anyway, having explicit error message is nicer - for blacklisted_mod in BLACKLISTED_MODS { - if thunderstore_mod_string.contains(blacklisted_mod) { - return Err("Cannot install Northstar as a mod!".to_string()); - } - } - - // Prevent installing mods that have specific install requirements - for special_mod in MODS_WITH_SPECIAL_REQUIREMENTS { - if thunderstore_mod_string.contains(special_mod) { - return Err(format!( - "{} has special install requirements and cannot be installed with FlightCore", - thunderstore_mod_string - )); - } - } - - // Get download URL for the specified mod - let download_url = get_ns_mod_download_url(thunderstore_mod_string).await?; - - // Create download directory - match std::fs::create_dir_all(download_directory.clone()) { - Ok(()) => (), - Err(err) => return Err(err.to_string()), - }; - - let path = format!( - "{}/___flightcore-temp/download-dir/{thunderstore_mod_string}.zip", - game_install.game_path - ); - - // Download the mod - let temp_file = TempFile::new( - std::fs::File::options() - .read(true) - .write(true) - .truncate(true) - .create(true) - .open(&path) - .map_err(|e| e.to_string())?, - (&path).into(), - ); - match thermite::core::manage::download(temp_file.file(), download_url) { - Ok(_written_bytes) => (), - Err(err) => return Err(err.to_string()), - }; - - // Get directory to install to made up of packages directory and Thunderstore mod string - let install_directory = format!( - "{}/{}/packages/", - game_install.game_path, game_install.profile - ); - - // Extract the mod to the mods directory - match thermite::core::manage::install_with_sanity( - thunderstore_mod_string, - temp_file.file(), - 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 match err { - ThermiteError::SanityError(e) => Err( - format!("Mod failed sanity check during install. It's probably not correctly formatted. {}", e) - ), - _ => 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); - } - }; - - Ok(()) -} - -/// Deletes a given Northstar mod folder -fn delete_mod_folder(ns_mod_directory: &str) -> Result<(), String> { - let ns_mod_dir_path = std::path::Path::new(&ns_mod_directory); - - // Safety check: Check whether `mod.json` exists and exit early if not - // If it does not exist, we might not be dealing with a Northstar mod - let mod_json_path = ns_mod_dir_path.join("mod.json"); - if !mod_json_path.exists() { - // If it doesn't exist, return an error - return Err(format!("mod.json does not exist in {}", ns_mod_directory)); - } - - match std::fs::remove_dir_all(ns_mod_directory) { - Ok(()) => Ok(()), - Err(err) => Err(format!("Failed deleting mod: {err}")), - } -} - -/// Deletes a Northstar mod based on its name -#[tauri::command] -pub fn delete_northstar_mod(game_install: GameInstall, nsmod_name: String) -> Result<(), String> { - // Prevent deleting core mod - for core_mod in CORE_MODS { - if nsmod_name == core_mod { - return Err(format!("Cannot remove core mod {nsmod_name}")); - } - } - - // Get installed mods - let installed_ns_mods = get_installed_mods_and_properties(game_install)?; - - // Get folder name based on northstarmods - for installed_ns_mod in installed_ns_mods { - // Installed mod matches specified mod - if installed_ns_mod.name == nsmod_name { - // Delete folder - return delete_mod_folder(&installed_ns_mod.directory); - } - } - - 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> { - // Check packages - let packages_folder = format!( - "{}/{}/packages", - game_install.game_path, game_install.profile - ); - 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; - } - - let entry_path = entry.path(); - let package_folder_ts_string = entry_path.file_name().unwrap().to_string_lossy(); - - if package_folder_ts_string != thunderstore_mod_string { - // Not the mod folder we are looking for, try the next one\ - continue; - } - - // All checks passed, this is the matching mod - return delete_package_folder(&entry.path().display().to_string()); - } - } - - // Try legacy mod installs as fallback - legacy::delete_thunderstore_mod(game_install, thunderstore_mod_string) -} diff --git a/src-tauri/src/mod_management/plugins.rs b/src-tauri/src/mod_management/plugins.rs deleted file mode 100644 index e2427a16..00000000 --- a/src-tauri/src/mod_management/plugins.rs +++ /dev/null @@ -1,26 +0,0 @@ -use tauri::api::dialog::blocking::MessageDialogBuilder; -use tauri::api::dialog::{MessageDialogButtons, MessageDialogKind}; - -/// Prompt on plugin -/// Returns: -/// - true: user accepted plugin install -/// - false: user denied plugin install -pub fn plugin_prompt() -> bool { - let dialog = MessageDialogBuilder::new( - "Plugin in package detected", - "This mod contains a plugin. Plugins have unrestricted access to your computer! - \nMake sure you trust the author! - \n - \nPress 'Ok' to continue or 'Cancel' to abort mod installation", - ) - .kind(MessageDialogKind::Warning) - .buttons(MessageDialogButtons::OkCancel); - - if dialog.show() { - log::info!("Accepted plugin install"); - true - } else { - log::warn!("Plugin install cancelled"); - false - } -} 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(()) -} diff --git a/src-tauri/src/platform_specific/linux.rs b/src-tauri/src/platform_specific/linux.rs deleted file mode 100644 index fcac5b67..00000000 --- a/src-tauri/src/platform_specific/linux.rs +++ /dev/null @@ -1,98 +0,0 @@ -// Linux specific code - -fn get_proton_dir() -> Result<String, String> { - let steam_dir = match steamlocate::SteamDir::locate() { - Ok(result) => result, - Err(_) => return Err("Unable to find Steam directory".to_string()), - }; - let compat_dir = format!("{}/compatibilitytools.d", steam_dir.path().display()); - - Ok(compat_dir) -} - -/// Downloads and installs NS proton -/// Assumes Steam install -pub fn install_ns_proton() -> Result<(), String> { - // Get latest NorthstarProton release - let latest = match thermite::core::latest_release() { - Ok(result) => result, - Err(_) => return Err("Failed to fetch latest NorthstarProton release".to_string()), - }; - - let temp_dir = std::env::temp_dir(); - let path = format!("{}/nsproton-{}.tar.gz", temp_dir.display(), latest); - let archive = match std::fs::File::create(path.clone()) { - Ok(result) => result, - Err(_) => return Err("Failed to allocate NorthstarProton archive on disk".to_string()), - }; - - // Download the latest Proton release - log::info!("Downloading NorthstarProton to {}", path); - match thermite::core::download_ns_proton(latest, archive) { - Ok(_) => {} - Err(_) => return Err("Failed to download NorthstarProton".to_string()), - } - - log::info!("Finished Download"); - - let compat_dir = get_proton_dir()?; - - match std::fs::create_dir_all(compat_dir.clone()) { - Ok(_) => {} - Err(_) => return Err("Failed to create compatibilitytools directory".to_string()), - } - - let finished = match std::fs::File::open(path.clone()) { - Ok(result) => result, - Err(_) => return Err("Failed to open NorthstarProton archive".to_string()), - }; - - // Extract to Proton dir - log::info!("Installing NorthstarProton to {}", compat_dir); - match thermite::core::install_ns_proton(&finished, compat_dir) { - Ok(_) => {} - Err(_) => return Err("Failed to create install NorthstarProton".to_string()), - } - log::info!("Finished Installation"); - drop(finished); - - // We installed NSProton, lets ignore this if it fails - let _ = std::fs::remove_file(path); - - Ok(()) -} - -/// Remove NS Proton -pub fn uninstall_ns_proton() -> Result<(), String> { - let compat_dir = get_proton_dir()?; - let pattern = format!("{}/NorthstarProton*", compat_dir); - for e in glob::glob(&pattern).expect("Failed to read glob pattern") { - match e { - Ok(path) => match std::fs::remove_dir_all(path.clone()) { - Ok(_) => {} - Err(_) => return Err(format!("Failed to remove {}", path.display())), - }, - Err(e) => return Err(format!("Found unprocessable entry {}", e)), - } - } - - 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 pattern = format!("{}/NorthstarProton*/version", compat_dir); - - if let Some(e) = glob::glob(&pattern) - .expect("Failed to read glob pattern") - .next() - { - let version_content = std::fs::read_to_string(e.unwrap()).unwrap(); - let version = version_content.split(' ').nth(1).unwrap().to_string(); - - return Ok(version); - } - - Err("Northstar Proton is not installed".to_string()) -} diff --git a/src-tauri/src/platform_specific/mod.rs b/src-tauri/src/platform_specific/mod.rs deleted file mode 100644 index 4e0514d4..00000000 --- a/src-tauri/src/platform_specific/mod.rs +++ /dev/null @@ -1,50 +0,0 @@ -#[cfg(target_os = "windows")] -pub mod windows; - -#[cfg(target_os = "linux")] -pub mod linux; - -/// Returns identifier of host OS FlightCore is running on -#[tauri::command] -pub fn get_host_os() -> String { - std::env::consts::OS.to_string() -} - -/// On Linux attempts to install NorthstarProton -/// On Windows simply returns an error message -#[tauri::command] -pub async fn install_northstar_proton_wrapper() -> Result<(), String> { - #[cfg(target_os = "linux")] - return linux::install_ns_proton().map_err(|err| err.to_string()); - - #[cfg(target_os = "windows")] - Err("Not supported on Windows".to_string()) -} - -#[tauri::command] -pub async fn uninstall_northstar_proton_wrapper() -> Result<(), String> { - #[cfg(target_os = "linux")] - return linux::uninstall_ns_proton(); - - #[cfg(target_os = "windows")] - Err("Not supported on Windows".to_string()) -} - -#[tauri::command] -pub 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()) -} - -/// Check whether the current device might be behind a CGNAT -#[tauri::command] -pub async fn check_cgnat() -> Result<String, String> { - #[cfg(target_os = "linux")] - return Err("Not supported on Linux".to_string()); - - #[cfg(target_os = "windows")] - windows::check_cgnat().await -} diff --git a/src-tauri/src/platform_specific/windows.rs b/src-tauri/src/platform_specific/windows.rs deleted file mode 100644 index fc6aab5d..00000000 --- a/src-tauri/src/platform_specific/windows.rs +++ /dev/null @@ -1,104 +0,0 @@ -/// Windows specific code -use anyhow::{anyhow, Result}; -use std::net::Ipv4Addr; - -#[cfg(target_os = "windows")] -use winreg::{enums::HKEY_LOCAL_MACHINE, RegKey}; - -use crate::repair_and_verify::check_is_valid_game_path; - -/// Gets Titanfall2 install location on Origin -pub fn origin_install_location_detection() -> Result<String, anyhow::Error> { - #[cfg(target_os = "windows")] - { - let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); - match hklm.open_subkey("SOFTWARE\\Respawn\\Titanfall2") { - Ok(tf) => { - let game_path_str: String = tf.get_value("Install Dir")?; - - match check_is_valid_game_path(&game_path_str) { - Ok(()) => { - return Ok(game_path_str.to_string()); - } - Err(err) => { - log::warn!("{err}"); - } - } - } - Err(err) => { - log::warn!("{err}"); - } - } - } - - Err(anyhow!("No Origin / EA App install path found")) -} - -/// Check whether the current device might be behind a CGNAT -pub async fn check_cgnat() -> Result<String, String> { - // Use external service to grap IP - let url = "https://api.ipify.org"; - let response = reqwest::get(url).await.unwrap().text().await.unwrap(); - - // Check if valid IPv4 address and return early if not - if response.parse::<Ipv4Addr>().is_err() { - return Err(format!("Not valid IPv4 address: {}", response)); - } - - let hops_count = run_tracert(&response)?; - Ok(format!("Counted {} hops to {}", hops_count, response)) -} - -/// Count number of hops in tracert output -fn count_hops(output: &str) -> usize { - // Split the output into lines - let lines: Vec<&str> = output.lines().collect(); - - // Filter lines that appear to represent hops - let hop_lines: Vec<&str> = lines - .iter() - .filter(|&line| line.contains("ms") || line.contains("*")) // TODO check if it contains just the `ms` surrounded by whitespace, otherwise it might falsely pick up some domain names as well - .cloned() - .collect(); - - // Return the number of hops - hop_lines.len() -} - -/// Run `tracert` -fn run_tracert(target_ip: &str) -> Result<usize, String> { - // Ensure valid IPv4 address to avoid prevent command injection - assert!(target_ip.parse::<Ipv4Addr>().is_ok()); - - // Execute the `tracert` command - let output = match std::process::Command::new("tracert") - .arg("-4") // Force IPv4 - .arg("-d") // Prevent resolving intermediate IP addresses - .arg("-w") // Set timeout to 1 second - .arg("1000") - .arg("-h") // Set max hop count - .arg("5") - .arg(target_ip) - .output() - { - Ok(res) => res, - Err(err) => return Err(format!("Failed running tracert: {}", err)), - }; - - // Check if the command was successful - if output.status.success() { - // Convert the output to a string - let stdout = - std::str::from_utf8(&output.stdout).expect("Invalid UTF-8 sequence in command output"); - println!("{}", stdout); - - // Count the number of hops - let hop_count = count_hops(stdout); - Ok(hop_count) - } else { - let stderr = std::str::from_utf8(&output.stderr) - .expect("Invalid UTF-8 sequence in command error output"); - println!("{}", stderr); - Err(format!("Failed collecting tracert output: {}", stderr)) - } -} diff --git a/src-tauri/src/repair_and_verify/mod.rs b/src-tauri/src/repair_and_verify/mod.rs deleted file mode 100644 index 3c861609..00000000 --- a/src-tauri/src/repair_and_verify/mod.rs +++ /dev/null @@ -1,137 +0,0 @@ -use crate::mod_management::{get_enabled_mods, rebuild_enabled_mods_json, set_mod_enabled_status}; -/// Contains various functions to repair common issues and verifying installation -use crate::{constants::CORE_MODS, GameInstall}; - -/// Checks if is valid Titanfall2 install based on certain conditions -#[tauri::command] -pub async fn verify_install_location(game_path: String) -> bool { - match check_is_valid_game_path(&game_path) { - Ok(()) => true, - Err(err) => { - log::warn!("{}", err); - false - } - } -} - -/// 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(()) -} - -/// Verifies Titanfall2 game files -#[tauri::command] -pub fn verify_game_files(game_install: GameInstall) -> Result<String, String> { - dbg!(game_install); - Err("TODO, not yet implemented".to_string()) -} - -/// Disables all mods except core ones -/// Enables core mods if disabled -#[tauri::command] -pub fn disable_all_but_core(game_install: GameInstall) -> Result<(), String> { - // Rebuild `enabledmods.json` first to ensure all mods are added - rebuild_enabled_mods_json(&game_install)?; - - let current_mods = get_enabled_mods(&game_install)?; - - // Disable all mods, set core mods to enabled - for (key, _value) in current_mods.as_object().unwrap() { - if CORE_MODS.contains(&key.as_str()) { - // This is a core mod, we do not want to disable it - set_mod_enabled_status(game_install.clone(), key.to_string(), true)?; - } else { - // Not a core mod - set_mod_enabled_status(game_install.clone(), key.to_string(), false)?; - } - } - - Ok(()) -} - -/// Installs the specified mod -#[tauri::command] -pub async fn clean_up_download_folder_wrapper( - game_install: GameInstall, - force: bool, -) -> Result<(), String> { - match clean_up_download_folder(&game_install, force) { - Ok(()) => Ok(()), - Err(err) => Err(err.to_string()), - } -} - -/// Deletes download folder -/// If `force` is FALSE, bails on non-empty folder -/// If `force` is TRUE, deletes folder even if non-empty -pub fn clean_up_download_folder( - game_install: &GameInstall, - force: bool, -) -> Result<(), anyhow::Error> { - const TEMPORARY_DIRECTORIES: [&str; 4] = [ - "___flightcore-temp-download-dir", - "___flightcore-temp/download-dir", - "___flightcore-temp/extract-dir", - "___flightcore-temp", - ]; - - for directory in TEMPORARY_DIRECTORIES { - // Get download directory - let download_directory = format!("{}/{}/", game_install.game_path, directory); - - // Check if files in folder - let download_dir_contents = match std::fs::read_dir(download_directory.clone()) { - Ok(contents) => contents, - Err(_) => continue, - }; - - let mut count = 0; - download_dir_contents.for_each(|_| count += 1); - - if count > 0 && !force { - // Skip folder if not empty - log::warn!("Folder not empty, not deleting: {directory}"); - continue; - } - - // Delete folder - std::fs::remove_dir_all(download_directory)?; - } - Ok(()) -} - -/// Get list of Northstar logs -#[tauri::command] -pub fn get_log_list(game_install: GameInstall) -> Result<Vec<std::path::PathBuf>, String> { - let ns_log_folder = format!("{}/{}/logs", game_install.game_path, game_install.profile); - - // List files in logs folder - let paths = match std::fs::read_dir(ns_log_folder) { - Ok(paths) => paths, - Err(_err) => return Err("No logs folder found".to_string()), - }; - - // Stores paths of log files - let mut log_files: Vec<std::path::PathBuf> = Vec::new(); - - for path in paths { - let path = path.unwrap().path(); - if path.display().to_string().contains("nslog") { - log_files.push(path); - } - } - - if !log_files.is_empty() { - Ok(log_files) - } else { - Err("No logs found".to_string()) - } -} diff --git a/src-tauri/src/thunderstore/mod.rs b/src-tauri/src/thunderstore/mod.rs deleted file mode 100644 index fc2acb02..00000000 --- a/src-tauri/src/thunderstore/mod.rs +++ /dev/null @@ -1,86 +0,0 @@ -//! For interacting with Thunderstore API -use crate::constants::{APP_USER_AGENT, BLACKLISTED_MODS}; -use serde::{Deserialize, Serialize}; -use std::collections::HashSet; -use ts_rs::TS; - -#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, TS)] -#[ts(export)] -pub struct ThunderstoreMod { - pub name: String, - pub full_name: String, - pub owner: String, - pub package_url: String, - pub date_created: String, - pub date_updated: String, - pub uuid4: String, - pub rating_score: i32, - pub is_pinned: bool, - pub is_deprecated: bool, - pub has_nsfw_content: bool, - pub categories: Vec<String>, - pub versions: Vec<ThunderstoreModVersion>, -} - -#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, TS)] -#[ts(export)] -pub struct ThunderstoreModVersion { - pub name: String, - pub full_name: String, - pub description: String, - pub icon: String, - pub version_number: String, - pub dependencies: Vec<String>, - pub download_url: String, - pub downloads: i32, - pub date_created: String, - pub website_url: String, - pub is_active: bool, - pub uuid4: String, - pub file_size: i64, -} - -/// Performs actual fetch from Thunderstore and returns response -async fn fetch_thunderstore_packages() -> Result<String, reqwest::Error> { - log::info!("Fetching Thunderstore API"); - - // Fetches - let url = "https://northstar.thunderstore.io/api/v1/package/"; - - let client = reqwest::Client::new(); - client - .get(url) - .header(reqwest::header::USER_AGENT, APP_USER_AGENT) - .send() - .await? - .text() - .await -} - -/// Queries Thunderstore packages API -#[tauri::command] -pub async fn query_thunderstore_packages_api() -> Result<Vec<ThunderstoreMod>, String> { - let res = match fetch_thunderstore_packages().await { - Ok(res) => res, - Err(err) => { - let warn_response = format!("Couldn't fetch from Thunderstore: {err}"); - log::warn!("{warn_response}"); - return Err(warn_response); - } - }; - - // Parse response - let parsed_json: Vec<ThunderstoreMod> = match serde_json::from_str(&res) { - Ok(res) => res, - Err(err) => return Err(err.to_string()), - }; - - // Remove some mods from listing - let to_remove_set: HashSet<&str> = BLACKLISTED_MODS.iter().copied().collect(); - let filtered_packages = parsed_json - .into_iter() - .filter(|package| !to_remove_set.contains(&package.full_name.as_ref())) - .collect::<Vec<ThunderstoreMod>>(); - - Ok(filtered_packages) -} diff --git a/src-tauri/src/util.rs b/src-tauri/src/util.rs deleted file mode 100644 index 1d355997..00000000 --- a/src-tauri/src/util.rs +++ /dev/null @@ -1,324 +0,0 @@ -//! This module contains various utility/helper functions that do not fit into any other module - -use anyhow::{Context, Result}; -use serde::{Deserialize, Serialize}; -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<R: tauri::Runtime>(app: tauri::AppHandle<R>) -> Result<(), String> { - app.exit(0); // Close application - Ok(()) -} - -/// Fetches `/client/servers` endpoint from master server -async fn fetch_server_list() -> Result<String, anyhow::Error> { - 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<NorthstarServer> = - 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 -pub fn copy_dir_all( - src: impl AsRef<std::path::Path>, - dst: impl AsRef<std::path::Path>, -) -> 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<std::path::Path>, - dst: impl AsRef<std::path::Path>, -) -> 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 { - let release_candidate_suffix = "-rc"; - - if !version_number.contains(release_candidate_suffix) { - // Not an release-candidate version number, nothing to do, return early - return version_number; - } - - // Version number is guaranteed to contain `-rc` - let re = regex::Regex::new(r"(\d+)\.(\d+)\.(\d+)-rc(\d+)").unwrap(); - if let Some(captures) = re.captures(&version_number) { - // Extract versions - let major_version: u32 = captures[1].parse().unwrap(); - let minor_version: u32 = captures[2].parse().unwrap(); - let patch_version: u32 = captures[3].parse().unwrap(); - let release_candidate: u32 = captures[4].parse().unwrap(); - - // Zero pad - let padded_release_candidate = format!("{:02}", release_candidate); - - // Combine - let combined_patch_version = format!("{}{}", patch_version, padded_release_candidate); - - // Strip leading zeroes - let trimmed_combined_patch_version = combined_patch_version.trim_start_matches('0'); - - // Combine all - let version_number = format!( - "{}.{}.{}", - major_version, minor_version, trimmed_combined_patch_version - ); - return version_number; - } - - // We should never end up here - panic!(); -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_not_release_candidate() { - let input = "1.2.3".to_string(); - let output = convert_release_candidate_number(input.clone()); - let expected_output = input; - assert_eq!(output, expected_output); - } - - #[test] - fn test_basic_release_candidate_number_conversion() { - let input = "1.2.3-rc4".to_string(); - let output = convert_release_candidate_number(input); - let expected_output = "1.2.304"; - assert_eq!(output, expected_output); - } - - #[test] - fn test_leading_zero_release_candidate_number_conversion() { - let input = "1.2.0-rc3".to_string(); - let output = convert_release_candidate_number(input); - let expected_output = "1.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 = "1.19.10-rc1".to_string(); - let output = convert_release_candidate_number(input); - let expected_output = "1.19.1001"; - - assert_eq!(output, expected_output); - } - - #[test] - fn test_double_digit_release_candidate_number_conversion() { - let input = "1.2.3-rc45".to_string(); - let output = convert_release_candidate_number(input); - let expected_output = "1.2.345"; - - assert_eq!(output, expected_output); - } - - #[test] - fn test_double_digit_patch_and_rc_number_conversion() { - let input = "1.2.34-rc56".to_string(); - let output = convert_release_candidate_number(input); - let expected_output = "1.2.3456"; - - assert_eq!(output, expected_output); - } -} |