hhhhh
This commit is contained in:
parent
2af86b5fda
commit
022936d35c
44 changed files with 1412 additions and 1149 deletions
30
Cargo.toml
30
Cargo.toml
|
|
@ -2,7 +2,7 @@
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
"src-tauri",
|
"src-tauri",
|
||||||
"core"
|
"onelauncher"
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
|
|
@ -18,14 +18,26 @@ authors = [ "Polyfrost" ]
|
||||||
tauri = { version = "2.0.0-beta", features = [] }
|
tauri = { version = "2.0.0-beta", features = [] }
|
||||||
tauri-build = { 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-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-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" ] }
|
uuid = { version = "1.8", features = [ "serde" ] }
|
||||||
tokio = { version = "1", features = [ "full" ] }
|
tokio = { version = "1", features = [ "full" ] }
|
||||||
serde = { version = "1", features = [ "derive" ] }
|
serde = { version = "1", features = [ "derive" ] }
|
||||||
chrono = { version = "0.4", features = [ "serde" ] }
|
chrono = { version = "0.4", features = [ "serde" ] }
|
||||||
reqwest = { version = "0.12", features = [ "json" ] }
|
reqwest = { version = "0.12", features = [
|
||||||
|
"json",
|
||||||
|
"stream",
|
||||||
|
] }
|
||||||
serde_json = { version = "1" }
|
serde_json = { version = "1" }
|
||||||
flate2 = { version = "1" }
|
flate2 = { version = "1" }
|
||||||
tar = { version = "0.4" }
|
tar = { version = "0.4" }
|
||||||
|
|
@ -35,6 +47,18 @@ thiserror = { version = "1" }
|
||||||
anyhow = { version = "1" }
|
anyhow = { version = "1" }
|
||||||
dirs = { version = "5" }
|
dirs = { version = "5" }
|
||||||
open = { 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]
|
[profile.dev]
|
||||||
# Make compilation faster on macOS
|
# Make compilation faster on macOS
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
# Polyfrost Launcher
|
# OneLauncher
|
||||||
|
|
|
||||||
|
|
@ -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<chrono::Utc>
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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<Local>,
|
|
||||||
pub name: String,
|
|
||||||
pub cover: Option<String>,
|
|
||||||
pub group: Option<Uuid>,
|
|
||||||
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<Vec<Version>> {
|
|
||||||
match self {
|
|
||||||
ClientType::Vanilla(_) => vanilla::get_versions(file).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_impl<'a>(client: &'a ClientType, instance: &'a Instance, manifest: &'a Manifest) -> Box<dyn ClientTrait<'a> + '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<PathBuf>;
|
|
||||||
async fn install_libraries(&self) -> PolyResult<String>;
|
|
||||||
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<Uuid, Instance>,
|
|
||||||
manifests: HashMap<Uuid, Manifest>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ClientManager {
|
|
||||||
pub fn new() -> PolyResult<Self> {
|
|
||||||
let instances = load_and_serialize::<Instance>(
|
|
||||||
&dirs::instances_dir()?
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let manifests = load_and_serialize::<Manifest>(
|
|
||||||
&dirs::manifests_dir()?
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
instances,
|
|
||||||
manifests,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_impl_uuid<'a>(&'a self, uuid: Uuid) -> PolyResult<Box<dyn ClientTrait<'a> + '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<Instance> {
|
|
||||||
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<String>,
|
|
||||||
group: Option<Uuid>,
|
|
||||||
client: ClientType
|
|
||||||
) -> PolyResult<Uuid> {
|
|
||||||
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<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(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_and_serialize<T>(dir: &PathBuf) -> PolyResult<HashMap<Uuid, T>> where T: DeserializeOwned {
|
|
||||||
let mut result = HashMap::<Uuid, T>::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::<T>(&content) {
|
|
||||||
Ok(parsed) => parsed,
|
|
||||||
Err(_) => {
|
|
||||||
eprintln!("Couldn't parse file content as JSON: '{:?}'", content);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
result.insert(uuid, parsed);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
|
|
@ -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<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)
|
|
||||||
}
|
|
||||||
|
|
@ -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<Library>,
|
|
||||||
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<String>,
|
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub arguments: Option<ModernArguments>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct ModernArguments {
|
|
||||||
game: Vec<ModernArgumentsItem>,
|
|
||||||
jvm: Vec<ModernArgumentsItem>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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<Rule>,
|
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
|
||||||
#[serde(deserialize_with = "string_or_seq")]
|
|
||||||
value: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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<Rule>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub natives: Option<HashMap<String, String>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct LibraryDownload {
|
|
||||||
#[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<Artifact>,
|
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
#[serde(rename = "natives-linux")]
|
|
||||||
pub natives_linux: Option<Artifact>,
|
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
#[serde(rename = "natives-windows")]
|
|
||||||
pub natives_windows: Option<Artifact>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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<Os>,
|
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub features: Option<HashMap<String, bool>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub arch: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub version: 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,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn string_or_seq<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
struct StringOrVec(PhantomData<Vec<String>>);
|
|
||||||
|
|
||||||
impl<'de> Visitor<'de> for StringOrVec {
|
|
||||||
type Value = Vec<String>;
|
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
formatter.write_str("string or list of strings")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
|
||||||
where
|
|
||||||
E: de::Error,
|
|
||||||
{
|
|
||||||
Ok(vec![value.into()])
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_seq<S>(self, seq: S) -> Result<Self::Value, S::Error>
|
|
||||||
where
|
|
||||||
S: SeqAccess<'de>,
|
|
||||||
{
|
|
||||||
Deserialize::deserialize(de::value::SeqAccessDeserializer::new(seq))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deserializer.deserialize_any(StringOrVec(PhantomData))
|
|
||||||
}
|
|
||||||
|
|
@ -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<PathBuf> {
|
|
||||||
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<String> {
|
|
||||||
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<Version>
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
struct CachedVersions {
|
|
||||||
last_updated: i64,
|
|
||||||
versions: Vec<Version>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_versions(file: Option<&PathBuf>) -> PolyResult<Vec<Version>> {
|
|
||||||
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::<VersionList>().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<Option<Vec<Version>>> {
|
|
||||||
let cached = serde_json::from_str::<CachedVersions>(&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<MinecraftManifest> {
|
|
||||||
let manifest = http::create_client()?.get(url).send().await?.json::<MinecraftManifest>().await?;
|
|
||||||
Ok(manifest)
|
|
||||||
}
|
|
||||||
|
|
@ -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<PolyError> for String {
|
|
||||||
fn from(value: PolyError) -> Self {
|
|
||||||
value.to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Alias for a [`Result`] with the error type [`PolyError`].
|
|
||||||
pub type PolyResult<T> = anyhow::Result<T, PolyError>;
|
|
||||||
|
|
@ -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<PathBuf> {
|
|
||||||
dirs::config_dir()
|
|
||||||
.and_then(|dir| Some(dir.join(constants::APP_CONFIG_DIR)))
|
|
||||||
.ok_or(DirectoryError::ConfigDir.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn libraries_dir() -> PolyResult<PathBuf> {
|
|
||||||
Ok(app_config_dir()?.join("libraries"))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn java_dir() -> PolyResult<PathBuf> {
|
|
||||||
Ok(app_config_dir()?.join("java"))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn instances_dir() -> PolyResult<PathBuf> {
|
|
||||||
Ok(app_config_dir()?.join("instances"))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn manifests_dir() -> PolyResult<PathBuf> {
|
|
||||||
Ok(app_config_dir()?.join("manifests"))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clients_dir() -> PolyResult<PathBuf> {
|
|
||||||
Ok(app_config_dir()?.join("clients"))
|
|
||||||
}
|
|
||||||
|
|
@ -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<String> {
|
|
||||||
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(())
|
|
||||||
}
|
|
||||||
|
|
@ -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<Client> {
|
|
||||||
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(())
|
|
||||||
}
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="theme-color" content="#000000" />
|
<meta name="theme-color" content="#000000" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/src/assets/logo.svg" />
|
<link rel="icon" type="image/svg+xml" href="/src/assets/logo.svg" />
|
||||||
<title>Polyfrost Launcher</title>
|
<title>OneLauncher</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
[package]
|
[package]
|
||||||
name = "launcher-core"
|
name = "onelauncher"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
description = "The core for our launcher"
|
description = "The core of OneLauncher"
|
||||||
license = { workspace = true }
|
license = { workspace = true }
|
||||||
edition = { workspace = true }
|
edition = { workspace = true }
|
||||||
repository = { workspace = true }
|
repository = { workspace = true }
|
||||||
|
|
@ -10,7 +10,19 @@ readme = { workspace = true }
|
||||||
homepage = { workspace = true }
|
homepage = { workspace = true }
|
||||||
authors = { workspace = true }
|
authors = { workspace = true }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
tauri = [ "dep:tauri" ]
|
||||||
|
cli = [ "dep:indicatif" ]
|
||||||
|
|
||||||
[dependencies]
|
[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 }
|
tokio = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
|
|
@ -25,5 +37,10 @@ chrono = { workspace = true }
|
||||||
reqwest = { workspace = true }
|
reqwest = { workspace = true }
|
||||||
dirs = { workspace = true }
|
dirs = { workspace = true }
|
||||||
open = { 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 }
|
||||||
|
|
@ -4,16 +4,19 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use serde_json::json;
|
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};
|
use super::{Account, AuthenticationError, AuthenticationMethod};
|
||||||
|
|
||||||
// TODO: store auth keys with https://beta.tauri.app/features/stronghold/
|
// TODO: store auth keys with https://beta.tauri.app/features/stronghold/
|
||||||
// TODO: store auth key public in prisma for easy access
|
// TODO: store auth key public in prisma for easy access
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum MicrosoftAuthError {
|
pub enum MicrosoftAuthError {
|
||||||
#[error("failed to get Minecraft access token")]
|
#[error("failed to get Minecraft access token")]
|
||||||
MSAccessToken,
|
MSAccessToken,
|
||||||
|
|
@ -23,7 +26,9 @@ pub enum MicrosoftAuthError {
|
||||||
XBLToken,
|
XBLToken,
|
||||||
#[error("failed to get XBL user hash")]
|
#[error("failed to get XBL user hash")]
|
||||||
XBLUserHash,
|
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,
|
NoXBoxAccount,
|
||||||
#[error("this account is from a country where xbox live is not available")]
|
#[error("this account is from a country where xbox live is not available")]
|
||||||
XBoxLiveBlocked,
|
XBoxLiveBlocked,
|
||||||
|
|
@ -37,7 +42,7 @@ pub enum MicrosoftAuthError {
|
||||||
|
|
||||||
pub struct MicrosoftAuthenticationMethod;
|
pub struct MicrosoftAuthenticationMethod;
|
||||||
impl AuthenticationMethod for MicrosoftAuthenticationMethod {
|
impl AuthenticationMethod for MicrosoftAuthenticationMethod {
|
||||||
async fn auth<F>(stage: F) -> PolyResult<Account>
|
async fn auth<F>(stage: F) -> crate::Result<Account>
|
||||||
where
|
where
|
||||||
F: Fn(String, u8, bool) -> (),
|
F: Fn(String, u8, bool) -> (),
|
||||||
{
|
{
|
||||||
|
|
@ -63,7 +68,7 @@ impl AuthenticationMethod for MicrosoftAuthenticationMethod {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn auth_minecraft(xsts_token: String, user_hash: String) -> PolyResult<String> {
|
async fn auth_minecraft(xsts_token: String, user_hash: String) -> crate::Result<String> {
|
||||||
let response = create_client()?
|
let response = create_client()?
|
||||||
.post("https://api.minecraftservices.com/authentication/login_with_xbox")
|
.post("https://api.minecraftservices.com/authentication/login_with_xbox")
|
||||||
.json(&json!({
|
.json(&json!({
|
||||||
|
|
@ -76,16 +81,17 @@ async fn auth_minecraft(xsts_token: String, user_hash: String) -> PolyResult<Str
|
||||||
let access_token = match response.get("access_token") {
|
let access_token = match response.get("access_token") {
|
||||||
Some(token) => token.as_str().unwrap(),
|
Some(token) => token.as_str().unwrap(),
|
||||||
None => {
|
None => {
|
||||||
return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(
|
return Err(ErrorKind::AuthError(AuthenticationError::MicrosoftError(
|
||||||
MicrosoftAuthError::MSAccessToken,
|
MicrosoftAuthError::MSAccessToken,
|
||||||
)))
|
))
|
||||||
|
.into())
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(access_token.to_string())
|
Ok(access_token.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn auth_xsts(token: String) -> PolyResult<String> {
|
async fn auth_xsts(token: String) -> crate::Result<String> {
|
||||||
let response = create_client()?
|
let response = create_client()?
|
||||||
.post("https://xsts.auth.xboxlive.com/xsts/authorize")
|
.post("https://xsts.auth.xboxlive.com/xsts/authorize")
|
||||||
.json(&json!({
|
.json(&json!({
|
||||||
|
|
@ -108,29 +114,34 @@ async fn auth_xsts(token: String) -> PolyResult<String> {
|
||||||
|
|
||||||
match error_code {
|
match error_code {
|
||||||
2148916233 => {
|
2148916233 => {
|
||||||
return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(
|
return Err(ErrorKind::AuthError(AuthenticationError::MicrosoftError(
|
||||||
MicrosoftAuthError::NoXBoxAccount,
|
MicrosoftAuthError::NoXBoxAccount,
|
||||||
)))
|
))
|
||||||
|
.into())
|
||||||
}
|
}
|
||||||
2148916235 => {
|
2148916235 => {
|
||||||
return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(
|
return Err(ErrorKind::AuthError(AuthenticationError::MicrosoftError(
|
||||||
MicrosoftAuthError::XBoxLiveBlocked,
|
MicrosoftAuthError::XBoxLiveBlocked,
|
||||||
)))
|
))
|
||||||
|
.into())
|
||||||
}
|
}
|
||||||
2148916236 | 2148916237 => {
|
2148916236 | 2148916237 => {
|
||||||
return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(
|
return Err(ErrorKind::AuthError(AuthenticationError::MicrosoftError(
|
||||||
MicrosoftAuthError::AdultVerification,
|
MicrosoftAuthError::AdultVerification,
|
||||||
)))
|
))
|
||||||
|
.into())
|
||||||
}
|
}
|
||||||
2148916238 => {
|
2148916238 => {
|
||||||
return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(
|
return Err(ErrorKind::AuthError(AuthenticationError::MicrosoftError(
|
||||||
MicrosoftAuthError::ChildAccount,
|
MicrosoftAuthError::ChildAccount,
|
||||||
)))
|
))
|
||||||
|
.into())
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(
|
return Err(ErrorKind::AuthError(AuthenticationError::MicrosoftError(
|
||||||
MicrosoftAuthError::UnknownError(error_code),
|
MicrosoftAuthError::UnknownError(error_code),
|
||||||
)))
|
))
|
||||||
|
.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -139,16 +150,17 @@ async fn auth_xsts(token: String) -> PolyResult<String> {
|
||||||
let xsts_token = match response.get("Token") {
|
let xsts_token = match response.get("Token") {
|
||||||
Some(token) => token.as_str().unwrap(),
|
Some(token) => token.as_str().unwrap(),
|
||||||
None => {
|
None => {
|
||||||
return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(
|
return Err(ErrorKind::AuthError(AuthenticationError::MicrosoftError(
|
||||||
MicrosoftAuthError::XSTSToken,
|
MicrosoftAuthError::XSTSToken,
|
||||||
)))
|
))
|
||||||
|
.into())
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(xsts_token.to_string())
|
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()?
|
let response = create_client()?
|
||||||
.post("https://user.auth.xboxlive.com/user/authenticate")
|
.post("https://user.auth.xboxlive.com/user/authenticate")
|
||||||
.json(&json!({
|
.json(&json!({
|
||||||
|
|
@ -167,9 +179,10 @@ async fn auth_xbl(code: String) -> PolyResult<(String, String)> {
|
||||||
let xbl_token = match response.get("Token") {
|
let xbl_token = match response.get("Token") {
|
||||||
Some(token) => token.as_str().unwrap(),
|
Some(token) => token.as_str().unwrap(),
|
||||||
None => {
|
None => {
|
||||||
return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(
|
return Err(ErrorKind::AuthError(AuthenticationError::MicrosoftError(
|
||||||
MicrosoftAuthError::XBLToken,
|
MicrosoftAuthError::XBLToken,
|
||||||
)))
|
))
|
||||||
|
.into())
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -180,21 +193,25 @@ async fn auth_xbl(code: String) -> PolyResult<(String, String)> {
|
||||||
}) {
|
}) {
|
||||||
Some(hash) => hash.as_str().unwrap(),
|
Some(hash) => hash.as_str().unwrap(),
|
||||||
None => {
|
None => {
|
||||||
return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(
|
return Err(ErrorKind::AuthError(AuthenticationError::MicrosoftError(
|
||||||
MicrosoftAuthError::XBLUserHash,
|
MicrosoftAuthError::XBLUserHash,
|
||||||
)))
|
))
|
||||||
|
.into())
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok((xbl_token.to_string(), user_hash.to_string()))
|
Ok((xbl_token.to_string(), user_hash.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn msa_code_to_token(code: String) -> PolyResult<String> {
|
async fn msa_code_to_token(code: String) -> crate::Result<String> {
|
||||||
let token = create_client()?
|
let token = create_client()?
|
||||||
.post("https://login.live.com/oauth20_token.srf")
|
.post("https://login.live.com/oauth20_token.srf")
|
||||||
.form(&[
|
.form(&[
|
||||||
("client_id", CLIENT_ID),
|
("client_id", CLIENT_ID),
|
||||||
("redirect_uri", &format!("http://localhost:{}/", constants::MSA_PORT)),
|
(
|
||||||
|
"redirect_uri",
|
||||||
|
&format!("http://localhost:{}/", constants::MSA_PORT),
|
||||||
|
),
|
||||||
("code", &code),
|
("code", &code),
|
||||||
("grant_type", "authorization_code"),
|
("grant_type", "authorization_code"),
|
||||||
])
|
])
|
||||||
|
|
@ -206,19 +223,22 @@ async fn msa_code_to_token(code: String) -> PolyResult<String> {
|
||||||
Ok(token["access_token"].as_str().unwrap().to_string())
|
Ok(token["access_token"].as_str().unwrap().to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn msa_code() -> PolyResult<String> {
|
async fn msa_code() -> crate::Result<String> {
|
||||||
let url = format!(
|
let url = format!(
|
||||||
"https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?client_id={}&response_type=code&redirect_uri=http://localhost:{}/&response_mode=query&scope=XboxLive.signin%20offline_access&prompt=consent",
|
"https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?client_id={}&response_type=code&redirect_uri=http://localhost:{}/&response_mode=query&scope=XboxLive.signin%20offline_access&prompt=consent",
|
||||||
CLIENT_ID,
|
CLIENT_ID,
|
||||||
constants::MSA_PORT
|
constants::MSA_PORT
|
||||||
);
|
);
|
||||||
|
|
||||||
open::that(url)?;
|
open::that(url)?;
|
||||||
|
|
||||||
let mut token: String = String::new();
|
let mut token: String = String::new();
|
||||||
|
|
||||||
let listener = TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], constants::MSA_PORT)))?;
|
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() {
|
for conn in listener.incoming() {
|
||||||
match conn {
|
match conn {
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
use crate::{utils::http, PolyError, PolyResult};
|
use crate::{utils::http, ErrorKind};
|
||||||
|
|
||||||
pub mod microsoft_auth;
|
pub mod microsoft_auth;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub enum AuthenticationError {
|
pub enum AuthenticationError {
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
MicrosoftError(#[from] microsoft_auth::MicrosoftAuthError),
|
MicrosoftError(#[from] microsoft_auth::MicrosoftAuthError),
|
||||||
|
|
@ -31,11 +30,11 @@ pub struct AccountSkin {
|
||||||
pub trait AuthenticationMethod {
|
pub trait AuthenticationMethod {
|
||||||
/// Authenticate with a given method. Stage is a function that takes a string and a u8.
|
/// 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.
|
/// The string is the message to display to the user, and the u8 is the progress of the authentication.
|
||||||
async fn auth<F>(stage: F) -> PolyResult<Account>
|
async fn auth<F>(stage: F) -> crate::Result<Account>
|
||||||
where
|
where
|
||||||
F: Fn(String, u8, bool) -> ();
|
F: Fn(String, u8, bool) -> ();
|
||||||
|
|
||||||
async fn get_profile(access_token: String) -> PolyResult<Account> {
|
async fn get_profile(access_token: String) -> crate::Result<Account> {
|
||||||
let response = http::create_client()?
|
let response = http::create_client()?
|
||||||
.get("https://api.minecraftservices.com/minecraft/profile")
|
.get("https://api.minecraftservices.com/minecraft/profile")
|
||||||
.header("Authorization", format!("Bearer {}", access_token))
|
.header("Authorization", format!("Bearer {}", access_token))
|
||||||
|
|
@ -44,7 +43,7 @@ pub trait AuthenticationMethod {
|
||||||
|
|
||||||
let response = response.json::<serde_json::Value>().await?;
|
let response = response.json::<serde_json::Value>().await?;
|
||||||
if let Some(error) = response.get("error") {
|
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::<Account>(response)?;
|
let account = serde_json::from_value::<Account>(response)?;
|
||||||
|
|
@ -1,3 +1,7 @@
|
||||||
|
// Basic Metadata
|
||||||
|
pub const NAME: &str = "OneLauncher";
|
||||||
|
pub const VERSION: &str = "0.1.0";
|
||||||
|
|
||||||
// Authentication and HTTP (TODO: env?)
|
// Authentication and HTTP (TODO: env?)
|
||||||
pub const APP_CONFIG_DIR: &str = "org.polyfrost.launcher";
|
pub const APP_CONFIG_DIR: &str = "org.polyfrost.launcher";
|
||||||
pub const USER_AGENT: &str = "OneLauncher/1.0.0 (polyfrost.org)";
|
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;
|
pub const MSA_PORT: u16 = 13523;
|
||||||
|
|
||||||
// Minecraft
|
// 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
|
// Java Downloading
|
||||||
117
onelauncher/src/error.rs
Normal file
117
onelauncher/src/error.rs
Normal file
|
|
@ -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<ErrorKind>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Error> 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<E: Into<ErrorKind>> From<E> for Error {
|
||||||
|
fn from(source: E) -> Self {
|
||||||
|
Self {
|
||||||
|
source: Into::<ErrorKind>::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<T> = anyhow::Result<T, Error>;
|
||||||
304
onelauncher/src/game/client.rs
Normal file
304
onelauncher/src/game/client.rs
Normal file
|
|
@ -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<chrono::Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<Local>,
|
||||||
|
pub name: String,
|
||||||
|
pub cover: Option<String>,
|
||||||
|
pub group: Option<Uuid>,
|
||||||
|
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<Vec<Version>> {
|
||||||
|
match self {
|
||||||
|
ClientType::Vanilla(_) => vanilla::get_versions(file).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_impl<'a>(
|
||||||
|
client: &'a ClientType,
|
||||||
|
instance: &'a Instance,
|
||||||
|
manifest: &'a Manifest,
|
||||||
|
) -> Box<dyn ClientTrait<'a> + '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<PathBuf>;
|
||||||
|
async fn install_libraries(&self) -> crate::Result<String>;
|
||||||
|
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<Uuid, Instance>,
|
||||||
|
manifests: HashMap<Uuid, Manifest>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClientManager {
|
||||||
|
pub fn new() -> crate::Result<Self> {
|
||||||
|
let instances = load_and_serialize::<Instance>(&dirs::instances_dir()?)?;
|
||||||
|
|
||||||
|
let manifests = load_and_serialize::<Manifest>(&dirs::manifests_dir()?)?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
instances,
|
||||||
|
manifests,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_impl_uuid<'a>(&'a self, uuid: Uuid) -> crate::Result<Box<dyn ClientTrait<'a> + '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<Instance> {
|
||||||
|
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<String>,
|
||||||
|
group: Option<Uuid>,
|
||||||
|
client: ClientType,
|
||||||
|
) -> crate::Result<Uuid> {
|
||||||
|
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<T>(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<T>(dir: &PathBuf) -> crate::Result<HashMap<Uuid, T>>
|
||||||
|
where
|
||||||
|
T: DeserializeOwned,
|
||||||
|
{
|
||||||
|
let mut result = HashMap::<Uuid, T>::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::<T>(&content) {
|
||||||
|
Ok(parsed) => parsed,
|
||||||
|
Err(_) => {
|
||||||
|
eprintln!("Couldn't parse file content as JSON: '{:?}'", content);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
result.insert(uuid, parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
159
onelauncher/src/game/java.rs
Normal file
159
onelauncher/src/game/java.rs
Normal file
|
|
@ -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<PathBuf> {
|
||||||
|
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<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(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)
|
||||||
|
}
|
||||||
209
onelauncher/src/game/minecraft.rs
Normal file
209
onelauncher/src/game/minecraft.rs
Normal file
|
|
@ -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<Library>,
|
||||||
|
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<ModernArgumentsItem>,
|
||||||
|
jvm: Vec<ModernArgumentsItem>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Rule>,
|
||||||
|
value: ArgumentRuleValue,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum ArgumentRuleValue {
|
||||||
|
String(String),
|
||||||
|
List(Vec<String>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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(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<Os>,
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub features: Option<Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<String>,
|
||||||
|
pub arch: Option<String>,
|
||||||
|
pub version: Option<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,4 +1,4 @@
|
||||||
pub mod client;
|
pub mod client;
|
||||||
pub mod java;
|
pub mod java;
|
||||||
|
pub mod minecraft;
|
||||||
pub mod vanilla;
|
pub mod vanilla;
|
||||||
pub mod minecraft;
|
|
||||||
144
onelauncher/src/game/vanilla.rs
Normal file
144
onelauncher/src/game/vanilla.rs
Normal file
|
|
@ -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<PathBuf> {
|
||||||
|
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<String> {
|
||||||
|
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<Version>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
struct CachedVersions {
|
||||||
|
last_updated: i64,
|
||||||
|
versions: Vec<Version>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_versions(file: Option<&PathBuf>) -> crate::Result<Vec<Version>> {
|
||||||
|
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::<VersionList>().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<Option<Vec<Version>>> {
|
||||||
|
let cached = serde_json::from_str::<CachedVersions>(&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<MinecraftManifest> {
|
||||||
|
let manifest = http::create_client()?
|
||||||
|
.get(url)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.json::<MinecraftManifest>()
|
||||||
|
.await?;
|
||||||
|
Ok(manifest)
|
||||||
|
}
|
||||||
12
onelauncher/src/lib.rs
Normal file
12
onelauncher/src/lib.rs
Normal file
|
|
@ -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;
|
||||||
54
onelauncher/src/logger.rs
Normal file
54
onelauncher/src/logger.rs
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
use tracing_appender::non_blocking::WorkerGuard;
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
pub fn start_logger() -> Option<WorkerGuard> {
|
||||||
|
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<WorkerGuard> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
35
onelauncher/src/utils/dirs.rs
Normal file
35
onelauncher/src/utils/dirs.rs
Normal file
|
|
@ -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<PathBuf> {
|
||||||
|
dirs::config_dir()
|
||||||
|
.and_then(|dir| Some(dir.join(constants::APP_CONFIG_DIR)))
|
||||||
|
.ok_or(DirectoryError::ConfigDir.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn libraries_dir() -> crate::Result<PathBuf> {
|
||||||
|
Ok(app_config_dir()?.join("libraries"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn java_dir() -> crate::Result<PathBuf> {
|
||||||
|
Ok(app_config_dir()?.join("java"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn instances_dir() -> crate::Result<PathBuf> {
|
||||||
|
Ok(app_config_dir()?.join("instances"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn manifests_dir() -> crate::Result<PathBuf> {
|
||||||
|
Ok(app_config_dir()?.join("manifests"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clients_dir() -> crate::Result<PathBuf> {
|
||||||
|
Ok(app_config_dir()?.join("clients"))
|
||||||
|
}
|
||||||
52
onelauncher/src/utils/file.rs
Normal file
52
onelauncher/src/utils/file.rs
Normal file
|
|
@ -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<String> {
|
||||||
|
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(())
|
||||||
|
}
|
||||||
34
onelauncher/src/utils/http.rs
Normal file
34
onelauncher/src/utils/http.rs
Normal file
|
|
@ -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<Client> {
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
|
pub mod dirs;
|
||||||
pub mod file;
|
pub mod file;
|
||||||
pub mod http;
|
pub mod http;
|
||||||
pub mod dirs;
|
|
||||||
|
|
@ -15,7 +15,6 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/poppins": "^5.0.12",
|
"@fontsource/poppins": "^5.0.12",
|
||||||
"@sentry/vite-plugin": "^2.16.0",
|
|
||||||
"@solidjs/router": "^0.13.1",
|
"@solidjs/router": "^0.13.1",
|
||||||
"@tauri-apps/api": "^2.0.0-beta",
|
"@tauri-apps/api": "^2.0.0-beta",
|
||||||
"@tauri-apps/plugin-shell": "github:tauri-apps/tauri-plugin-shell#v2",
|
"@tauri-apps/plugin-shell": "github:tauri-apps/tauri-plugin-shell#v2",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
[package]
|
[package]
|
||||||
name = "polyfrost-launcher"
|
name = "onelauncher_gui"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
description = "A Tauri App"
|
description = "A Tauri App"
|
||||||
license = { workspace = true }
|
license = { workspace = true }
|
||||||
|
|
@ -14,10 +14,12 @@ authors = { workspace = true }
|
||||||
tauri-build = { workspace = true }
|
tauri-build = { workspace = true }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
launcher-core = { path = "../core" }
|
onelauncher = { path = "../onelauncher", features = [ "tauri" ] }
|
||||||
|
|
||||||
tauri = { workspace = true }
|
tauri = { workspace = true }
|
||||||
tauri-plugin-shell = { 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 }
|
tokio = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
|
|
@ -27,6 +29,9 @@ anyhow = { workspace = true }
|
||||||
chrono = { workspace = true }
|
chrono = { workspace = true }
|
||||||
uuid = { workspace = true }
|
uuid = { workspace = true }
|
||||||
|
|
||||||
|
tracing = { workspace = true }
|
||||||
|
tracing-error = { workspace = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!!
|
# This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!!
|
||||||
custom-protocol = [ "tauri/custom-protocol" ]
|
custom-protocol = [ "tauri/custom-protocol" ]
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,15 @@
|
||||||
use tauri_build::{Attributes, InlinedPlugin};
|
use tauri_build::{Attributes, InlinedPlugin};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let _ = tauri_build::try_build(
|
let _ = tauri_build::try_build(Attributes::new().plugin(
|
||||||
Attributes::new()
|
"onelauncher",
|
||||||
.plugin(
|
InlinedPlugin::new().commands(&[
|
||||||
"launcher-core",
|
"login_msa",
|
||||||
InlinedPlugin::new().commands(&[
|
"refresh_client_manager",
|
||||||
"login_msa",
|
"create_instance",
|
||||||
"refresh_client_manager",
|
"get_instances",
|
||||||
"create_instance",
|
"get_instance",
|
||||||
"get_instances",
|
"get_manifest",
|
||||||
"get_instance",
|
]),
|
||||||
"get_manifest"
|
));
|
||||||
]),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
"*"
|
"*"
|
||||||
],
|
],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"launcher-core:default",
|
"onelauncher:default",
|
||||||
"window:allow-close",
|
"window:allow-close",
|
||||||
"window:allow-destroy",
|
"window:allow-destroy",
|
||||||
"window:allow-start-dragging",
|
"window:allow-start-dragging",
|
||||||
|
|
|
||||||
|
|
@ -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;
|
use tauri::Manager;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|
@ -11,4 +13,4 @@ pub async fn login_msa<R: tauri::Runtime>(app: tauri::AppHandle<R>) -> Result<Ac
|
||||||
Ok(account) => Ok(account),
|
Ok(account) => Ok(account),
|
||||||
Err(err) => Err(err.to_string()),
|
Err(err) => Err(err.to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
50
src-tauri/src/api/game.rs
Normal file
50
src-tauri/src/api/game.rs
Normal file
|
|
@ -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<GameManagerState>>,
|
||||||
|
name: String,
|
||||||
|
version: String,
|
||||||
|
client: ClientType,
|
||||||
|
cover: Option<String>,
|
||||||
|
group: Option<Uuid>,
|
||||||
|
) -> Result<Uuid, String> {
|
||||||
|
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<GameManagerState>>,
|
||||||
|
) -> Result<Vec<Instance>, String> {
|
||||||
|
let manager = &mut state.lock().await.client_manager;
|
||||||
|
Ok(manager.get_instances_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_instance(
|
||||||
|
state: State<'_, Mutex<GameManagerState>>,
|
||||||
|
uuid: Uuid,
|
||||||
|
) -> Result<Instance, String> {
|
||||||
|
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<GameManagerState>>,
|
||||||
|
uuid: Uuid,
|
||||||
|
) -> Result<Manifest, String> {
|
||||||
|
let manager = &mut state.lock().await.client_manager;
|
||||||
|
let manifest = manager.get_manifest(uuid)?;
|
||||||
|
Ok(manifest.clone())
|
||||||
|
}
|
||||||
41
src-tauri/src/api/mod.rs
Normal file
41
src-tauri/src/api/mod.rs
Normal file
|
|
@ -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::Wry> {
|
||||||
|
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<GameManagerState>>) -> Result<(), String> {
|
||||||
|
let mut state = state.lock().await;
|
||||||
|
state.client_manager = ClientManager::new()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
// Basic Metadata
|
|
||||||
pub const NAME: &str = "OneLauncher";
|
|
||||||
pub const VERSION: &str = "0.1.0";
|
|
||||||
18
src-tauri/src/error.rs
Normal file
18
src-tauri/src/error.rs
Normal file
|
|
@ -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())
|
||||||
|
}
|
||||||
|
|
@ -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<GameManagerState>>,
|
|
||||||
name: String,
|
|
||||||
version: String,
|
|
||||||
client: ClientType,
|
|
||||||
cover: Option<String>,
|
|
||||||
group: Option<Uuid>,
|
|
||||||
) -> Result<Uuid, String> {
|
|
||||||
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<GameManagerState>>
|
|
||||||
) -> Result<Vec<Instance>, String> {
|
|
||||||
let manager = &mut state.lock().await.client_manager;
|
|
||||||
Ok(manager.get_instances_owned())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn get_instance(
|
|
||||||
state: State<'_, Mutex<GameManagerState>>,
|
|
||||||
uuid: Uuid
|
|
||||||
) -> Result<Instance, String> {
|
|
||||||
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<GameManagerState>>,
|
|
||||||
uuid: Uuid
|
|
||||||
) -> Result<Manifest, String> {
|
|
||||||
let manager = &mut state.lock().await.client_manager;
|
|
||||||
let manifest = manager.get_manifest(uuid)?;
|
|
||||||
Ok(manifest.clone())
|
|
||||||
}
|
|
||||||
|
|
@ -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::Wry> {
|
|
||||||
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<GameManagerState>>
|
|
||||||
) -> Result<(), String> {
|
|
||||||
let mut state = state.lock().await;
|
|
||||||
state.client_manager = ClientManager::new()?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
pub mod constants;
|
pub mod api;
|
||||||
pub mod launcher_core;
|
pub mod error;
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,30 @@
|
||||||
|
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
|
||||||
use polyfrost_launcher::launcher_core;
|
use polyfrost_launcher::api;
|
||||||
use tauri::{menu::Menu, App};
|
use tauri::{menu::Menu, App};
|
||||||
|
|
||||||
|
#[derive(Clone, serde::Serialize)]
|
||||||
|
struct Payload {
|
||||||
|
args: Vec<String>,
|
||||||
|
cwd: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
|
let _log_guard = launcher_core::start_logger();
|
||||||
|
tracing::info!("initialized tracing subscriber. loading OneLauncher");
|
||||||
|
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_shell::init())
|
.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))
|
.menu(|handle| Menu::new(handle))
|
||||||
.setup(setup)
|
.setup(setup)
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
{
|
{
|
||||||
"productName": "polyfrost-launcher",
|
"productName": "onelauncher",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"identifier": "org.polyfrost.launcher",
|
"identifier": "org.polyfrost.launcher",
|
||||||
"app": {
|
"app": {
|
||||||
"withGlobalTauri": true,
|
"withGlobalTauri": true,
|
||||||
"security": {
|
"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": []
|
"capabilities": []
|
||||||
},
|
},
|
||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
"title": "polyfrost-launcher",
|
"title": "OneLauncher",
|
||||||
"width": 1280,
|
"width": 1280,
|
||||||
"height": 802,
|
"height": 802,
|
||||||
"minWidth": 1080,
|
"minWidth": 1080,
|
||||||
|
|
@ -31,6 +31,7 @@
|
||||||
"bundle": {
|
"bundle": {
|
||||||
"active": true,
|
"active": true,
|
||||||
"targets": "all",
|
"targets": "all",
|
||||||
|
"category": "Game",
|
||||||
"icon": [
|
"icon": [
|
||||||
"icons/32x32.png",
|
"icons/32x32.png",
|
||||||
"icons/128x128.png",
|
"icons/128x128.png",
|
||||||
|
|
@ -38,7 +39,13 @@
|
||||||
"icons/icon.icns",
|
"icons/icon.icns",
|
||||||
"icons/icon.ico"
|
"icons/icon.ico"
|
||||||
],
|
],
|
||||||
"copyright": "Polyfrost"
|
"copyright": "Polyfrost",
|
||||||
|
"fileAssociations": [
|
||||||
|
{
|
||||||
|
"ext": ["mrpack"],
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"plugins": {}
|
"plugins": {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
|
||||||
export async function getInstance(uuid: string): Promise<Core.Instance> {
|
export async function getInstance(uuid: string): Promise<Core.Instance> {
|
||||||
return await invoke<Core.Instance>('plugin:launcher-core|get_instance', {
|
return await invoke<Core.Instance>('plugin:onelauncher|get_instance', {
|
||||||
uuid,
|
uuid,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getManifest(uuid: string): Promise<Core.Manifest> {
|
export async function getManifest(uuid: string): Promise<Core.Manifest> {
|
||||||
return await invoke<Core.Manifest>('plugin:launcher-core|get_manifest', {
|
return await invoke<Core.Manifest>('plugin:onelauncher|get_manifest', {
|
||||||
uuid,
|
uuid,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -39,12 +39,12 @@ export async function getGroupedInstances(): Promise<Map<string, Core.InstanceWi
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getInstances(): Promise<Core.Instance[]> {
|
export async function getInstances(): Promise<Core.Instance[]> {
|
||||||
return await invoke<Core.Instance[]>('plugin:launcher-core|get_instances');
|
return await invoke<Core.Instance[]>('plugin:onelauncher|get_instances');
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createInstance(instance: Omit<Core.Instance, 'id' | 'createdAt'> & { version: string }): Promise<void> {
|
export async function createInstance(instance: Omit<Core.Instance, 'id' | 'createdAt'> & { version: string }): Promise<void> {
|
||||||
return await invoke(
|
return await invoke(
|
||||||
'plugin:launcher-core|create_instance',
|
'plugin:onelauncher|create_instance',
|
||||||
{
|
{
|
||||||
name: instance.name,
|
name: instance.name,
|
||||||
version: instance.version,
|
version: instance.version,
|
||||||
|
|
@ -61,5 +61,5 @@ export async function createInstance(instance: Omit<Core.Instance, 'id' | 'creat
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function refreshClientManager(): Promise<void> {
|
export async function refreshClientManager(): Promise<void> {
|
||||||
return await invoke('plugin:launcher-core|refresh_client_manager');
|
return await invoke('plugin:onelauncher|refresh_client_manager');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,9 @@
|
||||||
import process from 'node:process';
|
import process from 'node:process';
|
||||||
import type { Plugin } from 'vite';
|
import type { Plugin, UserConfig } from 'vite';
|
||||||
import { defineConfig, loadEnv } from 'vite';
|
import { defineConfig, loadEnv } from 'vite';
|
||||||
|
|
||||||
import solid from 'vite-plugin-solid';
|
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 = {
|
const devtoolsPlugin: Plugin = {
|
||||||
name: 'devtools-plugin',
|
name: 'devtools-plugin',
|
||||||
transformIndexHtml(html) {
|
transformIndexHtml(html) {
|
||||||
|
|
@ -26,12 +21,19 @@ const devtoolsPlugin: Plugin = {
|
||||||
export default defineConfig(async ({ mode }) => {
|
export default defineConfig(async ({ mode }) => {
|
||||||
process.env = { ...process.env, ...loadEnv(mode, process.cwd(), '') };
|
process.env = { ...process.env, ...loadEnv(mode, process.cwd(), '') };
|
||||||
|
|
||||||
const config = {
|
const config: UserConfig = {
|
||||||
plugins: [
|
plugins: [
|
||||||
solid(),
|
solid(),
|
||||||
devtoolsPlugin,
|
devtoolsPlugin,
|
||||||
],
|
],
|
||||||
|
|
||||||
|
envPrefix: ['VITE_', 'TAURI_'],
|
||||||
|
|
||||||
|
build: {
|
||||||
|
minify: !process.env.TAURI_DEBUG ? 'esbuild' : false,
|
||||||
|
sourcemap: !!process.env.TAURI_DEBUG,
|
||||||
|
},
|
||||||
|
|
||||||
clearScreen: false,
|
clearScreen: false,
|
||||||
server: {
|
server: {
|
||||||
port: 8001,
|
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;
|
return config;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue