From 4d2ed01cc05ad9e9646a55ec7ffb423c5aff26de Mon Sep 17 00:00:00 2001 From: Quentin Legot Date: Thu, 20 Apr 2023 19:51:36 +0200 Subject: [PATCH] Add GameProfile struct as aith result, serde now use stringly typed structures to parse version manifest json, add specific version json parsing --- src-tauri/Cargo.toml | 2 +- src-tauri/src/authentification/mod.rs | 20 ++- src-tauri/src/launcher/mod.rs | 175 ++++++++++++++++++++++---- src-tauri/src/main.rs | 2 +- 4 files changed, 167 insertions(+), 32 deletions(-) diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 8080b6c..70048a1 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -6,7 +6,7 @@ authors = ["you"] license = "" repository = "" edition = "2021" -rust-version = "1.57" +rust-version = "1.64" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/src-tauri/src/authentification/mod.rs b/src-tauri/src/authentification/mod.rs index cead5c1..bde9eb6 100644 --- a/src-tauri/src/authentification/mod.rs +++ b/src-tauri/src/authentification/mod.rs @@ -2,7 +2,7 @@ use std::{fmt, net::TcpListener, sync::Arc}; use rand::{thread_rng, Rng}; use reqwest::{header::{CONTENT_TYPE, CONNECTION, ACCEPT, AUTHORIZATION}, Client}; -use serde_json::{Value, json}; +use serde_json::{Value, json, Map}; use tokio::{sync::mpsc, join}; use urlencoding::encode; use serde::{Deserialize, Serialize}; @@ -50,6 +50,14 @@ pub struct ReceivedCode { pub state: String, } +#[derive(Deserialize, Debug)] +pub struct GameProfile { + pub id: String, + pub name: String, + pub skins: Vec, + pub capes: Vec, +} + pub struct Authentification; impl Authentification { @@ -233,7 +241,7 @@ impl Authentification { } } - async fn fetch_minecraft_profile(mc_token: &String, reqwest_client: &Client) -> Result<(String, String)> { + async fn fetch_minecraft_profile(mc_token: &String, reqwest_client: &Client) -> Result { let received: Value = reqwest_client .get("https://api.minecraftservices.com/minecraft/profile") .header(AUTHORIZATION, format!("Bearer {}", mc_token)) @@ -245,11 +253,15 @@ impl Authentification { if let Some(val) = received.get("error") { bail!(String::from(val.as_str().unwrap())) } else { - Ok((String::from(received["id"].as_str().unwrap()), String::from(received["name"].as_str().unwrap()))) + let received: GameProfile = match serde_json::from_value(received) { + Ok(gp) => gp, + Err(err) => bail!(err), + }; + Ok(received) } } - pub async fn login(prompt: Prompt, app: tauri::AppHandle) -> Result<(String, String)> { + pub async fn login(prompt: Prompt, app: tauri::AppHandle) -> Result { let reqwest_client = Client::new(); let oauth_token = Self::fetch_oauth2_token(prompt, app).await?; let access_refresh_token = Self::fetch_token(oauth_token.0, oauth_token.1, &reqwest_client).await?; diff --git a/src-tauri/src/launcher/mod.rs b/src-tauri/src/launcher/mod.rs index be4b392..1f34981 100644 --- a/src-tauri/src/launcher/mod.rs +++ b/src-tauri/src/launcher/mod.rs @@ -1,13 +1,31 @@ -use std::fmt::Display; +use std::{fmt::Display, path::{self, Path}}; -use anyhow::{Result, bail, anyhow}; +use anyhow::{Result, bail}; use reqwest::Client; +use serde::{Serialize, Deserialize}; use serde_json::{Value, Map}; +use tokio::fs; + +use crate::authentification::GameProfile; + +#[derive(Serialize, Deserialize, Debug)] +pub struct VersionManifestV2 { + latest: Value, + versions: Vec +} + +#[derive(Serialize, Deserialize, Debug)] +struct Version { + id: String, + #[serde(rename(serialize = "type", deserialize = "type"))] + v_type: VersionType, + url: String, + sha1: String +} - -async fn get_version_manifest(reqwest: &Client) -> Result { - let received: Value = reqwest +async fn get_version_manifest(reqwest: &Client) -> Result { + let received: VersionManifestV2 = reqwest .get("https://piston-meta.mojang.com/mc/game/version_manifest_v2.json") .send() .await? @@ -16,40 +34,145 @@ async fn get_version_manifest(reqwest: &Client) -> Result { Ok(received) } -fn get_version_from_manifest<'a>(manifest: &'a Value, game_version: String, version_type: &VersionType) -> Result<&'a Map> { - let versions = manifest.get("versions").ok_or(anyhow!("Manifest format is invalid"))?; - let arr = versions.as_array().ok_or(anyhow!("Manifest format is invalid"))?; - for i in arr.iter().enumerate() { - let map = i.1.as_object().ok_or(anyhow!("Manifest format is invalid"))?; - let id = map.get("id").ok_or(anyhow!("Manifest format is invalid, cannot find version id"))?; - let id = id.as_str().ok_or(anyhow!("Manifest format is invalid, id is not a str"))?; - let v_type = map.get("type").ok_or(anyhow!("Manifest format is invalid, cannot find version type"))?; - let v_type = v_type.as_str().ok_or(anyhow!("Manifest format is invalid, type is not a str"))?; - if id == game_version && v_type.try_into() == Ok(version_type) { - return Ok(map); +fn get_version_from_manifest<'a>(manifest: &'a VersionManifestV2, game_version: String, version_type: &VersionType) -> Result<&'a Version> { + for i in manifest.versions.iter().enumerate() { + let id = i.1.id.clone(); + let v_type = i.1.v_type; + if id == game_version && &v_type == version_type { + return Ok(i.1); } } bail!("Version not Found") } -fn get_version_detail(reqwest: &Client, version : &Map) -> Result<()> { - bail!("Not implemented yet") +#[derive(Serialize, Deserialize)] +struct VersionDetail { + arguments: Map, + #[serde(rename(serialize = "assetIndex", deserialize = "assetIndex"))] + asset_index: Map, + assets: String, + downloads: Map, + id: String, + #[serde(rename(serialize = "javaVersion", deserialize = "javaVersion"))] + java_version: Map, + libraries: Vec, + logging: Map, + #[serde(rename(serialize = "mainClass", deserialize = "mainClass"))] + main_class: String, + #[serde(rename(serialize = "type", deserialize = "type"))] + v_type: VersionType } -pub async fn download_assets(game_version: String, version_type: &VersionType) -> Result<()> { - let reqwest_client = Client::new(); - let manifest = get_version_manifest(&reqwest_client).await?; - let version = get_version_from_manifest(&manifest, game_version, version_type)?; - Ok(()) - +#[derive(Serialize, Deserialize)] +struct Library { + downloads: LibraryDownload, + name: String, + rules: Vec +} + +#[derive(Serialize, Deserialize)] +struct LibraryRule { + action: String, + os: LibraryOSRule +} +#[derive(Serialize, Deserialize)] +struct LibraryOSRule { + name: OSName, +} + +#[derive(Serialize, Deserialize)] +enum OSName { + #[serde(rename(serialize = "osx", deserialize = "osx"))] + MacOsX, + #[serde(rename(serialize = "linux", deserialize = "linux"))] + Linux, + #[serde(rename(serialize = "windows", deserialize = "windows"))] + Windows +} + +#[derive(Serialize, Deserialize)] +struct LibraryDownload { + artifact: LibraryArtifact +} + +#[derive(Serialize, Deserialize)] +struct LibraryArtifact { + path: String, + sha1: String, + size: i64, + url: String, +} + +async fn get_version_detail(reqwest: &Client, version : &Version) -> Result { + let received: VersionDetail = reqwest + .get(version.url.clone()) + .send() + .await? + .json() + .await?; + Ok(received) +} +pub struct ClientOptions<'a> { + authorization: GameProfile, + root_path: &'a Path, + javaPath: String, + version_number: String, + version_type: VersionType, + // version_custom: String, // for a next update + memory_min: String, + memory_max: String, +} + +pub struct MinecraftClient<'a> { + opts: ClientOptions<'a>, + details: VersionDetail, + reqwest_client: Client, + +} + +impl<'a> MinecraftClient<'_> { + pub async fn new(opts: ClientOptions<'a>) -> Result> { + let reqwest_client = Client::new(); + let details = Self::load_manifest(&reqwest_client, &opts).await?; + Ok(MinecraftClient { + opts, + reqwest_client, + details, + }) + } + + async fn load_manifest(reqwest_client: &Client, opts: &ClientOptions<'a>) -> Result { + let manifest = get_version_manifest(&reqwest_client).await?; + let version = get_version_from_manifest(&manifest, opts.version_number.clone(), &opts.version_type)?; + let details = get_version_detail(&reqwest_client, version).await?; + Ok(details) + } + + pub async fn download_assets(&self) -> Result<()> { + // create root folder if it doesn't exist + fs::create_dir_all(self.opts.root_path).await?; + fs::create_dir(self.opts.root_path.join("librairies")).await?; + + Ok(()) + } + /// Filter non necessary librairies for the current OS + fn filter_non_necessary_librairies(&self) -> Result<()> { + bail!("Not implemented yet") + } } -#[derive(PartialEq)] + + +#[derive(PartialEq, Serialize, Deserialize, Debug, Clone, Copy)] pub enum VersionType { + #[serde(alias = "release")] Release, + #[serde(alias = "snapshot")] Snapshot, + #[serde(alias = "old_alpha")] OldAlpha, + #[serde(alias = "old_beta")] OldBeta, } @@ -79,4 +202,4 @@ impl Display for VersionType { }; write!(f, "{}", str) } -} \ No newline at end of file +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index e41c54a..c29bbdb 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -19,7 +19,7 @@ fn greet(name: &str) -> String { async fn login(app: tauri::AppHandle, _window: tauri::Window) -> Result { let result = Authentification::login(Prompt::SelectAccount, app).await; match result { - Ok(val) => Ok(format!("Hello {}", val.1)), + Ok(val) => Ok(format!("Hello {}", val.name)), Err(err) => Err(err.to_string()) } }