use daedalus::minecraft::{ArgumentType, VersionInfo}; use serde::{Deserialize, Serialize}; use std::path::Path; use std::process::{Command, Stdio}; use thiserror::Error; pub use crate::launcher::auth::provider::Credentials; mod args; mod auth; mod download; mod java; mod rules; #[derive(Error, Debug)] pub enum LauncherError { #[error("Failed to violate file checksum at url {url} with hash {hash} after {tries} tries")] ChecksumFailure { hash: String, url: String, tries: u32, }, #[error("Failed to run processor: {0}")] ProcessorError(String), #[error("Invalid input: {0}")] InvalidInput(String), #[error("Error while managing asynchronous tasks")] TaskError(#[from] tokio::task::JoinError), #[error("Error while reading/writing to the disk: {0}")] IoError(#[from] std::io::Error), #[error("Error while spawning child process {process}")] ProcessError { inner: std::io::Error, process: String, }, #[error("Error while deserializing JSON")] SerdeError(#[from] serde_json::Error), #[error("Unable to fetch {item}")] FetchError { inner: reqwest::Error, item: String }, #[error("{0}")] ParseError(String), #[error("Error while fetching metadata: {0}")] DaedalusError(#[from] daedalus::Error), #[error("Error while reading metadata: {0}")] MetaError(#[from] crate::data::DataError), #[error("Java error: {0}")] JavaError(String), } #[derive(Debug, Eq, PartialEq, Clone, Copy, Deserialize, Serialize)] #[serde(rename_all = "lowercase")] pub enum ModLoader { Vanilla, Forge, Fabric, } impl Default for ModLoader { fn default() -> Self { ModLoader::Vanilla } } pub async fn launch_minecraft( version_name: &str, mod_loader: Option, root_dir: &Path, credentials: &Credentials, ) -> Result<(), LauncherError> { let metadata = crate::data::Metadata::get().await?; let settings = crate::data::Settings::get().await?; let versions_path = crate::util::absolute_path(root_dir.join("versions"))?; let libraries_path = crate::util::absolute_path(root_dir.join("libraries"))?; let assets_path = crate::util::absolute_path(root_dir.join("assets"))?; let legacy_assets_path = crate::util::absolute_path(root_dir.join("resources"))?; let version = metadata .minecraft .versions .iter() .find(|x| x.id == version_name) .ok_or_else(|| { LauncherError::InvalidInput(format!("Version {} does not exist", version_name)) })?; let loader_version = match mod_loader.unwrap_or_default() { ModLoader::Vanilla => None, ModLoader::Forge | ModLoader::Fabric => { let loaders = if mod_loader.unwrap_or_default() == ModLoader::Forge { &metadata .forge .game_versions .iter() .find(|x| x.id == version_name) .ok_or_else(|| { LauncherError::InvalidInput(format!( "Version {} for mod loader Forge does not exist", version_name )) })? .loaders } else { &metadata .fabric .game_versions .iter() .find(|x| x.id == version_name) .ok_or_else(|| { LauncherError::InvalidInput(format!( "Version {} for mod loader Fabric does not exist", version_name )) })? .loaders }; let loader = if let Some(version) = loaders.iter().find(|x| x.stable) { Some(version.clone()) } else { loaders.first().cloned() }; Some(loader.ok_or_else(|| { LauncherError::InvalidInput(format!( "No mod loader version found for version {}", version_name )) })?) } }; let version_jar_name = if let Some(loader) = &loader_version { loader.id.clone() } else { version.id.clone() }; let mut version = download::download_version_info( &versions_path, version, loader_version.as_ref(), ) .await?; let java_path = if let Some(java) = &version.java_version { if java.major_version == 17 || java.major_version == 16 { settings.java_17_path.as_deref().ok_or_else(|| LauncherError::JavaError("Please install Java 17 or select your Java 17 installation settings before launching this version!".to_string()))? } else { &settings.java_8_path.as_deref().ok_or_else(|| LauncherError::JavaError("Please install Java 8 or select your Java 8 installation settings before launching this version!".to_string()))? } } else { &settings.java_8_path.as_deref().ok_or_else(|| LauncherError::JavaError("Please install Java 8 or select your Java 8 installation settings before launching this version!".to_string()))? }; let client_path = crate::util::absolute_path( root_dir .join("versions") .join(&version.id) .join(format!("{}.jar", &version.id)), )?; let natives_path = crate::util::absolute_path(root_dir.join("natives").join(&version.id))?; download_minecraft( &version, &versions_path, &assets_path, &legacy_assets_path, &libraries_path, &natives_path, ) .await?; if let Some(processors) = &version.processors { if let Some(ref mut data) = version.data { data.insert( "SIDE".to_string(), daedalus::modded::SidedDataEntry { client: "client".to_string(), server: "".to_string(), }, ); data.insert( "MINECRAFT_JAR".to_string(), daedalus::modded::SidedDataEntry { client: client_path.to_string_lossy().to_string(), server: "".to_string(), }, ); data.insert( "MINECRAFT_VERSION".to_string(), daedalus::modded::SidedDataEntry { client: version_name.to_string(), server: "".to_string(), }, ); data.insert( "ROOT".to_string(), daedalus::modded::SidedDataEntry { client: root_dir.to_string_lossy().to_string(), server: "".to_string(), }, ); data.insert( "LIBRARY_DIR".to_string(), daedalus::modded::SidedDataEntry { client: libraries_path.to_string_lossy().to_string(), server: "".to_string(), }, ); for processor in processors { if let Some(sides) = &processor.sides { if !sides.contains(&"client".to_string()) { continue; } } let mut cp = processor.classpath.clone(); cp.push(processor.jar.clone()); let child = Command::new("java") .arg("-cp") .arg(args::get_class_paths_jar(&libraries_path, &cp)?) .arg( args::get_processor_main_class(args::get_lib_path( &libraries_path, &processor.jar, )?) .await? .ok_or_else(|| { LauncherError::ProcessorError(format!( "Could not find processor main class for {}", processor.jar )) })?, ) .args(args::get_processor_arguments( &libraries_path, &processor.args, data, )?) .output() .map_err(|err| LauncherError::ProcessError { inner: err, process: "java".to_string(), })?; if !child.status.success() { return Err(LauncherError::ProcessorError( String::from_utf8_lossy(&child.stderr).to_string(), )); } } } } let arguments = version.arguments.unwrap_or_default(); let mut command = Command::new(if let Some(wrapper) = &settings.wrapper_command { wrapper.clone() } else { java_path.to_string() }); if settings.wrapper_command.is_some() { command.arg(java_path); } command .args(args::get_jvm_arguments( arguments.get(&ArgumentType::Jvm).map(|x| x.as_slice()), &natives_path, &libraries_path, &args::get_class_paths(&libraries_path, version.libraries.as_slice(), &client_path)?, &version_jar_name, settings.memory, settings .custom_java_args .split(" ") .into_iter() .map(|x| x.to_string()) .collect(), )?) .arg(version.main_class) .args(args::get_minecraft_arguments( arguments.get(&ArgumentType::Game).map(|x| x.as_slice()), version.minecraft_arguments.as_deref(), credentials, &version.id, &version.asset_index.id, root_dir, &assets_path, &version.type_, settings.game_resolution, )?) .current_dir(root_dir) .stdout(Stdio::inherit()) .stderr(Stdio::inherit()); let mut child = command.spawn().map_err(|err| LauncherError::ProcessError { inner: err, process: "minecraft".to_string(), })?; child.wait().map_err(|err| LauncherError::ProcessError { inner: err, process: "minecraft".to_string(), })?; Ok(()) } pub async fn download_minecraft( version: &VersionInfo, versions_dir: &Path, assets_dir: &Path, legacy_assets_dir: &Path, libraries_dir: &Path, natives_dir: &Path, ) -> Result<(), LauncherError> { let assets_index = download::download_assets_index(assets_dir, version).await?; let (a, b, c) = futures::future::join3( download::download_client(versions_dir, version), download::download_assets( assets_dir, if version.assets == "legacy" { Some(legacy_assets_dir) } else { None }, &assets_index, ), download::download_libraries(libraries_dir, natives_dir, version.libraries.as_slice()), ) .await; a?; b?; c?; Ok(()) }