diff --git a/crates/prisma-cli/src/bin/prisma.rs b/crates/prisma-cli/src/bin/prisma.rs index 8bdc6c44..96e11d24 100644 --- a/crates/prisma-cli/src/bin/prisma.rs +++ b/crates/prisma-cli/src/bin/prisma.rs @@ -1,3 +1,3 @@ fn main() { - prisma_client_rust_cli::run(); -} \ No newline at end of file + prisma_client_rust_cli::run(); +} diff --git a/crates/prisma/src/lib.rs b/crates/prisma/src/lib.rs index a4041c78..1fac3e75 100644 --- a/crates/prisma/src/lib.rs +++ b/crates/prisma/src/lib.rs @@ -1,2 +1,2 @@ #[allow(warnings, unused)] -pub mod prisma; \ No newline at end of file +pub mod prisma; diff --git a/package.json b/package.json index 76f0ea4c..0296a8a8 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "serve": "vite preview", "tauri": "tauri", "lint": "eslint . --cache", - "format": "pnpm lint --fix", + "format": "pnpm lint --fix && cargo fmt", "prisma": "cd src-tauri && cargo prisma" }, "dependencies": { diff --git a/src-tauri/build.rs b/src-tauri/build.rs index 344a5a34..86edf9eb 100644 --- a/src-tauri/build.rs +++ b/src-tauri/build.rs @@ -1,13 +1,12 @@ use tauri_build::{Attributes, InlinedPlugin}; fn main() { - let _ = tauri_build::try_build(Attributes::new() - .plugin("auth", InlinedPlugin::new().commands(&[ - "login_msa" - ])) - .plugin("game", InlinedPlugin::new().commands(&[ - "launch_game", - "set_selected_client" - ])) - ); + let _ = tauri_build::try_build( + Attributes::new() + .plugin("auth", InlinedPlugin::new().commands(&["login_msa"])) + .plugin( + "game", + InlinedPlugin::new().commands(&["launch_game", "set_selected_client"]), + ), + ); } diff --git a/src-tauri/prisma/migrations/migration_lock.toml b/src-tauri/prisma/migrations/migration_lock.toml index e5e5c470..6fcf33da 100644 --- a/src-tauri/prisma/migrations/migration_lock.toml +++ b/src-tauri/prisma/migrations/migration_lock.toml @@ -1,3 +1,3 @@ # Please do not edit this file manually # It should be added in your version-control system (i.e. Git) -provider = "sqlite" \ No newline at end of file +provider = "sqlite" diff --git a/src-tauri/src/auth/microsoft_auth.rs b/src-tauri/src/auth/microsoft_auth.rs index de21ec29..c4e6cc7a 100644 --- a/src-tauri/src/auth/microsoft_auth.rs +++ b/src-tauri/src/auth/microsoft_auth.rs @@ -1,4 +1,7 @@ -use std::{io::{Read, Write}, net::{SocketAddr, TcpListener, TcpStream}}; +use std::{ + io::{Read, Write}, + net::{SocketAddr, TcpListener, TcpStream}, +}; use serde_json::json; use tauri::{AppHandle, Runtime}; @@ -14,197 +17,246 @@ use super::{Account, AuthenticationError, AuthenticationMethod}; #[derive(Debug, Error)] pub enum MicrosoftAuthError { - #[error("failed to get Minecraft access token")] - MSAccessToken, - #[error("failed to get XSTS token")] - XSTSToken, - #[error("failed to get XBL token")] - XBLToken, - #[error("failed to get XBL user hash")] - XBLUserHash, - #[error("this account doesn't have an xbox account, please sign in through minecraft.net and try again")] - NoXBoxAccount, - #[error("this account is from a country where xbox live is not available")] - XBoxLiveBlocked, - #[error("this account needs adult verification on the xbox page")] - AdultVerification, - #[error("this account is under 18, please add this account to a family by an adult and try again")] - ChildAccount, - #[error("unknown xbox live error: {0}")] - UnknownError(u64) + #[error("failed to get Minecraft access token")] + MSAccessToken, + #[error("failed to get XSTS token")] + XSTSToken, + #[error("failed to get XBL token")] + XBLToken, + #[error("failed to get XBL user hash")] + XBLUserHash, + #[error("this account doesn't have an xbox account, sign in through minecraft.net and try again")] + NoXBoxAccount, + #[error("this account is from a country where xbox live is not available")] + XBoxLiveBlocked, + #[error("this account needs adult verification on the xbox page")] + AdultVerification, + #[error("this account is a child, ask an adult to add this account to a Family")] + ChildAccount, + #[error("unknown xbox live error: {0}")] + UnknownError(u64), } - pub(super) struct MicrosoftAuthenticationMethod; impl AuthenticationMethod for MicrosoftAuthenticationMethod { - async fn auth(handle: &AppHandle, stage: F) -> PolyResult - where F: Fn(String, u8, bool) -> () { - - stage("Authenticating with Microsoft".into(), 0, false); - let msa_code: String = msa_code(handle).await?; + async fn auth(handle: &AppHandle, stage: F) -> PolyResult + where + F: Fn(String, u8, bool) -> (), + { + stage("Authenticating with Microsoft".into(), 0, false); + let msa_code: String = msa_code(handle).await?; - stage("Authenticating with Microsoft".into(), 1, false); - let msa_token: String = msa_code_to_token(msa_code).await?; + stage("Authenticating with Microsoft".into(), 1, false); + let msa_token: String = msa_code_to_token(msa_code).await?; - stage("Authenticating with Xbox Live".into(), 2, false); - let (xbl_token, user_hash): (String, String) = auth_xbl(msa_token).await?; + stage("Authenticating with Xbox Live".into(), 2, false); + let (xbl_token, user_hash): (String, String) = auth_xbl(msa_token).await?; - stage("Retrieving XSTS token".into(), 3, false); - let xsts_token: String = auth_xsts(xbl_token).await?; + stage("Retrieving XSTS token".into(), 3, false); + let xsts_token: String = auth_xsts(xbl_token).await?; - stage("Authenticating with Minecraft".into(), 4, false); - let access_token: String = auth_minecraft(xsts_token, user_hash).await?; + stage("Authenticating with Minecraft".into(), 4, false); + let access_token: String = auth_minecraft(xsts_token, user_hash).await?; - stage("Retrieving Minecraft profile".into(), 5, true); - let account: Account = Self::get_profile(access_token).await?; + stage("Retrieving Minecraft profile".into(), 5, true); + let account: Account = Self::get_profile(access_token).await?; - Ok(account) - } + Ok(account) + } } async fn auth_minecraft(xsts_token: String, user_hash: String) -> PolyResult { - let response = create_client()? - .post("https://api.minecraftservices.com/authentication/login_with_xbox") - .json(&json!({ - "identityToken": format!("XBL3.0 x={};{}", user_hash, xsts_token) - })) - .send() - .await?; + let response = create_client()? + .post("https://api.minecraftservices.com/authentication/login_with_xbox") + .json(&json!({ + "identityToken": format!("XBL3.0 x={};{}", user_hash, xsts_token) + })) + .send() + .await?; - let response = response.json::().await?; - let access_token = match response.get("access_token") { - Some(token) => token.as_str().unwrap(), - None => return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(MicrosoftAuthError::MSAccessToken))) - }; + let response = response.json::().await?; + let access_token = match response.get("access_token") { + Some(token) => token.as_str().unwrap(), + None => { + return Err(PolyError::AuthError(AuthenticationError::MicrosoftError( + MicrosoftAuthError::MSAccessToken, + ))) + } + }; - Ok(access_token.to_string()) + Ok(access_token.to_string()) } async fn auth_xsts(token: String) -> PolyResult { - let response = create_client()? - .post("https://xsts.auth.xboxlive.com/xsts/authorize") - .json(&json!({ - "Properties": { - "SandboxId": "RETAIL", - "UserTokens": [ - token - ] - }, - "RelyingParty": "rp://api.minecraftservices.com/", - "TokenType": "JWT" - })) - .send() - .await?; + let response = create_client()? + .post("https://xsts.auth.xboxlive.com/xsts/authorize") + .json(&json!({ + "Properties": { + "SandboxId": "RETAIL", + "UserTokens": [ + token + ] + }, + "RelyingParty": "rp://api.minecraftservices.com/", + "TokenType": "JWT" + })) + .send() + .await?; - if response.status().as_u16() == 401 { - let error_code = response.json::().await?["XErr"].as_u64().unwrap(); + if response.status().as_u16() == 401 { + let error_code = response.json::().await?["XErr"] + .as_u64() + .unwrap(); - match error_code { - 2148916233 => return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(MicrosoftAuthError::NoXBoxAccount))), - 2148916235 => return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(MicrosoftAuthError::XBoxLiveBlocked))), - 2148916236 | 2148916237 => return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(MicrosoftAuthError::AdultVerification))), - 2148916238 => return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(MicrosoftAuthError::ChildAccount))), - _ => return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(MicrosoftAuthError::UnknownError(error_code)))), - } - } + match error_code { + 2148916233 => { + return Err(PolyError::AuthError(AuthenticationError::MicrosoftError( + MicrosoftAuthError::NoXBoxAccount, + ))) + } + 2148916235 => { + return Err(PolyError::AuthError(AuthenticationError::MicrosoftError( + MicrosoftAuthError::XBoxLiveBlocked, + ))) + } + 2148916236 | 2148916237 => { + return Err(PolyError::AuthError(AuthenticationError::MicrosoftError( + MicrosoftAuthError::AdultVerification, + ))) + } + 2148916238 => { + return Err(PolyError::AuthError(AuthenticationError::MicrosoftError( + MicrosoftAuthError::ChildAccount, + ))) + } + _ => { + return Err(PolyError::AuthError(AuthenticationError::MicrosoftError( + MicrosoftAuthError::UnknownError(error_code), + ))) + } + } + } - let response = response.json::().await?; - let xsts_token = match response.get("Token") { - Some(token) => token.as_str().unwrap(), - None => return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(MicrosoftAuthError::XSTSToken))) - }; + let response = response.json::().await?; + let xsts_token = match response.get("Token") { + Some(token) => token.as_str().unwrap(), + None => { + return Err(PolyError::AuthError(AuthenticationError::MicrosoftError( + MicrosoftAuthError::XSTSToken, + ))) + } + }; - Ok(xsts_token.to_string()) + Ok(xsts_token.to_string()) } async fn auth_xbl(code: String) -> PolyResult<(String, String)> { - let response = create_client()? - .post("https://user.auth.xboxlive.com/user/authenticate") - .json(&json!({ - "Properties": { - "AuthMethod": "RPS", - "SiteName": "user.auth.xboxlive.com", - "RpsTicket": &format!("d={}", code) - }, - "RelyingParty": "http://auth.xboxlive.com", - "TokenType": "JWT" - })) - .send() - .await?; + let response = create_client()? + .post("https://user.auth.xboxlive.com/user/authenticate") + .json(&json!({ + "Properties": { + "AuthMethod": "RPS", + "SiteName": "user.auth.xboxlive.com", + "RpsTicket": &format!("d={}", code) + }, + "RelyingParty": "http://auth.xboxlive.com", + "TokenType": "JWT" + })) + .send() + .await?; - let response = response.json::().await?; - let xbl_token = match response.get("Token") { - Some(token) => token.as_str().unwrap(), - None => return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(MicrosoftAuthError::XBLToken))) - }; + let response = response.json::().await?; + let xbl_token = match response.get("Token") { + Some(token) => token.as_str().unwrap(), + None => { + return Err(PolyError::AuthError(AuthenticationError::MicrosoftError( + MicrosoftAuthError::XBLToken, + ))) + } + }; - let user_hash = match response.get("DisplayClaims").and_then(|claims| claims.get("xui").and_then(|xui| xui.get(0).and_then(|xui| xui.get("uhs")))) { - Some(hash) => hash.as_str().unwrap(), - None => return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(MicrosoftAuthError::XBLUserHash))) - }; + let user_hash = match response.get("DisplayClaims").and_then(|claims| { + claims + .get("xui") + .and_then(|xui| xui.get(0).and_then(|xui| xui.get("uhs"))) + }) { + Some(hash) => hash.as_str().unwrap(), + None => { + return Err(PolyError::AuthError(AuthenticationError::MicrosoftError( + MicrosoftAuthError::XBLUserHash, + ))) + } + }; - Ok((xbl_token.to_string(), user_hash.to_string())) + Ok((xbl_token.to_string(), user_hash.to_string())) } async fn msa_code_to_token(code: String) -> PolyResult { - let token = create_client()? - .post("https://login.live.com/oauth20_token.srf") - .form(&[ - ("client_id", CLIENT_ID), - ("redirect_uri", &format!("http://localhost:{}/", PORT)), - ("code", &code), - ("grant_type", "authorization_code") - ]) - .send() - .await? - .json::() - .await?; + let token = create_client()? + .post("https://login.live.com/oauth20_token.srf") + .form(&[ + ("client_id", CLIENT_ID), + ("redirect_uri", &format!("http://localhost:{}/", PORT)), + ("code", &code), + ("grant_type", "authorization_code"), + ]) + .send() + .await? + .json::() + .await?; - Ok(token["access_token"].as_str().unwrap().to_string()) + Ok(token["access_token"].as_str().unwrap().to_string()) } const PORT: u16 = 13523; async fn msa_code(handle: &AppHandle) -> PolyResult { - let url = format!( + let url = format!( "https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?client_id={}&response_type=code&redirect_uri=http://localhost:{}/&response_mode=query&scope=XboxLive.signin%20offline_access&prompt=consent", - CLIENT_ID, + CLIENT_ID, PORT ); - handle.shell().open(url, None)?; + handle.shell().open(url, None)?; - let mut token: String = String::new(); + let mut token: String = String::new(); - let listener = TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], PORT)))?; - println!("Started local server for MSA on port {}", PORT); + let listener = TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], PORT)))?; + println!("Started local server for MSA on port {}", PORT); - for conn in listener.incoming() { - match conn { - Err(err) => return Err(err.into()), - Ok(stream) => { - if let Some(code) = handle_connection(stream) { - token = code; - break; - } - } - } - } + for conn in listener.incoming() { + match conn { + Err(err) => return Err(err.into()), + Ok(stream) => { + if let Some(code) = handle_connection(stream) { + token = code; + break; + } + } + } + } - Ok(token) + Ok(token) } fn handle_connection(mut stream: TcpStream) -> Option { - let mut buffer = [0; 512]; - if let Err(err) = stream.read(&mut buffer) { - eprintln!("Failed to read from stream: {}", err); - return None; - } + let mut buffer = [0; 512]; + if let Err(err) = stream.read(&mut buffer) { + eprintln!("Failed to read from stream: {}", err); + return None; + } - let request = String::from_utf8_lossy(&buffer[..]); - let url = request.split_whitespace().nth(1)?; - let code = url.split('?').nth(1)?.split('&').find(|s| s.starts_with("code="))?.split_once('=').unwrap().1; - - let response = r" + let request = String::from_utf8_lossy(&buffer[..]); + let url = request.split_whitespace().nth(1)?; + let code = url + .split('?') + .nth(1)? + .split('&') + .find(|s| s.starts_with("code="))? + .split_once('=') + .unwrap() + .1; + + let response = r" @@ -239,14 +291,16 @@ fn handle_connection(mut stream: TcpStream) -> Option { "; - stream.write_all( - format!( - "HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n{}", - response.len(), - response - ) - .as_bytes(), - ).unwrap(); + stream + .write_all( + format!( + "HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n{}", + response.len(), + response + ) + .as_bytes(), + ) + .unwrap(); - Some(code.to_string()) + Some(code.to_string()) } diff --git a/src-tauri/src/auth/mod.rs b/src-tauri/src/auth/mod.rs index 27b9c2e8..b2c72e28 100644 --- a/src-tauri/src/auth/mod.rs +++ b/src-tauri/src/auth/mod.rs @@ -1,8 +1,8 @@ use anyhow::anyhow; -use thiserror::Error; use serde::{Deserialize, Serialize}; use tauri::{plugin::TauriPlugin, AppHandle, Manager, Runtime}; use tauri_plugin_http::reqwest::Client; +use thiserror::Error; use crate::{PolyError, PolyResult}; @@ -12,64 +12,65 @@ mod microsoft_auth; #[derive(Error, Debug)] pub enum AuthenticationError { - #[error(transparent)] - MicrosoftError(#[from] microsoft_auth::MicrosoftAuthError) + #[error(transparent)] + MicrosoftError(#[from] microsoft_auth::MicrosoftAuthError), } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Account { - pub username: String, - pub uuid: String, - pub skins: Vec, + pub username: String, + pub uuid: String, + pub skins: Vec, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct AccountSkin { - pub id: String, - pub state: String, - pub url: String, - pub variant: String, + pub id: String, + pub state: String, + pub url: String, + pub variant: String, } #[allow(async_fn_in_trait)] pub trait AuthenticationMethod { - /// Authenticate with a given method. Stage is a function that takes a string and a u8. - /// The string is the message to display to the user, and the u8 is the progress of the authentication. - async fn auth(handle: &AppHandle, stage: F) -> PolyResult - where F: Fn(String, u8, bool) -> (); + /// Authenticate with a given method. Stage is a function that takes a string and a u8. + /// The string is the message to display to the user, and the u8 is the progress of the authentication. + async fn auth(handle: &AppHandle, stage: F) -> PolyResult + where + F: Fn(String, u8, bool) -> (); - async fn get_profile(access_token: String) -> PolyResult { - let response = Client::new() - .get("https://api.minecraftservices.com/minecraft/profile") - .header("Authorization", format!("Bearer {}", access_token)) - .send() - .await?; - - let response = response.json::().await?; - if let Some(error) = response.get("error") { - return Err(PolyError::AnyhowError(anyhow!(error.to_string()))); - } - - let account = serde_json::from_value::(response)?; - Ok(account) - } + async fn get_profile(access_token: String) -> PolyResult { + let response = Client::new() + .get("https://api.minecraftservices.com/minecraft/profile") + .header("Authorization", format!("Bearer {}", access_token)) + .send() + .await?; + + let response = response.json::().await?; + if let Some(error) = response.get("error") { + return Err(PolyError::AnyhowError(anyhow!(error.to_string()))); + } + + let account = serde_json::from_value::(response)?; + Ok(account) + } } // Plugin for MSA pub fn init() -> TauriPlugin { - tauri::plugin::Builder::new("auth") - .invoke_handler(tauri::generate_handler![ - login_msa - ]) - .build() + tauri::plugin::Builder::new("auth") + .invoke_handler(tauri::generate_handler![login_msa]) + .build() } #[tauri::command] async fn login_msa(app: tauri::AppHandle) -> Result { - match MicrosoftAuthenticationMethod::auth(&app, |status, stage, was_last| { - let _ = app.emit("msa:status", (status, stage, was_last)); - }).await { - Ok(account) => Ok(account), - Err(err) => Err(err.to_string()) - } + match MicrosoftAuthenticationMethod::auth(&app, |status, stage, was_last| { + let _ = app.emit("msa:status", (status, stage, was_last)); + }) + .await + { + Ok(account) => Ok(account), + Err(err) => Err(err.to_string()), + } } diff --git a/src-tauri/src/game/client.rs b/src-tauri/src/game/client.rs index 945baab8..2c86081e 100644 --- a/src-tauri/src/game/client.rs +++ b/src-tauri/src/game/client.rs @@ -7,9 +7,15 @@ use tauri::{AppHandle, Manager}; use thiserror::Error; use uuid::Uuid; -use crate::{utils::{file, http}, PolyError, PolyResult}; +use crate::{ + utils::{file, http}, + PolyError, PolyResult, +}; -use super::{clients::vanilla::{vanilla_manifest, VanillaClient, VanillaManifest}, JavaVersion}; +use super::{ + clients::vanilla::{vanilla_manifest, VanillaClient, VanillaManifest}, + JavaVersion, +}; #[macro_export] macro_rules! create_manifest { @@ -36,178 +42,228 @@ macro_rules! create_client { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type", content = "manifest")] pub enum GameClientType { - Vanilla(VanillaManifest), - // Forge(ForgeManifest), + Vanilla(VanillaManifest), + // Forge(ForgeManifest), } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GameClientDetails { - pub uuid: Uuid, - pub name: String, - pub version: String, - pub main_class: String, - pub java_version: JavaVersion, - pub startup_args: Vec, - pub client_type: GameClientType, + pub uuid: Uuid, + pub name: String, + pub version: String, + pub main_class: String, + pub java_version: JavaVersion, + pub startup_args: Vec, + pub client_type: GameClientType, } pub fn get_game_client(handle: AppHandle, details: GameClientDetails) -> Box { - Box::new(match details.client_type { - GameClientType::Vanilla(_) => VanillaClient::new(handle, details), - // GameClientType::Forge(_) => Box::new(ForgeClient::new(handle, details)), - }) + Box::new(match details.client_type { + GameClientType::Vanilla(_) => VanillaClient::new(handle, details), + // GameClientType::Forge(_) => Box::new(ForgeClient::new(handle, details)), + }) } #[async_trait] pub trait GameClient: Send + Sync { - fn new(handle: AppHandle, details: GameClientDetails) -> Self where Self: Sized; + fn new(handle: AppHandle, details: GameClientDetails) -> Self + where + Self: Sized; - async fn get_all_version_urls() -> PolyResult> where Self: Sized; - async fn get_minecraft_manifest(version: String) -> PolyResult where Self: Sized; + async fn get_all_version_urls() -> PolyResult> + where + Self: Sized; + async fn get_minecraft_manifest( + version: String, + ) -> PolyResult + where + Self: Sized; - // async fn get_details_from_version(version: String) -> Result> where Self: Sized; + // async fn get_details_from_version(version: String) -> Result> where Self: Sized; - fn get_handle(&self) -> &AppHandle; - fn get_details(&self) -> &GameClientDetails; - fn get_client(&self) -> &GameClientType { - &self.get_details().client_type - } - - async fn launch(&self) -> PolyResult<()>; - async fn setup(&self) -> PolyResult<()>; + fn get_handle(&self) -> &AppHandle; + fn get_details(&self) -> &GameClientDetails; + fn get_client(&self) -> &GameClientType { + &self.get_details().client_type + } - async fn download_java(&self) -> PolyResult { - let config_dir = match self.get_handle().path().app_config_dir() { - Ok(dir) => dir, - // SURELY THERES A BETTER WAY TO DO THIS IDK LOLL - Err(err) => return Err(PolyError::JavaError(JavaDownloadError::UnsupportedOS(err.to_string()))) - }; - - let java_dir = config_dir.join("java"); - - if !java_dir.exists() { - match fs::create_dir_all(&java_dir) { - Ok(_) => (), - Err(err) => return Err(PolyError::JavaError(JavaDownloadError::PermissionDenied(err.to_string()))) - }; - } - - let java_version = &self.get_details().java_version; - let os = env::consts::OS; - let archive_type = match os { - "windows" => "zip", - "macos" => "tar.gz", - "linux" => "tar.gz", - _ => return Err(PolyError::JavaError(JavaDownloadError::UnsupportedOS(os.to_string()))) - }; - - let archive_name = format!("zulu-{}.{}", java_version.to_string(), archive_type); - let archive = java_dir.join(&archive_name); - let dest = java_dir.join(format!("zulu-{}-{}", java_version.to_string(), get_arch())); - - if archive.exists() && dest.exists() { - let _ = fs::remove_file(archive.as_path()); - } else if archive.exists() && !dest.exists() { - extract(&archive, &dest)?; - } else if !archive.exists() && !dest.exists() { - download(java_version, os, archive_type, &archive).await?; - extract(&archive, &dest)?; - } - - if let Ok(mut files) = fs::read_dir(&dest) { - let file = match files.nth(0) { - Some(file) => file.unwrap().path(), - None => return Err(PolyError::JavaError(JavaDownloadError::NoJavaVersionFound("unable to get the java executable file".to_string()))) - }; - - return Ok(file.join("bin").join("java")); - } - - Err(PolyError::JavaError(JavaDownloadError::NoJavaVersionFound("unable to download the java executable file".to_string()))) - } + async fn launch(&self) -> PolyResult<()>; + async fn setup(&self) -> PolyResult<()>; + + async fn download_java(&self) -> PolyResult { + let config_dir = match self.get_handle().path().app_config_dir() { + Ok(dir) => dir, + // SURELY THERES A BETTER WAY TO DO THIS IDK LOLL + Err(err) => { + return Err(PolyError::JavaError(JavaDownloadError::UnsupportedOS( + err.to_string(), + ))) + } + }; + + let java_dir = config_dir.join("java"); + + if !java_dir.exists() { + match fs::create_dir_all(&java_dir) { + Ok(_) => (), + Err(err) => { + return Err(PolyError::JavaError(JavaDownloadError::PermissionDenied( + err.to_string(), + ))) + } + }; + } + + let java_version = &self.get_details().java_version; + let os = env::consts::OS; + let archive_type = match os { + "windows" => "zip", + "macos" => "tar.gz", + "linux" => "tar.gz", + _ => { + return Err(PolyError::JavaError(JavaDownloadError::UnsupportedOS( + os.to_string(), + ))) + } + }; + + let archive_name = format!("zulu-{}.{}", java_version.to_string(), archive_type); + let archive = java_dir.join(&archive_name); + let dest = java_dir.join(format!("zulu-{}-{}", java_version.to_string(), get_arch())); + + if archive.exists() && dest.exists() { + let _ = fs::remove_file(archive.as_path()); + } else if archive.exists() && !dest.exists() { + extract(&archive, &dest)?; + } else if !archive.exists() && !dest.exists() { + download(java_version, os, archive_type, &archive).await?; + extract(&archive, &dest)?; + } + + if let Ok(mut files) = fs::read_dir(&dest) { + let file = match files.nth(0) { + Some(file) => file.unwrap().path(), + None => { + return Err(PolyError::JavaError(JavaDownloadError::NoJavaVersionFound( + "unable to get the java executable file".to_string(), + ))) + } + }; + + return Ok(file.join("bin").join("java")); + } + + Err(PolyError::JavaError(JavaDownloadError::NoJavaVersionFound( + "unable to download the java executable file".to_string(), + ))) + } } #[async_trait] pub trait ModdedGameClient: GameClient { - async fn install_mods(&self) -> PolyResult<()>; + async fn install_mods(&self) -> PolyResult<()>; } #[derive(Debug, Error)] pub enum JavaDownloadError { - #[error("failed to find a valid java version: {0}")] - NoJavaVersionFound(String), - #[error("failed creating a java directory (try running at an elevated permission): {0}")] - PermissionDenied(String), - #[error("unable to match your operating system ({0}) with a valid java version")] - UnsupportedOS(String), - #[error("unable to match your architecture ({0}) with a valid java version")] - UnsupportedArch(String), - #[error("failed to extract the java executable: {0}")] - ExtractError(String), - #[error("failed to download the java executable")] - DownloadError(String), + #[error("failed to find a valid java version: {0}")] + NoJavaVersionFound(String), + #[error("failed creating a java directory (try running at an elevated permission): {0}")] + PermissionDenied(String), + #[error("unable to match your operating system ({0}) with a valid java version")] + UnsupportedOS(String), + #[error("unable to match your architecture ({0}) with a valid java version")] + UnsupportedArch(String), + #[error("failed to extract the java executable: {0}")] + ExtractError(String), + #[error("failed to download the java executable")] + DownloadError(String), } fn get_arch() -> String { - let arch = env::consts::ARCH; - match arch { - "x86" => "x86", - "x86_64" => "x64", - "arm" => "aarch32", - "aarch64" => "aarch64", - _ => "unsupported" - }.to_string() + let arch = env::consts::ARCH; + match arch { + "x86" => "x86", + "x86_64" => "x64", + "arm" => "aarch32", + "aarch64" => "aarch64", + _ => "unsupported", + } + .to_string() } -async fn get_java_versions(java_version: &JavaVersion, os: &str, archive_type: &str) -> PolyResult { - let url = format!(" +async fn get_java_versions( + java_version: &JavaVersion, + os: &str, + archive_type: &str, +) -> PolyResult { + let url = format!( + " https://api.azul.com/metadata/v1/zulu/packages/\ ?java_version={}\ &os={}\ &arch={}\ &archive_type={}\ - &java_package_type=jre&javafx_bundled=true&release_status=ga&latest=true", - java_version.to_string(), - os, - get_arch(), - archive_type - ); + &java_package_type=jre&javafx_bundled=true&release_status=ga&latest=true", + java_version.to_string(), + os, + get_arch(), + archive_type + ); - let response = match http::create_client()?.get(&url).send().await { - Ok(response) => match response.json::().await { - Ok(json) => json, - Err(err) => return Err(PolyError::JavaError(JavaDownloadError::NoJavaVersionFound(err.to_string()))) - }, - Err(err) => return Err(PolyError::JavaError(JavaDownloadError::NoJavaVersionFound(err.to_string()))) - }; + let response = match http::create_client()?.get(&url).send().await { + Ok(response) => match response.json::().await { + Ok(json) => json, + Err(err) => { + return Err(PolyError::JavaError(JavaDownloadError::NoJavaVersionFound( + err.to_string(), + ))) + } + }, + Err(err) => { + return Err(PolyError::JavaError(JavaDownloadError::NoJavaVersionFound( + err.to_string(), + ))) + } + }; - if !response.is_array() || response.as_array().unwrap().is_empty() { - return Err(PolyError::JavaError(JavaDownloadError::NoJavaVersionFound("didn't get the expected java api result".to_string()))); - } + if !response.is_array() || response.as_array().unwrap().is_empty() { + return Err(PolyError::JavaError(JavaDownloadError::NoJavaVersionFound( + "didn't get the expected java api result".to_string(), + ))); + } - Ok(response) + Ok(response) } -async fn download(java_version: &JavaVersion, os: &str, archive_type: &str, archive: &PathBuf) -> PolyResult<()> { - let response = get_java_versions(java_version, os, archive_type).await?; - let latest = response.as_array().unwrap().first().unwrap(); - let download_url = latest.get("download_url").unwrap().as_str().unwrap(); +async fn download( + java_version: &JavaVersion, + os: &str, + archive_type: &str, + archive: &PathBuf, +) -> PolyResult<()> { + let response = get_java_versions(java_version, os, archive_type).await?; + let latest = response.as_array().unwrap().first().unwrap(); + let download_url = latest.get("download_url").unwrap().as_str().unwrap(); - if let Err(err) = http::download_file(download_url, archive.as_path()).await { - // eprintln!("{}", err); - return Err(PolyError::JavaError(JavaDownloadError::DownloadError(err.to_string()))); - }; - - Ok(()) + if let Err(err) = http::download_file(download_url, archive.as_path()).await { + // eprintln!("{}", err); + return Err(PolyError::JavaError(JavaDownloadError::DownloadError( + err.to_string(), + ))); + }; + + Ok(()) } fn extract(archive: &PathBuf, dest: &PathBuf) -> PolyResult<()> { - if let Err(err) = file::extract_archive(archive.as_path(), dest.as_path()) { - // eprintln!("{}", err); - let _ = fs::remove_file(dest.as_path()); - return Err(PolyError::JavaError(JavaDownloadError::ExtractError(err.to_string()))); - } + if let Err(err) = file::extract_archive(archive.as_path(), dest.as_path()) { + let _ = fs::remove_file(dest.as_path()); + return Err(PolyError::JavaError(JavaDownloadError::ExtractError( + err.to_string(), + ))); + } - let _ = fs::remove_file(archive.as_path()); - Ok(()) + let _ = fs::remove_file(archive.as_path()); + Ok(()) } diff --git a/src-tauri/src/game/clients/mod.rs b/src-tauri/src/game/clients/mod.rs index 8ceb50c3..f0e7b584 100644 --- a/src-tauri/src/game/clients/mod.rs +++ b/src-tauri/src/game/clients/mod.rs @@ -1 +1 @@ -pub mod vanilla; \ No newline at end of file +pub mod vanilla; diff --git a/src-tauri/src/game/clients/vanilla.rs b/src-tauri/src/game/clients/vanilla.rs index c0522d32..754e1d3d 100644 --- a/src-tauri/src/game/clients/vanilla.rs +++ b/src-tauri/src/game/clients/vanilla.rs @@ -6,7 +6,12 @@ use serde_json::Value; use tauri::{AppHandle, Manager}; use tokio::io::{stdout, AsyncWriteExt}; -use crate::{create_client, create_manifest, game::client::{GameClient, GameClientDetails, GameClientType}, utils::http, PolyError, PolyResult}; +use crate::{ + create_client, create_manifest, + game::client::{GameClient, GameClientDetails, GameClientType}, + utils::http, + PolyError, PolyResult, +}; create_manifest!(VanillaManifest { // minecraft: vanilla_manifest::MinecraftManifest @@ -16,303 +21,336 @@ create_client!(VanillaClient {}); #[async_trait] impl GameClient for VanillaClient { - fn new(handle: AppHandle, details: GameClientDetails) -> Self { - Self { - handle: handle.clone(), - details - } - } + fn new(handle: AppHandle, details: GameClientDetails) -> Self { + Self { + handle: handle.clone(), + details, + } + } - async fn get_all_version_urls() -> PolyResult> { - const URL: &str = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json"; - let request = http::create_client()?.get(URL).send().await.map_err(|err| PolyError::HTTPError(err))?; + async fn get_all_version_urls() -> PolyResult> { + const URL: &str = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json"; + let request = http::create_client()? + .get(URL) + .send() + .await + .map_err(|err| PolyError::HTTPError(err))?; - let json = request.json::().await.map_err(|err| PolyError::HTTPError(err))?; + let json = request + .json::() + .await + .map_err(|err| PolyError::HTTPError(err))?; - let versions = json.get("versions").ok_or(PolyError::AnyhowError(anyhow!("failed to get versions object")))? - .as_array().ok_or(PolyError::AnyhowError(anyhow!("invalid versions object")))?; + let versions = json + .get("versions") + .ok_or(PolyError::AnyhowError(anyhow!( + "failed to get versions object" + )))? + .as_array() + .ok_or(PolyError::AnyhowError(anyhow!("invalid versions object")))?; - let mut map = HashMap::new(); + let mut map = HashMap::new(); - for version in versions { - let version = version.as_object().ok_or("Invalid version object")?; + for version in versions { + let version = version.as_object().ok_or("Invalid version object")?; - let id = version.get("id").ok_or("No id object")? - .as_str().ok_or("Invalid id object")?; - let url = version.get("url").ok_or("No url object")? - .as_str().ok_or("Invalid url object")?; + let id = version + .get("id") + .ok_or("No id object")? + .as_str() + .ok_or("Invalid id object")?; + let url = version + .get("url") + .ok_or("No url object")? + .as_str() + .ok_or("Invalid url object")?; - map.insert(id.to_string(), url.to_string()); - } + map.insert(id.to_string(), url.to_string()); + } - Ok(map) - } - - async fn get_minecraft_manifest(version_url: String) -> Result> { - let request = http::create_client().get(version_url).send().await?; + Ok(map) + } - match request.json::().await { - Ok(manifest) => Ok(manifest), - Err(e) => Err(e.into()) - } - } + async fn get_minecraft_manifest( + version_url: String, + ) -> Result> { + let request = http::create_client().get(version_url).send().await?; - fn get_handle(&self) -> &AppHandle { - &self.handle - } + match request.json::().await { + Ok(manifest) => Ok(manifest), + Err(e) => Err(e.into()), + } + } - fn get_details(&self) -> &GameClientDetails { - &self.details - } + fn get_handle(&self) -> &AppHandle { + &self.handle + } - fn get_client(&self) -> &GameClientType { - &self.get_details().client_type - } + fn get_details(&self) -> &GameClientDetails { + &self.details + } - async fn setup(&self) -> Result<(), Box> { - let libraries_dir = self.get_handle().path().app_config_dir()?.join("libraries"); - let versions = Self::get_all_version_urls().await?; - let version = versions.get(&self.get_details().version).ok_or("Version not found")?.to_owned(); - let manifest = Self::get_minecraft_manifest(version).await?; + fn get_client(&self) -> &GameClientType { + &self.get_details().client_type + } - let libraries = vanilla_impl::setup_libraries(&manifest.libraries, &libraries_dir).await?; - println!("{:#?}", libraries); - // vanilla_impl::setup_natives().await?; - // vanilla_impl::setup_assets().await?; - Ok(()) - } + async fn setup(&self) -> Result<(), Box> { + let libraries_dir = self.get_handle().path().app_config_dir()?.join("libraries"); + let versions = Self::get_all_version_urls().await?; + let version = versions + .get(&self.get_details().version) + .ok_or("Version not found")? + .to_owned(); + let manifest = Self::get_minecraft_manifest(version).await?; - async fn launch(&self) -> Result<(), Box> { - println!("Launching Vanilla client"); - tokio::time::sleep(std::time::Duration::from_secs(5)).await; - println!("Vanilla client launched"); - tokio::time::sleep(std::time::Duration::from_secs(5)).await; - println!("Vanilla client exited"); - stdout().flush().await?; // why... - Ok(()) - } + let libraries = vanilla_impl::setup_libraries(&manifest.libraries, &libraries_dir).await?; + println!("{:#?}", libraries); + // vanilla_impl::setup_natives().await?; + // vanilla_impl::setup_assets().await?; + Ok(()) + } + + async fn launch(&self) -> Result<(), Box> { + println!("Launching Vanilla client"); + tokio::time::sleep(std::time::Duration::from_secs(5)).await; + println!("Vanilla client launched"); + tokio::time::sleep(std::time::Duration::from_secs(5)).await; + println!("Vanilla client exited"); + stdout().flush().await?; // why... + Ok(()) + } } pub mod vanilla_impl { - use std::{error::Error, fs, path::PathBuf}; + use std::{error::Error, fs, path::PathBuf}; - use serde_json::{Map, Value}; - use tauri::AppHandle; + use serde_json::{Map, Value}; + use tauri::AppHandle; - use crate::utils::http::download_file; + use crate::utils::http::download_file; - use super::vanilla_manifest::{self, Library}; + use super::vanilla_manifest::{self, Library}; - pub async fn setup_libraries(libraries: &Vec, libraries_folder: &PathBuf) -> Result, Box> { - let mut natives_ret: Vec<&Library> = vec![]; - let mut libraries_ret: Vec = vec![]; + pub async fn setup_libraries( + libraries: &Vec, + libraries_folder: &PathBuf, + ) -> Result, Box> { + let mut natives_ret: Vec<&Library> = vec![]; + let mut libraries_ret: Vec = vec![]; - for library in libraries { - // let library: &Map = library.as_object().unwrap(); - - // if let Some(_) = library.get("natives") { - // natives_ret.push(library); - // continue; - // } - - // let artifact = library.get("downloads").ok_or("No downloads object")? - // .as_object().ok_or("Invalid downloads object")? - // .get("artifact").ok_or("No artifact object")? - // .as_object().ok_or("Invalid artifact object")?; + for library in libraries { + // let library: &Map = library.as_object().unwrap(); - // let path = artifact.get("path").ok_or("No path object")? - // .as_str().ok_or("Invalid path object")?; - // let url = artifact.get("url").ok_or("No url object")? - // .as_str().ok_or("Invalid url object")?; + // if let Some(_) = library.get("natives") { + // natives_ret.push(library); + // continue; + // } - if let Some(_) = library.natives { - natives_ret.push(library); - continue; - } + // let artifact = library.get("downloads").ok_or("No downloads object")? + // .as_object().ok_or("Invalid downloads object")? + // .get("artifact").ok_or("No artifact object")? + // .as_object().ok_or("Invalid artifact object")?; - let artifact = library.downloads.artifact.clone().ok_or("No artifact object")?; - let path = artifact.path; - let url = artifact.url; - - // TODO: Add checks for rules + platform - - let dest = libraries_folder.join(path); - fs::create_dir_all(dest.parent().ok_or("Couldn't get library parent")?)?; + // let path = artifact.get("path").ok_or("No path object")? + // .as_str().ok_or("Invalid path object")?; + // let url = artifact.get("url").ok_or("No url object")? + // .as_str().ok_or("Invalid url object")?; - if !dest.exists() { - download_file(url.as_str(), &dest).await?; - } + if let Some(_) = library.natives { + natives_ret.push(library); + continue; + } - libraries_ret.push(dest.to_str().ok_or("Couldn't get library path")?.to_string()); - } + let artifact = library + .downloads + .artifact + .clone() + .ok_or("No artifact object")?; + let path = artifact.path; + let url = artifact.url; - Ok(libraries_ret) - } + // TODO: Add checks for rules + platform - pub async fn setup_natives() -> Result<(), Box> { - Ok(()) - } + let dest = libraries_folder.join(path); + fs::create_dir_all(dest.parent().ok_or("Couldn't get library parent")?)?; - pub async fn setup_assets() -> Result<(), Box> { - Ok(()) - } + if !dest.exists() { + download_file(url.as_str(), &dest).await?; + } + + libraries_ret.push( + dest.to_str() + .ok_or("Couldn't get library path")? + .to_string(), + ); + } + + Ok(libraries_ret) + } + + pub async fn setup_natives() -> Result<(), Box> { + Ok(()) + } + + pub async fn setup_assets() -> Result<(), Box> { + Ok(()) + } } // todo move to prisma pub mod vanilla_manifest { - use serde::Deserialize; - use serde::Serialize; + use serde::Deserialize; + use serde::Serialize; - #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct MinecraftManifest { - pub asset_index: AssetIndex, - pub assets: String, - pub compliance_level: i64, - pub downloads: Downloads, - pub id: String, - pub java_version: JavaVersion, - pub libraries: Vec, - pub logging: Logging, - pub main_class: String, - pub minecraft_arguments: String, - pub minimum_launcher_version: i64, - pub release_time: String, - pub time: String, + #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct MinecraftManifest { + pub asset_index: AssetIndex, + pub assets: String, + pub compliance_level: i64, + pub downloads: Downloads, + pub id: String, + pub java_version: JavaVersion, + pub libraries: Vec, + pub logging: Logging, + pub main_class: String, + pub minecraft_arguments: String, + pub minimum_launcher_version: i64, + pub release_time: String, + pub time: String, - #[serde(rename = "type")] - pub type_field: String, - } + #[serde(rename = "type")] + pub type_field: String, + } - #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct AssetIndex { - pub id: String, - pub sha1: String, - pub size: i64, - pub total_size: i64, - pub url: String, - } + #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct AssetIndex { + pub id: String, + pub sha1: String, + pub size: i64, + pub total_size: i64, + pub url: String, + } - #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] - pub struct Downloads { - pub client: Client, - } + #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct Downloads { + pub client: Client, + } - #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] - pub struct Client { - pub sha1: String, - pub size: i64, - pub url: String, - } + #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct Client { + pub sha1: String, + pub size: i64, + pub url: String, + } - #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct JavaVersion { - pub component: String, - pub major_version: i64, - } + #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct JavaVersion { + pub component: String, + pub major_version: i64, + } - #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct Library { - pub downloads: Downloads2, - pub name: String, - #[serde(default)] - pub rules: Vec, - pub extract: Option, - pub natives: Option, - } + #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct Library { + pub downloads: Downloads2, + pub name: String, + #[serde(default)] + pub rules: Vec, + pub extract: Option, + pub natives: Option, + } - #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct Downloads2 { - pub artifact: Option, - pub classifiers: Option, - } + #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct Downloads2 { + pub artifact: Option, + pub classifiers: Option, + } - #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct Artifact { - pub path: String, - pub sha1: String, - pub size: i64, - pub url: String, - } + #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct Artifact { + pub path: String, + pub sha1: String, + pub size: i64, + pub url: String, + } - #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct Classifiers { - #[serde(rename = "natives-windows-32")] - pub natives_windows_32: Option, - #[serde(rename = "natives-windows-64")] - pub natives_windows_64: Option, - #[serde(rename = "natives-osx")] - pub natives_osx: Option, - #[serde(rename = "natives-linux")] - pub natives_linux: Option, - #[serde(rename = "natives-windows")] - pub natives_windows: Option, - } + #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct Classifiers { + #[serde(rename = "natives-windows-32")] + pub natives_windows_32: Option, + #[serde(rename = "natives-windows-64")] + pub natives_windows_64: Option, + #[serde(rename = "natives-osx")] + pub natives_osx: Option, + #[serde(rename = "natives-linux")] + pub natives_linux: Option, + #[serde(rename = "natives-windows")] + pub natives_windows: Option, + } - #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct NativeLibrary { - pub path: String, - pub sha1: String, - pub size: i64, - pub url: String, - } + #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct NativeLibrary { + pub path: String, + pub sha1: String, + pub size: i64, + pub url: String, + } - #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct Rule { - pub action: String, - pub os: Option, - } + #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct Rule { + pub action: String, + pub os: Option, + } - #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct Os { - pub name: String, - } + #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct Os { + pub name: String, + } - #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct Extract { - pub exclude: Vec, - } + #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct Extract { + pub exclude: Vec, + } - #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct Natives { - pub windows: String, - pub linux: Option, - pub osx: Option, - } + #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct Natives { + pub windows: String, + pub linux: Option, + pub osx: Option, + } - #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct Logging { - pub client: Client2, - } + #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct Logging { + pub client: Client2, + } - #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct Client2 { - pub argument: String, - pub file: File, - #[serde(rename = "type")] - pub type_field: String, - } - - #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct File { - pub id: String, - pub sha1: String, - pub size: i64, - pub url: String, - } + #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct Client2 { + pub argument: String, + pub file: File, + #[serde(rename = "type")] + pub type_field: String, + } + #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct File { + pub id: String, + pub sha1: String, + pub size: i64, + pub url: String, + } } diff --git a/src-tauri/src/game/mod.rs b/src-tauri/src/game/mod.rs index b44b1d52..80b79954 100644 --- a/src-tauri/src/game/mod.rs +++ b/src-tauri/src/game/mod.rs @@ -11,72 +11,73 @@ mod clients; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub enum JavaVersion { - V8, - V16, - V17 + V8, + V16, + V17, } impl ToString for JavaVersion { - fn to_string(&self) -> String { - match self { - JavaVersion::V8 => "8".to_string(), - JavaVersion::V16 => "16".to_string(), - JavaVersion::V17 => "17".to_string() - } - } + fn to_string(&self) -> String { + match self { + JavaVersion::V8 => "8".to_string(), + JavaVersion::V16 => "16".to_string(), + JavaVersion::V17 => "17".to_string(), + } + } } // Tauri plugin for the game module pub struct GameManagerState { - pub selected_client: Option>, + pub selected_client: Option>, } unsafe impl Send for GameManagerState {} unsafe impl Sync for GameManagerState {} impl Default for GameManagerState { - fn default() -> Self { - GameManagerState { - selected_client: None - } - } + fn default() -> Self { + GameManagerState { + selected_client: None, + } + } } pub fn init() -> TauriPlugin { - tauri::plugin::Builder::new("game") - .setup(|app, _| { - app.manage(Mutex::new(GameManagerState::default())); - Ok(()) - }) - .invoke_handler(tauri::generate_handler![ - launch_game, - set_selected_client - ]) - .build() + tauri::plugin::Builder::new("game") + .setup(|app, _| { + app.manage(Mutex::new(GameManagerState::default())); + Ok(()) + }) + .invoke_handler(tauri::generate_handler![launch_game, set_selected_client]) + .build() } #[tauri::command] async fn launch_game(state: tauri::State<'_, Mutex>) -> Result<(), String> { - let state = state.lock().await; - let selected_client = match &state.selected_client { - Some(client) => client, - None => return Err("No client selected".into()) - }; + let state = state.lock().await; + let selected_client = match &state.selected_client { + Some(client) => client, + None => return Err("No client selected".into()), + }; - if let Err(err) = selected_client.setup().await { - return Err(format!("An error has occurred during install: {}", err)); - } + if let Err(err) = selected_client.setup().await { + return Err(format!("An error has occurred during install: {}", err)); + } - match selected_client.launch().await { - Ok(_) => Ok(()), - Err(e) => Err(format!("An error has occurred: {}", e)) - } + match selected_client.launch().await { + Ok(_) => Ok(()), + Err(e) => Err(format!("An error has occurred: {}", e)), + } } // TODO: Implement setting client by UUID, as currently this is creating a new client and setting that #[tauri::command] -async fn set_selected_client(handle: tauri::AppHandle, state: tauri::State<'_, Mutex>, details: client::GameClientDetails) -> Result<(), String> { - let mut state = state.lock().await; - let client = get_game_client(handle, details); - state.selected_client = Some(client); - Ok(()) +async fn set_selected_client( + handle: tauri::AppHandle, + state: tauri::State<'_, Mutex>, + details: client::GameClientDetails, +) -> Result<(), String> { + let mut state = state.lock().await; + let client = get_game_client(handle, details); + state.selected_client = Some(client); + Ok(()) } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index aa80bfb4..032c83cf 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,58 +1,58 @@ use thiserror::Error; pub mod auth; +pub mod constants; pub mod game; pub mod utils; -pub mod constants; /// A standardized [`Error`] type that is used across the launcher #[derive(Debug, Error)] pub enum PolyError { - #[error("a tauri runtime error occured (this should not happen): {0}")] - /// Wrapper around [`tauri::Error`] to handle expected Tauri errors. - TauriError(#[from] tauri::Error), - #[error("failed to manage shell task: {0}")] - /// Wrapper around [`tauri_plugin_shell::Error`] to handle shell errors. - TauriShellError(#[from] tauri_plugin_shell::Error), - #[error("failed to manage a tokio semaphore: {0}")] - /// Wrapper around [`tokio::sync::AcquireError`] to handle sempahore errors. - TokioError(#[from] tokio::sync::AcquireError), - #[error("failed to compress a file with flate: {0}")] - /// Wrapper around [`flate2::CompressError`] to handle flate compression errors. - FlateError(#[from] flate2::CompressError), - // im actually the funniest person alive dont @ me - #[error("failed to decompress a file with flate: {0}")] - /// Wrapper around [`flate2::DecompressError`] to handle flate decompression errors. - DeflateError(#[from] flate2::DecompressError), - #[error("failed to parse uuids: {0}")] - /// Wrapper around [`uuid::Error`] to handle UUID parsing errors. - UUIDError(#[from] uuid::Error), - #[error("failed to manage serialization of json: {0}")] - /// Wrapper around [`serde_json::Error`] to handle Serde JSON parsing errors. - SerdeError(#[from] serde_json::Error), - #[error("failed to manage writing and reading of files: {0}")] - /// Wrapper around [`std::io::Error`] only to be used if a more defined error isn't available (e.g. when using `zip`) - IOError(#[from] std::io::Error), - #[error("failed to query the database: {0}")] - /// Wrapper around [`prisma_client_rust::QueryError`] to handle DB query errors. - DBQueryError(#[from] prisma_client_rust::QueryError), - #[error("failed to establish a connection to the database: {0}")] - /// Wrapper around [`prisma_client_rust::NewClientError`] to handle errors occuring while programmatically connecting to Prisma. - DBConnectionError(#[from] prisma_client_rust::NewClientError), - #[error("failed to establish a HTTP connection: {0}")] - /// Wrapper around [`reqwest::Error`] to handle HTTP errors. - HTTPError(#[from] tauri_plugin_http::reqwest::Error), - #[error("failed to download java: {0}")] - /// Wrapper around [`game::client::JavaDownloadError`] to handle Java executable downloading. - JavaError(#[from] game::client::JavaDownloadError), - /// Wrapper around [`zip::result::ZipError`] to handle zip errors. - #[error("failed to manage zip files: {0}")] - ZipError(#[from] zip::result::ZipError), - #[error("failed to authenticate: {0}")] - // Wrapper around [`auth::AuthenticationError`] to handle authentication errors. - AuthError(#[from] auth::AuthenticationError), - #[error(transparent)] - AnyhowError(#[from] anyhow::Error), + #[error("a tauri runtime error occured (this should not happen): {0}")] + /// Wrapper around [`tauri::Error`] to handle expected Tauri errors. + TauriError(#[from] tauri::Error), + #[error("failed to manage shell task: {0}")] + /// Wrapper around [`tauri_plugin_shell::Error`] to handle shell errors. + TauriShellError(#[from] tauri_plugin_shell::Error), + #[error("failed to manage a tokio semaphore: {0}")] + /// Wrapper around [`tokio::sync::AcquireError`] to handle sempahore errors. + TokioError(#[from] tokio::sync::AcquireError), + #[error("failed to compress a file with flate: {0}")] + /// Wrapper around [`flate2::CompressError`] to handle flate compression errors. + FlateError(#[from] flate2::CompressError), + // im actually the funniest person alive dont @ me + #[error("failed to decompress a file with flate: {0}")] + /// Wrapper around [`flate2::DecompressError`] to handle flate decompression errors. + DeflateError(#[from] flate2::DecompressError), + #[error("failed to parse uuids: {0}")] + /// Wrapper around [`uuid::Error`] to handle UUID parsing errors. + UUIDError(#[from] uuid::Error), + #[error("failed to manage serialization of json: {0}")] + /// Wrapper around [`serde_json::Error`] to handle Serde JSON parsing errors. + SerdeError(#[from] serde_json::Error), + #[error("failed to manage writing and reading of files: {0}")] + /// Wrapper around [`std::io::Error`] only to be used if a more defined error isn't available (e.g. when using `zip`) + IOError(#[from] std::io::Error), + #[error("failed to query the database: {0}")] + /// Wrapper around [`prisma_client_rust::QueryError`] to handle DB query errors. + DBQueryError(#[from] prisma_client_rust::QueryError), + #[error("failed to establish a connection to the database: {0}")] + /// Wrapper around [`prisma_client_rust::NewClientError`] to handle errors occuring while programmatically connecting to Prisma. + DBConnectionError(#[from] prisma_client_rust::NewClientError), + #[error("failed to establish a HTTP connection: {0}")] + /// Wrapper around [`reqwest::Error`] to handle HTTP errors. + HTTPError(#[from] tauri_plugin_http::reqwest::Error), + #[error("failed to download java: {0}")] + /// Wrapper around [`game::client::JavaDownloadError`] to handle Java executable downloading. + JavaError(#[from] game::client::JavaDownloadError), + /// Wrapper around [`zip::result::ZipError`] to handle zip errors. + #[error("failed to manage zip files: {0}")] + ZipError(#[from] zip::result::ZipError), + #[error("failed to authenticate: {0}")] + // Wrapper around [`auth::AuthenticationError`] to handle authentication errors. + AuthError(#[from] auth::AuthenticationError), + #[error(transparent)] + AnyhowError(#[from] anyhow::Error), } /// Alias for a [`Result`] with the error type [`PolyError`]. diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index cbc0f085..7113181e 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -7,16 +7,16 @@ use polyfrost_launcher::{auth, game}; use tauri::{menu::Menu, App}; fn main() { - tauri::Builder::default() - .plugin(tauri_plugin_shell::init()) - .plugin(auth::init()) - .plugin(game::init()) - .menu(|handle| Menu::new(handle)) - .setup(setup) - .run(tauri::generate_context!()) - .expect("error while running tauri application"); + tauri::Builder::default() + .plugin(tauri_plugin_shell::init()) + .plugin(auth::init()) + .plugin(game::init()) + .menu(|handle| Menu::new(handle)) + .setup(setup) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); } fn setup(_: &mut App) -> Result<(), Box> { - Ok(()) + Ok(()) } diff --git a/src-tauri/src/utils/file.rs b/src-tauri/src/utils/file.rs index 78451a7b..aed2019c 100644 --- a/src-tauri/src/utils/file.rs +++ b/src-tauri/src/utils/file.rs @@ -1,33 +1,41 @@ -use std::{fs::File, path::Path}; use anyhow::anyhow; +use std::{fs::File, path::Path}; use crate::{PolyError, PolyResult}; pub fn extract_archive(archive: &Path, dest: &Path) -> PolyResult<()> { - let ext = match archive.extension() { - Some(ext) => ext.to_str().unwrap(), - None => return Err(PolyError::AnyhowError(anyhow!("unsupported operating system"))) - }; + let ext = match archive.extension() { + Some(ext) => ext.to_str().unwrap(), + None => { + return Err(PolyError::AnyhowError(anyhow!( + "unsupported operating system" + ))) + } + }; - match ext { - "zip" => extract_zip(archive, dest), - "gz" => extract_tar_gz(archive, dest), - _ => Err(PolyError::AnyhowError(anyhow!("unsupported file extension {:?}", ext))) - } + match ext { + "zip" => extract_zip(archive, dest), + "gz" => extract_tar_gz(archive, dest), + _ => Err(PolyError::AnyhowError(anyhow!( + "unsupported file extension {:?}", + ext + ))), + } } pub fn extract_zip(archive: &Path, dest: &Path) -> PolyResult<()> { - let file = File::open(archive).map_err(|err| PolyError::IOError(err))?; - let mut archive = zip::ZipArchive::new(file) - .map_err(|err| PolyError::ZipError(err)); - archive?.extract(dest).map_err(|err| PolyError::ZipError(err)); - Ok(()) + let file = File::open(archive).map_err(|err| PolyError::IOError(err))?; + let mut archive = zip::ZipArchive::new(file).map_err(|err| PolyError::ZipError(err)); + archive? + .extract(dest) + .map_err(|err| PolyError::ZipError(err)); + Ok(()) } pub fn extract_tar_gz(archive: &Path, dest: &Path) -> PolyResult<()> { - let file = File::open(archive).map_err(|err| PolyError::IOError(err))?; - let tar_gz = flate2::read::GzDecoder::new(file); - let mut archive = tar::Archive::new(tar_gz); - archive.unpack(dest).map_err(|err| PolyError::IOError(err)); - Ok(()) + let file = File::open(archive).map_err(|err| PolyError::IOError(err))?; + let tar_gz = flate2::read::GzDecoder::new(file); + let mut archive = tar::Archive::new(tar_gz); + archive.unpack(dest).map_err(|err| PolyError::IOError(err)); + Ok(()) } diff --git a/src-tauri/src/utils/http.rs b/src-tauri/src/utils/http.rs index dbd6f4ca..e9082c1a 100644 --- a/src-tauri/src/utils/http.rs +++ b/src-tauri/src/utils/http.rs @@ -1,20 +1,34 @@ -use std::{fs::File, io::{self, Cursor}, path::Path}; +use std::{ + fs::File, + io::{self, Cursor}, + path::Path, +}; use tauri_plugin_http::reqwest::{Client, ClientBuilder}; use crate::{constants::USER_AGENT, PolyError, PolyResult}; pub fn create_client() -> PolyResult { - ClientBuilder::new() - .user_agent(USER_AGENT) - .build().map_err(|err| PolyError::HTTPError(err)) + ClientBuilder::new() + .user_agent(USER_AGENT) + .build() + .map_err(|err| PolyError::HTTPError(err)) } // can we not make mapping the errors automatic, isnt that the whole point pub async fn download_file(url: &str, path: &Path) -> PolyResult<()> { - let response = create_client()?.get(url).send().await.map_err(|err| PolyError::HTTPError(err))?; - let mut file = File::create(path).map_err(|err| PolyError::IOError(err))?; - let mut content = Cursor::new(response.bytes().await.map_err(|err| PolyError::HTTPError(err))?); - io::copy(&mut content, &mut file).map_err(|err| PolyError::IOError(err)); - Ok(()) + let response = create_client()? + .get(url) + .send() + .await + .map_err(|err| PolyError::HTTPError(err))?; + let mut file = File::create(path).map_err(|err| PolyError::IOError(err))?; + let mut content = Cursor::new( + response + .bytes() + .await + .map_err(|err| PolyError::HTTPError(err))?, + ); + io::copy(&mut content, &mut file).map_err(|err| PolyError::IOError(err)); + Ok(()) } diff --git a/src-tauri/src/utils/mod.rs b/src-tauri/src/utils/mod.rs index a1c407b2..ac34b973 100644 --- a/src-tauri/src/utils/mod.rs +++ b/src-tauri/src/utils/mod.rs @@ -1,2 +1,2 @@ +pub mod file; pub mod http; -pub mod file; \ No newline at end of file