diff --git a/src-tauri/src/constants.rs b/src-tauri/src/constants.rs index bad69356..f3b6ee28 100644 --- a/src-tauri/src/constants.rs +++ b/src-tauri/src/constants.rs @@ -6,4 +6,7 @@ pub const VERSION: &str = "0.1.0"; pub const USER_AGENT: &str = "OneLauncher/1.0.0 (polyfrost.org)"; pub const CLIENT_ID: &str = "9419b7ee-1448-4d1b-b52a-550d8f36ab56"; +// Minecraft +pub const MINECRAFT_VERSIONS_MANIFEST: &str = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json"; + // Java Downloading diff --git a/src-tauri/src/game/client.rs b/src-tauri/src/game/client.rs index 8bf0cecc..8588ee34 100644 --- a/src-tauri/src/game/client.rs +++ b/src-tauri/src/game/client.rs @@ -9,7 +9,7 @@ use uuid::Uuid; use crate::PolyResult; -use super::{minecraft::MinecraftManifest, vanilla}; +use super::{minecraft::{MinecraftManifest, ReleaseType}, vanilla::{self, VanillaImpl}}; #[derive(Debug, Serialize, Deserialize)] pub struct Manifest { @@ -27,16 +27,33 @@ pub struct Instance { pub client: ClientType, } +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Version { + id: String, + url: String, + #[serde(default)] + release_type: ReleaseType, + #[serde(default)] + release_time: chrono::DateTime +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type")] pub enum ClientType { - Vanilla + Vanilla(VanillaImpl) +} + +impl ClientType { + pub async fn get_versions(&self, file: Option<&PathBuf>) -> PolyResult> { + match self { + ClientType::Vanilla(_) => vanilla::get_versions(file).await + } + } } #[async_trait] pub trait ClientTrait: Send + Sync { - fn from_instance(instance: Instance) -> Self where Self: Sized; - async fn launch(&self) -> PolyResult<()>; async fn setup(&self) -> PolyResult<()>; @@ -47,6 +64,7 @@ pub trait ClientTrait: Send + Sync { } pub struct ClientManager { + handle: AppHandle, instances: HashMap, instances_dir: PathBuf, @@ -63,6 +81,7 @@ impl ClientManager { let manifests = load_and_serialize::(&manifests_dir)?; Ok(Self { + handle: handle.clone(), instances, instances_dir, manifests, @@ -101,8 +120,10 @@ impl ClientManager { group: Option, client: ClientType ) -> PolyResult { - // TODO: Implement cahcing - let url = String::from("https://piston-meta.mojang.com/v1/packages/d546f1707a3f2b7d034eece5ea2e311eda875787/1.8.9.json"); + let version_cache = self.handle.path().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 diff --git a/src-tauri/src/game/vanilla.rs b/src-tauri/src/game/vanilla.rs index aa754860..d5b6aa41 100644 --- a/src-tauri/src/game/vanilla.rs +++ b/src-tauri/src/game/vanilla.rs @@ -1,16 +1,89 @@ -use std::collections::HashMap; +use std::{fs, path::PathBuf}; +use anyhow::anyhow; +use async_trait::async_trait; +use serde::{Deserialize, Serialize}; +use crate::{constants::MINECRAFT_VERSIONS_MANIFEST, utils::http, PolyResult}; +use super::{client::{ClientTrait, Version}, minecraft::MinecraftManifest}; -use crate::{utils::http, PolyResult}; +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VanillaImpl; -use super::minecraft::MinecraftManifest; +#[async_trait] +impl ClientTrait for VanillaImpl { + async fn launch(&self) -> PolyResult<()> { + Ok(()) + } -pub async fn retrieve_versions() -> PolyResult> { - let map = HashMap::new(); + async fn setup(&self) -> PolyResult<()> { + Ok(()) + } - Ok(map) + async fn install_game(&self) -> PolyResult<()> { + Ok(()) + } + + async fn install_libraries(&self) -> PolyResult { + Ok("".to_string()) + } + + async fn install_natives(&self) -> PolyResult<()> { + Ok(()) + } + + async fn install_assets(&self) -> PolyResult<()> { + Ok(()) + } } -pub async fn retrieve_version_manifest(url: String) -> PolyResult { +#[derive(Clone, Serialize, Deserialize)] +struct VersionList { + versions: Vec +} + +#[derive(Serialize, Deserialize)] +struct CachedVersions { + last_updated: i64, + versions: Vec, +} + +pub async fn get_versions(file: Option<&PathBuf>) -> PolyResult> { + if let Some(file) = file { + if file.exists() && file.is_file() { + if let Some(versions) = get_cached_versions(file).await? { + return Ok(versions); + } + } + } + + let response = http::create_client()?.get(MINECRAFT_VERSIONS_MANIFEST).send().await?; + let response = response.json::().await?; + + if let Some(file) = file { + fs::write(file, serde_json::to_string(&CachedVersions { + last_updated: chrono::Utc::now().timestamp(), + versions: response.versions.clone() + })?)?; + } + + Ok(response.versions) +} + +async fn get_cached_versions(file: &PathBuf) -> PolyResult>> { + let cached = serde_json::from_str::(&fs::read_to_string(file)?)?; + let head_request = http::create_client()?.head(MINECRAFT_VERSIONS_MANIFEST).send().await?; + + let last_updated = head_request.headers().get("Last-Modified").ok_or(anyhow!("Last-Modified header not found"))?; + let last_updated = last_updated.to_str().map_err(|_| anyhow!("Failed to convert Last-Modified header to string"))?; + let last_updated = chrono::DateTime::parse_from_rfc2822(last_updated)?; + + if cached.last_updated < last_updated.timestamp() { + Ok(None) + } else { + Ok(Some(cached.versions)) + } +} + +pub async fn retrieve_version_manifest(url: &str) -> PolyResult { let manifest = http::create_client()?.get(url).send().await?.json::().await?; Ok(manifest) } \ No newline at end of file diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 8a5105dc..2867edb6 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -52,6 +52,9 @@ pub enum PolyError { /// 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), } /// Alias for a [`Result`] with the error type [`PolyError`]. diff --git a/src/ui/pages/Browser.tsx b/src/ui/pages/Browser.tsx index b2e0577d..dd5c68f8 100644 --- a/src/ui/pages/Browser.tsx +++ b/src/ui/pages/Browser.tsx @@ -1,5 +1,4 @@ import { invoke } from '@tauri-apps/api/core'; -import * as uuid from 'uuid'; import Button from '../components/base/Button'; function BrowserPage() {