From 6cdf71b5b6513d583e10fc260e54f8e269b803af Mon Sep 17 00:00:00 2001 From: Quentin Legot Date: Sat, 22 Apr 2023 00:32:27 +0200 Subject: [PATCH] Add a dowload game button, move all related version manifest component to manifest.rs --- src-tauri/Cargo.lock | 23 +++ src-tauri/Cargo.toml | 7 +- src-tauri/src/authentification/mod.rs | 2 +- src-tauri/src/launcher/manifest.rs | 107 ++++++++++++++ src-tauri/src/launcher/mod.rs | 202 +++++++------------------- src-tauri/src/main.rs | 64 +++++++- src/components/login.js | 27 +++- src/style.css | 4 + 8 files changed, 267 insertions(+), 169 deletions(-) create mode 100644 src-tauri/src/launcher/manifest.rs diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 226ec80..10bb35f 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -7,6 +7,7 @@ name = "Launcher-tauri" version = "0.0.0" dependencies = [ "anyhow", + "directories", "log4rs", "rand 0.8.5", "reqwest", @@ -618,6 +619,15 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "directories" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74be3be809c18e089de43bdc504652bb2bc473fca8756131f8689db8cf079ba9" +dependencies = [ + "dirs-sys", +] + [[package]] name = "dirs-next" version = "2.0.0" @@ -628,6 +638,17 @@ dependencies = [ "dirs-sys-next", ] +[[package]] +name = "dirs-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04414300db88f70d74c5ff54e50f9e1d1737d9a5b90f53fcf2e95ca2a9ab554b" +dependencies = [ + "libc", + "redox_users", + "windows-sys 0.45.0", +] + [[package]] name = "dirs-sys-next" version = "0.1.2" @@ -855,9 +876,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" dependencies = [ "futures-core", + "futures-io", "futures-macro", "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", "slab", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 70048a1..67ae0c8 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -20,16 +20,17 @@ tauri = {version = "1.2", features = ["api-all"] } tokio = { version = "1", features = ["full"] } uuid = "1.2.2" log4rs = "1.2.0" -reqwest = { version = "0.11.13", default-features = true, features = ["json"] } +reqwest = { version = "0.11.13", default-features = true, features = ["json", "blocking"] } urlencoding = "2.1.2" warp = "0.3.3" anyhow = "1.0.66" rand = "0.8.5" +directories = "5.0.0" [features] # by default Tauri runs in production mode # when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL -default = [ "custom-protocol" ] +default = ["custom-protocol"] # this feature is used used for production builds where `devPath` points to the filesystem # DO NOT remove this -custom-protocol = [ "tauri/custom-protocol" ] +custom-protocol = ["tauri/custom-protocol"] diff --git a/src-tauri/src/authentification/mod.rs b/src-tauri/src/authentification/mod.rs index bde9eb6..90c06d0 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, Map}; +use serde_json::{Value, json}; use tokio::{sync::mpsc, join}; use urlencoding::encode; use serde::{Deserialize, Serialize}; diff --git a/src-tauri/src/launcher/manifest.rs b/src-tauri/src/launcher/manifest.rs new file mode 100644 index 0000000..5426b40 --- /dev/null +++ b/src-tauri/src/launcher/manifest.rs @@ -0,0 +1,107 @@ +use anyhow::{Result, bail}; +use reqwest::blocking::Client; +use serde::{Serialize, Deserialize}; +use serde_json::{Value, Map}; + +use super::VersionType; + +#[derive(Serialize, Deserialize, Debug)] +pub struct VersionManifestV2 { + latest: Value, + versions: Vec +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Version { + id: String, + #[serde(rename(serialize = "type", deserialize = "type"))] + v_type: VersionType, + url: String, + sha1: String +} + + +pub fn get_version_manifest(reqwest: &Client) -> Result { + let received: VersionManifestV2 = reqwest + .get("https://piston-meta.mojang.com/mc/game/version_manifest_v2.json") + .send()? + .json()?; + Ok(received) +} + +pub 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") +} + +#[derive(Serialize, Deserialize)] +pub 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, + pub libraries: Vec, + logging: Map, + #[serde(rename(serialize = "mainClass", deserialize = "mainClass"))] + main_class: String, + #[serde(rename(serialize = "type", deserialize = "type"))] + v_type: VersionType +} + +#[derive(Serialize, Deserialize)] +pub struct Library { + pub downloads: LibraryDownload, + pub name: String, + pub rules: Vec +} + +#[derive(Serialize, Deserialize)] +pub struct LibraryDownload { + artifact: LibraryArtifact +} + +#[derive(Serialize, Deserialize)] +pub struct LibraryRule { + pub action: String, + pub os: LibraryOSRule +} +#[derive(Serialize, Deserialize)] +pub struct LibraryOSRule { + pub name: OSName, +} + +#[derive(Serialize, Deserialize, PartialEq)] +pub 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 LibraryArtifact { + path: String, + sha1: String, + size: i64, + url: String, +} + +pub fn get_version_detail(reqwest: &Client, version : &Version) -> Result { + let received: VersionDetail = reqwest + .get(version.url.clone()) + .send()? + .json()?; + Ok(received) +} \ No newline at end of file diff --git a/src-tauri/src/launcher/mod.rs b/src-tauri/src/launcher/mod.rs index 1f34981..d60d55a 100644 --- a/src-tauri/src/launcher/mod.rs +++ b/src-tauri/src/launcher/mod.rs @@ -1,139 +1,45 @@ -use std::{fmt::Display, path::{self, Path}}; +mod manifest; -use anyhow::{Result, bail}; -use reqwest::Client; +use std::{path::Path, fs}; + +use anyhow::Result; +use reqwest::blocking::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 -} +use self::manifest::{VersionDetail, get_version_manifest, get_version_from_manifest, get_version_detail, Library, OSName}; -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? - .json() - .await?; - Ok(received) -} +#[cfg(target_os="windows")] +const ACTUAL_OS: OSName = OSName::Windows; +#[cfg(target_os="linux")] +const ACTUAL_OS: OSName = OSName::Linux; +#[cfg(target_os="macos")] +const ACTUAL_OS: OSName = OSName::MacOsX; -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") -} - -#[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 -} - -#[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, + pub authorization: &'a GameProfile, + pub root_path: &'a Path, + pub java_path: &'a Path, + pub version_number: String, + pub version_type: VersionType, // version_custom: String, // for a next update - memory_min: String, - memory_max: String, + pub memory_min: String, + pub memory_max: String, } pub struct MinecraftClient<'a> { - opts: ClientOptions<'a>, + opts: &'a ClientOptions<'a>, details: VersionDetail, reqwest_client: Client, } impl<'a> MinecraftClient<'_> { - pub async fn new(opts: ClientOptions<'a>) -> Result> { + pub fn new(opts: &'a ClientOptions<'a>) -> Result> { let reqwest_client = Client::new(); - let details = Self::load_manifest(&reqwest_client, &opts).await?; + let details = Self::load_manifest(&reqwest_client, &opts)?; Ok(MinecraftClient { opts, reqwest_client, @@ -141,23 +47,41 @@ impl<'a> MinecraftClient<'_> { }) } - async fn load_manifest(reqwest_client: &Client, opts: &ClientOptions<'a>) -> Result { - let manifest = get_version_manifest(&reqwest_client).await?; + fn load_manifest(reqwest_client: &Client, opts: &ClientOptions<'a>) -> Result { + let manifest = get_version_manifest(&reqwest_client)?; let version = get_version_from_manifest(&manifest, opts.version_number.clone(), &opts.version_type)?; - let details = get_version_detail(&reqwest_client, version).await?; + let details = get_version_detail(&reqwest_client, version)?; Ok(details) } - pub async fn download_assets(&self) -> Result<()> { + pub fn download_assets(&mut 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?; - + fs::create_dir_all(self.opts.root_path)?; + fs::create_dir(self.opts.root_path.join("librairies"))?; + self.filter_non_necessary_librairies(); Ok(()) } /// Filter non necessary librairies for the current OS - fn filter_non_necessary_librairies(&self) -> Result<()> { - bail!("Not implemented yet") + fn filter_non_necessary_librairies(&mut self) { + self.details.libraries.retain(|e| { Self::should_use_library(e) }); + } + + fn should_use_library(library: &Library) -> bool { + if library.rules.is_empty() { + true + } else { + for i in library.rules.iter().enumerate() { + let op = if i.1.action == "allow" { + true + } else { + false + }; + if i.1.os.name == ACTUAL_OS { + return op; + } + } + false + } } } @@ -175,31 +99,3 @@ pub enum VersionType { #[serde(alias = "old_beta")] OldBeta, } - -impl<'a> TryInto<&'a VersionType> for &str { - type Error = (); - - fn try_into(self) -> std::result::Result<&'a VersionType, Self::Error> { - match self { - "release" => Ok(&VersionType::Release), - "snapshot" => Ok(&VersionType::Snapshot), - "old_alpha" => Ok(&VersionType::OldAlpha), - "old_beta" => Ok(&VersionType::OldBeta), - _ => Err(()), - } - } - - -} - -impl Display for VersionType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let str = match self { - Self::Release => "release", - Self::Snapshot => "snapshot", - Self::OldAlpha => "old_alpha", - Self::OldBeta => "old_beta", - }; - write!(f, "{}", str) - } -} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index c29bbdb..c3fdbef 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -6,8 +6,14 @@ pub mod authentification; pub mod launcher; -use authentification::{Authentification, Prompt}; +use std::sync::{Mutex, Arc}; + +use authentification::{Authentification, Prompt, GameProfile}; use anyhow::Result; +use directories::BaseDirs; +use launcher::{MinecraftClient, ClientOptions}; + +struct CustomState (Option); // Learn more about Tauri commands at https://tauri.app/v1/guides/features/command #[tauri::command] @@ -16,18 +22,68 @@ fn greet(name: &str) -> String { } #[tauri::command] -async fn login(app: tauri::AppHandle, _window: tauri::Window) -> Result { +async fn login(app: tauri::AppHandle, _window: tauri::Window, state: tauri::State<'_, Mutex>) -> Result { let result = Authentification::login(Prompt::SelectAccount, app).await; match result { - Ok(val) => Ok(format!("Hello {}", val.name)), + Ok(val) => { + let name = val.name.clone(); + state.lock().unwrap().0.replace(val); + Ok(format!("Hello {}", name)) + }, Err(err) => Err(err.to_string()) } } +#[tauri::command] +async fn download(app: tauri::AppHandle, _window: tauri::Window, state: tauri::State<'_, Mutex>) -> Result { + if let Some(base_dir) = BaseDirs::new() { + let data_folder = base_dir.data_dir().join(".altarik"); + let root_path = data_folder.as_path(); + match state.lock() { + Ok(game_profile) => { + let game_profile = game_profile.0.as_ref().unwrap(); + let java_path = root_path.join("java"); + let opts = ClientOptions { + authorization: &game_profile, + root_path, + java_path: &java_path.as_path(), + version_number: "1.19.4".to_string(), + version_type: launcher::VersionType::Release, + memory_min: "2G".to_string(), + memory_max: "4G".to_string(), + }; + let client = MinecraftClient::new(&opts); + match client { + Ok(mut client) => { + match client.download_assets() { + Ok(_) => { + Ok("Content downloaded".to_string()) + }, + Err(err) => { + Err(err.to_string()) + } + } + }, + Err(err) => { + Err(err.to_string()) + } + } + }, + Err(err) => { + Err(err.to_string()) + } + } + + } else { + Err("Cannot download files".to_string()) + } +} + #[tokio::main] async fn main() { tauri::Builder::default() - .invoke_handler(tauri::generate_handler![greet, login]) + .manage(Arc::new(CustomState(None))) + .invoke_handler(tauri::generate_handler![greet, login, download]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/src/components/login.js b/src/components/login.js index 4c36db0..82b2e10 100644 --- a/src/components/login.js +++ b/src/components/login.js @@ -2,18 +2,28 @@ export default { data() { return { button_message: "Login to minecraft", - greet_message: "" + greet_message: "", + greetDisabled: 0, + hideDownloadButton: true, } }, methods: { login (e) { e.preventDefault() - this.invoke("login", {}).then(value => { - this.greet_message = value - }).catch(err => { - this.greet_message = "Error: " + err - }) - } + if(!this.greetDisabled) { + this.greetDisabled = true + this.invoke("login", {}).then(value => { + this.greet_message = value + this.hideDownloadButton = false + }).catch(err => { + this.greet_message = "Error: " + err + this.greetDisabled = false + }) + } + }, + download (e) { + + }, }, props: { invoke: Object @@ -23,7 +33,8 @@ export default {
- + +
diff --git a/src/style.css b/src/style.css index b5dc5cc..07e4fb2 100644 --- a/src/style.css +++ b/src/style.css @@ -84,6 +84,10 @@ button { margin-right: 5px; } +.hide { + display: none; +} + @media (prefers-color-scheme: dark) { :root { color: #f6f6f6;