Add a dowload game button, move all related version manifest component to manifest.rs

This commit is contained in:
Quentin Legot 2023-04-22 00:32:27 +02:00
parent 4d2ed01cc0
commit 6cdf71b5b6
8 changed files with 267 additions and 169 deletions

23
src-tauri/Cargo.lock generated
View File

@ -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",

View File

@ -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"]

View File

@ -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};

View File

@ -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<Version>
}
#[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<VersionManifestV2> {
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<String, Value>,
#[serde(rename(serialize = "assetIndex", deserialize = "assetIndex"))]
asset_index: Map<String, Value>,
assets: String,
downloads: Map<String, Value>,
id: String,
#[serde(rename(serialize = "javaVersion", deserialize = "javaVersion"))]
java_version: Map<String, Value>,
pub libraries: Vec<Library>,
logging: Map<String, Value>,
#[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<LibraryRule>
}
#[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<VersionDetail> {
let received: VersionDetail = reqwest
.get(version.url.clone())
.send()?
.json()?;
Ok(received)
}

View File

@ -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<Version>
}
#[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<VersionManifestV2> {
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<String, Value>,
#[serde(rename(serialize = "assetIndex", deserialize = "assetIndex"))]
asset_index: Map<String, Value>,
assets: String,
downloads: Map<String, Value>,
id: String,
#[serde(rename(serialize = "javaVersion", deserialize = "javaVersion"))]
java_version: Map<String, Value>,
libraries: Vec<Library>,
logging: Map<String, Value>,
#[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<LibraryRule>
}
#[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<VersionDetail> {
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<MinecraftClient<'a>> {
pub fn new(opts: &'a ClientOptions<'a>) -> Result<MinecraftClient<'a>> {
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<VersionDetail> {
let manifest = get_version_manifest(&reqwest_client).await?;
fn load_manifest(reqwest_client: &Client, opts: &ClientOptions<'a>) -> Result<VersionDetail> {
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)
}
}

View File

@ -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<GameProfile>);
// 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<String, String> {
async fn login(app: tauri::AppHandle, _window: tauri::Window, state: tauri::State<'_, Mutex<CustomState>>) -> Result<String, String> {
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<CustomState>>) -> Result<String, String> {
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");
}

View File

@ -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 {
<div class="row">
<div>
<button id="greet-button" type="button" v-on:click="login">{{ button_message }}</button>
<button id="greet-button" :disabled="greetDisabled == 1" type="button" v-on:click="login">{{ button_message }}</button>
<button id="download-button" :class="{hide: hideDownloadButton }" v-on:click="download">Download game</button>
</div>
</div>

View File

@ -84,6 +84,10 @@ button {
margin-right: 5px;
}
.hide {
display: none;
}
@media (prefers-color-scheme: dark) {
:root {
color: #f6f6f6;