From 337c4878fdaf267e2e8a0b64591d6eca7bf86e55 Mon Sep 17 00:00:00 2001 From: LynithDev <61880709+LynithDev@users.noreply.github.com> Date: Tue, 19 Mar 2024 17:39:22 +0100 Subject: [PATCH] feat: Finally, a proper client state management --- src-tauri/Cargo.lock | 25 ++++ src-tauri/Cargo.toml | 3 +- src-tauri/build.rs | 12 +- src-tauri/capabilities/migrated.json | 4 +- src-tauri/permissions/game/default.toml | 5 +- src-tauri/src/game/client.rs | 189 ++++++++++++++++-------- src-tauri/src/game/clients/mod.rs | 1 + src-tauri/src/game/clients/vanilla.rs | 49 ++++++ src-tauri/src/game/manifest.rs | 11 -- src-tauri/src/game/mod.rs | 45 +++--- src-tauri/src/main.rs | 3 +- src/types/game.d.ts | 21 ++- src/ui/pages/Browser.tsx | 35 ++++- src/ui/pages/Home.tsx | 53 +------ 14 files changed, 298 insertions(+), 158 deletions(-) create mode 100644 src-tauri/src/game/clients/mod.rs create mode 100644 src-tauri/src/game/clients/vanilla.rs delete mode 100644 src-tauri/src/game/manifest.rs diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 089c164e..3fd871ca 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2377,6 +2377,7 @@ dependencies = [ "tauri-build", "tauri-plugin-http", "tauri-plugin-shell", + "tokio", "uuid", "zip", ] @@ -2970,6 +2971,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + [[package]] name = "simd-adler32" version = "0.3.7" @@ -3649,11 +3659,25 @@ dependencies = [ "libc", "mio", "num_cpus", + "parking_lot", "pin-project-lite", + "signal-hook-registry", "socket2", + "tokio-macros", "windows-sys 0.48.0", ] +[[package]] +name = "tokio-macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + [[package]] name = "tokio-util" version = "0.7.10" @@ -3941,6 +3965,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" dependencies = [ "getrandom 0.2.12", + "serde", ] [[package]] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index dfc69aac..4b40a0bc 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -10,6 +10,7 @@ tauri-build = { version = "2.0.0-beta", features = [] } [dependencies] tauri = { version = "2.0.0-beta", features = [] } +tokio = { version = "1", features = [ "full" ] } serde = { version = "1", features = [ "derive" ] } serde_json = "1" tauri-plugin-shell = "2.0.0-beta.2" @@ -17,7 +18,7 @@ tauri-plugin-http = "2.0.0-beta" flate2 = "1.0.28" tar = "0.4.40" zip = "0.6.6" -uuid = "1.7.0" +uuid = { version = "1.7.0", features = [ "serde" ] } async-trait = "0.1.78" [features] diff --git a/src-tauri/build.rs b/src-tauri/build.rs index ef23dec0..344a5a34 100644 --- a/src-tauri/build.rs +++ b/src-tauri/build.rs @@ -2,8 +2,12 @@ use tauri_build::{Attributes, InlinedPlugin}; fn main() { let _ = tauri_build::try_build(Attributes::new() - .plugin( - "auth", - InlinedPlugin::new().commands(&["login_msa"]) - )); + .plugin("auth", InlinedPlugin::new().commands(&[ + "login_msa" + ])) + .plugin("game", InlinedPlugin::new().commands(&[ + "launch_game", + "set_selected_client" + ])) + ); } diff --git a/src-tauri/capabilities/migrated.json b/src-tauri/capabilities/migrated.json index 3e5f44f6..d24b2891 100644 --- a/src-tauri/capabilities/migrated.json +++ b/src-tauri/capabilities/migrated.json @@ -11,6 +11,8 @@ "window:allow-destroy", "window:allow-minimize", "window:allow-start-dragging", - "event:default" + "event:default", + "auth:default", + "game:default" ] } diff --git a/src-tauri/permissions/game/default.toml b/src-tauri/permissions/game/default.toml index 984948a6..5a36fd10 100644 --- a/src-tauri/permissions/game/default.toml +++ b/src-tauri/permissions/game/default.toml @@ -1,3 +1,6 @@ [default] description = "Default permissions for the plugin" -permissions = [ "launch_game" ] +permissions = [ + "allow-launch-game", + "allow-set-selected-client" +] diff --git a/src-tauri/src/game/client.rs b/src-tauri/src/game/client.rs index f4469117..6cd1bfa2 100644 --- a/src-tauri/src/game/client.rs +++ b/src-tauri/src/game/client.rs @@ -1,81 +1,128 @@ use std::{env, error::Error, fs, path::PathBuf}; +use async_trait::async_trait; +use serde::{Deserialize, Serialize}; use serde_json::Value; -use tauri::{AppHandle, Manager, Runtime}; +use tauri::{AppHandle, Manager}; +use uuid::Uuid; use crate::utils::{file, http}; -use super::{manifest::Manifest, JavaVersion}; +use super::{clients::vanilla::{VanillaClient, VanillaManifest}, JavaVersion}; -#[async_trait::async_trait] +#[macro_export] +macro_rules! create_manifest { + ($manifest_name:ident { $($name:ident: $type:ty),* }) => { + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] + pub struct $manifest_name { + $(pub $name: $type,)* + } + }; +} + +#[macro_export] +macro_rules! create_client { + ($client_name:ident { $($name:ident: $type:ty),* }) => { + #[derive(Debug, Clone)] + pub struct $client_name { + handle: tauri::AppHandle, + details: crate::game::client::GameClientDetails, + $(pub $name: $type,)* + } + }; +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", content = "manifest")] +pub enum GameClientType { + 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 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)), + }) +} + +#[async_trait] pub trait GameClient { + fn new(handle: AppHandle, details: GameClientDetails) -> Self where Self: Sized; + fn get_handle(&self) -> &AppHandle; - fn get_manifest(&self) -> &Manifest; + fn get_details(&self) -> &GameClientDetails; + fn get_client(&self) -> &GameClientType { + &self.get_details().client_type + } + async fn launch(&self) -> Result<(), Box>; async fn install(&self) -> Result<(), Box>; + async fn download_java(&self) -> Result { - download_java(self.get_handle(), self.get_manifest().java_version.to_owned()).await + let config_dir = match self.get_handle().path().app_config_dir() { + Ok(dir) => dir, + Err(_) => return Err(JavaDownloadError::UnsupportedOS) + }; + + let java_dir = config_dir.join("java"); + + if !java_dir.exists() { + match fs::create_dir_all(&java_dir) { + Ok(_) => (), + Err(_) => return Err(JavaDownloadError::PermissionDenied) + }; + } + + 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(JavaDownloadError::UnsupportedOS) + }; + + 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(JavaDownloadError::NoJavaVersionFound) + }; + + return Ok(file.join("bin").join("java")); + } + + Err(JavaDownloadError::NoJavaVersionFound) } } -async fn download_java(handle: &AppHandle, java_version: JavaVersion) -> Result { - let config_dir = match handle.path().app_config_dir() { - Ok(dir) => dir, - Err(_) => return Err(JavaDownloadError::UnsupportedOS) - }; - - let java_dir = config_dir.join("java"); - - if !java_dir.exists() { - match fs::create_dir_all(&java_dir) { - Ok(_) => (), - Err(_) => return Err(JavaDownloadError::PermissionDenied) - }; - } - - // let java_version = &self.get_manifest().java_version; - let os = env::consts::OS; - let archive_type = match os { - "windows" => "zip", - "macos" => "tar.gz", - "linux" => "tar.gz", - _ => return Err(JavaDownloadError::UnsupportedOS) - }; - - 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(JavaDownloadError::NoJavaVersionFound) - }; - - return Ok(file.join("bin").join("java")); - } - - Err(JavaDownloadError::NoJavaVersionFound) -} - -pub fn get_arch() -> String { - let arch = env::consts::ARCH; - match arch { - "x86" => "x86", - "x86_64" => "x64", - "arm" => "aarch32", - "aarch64" => "aarch64", - _ => "unsupported" - }.to_string() +#[async_trait] +pub trait ModdedGameClient: GameClient { + async fn install_mods(&self) -> Result<(), Box>; } #[derive(Debug)] @@ -88,7 +135,18 @@ pub enum JavaDownloadError { DownloadError, } -async fn get_java_versions(java_version: JavaVersion, os: &str, archive_type: &str) -> Result { +fn get_arch() -> 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) -> Result { let url = format!(" https://api.azul.com/metadata/v1/zulu/packages/\ ?java_version={}\ @@ -116,7 +174,8 @@ async fn get_java_versions(java_version: JavaVersion, os: &str, archive_type: &s Ok(response) } -async fn download(java_version: JavaVersion, os: &str, archive_type: &str, archive: &PathBuf) -> Result<(), JavaDownloadError> { + +async fn download(java_version: &JavaVersion, os: &str, archive_type: &str, archive: &PathBuf) -> Result<(), JavaDownloadError> { 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(); diff --git a/src-tauri/src/game/clients/mod.rs b/src-tauri/src/game/clients/mod.rs new file mode 100644 index 00000000..8ceb50c3 --- /dev/null +++ b/src-tauri/src/game/clients/mod.rs @@ -0,0 +1 @@ +pub mod vanilla; \ No newline at end of file diff --git a/src-tauri/src/game/clients/vanilla.rs b/src-tauri/src/game/clients/vanilla.rs new file mode 100644 index 00000000..8878a277 --- /dev/null +++ b/src-tauri/src/game/clients/vanilla.rs @@ -0,0 +1,49 @@ +use std::error::Error; + +use async_trait::async_trait; +use tauri::AppHandle; +use tokio::io::{stdout, AsyncWriteExt}; + +use crate::{create_client, create_manifest, game::client::{GameClient, GameClientDetails, GameClientType}}; + +create_manifest!(VanillaManifest {}); +create_client!(VanillaClient {}); + +#[async_trait] +impl GameClient for VanillaClient { + fn new(handle: AppHandle, details: GameClientDetails) -> Self { + Self { + handle: handle.clone(), + details + } + } + + fn get_handle(&self) -> &AppHandle { + &self.handle + } + + fn get_details(&self) -> &GameClientDetails { + &self.details + } + + fn get_client(&self) -> &GameClientType { + &self.get_details().client_type + } + + async fn install(&self) -> Result<(), Box> { + println!("Installing Vanilla client"); + tokio::time::sleep(std::time::Duration::from_secs(5)).await; + println!("Vanilla client installed"); + 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(()) + } +} \ No newline at end of file diff --git a/src-tauri/src/game/manifest.rs b/src-tauri/src/game/manifest.rs deleted file mode 100644 index 6ac3c99e..00000000 --- a/src-tauri/src/game/manifest.rs +++ /dev/null @@ -1,11 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use super::JavaVersion; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Manifest { - pub name: String, - pub version: String, - pub main_class: String, - pub java_version: JavaVersion, -} \ No newline at end of file diff --git a/src-tauri/src/game/mod.rs b/src-tauri/src/game/mod.rs index 7be43599..4690f61e 100644 --- a/src-tauri/src/game/mod.rs +++ b/src-tauri/src/game/mod.rs @@ -1,12 +1,14 @@ -use std::sync::Mutex; - use serde::{Deserialize, Serialize}; use tauri::{plugin::TauriPlugin, Manager}; +use tokio::sync::Mutex; + +use self::client::get_game_client; pub mod client; -pub mod manifest; +mod clients; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub enum JavaVersion { V8, V16, @@ -29,7 +31,7 @@ pub struct GameManagerState { } unsafe impl Send for GameManagerState {} - +unsafe impl Sync for GameManagerState {} impl Default for GameManagerState { fn default() -> Self { GameManagerState { @@ -38,31 +40,38 @@ impl Default for GameManagerState { } } -pub fn init() -> TauriPlugin { +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 + launch_game, + set_selected_client ]) .build() } -enum LaunchGameError { - NoGameSelected +#[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 res = selected_client.launch().await; + match res { + Ok(_) => Ok(()), + Err(e) => Err(format!("An error has occurred: {}", e)) + } } #[tauri::command] -async fn launch_game(app: tauri::AppHandle) -> Result<(), u8> { - let state = app.state::>(); - let state = state.lock().unwrap(); - match state.selected_client { - Some(ref client) => println!("Launching game: {:?}", client.get_manifest().name), - None => println!("No game selected") - } - - Err(LaunchGameError::NoGameSelected as u8) +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(()) } \ No newline at end of file diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index fb95bf10..cbc0f085 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -3,13 +3,14 @@ use std::error::Error; -use polyfrost_launcher::auth; +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!()) diff --git a/src/types/game.d.ts b/src/types/game.d.ts index 0dd620b7..e2c5713f 100644 --- a/src/types/game.d.ts +++ b/src/types/game.d.ts @@ -1,3 +1,22 @@ declare namespace game { - export type Client = 'Fabric' | 'Forge' | 'Quilt' | 'Vanilla' | 'Other'; + export interface GameClientDetails { + uuid: Uuid; + name: string; + version: string; + main_class: string; + java_version: JavaVersion; + startup_args: Vec; + client_type: { + type: keyof GameClientType; + manifest: GameClientType[keyof GameClientType]; + }; + } + + export interface GameClientType { + Vanilla: VanillaManifest; + } + + export type ClientType = keyof GameClientType; + + export interface VanillaManifest {} } diff --git a/src/ui/pages/Browser.tsx b/src/ui/pages/Browser.tsx index fd5006f9..502ab3fc 100644 --- a/src/ui/pages/Browser.tsx +++ b/src/ui/pages/Browser.tsx @@ -1,15 +1,40 @@ import { invoke } from '@tauri-apps/api/core'; +import * as uuid from 'uuid'; import Button from '../components/base/Button'; function BrowserPage() { - function downloadJava() { - invoke('download_java_test'); - } - return (

Browser

- + + +
); } diff --git a/src/ui/pages/Home.tsx b/src/ui/pages/Home.tsx index 1c0bc016..5133c06a 100644 --- a/src/ui/pages/Home.tsx +++ b/src/ui/pages/Home.tsx @@ -14,7 +14,6 @@ import Button from '../components/base/Button'; import Tag from '../components/base/Tag'; import TextField from '../components/base/TextField'; import defaultCover from '../../assets/images/default_instance_cover.jpg'; -import { upperFirst } from '../../utils/string'; // TODO: Replace this into it's own component function OneConfigLogo() { @@ -62,7 +61,7 @@ function Banner() { interface InstanceCardProps { name: string; version: string; - client: game.Client; + clientType: game.ClientType; id: number; mods?: number; cover?: string; @@ -100,7 +99,7 @@ function InstanceCard(props: InstanceCardProps) {

{props.name}

- {props.client} + {props.clientType} {' '} {props.version} {' '} @@ -167,56 +166,10 @@ function HomePage() { const myInstances: InstanceCardProps[] = [ { name: 'Hypixel', - client: 'Vanilla', + clientType: 'Vanilla', version: '1.8.9', id: 1, }, - { - name: 'OneConfig ??', - client: 'Forge', - version: '1.8.9', - mods: 12, - id: 2, - }, - { - name: 'Fabulously Optimised', - client: 'Fabric', - version: '1.20.4', - mods: 48, - id: 3, - }, - { - name: 'sus client', - client: 'Other', - version: '1.8.9', - id: 4, - }, - { - name: 'Hypixel', - client: 'Vanilla', - version: '1.8.9', - id: 5, - }, - { - name: 'OneConfig ??', - client: 'Forge', - version: '1.8.9', - mods: 12, - id: 6, - }, - { - name: 'Fabulously Optimised', - client: 'Fabric', - version: '1.20.4', - mods: 48, - id: 7, - }, - { - name: 'sus client', - client: 'Other', - version: '1.8.9', - id: 8, - }, ]; return (