diff --git a/onelauncher/src/game/client_manager.rs b/onelauncher/src/game/client_manager.rs index bbb71f19..54cd6e25 100644 --- a/onelauncher/src/game/client_manager.rs +++ b/onelauncher/src/game/client_manager.rs @@ -2,7 +2,7 @@ use std::{collections::HashMap, fs, path::PathBuf}; use anyhow::anyhow; use chrono::Local; -use serde::{de::DeserializeOwned, Serialize}; +use serde::Serialize; use uuid::Uuid; use crate::{auth::Account, utils::dirs}; @@ -18,57 +18,42 @@ pub enum ClientManagerError { ManifestNotFound, } +#[derive(Debug, Clone, Serialize)] +pub struct ClusterWithManifest { + pub cluster: Cluster, + pub manifest: Manifest +} + pub struct ClientManager { - clusters: HashMap, - manifests: HashMap, + clusters: HashMap, } impl ClientManager { pub fn new() -> crate::Result { Ok(Self { - clusters: load_and_serialize::(&dirs::clusters_dir()?)?, - manifests: load_and_serialize::(&dirs::manifests_dir()?)?, - }) + clusters: load_and_serialize()? + }) } pub fn get_impl_uuid<'a>(&'a self, uuid: Uuid) -> crate::Result + 'a>> { - let cluster = self.get_cluster(uuid)?; - let manifest = self.get_manifest(uuid)?; + let ClusterWithManifest { cluster, manifest } = self.get_cluster(uuid)?; - Ok(clients::get_impl(&cluster.client, &cluster, &manifest)) + Ok(clients::get_impl(&cluster.client, cluster, manifest)) } - pub fn get_clusters(&self) -> Vec<&Cluster> { + pub fn get_clusters(&self) -> Vec<&ClusterWithManifest> { self.clusters.values().collect() } - pub fn get_clusters_owned(&self) -> Vec { + pub fn get_clusters_owned(&self) -> Vec { self.clusters.values().cloned().collect() } - pub fn get_cluster(&self, uuid: Uuid) -> crate::Result<&Cluster> { - self.clusters - .get(&uuid) - .ok_or(ClientManagerError::ClusterNotFound.into()) - } - - pub fn get_cluster_mut(&mut self, uuid: Uuid) -> crate::Result<&mut Cluster> { - self.clusters - .get_mut(&uuid) - .ok_or(ClientManagerError::ClusterNotFound.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 fn get_cluster(&self, uuid: Uuid) -> crate::Result<&ClusterWithManifest> { + self.clusters + .get(&uuid) + .ok_or(ClientManagerError::ClusterNotFound.into()) + } } impl ClientManager { @@ -117,6 +102,7 @@ impl ClientManager { .url; let uuid = Uuid::new_v4(); + let dir = &dirs::cluster_dir(uuid.to_string())?; // Save the manifest // TODO: Is this correct? @@ -127,10 +113,9 @@ impl ClientManager { }; save( - &dirs::manifests_dir()?.join(format!("{}.json", uuid)), + &dir.join("manifest.json"), &manifest, )?; - self.manifests.insert(uuid, manifest); // Save the cluster let cluster = Cluster { @@ -143,10 +128,11 @@ impl ClientManager { }; save( - &dirs::clusters_dir()?.join(format!("{}.json", uuid)), + &dir.join("cluster.json"), &cluster, )?; - self.clusters.insert(uuid, cluster); + + self.clusters.insert(uuid, ClusterWithManifest { cluster, manifest }); Ok(uuid) } @@ -168,51 +154,40 @@ where Ok(()) } -fn load_and_serialize(dir: &PathBuf) -> crate::Result> -where - T: DeserializeOwned, -{ - let mut result = HashMap::::new(); +fn load_and_serialize() -> crate::Result> { + let mut result = HashMap::::new(); - if !dir.exists() { - fs::create_dir_all(dir)?; - return Ok(result); - } + let dir = dirs::clusters_dir()?; + if !dir.exists() { + fs::create_dir_all(&dir)?; + return Ok(result); + } + + for entry in fs::read_dir(dir)? { + let entry = entry?; + let path = entry.path(); - for file in fs::read_dir(dir)? { - let file = file?; - let file_name = file.file_name(); - let file_name = file_name - .to_str() - .ok_or(anyhow!("Couldn't convert OsStr to str"))? - .replace(".json", ""); - let uuid = match Uuid::parse_str(&file_name) { - Ok(uuid) => uuid, - Err(_) => { - eprintln!("Couldn't parse file name as UUID: '{:?}'", file_name); - continue; - } - }; - - let path = file.path(); - let content = match fs::read_to_string(&path) { - Ok(content) => content, - Err(_) => { - eprintln!("Couldn't read file: '{:?}'", path); - continue; - } - }; - - let parsed = match serde_json::from_str::(&content) { - Ok(parsed) => parsed, - Err(_) => { - eprintln!("Couldn't parse file '{}' content as JSON", path.display()); - continue; - } - }; - - result.insert(uuid, parsed); - } + if path.is_dir() { + match get_entry(&path) { + Ok((uuid, cluster)) => { + result.insert(uuid, cluster); + } + Err(e) => { + eprintln!("Failed to load cluster: {}", e); + } + } + } + } Ok(result) } + +fn get_entry(path: &PathBuf) -> crate::Result<(Uuid, ClusterWithManifest)> { + let manifest_path = path.join("manifest.json"); + let cluster_path = path.join("cluster.json"); + + let manifest: Manifest = serde_json::from_str(&fs::read_to_string(manifest_path)?)?; + let cluster: Cluster = serde_json::from_str(&fs::read_to_string(cluster_path)?)?; + + Ok((cluster.id, ClusterWithManifest { cluster, manifest })) +} diff --git a/onelauncher/src/game/clients/vanilla.rs b/onelauncher/src/game/clients/vanilla.rs index 89f542c0..ca79437f 100644 --- a/onelauncher/src/game/clients/vanilla.rs +++ b/onelauncher/src/game/clients/vanilla.rs @@ -1,7 +1,7 @@ use crate::{ constants::{self, MINECRAFT_VERSIONS_MANIFEST}, create_game_client, - game::{client::{ClientTrait, Cluster, Manifest, SetupInfo}, minecraft::{Library, MinecraftManifest, MinecraftVersion, RuleListExt}}, + game::{client::{ClientTrait, Cluster, Manifest, SetupInfo}, minecraft::{AssetIndexFile, Library, MinecraftManifest, MinecraftVersion, RuleListExt}}, impl_game_client, utils::{dirs, file, http} }; @@ -37,7 +37,7 @@ impl<'a> ClientTrait<'a> for VanillaClient<'a> { libraries.push_str(constants::LIBRARY_SPLITTER); libraries.push_str(client_path.to_str().unwrap()); - let game_dir = dirs::game_dir()?.join(&self.cluster.id.to_string()); + let game_dir = dirs::cluster_dir(self.cluster.id.to_string())?.join("game"); fs::create_dir_all(&game_dir)?; Ok(SetupInfo { @@ -125,11 +125,35 @@ pub async fn install_natives(natives: Vec<&Library>) -> crate::Result<()> { Ok(()) } -pub async fn install_assets() -> crate::Result<()> { +pub async fn install_assets(manifest: &MinecraftManifest) -> crate::Result<()> { + let assets = dirs::assets_dir()?; + let objects = assets.join("objects"); + let indexes = assets.join("indexes"); + let index = indexes.join(format!("{}.json", manifest.asset_index.id)); + + fs::create_dir_all(&objects)?; + fs::create_dir_all(&indexes)?; + + if !index.exists() { + let artifact = &manifest.asset_index; + http::download_file_sha1_check(&artifact.url, &index, &artifact.sha1).await?; + } + + let contents = serde_json::from_str::(&fs::read_to_string(&index)?)?; + for (hash, asset) in contents.objects { + let short = &hash[..2]; + let file = objects.join(&short).join(&hash); + fs::create_dir_all(file.parent().ok_or(anyhow!("Couldn't get asset parent"))?)?; + + if !file.exists() { + let url = format!("https://resources.download.minecraft.net/{}/{}", &short, hash); + http::download_file_sha1_check(&url, &file, &asset.hash).await?; + } + } + Ok(()) } - #[derive(Clone, Serialize, Deserialize)] struct VersionList { versions: Vec, diff --git a/onelauncher/src/game/minecraft.rs b/onelauncher/src/game/minecraft.rs index bb200b47..b08a9357 100644 --- a/onelauncher/src/game/minecraft.rs +++ b/onelauncher/src/game/minecraft.rs @@ -266,6 +266,17 @@ pub struct File { pub url: String, } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct AssetIndexFile { + pub objects: HashMap, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct AssetObject { + pub hash: String, + pub size: u64, +} + fn string_or_seq<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, diff --git a/onelauncher/src/utils/dirs.rs b/onelauncher/src/utils/dirs.rs index 83914640..7274aec0 100644 --- a/onelauncher/src/utils/dirs.rs +++ b/onelauncher/src/utils/dirs.rs @@ -14,6 +14,10 @@ pub fn app_config_dir() -> crate::Result { .ok_or(DirectoryError::ConfigDir.into()) } +pub fn assets_dir() -> crate::Result { + Ok(app_config_dir()?.join("assets")) +} + pub fn libraries_dir() -> crate::Result { Ok(app_config_dir()?.join("libraries")) } @@ -30,14 +34,10 @@ pub fn clusters_dir() -> crate::Result { Ok(app_config_dir()?.join("clusters")) } -pub fn manifests_dir() -> crate::Result { - Ok(app_config_dir()?.join("manifests")) +pub fn cluster_dir(uuid: String) -> crate::Result { + Ok(clusters_dir()?.join(uuid)) } pub fn clients_dir() -> crate::Result { Ok(app_config_dir()?.join("clients")) } - -pub fn game_dir() -> crate::Result { - Ok(app_config_dir()?.join("games")) -} diff --git a/src-tauri/src/api/game.rs b/src-tauri/src/api/game.rs index 86d4b2b0..666c9e7b 100644 --- a/src-tauri/src/api/game.rs +++ b/src-tauri/src/api/game.rs @@ -1,4 +1,4 @@ -use onelauncher::{game::{client::{Cluster, LaunchCallbacks, Manifest}, clients::ClientType}, AppState}; +use onelauncher::{game::{client::LaunchCallbacks, client_manager::ClusterWithManifest, clients::ClientType}, AppState}; use tauri::{Manager, State}; use tokio::sync::Mutex; use uuid::Uuid; @@ -22,7 +22,7 @@ pub async fn create_cluster( #[tauri::command] pub async fn get_clusters( state: State<'_, Mutex>, -) -> Result, String> { +) -> Result, String> { let manager = &mut state.lock().await.clients; Ok(manager.get_clusters_owned()) } @@ -31,10 +31,10 @@ pub async fn get_clusters( pub async fn get_cluster( state: State<'_, Mutex>, uuid: Uuid, -) -> Result { +) -> Result { let manager = &mut state.lock().await.clients; - let instance = manager.get_cluster(uuid)?; - Ok(instance.clone()) + let cluster = manager.get_cluster(uuid)?; + Ok(cluster.clone()) } #[tauri::command] @@ -66,13 +66,3 @@ pub async fn launch_cluster( let exit_code = manager.launch_cluster(uuid, callbacks).await?; Ok(exit_code) } - -#[tauri::command] -pub async fn get_manifest( - state: State<'_, Mutex>, - uuid: Uuid, -) -> Result { - let manager = &mut state.lock().await.clients; - let manifest = manager.get_manifest(uuid)?; - Ok(manifest.clone()) -} diff --git a/src-tauri/src/api/mod.rs b/src-tauri/src/api/mod.rs index 1b72849c..b1d04ecf 100644 --- a/src-tauri/src/api/mod.rs +++ b/src-tauri/src/api/mod.rs @@ -17,7 +17,6 @@ pub fn init() -> TauriPlugin { game::create_cluster, game::get_clusters, game::get_cluster, - game::get_manifest, game::launch_cluster, refresh_client_manager, ]) diff --git a/src/bridge/game.ts b/src/bridge/game.ts index d7625dba..745292d1 100644 --- a/src/bridge/game.ts +++ b/src/bridge/game.ts @@ -1,36 +1,23 @@ import { invoke } from '@tauri-apps/api/core'; import { listen } from '@tauri-apps/api/event'; -export async function getCluster(uuid: string): Promise { - return await invoke( +export async function getCluster(uuid: string): Promise { + return await invoke( 'plugin:onelauncher|get_cluster', { uuid }, ); } -export async function getManifest(uuid: string): Promise { - return await invoke( - 'plugin:onelauncher|get_manifest', - { uuid }, - ); -} - -export async function getClustersWithManifests(): Promise { +export async function getClustersGrouped(): Promise[]>> { const clusters = await getClusters(); - - return Promise.all(clusters.map(async cluster => ({ - cluster, - manifest: await getManifest(cluster.id), - }))); -} - -export async function getClustersGrouped(): Promise> { - const clusters = await getClustersWithManifests(); - const map = new Map(); + const map = new Map[]>(); clusters.forEach((cluster, index) => { const groupName = cluster.cluster.group || 'Unnamed'; - const value = { ...cluster, index }; + const value: WithIndex = { + ...cluster, + index, + }; if (map.has(groupName)) map.get(groupName)!.push(value); @@ -41,8 +28,8 @@ export async function getClustersGrouped(): Promise { - return await invoke('plugin:onelauncher|get_clusters'); +export async function getClusters(): Promise { + return await invoke('plugin:onelauncher|get_clusters'); } export async function createCluster(cluster: Omit & { version: string }): Promise { diff --git a/src/types/game.d.ts b/src/types/game.d.ts index 5e25c09c..37e039d2 100644 --- a/src/types/game.d.ts +++ b/src/types/game.d.ts @@ -1,7 +1,7 @@ declare namespace Core { - export interface ClusterWithManifest { - cluster: Cluster, - manifest: Manifest, + export type ClusterWithManifest = { + cluster: Cluster, + manifest: Manifest } export interface Manifest { diff --git a/src/types/global.d.ts b/src/types/global.d.ts new file mode 100644 index 00000000..5fbb961c --- /dev/null +++ b/src/types/global.d.ts @@ -0,0 +1,3 @@ +declare namespace globalThis { + export type WithIndex = T & { index: number } +} \ No newline at end of file diff --git a/src/ui/pages/Home.tsx b/src/ui/pages/Home.tsx index d2bdfee4..d1194eb4 100644 --- a/src/ui/pages/Home.tsx +++ b/src/ui/pages/Home.tsx @@ -75,8 +75,8 @@ function ClusterCardContextMenu(props: ClusterCardContextMenuProps) { navigate(`/clusters/?id=${props.id}`); } - function launch() { - const exit_code = launchCluster(props.id, { + async function launch() { + const exit_code = await launchCluster(props.id, { on_launch: pid => console.log('Launched with PID:', pid), on_stdout: line => console.log('STDOUT:', line), on_stderr: line => console.log('STDERR:', line),