feat: Finally, a proper client state management

This commit is contained in:
LynithDev 2024-03-19 17:39:22 +01:00
parent af9c628612
commit 337c4878fd
14 changed files with 298 additions and 158 deletions

25
src-tauri/Cargo.lock generated
View file

@ -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]]

View file

@ -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]

View file

@ -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"
]))
);
}

View file

@ -11,6 +11,8 @@
"window:allow-destroy",
"window:allow-minimize",
"window:allow-start-dragging",
"event:default"
"event:default",
"auth:default",
"game:default"
]
}

View file

@ -1,3 +1,6 @@
[default]
description = "Default permissions for the plugin"
permissions = [ "launch_game" ]
permissions = [
"allow-launch-game",
"allow-set-selected-client"
]

View file

@ -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<String>,
pub client_type: GameClientType,
}
pub fn get_game_client(handle: AppHandle, details: GameClientDetails) -> Box<dyn GameClient> {
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<dyn Error>>;
async fn install(&self) -> Result<(), Box<dyn Error>>;
async fn download_java(&self) -> Result<PathBuf, JavaDownloadError> {
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<R: Runtime>(handle: &AppHandle<R>, java_version: JavaVersion) -> Result<PathBuf, JavaDownloadError> {
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<dyn Error>>;
}
#[derive(Debug)]
@ -88,7 +135,18 @@ pub enum JavaDownloadError {
DownloadError,
}
async fn get_java_versions(java_version: JavaVersion, os: &str, archive_type: &str) -> Result<Value, JavaDownloadError> {
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<Value, JavaDownloadError> {
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();

View file

@ -0,0 +1 @@
pub mod vanilla;

View file

@ -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<dyn Error>> {
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<dyn Error>> {
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(())
}
}

View file

@ -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,
}

View file

@ -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<R: tauri::Runtime>() -> TauriPlugin<R> {
pub fn init() -> TauriPlugin<tauri::Wry> {
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<GameManagerState>>) -> 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<R: tauri::Runtime>(app: tauri::AppHandle<R>) -> Result<(), u8> {
let state = app.state::<Mutex<GameManagerState>>();
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<GameManagerState>>, details: client::GameClientDetails) -> Result<(), String> {
let mut state = state.lock().await;
let client = get_game_client(handle, details);
state.selected_client = Some(client);
Ok(())
}

View file

@ -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!())

21
src/types/game.d.ts vendored
View file

@ -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<string>;
client_type: {
type: keyof GameClientType;
manifest: GameClientType[keyof GameClientType];
};
}
export interface GameClientType {
Vanilla: VanillaManifest;
}
export type ClientType = keyof GameClientType;
export interface VanillaManifest {}
}

View file

@ -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 (
<div class="flex flex-col gap-y-4">
<h1>Browser</h1>
<Button onClick={() => downloadJava()}>Download java</Button>
<Button
onClick={async () => {
const value = await invoke('plugin:game|launch_game');
console.log(`hello${value}`);
}}
>
Test Button
</Button>
<Button
onClick={() => {
invoke('plugin:game|set_selected_client', {
details: {
uuid: uuid.v4(),
name: 'my vanilla instance',
version: '1.8.9',
main_class: 'net.minecraft.client.main.Main',
java_version: 'v8',
startup_args: ['--username', 'player', '--password', 'password'],
client_type: {
type: 'Vanilla',
manifest: {},
},
} as game.GameClientDetails,
});
}}
>
Test Button2
</Button>
</div>
);
}

View file

@ -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) {
<div class="flex flex-col gap-1.5">
<p class="h-4 font-semibold">{props.name}</p>
<p class="h-4 text-xs">
{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 (