diff --git a/Cargo.lock b/Cargo.lock index cbf66b6..eb4e1f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6,6 +6,7 @@ version = 4 name = "CraftX" version = "0.1.0" dependencies = [ + "base64 0.22.1", "dirs", "futures", "rand 0.9.0", diff --git a/Cargo.toml b/Cargo.toml index f49012a..7708d96 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,4 +13,5 @@ rand = "0.9.0" serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0" toml = "0.8.20" -surf = { version = "2.3.2", features = ["hyper-client"] } \ No newline at end of file +surf = { version = "2.3.2", features = ["hyper-client"] } +base64 = "0.22.1" \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index bf67ca7..e5c6cd9 100644 --- a/src/config.rs +++ b/src/config.rs @@ -2,6 +2,8 @@ use std::path::PathBuf; use serde::{Deserialize, Serialize}; + + #[derive(Default, Serialize, Deserialize)] pub struct LauncherConfig { is_portable: bool, diff --git a/src/launcher.rs b/src/launcher.rs index 0309df6..7da06a8 100644 --- a/src/launcher.rs +++ b/src/launcher.rs @@ -1,7 +1,10 @@ use core::str; -use std::sync::Arc; -use tokio::sync::{mpsc, Mutex}; -use tokio::sync::mpsc::{Sender, UnboundedReceiver, UnboundedSender}; +use base64::{encode, Engine}; +use base64::prelude::BASE64_STANDARD; +use tokio::fs::File; +use tokio::sync::mpsc; +use tokio::sync::mpsc::UnboundedSender; +use crate::minecraft::versions::Version; use crate::{config::LauncherConfig, minecraft::versions::VersionConfig, util}; #[derive(Default)] @@ -27,7 +30,7 @@ impl Launcher { } pub fn save_config(&self) { - std::fs::write(self.config.config_path(), toml::to_string_pretty(&self.config).unwrap()); + let _ = std::fs::write(self.config.config_path(), toml::to_string_pretty(&self.config).unwrap()); } pub fn init_config(&mut self, user_name: String) { @@ -37,24 +40,54 @@ impl Launcher { self.save_config(); } - pub async fn new_vanilla_instance(&mut self, config: VersionConfig, sender: UnboundedSender<(u8, String)>) { + pub fn get_instances_list(&self) -> Vec<(String, String, String)> { + let mut v = Vec::new(); + let mut instances = self.config.launcher_dir(); + instances.push("instances"); + if let Ok(entries) = std::fs::read_dir(instances) { + for entry in entries { + if entry.is_err() { continue; } + let entry = entry.unwrap(); + if !entry.metadata().unwrap().is_dir() { continue; } + let mut p = entry.path(); + p.push("client.json"); + if let Ok(data) = std::fs::read(p) { + let config: VersionConfig = serde_json::from_slice(&data).unwrap(); + v.push((config.id, config.r#type, format!("data:image/png;base64,{}", BASE64_STANDARD.encode(include_bytes!("www/icons/alpha.png"))))); + } + } + } + v + } + + pub async fn new_vanilla_instance(&mut self, config: VersionConfig, version_object: &Version, sender: UnboundedSender<(u8, String)>) { let (sx, mut rx) = mpsc::unbounded_channel(); let root = self.config.launcher_dir(); let mut instances = root.clone(); instances.push("instances"); - instances.push(config.id); + instances.push(&config.id); - std::fs::create_dir_all(&instances); + let _ = std::fs::create_dir_all(&instances); instances.push("client.jar"); let mut overall_size = config.downloads.client.size as usize; + let mut cnt = 0; let client_jar_url = config.downloads.client.url; - util::download_file(&client_jar_url, instances.to_str().unwrap(), config.downloads.client.size, sx.clone(), "Downloading client.jar"); + let mut client_json_path = root.clone(); + client_json_path.push("instances"); + client_json_path.push(config.id); + client_json_path.push("client.json"); + + let _ = util::download_file(&version_object.url, client_json_path.to_str().unwrap(), sx.clone(), "Downloading client.json"); + cnt += 1; + + let _ = util::download_file(&client_jar_url, instances.to_str().unwrap(), sx.clone(), "Downloading client.jar"); + cnt += 1; let mut libraries = root.clone(); libraries.push("libraries"); @@ -66,17 +99,72 @@ impl Launcher { let mut dl_path = libraries.clone(); let mut dl_pp = libraries.clone(); dl_pp.push(library.to_pathbuf_path()); - std::fs::create_dir_all(dl_pp); + let _ = std::fs::create_dir_all(dl_pp); dl_path.push(library.to_pathbuf_file()); - util::download_file(&artifact.url, dl_path.to_str().unwrap(), config.downloads.client.size, sx.clone(), "Downloading libraries"); + let _ = util::download_file(&artifact.url, dl_path.to_str().unwrap(), sx.clone(), "Downloading libraries"); + cnt += 1; } + + if let Some(classifiers) = &library.downloads.classifiers { + if let Some(natives) = &classifiers.natives { + overall_size += natives.size as usize; + let mut dl_path = libraries.clone(); + dl_path.push(&natives.path); + let t_p = dl_path.to_str().unwrap().split("/").collect::>(); + let t_p = t_p[..t_p.len()-1].join("/"); + let _ = std::fs::create_dir_all(&t_p); + let _ = util::download_file(&natives.url, dl_path.to_str().unwrap(), sx.clone(), "Downloading natives"); + cnt += 1; + } + } + } + + let mut assets_path = root.clone(); + assets_path.push("assets"); + + let mut indexes = assets_path.clone(); + indexes.push("indexes"); + let _ = std::fs::create_dir_all(indexes); + + let mut objects = assets_path.clone(); + objects.push("objects"); + let _ = std::fs::create_dir_all(objects); + + let mut index = assets_path.clone(); + index.push(config.assetIndex.to_path()); + + let _ = util::download_file(&config.assetIndex.url, index.to_str().unwrap(), sx.clone(), "Downloading assets indexes"); + cnt += 1; + + let asset_index = config.assetIndex.url; + + overall_size += config.assetIndex.size as usize; + overall_size += config.assetIndex.totalSize as usize; + + let assets = crate::minecraft::assets::fetch_assets_list(&asset_index).await.unwrap().objects; + + for (_key, asset) in assets { + let mut single_object = assets_path.clone(); + single_object.push(asset.to_path()); + + let mut single_object_path = assets_path.clone(); + single_object_path.push(asset.to_small_path()); + std::fs::create_dir_all(single_object_path); + + util::download_file(&asset.to_url(), single_object.to_str().unwrap(), sx.clone(), "Downloading assets objects"); + cnt += 1; } tokio::spawn(async move { let mut current_size = 0; + let mut current_cnt = 0; while let Some((size, status)) = rx.recv().await { current_size += size; + current_cnt += 1; sender.send((((current_size as f32 / overall_size as f32) * 100.0) as u8, status)); + if current_cnt >= cnt { + sender.send((100, "_".to_string())); + } } }); } diff --git a/src/main.rs b/src/main.rs index f42da65..567dd6c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -82,7 +82,7 @@ async fn main() { if let Some((ui_action, params, responder)) = receiver.recv().await { let ui_action = &ui_action[16..]; match ui_action { - "ui" => responder.respond(Response::new(include_str!("www/portable.html").as_bytes())), + "ui" => responder.respond(Response::new(include_bytes!("www/portable.html"))), "portable" => { launcher.config.set_portable(true); launcher.init_dirs(); @@ -128,7 +128,7 @@ async fn main() { Ok(config ) => { println!("Config: {}", config.id); responder.respond(Response::new(serde_json::to_vec(&UIMessage { params: vec!["show_loading".to_string(), "sidebar_off".to_string()] }).unwrap())); - launcher.new_vanilla_instance(config, sx.clone()).await; + launcher.new_vanilla_instance(config, version, sx.clone()).await; } Err(e) => { println!("Error: {}", e); @@ -137,6 +137,18 @@ async fn main() { } } } + "fetch_instances_list" => { + let resp = launcher.get_instances_list(); + let mut v: Vec = Vec::new(); + v.push("set_instances_list".to_string()); + for (id, release_type, img) in resp { + v.push(id); + v.push(release_type); + v.push(img); + } + + responder.respond(Response::new(serde_json::to_vec(&UIMessage { params: v }).unwrap())); + } "check_download_status" => { if let Ok((percent, text)) = dl_rec.try_recv() { responder.respond(Response::new(serde_json::to_vec(&UIMessage { params: vec!["update_downloads".to_string(), text, percent.to_string()] }).unwrap())); diff --git a/src/minecraft.rs b/src/minecraft.rs index b7110e3..fca9ea9 100644 --- a/src/minecraft.rs +++ b/src/minecraft.rs @@ -33,6 +33,7 @@ pub mod versions { pub mainClass: String, pub downloads: ConfigDownloads, pub id: String, + pub r#type: String, pub libraries: Vec } @@ -77,9 +78,24 @@ pub mod versions { } } + #[derive(Serialize, Deserialize)] + pub struct LibraryClassifiers { + #[serde(rename = "natives-windows")] + pub natives: Option + } + + #[derive(Serialize, Deserialize)] + pub struct LibraryNatives { + pub path: String, + pub sha1: String, + pub size: u64, + pub url: String + } + #[derive(Serialize, Deserialize)] pub struct LibraryDownloads { - pub artifact: Option + pub artifact: Option, + pub classifiers: Option } #[derive(Serialize, Deserialize)] @@ -107,9 +123,19 @@ pub mod versions { pub id: String, pub sha1: String, pub totalSize: u64, + pub size: u64, pub url: String } + impl ConfigAssetIndex { + pub fn to_path(&self) -> PathBuf { + let mut p = PathBuf::new(); + p.push("indexes"); + p.push([&self.id, ".json"].concat()); + p + } + } + pub async fn fetch_versions_list() -> Result> { let mut r = surf::get("https://piston-meta.mojang.com/mc/game/version_manifest_v2.json").await?; let resp = r.body_bytes().await.unwrap(); @@ -124,4 +150,48 @@ pub mod versions { let resp: VersionConfig = serde_json::from_slice(&resp)?; Ok(resp) } +} + +pub mod assets { + use std::{collections::HashMap, error::Error, path::PathBuf}; + use serde::{Deserialize, Serialize}; + + #[derive(Serialize, Deserialize)] + pub struct SingleAsset { + pub hash: String, + pub sha1: Option + } + + impl SingleAsset { + pub fn to_url(&self) -> String { + ["https://resources.download.minecraft.net/", &self.hash[..2], "/", &self.hash].concat() + } + + pub fn to_path(&self) -> PathBuf { + let mut p = PathBuf::new(); + p.push("objects"); + p.push(&self.hash[..2]); + p.push(&self.hash); + p + } + + pub fn to_small_path(&self) -> PathBuf { + let mut p = PathBuf::new(); + p.push("objects"); + p.push(&self.hash[..2]); + p + } + } + + #[derive(Serialize, Deserialize)] + pub struct Assets { + pub objects: HashMap + } + + pub async fn fetch_assets_list(url: &str) -> Result> { + let mut r = surf::get(url).await?; + let resp = r.body_bytes().await.unwrap(); + let resp: Assets = serde_json::from_slice(&resp)?; + Ok(resp) + } } \ No newline at end of file diff --git a/src/util.rs b/src/util.rs index d056c19..efb0b59 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,10 +1,7 @@ -use std::sync::{Arc, Mutex}; use futures::AsyncReadExt; use rand::{distr::Alphanumeric, Rng}; use tokio::{fs::File, io::AsyncWriteExt}; -use tokio::sync::mpsc::{Sender, UnboundedReceiver, UnboundedSender}; -use tokio::sync::{mpsc}; -use crate::launcher::Launcher; +use tokio::sync::mpsc::UnboundedSender; pub fn random_string(len: usize) -> String { rand::rng() @@ -14,29 +11,30 @@ pub fn random_string(len: usize) -> String { .collect() } -pub fn download_file(url: &str, file_path: &str, size: u64, sender: UnboundedSender<(usize, String)>, status: &str) -> Result<(), Box> { +pub fn download_file(url: &str, file_path: &str, sender: UnboundedSender<(usize, String)>, status: &str) -> Result<(), Box> { let url = url.to_string(); let file_path = file_path.to_string(); let status = status.to_string(); tokio::spawn( async move { - let mut res = surf::get(url).await.unwrap(); - - let total_size = res.len().unwrap_or(0); // Total size in bytes (if available) - let mut downloaded = 0; - let mut buf = vec![0; 8192]; // Buffer for reading chunks - - let mut file = File::create(file_path).await.unwrap(); - - let mut r= res.take_body().into_reader(); - while let Ok(n) = r.read(&mut buf).await { - if n == 0 { - break; - } - downloaded += n; + if let Ok(mut res) = surf::get(url).await { + let mut downloaded = 0; + let mut buf = vec![0; 8192]; // Buffer for reading chunks - file.write(&buf[..n]).await; + let mut file = File::create(file_path).await.unwrap(); + + let mut r= res.take_body().into_reader(); + while let Ok(n) = r.read(&mut buf).await { + if n == 0 { + break; + } + downloaded += n; + + let _ = file.write(&buf[..n]).await; + } + let _ = sender.send((downloaded, status.clone())); + } else { + let _ = sender.send((0, status.clone())); } - sender.send((downloaded, status.clone())); }); Ok(()) } diff --git a/src/www/icons/alpha.png b/src/www/icons/alpha.png new file mode 100644 index 0000000..fd7a724 Binary files /dev/null and b/src/www/icons/alpha.png differ diff --git a/src/www/icons/new_era.png b/src/www/icons/new_era.png new file mode 100644 index 0000000..710fef8 Binary files /dev/null and b/src/www/icons/new_era.png differ diff --git a/src/www/icons/release.png b/src/www/icons/release.png new file mode 100644 index 0000000..617d353 Binary files /dev/null and b/src/www/icons/release.png differ diff --git a/src/www/portable.html b/src/www/portable.html index af0c77f..d334126 100644 --- a/src/www/portable.html +++ b/src/www/portable.html @@ -27,7 +27,7 @@
-