refactor: Way cleaner version + instance management system
This commit is contained in:
parent
629a9c2a44
commit
ffab61ebf0
26 changed files with 574 additions and 877 deletions
|
|
@ -1,2 +0,0 @@
|
|||
[alias]
|
||||
prisma = "run -p prisma-cli --bin prisma --"
|
||||
11
Cargo.toml
11
Cargo.toml
|
|
@ -1,9 +1,6 @@
|
|||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"src-tauri",
|
||||
"crates/*",
|
||||
]
|
||||
members = [ "src-tauri", ]
|
||||
|
||||
[workspace.package]
|
||||
license = "AGPL-3.0-only"
|
||||
|
|
@ -20,12 +17,10 @@ tauri-build = { version = "2.0.0-beta", features = [] }
|
|||
tauri-plugin-shell = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
tauri-plugin-http = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
|
||||
prisma-client-rust = { git = "https://github.com/Brendonovich/prisma-client-rust", tag = "0.6.11", default-features = false, features = [ "sqlite" ] }
|
||||
prisma-client-rust-cli = { git = "https://github.com/Brendonovich/prisma-client-rust", tag = "0.6.11", default-features = false, features = [ "sqlite" ] }
|
||||
|
||||
uuid = { version = "1.8", features = [ "serde" ] }
|
||||
serde = { version = "1", features = [ "derive" ] }
|
||||
tokio = { version = "1", features = [ "full" ] }
|
||||
serde = { version = "1", features = [ "derive" ] }
|
||||
chrono = { version = "0.4", features = [ "serde" ] }
|
||||
|
||||
serde_json = { version = "1" }
|
||||
flate2 = { version = "1" }
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
[package]
|
||||
name = "prisma-cli"
|
||||
version = "0.1.0"
|
||||
license = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
documentation = { workspace = true }
|
||||
readme = { workspace = true }
|
||||
homepage = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
prisma-client-rust-cli = { workspace = true }
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
fn main() {
|
||||
prisma_client_rust_cli::run();
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
[package]
|
||||
name = "poly-prisma"
|
||||
version = "0.1.0"
|
||||
license = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
documentation = { workspace = true }
|
||||
readme = { workspace = true }
|
||||
homepage = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
prisma-client-rust = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
#[allow(warnings, unused)]
|
||||
pub mod prisma;
|
||||
|
|
@ -11,8 +11,7 @@
|
|||
"serve": "vite preview",
|
||||
"tauri": "tauri",
|
||||
"lint": "eslint . --cache",
|
||||
"format": "pnpm lint --fix && cargo fmt",
|
||||
"prisma": "cd src-tauri && cargo prisma"
|
||||
"format": "pnpm lint --fix && cargo fmt"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/poppins": "^5.0.12",
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ authors = { workspace = true }
|
|||
tauri-build = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
poly-prisma = { path = "../crates/prisma" }
|
||||
tauri = { workspace = true }
|
||||
tauri-plugin-shell = { workspace = true }
|
||||
tauri-plugin-http = { workspace = true }
|
||||
|
|
@ -26,9 +25,10 @@ tar = { workspace = true }
|
|||
zip = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
prisma-client-rust = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
sha1 = "0.10.6"
|
||||
|
||||
[features]
|
||||
# This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!!
|
||||
|
|
|
|||
|
|
@ -6,7 +6,11 @@ fn main() {
|
|||
.plugin("auth", InlinedPlugin::new().commands(&["login_msa"]))
|
||||
.plugin(
|
||||
"game",
|
||||
InlinedPlugin::new().commands(&["launch_game", "set_selected_client"]),
|
||||
InlinedPlugin::new().commands(&[
|
||||
"create_instance",
|
||||
"get_instances",
|
||||
"get_instance"
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
[default]
|
||||
description = "Default permissions for the plugin"
|
||||
permissions = [
|
||||
"allow-launch-game",
|
||||
"allow-set-selected-client"
|
||||
"allow-get-instance",
|
||||
"allow-get-instances",
|
||||
"allow-create-instance"
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,32 +0,0 @@
|
|||
-- CreateTable
|
||||
CREATE TABLE "instance" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"name" TEXT,
|
||||
"path" TEXT,
|
||||
"hidden" BOOLEAN,
|
||||
"time_played" DATETIME NOT NULL,
|
||||
"last_played" DATETIME NOT NULL,
|
||||
"date_created" DATETIME NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "java_instance" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"name" TEXT,
|
||||
"path" TEXT,
|
||||
"version" TEXT,
|
||||
"arch" TEXT,
|
||||
"instance_id" INTEGER
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "location" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"name" TEXT,
|
||||
"path" TEXT,
|
||||
"enabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"hidden" BOOLEAN,
|
||||
"date_added" DATETIME,
|
||||
"instance_id" INTEGER,
|
||||
CONSTRAINT "location_instance_id_fkey" FOREIGN KEY ("instance_id") REFERENCES "instance" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "sqlite"
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
datasource db {
|
||||
provider = "sqlite"
|
||||
url = "file:dev.db"
|
||||
}
|
||||
|
||||
generator client {
|
||||
provider = "cargo prisma"
|
||||
output = "../../crates/prisma/src/prisma.rs"
|
||||
}
|
||||
|
||||
// todo: replace all `path` fields with a model with a checksum n shit
|
||||
// todo: replace all `String` fields with an enum if applicable
|
||||
|
||||
// represents a single minecraft instance
|
||||
model Instance {
|
||||
id Int @id @default(autoincrement())
|
||||
name String?
|
||||
path String?
|
||||
hidden Boolean?
|
||||
|
||||
time_played DateTime
|
||||
last_played DateTime
|
||||
date_created DateTime @default(now())
|
||||
|
||||
mods Mod[]
|
||||
|
||||
java_instance JavaInstance @relation(fields: [java_instance_id], references: [id])
|
||||
java_instance_id Int
|
||||
|
||||
@@map("instance")
|
||||
}
|
||||
|
||||
model JavaInstance {
|
||||
id Int @id @default(autoincrement())
|
||||
name String?
|
||||
path String?
|
||||
version String?
|
||||
arch String?
|
||||
|
||||
instance Instance[]
|
||||
|
||||
@@map("java_instance")
|
||||
}
|
||||
|
||||
model Asset {
|
||||
id Int @id @default(autoincrement())
|
||||
|
||||
name String?
|
||||
path String?
|
||||
enabled Boolean @default(true)
|
||||
hidden Boolean?
|
||||
date_added DateTime?
|
||||
|
||||
/// @local
|
||||
instance_id Int?
|
||||
instance Instance? @relation(fields: [instance_id], references: [id], onDelete: SetNull)
|
||||
|
||||
@@map("asset")
|
||||
}
|
||||
|
||||
model MinecraftManifest {
|
||||
id String @id
|
||||
assets String
|
||||
compliance_level Int
|
||||
downloads Downloads
|
||||
|
||||
@@map("minecraft_manifest")
|
||||
}
|
||||
|
||||
model Downloads {
|
||||
client Client
|
||||
|
||||
@@map("downloads")
|
||||
}
|
||||
|
||||
model Client {
|
||||
sha1 String @unique
|
||||
size Int
|
||||
url String
|
||||
|
||||
@@map("client")
|
||||
}
|
||||
|
||||
model JavaVersion {
|
||||
component String
|
||||
major_version MajorVersion @default(VERSION_1_21)
|
||||
}
|
||||
|
||||
enum MajorVersion {
|
||||
VERSION_1_21
|
||||
VERSION_1_8
|
||||
}
|
||||
|
||||
model Library {
|
||||
downloads LibraryDownloads
|
||||
name String
|
||||
rules Rule[]
|
||||
extract Extract?
|
||||
natives Natives?
|
||||
}
|
||||
|
|
@ -1,270 +1,173 @@
|
|||
use std::{collections::HashMap, env, fs, path::PathBuf};
|
||||
use std::{collections::HashMap, fs, path::PathBuf};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use async_trait::async_trait;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use chrono::{DateTime, Local};
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use tauri::{AppHandle, Manager};
|
||||
use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
utils::{file, http},
|
||||
PolyError, PolyResult,
|
||||
};
|
||||
use crate::PolyResult;
|
||||
|
||||
use super::{
|
||||
clients::vanilla::{vanilla_manifest, VanillaClient, VanillaManifest},
|
||||
JavaVersion,
|
||||
};
|
||||
use super::{minecraft::MinecraftManifest, vanilla};
|
||||
|
||||
#[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, Serialize, Deserialize)]
|
||||
pub struct Manifest {
|
||||
pub id: Uuid,
|
||||
pub manifest: MinecraftManifest
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", content = "manifest")]
|
||||
pub enum GameClientType {
|
||||
Vanilla(VanillaManifest),
|
||||
// Forge(ForgeManifest),
|
||||
pub struct Instance {
|
||||
pub id: Uuid,
|
||||
pub created_at: DateTime<Local>,
|
||||
pub name: String,
|
||||
pub cover: Option<String>,
|
||||
pub group: Option<Uuid>,
|
||||
pub client: ClientType,
|
||||
}
|
||||
|
||||
#[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)),
|
||||
})
|
||||
#[serde(tag = "type")]
|
||||
pub enum ClientType {
|
||||
Vanilla
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait GameClient: Send + Sync {
|
||||
fn new(handle: AppHandle, details: GameClientDetails) -> Self
|
||||
where
|
||||
Self: Sized;
|
||||
pub trait ClientTrait: Send + Sync {
|
||||
fn from_instance(instance: Instance) -> Self where Self: Sized;
|
||||
|
||||
async fn get_all_version_urls() -> PolyResult<HashMap<String, String>>
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
async fn get_minecraft_manifest(
|
||||
version: String,
|
||||
) -> PolyResult<vanilla_manifest::MinecraftManifest>
|
||||
where
|
||||
Self: Sized;
|
||||
async fn launch(&self) -> PolyResult<()>;
|
||||
async fn setup(&self) -> PolyResult<()>;
|
||||
|
||||
// async fn get_details_from_version(version: String) -> Result<GameClientDetails, Box<dyn Error>> 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<()>;
|
||||
|
||||
async fn download_java(&self) -> PolyResult<PathBuf> {
|
||||
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 install_game(&self) -> PolyResult<()>;
|
||||
async fn install_libraries(&self) -> PolyResult<String>;
|
||||
async fn install_natives(&self) -> PolyResult<()>;
|
||||
async fn install_assets(&self) -> PolyResult<()>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait ModdedGameClient: GameClient {
|
||||
async fn install_mods(&self) -> PolyResult<()>;
|
||||
pub struct ClientManager {
|
||||
instances: HashMap<Uuid, Instance>,
|
||||
instances_dir: PathBuf,
|
||||
|
||||
manifests: HashMap<Uuid, Manifest>,
|
||||
manifests_dir: PathBuf,
|
||||
}
|
||||
|
||||
#[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),
|
||||
impl ClientManager {
|
||||
pub fn new(handle: &AppHandle) -> PolyResult<Self> {
|
||||
let instances_dir = handle.path().app_config_dir()?.join("instances");
|
||||
let instances = load_and_serialize::<Instance>(&instances_dir)?;
|
||||
|
||||
let manifests_dir = handle.path().app_config_dir()?.join("manifests");
|
||||
let manifests = load_and_serialize::<Manifest>(&manifests_dir)?;
|
||||
|
||||
Ok(Self {
|
||||
instances,
|
||||
instances_dir,
|
||||
manifests,
|
||||
manifests_dir,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_instances(&self) -> Vec<&Instance> {
|
||||
self.instances.values().collect()
|
||||
}
|
||||
|
||||
pub fn get_instances_owned(&self) -> Vec<Instance> {
|
||||
self.instances.values().cloned().collect()
|
||||
}
|
||||
|
||||
pub fn get_instance(&self, uuid: Uuid) -> Option<&Instance> {
|
||||
self.instances.get(&uuid)
|
||||
}
|
||||
|
||||
pub fn get_instance_mut(&mut self, uuid: Uuid) -> Option<&mut Instance> {
|
||||
self.instances.get_mut(&uuid)
|
||||
}
|
||||
|
||||
pub fn get_manifest(&self, uuid: Uuid) -> Option<&Manifest> {
|
||||
self.manifests.get(&uuid)
|
||||
}
|
||||
|
||||
pub fn get_manifest_mut(&mut self, uuid: Uuid) -> Option<&mut Manifest> {
|
||||
self.manifests.get_mut(&uuid)
|
||||
}
|
||||
|
||||
pub async fn create_instance(&mut self,
|
||||
name: String,
|
||||
version: String,
|
||||
cover: Option<String>,
|
||||
group: Option<Uuid>,
|
||||
client: ClientType
|
||||
) -> PolyResult<Uuid> {
|
||||
// TODO: Implement cahcing
|
||||
let url = String::from("https://piston-meta.mojang.com/v1/packages/d546f1707a3f2b7d034eece5ea2e311eda875787/1.8.9.json");
|
||||
let uuid = Uuid::new_v4();
|
||||
|
||||
// Save the manifest
|
||||
let minecraft_manifest = vanilla::retrieve_version_manifest(url).await?;
|
||||
let manifest = Manifest {
|
||||
id: uuid,
|
||||
manifest: minecraft_manifest
|
||||
};
|
||||
|
||||
save(&self.manifests_dir.join(format!("{}.json", uuid)), &manifest)?;
|
||||
self.manifests.insert(uuid, manifest);
|
||||
|
||||
// Save the instance
|
||||
let instance = Instance {
|
||||
id: uuid,
|
||||
created_at: Local::now(),
|
||||
name,
|
||||
cover,
|
||||
group,
|
||||
client
|
||||
};
|
||||
|
||||
save(&self.instances_dir.join(format!("{}.json", uuid)), &instance)?;
|
||||
self.instances.insert(uuid, instance);
|
||||
|
||||
Ok(uuid)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_arch() -> String {
|
||||
let arch = env::consts::ARCH;
|
||||
match arch {
|
||||
"x86" => "x86",
|
||||
"x86_64" => "x64",
|
||||
"arm" => "aarch32",
|
||||
"aarch64" => "aarch64",
|
||||
_ => "unsupported",
|
||||
}
|
||||
.to_string()
|
||||
fn save<T>(path: &PathBuf, object: T) -> PolyResult<()> where T: Serialize {
|
||||
if !path.exists() {
|
||||
fs::create_dir_all(path.parent().expect("Couldn't get parent of path whilst saving object. 'client.rs'"))?;
|
||||
}
|
||||
|
||||
let content = serde_json::to_string(&object)?;
|
||||
fs::write(path, content)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_java_versions(
|
||||
java_version: &JavaVersion,
|
||||
os: &str,
|
||||
archive_type: &str,
|
||||
) -> PolyResult<Value> {
|
||||
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
|
||||
);
|
||||
fn load_and_serialize<T>(dir: &PathBuf) -> PolyResult<HashMap<Uuid, T>> where T: DeserializeOwned {
|
||||
let mut result = HashMap::<Uuid, T>::new();
|
||||
|
||||
let response = match http::create_client()?.get(&url).send().await {
|
||||
Ok(response) => match response.json::<serde_json::Value>().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 !dir.exists() {
|
||||
fs::create_dir_all(dir)?;
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
for file in fs::read_dir(dir)? {
|
||||
let file = file?;
|
||||
let file_name = file.file_name();
|
||||
let file_name = file_name.to_str().ok_or(anyhow!("Couldn't convert OsStr to str"))?.replace(".json", "");
|
||||
let uuid = match Uuid::parse_str(&file_name) {
|
||||
Ok(uuid) => uuid,
|
||||
Err(_) => {
|
||||
eprintln!("Couldn't parse file name as UUID: '{:?}'", file_name);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
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(),
|
||||
)));
|
||||
}
|
||||
let path = file.path();
|
||||
let content = fs::read_to_string(&path)?;
|
||||
let parsed = serde_json::from_str::<T>(&content)?;
|
||||
|
||||
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();
|
||||
|
||||
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()) {
|
||||
let _ = fs::remove_file(dest.as_path());
|
||||
return Err(PolyError::JavaError(JavaDownloadError::ExtractError(
|
||||
err.to_string(),
|
||||
)));
|
||||
}
|
||||
|
||||
let _ = fs::remove_file(archive.as_path());
|
||||
Ok(())
|
||||
result.insert(uuid, parsed);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
pub mod vanilla;
|
||||
|
|
@ -1,338 +0,0 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use async_trait::async_trait;
|
||||
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,
|
||||
};
|
||||
|
||||
create_manifest!(VanillaManifest {
|
||||
// minecraft: vanilla_manifest::MinecraftManifest
|
||||
});
|
||||
|
||||
create_client!(VanillaClient {});
|
||||
|
||||
#[async_trait]
|
||||
impl GameClient for VanillaClient {
|
||||
fn new(handle: AppHandle, details: GameClientDetails) -> Self {
|
||||
Self {
|
||||
handle: handle.clone(),
|
||||
details,
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_all_version_urls() -> PolyResult<HashMap<String, String>> {
|
||||
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::<Value>()
|
||||
.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 mut map = HashMap::new();
|
||||
|
||||
for version in versions {
|
||||
let version = version.as_object().ok_or(anyhow!("Invalid version object"))?;
|
||||
|
||||
let id = version
|
||||
.get("id")
|
||||
.ok_or(anyhow!("No id object"))?
|
||||
.as_str()
|
||||
.ok_or(anyhow!("Invalid id object"))?;
|
||||
let url = version
|
||||
.get("url")
|
||||
.ok_or(anyhow!("No url object"))?
|
||||
.as_str()
|
||||
.ok_or(anyhow!("Invalid url object"))?;
|
||||
|
||||
map.insert(id.to_string(), url.to_string());
|
||||
}
|
||||
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
async fn get_minecraft_manifest(
|
||||
version_url: String,
|
||||
) -> PolyResult<vanilla_manifest::MinecraftManifest> {
|
||||
let request = http::create_client()?.get(version_url).send().await?;
|
||||
|
||||
match request.json::<vanilla_manifest::MinecraftManifest>().await {
|
||||
Ok(manifest) => Ok(manifest),
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
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 setup(&self) -> PolyResult<()> {
|
||||
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(anyhow!("Version not found"))?
|
||||
.to_owned();
|
||||
let manifest = Self::get_minecraft_manifest(version).await?;
|
||||
|
||||
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) -> PolyResult<()> {
|
||||
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::{fs, path::PathBuf};
|
||||
|
||||
use anyhow::anyhow;
|
||||
|
||||
use crate::{utils::http::download_file, PolyResult};
|
||||
|
||||
use super::vanilla_manifest::Library;
|
||||
|
||||
pub async fn setup_libraries(
|
||||
libraries: &Vec<Library>,
|
||||
libraries_folder: &PathBuf,
|
||||
) -> PolyResult<Vec<String>> {
|
||||
let mut natives_ret: Vec<&Library> = vec![];
|
||||
let mut libraries_ret: Vec<String> = vec![];
|
||||
|
||||
for library in libraries {
|
||||
if let Some(_) = library.natives {
|
||||
natives_ret.push(library);
|
||||
continue;
|
||||
}
|
||||
|
||||
let artifact = library
|
||||
.downloads
|
||||
.artifact
|
||||
.clone()
|
||||
.ok_or(anyhow!("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(anyhow!("Couldn't get library parent"))?)?;
|
||||
|
||||
if !dest.exists() {
|
||||
download_file(url.as_str(), &dest).await?;
|
||||
}
|
||||
|
||||
libraries_ret.push(
|
||||
dest.to_str()
|
||||
.ok_or(anyhow!("Couldn't get library path"))?
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(libraries_ret)
|
||||
}
|
||||
|
||||
pub async fn setup_natives() -> PolyResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn setup_assets() -> PolyResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// todo move to prisma
|
||||
pub mod vanilla_manifest {
|
||||
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<Library>,
|
||||
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,
|
||||
}
|
||||
|
||||
#[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 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 Library {
|
||||
pub downloads: Downloads2,
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub rules: Vec<Rule>,
|
||||
pub extract: Option<Extract>,
|
||||
pub natives: Option<Natives>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Downloads2 {
|
||||
pub artifact: Option<Artifact>,
|
||||
pub classifiers: Option<Classifiers>,
|
||||
}
|
||||
|
||||
#[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<NativeLibrary>,
|
||||
#[serde(rename = "natives-windows-64")]
|
||||
pub natives_windows_64: Option<NativeLibrary>,
|
||||
#[serde(rename = "natives-osx")]
|
||||
pub natives_osx: Option<NativeLibrary>,
|
||||
#[serde(rename = "natives-linux")]
|
||||
pub natives_linux: Option<NativeLibrary>,
|
||||
#[serde(rename = "natives-windows")]
|
||||
pub natives_windows: Option<NativeLibrary>,
|
||||
}
|
||||
|
||||
#[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<Os>,
|
||||
}
|
||||
|
||||
#[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<String>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Natives {
|
||||
pub windows: String,
|
||||
pub linux: Option<String>,
|
||||
pub osx: Option<String>,
|
||||
}
|
||||
|
||||
#[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,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
use std::{fs, path::PathBuf};
|
||||
|
||||
use serde_json::Value;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{utils::{file, http}, PolyError, 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),
|
||||
}
|
||||
|
||||
pub async fn download_java(dir: &PathBuf, version: u8) -> PolyResult<PathBuf> {
|
||||
if !dir.exists() {
|
||||
match fs::create_dir_all(&dir) {
|
||||
Ok(_) => (),
|
||||
Err(err) => {
|
||||
return Err(PolyError::JavaError(JavaDownloadError::PermissionDenied(
|
||||
err.to_string(),
|
||||
)))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let os = std::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-{}.{}", version.to_string(), archive_type);
|
||||
let archive = dir.join(&archive_name);
|
||||
let dest = dir.join(format!("zulu-{}-{}", 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(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(),
|
||||
)))
|
||||
}
|
||||
|
||||
fn get_arch() -> String {
|
||||
let arch = std::env::consts::ARCH;
|
||||
match arch {
|
||||
"x86" => "x86",
|
||||
"x86_64" => "x64",
|
||||
"arm" => "aarch32",
|
||||
"aarch64" => "aarch64",
|
||||
_ => "unsupported",
|
||||
}.to_string()
|
||||
}
|
||||
|
||||
async fn download(
|
||||
java_version: u8,
|
||||
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 {
|
||||
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()) {
|
||||
let _ = fs::remove_file(dest.as_path());
|
||||
return Err(PolyError::JavaError(JavaDownloadError::ExtractError(
|
||||
err.to_string(),
|
||||
)));
|
||||
}
|
||||
|
||||
let _ = fs::remove_file(archive.as_path());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_java_versions(
|
||||
java_version: u8,
|
||||
os: &str,
|
||||
archive_type: &str,
|
||||
) -> PolyResult<Value> {
|
||||
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
|
||||
);
|
||||
|
||||
let response = match http::create_client()?.get(&url).send().await {
|
||||
Ok(response) => match response.json::<serde_json::Value>().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(),
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
156
src-tauri/src/game/minecraft.rs
Normal file
156
src-tauri/src/game/minecraft.rs
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
use serde::{Serialize, Deserialize};
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MinecraftManifest {
|
||||
pub asset_index: AssetIndex,
|
||||
pub downloads: Downloads,
|
||||
#[serde(rename = "id")]
|
||||
pub version: String,
|
||||
pub java_version: JavaVersion,
|
||||
pub libraries: Vec<Library>,
|
||||
pub logging: Logging,
|
||||
pub main_class: String,
|
||||
pub minecraft_arguments: String,
|
||||
pub release_time: String,
|
||||
#[serde(rename = "type")]
|
||||
pub release_type: ReleaseType,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum ReleaseType {
|
||||
Snapshot,
|
||||
Release,
|
||||
OldBeta,
|
||||
OldAlpha,
|
||||
}
|
||||
|
||||
impl Default for ReleaseType {
|
||||
fn default() -> Self { ReleaseType::Release }
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AssetIndex {
|
||||
pub id: String,
|
||||
pub sha1: String,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Downloads {
|
||||
pub client: Client,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Client {
|
||||
pub sha1: String,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct JavaVersion {
|
||||
pub major_version: u8,
|
||||
}
|
||||
|
||||
#[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<Rule>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub natives: Option<Natives>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Downloads2 {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub artifact: Option<Artifact>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub classifiers: Option<Classifiers>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Artifact {
|
||||
pub path: String,
|
||||
pub sha1: String,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Classifiers {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(rename = "natives-osx")]
|
||||
pub natives_osx: Option<Native>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(rename = "natives-linux")]
|
||||
pub natives_linux: Option<Native>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(rename = "natives-windows")]
|
||||
pub natives_windows: Option<Native>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Native {
|
||||
pub path: String,
|
||||
pub sha1: String,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Rule {
|
||||
pub action: String,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub os: Option<Os>,
|
||||
}
|
||||
|
||||
#[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 Natives {
|
||||
pub windows: Option<String>,
|
||||
pub linux: Option<String>,
|
||||
pub osx: Option<String>,
|
||||
}
|
||||
|
||||
#[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 url: String,
|
||||
}
|
||||
|
|
@ -1,83 +1,5 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{plugin::TauriPlugin, Manager};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use self::client::get_game_client;
|
||||
|
||||
pub mod client;
|
||||
mod clients;
|
||||
|
||||
// todo move to prisma
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum JavaVersion {
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tauri plugin for the game module
|
||||
pub struct GameManagerState {
|
||||
pub selected_client: Option<Box<dyn client::GameClient>>,
|
||||
}
|
||||
|
||||
unsafe impl Send for GameManagerState {}
|
||||
unsafe impl Sync for GameManagerState {}
|
||||
impl Default for GameManagerState {
|
||||
fn default() -> Self {
|
||||
GameManagerState {
|
||||
selected_client: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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, set_selected_client])
|
||||
.build()
|
||||
}
|
||||
|
||||
#[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()),
|
||||
};
|
||||
|
||||
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)),
|
||||
}
|
||||
}
|
||||
|
||||
// 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<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(())
|
||||
}
|
||||
pub mod java;
|
||||
pub mod vanilla;
|
||||
pub mod plugin;
|
||||
pub mod minecraft;
|
||||
50
src-tauri/src/game/plugin.rs
Normal file
50
src-tauri/src/game/plugin.rs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
use tauri::{plugin::TauriPlugin, Manager, State};
|
||||
use tokio::sync::Mutex;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::client::{ClientManager, ClientType, Instance};
|
||||
|
||||
pub struct GameManagerState {
|
||||
pub client_manager: ClientManager,
|
||||
}
|
||||
|
||||
unsafe impl Send for GameManagerState {}
|
||||
unsafe impl Sync for GameManagerState {}
|
||||
|
||||
pub fn init() -> TauriPlugin<tauri::Wry> {
|
||||
tauri::plugin::Builder::new("game")
|
||||
.setup(|app, _| {
|
||||
app.manage(Mutex::new(GameManagerState {
|
||||
client_manager: ClientManager::new(app).expect("Failed to initialize client manager"),
|
||||
}));
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
create_instance,
|
||||
get_instances,
|
||||
])
|
||||
.build()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn create_instance(
|
||||
state: State<'_, Mutex<GameManagerState>>,
|
||||
name: String,
|
||||
version: String,
|
||||
cover: Option<String>,
|
||||
group: Option<Uuid>,
|
||||
client: ClientType
|
||||
) -> Result<Uuid, String> {
|
||||
let manager = &mut state.lock().await.client_manager;
|
||||
|
||||
manager.create_instance(name, version, cover, group, client).await.or_else(|e| {
|
||||
Err(e.to_string())
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_instances(state: State<'_, Mutex<GameManagerState>>) -> Result<Vec<Instance>, String> {
|
||||
let manager = &mut state.lock().await.client_manager;
|
||||
|
||||
Ok(manager.get_instances_owned())
|
||||
}
|
||||
16
src-tauri/src/game/vanilla.rs
Normal file
16
src-tauri/src/game/vanilla.rs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use crate::{utils::http, PolyResult};
|
||||
|
||||
use super::minecraft::MinecraftManifest;
|
||||
|
||||
pub async fn retrieve_versions() -> PolyResult<HashMap<String, String>> {
|
||||
let map = HashMap::new();
|
||||
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
pub async fn retrieve_version_manifest(url: String) -> PolyResult<MinecraftManifest> {
|
||||
let manifest = http::create_client()?.get(url).send().await?.json::<MinecraftManifest>().await?;
|
||||
Ok(manifest)
|
||||
}
|
||||
|
|
@ -5,6 +5,10 @@ pub mod constants;
|
|||
pub mod game;
|
||||
pub mod utils;
|
||||
|
||||
pub struct AppState {
|
||||
// pub ,
|
||||
}
|
||||
|
||||
/// A standardized [`Error`] type that is used across the launcher
|
||||
#[derive(Debug, Error)]
|
||||
pub enum PolyError {
|
||||
|
|
@ -33,24 +37,19 @@ pub enum PolyError {
|
|||
#[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),
|
||||
JavaError(#[from] game::java::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.
|
||||
/// Wrapper around [`auth::AuthenticationError`] to handle authentication errors.
|
||||
AuthError(#[from] auth::AuthenticationError),
|
||||
/// Wrapper around [`anyhow::Error`] to handle generic errors.
|
||||
#[error(transparent)]
|
||||
AnyhowError(#[from] anyhow::Error),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,11 +6,12 @@ use std::error::Error;
|
|||
use polyfrost_launcher::{auth, game};
|
||||
use tauri::{menu::Menu, App};
|
||||
|
||||
fn main() {
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(auth::init())
|
||||
.plugin(game::init())
|
||||
.plugin(game::plugin::init())
|
||||
.menu(|handle| Menu::new(handle))
|
||||
.setup(setup)
|
||||
.run(tauri::generate_context!())
|
||||
|
|
|
|||
|
|
@ -25,10 +25,10 @@ pub fn extract_archive(archive: &Path, dest: &Path) -> PolyResult<()> {
|
|||
|
||||
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));
|
||||
let archive = zip::ZipArchive::new(file).map_err(|err| PolyError::ZipError(err));
|
||||
archive?
|
||||
.extract(dest)
|
||||
.map_err(|err| PolyError::ZipError(err));
|
||||
.map_err(|err| PolyError::ZipError(err))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -36,6 +36,6 @@ 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));
|
||||
archive.unpack(dest).map_err(|err| PolyError::IOError(err))?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,6 @@ pub async fn download_file(url: &str, path: &Path) -> PolyResult<()> {
|
|||
.await
|
||||
.map_err(|err| PolyError::HTTPError(err))?,
|
||||
);
|
||||
io::copy(&mut content, &mut file).map_err(|err| PolyError::IOError(err));
|
||||
io::copy(&mut content, &mut file).map_err(|err| PolyError::IOError(err))?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,24 +18,28 @@ function BrowserPage() {
|
|||
</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,
|
||||
});
|
||||
onClick={async () => {
|
||||
const result = invoke('plugin:game|get_instances');
|
||||
console.log(result);
|
||||
}}
|
||||
>
|
||||
Test Button2
|
||||
Test Button 2
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={async () => {
|
||||
const result = await invoke('plugin:game|create_instance', {
|
||||
name: 'My epic instance name',
|
||||
version: '1.8.9',
|
||||
client: {
|
||||
type: 'Vanilla',
|
||||
},
|
||||
});
|
||||
|
||||
console.log(result);
|
||||
}}
|
||||
>
|
||||
Test Button 3
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue