diff --git a/.idea/discord.xml b/.idea/discord.xml new file mode 100644 index 000000000..d8e956166 --- /dev/null +++ b/.idea/discord.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index f88a687a4..84bfe7104 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,7 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +version = 3 + [[package]] name = "adler" version = "1.0.2" @@ -123,6 +125,22 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "daedalus" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "667dec20054908ee40916a3fd8ea5bfc6ed8b1bde6f7741dbea18500a1049ea6" +dependencies = [ + "bytes", + "chrono", + "reqwest", + "serde", + "serde_json", + "sha1", + "thiserror", + "tokio", +] + [[package]] name = "encoding_rs" version = "0.8.28" @@ -636,6 +654,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "path-clean" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecba01bf2678719532c5e3059e0b5f0811273d94b397088b82e3bd0a78c78fdd" + [[package]] name = "percent-encoding" version = "2.1.0" @@ -972,8 +996,10 @@ version = "0.1.0" dependencies = [ "bytes", "chrono", + "daedalus", "futures", "lazy_static", + "path-clean", "regex", "reqwest", "serde", diff --git a/theseus/Cargo.toml b/theseus/Cargo.toml index c4d8a9f57..a1750dcfe 100644 --- a/theseus/Cargo.toml +++ b/theseus/Cargo.toml @@ -9,6 +9,8 @@ edition = "2018" [dependencies] thiserror = "1.0" +daedalus = "0.1.6" + reqwest = { version = "0.11", features = ["json"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" @@ -17,6 +19,7 @@ uuid = { version = "0.8", features = ["serde", "v4"] } bytes = "1" zip = "0.5" sha1 = { version = "0.6.0", features = ["std"]} +path-clean = "0.1.0" regex = "1.5" lazy_static = "1.4" diff --git a/theseus/src/launcher/args.rs b/theseus/src/launcher/args.rs index a7e9b47d4..46dc99fc2 100644 --- a/theseus/src/launcher/args.rs +++ b/theseus/src/launcher/args.rs @@ -1,7 +1,11 @@ use crate::launcher::auth::provider::Credentials; -use crate::launcher::meta::{Argument, ArgumentValue, Library, Os, VersionType}; use crate::launcher::rules::parse_rules; use crate::launcher::LauncherError; +use daedalus::get_path_from_artifact; +use daedalus::minecraft::{Argument, ArgumentValue, Library, Os, VersionType}; +use daedalus::modded::SidedDataEntry; +use std::collections::HashMap; +use std::io::{BufRead, BufReader}; use std::path::Path; use uuid::Uuid; @@ -13,63 +17,24 @@ pub fn get_class_paths( let mut class_paths = Vec::new(); for library in libraries { - if library.downloads.artifact.is_some() { - if let Some(rules) = &library.rules { - if !super::rules::parse_rules(rules.as_slice()) { - continue; - } + if let Some(rules) = &library.rules { + if !super::rules::parse_rules(rules.as_slice()) { + continue; } - - let name_items = library.name.split(':').collect::>(); - - let package = name_items.get(0).ok_or_else(|| { - LauncherError::ParseError(format!( - "Unable to find package for library {}", - &library.name - )) - })?; - let name = name_items.get(1).ok_or_else(|| { - LauncherError::ParseError(format!( - "Unable to find name for library {}", - &library.name - )) - })?; - let version = name_items.get(2).ok_or_else(|| { - LauncherError::ParseError(format!( - "Unable to find version for library {}", - &library.name - )) - })?; - - let mut path = libraries_path.to_path_buf(); - - for directory in package.split('.') { - path.push(directory); - } - - path.push(name); - path.push(version); - path.push(format!("{}-{}.jar", name, version)); - - class_paths.push( - std::fs::canonicalize(&path) - .map_err(|_| { - LauncherError::InvalidInput(format!( - "Library file at path {} does not exist", - path.to_string_lossy() - )) - })? - .to_string_lossy() - .to_string(), - ) } + + if !library.include_in_classpath { + continue; + } + + class_paths.push(get_lib_path(libraries_path, &library.name)?); } class_paths.push( - std::fs::canonicalize(&client_path) + crate::util::absolute_path(&client_path) .map_err(|_| { LauncherError::InvalidInput(format!( - "Specified client path {} does not exist", + "Specified class path {} does not exist", client_path.to_string_lossy() )) })? @@ -83,6 +48,45 @@ pub fn get_class_paths( })) } +pub fn get_class_paths_jar>( + libraries_path: &Path, + libraries: &[T], +) -> Result { + let mut class_paths = Vec::new(); + + for library in libraries { + class_paths.push(get_lib_path(libraries_path, library)?) + } + + Ok(class_paths.join(match super::download::get_os() { + Os::Osx | Os::Linux | Os::Unknown => ":", + Os::Windows => ";", + })) +} + +pub fn get_lib_path>(libraries_path: &Path, lib: T) -> Result { + let mut path = libraries_path.to_path_buf(); + + path.push(get_path_from_artifact(lib.as_ref())?); + + let path = crate::util::absolute_path(&path).map_err(|_| { + LauncherError::InvalidInput(format!( + "Library file at path {} does not exist", + path.to_string_lossy() + )) + })?; + + /*if !path.exists() { + if let Some(parent) = &path.parent() { + std::fs::create_dir_all(parent)?; + } + + std::fs::File::create(&path)?; + }*/ + + Ok(path.to_string_lossy().to_string()) +} + pub fn get_jvm_arguments( arguments: Option<&[Argument]>, natives_path: &Path, @@ -97,7 +101,7 @@ pub fn get_jvm_arguments( } else { parsed_arguments.push(format!( "-Djava.library.path={}", - &*std::fs::canonicalize(natives_path) + &*crate::util::absolute_path(natives_path) .map_err(|_| LauncherError::InvalidInput(format!( "Specified natives path {} does not exist", natives_path.to_string_lossy() @@ -117,10 +121,12 @@ fn parse_jvm_argument( natives_path: &Path, class_paths: &str, ) -> Result { + let mut argument = argument.to_string(); + argument.retain(|c| !c.is_whitespace()); Ok(argument .replace( "${natives_directory}", - &*std::fs::canonicalize(natives_path) + &*crate::util::absolute_path(natives_path) .map_err(|_| { LauncherError::InvalidInput(format!( "Specified natives path {} does not exist", @@ -208,7 +214,7 @@ fn parse_minecraft_argument( .replace("${assets_index_name}", asset_index_name) .replace( "${game_directory}", - &*std::fs::canonicalize(game_directory) + &*crate::util::absolute_path(game_directory) .map_err(|_| { LauncherError::InvalidInput(format!( "Specified game directory {} does not exist", @@ -220,7 +226,7 @@ fn parse_minecraft_argument( ) .replace( "${assets_root}", - &*std::fs::canonicalize(assets_directory) + &*crate::util::absolute_path(assets_directory) .map_err(|_| { LauncherError::InvalidInput(format!( "Specified assets directory {} does not exist", @@ -232,7 +238,7 @@ fn parse_minecraft_argument( ) .replace( "${game_assets}", - &*std::fs::canonicalize(assets_directory) + &*crate::util::absolute_path(assets_directory) .map_err(|_| { LauncherError::InvalidInput(format!( "Specified assets directory {} does not exist", @@ -281,3 +287,59 @@ where Ok(()) } + +pub fn get_processor_arguments>( + libraries_path: &Path, + arguments: &[T], + data: &HashMap, +) -> Result, LauncherError> { + let mut new_arguments = Vec::new(); + + for argument in arguments { + let trimmed_arg = &argument.as_ref()[1..argument.as_ref().len() - 1]; + if argument.as_ref().starts_with('{') { + if let Some(entry) = data.get(trimmed_arg) { + new_arguments.push(if entry.client.starts_with('[') { + get_lib_path(libraries_path, &entry.client[1..entry.client.len() - 1])? + } else { + entry.client.clone() + }) + } + } else if argument.as_ref().starts_with('[') { + new_arguments.push(get_lib_path(libraries_path, trimmed_arg)?) + } else { + new_arguments.push(argument.as_ref().to_string()) + } + } + + Ok(new_arguments) +} + +pub async fn get_processor_main_class(path: String) -> Result, LauncherError> { + Ok(tokio::task::spawn_blocking(move || { + let zipfile = std::fs::File::open(&path)?; + let mut archive = zip::ZipArchive::new(zipfile).map_err(|_| { + LauncherError::ProcessorError(format!("Cannot read processor at {}", path)) + })?; + + let file = archive.by_name("META-INF/MANIFEST.MF").map_err(|_| { + LauncherError::ProcessorError(format!("Cannot read processor manifest at {}", path)) + })?; + + let reader = BufReader::new(file); + + for line in reader.lines() { + let mut line = line?; + line.retain(|c| !c.is_whitespace()); + + if line.starts_with("Main-Class:") { + if let Some(class) = line.split(':').nth(1) { + return Ok(Some(class.to_string())); + } + } + } + + Ok::, LauncherError>(None) + }) + .await??) +} diff --git a/theseus/src/launcher/auth.rs b/theseus/src/launcher/auth.rs index 0981010d6..909eb5d27 100644 --- a/theseus/src/launcher/auth.rs +++ b/theseus/src/launcher/auth.rs @@ -167,12 +167,39 @@ pub mod api { } pub mod provider { + use crate::launcher::auth::api::login; + use crate::launcher::LauncherError; use uuid::Uuid; #[derive(Debug)] + /// The credentials of a user pub struct Credentials { + /// The user UUID the credentials belong to pub id: Uuid, + /// The username of the user pub username: String, + /// The access token associated with the credentials pub access_token: String, } + + impl Credentials { + /// Gets a credentials instance from a user's login + pub async fn from_login(username: &str, password: &str) -> Result { + let login = + login(username, password, true) + .await + .map_err(|err| LauncherError::FetchError { + inner: err, + item: "authentication credentials".to_string(), + })?; + + let profile = login.selected_profile.unwrap(); + + Ok(Credentials { + id: profile.id, + username: profile.name, + access_token: login.access_token, + }) + } + } } diff --git a/theseus/src/launcher/download.rs b/theseus/src/launcher/download.rs index b26007a45..e50de820a 100644 --- a/theseus/src/launcher/download.rs +++ b/theseus/src/launcher/download.rs @@ -1,30 +1,36 @@ -use crate::launcher::meta::{ +use crate::launcher::LauncherError; +use daedalus::get_path_from_artifact; +use daedalus::minecraft::{ fetch_assets_index, fetch_version_info, Asset, AssetsIndex, DownloadType, Library, Os, Version, VersionInfo, }; -use crate::launcher::LauncherError; +use daedalus::modded::{fetch_partial_version, merge_partial_version, LoaderVersion}; use futures::future; use std::fs::File; -use std::io::{BufReader, Write}; +use std::io::Write; use std::path::Path; pub async fn download_version_info( client_path: &Path, version: &Version, + loader_version: Option<&LoaderVersion>, ) -> Result { - let path = &*client_path - .join(&version.id) - .join(format!("{}.json", &version.id)); + let id = loader_version.map(|x| &x.id).unwrap_or(&version.id); + + let path = &*client_path.join(id).join(format!("{}.json", id)); if path.exists() { Ok(serde_json::from_str(&std::fs::read_to_string(path)?)?) } else { - let info = fetch_version_info(version) - .await - .map_err(|err| LauncherError::FetchError { - inner: err, - item: "version info".to_string(), - })?; + let mut info = fetch_version_info(version).await?; + + if let Some(loader_version) = loader_version { + let partial = fetch_partial_version(&*loader_version.url).await?; + + info = merge_partial_version(partial, info); + + info.id = loader_version.id.clone(); + } save_file(path, &bytes::Bytes::from(serde_json::to_string(&info)?))?; @@ -50,7 +56,7 @@ pub async fn download_client( .join(&version_info.id) .join(format!("{}.jar", &version_info.id)); - save_and_download_file(path, &client_download.url, &client_download.sha1).await?; + save_and_download_file(path, &client_download.url, Some(&client_download.sha1)).await?; Ok(()) } @@ -66,12 +72,7 @@ pub async fn download_assets_index( if path.exists() { Ok(serde_json::from_str(&std::fs::read_to_string(path)?)?) } else { - let index = fetch_assets_index(version) - .await - .map_err(|err| LauncherError::FetchError { - inner: err, - item: "assets index".to_string(), - })?; + let index = fetch_assets_index(version).await?; save_file(path, &bytes::Bytes::from(serde_json::to_string(&index)?))?; @@ -113,7 +114,7 @@ async fn download_asset( "https://resources.download.minecraft.net/{}/{}", sub_hash, asset.hash ), - &*asset.hash, + Some(&*asset.hash), ) .await?; @@ -154,34 +155,9 @@ async fn download_library( } } - let name_items = library.name.split(':').collect::>(); - - let package = name_items.get(0).ok_or_else(|| { - LauncherError::ParseError(format!( - "Unable to find package for library {}", - &library.name - )) - })?; - let name = name_items.get(1).ok_or_else(|| { - LauncherError::ParseError(format!("Unable to find name for library {}", &library.name)) - })?; - let version = name_items.get(2).ok_or_else(|| { - LauncherError::ParseError(format!( - "Unable to find version for library {}", - &library.name - )) - })?; - let (a, b) = future::join( - download_library_jar(libraries_path, library, package, name, version), - download_native( - libraries_path, - natives_path, - library, - package, - name, - version, - ), + download_library_jar(libraries_path, library), + download_native(natives_path, library), ) .await; @@ -194,61 +170,51 @@ async fn download_library( async fn download_library_jar( libraries_path: &Path, library: &Library, - package: &str, - name: &str, - version: &str, ) -> Result<(), LauncherError> { - if let Some(library) = &library.downloads.artifact { - let mut path = libraries_path.to_path_buf(); + let mut path = libraries_path.to_path_buf(); + path.push(get_path_from_artifact(&*library.name)?); - for directory in package.split('.') { - path.push(directory); + if let Some(downloads) = &library.downloads { + if let Some(library) = &downloads.artifact { + save_and_download_file(&*path, &library.url, Some(&library.sha1)).await?; } - - path.push(name); - path.push(version); - path.push(format!("{}-{}.jar", name, version)); - - save_and_download_file(&*path, &library.url, &library.sha1).await?; + } else { + save_and_download_file( + &*path, + &format!( + "{}{}", + library + .url + .as_deref() + .unwrap_or("https://libraries.minecraft.net/"), + get_path_from_artifact(&*library.name)? + ), + None, + ) + .await?; } Ok(()) } -async fn download_native( - libraries_path: &Path, - natives_path: &Path, - library: &Library, - package: &str, - name: &str, - version: &str, -) -> Result<(), LauncherError> { +async fn download_native(natives_path: &Path, library: &Library) -> Result<(), LauncherError> { if let Some(natives) = &library.natives { if let Some(os_key) = natives.get(&get_os()) { - if let Some(classifiers) = &library.downloads.classifiers { - #[cfg(target_pointer_width = "64")] - let parsed_key = os_key.replace("${arch}", "64"); - #[cfg(target_pointer_width = "32")] - let parsed_key = os_key.replace("${arch}", "32"); + if let Some(downloads) = &library.downloads { + if let Some(classifiers) = &downloads.classifiers { + #[cfg(target_pointer_width = "64")] + let parsed_key = os_key.replace("${arch}", "64"); + #[cfg(target_pointer_width = "32")] + let parsed_key = os_key.replace("${arch}", "32"); - if let Some(native) = classifiers.get(&*parsed_key) { - let mut path = libraries_path.to_path_buf(); + if let Some(native) = classifiers.get(&*parsed_key) { + let file = download_file(&native.url, Some(&native.sha1)).await?; - for directory in package.split('.') { - path.push(directory); + let reader = std::io::Cursor::new(&*file); + + let mut archive = zip::ZipArchive::new(reader).unwrap(); + archive.extract(natives_path).unwrap(); } - - path.push(name); - path.push(version); - path.push(format!("{}-{}-{}.jar", name, version, parsed_key)); - - save_and_download_file(&*path, &native.url, &native.sha1).await?; - - let file = File::open(&path).unwrap(); - let reader = BufReader::new(file); - - let mut archive = zip::ZipArchive::new(reader).unwrap(); - archive.extract(natives_path).unwrap(); } } } @@ -260,14 +226,14 @@ async fn download_native( async fn save_and_download_file( path: &Path, url: &str, - sha1: &str, + sha1: Option<&str>, ) -> Result { let read = std::fs::read(path).ok().map(bytes::Bytes::from); if let Some(bytes) = read { Ok(bytes) } else { - let file = download_file(url, Some(sha1)).await?; + let file = download_file(url, sha1).await?; save_file(path, &file)?; @@ -286,7 +252,16 @@ fn save_file(path: &Path, bytes: &bytes::Bytes) -> Result<(), std::io::Error> { Ok(()) } -async fn download_file(url: &str, sha1: Option<&str>) -> Result { +pub fn get_os() -> Os { + match std::env::consts::OS { + "windows" => Os::Windows, + "macos" => Os::Osx, + "linux" => Os::Linux, + _ => Os::Unknown, + } +} + +pub async fn download_file(url: &str, sha1: Option<&str>) -> Result { let client = reqwest::Client::builder() .tcp_keepalive(Some(std::time::Duration::from_secs(10))) .build() @@ -295,7 +270,7 @@ async fn download_file(url: &str, sha1: Option<&str>) -> Result) -> Result Result { +/// Computes a checksum of the input bytes +pub async fn get_hash(bytes: bytes::Bytes) -> Result { let hash = tokio::task::spawn_blocking(|| sha1::Sha1::from(bytes).hexdigest()).await?; Ok(hash) } - -pub fn get_os() -> Os { - match std::env::consts::OS { - "windows" => Os::Windows, - "macos" => Os::Osx, - "linux" => Os::Linux, - _ => Os::Unknown, - } -} diff --git a/theseus/src/launcher/meta.rs b/theseus/src/launcher/meta.rs deleted file mode 100644 index ac964b400..000000000 --- a/theseus/src/launcher/meta.rs +++ /dev/null @@ -1,205 +0,0 @@ -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; - -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "snake_case")] -pub enum VersionType { - Release, - Snapshot, - OldAlpha, - OldBeta, -} - -impl VersionType { - pub fn as_str(&self) -> &'static str { - match self { - VersionType::Release => "release", - VersionType::Snapshot => "snapshot", - VersionType::OldAlpha => "old_alpha", - VersionType::OldBeta => "old_beta", - } - } -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct Version { - pub id: String, - #[serde(rename = "type")] - pub type_: VersionType, - pub url: String, - pub time: DateTime, - pub release_time: DateTime, -} - -#[derive(Deserialize, Debug)] -pub struct LatestVersion { - pub release: String, - pub snapshot: String, -} - -#[derive(Deserialize, Debug)] -pub struct VersionManifest { - pub latest: LatestVersion, - pub versions: Vec, -} - -pub async fn fetch_version_manifest() -> Result { - reqwest::get("https://launchermeta.mojang.com/mc/game/version_manifest.json") - .await? - .json() - .await -} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct AssetIndex { - pub id: String, - pub sha1: String, - pub size: u32, - pub total_size: u32, - pub url: String, -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Hash)] -#[serde(rename_all = "snake_case")] -pub enum DownloadType { - Client, - ClientMappings, - Server, - ServerMappings, - WindowsServer, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct Download { - pub sha1: String, - pub size: u32, - pub url: String, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct LibraryDownload { - pub path: String, - pub sha1: String, - pub size: u32, - pub url: String, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct LibraryDownloads { - pub artifact: Option, - pub classifiers: Option>, -} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "snake_case")] -pub enum RuleAction { - Allow, - Disallow, -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Hash)] -#[serde(rename_all = "snake_case")] -pub enum Os { - Osx, - Windows, - Linux, - Unknown, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct OsRule { - pub name: Option, - pub version: Option, - pub arch: Option, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct FeatureRule { - pub is_demo_user: Option, - pub has_demo_resolution: Option, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct Rule { - pub action: RuleAction, - pub os: Option, - pub features: Option, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct LibraryExtract { - pub exclude: Option>, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct Library { - pub downloads: LibraryDownloads, - pub extract: Option, - pub name: String, - pub natives: Option>, - pub rules: Option>, -} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(untagged)] -pub enum ArgumentValue { - Single(String), - Many(Vec), -} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(untagged)] -pub enum Argument { - Normal(String), - Ruled { - rules: Vec, - value: ArgumentValue, - }, -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Hash)] -#[serde(rename_all = "snake_case")] -pub enum ArgumentType { - Game, - Jvm, -} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct VersionInfo { - pub arguments: Option>>, - pub asset_index: AssetIndex, - pub assets: String, - pub downloads: HashMap, - pub id: String, - pub libraries: Vec, - pub main_class: String, - pub minecraft_arguments: Option, - pub minimum_launcher_version: u32, - pub release_time: DateTime, - pub time: DateTime, - #[serde(rename = "type")] - pub type_: VersionType, -} - -pub async fn fetch_version_info(version: &Version) -> Result { - reqwest::get(&version.url).await?.json().await -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct Asset { - pub hash: String, - pub size: u32, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct AssetsIndex { - pub objects: HashMap, -} - -pub async fn fetch_assets_index(version: &VersionInfo) -> Result { - reqwest::get(&version.asset_index.url).await?.json().await -} diff --git a/theseus/src/launcher/mod.rs b/theseus/src/launcher/mod.rs index 2b7994eae..8bf104060 100644 --- a/theseus/src/launcher/mod.rs +++ b/theseus/src/launcher/mod.rs @@ -1,14 +1,15 @@ -use crate::launcher::auth::provider::Credentials; +use daedalus::minecraft::{ArgumentType, VersionInfo}; use std::path::Path; use std::process::{Command, Stdio}; use thiserror::Error; -pub mod args; -pub mod auth; -pub mod download; -pub mod java; -pub mod meta; -pub mod rules; +pub use crate::launcher::auth::provider::Credentials; + +mod args; +mod auth; +mod download; +mod java; +mod rules; #[derive(Error, Debug)] pub enum LauncherError { @@ -18,11 +19,13 @@ pub enum LauncherError { 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")] + #[error("Error while reading/writing to the disk: {0}")] IoError(#[from] std::io::Error), #[error("Error while spawning child process {process}")] ProcessError { @@ -35,57 +38,237 @@ pub enum LauncherError { FetchError { inner: reqwest::Error, item: String }, #[error("{0}")] ParseError(String), + #[error("Error while fetching metadata: {0}")] + DaedalusError(#[from] daedalus::Error), +} + +const META_URL: &str = "https://staging-cdn.modrinth.com/gamedata"; + +pub async fn fetch_metadata() -> Result< + ( + daedalus::minecraft::VersionManifest, + daedalus::modded::Manifest, + daedalus::modded::Manifest, + ), + LauncherError, +> { + let (game, forge, fabric) = futures::future::join3( + daedalus::minecraft::fetch_version_manifest(Some(&*format!( + "{}/minecraft/v0/manifest.json", + META_URL + ))), + daedalus::modded::fetch_manifest(&*format!("{}/forge/v0/manifest.json", META_URL)), + daedalus::modded::fetch_manifest(&*format!("{}/fabric/v0/manifest.json", META_URL)), + ) + .await; + + Ok((game?, forge?, fabric?)) +} + +#[derive(Debug, Eq, PartialEq, Clone, Copy)] +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 manifest = meta::fetch_version_manifest().await.unwrap(); + let (game, forge, fabric) = fetch_metadata().await?; - let version = download::download_version_info( - &*root_dir.join("versions"), - manifest - .versions + 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 mut version = download::download_version_info( + &versions_path, + game.versions .iter() .find(|x| x.id == version_name) .ok_or_else(|| { LauncherError::InvalidInput(format!("Version {} does not exist", version_name)) })?, + match mod_loader.unwrap_or_default() { + ModLoader::Vanilla => None, + ModLoader::Forge | ModLoader::Fabric => { + let loaders = if mod_loader.unwrap_or_default() == ModLoader::Forge { + &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 { + &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.get(&daedalus::modded::LoaderType::Stable) + { + Some(version.clone()) + } else if let Some(version) = loaders.get(&daedalus::modded::LoaderType::Latest) { + Some(version.clone()) + } else { + None + }; + + Some(loader.ok_or_else(|| { + LauncherError::InvalidInput(format!( + "No mod loader version found for version {}", + version_name + )) + })?) + } + } + .as_ref(), ) .await?; - download_minecraft(&version, root_dir).await?; + 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))?; - let arguments = version.arguments.unwrap(); + 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 child = Command::new("java") .args(args::get_jvm_arguments( - arguments - .get(&meta::ArgumentType::Jvm) - .map(|x| x.as_slice()), - &*root_dir.join("natives").join(&version.id), - &*args::get_class_paths( - &*root_dir.join("libraries"), - version.libraries.as_slice(), - &*root_dir - .join("versions") - .join(&version.id) - .join(format!("{}.jar", &version.id)), - )?, + arguments.get(&ArgumentType::Jvm).map(|x| x.as_slice()), + &natives_path, + &*args::get_class_paths(&libraries_path, version.libraries.as_slice(), &client_path)?, )?) .arg(version.main_class) .args(args::get_minecraft_arguments( - arguments - .get(&meta::ArgumentType::Game) - .map(|x| x.as_slice()), + arguments.get(&ArgumentType::Game).map(|x| x.as_slice()), version.minecraft_arguments.as_deref(), credentials, &*version.id, &version.asset_index.id, root_dir, - &*root_dir.join("assets"), + &assets_path, &version.type_, )?) .current_dir(root_dir) @@ -106,29 +289,27 @@ pub async fn launch_minecraft( } pub async fn download_minecraft( - version: &meta::VersionInfo, - root_dir: &Path, + 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(&*root_dir.join("assets"), &version).await?; - - let legacy_dir = root_dir.join("resources"); + let assets_index = download::download_assets_index(assets_dir, version).await?; let (a, b, c) = futures::future::join3( - download::download_client(&*root_dir.join("versions"), &version), + download::download_client(versions_dir, version), download::download_assets( - &*root_dir.join("assets"), + assets_dir, if version.assets == "legacy" { - Some(legacy_dir.as_path()) + Some(legacy_assets_dir) } else { None }, &assets_index, ), - download::download_libraries( - &*root_dir.join("libraries"), - &*root_dir.join("natives").join(&version.id), - version.libraries.as_slice(), - ), + download::download_libraries(libraries_dir, natives_dir, version.libraries.as_slice()), ) .await; diff --git a/theseus/src/launcher/rules.rs b/theseus/src/launcher/rules.rs index 1970c8852..43c40b814 100644 --- a/theseus/src/launcher/rules.rs +++ b/theseus/src/launcher/rules.rs @@ -1,5 +1,5 @@ use crate::launcher::download::get_os; -use crate::launcher::meta::{OsRule, Rule, RuleAction}; +use daedalus::minecraft::{OsRule, Rule, RuleAction}; use regex::Regex; pub fn parse_rules(rules: &[Rule]) -> bool { @@ -9,10 +9,8 @@ pub fn parse_rules(rules: &[Rule]) -> bool { pub fn parse_rule(rule: &Rule) -> bool { let result = if let Some(os) = &rule.os { parse_os_rule(os) - } else if rule.features.is_some() { - false } else { - true + rule.features.is_none() }; match rule.action { diff --git a/theseus/src/lib.rs b/theseus/src/lib.rs index da52d023b..dbf0e2036 100644 --- a/theseus/src/lib.rs +++ b/theseus/src/lib.rs @@ -6,3 +6,4 @@ #![warn(missing_docs, unused_import_braces, missing_debug_implementations)] pub mod launcher; +mod util; diff --git a/theseus/src/util.rs b/theseus/src/util.rs new file mode 100644 index 000000000..48d4fdc5a --- /dev/null +++ b/theseus/src/util.rs @@ -0,0 +1,17 @@ +use std::path::{Path, PathBuf}; +use std::{env, io}; + +use path_clean::PathClean; + +pub fn absolute_path(path: impl AsRef) -> io::Result { + let path = path.as_ref(); + + let absolute_path = if path.is_absolute() { + path.to_path_buf() + } else { + env::current_dir()?.join(path) + } + .clean(); + + Ok(absolute_path) +}