From affa1c71d7fd007897e99351bdc0295cdfe6f477 Mon Sep 17 00:00:00 2001 From: kawaiinekololis Date: Sat, 30 Mar 2024 11:20:23 +0100 Subject: [PATCH] refactor: whole launch base + bump v0.2.6 --- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/app/gui.rs | 62 +-- src-tauri/src/app/webview.rs | 15 +- src-tauri/src/main.rs | 15 +- src-tauri/src/minecraft/java/distribution.rs | 2 +- .../src/minecraft/java/jre_downloader.rs | 41 +- src-tauri/src/minecraft/java/runtime.rs | 2 + src-tauri/src/minecraft/launcher.rs | 382 ------------------ src-tauri/src/minecraft/launcher/assets.rs | 88 ++++ .../src/minecraft/launcher/client_jar.rs | 89 ++++ src-tauri/src/minecraft/launcher/jre.rs | 53 +++ src-tauri/src/minecraft/launcher/libraries.rs | 120 ++++++ src-tauri/src/minecraft/launcher/mod.rs | 309 ++++++++++++++ src-tauri/src/minecraft/prelauncher.rs | 240 ++++++++--- src-tauri/src/minecraft/progress.rs | 1 + src-tauri/src/minecraft/version.rs | 58 +-- src-tauri/src/utils/macros.rs | 35 ++ src-tauri/src/utils/mod.rs | 1 + src-tauri/tauri.conf.json | 2 +- 21 files changed, 982 insertions(+), 539 deletions(-) delete mode 100644 src-tauri/src/minecraft/launcher.rs create mode 100644 src-tauri/src/minecraft/launcher/assets.rs create mode 100644 src-tauri/src/minecraft/launcher/client_jar.rs create mode 100644 src-tauri/src/minecraft/launcher/jre.rs create mode 100644 src-tauri/src/minecraft/launcher/libraries.rs create mode 100644 src-tauri/src/minecraft/launcher/mod.rs create mode 100644 src-tauri/src/utils/macros.rs diff --git a/package.json b/package.json index b3c925a..0ce5445 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "liquidlauncher", "private": true, - "version": "0.2.5", + "version": "0.2.6", "type": "module", "scripts": { "dev": "vite", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 99218cc..89b1b75 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2285,7 +2285,7 @@ checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "liquidlauncher" -version = "0.2.5" +version = "0.2.6" dependencies = [ "anyhow", "async-compression", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 729eb95..a61b0cd 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "liquidlauncher" -version = "0.2.5" +version = "0.2.6" description = "A LiquidBounce launcher for Minecraft, written in Rust using Tauri." authors = ["1zuna ", "superblaubeere27"] license = "GNU General Public License v3.0" diff --git a/src-tauri/src/app/gui.rs b/src-tauri/src/app/gui.rs index 9572d77..c6ed660 100644 --- a/src-tauri/src/app/gui.rs +++ b/src-tauri/src/app/gui.rs @@ -31,6 +31,8 @@ use crate::utils::percentage_of_total_memory; use super::{api::{ApiEndpoints, Build, LoaderMod, ModSource}, app_data::LauncherOptions}; +pub type ShareableWindow = Arc>; + struct RunnerInstance { terminator: tokio::sync::oneshot::Sender<()>, } @@ -204,15 +206,7 @@ async fn delete_custom_mod(branch: &str, mc_version: &str, mod_name: &str) -> Re Ok(()) } -pub fn log(window: &Arc>, msg: &str) { - info!("{}", msg); - - if let Ok(k) = window.lock() { - let _ = k.emit("process-output", msg); - } -} - -fn handle_stdout(window: &Arc>, data: &[u8]) -> anyhow::Result<()> { +fn handle_stdout(window: &ShareableWindow, data: &[u8]) -> anyhow::Result<()> { let data = String::from_utf8(data.to_vec())?; if data.is_empty() { return Ok(()); // ignore empty lines @@ -223,7 +217,7 @@ fn handle_stdout(window: &Arc>, data: &[u8]) -> anyhow: Ok(()) } -fn handle_stderr(window: &Arc>, data: &[u8]) -> anyhow::Result<()> { +fn handle_stderr(window: &ShareableWindow, data: &[u8]) -> anyhow::Result<()> { let data = String::from_utf8(data.to_vec())?; if data.is_empty() { return Ok(()); // ignore empty lines @@ -234,14 +228,29 @@ fn handle_stderr(window: &Arc>, data: &[u8]) -> anyhow: Ok(()) } -fn handle_progress(window: &Arc>, progress_update: ProgressUpdate) -> anyhow::Result<()> { - window.lock().map_err(|_| anyhow!("Window lock is poisoned"))?.emit("progress-update", progress_update)?; +fn handle_progress(window: &ShareableWindow, progress_update: ProgressUpdate) -> anyhow::Result<()> { + window.lock().map_err(|_| anyhow!("Window lock is poisoned"))?.emit("progress-update", &progress_update)?; + + // Check if progress update is label update + if let ProgressUpdate::SetLabel(label) = progress_update { + handle_log(window, &label)?; + } + Ok(()) +} + +fn handle_log(window: &ShareableWindow, msg: &str) -> anyhow::Result<()> { + info!("{}", msg); + + if let Ok(k) = window.lock() { + let _ = k.emit("process-output", msg); + } Ok(()) } #[tauri::command] async fn run_client(build_id: u32, account_data: MinecraftAccount, options: LauncherOptions, mods: Vec, window: Window, app_state: tauri::State<'_, AppState>) -> Result<(), String> { - let window_mutex = Arc::new(std::sync::Mutex::new(window)); + // A shared mutex for the window object. + let shareable_window: ShareableWindow = Arc::new(Mutex::new(window)); let (account_name, uuid, token, user_type) = match account_data { MinecraftAccount::MsaAccount { msa: _, xbl: _, mca, profile } => (profile.name, profile.id.to_string(), mca.data.access_token, "msa".to_string()), @@ -292,31 +301,34 @@ async fn run_client(build_id: u32, account_data: MinecraftAccount, options: Laun .block_on(async { let keep_launcher_open = parameters.keep_launcher_open; + let launcher_data = LauncherData { + on_stdout: handle_stdout, + on_stderr: handle_stderr, + on_progress: handle_progress, + on_log: handle_log, + hide_window: |w| w.lock().unwrap().hide().unwrap(), + data: Box::new(shareable_window.clone()), + terminator: terminator_rx + }; + if let Err(e) = prelauncher::launch( launch_manifest, parameters, mods, - LauncherData { - on_stdout: handle_stdout, - on_stderr: handle_stderr, - on_progress: handle_progress, - data: Box::new(window_mutex.clone()), - terminator: terminator_rx - }, - window_mutex.clone() + launcher_data ).await { if !keep_launcher_open { - window_mutex.lock().unwrap().show().unwrap(); + shareable_window.lock().unwrap().show().unwrap(); } let message = format!("An error occourd:\n\n{:?}", e); - window_mutex.lock().unwrap().emit("client-error", format!("{}\n\n{}", message, ERROR_MSG)).unwrap(); - handle_stderr(&window_mutex, message.as_bytes()).unwrap(); + shareable_window.lock().unwrap().emit("client-error", format!("{}\n\n{}", message, ERROR_MSG)).unwrap(); + handle_stderr(&shareable_window, message.as_bytes()).unwrap(); }; *copy_of_runner_instance.lock().map_err(|e| format!("unable to lock runner instance: {:?}", e)).unwrap() = None; - window_mutex.lock().unwrap().emit("client-exited", ()).unwrap() + shareable_window.lock().unwrap().emit("client-exited", ()).unwrap() }); }); diff --git a/src-tauri/src/app/webview.rs b/src-tauri/src/app/webview.rs index 16889a0..7d3beb7 100644 --- a/src-tauri/src/app/webview.rs +++ b/src-tauri/src/app/webview.rs @@ -23,13 +23,13 @@ use anyhow::{anyhow, bail, Context, Result}; use tracing::{info, debug}; use tauri::{Manager, Url, WindowBuilder}; use tokio::time::sleep; -use crate::minecraft::progress::{ProgressReceiver, ProgressUpdate}; +use crate::minecraft::{launcher::LauncherData, progress::{ProgressReceiver, ProgressUpdate}}; -use super::gui::log; +use super::gui::ShareableWindow; const MAX_DOWNLOAD_ATTEMPTS: u8 = 2; -pub async fn open_download_page(url: &str, on_progress: &impl ProgressReceiver, window: &Arc>) -> Result { +pub async fn open_download_page(url: &str, launcher_data: &LauncherData) -> Result { let download_page: Url = format!("{}&liquidlauncher=1", url).parse() .context("Failed to parse download page URL")?; @@ -42,13 +42,12 @@ pub async fn open_download_page(url: &str, on_progress: &impl ProgressReceiver, bail!("Failed to open download page after {} attempts", MAX_DOWNLOAD_ATTEMPTS); } - log(&window, &format!("Opening download page... (Attempt {}/{})", count, MAX_DOWNLOAD_ATTEMPTS)); - on_progress.progress_update(ProgressUpdate::SetLabel(format!("Opening download page... (Attempt {}/{})", count, MAX_DOWNLOAD_ATTEMPTS))); + launcher_data.progress_update(ProgressUpdate::SetLabel(format!("Opening download page... (Attempt {}/{})", count, MAX_DOWNLOAD_ATTEMPTS))); - match show_webview(download_page.clone(), window).await { + match show_webview(download_page.clone(), &launcher_data.data).await { Ok(url) => break url, Err(e) => { - log(&window, &format!("Failed to open download page: {:?}", e)); + launcher_data.log(&format!("Failed to open download page: {:?}", e)); sleep(Duration::from_millis(500)).await; } } @@ -59,7 +58,7 @@ pub async fn open_download_page(url: &str, on_progress: &impl ProgressReceiver, async fn show_webview(url: Url, window: &Arc>) -> Result { // Find download_view window from the window manager - let mut download_view = { + let download_view = { let window = window.lock() .map_err(|_| anyhow!("Failed to lock window"))?; diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index da3d050..d27cec2 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -23,7 +23,7 @@ windows_subsystem = "windows" )] -use std::{fs, io}; +use std::io; use once_cell::sync::Lazy; use anyhow::Result; use directories::ProjectDirs; @@ -64,19 +64,6 @@ static HTTP_CLIENT: Lazy = Lazy::new(|| { client }); -/// Creates a directory tree if it doesn't exist -macro_rules! mkdir { - ($path:expr) => { - // Check if directory exists - if !$path.exists() { - // Create directory - if let Err(e) = fs::create_dir_all($path) { - error!("Failed to create directory {:?}: {}", $path, e); - } - } - }; -} - pub fn main() -> Result<()> { use tracing_subscriber::{fmt, EnvFilter}; diff --git a/src-tauri/src/minecraft/java/distribution.rs b/src-tauri/src/minecraft/java/distribution.rs index 4c879a4..7fbe4c4 100644 --- a/src-tauri/src/minecraft/java/distribution.rs +++ b/src-tauri/src/minecraft/java/distribution.rs @@ -18,7 +18,7 @@ impl Default for JavaDistribution { } impl JavaDistribution { - pub fn get_url(&self, jre_version: &str, os_name: &str, os_arch: &str) -> String { + pub fn get_url(&self, jre_version: &u32, os_name: &str, os_arch: &str) -> String { match self { JavaDistribution::Temurin => { format!( diff --git a/src-tauri/src/minecraft/java/jre_downloader.rs b/src-tauri/src/minecraft/java/jre_downloader.rs index f31d69d..bc51257 100644 --- a/src-tauri/src/minecraft/java/jre_downloader.rs +++ b/src-tauri/src/minecraft/java/jre_downloader.rs @@ -16,7 +16,7 @@ * You should have received a copy of the GNU General Public License * along with LiquidLauncher. If not, see . */ - + use std::io::Cursor; use std::path::{Path, PathBuf}; @@ -24,13 +24,18 @@ use anyhow::{bail, Result}; use path_absolutize::Absolutize; use tokio::fs; -use crate::utils::{download_file, tar_gz_extract, zip_extract, ARCHITECTURE, OperatingSystem, OS}; +use crate::utils::{download_file, tar_gz_extract, zip_extract, OperatingSystem, ARCHITECTURE, OS}; use super::JavaDistribution; /// Find java binary in JRE folder -pub async fn find_java_binary(runtimes_folder: &Path, jre_distribution: &JavaDistribution, jre_version: &str) -> Result { - let runtime_path = runtimes_folder.join(format!("{}_{}", jre_distribution.get_name(), jre_version)); +pub async fn find_java_binary( + runtimes_folder: &Path, + jre_distribution: &JavaDistribution, + jre_version: &u32, +) -> Result { + let runtime_path = + runtimes_folder.join(format!("{}_{}", jre_distribution.get_name(), jre_version)); // Find JRE in runtime folder let mut files = fs::read_dir(&runtime_path).await?; @@ -40,8 +45,12 @@ pub async fn find_java_binary(runtimes_folder: &Path, jre_distribution: &JavaDis let java_binary = match OS { OperatingSystem::WINDOWS => folder_path.join("bin").join("javaw.exe"), - OperatingSystem::OSX => folder_path.join("Contents").join("Home").join("bin").join("java"), - _ => folder_path.join("bin").join("java") + OperatingSystem::OSX => folder_path + .join("Contents") + .join("Home") + .join("bin") + .join("java"), + _ => folder_path.join("bin").join("java"), }; if java_binary.exists() { @@ -68,8 +77,17 @@ pub async fn find_java_binary(runtimes_folder: &Path, jre_distribution: &JavaDis } /// Download specific JRE to runtimes -pub async fn jre_download(runtimes_folder: &Path, jre_distribution: &JavaDistribution, jre_version: &str, on_progress: F) -> Result where F : Fn(u64, u64) { - let runtime_path = runtimes_folder.join(format!("{}_{}", jre_distribution.get_name(), jre_version)); +pub async fn jre_download( + runtimes_folder: &Path, + jre_distribution: &JavaDistribution, + jre_version: &u32, + on_progress: F, +) -> Result +where + F: Fn(u64, u64), +{ + let runtime_path = + runtimes_folder.join(format!("{}_{}", jre_distribution.get_name(), jre_version)); if runtime_path.exists() { // Clear out folder @@ -91,11 +109,12 @@ pub async fn jre_download(runtimes_folder: &Path, jre_distribution: &JavaDist match OS { OperatingSystem::WINDOWS => zip_extract(cursor, runtime_path.as_path()).await?, - OperatingSystem::LINUX | OperatingSystem::OSX => tar_gz_extract(cursor, runtime_path.as_path()).await?, - _ => bail!("Unsupported OS") + OperatingSystem::LINUX | OperatingSystem::OSX => { + tar_gz_extract(cursor, runtime_path.as_path()).await? + } + _ => bail!("Unsupported OS"), } // Find JRE afterwards find_java_binary(runtimes_folder, jre_distribution, jre_version).await } - diff --git a/src-tauri/src/minecraft/java/runtime.rs b/src-tauri/src/minecraft/java/runtime.rs index 6b53fef..c5a0626 100644 --- a/src-tauri/src/minecraft/java/runtime.rs +++ b/src-tauri/src/minecraft/java/runtime.rs @@ -36,6 +36,8 @@ impl JavaRuntime { if !self.0.exists() { bail!("Java runtime not found at: {}", self.0.display()); } + + debug!("Executing Java runtime: {}", self.0.display()); let mut command = Command::new(&self.0); command.current_dir(game_dir); diff --git a/src-tauri/src/minecraft/launcher.rs b/src-tauri/src/minecraft/launcher.rs deleted file mode 100644 index 5b0ad54..0000000 --- a/src-tauri/src/minecraft/launcher.rs +++ /dev/null @@ -1,382 +0,0 @@ -/* - * This file is part of LiquidLauncher (https://github.com/CCBlueX/LiquidLauncher) - * - * Copyright (c) 2015 - 2024 CCBlueX - * - * LiquidLauncher is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LiquidLauncher is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LiquidLauncher. If not, see . - */ - -use std::path::{Path, PathBuf}; -use std::collections::HashSet; -use std::fmt::Write; - -use std::process::exit; -use std::sync::{Arc, Mutex}; -use std::sync::atomic::{AtomicU64, Ordering}; - -use anyhow::{anyhow, bail, Result}; -use futures::stream::{self, StreamExt}; - -use tracing::*; -use path_absolutize::*; -use tokio::{fs, fs::OpenOptions}; - -use crate::app::gui::log; -use crate::{utils::{OS, OS_VERSION}, LAUNCHER_VERSION}; -use crate::app::api::LaunchManifest; -use crate::error::LauncherError; -use crate::minecraft::progress::{get_max, get_progress, ProgressReceiver, ProgressUpdate, ProgressUpdateSteps}; -use crate::minecraft::rule_interpreter; -use crate::minecraft::java::{find_java_binary, JavaRuntime, jre_downloader}; -use crate::minecraft::version::LibraryDownloadInfo; -use crate::utils::{download_file, sha1sum, zip_extract}; - -use super::version::VersionProfile; - -pub struct LauncherData { - pub(crate) on_stdout: fn(&D, &[u8]) -> Result<()>, - pub(crate) on_stderr: fn(&D, &[u8]) -> Result<()>, - pub(crate) on_progress: fn(&D, ProgressUpdate) -> Result<()>, - pub(crate) data: Box, - pub(crate) terminator: tokio::sync::oneshot::Receiver<()> -} - -impl ProgressReceiver for LauncherData { - fn progress_update(&self, progress_update: ProgressUpdate) { - let _ = (self.on_progress)(&self.data, progress_update); - } -} - -pub async fn launch(data: &Path, manifest: LaunchManifest, version_profile: VersionProfile, launching_parameter: LaunchingParameter, launcher_data: LauncherData, window: Arc>) -> Result<()> { - let launcher_data_arc = Arc::new(launcher_data); - - let features: HashSet = HashSet::new(); - log(&window, &format!("Determined OS to be {} {}", OS, OS_VERSION.clone())); - - // JRE download - let runtimes_folder = data.join("runtimes"); - if !runtimes_folder.exists() { - fs::create_dir(&runtimes_folder).await?; - } - - let java_bin = match &launching_parameter.custom_java_path { - Some(path) => PathBuf::from(path), - None => { - log(&window, "Checking for JRE..."); - launcher_data_arc.progress_update(ProgressUpdate::set_label("Checking for JRE...")); - - match find_java_binary(&runtimes_folder, &manifest.build.jre_distribution, &*manifest.build.jre_version.to_string()).await { - Ok(jre) => jre, - Err(e) => { - log(&window, &format!("Failed to find JRE: {}", e)); - - log(&window, "Downloading JRE..."); - launcher_data_arc.progress_update(ProgressUpdate::set_label("Download JRE...")); - jre_downloader::jre_download(&runtimes_folder, &manifest.build.jre_distribution, &*manifest.build.jre_version.to_string(), |a, b| { - launcher_data_arc.progress_update(ProgressUpdate::set_for_step(ProgressUpdateSteps::DownloadJRE, get_progress(0, a, b), get_max(1))); - }).await? - } - } - } - }; - - log(&window, &format!("Java binary: {:?}", java_bin)); - if !java_bin.exists() { - bail!("Java binary not found"); - } - - // Launch class path for JRE - let mut class_path = String::new(); - - // Client - let versions_folder = data.join("versions"); - - // Check if json has client download (or doesn't require one) - if let Some(client_download) = version_profile.downloads.as_ref().and_then(|x| x.client.as_ref()) { - let client_folder = versions_folder.join(&version_profile.id); - fs::create_dir_all(&client_folder).await?; - - let client_jar = client_folder.join(format!("{}.jar", &version_profile.id)); - - // Add client jar to class path - write!(class_path, "{}{}", &client_jar.absolutize().unwrap().to_str().unwrap(), OS.get_path_separator()?)?; - - // Download client jar - let requires_download = if !client_jar.exists() { - true - } else { - let hash = sha1sum(&client_jar)?; - hash != client_download.sha1 - }; - - if requires_download { - launcher_data_arc.progress_update(ProgressUpdate::set_label("Downloading client...")); - - let retrieved_bytes = download_file(&client_download.url, |a, b| { - launcher_data_arc.progress_update(ProgressUpdate::set_for_step(ProgressUpdateSteps::DownloadClientJar, get_progress(0, a, b), get_max(1))); - }).await?; - - fs::write(&client_jar, retrieved_bytes).await?; - - // After downloading, check sha1 - let hash = sha1sum(&client_jar)?; - if hash != client_download.sha1 { - bail!("Client JAR download failed. SHA1 mismatch."); - } - } - } else { - return Err(LauncherError::InvalidVersionProfile("No client JAR downloads were specified.".to_string()).into()); - } - - // Libraries - let libraries_folder = data.join("libraries"); - let natives_folder = data.join("natives"); - let natives_path = natives_folder.as_path(); - if natives_folder.exists() { - fs::remove_dir_all(&natives_folder).await?; - } - fs::create_dir_all(&natives_folder).await?; - - let libraries_to_download = version_profile.libraries.iter().map(|x| x.to_owned()).collect::>(); - // let libraries_downloaded = Arc::new(AtomicU64::new(0)); - let libraries_max = libraries_to_download.len() as u64; - - launcher_data_arc.progress_update(ProgressUpdate::set_label("Checking libraries...")); - launcher_data_arc.progress_update(ProgressUpdate::set_for_step(ProgressUpdateSteps::DownloadLibraries, 0, libraries_max)); - - let class_paths: Vec>> = stream::iter( - libraries_to_download.into_iter().filter_map(|library| { - // let download_count = libraries_downloaded.clone(); - let data_clone = launcher_data_arc.clone(); - let folder_clone = libraries_folder.to_path_buf(); - - if !rule_interpreter::check_condition(&library.rules, &features).unwrap_or(false) { - return None; - } - - Some(async move { - if let Some(natives) = &library.natives { - if let Some(required_natives) = natives.get(OS.get_simple_name()?) { - if let Some(classifiers) = library.downloads.as_ref().and_then(|x| x.classifiers.as_ref()) { - if let Some(artifact) = classifiers.get(required_natives).map(LibraryDownloadInfo::from) { - let path = artifact.download(library.name, folder_clone.as_path(), data_clone).await?; - - info!("Natives zip extract: {:?}", path); - let file = OpenOptions::new().read(true).open(path).await?; - zip_extract(file, natives_path).await?; - } - } else { - return Err(LauncherError::InvalidVersionProfile("missing classifiers, but natives required.".to_string()).into()); - } - } - - return Ok(None); - } - - // Download regular artifact - let artifact = library.get_library_download()?; - let path = artifact.download(library.name, folder_clone.as_path(), data_clone).await?; - - // Natives are not included in the classpath - return if library.natives.is_none() { - return Ok(path.absolutize()?.to_str().map(|x| x.to_string())) - } else { - Ok(None) - }; - }) - }) - ).buffer_unordered(launching_parameter.concurrent_downloads as usize).collect().await; - for x in class_paths { - if let Some(library_path) = x? { - write!(class_path, "{}{}", &library_path, OS.get_path_separator()?)?; - } - } - - launcher_data_arc.progress_update(ProgressUpdate::set_for_step(ProgressUpdateSteps::DownloadLibraries, libraries_max, libraries_max)); - - // Assets - let assets_folder = data.join("assets"); - let indexes_folder: PathBuf = assets_folder.join("indexes"); - let objects_folder: PathBuf = assets_folder.join("objects"); - - fs::create_dir_all(&indexes_folder).await?; - fs::create_dir_all(&objects_folder).await?; - - let asset_index_location = version_profile.asset_index_location.as_ref().ok_or_else(|| LauncherError::InvalidVersionProfile("Asset index unspecified".to_string()))?; - let asset_index = asset_index_location.load_asset_index(&indexes_folder).await?; - let asset_objects_to_download = asset_index.objects.values().map(|x| x.to_owned()).collect::>(); - let assets_downloaded = Arc::new(AtomicU64::new(0)); - let asset_max = asset_objects_to_download.len() as u64; - - launcher_data_arc.progress_update(ProgressUpdate::set_label("Checking assets...")); - launcher_data_arc.progress_update(ProgressUpdate::set_for_step(ProgressUpdateSteps::DownloadAssets, 0, asset_max)); - - let _: Vec> = stream::iter( - asset_objects_to_download.into_iter().map(|asset_object| { - let download_count = assets_downloaded.clone(); - let data_clone = launcher_data_arc.clone(); - let folder_clone = objects_folder.clone(); - - async move { - let hash = asset_object.hash.clone(); - match asset_object.download_destructing(folder_clone, data_clone.clone()).await { - Ok(downloaded) => { - let curr = download_count.fetch_add(1, Ordering::Relaxed); - - if downloaded { - // the progress bar is only being updated when a asset has been downloaded to improve speeds - data_clone.progress_update(ProgressUpdate::set_for_step(ProgressUpdateSteps::DownloadAssets, curr, asset_max)); - } - }, - Err(err) => error!("Unable to download asset {}: {:?}", hash, err) - } - - Ok(()) - } - }) - ).buffer_unordered(launching_parameter.concurrent_downloads as usize).collect().await; - - launcher_data_arc.progress_update(ProgressUpdate::set_for_step(ProgressUpdateSteps::DownloadAssets, asset_max, asset_max)); - - // Game - let game_dir = data.join("gameDir").join(manifest.build.branch); - fs::create_dir_all(&game_dir).await?; - - let java_runtime = JavaRuntime::new(java_bin); - - - let mut command_arguments = Vec::new(); - - // JVM Args - version_profile.arguments.add_jvm_args_to_vec(&mut command_arguments, &launching_parameter, &features)?; - - // Main class - command_arguments.push(version_profile.main_class.as_ref().ok_or_else(|| LauncherError::InvalidVersionProfile("Main class unspecified".to_string()))?.to_owned()); - - // Game args - version_profile.arguments.add_game_args_to_vec(&mut command_arguments, &features)?; - - let mut mapped: Vec = Vec::with_capacity(command_arguments.len()); - - for x in command_arguments.iter() { - mapped.push( - process_templates(x, |output, param| { - match param { - "auth_player_name" => output.push_str(&launching_parameter.auth_player_name), - "version_name" => output.push_str(&version_profile.id), - "game_directory" => output.push_str(game_dir.absolutize().unwrap().to_str().unwrap()), - "assets_root" => output.push_str(assets_folder.absolutize().unwrap().to_str().unwrap()), - "assets_index_name" => output.push_str(&asset_index_location.id), - "auth_uuid" => output.push_str(&launching_parameter.auth_uuid), - "auth_access_token" => output.push_str(&launching_parameter.auth_access_token), - "user_type" => output.push_str(&launching_parameter.user_type), - "version_type" => output.push_str(&version_profile.version_type), - "natives_directory" => output.push_str(natives_folder.absolutize().unwrap().to_str().unwrap()), - "launcher_name" => output.push_str("LiquidLauncher"), - "launcher_version" => output.push_str(LAUNCHER_VERSION), - "classpath" => output.push_str(&class_path), - "user_properties" => output.push_str("{}"), - "clientid" => output.push_str(&launching_parameter.clientid), - "auth_xuid" => output.push_str(&launching_parameter.auth_xuid), - _ => return Err(LauncherError::UnknownTemplateParameter(param.to_owned()).into()) - }; - - Ok(()) - })? - ); - } - - launcher_data_arc.progress_update(ProgressUpdate::set_label("Launching...")); - launcher_data_arc.progress_update(ProgressUpdate::set_to_max()); - log(&window, "Launching..."); - - let mut running_task = java_runtime.execute(mapped, &game_dir).await?; - - launcher_data_arc.progress_update(ProgressUpdate::set_label("Running...")); - - if !launching_parameter.keep_launcher_open { - // Hide launcher window - if let Err(err) = window.lock() - .map_err(|_| anyhow!("Unable to lock window due to poisoned mutex"))? - .hide() { - error!("Failed to hide window: {}", err); - } - } - - let launcher_data = Arc::try_unwrap(launcher_data_arc) - .map_err(|_| anyhow!("Failed to unwrap launcher data"))?; - let terminator = launcher_data.terminator; - let data = launcher_data.data; - - java_runtime.handle_io(&mut running_task, launcher_data.on_stdout, launcher_data.on_stderr, terminator, &data) - .await?; - - if !launching_parameter.keep_launcher_open { - // Hide launcher window - exit(0); - } - - Ok(()) -} - -pub struct LaunchingParameter { - pub memory: i64, - pub custom_data_path: Option, - pub custom_java_path: Option, - pub auth_player_name: String, - pub auth_uuid: String, - pub auth_access_token: String, - pub auth_xuid: String, - pub clientid: String, - pub user_type: String, - pub keep_launcher_open: bool, - pub concurrent_downloads: i32, -} - -fn process_templates Result<()>>(input: &String, retriever: F) -> Result { - let mut output = String::with_capacity(input.len() * 3 / 2); - - let mut chars = input.chars().peekable(); - - while let Some(c) = chars.next() { - if c == '$' && chars.peek().map_or(false, |&x| x == '{') { - // Consuuuuume the '{' - chars.next(); - - let mut template_arg = String::with_capacity(input.len() - 3); - - let mut c; - - loop { - c = chars.next().ok_or_else(|| LauncherError::InvalidVersionProfile("invalid template, missing '}'".to_string()))?; - - if c == '}' { - break; - } - if !matches!(c, 'a'..='z' | 'A'..='Z' | '_' | '0'..='9') { - return Err(LauncherError::InvalidVersionProfile(format!("invalid character in template: '{}'", c)).into()); - } - - template_arg.push(c); - } - - retriever(&mut output, template_arg.as_str())?; - continue; - } - - output.push(c); - } - - Ok(output) -} \ No newline at end of file diff --git a/src-tauri/src/minecraft/launcher/assets.rs b/src-tauri/src/minecraft/launcher/assets.rs new file mode 100644 index 0000000..118ffbb --- /dev/null +++ b/src-tauri/src/minecraft/launcher/assets.rs @@ -0,0 +1,88 @@ +use std::{path::{Path, PathBuf}, sync::{atomic::{AtomicU64, Ordering}, Arc}}; + +use anyhow::Result; +use futures::{stream, StreamExt}; +use tracing::error; + +use crate::{ + error::LauncherError, + join_and_mkdir, + minecraft::{progress::{ProgressReceiver, ProgressUpdate, ProgressUpdateSteps}, version::{AssetIndexLocation, VersionProfile}}, +}; + +use super::{LauncherData, LaunchingParameter}; + +pub async fn setup_assets<'a, D: Send + Sync>( + assets_folder: &'a Path, + version_profile: &'a VersionProfile, + launching_parameter: &'a LaunchingParameter, + launcher_data: &'a LauncherData, +) -> Result<&'a AssetIndexLocation> { + let indexes_folder: PathBuf = join_and_mkdir!(assets_folder, "indexes"); + let objects_folder: PathBuf = join_and_mkdir!(assets_folder, "objects"); + + let asset_index_location = version_profile + .asset_index_location + .as_ref() + .ok_or_else(|| { + LauncherError::InvalidVersionProfile("Asset index unspecified".to_string()) + })?; + let asset_index = asset_index_location + .load_asset_index(&indexes_folder) + .await?; + let asset_objects_to_download = asset_index + .objects + .values() + .map(|x| x.to_owned()) + .collect::>(); + let assets_downloaded = Arc::new(AtomicU64::new(0)); + let asset_max = asset_objects_to_download.len() as u64; + + launcher_data.progress_update(ProgressUpdate::set_label("Checking assets...")); + launcher_data.progress_update(ProgressUpdate::set_for_step( + ProgressUpdateSteps::DownloadAssets, + 0, + asset_max, + )); + + let _: Vec> = + stream::iter(asset_objects_to_download.into_iter().map(|asset_object| { + let download_count = assets_downloaded.clone(); + let folder_clone = objects_folder.clone(); + + async move { + let hash = asset_object.hash.clone(); + match asset_object + .download_destructing(folder_clone, launcher_data) + .await + { + Ok(downloaded) => { + let curr = download_count.fetch_add(1, Ordering::Relaxed); + + if downloaded { + // the progress bar is only being updated when a asset has been downloaded to improve speeds + launcher_data.progress_update(ProgressUpdate::set_for_step( + ProgressUpdateSteps::DownloadAssets, + curr, + asset_max, + )); + } + } + Err(err) => error!("Unable to download asset {}: {:?}", hash, err), + } + + Ok(()) + } + })) + .buffer_unordered(launching_parameter.concurrent_downloads as usize) + .collect() + .await; + + launcher_data.progress_update(ProgressUpdate::set_for_step( + ProgressUpdateSteps::DownloadAssets, + asset_max, + asset_max, + )); + + Ok(asset_index_location) +} diff --git a/src-tauri/src/minecraft/launcher/client_jar.rs b/src-tauri/src/minecraft/launcher/client_jar.rs new file mode 100644 index 0000000..2a575c9 --- /dev/null +++ b/src-tauri/src/minecraft/launcher/client_jar.rs @@ -0,0 +1,89 @@ +use std::path::Path; +use std::fmt::Write; +use anyhow::{bail, Context, Result}; +use path_absolutize::Absolutize; +use tokio::fs; + +use crate::{ + error::LauncherError, minecraft::{progress::{get_max, get_progress, ProgressReceiver, ProgressUpdate, ProgressUpdateSteps}, version::VersionProfile}, utils::{download_file, sha1sum, OS} +}; + +use super::LauncherData; + +pub async fn setup_client_jar( + client_folder: &Path, + natives_folder: &Path, + version_profile: &VersionProfile, + launcher_data: &LauncherData, + class_path: &mut String, +) -> Result<()> { + if let Some(client_download) = version_profile + .downloads + .as_ref() + .and_then(|x| x.client.as_ref()) + { + let client_jar = client_folder.join(format!("{}.jar", &version_profile.id)); + + // Add client jar to class path + write!( + class_path, + "{}{}", + &client_jar.absolutize().unwrap().to_str().unwrap(), + OS.get_path_separator()? + )?; + + // Download client jar + let requires_download = if !client_jar.exists() { + true + } else { + let hash = sha1sum(&client_jar)?; + launcher_data.log(&*format!( + "Client JAR local hash: {}, remote: {}", + hash, client_download.sha1 + )); + hash != client_download.sha1 + }; + + if requires_download { + launcher_data.log("Downloading client..."); + launcher_data.progress_update(ProgressUpdate::set_label("Downloading client...")); + + let retrieved_bytes = download_file(&client_download.url, |a, b| { + launcher_data.progress_update(ProgressUpdate::set_for_step( + ProgressUpdateSteps::DownloadClientJar, + get_progress(0, a, b), + get_max(1), + )); + }) + .await?; + + fs::write(&client_jar, retrieved_bytes) + .await + .context("Failed to write client JAR")?; + + // After downloading, check sha1 + let hash = sha1sum(&client_jar)?; + launcher_data.log(&*format!( + "Client JAR local hash: {}, remote: {}", + hash, client_download.sha1 + )); + if hash != client_download.sha1 { + bail!("Client JAR download failed. SHA1 mismatch."); + } + } + + // Natives folder + if !natives_folder.exists() { + fs::create_dir_all(&natives_folder) + .await + .context("Failed to create natives folder")?; + } + } else { + return Err(LauncherError::InvalidVersionProfile( + "No client JAR downloads were specified.".to_string(), + ) + .into()); + } + + Ok(()) +} diff --git a/src-tauri/src/minecraft/launcher/jre.rs b/src-tauri/src/minecraft/launcher/jre.rs new file mode 100644 index 0000000..31ffdad --- /dev/null +++ b/src-tauri/src/minecraft/launcher/jre.rs @@ -0,0 +1,53 @@ +use std::path::{Path, PathBuf}; + +use anyhow::Result; + +use crate::{ + app::api::LaunchManifest, + minecraft::{ + java::{find_java_binary, jre_downloader}, + progress::{get_max, get_progress, ProgressReceiver, ProgressUpdate, ProgressUpdateSteps}, + }, +}; + +use super::{LauncherData, LaunchingParameter}; + +pub async fn load_jre( + runtimes_folder: &Path, + manifest: &LaunchManifest, + launching_parameter: &LaunchingParameter, + launcher_data: &LauncherData, +) -> Result { + if let Some(jre) = &launching_parameter.custom_java_path { + return Ok(PathBuf::from(jre)); + } + + launcher_data.progress_update(ProgressUpdate::set_label("Checking for JRE...")); + + if let Ok(jre) = find_java_binary( + runtimes_folder, + &manifest.build.jre_distribution, + &manifest.build.jre_version, + ) + .await + { + return Ok(jre); + } + + launcher_data.log("Downloading JRE..."); + launcher_data.progress_update(ProgressUpdate::set_label("Download JRE...")); + + jre_downloader::jre_download( + &runtimes_folder, + &manifest.build.jre_distribution, + &manifest.build.jre_version, + |a, b| { + launcher_data.progress_update(ProgressUpdate::set_for_step( + ProgressUpdateSteps::DownloadJRE, + get_progress(0, a, b), + get_max(1), + )); + }, + ) + .await +} diff --git a/src-tauri/src/minecraft/launcher/libraries.rs b/src-tauri/src/minecraft/launcher/libraries.rs new file mode 100644 index 0000000..f99a545 --- /dev/null +++ b/src-tauri/src/minecraft/launcher/libraries.rs @@ -0,0 +1,120 @@ +use std::{collections::HashSet, path::Path}; +use std::fmt::Write; +use anyhow::{Context, Result}; +use futures::{stream, StreamExt}; +use path_absolutize::Absolutize; +use tokio::fs::OpenOptions; + +use crate::{ + error::LauncherError, + minecraft::{progress::{ProgressReceiver, ProgressUpdate, ProgressUpdateSteps}, rule_interpreter, version::{LibraryDownloadInfo, VersionProfile}}, utils::{zip_extract, OS}, +}; + +use super::{LauncherData, LaunchingParameter}; + +pub async fn setup_libraries( + libraries_folder: &Path, + natives_folder: &Path, + version_profile: &VersionProfile, + launching_parameter: &LaunchingParameter, + launcher_data: &LauncherData, + features: &HashSet, + class_path: &mut String, +) -> Result<()> { + let libraries_to_download = version_profile + .libraries + .iter() + .map(|x| x.to_owned()) + .collect::>(); + let libraries_max = libraries_to_download.len() as u64; + + launcher_data.progress_update(ProgressUpdate::set_label("Checking libraries...")); + launcher_data.progress_update(ProgressUpdate::set_for_step( + ProgressUpdateSteps::DownloadLibraries, + 0, + libraries_max, + )); + + let class_paths: Vec>> = + stream::iter(libraries_to_download.into_iter().filter_map(|library| { + // let download_count = libraries_downloaded.clone(); + let folder_clone = libraries_folder.to_path_buf(); + let native_clone = natives_folder.to_path_buf(); + + if !rule_interpreter::check_condition(&library.rules, &features).unwrap_or(false) { + return None; + } + + Some(async move { + if let Some(natives) = &library.natives { + if let Some(required_natives) = natives.get(OS.get_simple_name()?) { + if let Some(classifiers) = library + .downloads + .as_ref() + .and_then(|x| x.classifiers.as_ref()) + { + if let Some(artifact) = classifiers + .get(required_natives) + .map(LibraryDownloadInfo::from) + { + let path = artifact + .download(&library.name, folder_clone.clone(), launcher_data) + .await + .with_context(|| { + format!("Failed to download native library: {}", &library.name) + })?; + + launcher_data.progress_update(ProgressUpdate::set_label("Extracting natives...")); + let file = OpenOptions::new() + .read(true) + .open(path) + .await + .context("Failed to open native library")?; + zip_extract(file, &native_clone).await + .context("Failed to extract native library")?; + } + } else { + return Err(LauncherError::InvalidVersionProfile( + "missing classifiers, but natives required.".to_string(), + ) + .into()); + } + } + + return Ok(None); + } + + // Download regular artifact + let artifact = library.get_library_download()?; + let path = artifact + .download(&library.name, folder_clone.clone(), launcher_data) + .await + .with_context(|| format!("Failed to download library: {}", &library.name))?; + + // Natives are not included in the classpath + return if library.natives.is_none() { + return Ok(path.absolutize()?.to_str().map(|x| x.to_string())); + } else { + Ok(None) + }; + }) + })) + .buffer_unordered(launching_parameter.concurrent_downloads as usize) + .collect() + .await; + + // Join class paths + for x in class_paths { + if let Some(library_path) = x? { + write!(class_path, "{}{}", &library_path, OS.get_path_separator()?)?; + } + } + + launcher_data.progress_update(ProgressUpdate::set_for_step( + ProgressUpdateSteps::DownloadLibraries, + libraries_max, + libraries_max, + )); + + Ok(()) +} diff --git a/src-tauri/src/minecraft/launcher/mod.rs b/src-tauri/src/minecraft/launcher/mod.rs new file mode 100644 index 0000000..0588498 --- /dev/null +++ b/src-tauri/src/minecraft/launcher/mod.rs @@ -0,0 +1,309 @@ +/* + * This file is part of LiquidLauncher (https://github.com/CCBlueX/LiquidLauncher) + * + * Copyright (c) 2015 - 2024 CCBlueX + * + * LiquidLauncher is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LiquidLauncher is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LiquidLauncher. If not, see . + */ + +use std::collections::HashSet; +use std::path::Path; + +use std::process::exit; + +use anyhow::{bail, Context, Result}; + +use path_absolutize::Absolutize; +use tracing::*; + +use crate::app::api::LaunchManifest; +use crate::error::LauncherError; +use crate::minecraft::java::JavaRuntime; +use crate::minecraft::progress::{ProgressReceiver, ProgressUpdate}; +use crate::{join_and_mkdir, join_and_mkdir_vec}; +use crate::{ + utils::{OS, OS_VERSION}, + LAUNCHER_VERSION, +}; + +use self::assets::setup_assets; +use self::client_jar::setup_client_jar; +use self::jre::load_jre; +use self::libraries::setup_libraries; + +use super::version::VersionProfile; + +mod assets; +mod client_jar; +mod jre; +mod libraries; + +pub struct LauncherData { + pub(crate) on_stdout: fn(&D, &[u8]) -> Result<()>, + pub(crate) on_stderr: fn(&D, &[u8]) -> Result<()>, + pub(crate) on_progress: fn(&D, ProgressUpdate) -> Result<()>, + pub(crate) on_log: fn(&D, &str) -> Result<()>, + pub(crate) hide_window: fn(&D), + pub(crate) data: Box, + pub(crate) terminator: tokio::sync::oneshot::Receiver<()>, +} + +impl LauncherData { + fn hide_window(&self) { + (self.hide_window)(&self.data); + } +} + +impl ProgressReceiver for LauncherData { + fn progress_update(&self, progress_update: ProgressUpdate) { + let _ = (self.on_progress)(&self.data, progress_update); + } + fn log(&self, msg: &str) { + let _ = (self.on_log)(&self.data, msg); + } +} + +/// +/// Launches the game +/// +pub async fn launch( + data: &Path, + manifest: LaunchManifest, + version_profile: VersionProfile, + launching_parameter: LaunchingParameter, + launcher_data: LauncherData, +) -> Result<()> { + let features: HashSet = HashSet::new(); + let mut class_path = String::new(); + + launcher_data.progress_update(ProgressUpdate::set_label("Setting up...")); + + launcher_data.log(&format!( + "Determined OS to be {} {}", + OS, + OS_VERSION.clone() + )); + + let runtimes_folder = join_and_mkdir!(data, "runtimes"); + let client_folder = join_and_mkdir_vec!(data, vec!["versions", &version_profile.id]); + let natives_folder = join_and_mkdir!(client_folder, "natives"); + let libraries_folder = join_and_mkdir!(data, "libraries"); + let assets_folder = join_and_mkdir!(data, "assets"); + let game_dir = join_and_mkdir_vec!(data, vec!["gameDir", &*manifest.build.branch]); + + let java_bin = load_jre( + &runtimes_folder, + &manifest, + &launching_parameter, + &launcher_data, + ) + .await + .context("Failed to load JRE")?; + + launcher_data.log(&format!("Java Path: {:?}", java_bin)); + if !java_bin.exists() { + bail!("Java binary not found"); + } + + // Check if json has client download (or doesn't require one) + setup_client_jar( + &client_folder, + &natives_folder, + &version_profile, + &launcher_data, + &mut class_path, + ) + .await + .context("Failed to setup client JAR")?; + + // Libraries + setup_libraries( + &libraries_folder, + &natives_folder, + &version_profile, + &launching_parameter, + &launcher_data, + &features, + &mut class_path, + ) + .await + .context("Failed to setup libraries")?; + + // Assets + let asset_index_location = setup_assets( + &assets_folder, + &version_profile, + &launching_parameter, + &launcher_data, + ) + .await + .context("Failed to setup assets")?; + + // Game + + let java_runtime = JavaRuntime::new(java_bin); + + let mut command_arguments = Vec::new(); + + // JVM Args + version_profile.arguments.add_jvm_args_to_vec( + &mut command_arguments, + &launching_parameter, + &features, + )?; + + // Main class + command_arguments.push( + version_profile + .main_class + .as_ref() + .ok_or_else(|| { + LauncherError::InvalidVersionProfile("Main class unspecified".to_string()) + })? + .to_owned(), + ); + + // Game args + version_profile + .arguments + .add_game_args_to_vec(&mut command_arguments, &features)?; + + let mut mapped: Vec = Vec::with_capacity(command_arguments.len()); + + for x in command_arguments.iter() { + mapped.push(process_templates(x, |output, param| { + match param { + "auth_player_name" => output.push_str(&launching_parameter.auth_player_name), + "version_name" => output.push_str(&version_profile.id), + "game_directory" => { + output.push_str(game_dir.absolutize().unwrap().to_str().unwrap()) + } + "assets_root" => { + output.push_str(assets_folder.absolutize().unwrap().to_str().unwrap()) + } + "assets_index_name" => output.push_str(&asset_index_location.id), + "auth_uuid" => output.push_str(&launching_parameter.auth_uuid), + "auth_access_token" => output.push_str(&launching_parameter.auth_access_token), + "user_type" => output.push_str(&launching_parameter.user_type), + "version_type" => output.push_str(&version_profile.version_type), + "natives_directory" => { + output.push_str(natives_folder.absolutize().unwrap().to_str().unwrap()) + } + "launcher_name" => output.push_str("LiquidLauncher"), + "launcher_version" => output.push_str(LAUNCHER_VERSION), + "classpath" => output.push_str(&class_path), + "user_properties" => output.push_str("{}"), + "clientid" => output.push_str(&launching_parameter.clientid), + "auth_xuid" => output.push_str(&launching_parameter.auth_xuid), + _ => return Err(LauncherError::UnknownTemplateParameter(param.to_owned()).into()), + }; + + Ok(()) + })?); + } + + launcher_data.progress_update(ProgressUpdate::set_label("Launching...")); + launcher_data.progress_update(ProgressUpdate::set_to_max()); + + let mut running_task = java_runtime.execute(mapped, &game_dir).await?; + + launcher_data.progress_update(ProgressUpdate::set_label("Running...")); + + if !launching_parameter.keep_launcher_open { + // Hide launcher window + launcher_data.hide_window(); + } + + let terminator = launcher_data.terminator; + let data = launcher_data.data; + + java_runtime + .handle_io( + &mut running_task, + launcher_data.on_stdout, + launcher_data.on_stderr, + terminator, + &data, + ) + .await?; + + if !launching_parameter.keep_launcher_open { + // Hide launcher window + exit(0); + } + + Ok(()) +} + +pub struct LaunchingParameter { + pub memory: i64, + pub custom_data_path: Option, + pub custom_java_path: Option, + pub auth_player_name: String, + pub auth_uuid: String, + pub auth_access_token: String, + pub auth_xuid: String, + pub clientid: String, + pub user_type: String, + pub keep_launcher_open: bool, + pub concurrent_downloads: i32, +} + +fn process_templates Result<()>>( + input: &String, + retriever: F, +) -> Result { + let mut output = String::with_capacity(input.len() * 3 / 2); + + let mut chars = input.chars().peekable(); + + while let Some(c) = chars.next() { + if c == '$' && chars.peek().map_or(false, |&x| x == '{') { + // Consuuuuume the '{' + chars.next(); + + let mut template_arg = String::with_capacity(input.len() - 3); + + let mut c; + + loop { + c = chars.next().ok_or_else(|| { + LauncherError::InvalidVersionProfile( + "invalid template, missing '}'".to_string(), + ) + })?; + + if c == '}' { + break; + } + if !matches!(c, 'a'..='z' | 'A'..='Z' | '_' | '0'..='9') { + return Err(LauncherError::InvalidVersionProfile(format!( + "invalid character in template: '{}'", + c + )) + .into()); + } + + template_arg.push(c); + } + + retriever(&mut output, template_arg.as_str())?; + continue; + } + + output.push(c); + } + + Ok(output) +} diff --git a/src-tauri/src/minecraft/prelauncher.rs b/src-tauri/src/minecraft/prelauncher.rs index 4e8e4ec..5049278 100644 --- a/src-tauri/src/minecraft/prelauncher.rs +++ b/src-tauri/src/minecraft/prelauncher.rs @@ -16,81 +16,122 @@ * You should have received a copy of the GNU General Public License * along with LiquidLauncher. If not, see . */ - + use std::path::Path; -use std::sync::{Mutex, Arc}; use anyhow::{bail, Context, Result}; use async_zip::read::mem::ZipFileReader; -use tracing::*; use tokio::fs; use tokio::io::AsyncReadExt; +use tracing::*; -use crate::app::api::{LaunchManifest, LoaderSubsystem, ModSource, LoaderMod}; -use crate::app::gui::log; -use crate::error::LauncherError; +use crate::app::api::{LaunchManifest, LoaderMod, LoaderSubsystem, ModSource}; +use crate::app::gui::ShareableWindow; use crate::app::webview::open_download_page; -use crate::LAUNCHER_DIRECTORY; +use crate::error::LauncherError; use crate::minecraft::launcher; use crate::minecraft::launcher::{LauncherData, LaunchingParameter}; -use crate::minecraft::progress::{get_max, get_progress, ProgressReceiver, ProgressUpdate, ProgressUpdateSteps}; +use crate::minecraft::progress::{ + get_max, get_progress, ProgressReceiver, ProgressUpdate, ProgressUpdateSteps, +}; use crate::minecraft::version::{VersionManifest, VersionProfile}; use crate::utils::{download_file, get_maven_artifact_path}; +use crate::LAUNCHER_DIRECTORY; /// /// Prelaunching client /// -pub(crate) async fn launch(launch_manifest: LaunchManifest, launching_parameter: LaunchingParameter, additional_mods: Vec, progress: LauncherData, window: Arc>) -> Result<()> { - log(&window, "Loading minecraft version manifest..."); +pub(crate) async fn launch( + launch_manifest: LaunchManifest, + launching_parameter: LaunchingParameter, + additional_mods: Vec, + launcher_data: LauncherData, +) -> Result<()> { + launcher_data.log("Loading minecraft version manifest..."); + let mc_version_manifest = VersionManifest::fetch().await?; let build = &launch_manifest.build; let subsystem = &launch_manifest.subsystem; - progress.progress_update(ProgressUpdate::set_max()); - progress.progress_update(ProgressUpdate::SetProgress(0)); + launcher_data.progress_update(ProgressUpdate::set_max()); + launcher_data.progress_update(ProgressUpdate::SetProgress(0)); - let data_directory = launching_parameter.custom_data_path + let data_directory = launching_parameter + .custom_data_path .clone() .map(|x| x.into()) .unwrap_or_else(|| LAUNCHER_DIRECTORY.data_dir().to_path_buf()); // Copy retrieve and copy mods from manifest clear_mods(&data_directory, &launch_manifest).await?; - retrieve_and_copy_mods(&data_directory, &launch_manifest, &launch_manifest.mods, &progress, &window).await?; - retrieve_and_copy_mods(&data_directory, &launch_manifest, &additional_mods, &progress, &window).await?; - - log(&window, "Loading version profile..."); + retrieve_and_copy_mods( + &data_directory, + &launch_manifest, + &launch_manifest.mods, + &launcher_data, + ) + .await?; + retrieve_and_copy_mods( + &data_directory, + &launch_manifest, + &additional_mods, + &launcher_data, + ) + .await?; + + launcher_data.log("Loading version profile..."); let manifest_url = match subsystem { LoaderSubsystem::Fabric { manifest, .. } => manifest .replace("{MINECRAFT_VERSION}", &build.mc_version) - .replace("{FABRIC_LOADER_VERSION}", &build.subsystem_specific_data.fabric_loader_version), - LoaderSubsystem::Forge { manifest, .. } => manifest.clone() + .replace( + "{FABRIC_LOADER_VERSION}", + &build.subsystem_specific_data.fabric_loader_version, + ), + LoaderSubsystem::Forge { manifest, .. } => manifest.clone(), }; let mut version = VersionProfile::load(&manifest_url).await?; if let Some(inherited_version) = &version.inherits_from { - let url = mc_version_manifest.versions + let url = mc_version_manifest + .versions .iter() .find(|x| &x.id == inherited_version) .map(|x| &x.url) - .ok_or_else(|| LauncherError::InvalidVersionProfile(format!("unable to find inherited version manifest {}", inherited_version)))?; - - debug!("Determined {}'s download url to be {}", inherited_version, url); - log(&window, &format!("Downloading inherited version {}...", inherited_version)); + .ok_or_else(|| { + LauncherError::InvalidVersionProfile(format!( + "unable to find inherited version manifest {}", + inherited_version + )) + })?; + + debug!( + "Determined {}'s download url to be {}", + inherited_version, url + ); + launcher_data.log(&format!("Downloading inherited version {}...", inherited_version)); let parent_version = VersionProfile::load(url).await?; - version.merge(parent_version)?; } - log(&window, &format!("Launching {}...", launch_manifest.build.commit_id)); - launcher::launch(&data_directory, launch_manifest, version, launching_parameter, progress, window).await?; + launcher_data.log(&format!("Launching {}...", launch_manifest.build.commit_id)); + launcher::launch( + &data_directory, + launch_manifest, + version, + launching_parameter, + launcher_data + ) + .await?; Ok(()) } pub(crate) async fn clear_mods(data: &Path, manifest: &LaunchManifest) -> Result<()> { - let mods_path = data.join("gameDir").join(&manifest.build.branch).join("mods"); + let mods_path = data + .join("gameDir") + .join(&manifest.build.branch) + .join("mods"); if !mods_path.exists() { return Ok(()); @@ -106,20 +147,39 @@ pub(crate) async fn clear_mods(data: &Path, manifest: &LaunchManifest) -> Result Ok(()) } -pub async fn retrieve_and_copy_mods(data: &Path, manifest: &LaunchManifest, mods: &Vec, progress: &impl ProgressReceiver, window: &Arc>) -> Result<()> { +pub async fn retrieve_and_copy_mods( + data: &Path, + manifest: &LaunchManifest, + mods: &Vec, + launcher_data: &LauncherData, +) -> Result<()> { let mod_cache_path = data.join("mod_cache"); - let mod_custom_path = data.join("custom_mods") - .join(format!("{}-{}", manifest.build.branch, manifest.build.mc_version)); - let mods_path = data.join("gameDir") + let mod_custom_path = data.join("custom_mods").join(format!( + "{}-{}", + manifest.build.branch, manifest.build.mc_version + )); + let mods_path = data + .join("gameDir") .join(&manifest.build.branch) .join("mods"); - fs::create_dir_all(&mod_cache_path).await - .with_context(|| format!("Failed to create mod cache directory {}", mod_cache_path.display()))?; - fs::create_dir_all(&mods_path).await + fs::create_dir_all(&mod_cache_path).await.with_context(|| { + format!( + "Failed to create mod cache directory {}", + mod_cache_path.display() + ) + })?; + fs::create_dir_all(&mods_path) + .await .with_context(|| format!("Failed to create mods directory {}", mods_path.display()))?; - fs::create_dir_all(&mod_custom_path).await - .with_context(|| format!("Failed to create custom mods directory {}", mod_custom_path.display()))?; + fs::create_dir_all(&mod_custom_path) + .await + .with_context(|| { + format!( + "Failed to create custom mods directory {}", + mod_custom_path.display() + ) + })?; // Download and copy mods let max = get_max(mods.len()); @@ -135,12 +195,17 @@ pub async fn retrieve_and_copy_mods(data: &Path, manifest: &LaunchManifest, mods fs::copy(mod_custom_path.join(file_name), mods_path.join(file_name)) .await .with_context(|| format!("Failed to copy custom mod {}", current_mod.name))?; - log(&window, &format!("Copied custom mod {}", current_mod.name)); - progress.progress_update(ProgressUpdate::set_label(format!("Copied custom mod {}", current_mod.name))); + launcher_data.progress_update(ProgressUpdate::set_label(format!( + "Copied custom mod {}", + current_mod.name + ))); continue; } - progress.progress_update(ProgressUpdate::set_label(format!("Downloading recommended mod {}", current_mod.name))); + launcher_data.progress_update(ProgressUpdate::set_label(format!( + "Downloading recommended mod {}", + current_mod.name + ))); let current_mod_path = mod_cache_path.join(current_mod.source.get_path()?); @@ -150,24 +215,50 @@ pub async fn retrieve_and_copy_mods(data: &Path, manifest: &LaunchManifest, mods fs::create_dir_all(¤t_mod_path.parent().unwrap()).await?; let contents = match ¤t_mod.source { - ModSource::SkipAd { artifact_name: _, url, extract } => { - log(&window, &format!("Opening download page for mod {} on {}", current_mod.name, url)); - progress.progress_update(ProgressUpdate::set_label(format!("Opening download page for mod {}", current_mod.name))); - let direct_url = open_download_page(url, progress, window).await?; - - log(&window, &format!("Downloading mod {} from {}", current_mod.name, direct_url)); - progress.progress_update(ProgressUpdate::set_label(format!("Downloading mod {}", current_mod.name))); - let retrieved_bytes = download_file(&direct_url, |a, b| progress.progress_update(ProgressUpdate::set_for_step(ProgressUpdateSteps::DownloadLiquidBounceMods, get_progress(mod_idx, a, b) as u64, max))).await?; + ModSource::SkipAd { + artifact_name: _, + url, + extract, + } => { + launcher_data.log(&format!( + "Opening download page for mod {} on {}", + current_mod.name, url + )); + launcher_data.progress_update(ProgressUpdate::set_label(format!( + "Opening download page for mod {}", + current_mod.name + ))); + let direct_url = open_download_page(url, launcher_data).await?; + + launcher_data.log(&format!("Downloading mod {} from {}", current_mod.name, direct_url)); + launcher_data.progress_update(ProgressUpdate::set_label(format!( + "Downloading mod {}", + current_mod.name + ))); + let retrieved_bytes = download_file(&direct_url, |a, b| { + launcher_data.progress_update(ProgressUpdate::set_for_step( + ProgressUpdateSteps::DownloadLiquidBounceMods, + get_progress(mod_idx, a, b) as u64, + max, + )) + }) + .await?; // Extract bytes if *extract { let reader = ZipFileReader::new(retrieved_bytes).await?; // Find .JAR file in archive and get index of it - let index_of_file_to_extract = reader.file().entries() + let index_of_file_to_extract = reader + .file() + .entries() .iter() .position(|x| x.entry().filename().ends_with(".jar")) - .ok_or_else(|| LauncherError::InvalidVersionProfile("There is no JAR in the downloaded archive".to_string()))?; + .ok_or_else(|| { + LauncherError::InvalidVersionProfile( + "There is no JAR in the downloaded archive".to_string(), + ) + })?; let entry = reader.file().entries()[index_of_file_to_extract].entry(); // Read file to extract @@ -180,31 +271,50 @@ pub async fn retrieve_and_copy_mods(data: &Path, manifest: &LaunchManifest, mods } else { retrieved_bytes } - }, - ModSource::Repository { repository, artifact } => { - log(&window, &format!("Downloading mod {} from {}", artifact, repository)); - let repository_url = manifest.repositories.get(repository) - .ok_or_else(|| LauncherError::InvalidVersionProfile(format!("There is no repository specified with the name {}", repository)))?; - - let retrieved_bytes = download_file(&format!("{}{}", repository_url, get_maven_artifact_path(artifact)?), |a, b| { - progress.progress_update(ProgressUpdate::set_for_step(ProgressUpdateSteps::DownloadLiquidBounceMods, get_progress(mod_idx, a, b), max)); - }).await?; + } + ModSource::Repository { + repository, + artifact, + } => { + launcher_data.log(&format!("Downloading mod {} from {}", artifact, repository)); + let repository_url = + manifest.repositories.get(repository).ok_or_else(|| { + LauncherError::InvalidVersionProfile(format!( + "There is no repository specified with the name {}", + repository + )) + })?; + + let retrieved_bytes = download_file( + &format!("{}{}", repository_url, get_maven_artifact_path(artifact)?), + |a, b| { + launcher_data.progress_update(ProgressUpdate::set_for_step( + ProgressUpdateSteps::DownloadLiquidBounceMods, + get_progress(mod_idx, a, b), + max, + )); + }, + ) + .await?; retrieved_bytes - }, + } _ => bail!("unsupported mod source: {:?}", current_mod.source), }; - fs::write(¤t_mod_path, contents).await + fs::write(¤t_mod_path, contents) + .await .with_context(|| format!("Failed to write mod {}", current_mod.name))?; } // Copy the mod. - fs::copy(¤t_mod_path, mods_path.join(format!("{}.jar", current_mod.name))) - .await - .with_context(|| format!("Failed to copy mod {}", current_mod.name))?; + fs::copy( + ¤t_mod_path, + mods_path.join(format!("{}.jar", current_mod.name)), + ) + .await + .with_context(|| format!("Failed to copy mod {}", current_mod.name))?; } Ok(()) - } diff --git a/src-tauri/src/minecraft/progress.rs b/src-tauri/src/minecraft/progress.rs index e1d6b18..82de131 100644 --- a/src-tauri/src/minecraft/progress.rs +++ b/src-tauri/src/minecraft/progress.rs @@ -84,5 +84,6 @@ impl ProgressUpdate { pub trait ProgressReceiver { fn progress_update(&self, update: ProgressUpdate); + fn log(&self, msg: &str); } diff --git a/src-tauri/src/minecraft/version.rs b/src-tauri/src/minecraft/version.rs index ae7b181..7547eda 100644 --- a/src-tauri/src/minecraft/version.rs +++ b/src-tauri/src/minecraft/version.rs @@ -27,7 +27,6 @@ use void::Void; use std::collections::HashSet; use crate::{error::LauncherError, HTTP_CLIENT, utils::{download_file_untracked, Architecture}}; use crate::utils::{get_maven_artifact_path, sha1sum}; -use std::sync::Arc; use crate::minecraft::launcher::LaunchingParameter; use crate::minecraft::progress::{ProgressReceiver, ProgressUpdate}; @@ -357,7 +356,7 @@ pub struct AssetObject { impl AssetObject { - pub async fn download(&self, assets_objects_folder: impl AsRef, progress: Arc) -> Result { + pub async fn download(&self, assets_objects_folder: impl AsRef, progress: &impl ProgressReceiver) -> Result { let assets_objects_folder = assets_objects_folder.as_ref().to_owned(); let asset_folder = assets_objects_folder.join(&self.hash[0..2]); @@ -380,7 +379,7 @@ impl AssetObject { } } - pub async fn download_destructing(self, assets_objects_folder: impl AsRef, progress: Arc) -> Result { + pub async fn download_destructing(self, assets_objects_folder: impl AsRef, progress: &impl ProgressReceiver) -> Result { return self.download(assets_objects_folder, progress).await; } @@ -507,32 +506,30 @@ impl LibraryDownloadInfo { .error_for_status()? .text() .await - .map_err(|e| anyhow::anyhow!(e)) + .context("Failed to fetch SHA1 of library") } - pub async fn download(&self, name: String, libraries_folder: &Path, progress: Arc) -> Result { - info!("Downloading library {}, sha1: {:?}, size: {:?}", name, &self.sha1, &self.size); - debug!("Library download url: {}", &self.url); - - let path = libraries_folder.to_path_buf(); - let library_path = path.join(&self.path); - + pub async fn download(&self, name: &str, libraries_folder: PathBuf, progress: &impl ProgressReceiver) -> Result { + let library_path = libraries_folder.join(&self.path); + let parent = library_path.parent().context("Failed to get parent of library path")?; + // Create parent directories - fs::create_dir_all(&library_path.parent().unwrap()).await?; + fs::create_dir_all(parent).await + .context("Failed to create parent directories for library")?; // SHA1 let sha1 = if let Some(sha1) = &self.sha1 { Some(sha1.clone()) } else { // Check if sha1 file exists - let sha1_path = path.join(&self.path).with_extension("sha1"); + let sha1_path = library_path.with_extension("sha1"); + // Fetch sha1 file if sha1_path.exists() { - // If sha1 file exists, read it - let sha1 = fs::read_to_string(&sha1_path).await?; - Some(sha1) + Some(fs::read_to_string(&sha1_path).await?) } else { // If sha1 file doesn't exist, fetch it + progress.log(&format!("Fetching SHA1 of library {}", name)); let sha1 = self.fetch_sha1().await .map(Some) .unwrap_or(None); @@ -541,7 +538,6 @@ impl LibraryDownloadInfo { if let Some(sha1) = &sha1 { fs::write(&sha1_path, &sha1).await?; } - sha1 } }; @@ -549,36 +545,40 @@ impl LibraryDownloadInfo { // Check if library already exists if library_path.exists() { // Check if sha1 matches - let hash = sha1sum(&library_path)?; + let hash = sha1sum(&library_path) + .context("Failed to calculate SHA1 of library")?; if let Some(sha1) = &sha1 { if hash == *sha1 { // If sha1 matches, return - info!("Library {} already exists and matches sha1.", name); + progress.log(&format!("Library {} already exists and SHA1 matches.", name)); return Ok(library_path); } } else { // If sha1 is not available, assume it matches - info!("Library {} already exists.", name); + progress.log(&format!("Library {} already exists.", name)); return Ok(library_path); } - // If sha1 doesn't match, remove the file - info!("Library {} already exists but sha1 doesn't match, redownloading", name); - fs::remove_file(&library_path).await?; + // If SHA1 doesn't match, remove the file + progress.log(&format!("Library {} already exists but sha1 does not match.", name)); + fs::remove_file(&library_path).await + .context("Failed to remove library file")?; } // Download library progress.progress_update(ProgressUpdate::set_label(format!("Downloading library {}", name))); + progress.log(&format!("Downloading library {} (sha1: {:?}, size: {:?}) from {} to {:}", name, &self.sha1, &self.size, &self.url, &library_path.display())); - download_file_untracked(&self.url, &library_path).await?; - info!("Downloaded {}", self.url); - - // After downloading, check sha1 + download_file_untracked(&self.url, &library_path).await + .context("Failed to download library")?; + + // After downloading, check SHA1 if let Some(sha1) = &sha1 { - let hash = sha1sum(&library_path)?; + let hash = sha1sum(&library_path) + .context("Failed to calculate SHA1 of library")?; if hash != *sha1 { - anyhow::bail!("sha1 of downloaded library {} doesn't match", name); + anyhow::bail!("SHA1 of library {} does not match.", name); } } diff --git a/src-tauri/src/utils/macros.rs b/src-tauri/src/utils/macros.rs new file mode 100644 index 0000000..167fbef --- /dev/null +++ b/src-tauri/src/utils/macros.rs @@ -0,0 +1,35 @@ +#[macro_export] +macro_rules! join_and_mkdir { + ($path:expr, $join:expr) => { + { + let path = $path.join($join); + $crate::mkdir!(&path); + path + } + }; +} + +#[macro_export] +macro_rules! join_and_mkdir_vec { + ($path:expr, $joins:expr) => { + { + let mut path = $path.to_path_buf(); + for join in $joins { + path = path.join(join); + $crate::mkdir!(&path); + } + path + } + }; +} + +#[macro_export] +macro_rules! mkdir { + ($path:expr) => { + if !$path.exists() { + if let Err(e) = std::fs::create_dir_all(&$path) { + error!("Failed to create directory {:?}: {}", $path, e); + } + } + } +} \ No newline at end of file diff --git a/src-tauri/src/utils/mod.rs b/src-tauri/src/utils/mod.rs index bd320fd..515d2f8 100644 --- a/src-tauri/src/utils/mod.rs +++ b/src-tauri/src/utils/mod.rs @@ -22,6 +22,7 @@ mod extract; mod download; mod maven; mod checksum; +mod macros; #[cfg(windows)] mod hosts; diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index f5c93b8..24c34bf 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -8,7 +8,7 @@ }, "package": { "productName": "liquidlauncher", - "version": "0.2.5" + "version": "0.2.6" }, "tauri": { "allowlist": {