You've already forked AstralRinth
forked from didirus/AstralRinth
Finish launching modded versions of Minecraft
This commit is contained in:
7
.idea/discord.xml
generated
Normal file
7
.idea/discord.xml
generated
Normal 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
26
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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??)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
17
theseus/src/util.rs
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user