aboutsummaryrefslogtreecommitdiff
path: root/src-tauri/src
diff options
context:
space:
mode:
Diffstat (limited to 'src-tauri/src')
-rw-r--r--src-tauri/src/constants.rs60
-rw-r--r--src-tauri/src/development/mod.rs84
-rw-r--r--src-tauri/src/github/mod.rs312
-rw-r--r--src-tauri/src/github/pull_requests.rs398
-rw-r--r--src-tauri/src/github/release_notes.rs244
-rw-r--r--src-tauri/src/lib.rs14
-rw-r--r--src-tauri/src/main.rs221
-rw-r--r--src-tauri/src/mod_management/legacy.rs213
-rw-r--r--src-tauri/src/mod_management/mod.rs797
-rw-r--r--src-tauri/src/mod_management/plugins.rs26
-rw-r--r--src-tauri/src/northstar/install.rs358
-rw-r--r--src-tauri/src/northstar/mod.rs276
-rw-r--r--src-tauri/src/northstar/profile.rs121
-rw-r--r--src-tauri/src/platform_specific/linux.rs98
-rw-r--r--src-tauri/src/platform_specific/mod.rs50
-rw-r--r--src-tauri/src/platform_specific/windows.rs104
-rw-r--r--src-tauri/src/repair_and_verify/mod.rs137
-rw-r--r--src-tauri/src/thunderstore/mod.rs86
-rw-r--r--src-tauri/src/util.rs324
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(&current_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);
- }
-}