From 022936d35cae70d31295f87584689ae391629c77 Mon Sep 17 00:00:00 2001 From: Pauline Date: Wed, 27 Mar 2024 05:38:58 +0100 Subject: [PATCH] hhhhh --- Cargo.toml | 30 +- README.md | 2 +- core/src/game/client.rs | 260 --------------- core/src/game/java.rs | 157 --------- core/src/game/minecraft.rs | 224 ------------- core/src/game/vanilla.rs | 117 ------- core/src/lib.rs | 75 ----- core/src/utils/dirs.rs | 37 --- core/src/utils/file.rs | 53 --- core/src/utils/http.rs | 34 -- index.html | 2 +- {core => onelauncher}/Cargo.toml | 23 +- .../src/auth/microsoft_auth.rs | 82 +++-- {core => onelauncher}/src/auth/mod.rs | 11 +- {core => onelauncher}/src/constants.rs | 9 +- onelauncher/src/error.rs | 117 +++++++ onelauncher/src/game/client.rs | 304 ++++++++++++++++++ onelauncher/src/game/java.rs | 159 +++++++++ onelauncher/src/game/minecraft.rs | 209 ++++++++++++ {core => onelauncher}/src/game/mod.rs | 2 +- onelauncher/src/game/vanilla.rs | 144 +++++++++ onelauncher/src/lib.rs | 12 + onelauncher/src/logger.rs | 54 ++++ onelauncher/src/utils/dirs.rs | 35 ++ onelauncher/src/utils/file.rs | 52 +++ onelauncher/src/utils/http.rs | 34 ++ {core => onelauncher}/src/utils/mod.rs | 2 +- package.json | 1 - src-tauri/Cargo.toml | 11 +- src-tauri/build.rs | 25 +- src-tauri/capabilities/default.json | 2 +- .../default.toml | 0 src-tauri/src/{launcher_core => api}/auth.rs | 6 +- src-tauri/src/api/game.rs | 50 +++ src-tauri/src/api/mod.rs | 41 +++ src-tauri/src/constants.rs | 3 - src-tauri/src/error.rs | 18 ++ src-tauri/src/launcher_core/game.rs | 48 --- src-tauri/src/launcher_core/mod.rs | 42 --- src-tauri/src/lib.rs | 4 +- src-tauri/src/main.rs | 20 +- src-tauri/tauri.conf.json | 15 +- src/bridge/game.ts | 10 +- vite.config.ts | 25 +- 44 files changed, 1412 insertions(+), 1149 deletions(-) delete mode 100644 core/src/game/client.rs delete mode 100644 core/src/game/java.rs delete mode 100644 core/src/game/minecraft.rs delete mode 100644 core/src/game/vanilla.rs delete mode 100644 core/src/lib.rs delete mode 100644 core/src/utils/dirs.rs delete mode 100644 core/src/utils/file.rs delete mode 100644 core/src/utils/http.rs rename {core => onelauncher}/Cargo.toml (54%) rename {core => onelauncher}/src/auth/microsoft_auth.rs (81%) rename {core => onelauncher}/src/auth/mod.rs (81%) rename {core => onelauncher}/src/constants.rs (57%) create mode 100644 onelauncher/src/error.rs create mode 100644 onelauncher/src/game/client.rs create mode 100644 onelauncher/src/game/java.rs create mode 100644 onelauncher/src/game/minecraft.rs rename {core => onelauncher}/src/game/mod.rs (71%) create mode 100644 onelauncher/src/game/vanilla.rs create mode 100644 onelauncher/src/lib.rs create mode 100644 onelauncher/src/logger.rs create mode 100644 onelauncher/src/utils/dirs.rs create mode 100644 onelauncher/src/utils/file.rs create mode 100644 onelauncher/src/utils/http.rs rename {core => onelauncher}/src/utils/mod.rs (66%) rename src-tauri/permissions/{launcher-core => onelauncher}/default.toml (100%) rename src-tauri/src/{launcher_core => api}/auth.rs (76%) create mode 100644 src-tauri/src/api/game.rs create mode 100644 src-tauri/src/api/mod.rs delete mode 100644 src-tauri/src/constants.rs create mode 100644 src-tauri/src/error.rs delete mode 100644 src-tauri/src/launcher_core/game.rs delete mode 100644 src-tauri/src/launcher_core/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 7494abe6..14f3fb2b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ resolver = "2" members = [ "src-tauri", - "core" + "onelauncher" ] [workspace.package] @@ -18,14 +18,26 @@ authors = [ "Polyfrost" ] tauri = { version = "2.0.0-beta", features = [] } 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" } tauri-plugin-stronghold = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } +tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } +tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } + +tracing = { version = "0.1" } +tracing-subscriber = { version = "0.3", features = [ + "chrono", + "env-filter" +] } +tracing-error = { version = "0.2" } +tracing-appender = { version = "0.2" } uuid = { version = "1.8", features = [ "serde" ] } tokio = { version = "1", features = [ "full" ] } serde = { version = "1", features = [ "derive" ] } chrono = { version = "0.4", features = [ "serde" ] } -reqwest = { version = "0.12", features = [ "json" ] } +reqwest = { version = "0.12", features = [ + "json", + "stream", +] } serde_json = { version = "1" } flate2 = { version = "1" } tar = { version = "0.4" } @@ -35,6 +47,18 @@ thiserror = { version = "1" } anyhow = { version = "1" } dirs = { version = "5" } open = { version = "5" } +discord-rich-presence = { version = "0.2" } +winreg = { version = "0.52" } +sha1 = { version = "0.10.6" } +bytes = { version = "1" } +lazy_static = { version = "1" } +once_cell = { version = "1" } +indicatif = { version = "0.17" } +futures = { version = "0.3" } +tokio-stream = { version = "0.1", features = [ "fs" ] } +async-recursion = { version = "1.1" } +notify = { version = "6.1", default-features = false, features = [ "serde" ] } +notify-debouncer-mini = { version = "0.4", default-features = false } [profile.dev] # Make compilation faster on macOS diff --git a/README.md b/README.md index 9baca72e..4b4df6b5 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -# Polyfrost Launcher +# OneLauncher diff --git a/core/src/game/client.rs b/core/src/game/client.rs deleted file mode 100644 index c5013be5..00000000 --- a/core/src/game/client.rs +++ /dev/null @@ -1,260 +0,0 @@ -use std::{collections::HashMap, fs, path::PathBuf}; - -use anyhow::anyhow; -use async_trait::async_trait; -use chrono::{DateTime, Local}; -use serde::{de::DeserializeOwned, Deserialize, Serialize}; -use thiserror::Error; -use uuid::Uuid; - -use crate::{utils::dirs, PolyResult}; - -use super::{minecraft::{MinecraftManifest, ReleaseType}, vanilla::{self, VanillaClientProps}}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Version { - id: String, - url: String, - #[serde(default)] - release_type: ReleaseType, - #[serde(default)] - release_time: chrono::DateTime -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Manifest { - pub id: Uuid, - pub manifest: MinecraftManifest -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Instance { - pub id: Uuid, - pub created_at: DateTime, - pub name: String, - pub cover: Option, - pub group: Option, - pub client: ClientType, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "type")] -pub enum ClientType { - Vanilla(VanillaClientProps) -} - -impl ClientType { - pub async fn get_versions(&self, file: Option<&PathBuf>) -> PolyResult> { - match self { - ClientType::Vanilla(_) => vanilla::get_versions(file).await - } - } -} - -pub fn get_impl<'a>(client: &'a ClientType, instance: &'a Instance, manifest: &'a Manifest) -> Box + 'a> { - Box::new(match client { - ClientType::Vanilla(_) => vanilla::VanillaClient::new(instance, manifest) - }) -} - -#[async_trait] -pub trait ClientTrait<'a>: Send + Sync { - - fn new(instance: &'a Instance, manifest: &'a Manifest) -> Self where Self: Sized; - - async fn launch(&self) -> PolyResult<()>; - async fn setup(&self) -> PolyResult<()>; - - async fn install_game(&self) -> PolyResult; - async fn install_libraries(&self) -> PolyResult; - async fn install_natives(&self) -> PolyResult<()>; - async fn install_assets(&self) -> PolyResult<()>; - - fn get_instance(&self) -> &'a Instance where Self: Sized; - fn get_manifest(&self) -> &'a Manifest where Self: Sized; -} - -#[macro_export] -macro_rules! create_game_client { - ($props:ident { $($props_name:ident: $props_type:ty),* } $client:ident { $($name:ident: $type:ty),* }) => { - #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] - pub struct $props { - $(pub $props_name: $props_type),* - } - - #[derive(Debug, Clone)] - pub struct $client<'a> { - pub instance: &'a crate::game::client::Instance, - pub manifest: &'a crate::game::client::Manifest, - $(pub $name: $type),* - } - }; -} - -#[macro_export] -macro_rules! impl_game_client { - () => { - fn get_instance(&self) -> &'a crate::game::client::Instance where Self: Sized { - self.instance - } - - fn get_manifest(&self) -> &'a crate::game::client::Manifest where Self: Sized { - self.manifest - } - }; -} - -#[derive(Debug, Error)] -pub enum ClientManagerError { - #[error("Instance not found")] - InstanceNotFound, - #[error("Manifest not found")] - ManifestNotFound, -} - -pub struct ClientManager { - instances: HashMap, - manifests: HashMap, -} - -impl ClientManager { - pub fn new() -> PolyResult { - let instances = load_and_serialize::( - &dirs::instances_dir()? - )?; - - let manifests = load_and_serialize::( - &dirs::manifests_dir()? - )?; - - Ok(Self { - instances, - manifests, - }) - } - - pub fn get_impl_uuid<'a>(&'a self, uuid: Uuid) -> PolyResult + 'a>> { - let instance = self.get_instance(uuid)?; - let manifest = self.get_manifest(uuid)?; - - Ok(get_impl(&instance.client, &instance, &manifest)) - } - - pub fn get_instances(&self) -> Vec<&Instance> { - self.instances.values().collect() - } - - pub fn get_instances_owned(&self) -> Vec { - self.instances.values().cloned().collect() - } - - pub fn get_instance(&self, uuid: Uuid) -> PolyResult<&Instance> { - self.instances.get(&uuid).ok_or(ClientManagerError::InstanceNotFound.into()) - } - - pub fn get_instance_mut(&mut self, uuid: Uuid) -> PolyResult<&mut Instance> { - self.instances.get_mut(&uuid).ok_or(ClientManagerError::InstanceNotFound.into()) - } - - pub fn get_manifest(&self, uuid: Uuid) -> PolyResult<&Manifest> { - self.manifests.get(&uuid).ok_or(ClientManagerError::ManifestNotFound.into()) - } - - pub fn get_manifest_mut(&mut self, uuid: Uuid) -> PolyResult<&mut Manifest> { - self.manifests.get_mut(&uuid).ok_or(ClientManagerError::ManifestNotFound.into()) - } - - pub async fn create_instance(&mut self, - name: String, - version: String, - cover: Option, - group: Option, - client: ClientType - ) -> PolyResult { - let version_cache = dirs::app_config_dir()?.join("versions_cache.json"); - let versions = client.get_versions(Some(&version_cache)).await?; - let url = &versions.iter().find(|v| v.id == version).ok_or(anyhow!("Version not found"))?.url; - - 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(&dirs::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(&dirs::instances_dir()?.join(format!("{}.json", uuid)), &instance)?; - self.instances.insert(uuid, instance); - - Ok(uuid) - } -} - -fn save(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(()) -} - -fn load_and_serialize(dir: &PathBuf) -> PolyResult> where T: DeserializeOwned { - let mut result = HashMap::::new(); - - 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; - } - }; - - let path = file.path(); - let content = match fs::read_to_string(&path) { - Ok(content) => content, - Err(_) => { - eprintln!("Couldn't read file: '{:?}'", path); - continue; - } - }; - - let parsed = match serde_json::from_str::(&content) { - Ok(parsed) => parsed, - Err(_) => { - eprintln!("Couldn't parse file content as JSON: '{:?}'", content); - continue; - } - }; - - result.insert(uuid, parsed); - } - - Ok(result) -} diff --git a/core/src/game/java.rs b/core/src/game/java.rs deleted file mode 100644 index 3c1cadc4..00000000 --- a/core/src/game/java.rs +++ /dev/null @@ -1,157 +0,0 @@ -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 { - 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 { - 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::().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) -} \ No newline at end of file diff --git a/core/src/game/minecraft.rs b/core/src/game/minecraft.rs deleted file mode 100644 index b54a9243..00000000 --- a/core/src/game/minecraft.rs +++ /dev/null @@ -1,224 +0,0 @@ -use core::fmt; -use std::{collections::HashMap, marker::PhantomData}; - -use serde::{de::{self, SeqAccess, Visitor}, Deserialize, Deserializer, Serialize}; -use serde_json::Value; - -#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct MinecraftManifest { - pub asset_index: AssetIndex, - pub downloads: Downloads, - pub java_version: JavaVersion, - pub libraries: Vec, - pub logging: Logging, - pub main_class: String, - pub release_time: String, - - #[serde(rename = "id")] - pub version: String, - - #[serde(rename = "type")] - pub release_type: ReleaseType, - - #[serde(skip_serializing_if = "Option::is_none")] - pub minecraft_arguments: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub arguments: Option, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ModernArguments { - game: Vec, - jvm: Vec, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[serde(untagged)] -pub enum ModernArgumentsItem { - Simple(String), - Rule(ArgumentRule), -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ArgumentRule { - #[serde(skip_serializing_if = "Vec::is_empty")] - rules: Vec, - - #[serde(skip_serializing_if = "Vec::is_empty")] - #[serde(deserialize_with = "string_or_seq")] - value: Vec, -} - -#[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: LibraryDownload, - pub name: String, - #[serde(skip_serializing_if = "Vec::is_empty", default)] - pub rules: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - pub natives: Option>, -} - -#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct LibraryDownload { - #[serde(skip_serializing_if = "Option::is_none")] - pub artifact: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub classifiers: Option, -} - -#[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, - - #[serde(skip_serializing_if = "Option::is_none")] - #[serde(rename = "natives-linux")] - pub natives_linux: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - #[serde(rename = "natives-windows")] - pub natives_windows: Option, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Rule { - pub action: RuleAction, - - #[serde(skip_serializing_if = "Option::is_none")] - pub os: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub features: Option>, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub enum RuleAction { - #[serde(rename = "allow")] - Allow, - #[serde(rename = "disallow")] - Disallow, -} - -#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Os { - #[serde(skip_serializing_if = "Option::is_none")] - pub name: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub arch: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub version: 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 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, -} - -fn string_or_seq<'de, D>(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - struct StringOrVec(PhantomData>); - - impl<'de> Visitor<'de> for StringOrVec { - type Value = Vec; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("string or list of strings") - } - - fn visit_str(self, value: &str) -> Result - where - E: de::Error, - { - Ok(vec![value.into()]) - } - - fn visit_seq(self, seq: S) -> Result - where - S: SeqAccess<'de>, - { - Deserialize::deserialize(de::value::SeqAccessDeserializer::new(seq)) - } - } - - deserializer.deserialize_any(StringOrVec(PhantomData)) -} \ No newline at end of file diff --git a/core/src/game/vanilla.rs b/core/src/game/vanilla.rs deleted file mode 100644 index a7c45267..00000000 --- a/core/src/game/vanilla.rs +++ /dev/null @@ -1,117 +0,0 @@ -use std::{fs, path::PathBuf}; -use anyhow::anyhow; -use async_trait::async_trait; -use serde::{Deserialize, Serialize}; -use crate::{constants::MINECRAFT_VERSIONS_MANIFEST, create_game_client, impl_game_client, utils::{dirs, file, http}, PolyResult}; -use super::{client::{ClientTrait, Instance, Manifest, Version}, minecraft::MinecraftManifest}; - -create_game_client! { - VanillaClientProps {} - - VanillaClient {} -} - -#[async_trait] -impl<'a> ClientTrait<'a> for VanillaClient<'a> { - impl_game_client!(); - - fn new(instance: &'a Instance, manifest: &'a Manifest) -> Self { - VanillaClient { - instance, - manifest - } - } - - async fn launch(&self) -> PolyResult<()> { - Ok(()) - } - - async fn setup(&self) -> PolyResult<()> { - Ok(()) - } - - async fn install_game(&self) -> PolyResult { - let manifest = &self.manifest.manifest; - let file = dirs::clients_dir()?.join(format!("{}.jar", manifest.version)); - - if !file.exists() { - fs::create_dir_all(&file)?; - let artifact = &manifest.downloads.client; - - http::download_file(&artifact.url, &file).await?; - let file_hash = file::file_sha1(&file)?; - - println!("Downloaded: '{}' | '{}'", file_hash, artifact.sha1); - if file_hash != artifact.sha1 { - return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "Hashes do not match").into()); - } - } - - Ok(file) - } - - async fn install_libraries(&self) -> PolyResult { - Ok("".to_string()) - } - - async fn install_natives(&self) -> PolyResult<()> { - Ok(()) - } - - async fn install_assets(&self) -> PolyResult<()> { - Ok(()) - } -} - -#[derive(Clone, Serialize, Deserialize)] -struct VersionList { - versions: Vec -} - -#[derive(Serialize, Deserialize)] -struct CachedVersions { - last_updated: i64, - versions: Vec, -} - -pub async fn get_versions(file: Option<&PathBuf>) -> PolyResult> { - if let Some(file) = file { - if file.exists() && file.is_file() { - if let Some(versions) = get_cached_versions(file).await? { - return Ok(versions); - } - } - } - - let response = http::create_client()?.get(MINECRAFT_VERSIONS_MANIFEST).send().await?; - let response = response.json::().await?; - - if let Some(file) = file { - fs::write(file, serde_json::to_string(&CachedVersions { - last_updated: chrono::Utc::now().timestamp(), - versions: response.versions.clone() - })?)?; - } - - Ok(response.versions) -} - -async fn get_cached_versions(file: &PathBuf) -> PolyResult>> { - let cached = serde_json::from_str::(&fs::read_to_string(file)?)?; - let head_request = http::create_client()?.head(MINECRAFT_VERSIONS_MANIFEST).send().await?; - - let last_updated = head_request.headers().get("Last-Modified").ok_or(anyhow!("Last-Modified header not found"))?; - let last_updated = last_updated.to_str().map_err(|_| anyhow!("Failed to convert Last-Modified header to string"))?; - let last_updated = chrono::DateTime::parse_from_rfc2822(last_updated)?; - - if cached.last_updated < last_updated.timestamp() { - Ok(None) - } else { - Ok(Some(cached.versions)) - } -} - -pub async fn retrieve_version_manifest(url: &str) -> PolyResult { - let manifest = http::create_client()?.get(url).send().await?.json::().await?; - Ok(manifest) -} \ No newline at end of file diff --git a/core/src/lib.rs b/core/src/lib.rs deleted file mode 100644 index 88e2fbf9..00000000 --- a/core/src/lib.rs +++ /dev/null @@ -1,75 +0,0 @@ -use thiserror::Error; - -pub mod constants; -pub mod utils; -pub mod game; -pub mod auth; - -/// A standardized [`Error`] type that is used across the launcher -#[derive(Debug, Error)] -pub enum PolyError { - /// Wrapper around [`tokio::sync::AcquireError`] to handle sempahore errors. - #[error("failed to manage a tokio semaphore: {0}")] - TokioError(#[from] tokio::sync::AcquireError), - - /// Wrapper around [`flate2::CompressError`] to handle flate compression errors. - #[error("failed to compress a file with flate: {0}")] - FlateError(#[from] flate2::CompressError), - - /// Wrapper around [`flate2::DecompressError`] to handle flate decompression errors. - #[error("failed to decompress a file with flate: {0}")] - DeflateError(#[from] flate2::DecompressError), - - /// Wrapper around [`uuid::Error`] to handle UUID parsing errors. - #[error("failed to parse uuids: {0}")] - UUIDError(#[from] uuid::Error), - - /// Wrapper around [`serde_json::Error`] to handle Serde JSON parsing errors. - #[error("failed to manage serialization of json: {0}")] - SerdeError(#[from] serde_json::Error), - - /// Wrapper around [`std::io::Error`] only to be used if a more defined error isn't available (e.g. when using `zip`) - #[error("failed to manage writing and reading of files: {0}")] - IOError(#[from] std::io::Error), - - /// Wrapper around [`reqwest::Error`] to handle HTTP errors. - #[error("failed to establish a HTTP connection: {0}")] - HTTPError(#[from] reqwest::Error), - - /// Wrapper around [`zip::result::ZipError`] to handle zip errors. - #[error("failed to manage zip files: {0}")] - ZipError(#[from] zip::result::ZipError), - - /// Wrapper around [`anyhow::Error`] to handle generic errors. - #[error(transparent)] - AnyhowError(#[from] anyhow::Error), - - /// Wrapper around [`chrono::ParseError`] to handle date parsing errors. - #[error("failed to parse a date: {0}")] - ChronoError(#[from] chrono::ParseError), - - /// Wrapper around [`game::java::JavaDownloadError`] to handle Java executable downloading. - #[error("failed to download java: {0}")] - JavaError(#[from] game::java::JavaDownloadError), - - /// Wrapper around [`auth::AuthenticationError`] to handle authentication errors. - #[error("failed to authenticate: {0}")] - AuthError(#[from] auth::AuthenticationError), - - /// Wrapper around [`utils::dirs::DirectoryError`] to handle directory errors. - #[error("failed to get directory: {0}")] - DirectoryError(#[from] utils::dirs::DirectoryError), - - /// Wrapper around [`game::client::ClientManagerError`] to handle directory errors. - #[error("failed to manage clients: {0}")] - ClientManagerError(#[from] game::client::ClientManagerError), -} - -impl From for String { - fn from(value: PolyError) -> Self { - value.to_string() - } -} - -/// Alias for a [`Result`] with the error type [`PolyError`]. -pub type PolyResult = anyhow::Result; \ No newline at end of file diff --git a/core/src/utils/dirs.rs b/core/src/utils/dirs.rs deleted file mode 100644 index 2e8987a1..00000000 --- a/core/src/utils/dirs.rs +++ /dev/null @@ -1,37 +0,0 @@ -use std::path::PathBuf; - -use thiserror::Error; - -use crate::{constants, PolyResult}; - -#[derive(Debug, Error)] -pub enum DirectoryError { - #[error("failed to get the user's config directory")] - ConfigDir, -} - -pub fn app_config_dir() -> PolyResult { - dirs::config_dir() - .and_then(|dir| Some(dir.join(constants::APP_CONFIG_DIR))) - .ok_or(DirectoryError::ConfigDir.into()) -} - -pub fn libraries_dir() -> PolyResult { - Ok(app_config_dir()?.join("libraries")) -} - -pub fn java_dir() -> PolyResult { - Ok(app_config_dir()?.join("java")) -} - -pub fn instances_dir() -> PolyResult { - Ok(app_config_dir()?.join("instances")) -} - -pub fn manifests_dir() -> PolyResult { - Ok(app_config_dir()?.join("manifests")) -} - -pub fn clients_dir() -> PolyResult { - Ok(app_config_dir()?.join("clients")) -} diff --git a/core/src/utils/file.rs b/core/src/utils/file.rs deleted file mode 100644 index 1d91f6a9..00000000 --- a/core/src/utils/file.rs +++ /dev/null @@ -1,53 +0,0 @@ -use anyhow::anyhow; -use sha1::{Digest, Sha1}; -use std::{fs::File, io::BufReader, path::{Path, PathBuf}}; - -use crate::{PolyError, PolyResult}; - -pub fn file_sha1(file: &PathBuf) -> PolyResult { - let file = File::open(file)?; - let reader = BufReader::new(file); - - let mut hasher = Sha1::new(); - hasher.update(&reader.buffer()); - let hash = hasher.finalize(); - - Ok(format!("{:x}", hash)) -} - -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" - ))) - } - }; - - 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 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(()) -} diff --git a/core/src/utils/http.rs b/core/src/utils/http.rs deleted file mode 100644 index a834f094..00000000 --- a/core/src/utils/http.rs +++ /dev/null @@ -1,34 +0,0 @@ -use std::{ - fs::File, - io::{self, Cursor}, - path::Path, -}; - -use 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)) -} - -// 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(()) -} diff --git a/index.html b/index.html index 531d7ed4..7a7df5c4 100644 --- a/index.html +++ b/index.html @@ -5,7 +5,7 @@ - Polyfrost Launcher + OneLauncher diff --git a/core/Cargo.toml b/onelauncher/Cargo.toml similarity index 54% rename from core/Cargo.toml rename to onelauncher/Cargo.toml index 1aa178ab..b522b5c5 100644 --- a/core/Cargo.toml +++ b/onelauncher/Cargo.toml @@ -1,7 +1,7 @@ [package] -name = "launcher-core" +name = "onelauncher" version = "0.0.0" -description = "The core for our launcher" +description = "The core of OneLauncher" license = { workspace = true } edition = { workspace = true } repository = { workspace = true } @@ -10,7 +10,19 @@ readme = { workspace = true } homepage = { workspace = true } authors = { workspace = true } +[features] +tauri = [ "dep:tauri" ] +cli = [ "dep:indicatif" ] + [dependencies] +tauri = { workspace = true, optional = true } +indicatif = { workspace = true, optional = true } + +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +tracing-error = { workspace = true } +tracing-appender = { workspace = true } + tokio = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } @@ -25,5 +37,10 @@ chrono = { workspace = true } reqwest = { workspace = true } dirs = { workspace = true } open = { workspace = true } +sha1 = { workspace = true } +discord-rich-presence = { workspace = true } +notify = { workspace = true } +notify-debouncer-mini = { workspace = true } -sha1 = "0.10.6" +[target.'cfg(windows)'.dependencies] +winreg = { workspace = true } diff --git a/core/src/auth/microsoft_auth.rs b/onelauncher/src/auth/microsoft_auth.rs similarity index 81% rename from core/src/auth/microsoft_auth.rs rename to onelauncher/src/auth/microsoft_auth.rs index 1d5ae16d..1a109784 100644 --- a/core/src/auth/microsoft_auth.rs +++ b/onelauncher/src/auth/microsoft_auth.rs @@ -4,16 +4,19 @@ use std::{ }; use serde_json::json; -use thiserror::Error; -use crate::{constants::{self, CLIENT_ID}, utils::http::create_client, PolyError, PolyResult}; +use crate::{ + constants::{self, CLIENT_ID}, + utils::http::create_client, + ErrorKind, +}; use super::{Account, AuthenticationError, AuthenticationMethod}; // TODO: store auth keys with https://beta.tauri.app/features/stronghold/ // TODO: store auth key public in prisma for easy access -#[derive(Debug, Error)] +#[derive(Debug, thiserror::Error)] pub enum MicrosoftAuthError { #[error("failed to get Minecraft access token")] MSAccessToken, @@ -23,7 +26,9 @@ pub enum MicrosoftAuthError { 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")] + #[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, @@ -37,7 +42,7 @@ pub enum MicrosoftAuthError { pub struct MicrosoftAuthenticationMethod; impl AuthenticationMethod for MicrosoftAuthenticationMethod { - async fn auth(stage: F) -> PolyResult + async fn auth(stage: F) -> crate::Result where F: Fn(String, u8, bool) -> (), { @@ -63,7 +68,7 @@ impl AuthenticationMethod for MicrosoftAuthenticationMethod { } } -async fn auth_minecraft(xsts_token: String, user_hash: String) -> PolyResult { +async fn auth_minecraft(xsts_token: String, user_hash: String) -> crate::Result { let response = create_client()? .post("https://api.minecraftservices.com/authentication/login_with_xbox") .json(&json!({ @@ -76,16 +81,17 @@ async fn auth_minecraft(xsts_token: String, user_hash: String) -> PolyResult token.as_str().unwrap(), None => { - return Err(PolyError::AuthError(AuthenticationError::MicrosoftError( + return Err(ErrorKind::AuthError(AuthenticationError::MicrosoftError( MicrosoftAuthError::MSAccessToken, - ))) + )) + .into()) } }; Ok(access_token.to_string()) } -async fn auth_xsts(token: String) -> PolyResult { +async fn auth_xsts(token: String) -> crate::Result { let response = create_client()? .post("https://xsts.auth.xboxlive.com/xsts/authorize") .json(&json!({ @@ -108,29 +114,34 @@ async fn auth_xsts(token: String) -> PolyResult { match error_code { 2148916233 => { - return Err(PolyError::AuthError(AuthenticationError::MicrosoftError( + return Err(ErrorKind::AuthError(AuthenticationError::MicrosoftError( MicrosoftAuthError::NoXBoxAccount, - ))) + )) + .into()) } 2148916235 => { - return Err(PolyError::AuthError(AuthenticationError::MicrosoftError( + return Err(ErrorKind::AuthError(AuthenticationError::MicrosoftError( MicrosoftAuthError::XBoxLiveBlocked, - ))) + )) + .into()) } 2148916236 | 2148916237 => { - return Err(PolyError::AuthError(AuthenticationError::MicrosoftError( + return Err(ErrorKind::AuthError(AuthenticationError::MicrosoftError( MicrosoftAuthError::AdultVerification, - ))) + )) + .into()) } 2148916238 => { - return Err(PolyError::AuthError(AuthenticationError::MicrosoftError( + return Err(ErrorKind::AuthError(AuthenticationError::MicrosoftError( MicrosoftAuthError::ChildAccount, - ))) + )) + .into()) } _ => { - return Err(PolyError::AuthError(AuthenticationError::MicrosoftError( + return Err(ErrorKind::AuthError(AuthenticationError::MicrosoftError( MicrosoftAuthError::UnknownError(error_code), - ))) + )) + .into()) } } } @@ -139,16 +150,17 @@ async fn auth_xsts(token: String) -> PolyResult { let xsts_token = match response.get("Token") { Some(token) => token.as_str().unwrap(), None => { - return Err(PolyError::AuthError(AuthenticationError::MicrosoftError( + return Err(ErrorKind::AuthError(AuthenticationError::MicrosoftError( MicrosoftAuthError::XSTSToken, - ))) + )) + .into()) } }; Ok(xsts_token.to_string()) } -async fn auth_xbl(code: String) -> PolyResult<(String, String)> { +async fn auth_xbl(code: String) -> crate::Result<(String, String)> { let response = create_client()? .post("https://user.auth.xboxlive.com/user/authenticate") .json(&json!({ @@ -167,9 +179,10 @@ async fn auth_xbl(code: String) -> PolyResult<(String, String)> { let xbl_token = match response.get("Token") { Some(token) => token.as_str().unwrap(), None => { - return Err(PolyError::AuthError(AuthenticationError::MicrosoftError( + return Err(ErrorKind::AuthError(AuthenticationError::MicrosoftError( MicrosoftAuthError::XBLToken, - ))) + )) + .into()) } }; @@ -180,21 +193,25 @@ async fn auth_xbl(code: String) -> PolyResult<(String, String)> { }) { Some(hash) => hash.as_str().unwrap(), None => { - return Err(PolyError::AuthError(AuthenticationError::MicrosoftError( + return Err(ErrorKind::AuthError(AuthenticationError::MicrosoftError( MicrosoftAuthError::XBLUserHash, - ))) + )) + .into()) } }; Ok((xbl_token.to_string(), user_hash.to_string())) } -async fn msa_code_to_token(code: String) -> PolyResult { +async fn msa_code_to_token(code: String) -> crate::Result { let token = create_client()? .post("https://login.live.com/oauth20_token.srf") .form(&[ ("client_id", CLIENT_ID), - ("redirect_uri", &format!("http://localhost:{}/", constants::MSA_PORT)), + ( + "redirect_uri", + &format!("http://localhost:{}/", constants::MSA_PORT), + ), ("code", &code), ("grant_type", "authorization_code"), ]) @@ -206,19 +223,22 @@ async fn msa_code_to_token(code: String) -> PolyResult { Ok(token["access_token"].as_str().unwrap().to_string()) } -async fn msa_code() -> PolyResult { +async fn msa_code() -> crate::Result { 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, constants::MSA_PORT ); - open::that(url)?; + open::that(url)?; let mut token: String = String::new(); let listener = TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], constants::MSA_PORT)))?; - println!("Started local server for MSA on port {}", constants::MSA_PORT); + println!( + "Started local server for MSA on port {}", + constants::MSA_PORT + ); for conn in listener.incoming() { match conn { diff --git a/core/src/auth/mod.rs b/onelauncher/src/auth/mod.rs similarity index 81% rename from core/src/auth/mod.rs rename to onelauncher/src/auth/mod.rs index c18e1efc..0ad3eb44 100644 --- a/core/src/auth/mod.rs +++ b/onelauncher/src/auth/mod.rs @@ -1,12 +1,11 @@ use anyhow::anyhow; use serde::{Deserialize, Serialize}; -use thiserror::Error; -use crate::{utils::http, PolyError, PolyResult}; +use crate::{utils::http, ErrorKind}; pub mod microsoft_auth; -#[derive(Error, Debug)] +#[derive(thiserror::Error, Debug)] pub enum AuthenticationError { #[error(transparent)] MicrosoftError(#[from] microsoft_auth::MicrosoftAuthError), @@ -31,11 +30,11 @@ pub struct AccountSkin { 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(stage: F) -> PolyResult + async fn auth(stage: F) -> crate::Result where F: Fn(String, u8, bool) -> (); - async fn get_profile(access_token: String) -> PolyResult { + async fn get_profile(access_token: String) -> crate::Result { let response = http::create_client()? .get("https://api.minecraftservices.com/minecraft/profile") .header("Authorization", format!("Bearer {}", access_token)) @@ -44,7 +43,7 @@ pub trait AuthenticationMethod { let response = response.json::().await?; if let Some(error) = response.get("error") { - return Err(PolyError::AnyhowError(anyhow!(error.to_string()))); + return Err(ErrorKind::AnyhowError(anyhow!(error.to_string())).into()); } let account = serde_json::from_value::(response)?; diff --git a/core/src/constants.rs b/onelauncher/src/constants.rs similarity index 57% rename from core/src/constants.rs rename to onelauncher/src/constants.rs index efae1cdd..fad91da7 100644 --- a/core/src/constants.rs +++ b/onelauncher/src/constants.rs @@ -1,3 +1,7 @@ +// Basic Metadata +pub const NAME: &str = "OneLauncher"; +pub const VERSION: &str = "0.1.0"; + // Authentication and HTTP (TODO: env?) pub const APP_CONFIG_DIR: &str = "org.polyfrost.launcher"; pub const USER_AGENT: &str = "OneLauncher/1.0.0 (polyfrost.org)"; @@ -7,6 +11,7 @@ pub const CLIENT_ID: &str = "9419b7ee-1448-4d1b-b52a-550d8f36ab56"; pub const MSA_PORT: u16 = 13523; // Minecraft -pub const MINECRAFT_VERSIONS_MANIFEST: &str = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json"; +pub const MINECRAFT_VERSIONS_MANIFEST: &str = + "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json"; -// Java Downloading \ No newline at end of file +// Java Downloading diff --git a/onelauncher/src/error.rs b/onelauncher/src/error.rs new file mode 100644 index 00000000..829d6247 --- /dev/null +++ b/onelauncher/src/error.rs @@ -0,0 +1,117 @@ +use tracing_error::InstrumentError; + +/// A standardized [`thiserror::Error`] type that is used across the launcher +#[derive(thiserror::Error, Debug)] +pub enum ErrorKind { + /// Wrapper around [`tokio::sync::AcquireError`] to handle sempahore errors. + #[error("failed to manage a tokio semaphore: {0}")] + AcquireError(#[from] tokio::sync::AcquireError), + + /// Wrapper around [`tokio::sync::oneshot::error::RecvError`] to handle recieve errors. + #[error("tokio recieve error: {0}")] + RecvError(#[from] tokio::sync::oneshot::error::RecvError), + + /// Wrapper around [`tokio::task::JoinError`] to handle tokio join handle errors. + #[error("tokio join handle error: {0}")] + JoinError(#[from] tokio::task::JoinError), + + /// Wrapper around [`notify::Error`] to handle file watching errors. + #[error("failed to watch file: {0}")] + NotifyError(#[from] notify::Error), + + /// Wrapper around [`flate2::CompressError`] to handle flate compression errors. + #[error("failed to compress a file with flate: {0}")] + FlateError(#[from] flate2::CompressError), + + /// Wrapper around [`flate2::DecompressError`] to handle flate decompression errors. + #[error("failed to decompress a file with flate: {0}")] + DeflateError(#[from] flate2::DecompressError), + + /// Wrapper around [`uuid::Error`] to handle UUID parsing errors. + #[error("failed to parse uuids: {0}")] + UUIDError(#[from] uuid::Error), + + /// Wrapper around [`serde_json::Error`] to handle Serde JSON parsing errors. + #[error("failed to manage serialization of json: {0}")] + SerdeError(#[from] serde_json::Error), + + /// Wrapper around [`std::io::Error`] only to be used if a more defined error isn't available (e.g. when using `zip`) + #[error("failed to manage writing and reading of files: {0}")] + IOError(#[from] std::io::Error), + + /// Wrapper around [`reqwest::Error`] to handle HTTP errors. + #[error("failed to establish a HTTP connection: {0}")] + HTTPError(#[from] reqwest::Error), + + /// Wrapper around [`zip::result::ZipError`] to handle zip errors. + #[error("failed to manage zip files: {0}")] + ZipError(#[from] zip::result::ZipError), + + /// Wrapper around [`anyhow::Error`] to handle generic errors. + #[error(transparent)] + AnyhowError(#[from] anyhow::Error), + + /// Wrapper around [`chrono::ParseError`] to handle date parsing errors. + #[error("failed to parse a date: {0}")] + ChronoError(#[from] chrono::ParseError), + + /// Wrapper around [`crate::game::java::JavaDownloadError`] to handle Java executable downloading. + #[error("failed to download java: {0}")] + JavaError(#[from] crate::game::java::JavaDownloadError), + + /// Wrapper around [`crate::auth::AuthenticationError`] to handle authentication errors. + #[error("failed to authenticate: {0}")] + AuthError(#[from] crate::auth::AuthenticationError), + + /// Wrapper around [`crate::utils::dirs::DirectoryError`] to handle directory errors. + #[error("failed to get directory: {0}")] + DirectoryError(#[from] crate::utils::dirs::DirectoryError), + + /// Wrapper around [`crate::game::client::ClientManagerError`] to handle directory errors. + #[error("failed to manage clients: {0}")] + ClientManagerError(#[from] crate::game::client::ClientManagerError), + + #[cfg(feature = "tauri")] + #[error("failed to launch tauri: {0}")] + TauriError(#[from] tauri::Error), +} + +#[derive(Debug)] +pub struct Error { + source: tracing_error::TracedError, +} + +impl From for String { + fn from(value: Error) -> Self { + value.to_string() + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + self.source.source() + } +} + +impl std::fmt::Display for Error { + fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(fmt, "{}", self.source) + } +} + +impl> From for Error { + fn from(source: E) -> Self { + Self { + source: Into::::into(source).in_current_span(), + } + } +} + +impl ErrorKind { + pub fn as_error(self) -> Error { + self.into() + } +} + +/// Alias for a [`core::result::Result`] with the error type [`Error`]. +pub type Result = anyhow::Result; diff --git a/onelauncher/src/game/client.rs b/onelauncher/src/game/client.rs new file mode 100644 index 00000000..30ed7444 --- /dev/null +++ b/onelauncher/src/game/client.rs @@ -0,0 +1,304 @@ +use std::{collections::HashMap, fs, path::PathBuf}; + +use anyhow::anyhow; +use async_trait::async_trait; +use chrono::{DateTime, Local}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use uuid::Uuid; + +use crate::utils::dirs; + +use super::{ + minecraft::{MinecraftManifest, ReleaseType}, + vanilla::{self, VanillaClientProps}, +}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Version { + id: String, + url: String, + #[serde(default)] + release_type: ReleaseType, + #[serde(default)] + release_time: chrono::DateTime, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Manifest { + pub id: Uuid, + pub manifest: MinecraftManifest, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Instance { + pub id: Uuid, + pub created_at: DateTime, + pub name: String, + pub cover: Option, + pub group: Option, + pub client: ClientType, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type")] +pub enum ClientType { + Vanilla(VanillaClientProps), +} + +impl ClientType { + pub async fn get_versions(&self, file: Option<&PathBuf>) -> crate::Result> { + match self { + ClientType::Vanilla(_) => vanilla::get_versions(file).await, + } + } +} + +pub fn get_impl<'a>( + client: &'a ClientType, + instance: &'a Instance, + manifest: &'a Manifest, +) -> Box + 'a> { + Box::new(match client { + ClientType::Vanilla(_) => vanilla::VanillaClient::new(instance, manifest), + }) +} + +#[async_trait] +pub trait ClientTrait<'a>: Send + Sync { + fn new(instance: &'a Instance, manifest: &'a Manifest) -> Self + where + Self: Sized; + + async fn launch(&self) -> crate::Result<()>; + async fn setup(&self) -> crate::Result<()>; + + async fn install_game(&self) -> crate::Result; + async fn install_libraries(&self) -> crate::Result; + async fn install_natives(&self) -> crate::Result<()>; + async fn install_assets(&self) -> crate::Result<()>; + + fn get_instance(&self) -> &'a Instance + where + Self: Sized; + fn get_manifest(&self) -> &'a Manifest + where + Self: Sized; +} + +#[macro_export] +macro_rules! create_game_client { + ($props:ident { $($props_name:ident: $props_type:ty),* } $client:ident { $($name:ident: $type:ty),* }) => { + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] + pub struct $props { + $(pub $props_name: $props_type),* + } + + #[derive(Debug, Clone)] + pub struct $client<'a> { + pub instance: &'a crate::game::client::Instance, + pub manifest: &'a crate::game::client::Manifest, + $(pub $name: $type),* + } + }; +} + +#[macro_export] +macro_rules! impl_game_client { + () => { + fn get_instance(&self) -> &'a crate::game::client::Instance + where + Self: Sized, + { + self.instance + } + + fn get_manifest(&self) -> &'a crate::game::client::Manifest + where + Self: Sized, + { + self.manifest + } + }; +} + +#[derive(Debug, thiserror::Error)] +pub enum ClientManagerError { + #[error("Instance not found")] + InstanceNotFound, + #[error("Manifest not found")] + ManifestNotFound, +} + +pub struct ClientManager { + instances: HashMap, + manifests: HashMap, +} + +impl ClientManager { + pub fn new() -> crate::Result { + let instances = load_and_serialize::(&dirs::instances_dir()?)?; + + let manifests = load_and_serialize::(&dirs::manifests_dir()?)?; + + Ok(Self { + instances, + manifests, + }) + } + + pub fn get_impl_uuid<'a>(&'a self, uuid: Uuid) -> crate::Result + 'a>> { + let instance = self.get_instance(uuid)?; + let manifest = self.get_manifest(uuid)?; + + Ok(get_impl(&instance.client, &instance, &manifest)) + } + + pub fn get_instances(&self) -> Vec<&Instance> { + self.instances.values().collect() + } + + pub fn get_instances_owned(&self) -> Vec { + self.instances.values().cloned().collect() + } + + pub fn get_instance(&self, uuid: Uuid) -> crate::Result<&Instance> { + self.instances + .get(&uuid) + .ok_or(ClientManagerError::InstanceNotFound.into()) + } + + pub fn get_instance_mut(&mut self, uuid: Uuid) -> crate::Result<&mut Instance> { + self.instances + .get_mut(&uuid) + .ok_or(ClientManagerError::InstanceNotFound.into()) + } + + pub fn get_manifest(&self, uuid: Uuid) -> crate::Result<&Manifest> { + self.manifests + .get(&uuid) + .ok_or(ClientManagerError::ManifestNotFound.into()) + } + + pub fn get_manifest_mut(&mut self, uuid: Uuid) -> crate::Result<&mut Manifest> { + self.manifests + .get_mut(&uuid) + .ok_or(ClientManagerError::ManifestNotFound.into()) + } + + pub async fn create_instance( + &mut self, + name: String, + version: String, + cover: Option, + group: Option, + client: ClientType, + ) -> crate::Result { + let version_cache = dirs::app_config_dir()?.join("versions_cache.json"); + let versions = client.get_versions(Some(&version_cache)).await?; + let url = &versions + .iter() + .find(|v| v.id == version) + .ok_or(anyhow!("Version not found"))? + .url; + + 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( + &dirs::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( + &dirs::instances_dir()?.join(format!("{}.json", uuid)), + &instance, + )?; + self.instances.insert(uuid, instance); + + Ok(uuid) + } +} + +fn save(path: &PathBuf, object: T) -> crate::Result<()> +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(()) +} + +fn load_and_serialize(dir: &PathBuf) -> crate::Result> +where + T: DeserializeOwned, +{ + let mut result = HashMap::::new(); + + 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; + } + }; + + let path = file.path(); + let content = match fs::read_to_string(&path) { + Ok(content) => content, + Err(_) => { + eprintln!("Couldn't read file: '{:?}'", path); + continue; + } + }; + + let parsed = match serde_json::from_str::(&content) { + Ok(parsed) => parsed, + Err(_) => { + eprintln!("Couldn't parse file content as JSON: '{:?}'", content); + continue; + } + }; + + result.insert(uuid, parsed); + } + + Ok(result) +} diff --git a/onelauncher/src/game/java.rs b/onelauncher/src/game/java.rs new file mode 100644 index 00000000..483ee249 --- /dev/null +++ b/onelauncher/src/game/java.rs @@ -0,0 +1,159 @@ +use std::{fs, path::PathBuf}; + +use serde_json::Value; +use thiserror::Error; + +use crate::{ + utils::{file, http}, + ErrorKind, +}; + +#[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) -> crate::Result { + if !dir.exists() { + match fs::create_dir_all(&dir) { + Ok(_) => (), + Err(err) => { + return Err(ErrorKind::JavaError(JavaDownloadError::PermissionDenied( + err.to_string(), + )) + .into()) + } + }; + } + + let os = std::env::consts::OS; + let archive_type = match os { + "windows" => "zip", + "macos" => "tar.gz", + "linux" => "tar.gz", + _ => { + return Err( + ErrorKind::JavaError(JavaDownloadError::UnsupportedOS(os.to_string())).into(), + ) + } + }; + + 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(ErrorKind::JavaError(JavaDownloadError::NoJavaVersionFound( + "unable to get the java executable file".to_string(), + )) + .into()) + } + }; + + return Ok(file.join("bin").join("java")); + } + + Err(ErrorKind::JavaError(JavaDownloadError::NoJavaVersionFound( + "unable to download the java executable file".to_string(), + )) + .into()) +} + +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, +) -> crate::Result<()> { + 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(ErrorKind::JavaError(JavaDownloadError::DownloadError(err.to_string())).into()); + }; + + Ok(()) +} + +fn extract(archive: &PathBuf, dest: &PathBuf) -> crate::Result<()> { + if let Err(err) = file::extract_archive(archive.as_path(), dest.as_path()) { + let _ = fs::remove_file(dest.as_path()); + return Err(ErrorKind::JavaError(JavaDownloadError::ExtractError(err.to_string())).into()); + } + + let _ = fs::remove_file(archive.as_path()); + Ok(()) +} + +async fn get_java_versions(java_version: u8, os: &str, archive_type: &str) -> crate::Result { + 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::().await { + Ok(json) => json, + Err(err) => { + return Err(ErrorKind::JavaError(JavaDownloadError::NoJavaVersionFound( + err.to_string(), + )) + .into()) + } + }, + Err(err) => { + return Err(ErrorKind::JavaError(JavaDownloadError::NoJavaVersionFound( + err.to_string(), + )) + .into()) + } + }; + + if !response.is_array() || response.as_array().unwrap().is_empty() { + return Err(ErrorKind::JavaError(JavaDownloadError::NoJavaVersionFound( + "didn't get the expected java api crate::Result".to_string(), + )) + .into()); + } + + Ok(response) +} diff --git a/onelauncher/src/game/minecraft.rs b/onelauncher/src/game/minecraft.rs new file mode 100644 index 00000000..217059f0 --- /dev/null +++ b/onelauncher/src/game/minecraft.rs @@ -0,0 +1,209 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +#[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, + pub logging: Logging, + pub main_class: String, + pub arguments: Arguments, + pub release_time: String, + #[serde(rename = "type")] + pub release_type: ReleaseType, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[serde(untagged)] +pub enum Arguments { + // TODO: https://serde.rs/string-or-struct.html + MinecraftArguments(String), + Arguments { + game: Vec, + jvm: Vec, + }, +} + +impl Default for Arguments { + fn default() -> Self { + Arguments::MinecraftArguments(String::new()) + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[serde(untagged)] +pub enum ModernArgumentsItem { + Simple(String), + Rule { + rules: Vec, + value: ArgumentRuleValue, + }, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[serde(untagged)] +pub enum ArgumentRuleValue { + String(String), + List(Vec), +} + +#[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, + #[serde(skip_serializing_if = "Option::is_none")] + pub natives: Option, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Downloads2 { + #[serde(skip_serializing_if = "Option::is_none")] + pub artifact: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub classifiers: Option, +} + +#[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, + + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "natives-linux")] + pub natives_linux: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "natives-windows")] + pub natives_windows: Option, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Native { + pub path: String, + pub sha1: String, + pub url: String, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Rule { + pub action: RuleAction, + + #[serde(skip_serializing_if = "Option::is_none")] + pub os: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub features: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[serde(untagged)] +pub enum RuleAction { + Allow, + Disallow, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Os { + pub name: Option, + pub arch: Option, + pub version: Option, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Natives { + pub windows: Option, + 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 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, +} diff --git a/core/src/game/mod.rs b/onelauncher/src/game/mod.rs similarity index 71% rename from core/src/game/mod.rs rename to onelauncher/src/game/mod.rs index be79bbce..af961ef6 100644 --- a/core/src/game/mod.rs +++ b/onelauncher/src/game/mod.rs @@ -1,4 +1,4 @@ pub mod client; pub mod java; +pub mod minecraft; pub mod vanilla; -pub mod minecraft; \ No newline at end of file diff --git a/onelauncher/src/game/vanilla.rs b/onelauncher/src/game/vanilla.rs new file mode 100644 index 00000000..1f563394 --- /dev/null +++ b/onelauncher/src/game/vanilla.rs @@ -0,0 +1,144 @@ +use super::{ + client::{ClientTrait, Instance, Manifest, Version}, + minecraft::MinecraftManifest, +}; +use crate::{ + constants::MINECRAFT_VERSIONS_MANIFEST, + create_game_client, impl_game_client, + utils::{dirs, file, http}, +}; +use anyhow::anyhow; +use async_trait::async_trait; +use serde::{Deserialize, Serialize}; +use std::{fs, path::PathBuf}; + +create_game_client! { + VanillaClientProps {} + + VanillaClient {} +} + +#[async_trait] +impl<'a> ClientTrait<'a> for VanillaClient<'a> { + impl_game_client!(); + + fn new(instance: &'a Instance, manifest: &'a Manifest) -> Self { + VanillaClient { instance, manifest } + } + + async fn launch(&self) -> crate::Result<()> { + Ok(()) + } + + async fn setup(&self) -> crate::Result<()> { + Ok(()) + } + + async fn install_game(&self) -> crate::Result { + let manifest = &self.manifest.manifest; + let file = dirs::clients_dir()?.join(format!("{}.jar", manifest.version)); + + if !file.exists() { + fs::create_dir_all(&file)?; + let artifact = &manifest.downloads.client; + + http::download_file(&artifact.url, &file).await?; + let file_hash = file::file_sha1(&file)?; + + println!("Downloaded: '{}' | '{}'", file_hash, artifact.sha1); + if file_hash != artifact.sha1 { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Hashes do not match", + ) + .into()); + } + } + + Ok(file) + } + + async fn install_libraries(&self) -> crate::Result { + Ok("".to_string()) + } + + async fn install_natives(&self) -> crate::Result<()> { + Ok(()) + } + + async fn install_assets(&self) -> crate::Result<()> { + Ok(()) + } +} + +#[derive(Clone, Serialize, Deserialize)] +struct VersionList { + versions: Vec, +} + +#[derive(Serialize, Deserialize)] +struct CachedVersions { + last_updated: i64, + versions: Vec, +} + +pub async fn get_versions(file: Option<&PathBuf>) -> crate::Result> { + if let Some(file) = file { + if file.exists() && file.is_file() { + if let Some(versions) = get_cached_versions(file).await? { + return Ok(versions); + } + } + } + + let response = http::create_client()? + .get(MINECRAFT_VERSIONS_MANIFEST) + .send() + .await?; + let response = response.json::().await?; + + if let Some(file) = file { + fs::write( + file, + serde_json::to_string(&CachedVersions { + last_updated: chrono::Utc::now().timestamp(), + versions: response.versions.clone(), + })?, + )?; + } + + Ok(response.versions) +} + +async fn get_cached_versions(file: &PathBuf) -> crate::Result>> { + let cached = serde_json::from_str::(&fs::read_to_string(file)?)?; + let head_request = http::create_client()? + .head(MINECRAFT_VERSIONS_MANIFEST) + .send() + .await?; + + let last_updated = head_request + .headers() + .get("Last-Modified") + .ok_or(anyhow!("Last-Modified header not found"))?; + let last_updated = last_updated + .to_str() + .map_err(|_| anyhow!("Failed to convert Last-Modified header to string"))?; + let last_updated = chrono::DateTime::parse_from_rfc2822(last_updated)?; + + if cached.last_updated < last_updated.timestamp() { + Ok(None) + } else { + Ok(Some(cached.versions)) + } +} + +pub async fn retrieve_version_manifest(url: &str) -> crate::Result { + let manifest = http::create_client()? + .get(url) + .send() + .await? + .json::() + .await?; + Ok(manifest) +} diff --git a/onelauncher/src/lib.rs b/onelauncher/src/lib.rs new file mode 100644 index 00000000..2e6fa70e --- /dev/null +++ b/onelauncher/src/lib.rs @@ -0,0 +1,12 @@ +#![warn(unused_import_braces)] +#![deny(unused_must_use)] + +pub mod auth; +pub mod constants; +pub mod error; +pub mod game; +pub mod logger; +pub mod utils; + +pub use error::*; +pub use logger::start_logger; diff --git a/onelauncher/src/logger.rs b/onelauncher/src/logger.rs new file mode 100644 index 00000000..f4558665 --- /dev/null +++ b/onelauncher/src/logger.rs @@ -0,0 +1,54 @@ +use tracing_appender::non_blocking::WorkerGuard; + +#[cfg(debug_assertions)] +pub fn start_logger() -> Option { + use tracing_subscriber::prelude::*; + + let filter = tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| { + tracing_subscriber::EnvFilter::new("onelauncher=info,onelauncher_gui=info") + }); + + let subscriber = tracing_subscriber::registry() + .with(tracing_subscriber::fmt::layer()) + .with(filter) + .with(tracing_error::ErrorLayer::default()); + + tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); + + None +} + +#[cfg(not(debug_assertions))] +pub fn start_logger() -> Option { + use crate::game::client::DirectoryInfo; + use tracing_appender::rolling::{RollingFileAppender, Rotation}; + use tracing_subscriber::fmt::time::ChronoLocal; + use tracing_subscriber::prelude::*; + + let logs_dir = if let Some(directory) = DirectoryInfo::logs_dir() { + directory + } else { + eprintln!("failed to start logger"); + return None; + }; + + let filter = tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("onelauncher=info")); + + let file_appender = RollingFileAppender::new(Rotation::DAILY, logs_dir, "onelauncher.log"); + let (non_blocking, guard) = tracing_appender::non_blocking(file_appender); + + let subscriber = tracing_subscriber::registry() + .with( + tracing_subscriber::fmt::layer() + .with_writer(non_blocking) + .with_ansi(false) + .with_timer(ChronoLocal::rfc3339()), + ) + .with(filter) + .with(tracing_error::ErrorLayer::default()); + + tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); + + Some(guard); +} diff --git a/onelauncher/src/utils/dirs.rs b/onelauncher/src/utils/dirs.rs new file mode 100644 index 00000000..54edbff5 --- /dev/null +++ b/onelauncher/src/utils/dirs.rs @@ -0,0 +1,35 @@ +use std::path::PathBuf; + +use crate::constants; + +#[derive(Debug, thiserror::Error)] +pub enum DirectoryError { + #[error("failed to get the user's config directory")] + ConfigDir, +} + +pub fn app_config_dir() -> crate::Result { + dirs::config_dir() + .and_then(|dir| Some(dir.join(constants::APP_CONFIG_DIR))) + .ok_or(DirectoryError::ConfigDir.into()) +} + +pub fn libraries_dir() -> crate::Result { + Ok(app_config_dir()?.join("libraries")) +} + +pub fn java_dir() -> crate::Result { + Ok(app_config_dir()?.join("java")) +} + +pub fn instances_dir() -> crate::Result { + Ok(app_config_dir()?.join("instances")) +} + +pub fn manifests_dir() -> crate::Result { + Ok(app_config_dir()?.join("manifests")) +} + +pub fn clients_dir() -> crate::Result { + Ok(app_config_dir()?.join("clients")) +} diff --git a/onelauncher/src/utils/file.rs b/onelauncher/src/utils/file.rs new file mode 100644 index 00000000..94e08365 --- /dev/null +++ b/onelauncher/src/utils/file.rs @@ -0,0 +1,52 @@ +use anyhow::anyhow; +use sha1::{Digest, Sha1}; +use std::{ + fs::File, + io::BufReader, + path::{Path, PathBuf}, +}; + +use crate::ErrorKind; + +pub fn file_sha1(file: &PathBuf) -> crate::Result { + let file = File::open(file)?; + let reader = BufReader::new(file); + + let mut hasher = Sha1::new(); + hasher.update(&reader.buffer()); + let hash = hasher.finalize(); + + Ok(format!("{:x}", hash)) +} + +pub fn extract_archive(archive: &Path, dest: &Path) -> crate::Result<()> { + let ext = match archive.extension() { + Some(ext) => ext.to_str().unwrap(), + None => return Err(ErrorKind::AnyhowError(anyhow!("unsupported operating system")).into()), + }; + + match ext { + "zip" => extract_zip(archive, dest), + "gz" => extract_tar_gz(archive, dest), + _ => Err(ErrorKind::AnyhowError(anyhow!("unsupported file extension {:?}", ext)).into()), + } +} + +pub fn extract_zip(archive: &Path, dest: &Path) -> crate::Result<()> { + let file = File::open(archive).map_err(|err| ErrorKind::IOError(err))?; + let archive = zip::ZipArchive::new(file).map_err(|err| ErrorKind::ZipError(err)); + archive? + .extract(dest) + .map_err(|err| ErrorKind::ZipError(err))?; + Ok(()) +} + +pub fn extract_tar_gz(archive: &Path, dest: &Path) -> crate::Result<()> { + let file = File::open(archive).map_err(|err| ErrorKind::IOError(err))?; + let tar_gz = flate2::read::GzDecoder::new(file); + let mut archive = tar::Archive::new(tar_gz); + archive + .unpack(dest) + .map_err(|err| ErrorKind::IOError(err))?; + Ok(()) +} diff --git a/onelauncher/src/utils/http.rs b/onelauncher/src/utils/http.rs new file mode 100644 index 00000000..0524ecf9 --- /dev/null +++ b/onelauncher/src/utils/http.rs @@ -0,0 +1,34 @@ +use std::{ + fs::File, + io::{self, Cursor}, + path::Path, +}; + +use reqwest::{Client, ClientBuilder}; + +use crate::{constants::USER_AGENT, ErrorKind}; + +pub fn create_client() -> crate::Result { + Ok(ClientBuilder::new() + .user_agent(USER_AGENT) + .build() + .map_err(|err| ErrorKind::HTTPError(err))?) +} + +// can we not make mapping the errors automatic, isnt that the whole point +pub async fn download_file(url: &str, path: &Path) -> crate::Result<()> { + let response = create_client()? + .get(url) + .send() + .await + .map_err(|err| ErrorKind::HTTPError(err))?; + let mut file = File::create(path).map_err(|err| ErrorKind::IOError(err))?; + let mut content = Cursor::new( + response + .bytes() + .await + .map_err(|err| ErrorKind::HTTPError(err))?, + ); + io::copy(&mut content, &mut file).map_err(|err| ErrorKind::IOError(err))?; + Ok(()) +} diff --git a/core/src/utils/mod.rs b/onelauncher/src/utils/mod.rs similarity index 66% rename from core/src/utils/mod.rs rename to onelauncher/src/utils/mod.rs index 5980c9dc..57a94ec7 100644 --- a/core/src/utils/mod.rs +++ b/onelauncher/src/utils/mod.rs @@ -1,3 +1,3 @@ +pub mod dirs; pub mod file; pub mod http; -pub mod dirs; \ No newline at end of file diff --git a/package.json b/package.json index 36965bc3..1d5fe385 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,6 @@ }, "dependencies": { "@fontsource/poppins": "^5.0.12", - "@sentry/vite-plugin": "^2.16.0", "@solidjs/router": "^0.13.1", "@tauri-apps/api": "^2.0.0-beta", "@tauri-apps/plugin-shell": "github:tauri-apps/tauri-plugin-shell#v2", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index a24056cd..2bb64d9d 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "polyfrost-launcher" +name = "onelauncher_gui" version = "0.0.0" description = "A Tauri App" license = { workspace = true } @@ -14,10 +14,12 @@ authors = { workspace = true } tauri-build = { workspace = true } [dependencies] -launcher-core = { path = "../core" } +onelauncher = { path = "../onelauncher", features = [ "tauri" ] } + tauri = { workspace = true } tauri-plugin-shell = { workspace = true } -tauri-plugin-http = { workspace = true } +tauri-plugin-window-state = { workspace = true } +tauri-plugin-single-instance = { workspace = true } tokio = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } @@ -27,6 +29,9 @@ anyhow = { workspace = true } chrono = { workspace = true } uuid = { workspace = true } +tracing = { workspace = true } +tracing-error = { workspace = true } + [features] # This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!! custom-protocol = [ "tauri/custom-protocol" ] diff --git a/src-tauri/build.rs b/src-tauri/build.rs index 12b1a1d4..e739fd85 100644 --- a/src-tauri/build.rs +++ b/src-tauri/build.rs @@ -1,18 +1,15 @@ use tauri_build::{Attributes, InlinedPlugin}; fn main() { - let _ = tauri_build::try_build( - Attributes::new() - .plugin( - "launcher-core", - InlinedPlugin::new().commands(&[ - "login_msa", - "refresh_client_manager", - "create_instance", - "get_instances", - "get_instance", - "get_manifest" - ]), - ), - ); + let _ = tauri_build::try_build(Attributes::new().plugin( + "onelauncher", + InlinedPlugin::new().commands(&[ + "login_msa", + "refresh_client_manager", + "create_instance", + "get_instances", + "get_instance", + "get_manifest", + ]), + )); } diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 8f3d3e12..fbd04506 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -7,7 +7,7 @@ "*" ], "permissions": [ - "launcher-core:default", + "onelauncher:default", "window:allow-close", "window:allow-destroy", "window:allow-start-dragging", diff --git a/src-tauri/permissions/launcher-core/default.toml b/src-tauri/permissions/onelauncher/default.toml similarity index 100% rename from src-tauri/permissions/launcher-core/default.toml rename to src-tauri/permissions/onelauncher/default.toml diff --git a/src-tauri/src/launcher_core/auth.rs b/src-tauri/src/api/auth.rs similarity index 76% rename from src-tauri/src/launcher_core/auth.rs rename to src-tauri/src/api/auth.rs index 536d8216..67e81ca7 100644 --- a/src-tauri/src/launcher_core/auth.rs +++ b/src-tauri/src/api/auth.rs @@ -1,4 +1,6 @@ -use launcher_core::auth::{microsoft_auth::MicrosoftAuthenticationMethod, Account, AuthenticationMethod}; +use launcher_core::auth::{ + microsoft_auth::MicrosoftAuthenticationMethod, Account, AuthenticationMethod, +}; use tauri::Manager; #[tauri::command] @@ -11,4 +13,4 @@ pub async fn login_msa(app: tauri::AppHandle) -> Result Ok(account), Err(err) => Err(err.to_string()), } -} \ No newline at end of file +} diff --git a/src-tauri/src/api/game.rs b/src-tauri/src/api/game.rs new file mode 100644 index 00000000..48820062 --- /dev/null +++ b/src-tauri/src/api/game.rs @@ -0,0 +1,50 @@ +use launcher_core::game::client::{ClientType, Instance, Manifest}; +use tauri::State; +use tokio::sync::Mutex; +use uuid::Uuid; + +use super::GameManagerState; + +#[tauri::command] +pub async fn create_instance( + state: State<'_, Mutex>, + name: String, + version: String, + client: ClientType, + cover: Option, + group: Option, +) -> Result { + let manager = &mut state.lock().await.client_manager; + let uuid = manager + .create_instance(name, version, cover, group, client) + .await?; + Ok(uuid) +} + +#[tauri::command] +pub async fn get_instances( + state: State<'_, Mutex>, +) -> Result, String> { + let manager = &mut state.lock().await.client_manager; + Ok(manager.get_instances_owned()) +} + +#[tauri::command] +pub async fn get_instance( + state: State<'_, Mutex>, + uuid: Uuid, +) -> Result { + let manager = &mut state.lock().await.client_manager; + let instance = manager.get_instance(uuid)?; + Ok(instance.clone()) +} + +#[tauri::command] +pub async fn get_manifest( + state: State<'_, Mutex>, + uuid: Uuid, +) -> Result { + let manager = &mut state.lock().await.client_manager; + let manifest = manager.get_manifest(uuid)?; + Ok(manifest.clone()) +} diff --git a/src-tauri/src/api/mod.rs b/src-tauri/src/api/mod.rs new file mode 100644 index 00000000..de534859 --- /dev/null +++ b/src-tauri/src/api/mod.rs @@ -0,0 +1,41 @@ +use launcher_core::game::client::ClientManager; +use tauri::{plugin::TauriPlugin, Manager, State}; +use tokio::sync::Mutex; + +mod auth; +mod game; + +pub struct GameManagerState { + pub client_manager: ClientManager, +} + +unsafe impl Send for GameManagerState {} +unsafe impl Sync for GameManagerState {} + +pub fn init() -> TauriPlugin { + tauri::plugin::Builder::new("onelauncher") + .setup(|app, _| { + app.manage(Mutex::new(GameManagerState { + client_manager: ClientManager::new().expect("Failed to initialize client manager"), + })); + + Ok(()) + }) + .invoke_handler(tauri::generate_handler![ + auth::login_msa, + game::create_instance, + game::get_instances, + game::get_instance, + game::get_manifest, + refresh_client_manager, + ]) + .build() +} + +#[tauri::command] +#[tracing::instrument(skip_all)] +async fn refresh_client_manager(state: State<'_, Mutex>) -> Result<(), String> { + let mut state = state.lock().await; + state.client_manager = ClientManager::new()?; + Ok(()) +} diff --git a/src-tauri/src/constants.rs b/src-tauri/src/constants.rs deleted file mode 100644 index 7927316a..00000000 --- a/src-tauri/src/constants.rs +++ /dev/null @@ -1,3 +0,0 @@ -// Basic Metadata -pub const NAME: &str = "OneLauncher"; -pub const VERSION: &str = "0.1.0"; diff --git a/src-tauri/src/error.rs b/src-tauri/src/error.rs new file mode 100644 index 00000000..be05d925 --- /dev/null +++ b/src-tauri/src/error.rs @@ -0,0 +1,18 @@ +use tracing_error::ExtractSpanTrace; + +pub fn display_tracing_error(err: &launcher_core::Error) { + match get_span_trace(err) { + Some(span_trace) => { + tracing::error!(error = %err, span_trace = %span_trace); + } + None => { + tracing::error!(error = %err); + } + } +} + +pub fn get_span_trace<'a>( + error: &'a (dyn std::error::Error + 'static), +) -> Option<&'a tracing_error::SpanTrace> { + error.source().and_then(|e| e.span_trace()) +} diff --git a/src-tauri/src/launcher_core/game.rs b/src-tauri/src/launcher_core/game.rs deleted file mode 100644 index df1e7a09..00000000 --- a/src-tauri/src/launcher_core/game.rs +++ /dev/null @@ -1,48 +0,0 @@ -use launcher_core::game::client::{ClientType, Instance, Manifest}; -use tauri::State; -use tokio::sync::Mutex; -use uuid::Uuid; - -use super::GameManagerState; - -#[tauri::command] -pub async fn create_instance( - state: State<'_, Mutex>, - name: String, - version: String, - client: ClientType, - cover: Option, - group: Option, -) -> Result { - let manager = &mut state.lock().await.client_manager; - let uuid = manager.create_instance(name, version, cover, group, client).await?; - Ok(uuid) -} - -#[tauri::command] -pub async fn get_instances( - state: State<'_, Mutex> -) -> Result, String> { - let manager = &mut state.lock().await.client_manager; - Ok(manager.get_instances_owned()) -} - -#[tauri::command] -pub async fn get_instance( - state: State<'_, Mutex>, - uuid: Uuid -) -> Result { - let manager = &mut state.lock().await.client_manager; - let instance = manager.get_instance(uuid)?; - Ok(instance.clone()) -} - -#[tauri::command] -pub async fn get_manifest( - state: State<'_, Mutex>, - uuid: Uuid -) -> Result { - let manager = &mut state.lock().await.client_manager; - let manifest = manager.get_manifest(uuid)?; - Ok(manifest.clone()) -} \ No newline at end of file diff --git a/src-tauri/src/launcher_core/mod.rs b/src-tauri/src/launcher_core/mod.rs deleted file mode 100644 index 5a8a09aa..00000000 --- a/src-tauri/src/launcher_core/mod.rs +++ /dev/null @@ -1,42 +0,0 @@ -use launcher_core::game::client::ClientManager; -use tauri::{plugin::TauriPlugin, Manager, State}; -use tokio::sync::Mutex; - -mod auth; -mod game; - -pub struct GameManagerState { - pub client_manager: ClientManager, -} - -unsafe impl Send for GameManagerState {} -unsafe impl Sync for GameManagerState {} - -pub fn init() -> TauriPlugin { - tauri::plugin::Builder::new("launcher-core") - .setup(|app, _| { - app.manage(Mutex::new(GameManagerState { - client_manager: ClientManager::new().expect("Failed to initialize client manager"), - })); - - Ok(()) - }) - .invoke_handler(tauri::generate_handler![ - auth::login_msa, - game::create_instance, - game::get_instances, - game::get_instance, - game::get_manifest, - refresh_client_manager, - ]) - .build() -} - -#[tauri::command] -async fn refresh_client_manager( - state: State<'_, Mutex> -) -> Result<(), String> { - let mut state = state.lock().await; - state.client_manager = ClientManager::new()?; - Ok(()) -} \ No newline at end of file diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index da59eca8..c0bdfe56 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,2 +1,2 @@ -pub mod constants; -pub mod launcher_core; +pub mod api; +pub mod error; diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index b32a75d9..82f9f15b 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -3,14 +3,30 @@ use std::error::Error; -use polyfrost_launcher::launcher_core; +use polyfrost_launcher::api; use tauri::{menu::Menu, App}; +#[derive(Clone, serde::Serialize)] +struct Payload { + args: Vec, + cwd: String, +} + #[tokio::main] async fn main() { + let _log_guard = launcher_core::start_logger(); + tracing::info!("initialized tracing subscriber. loading OneLauncher"); + tauri::Builder::default() .plugin(tauri_plugin_shell::init()) - .plugin(launcher_core::init()) + .plugin(tauri_plugin_single_instance::init(|app, argv, cwd| { + println!("{}, {argv:?}, {cwd}", app.package_info().name); + + app.emit_all("single-instance", Payload { args: argv, cwd }) + .unwrap(); + })) + .plugin(tauri_plugin_window_state::Builder::default().build()) + .plugin(api::init()) .menu(|handle| Menu::new(handle)) .setup(setup) .run(tauri::generate_context!()) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 0dd434f2..9df5a541 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,16 +1,16 @@ { - "productName": "polyfrost-launcher", + "productName": "onelauncher", "version": "0.0.0", "identifier": "org.polyfrost.launcher", "app": { "withGlobalTauri": true, "security": { - "csp": null, + "csp": "default-src 'self'; connect-src https://modrinth.com https://*.modrinth.com https://polyfrost.org https://*.polyfrost.org https://*.cloudflare.com https://api.mclo.gs; img-src tauri: https: data: blob: 'unsafe-inline' asset: https://asset.localhost; script-src https://*.cloudflare.com 'self'; frame-src https://*.cloudflare.com https://www.youtube.com https://www.youtube-nocookie.com https://discord.com 'self'; style-src unsafe-inline 'self'", "capabilities": [] }, "windows": [ { - "title": "polyfrost-launcher", + "title": "OneLauncher", "width": 1280, "height": 802, "minWidth": 1080, @@ -31,6 +31,7 @@ "bundle": { "active": true, "targets": "all", + "category": "Game", "icon": [ "icons/32x32.png", "icons/128x128.png", @@ -38,7 +39,13 @@ "icons/icon.icns", "icons/icon.ico" ], - "copyright": "Polyfrost" + "copyright": "Polyfrost", + "fileAssociations": [ + { + "ext": ["mrpack"], + "description": "" + } + ] }, "plugins": {} } diff --git a/src/bridge/game.ts b/src/bridge/game.ts index f3dbcbda..6d4005a7 100644 --- a/src/bridge/game.ts +++ b/src/bridge/game.ts @@ -1,13 +1,13 @@ import { invoke } from '@tauri-apps/api/core'; export async function getInstance(uuid: string): Promise { - return await invoke('plugin:launcher-core|get_instance', { + return await invoke('plugin:onelauncher|get_instance', { uuid, }); } export async function getManifest(uuid: string): Promise { - return await invoke('plugin:launcher-core|get_manifest', { + return await invoke('plugin:onelauncher|get_manifest', { uuid, }); } @@ -39,12 +39,12 @@ export async function getGroupedInstances(): Promise { - return await invoke('plugin:launcher-core|get_instances'); + return await invoke('plugin:onelauncher|get_instances'); } export async function createInstance(instance: Omit & { version: string }): Promise { return await invoke( - 'plugin:launcher-core|create_instance', + 'plugin:onelauncher|create_instance', { name: instance.name, version: instance.version, @@ -61,5 +61,5 @@ export async function createInstance(instance: Omit { - return await invoke('plugin:launcher-core|refresh_client_manager'); + return await invoke('plugin:onelauncher|refresh_client_manager'); } diff --git a/vite.config.ts b/vite.config.ts index d59c5895..7375c252 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,14 +1,9 @@ import process from 'node:process'; -import type { Plugin } from 'vite'; +import type { Plugin, UserConfig } from 'vite'; import { defineConfig, loadEnv } from 'vite'; import solid from 'vite-plugin-solid'; -// idk if we are going to want to use sentry since we would have to selfhost -// we also need to consider self hosting a translation service -// and figuring out a way to implement analytics into this -import { sentryVitePlugin } from '@sentry/vite-plugin'; -// hopefully once we merge into nexus monorepo this can be one standard vite config const devtoolsPlugin: Plugin = { name: 'devtools-plugin', transformIndexHtml(html) { @@ -26,12 +21,19 @@ const devtoolsPlugin: Plugin = { export default defineConfig(async ({ mode }) => { process.env = { ...process.env, ...loadEnv(mode, process.cwd(), '') }; - const config = { + const config: UserConfig = { plugins: [ solid(), devtoolsPlugin, ], + envPrefix: ['VITE_', 'TAURI_'], + + build: { + minify: !process.env.TAURI_DEBUG ? 'esbuild' : false, + sourcemap: !!process.env.TAURI_DEBUG, + }, + clearScreen: false, server: { port: 8001, @@ -42,14 +44,5 @@ export default defineConfig(async ({ mode }) => { }, }; - if (process.env.SENTRY_AUTH_TOKEN) { - config.plugins.push(sentryVitePlugin({ - authToken: process.env.SENTRY_AUTH_TOKEN, - url: 'https://sentry.polyfrost.org', - org: 'polyfrost', - project: 'launcher', - })); - } - return config; });