Finish launching modded versions of Minecraft

This commit is contained in:
Jai A
2021-11-10 21:52:55 -07:00
parent b0214cfcf8
commit 359e81083e
11 changed files with 496 additions and 412 deletions

7
.idea/discord.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DiscordProjectSettings">
<option name="show" value="PROJECT_FILES" />
<option name="description" value="" />
</component>
</project>

26
Cargo.lock generated
View File

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

View File

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

View File

@@ -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::<Vec<&str>>();
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<T: AsRef<str>>(
libraries_path: &Path,
libraries: &[T],
) -> Result<String, LauncherError> {
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<T: AsRef<str>>(libraries_path: &Path, lib: T) -> Result<String, LauncherError> {
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<String, LauncherError> {
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<T: AsRef<str>>(
libraries_path: &Path,
arguments: &[T],
data: &HashMap<String, SidedDataEntry>,
) -> Result<Vec<String>, 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<Option<String>, 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::<Option<String>, LauncherError>(None)
})
.await??)
}

View File

@@ -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<Self, LauncherError> {
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,
})
}
}
}

View File

@@ -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<VersionInfo, LauncherError> {
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::<Vec<&str>>();
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<bytes::Bytes, LauncherError> {
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<bytes::Bytes, LauncherError> {
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<bytes::Bytes, LauncherError> {
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<bytes::Bytes, La
item: url.to_string(),
})?;
for attempt in 1..4 {
for attempt in 1..=4 {
let result = client.get(url).send().await;
match result {
@@ -340,17 +315,9 @@ async fn download_file(url: &str, sha1: Option<&str>) -> Result<bytes::Bytes, La
unreachable!()
}
async fn get_hash(bytes: bytes::Bytes) -> Result<String, LauncherError> {
/// Computes a checksum of the input bytes
pub async fn get_hash(bytes: bytes::Bytes) -> Result<String, LauncherError> {
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,
}
}

View File

@@ -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<Utc>,
pub release_time: DateTime<Utc>,
}
#[derive(Deserialize, Debug)]
pub struct LatestVersion {
pub release: String,
pub snapshot: String,
}
#[derive(Deserialize, Debug)]
pub struct VersionManifest {
pub latest: LatestVersion,
pub versions: Vec<Version>,
}
pub async fn fetch_version_manifest() -> Result<VersionManifest, reqwest::Error> {
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<LibraryDownload>,
pub classifiers: Option<HashMap<String, LibraryDownload>>,
}
#[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<Os>,
pub version: Option<String>,
pub arch: Option<String>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct FeatureRule {
pub is_demo_user: Option<bool>,
pub has_demo_resolution: Option<bool>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Rule {
pub action: RuleAction,
pub os: Option<OsRule>,
pub features: Option<FeatureRule>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct LibraryExtract {
pub exclude: Option<Vec<String>>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Library {
pub downloads: LibraryDownloads,
pub extract: Option<LibraryExtract>,
pub name: String,
pub natives: Option<HashMap<Os, String>>,
pub rules: Option<Vec<Rule>>,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(untagged)]
pub enum ArgumentValue {
Single(String),
Many(Vec<String>),
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(untagged)]
pub enum Argument {
Normal(String),
Ruled {
rules: Vec<Rule>,
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<HashMap<ArgumentType, Vec<Argument>>>,
pub asset_index: AssetIndex,
pub assets: String,
pub downloads: HashMap<DownloadType, Download>,
pub id: String,
pub libraries: Vec<Library>,
pub main_class: String,
pub minecraft_arguments: Option<String>,
pub minimum_launcher_version: u32,
pub release_time: DateTime<Utc>,
pub time: DateTime<Utc>,
#[serde(rename = "type")]
pub type_: VersionType,
}
pub async fn fetch_version_info(version: &Version) -> Result<VersionInfo, reqwest::Error> {
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<String, Asset>,
}
pub async fn fetch_assets_index(version: &VersionInfo) -> Result<AssetsIndex, reqwest::Error> {
reqwest::get(&version.asset_index.url).await?.json().await
}

View File

@@ -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<ModLoader>,
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;

View File

@@ -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 {

View File

@@ -6,3 +6,4 @@
#![warn(missing_docs, unused_import_braces, missing_debug_implementations)]
pub mod launcher;
mod util;

17
theseus/src/util.rs Normal file
View File

@@ -0,0 +1,17 @@
use std::path::{Path, PathBuf};
use std::{env, io};
use path_clean::PathClean;
pub fn absolute_path(path: impl AsRef<Path>) -> io::Result<PathBuf> {
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)
}