You've already forked AstralRinth
forked from didirus/AstralRinth
Refactor Library
The launcher code was in a position ripe for sphagetti, so this rewrites it in a more robust way. In addition to cleaner code, this provides the following changes: - Removal of obsolete Mojang authentication - The rebasing of some internal state into a Sled database - Tweaks which make some internal mechanisms more robust (e.g. profiles which fail to load can be removed) - Additional tooling integration such as direnv - Distinct public API to avoid messing with too much internal code - Unified error handling in the form of `theseus::Error` and `theseus::Result`
This commit is contained in:
@@ -1,32 +1,29 @@
|
||||
use crate::data::profiles::*;
|
||||
use crate::launcher::auth::provider::Credentials;
|
||||
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;
|
||||
//! Minecraft CLI argument logic
|
||||
// TODO: Rafactor this section
|
||||
use super::{auth::Credentials, parse_rule};
|
||||
use crate::{
|
||||
state::{MemorySettings, WindowSize},
|
||||
util::platform::classpath_separator,
|
||||
};
|
||||
use daedalus::{
|
||||
get_path_from_artifact,
|
||||
minecraft::{Argument, ArgumentValue, Library, VersionType},
|
||||
modded::SidedDataEntry,
|
||||
};
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::path::Path;
|
||||
use std::{collections::HashMap, path::Path};
|
||||
use uuid::Uuid;
|
||||
|
||||
fn get_cp_separator() -> &'static str {
|
||||
match super::download::get_os() {
|
||||
Os::Osx | Os::Linux | Os::Unknown => ":",
|
||||
Os::Windows => ";",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_class_paths(
|
||||
libraries_path: &Path,
|
||||
libraries: &[Library],
|
||||
client_path: &Path,
|
||||
) -> Result<String, LauncherError> {
|
||||
let mut class_paths = libraries
|
||||
) -> crate::Result<String> {
|
||||
let mut cps = libraries
|
||||
.iter()
|
||||
.filter_map(|library| {
|
||||
if let Some(rules) = &library.rules {
|
||||
if !super::rules::parse_rules(rules.as_slice()) {
|
||||
if !rules.iter().all(parse_rule) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
@@ -39,10 +36,11 @@ pub fn get_class_paths(
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
class_paths.push(
|
||||
crate::util::absolute_path(&client_path)
|
||||
cps.push(
|
||||
client_path
|
||||
.canonicalize()
|
||||
.map_err(|_| {
|
||||
LauncherError::InvalidInput(format!(
|
||||
crate::Error::LauncherError(format!(
|
||||
"Specified class path {} does not exist",
|
||||
client_path.to_string_lossy()
|
||||
))
|
||||
@@ -51,44 +49,35 @@ pub fn get_class_paths(
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
Ok(class_paths.join(get_cp_separator()))
|
||||
Ok(cps.join(classpath_separator()))
|
||||
}
|
||||
|
||||
pub fn get_class_paths_jar<T: AsRef<str>>(
|
||||
libraries_path: &Path,
|
||||
libraries: &[T],
|
||||
) -> Result<String, LauncherError> {
|
||||
let class_paths = libraries
|
||||
) -> crate::Result<String> {
|
||||
let cps = libraries
|
||||
.iter()
|
||||
.map(|library| get_lib_path(libraries_path, library.as_ref()))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
Ok(class_paths.join(get_cp_separator()))
|
||||
Ok(cps.join(classpath_separator()))
|
||||
}
|
||||
|
||||
pub fn get_lib_path(libraries_path: &Path, lib: &str) -> Result<String, LauncherError> {
|
||||
pub fn get_lib_path(libraries_path: &Path, lib: &str) -> crate::Result<String> {
|
||||
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!(
|
||||
let path = &path.canonicalize().map_err(|_| {
|
||||
crate::Error::LauncherError(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 +86,7 @@ pub fn get_jvm_arguments(
|
||||
version_name: &str,
|
||||
memory: MemorySettings,
|
||||
custom_args: Vec<String>,
|
||||
) -> Result<Vec<String>, LauncherError> {
|
||||
) -> crate::Result<Vec<String>> {
|
||||
let mut parsed_arguments = Vec::new();
|
||||
|
||||
if let Some(args) = arguments {
|
||||
@@ -113,8 +102,9 @@ pub fn get_jvm_arguments(
|
||||
} else {
|
||||
parsed_arguments.push(format!(
|
||||
"-Djava.library.path={}",
|
||||
&crate::util::absolute_path(natives_path)
|
||||
.map_err(|_| LauncherError::InvalidInput(format!(
|
||||
&natives_path
|
||||
.canonicalize()
|
||||
.map_err(|_| crate::Error::LauncherError(format!(
|
||||
"Specified natives path {} does not exist",
|
||||
natives_path.to_string_lossy()
|
||||
)))?
|
||||
@@ -144,14 +134,15 @@ fn parse_jvm_argument(
|
||||
libraries_path: &Path,
|
||||
class_paths: &str,
|
||||
version_name: &str,
|
||||
) -> Result<String, LauncherError> {
|
||||
) -> crate::Result<String> {
|
||||
argument.retain(|c| !c.is_whitespace());
|
||||
Ok(argument
|
||||
.replace(
|
||||
"${natives_directory}",
|
||||
&crate::util::absolute_path(natives_path)
|
||||
&natives_path
|
||||
.canonicalize()
|
||||
.map_err(|_| {
|
||||
LauncherError::InvalidInput(format!(
|
||||
crate::Error::LauncherError(format!(
|
||||
"Specified natives path {} does not exist",
|
||||
natives_path.to_string_lossy()
|
||||
))
|
||||
@@ -160,9 +151,10 @@ fn parse_jvm_argument(
|
||||
)
|
||||
.replace(
|
||||
"${library_directory}",
|
||||
&crate::util::absolute_path(libraries_path)
|
||||
&libraries_path
|
||||
.canonicalize()
|
||||
.map_err(|_| {
|
||||
LauncherError::InvalidInput(format!(
|
||||
crate::Error::LauncherError(format!(
|
||||
"Specified libraries path {} does not exist",
|
||||
libraries_path.to_string_lossy()
|
||||
))
|
||||
@@ -170,7 +162,7 @@ fn parse_jvm_argument(
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
)
|
||||
.replace("${classpath_separator}", get_cp_separator())
|
||||
.replace("${classpath_separator}", classpath_separator())
|
||||
.replace("${launcher_name}", "theseus")
|
||||
.replace("${launcher_version}", env!("CARGO_PKG_VERSION"))
|
||||
.replace("${version_name}", version_name)
|
||||
@@ -188,7 +180,7 @@ pub fn get_minecraft_arguments(
|
||||
assets_directory: &Path,
|
||||
version_type: &VersionType,
|
||||
resolution: WindowSize,
|
||||
) -> Result<Vec<String>, LauncherError> {
|
||||
) -> crate::Result<Vec<String>> {
|
||||
if let Some(arguments) = arguments {
|
||||
let mut parsed_arguments = Vec::new();
|
||||
|
||||
@@ -242,7 +234,7 @@ fn parse_minecraft_argument(
|
||||
assets_directory: &Path,
|
||||
version_type: &VersionType,
|
||||
resolution: WindowSize,
|
||||
) -> Result<String, LauncherError> {
|
||||
) -> crate::Result<String> {
|
||||
Ok(argument
|
||||
.replace("${auth_access_token}", access_token)
|
||||
.replace("${auth_session}", access_token)
|
||||
@@ -254,9 +246,10 @@ fn parse_minecraft_argument(
|
||||
.replace("${assets_index_name}", asset_index_name)
|
||||
.replace(
|
||||
"${game_directory}",
|
||||
&crate::util::absolute_path(game_directory)
|
||||
&game_directory
|
||||
.canonicalize()
|
||||
.map_err(|_| {
|
||||
LauncherError::InvalidInput(format!(
|
||||
crate::Error::LauncherError(format!(
|
||||
"Specified game directory {} does not exist",
|
||||
game_directory.to_string_lossy()
|
||||
))
|
||||
@@ -266,9 +259,10 @@ fn parse_minecraft_argument(
|
||||
)
|
||||
.replace(
|
||||
"${assets_root}",
|
||||
&crate::util::absolute_path(assets_directory)
|
||||
&assets_directory
|
||||
.canonicalize()
|
||||
.map_err(|_| {
|
||||
LauncherError::InvalidInput(format!(
|
||||
crate::Error::LauncherError(format!(
|
||||
"Specified assets directory {} does not exist",
|
||||
assets_directory.to_string_lossy()
|
||||
))
|
||||
@@ -278,9 +272,10 @@ fn parse_minecraft_argument(
|
||||
)
|
||||
.replace(
|
||||
"${game_assets}",
|
||||
&crate::util::absolute_path(assets_directory)
|
||||
&assets_directory
|
||||
.canonicalize()
|
||||
.map_err(|_| {
|
||||
LauncherError::InvalidInput(format!(
|
||||
crate::Error::LauncherError(format!(
|
||||
"Specified assets directory {} does not exist",
|
||||
assets_directory.to_string_lossy()
|
||||
))
|
||||
@@ -297,9 +292,9 @@ fn parse_arguments<F>(
|
||||
arguments: &[Argument],
|
||||
parsed_arguments: &mut Vec<String>,
|
||||
parse_function: F,
|
||||
) -> Result<(), LauncherError>
|
||||
) -> crate::Result<()>
|
||||
where
|
||||
F: Fn(&str) -> Result<String, LauncherError>,
|
||||
F: Fn(&str) -> crate::Result<String>,
|
||||
{
|
||||
for argument in arguments {
|
||||
match argument {
|
||||
@@ -311,7 +306,7 @@ where
|
||||
}
|
||||
}
|
||||
Argument::Ruled { rules, value } => {
|
||||
if parse_rules(rules.as_slice()) {
|
||||
if rules.iter().all(parse_rule) {
|
||||
match value {
|
||||
ArgumentValue::Single(arg) => {
|
||||
parsed_arguments.push(parse_function(arg)?);
|
||||
@@ -334,7 +329,7 @@ pub fn get_processor_arguments<T: AsRef<str>>(
|
||||
libraries_path: &Path,
|
||||
arguments: &[T],
|
||||
data: &HashMap<String, SidedDataEntry>,
|
||||
) -> Result<Vec<String>, LauncherError> {
|
||||
) -> crate::Result<Vec<String>> {
|
||||
let mut new_arguments = Vec::new();
|
||||
|
||||
for argument in arguments {
|
||||
@@ -342,7 +337,10 @@ pub fn get_processor_arguments<T: AsRef<str>>(
|
||||
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])?
|
||||
get_lib_path(
|
||||
libraries_path,
|
||||
&entry.client[1..entry.client.len() - 1],
|
||||
)?
|
||||
} else {
|
||||
entry.client.clone()
|
||||
})
|
||||
@@ -357,15 +355,23 @@ pub fn get_processor_arguments<T: AsRef<str>>(
|
||||
Ok(new_arguments)
|
||||
}
|
||||
|
||||
pub async fn get_processor_main_class(path: String) -> Result<Option<String>, LauncherError> {
|
||||
pub async fn get_processor_main_class(
|
||||
path: String,
|
||||
) -> crate::Result<Option<String>> {
|
||||
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))
|
||||
crate::Error::LauncherError(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))
|
||||
crate::Error::LauncherError(format!(
|
||||
"Cannot read processor manifest at {}",
|
||||
path
|
||||
))
|
||||
})?;
|
||||
|
||||
let reader = BufReader::new(file);
|
||||
@@ -381,7 +387,8 @@ pub async fn get_processor_main_class(path: String) -> Result<Option<String>, La
|
||||
}
|
||||
}
|
||||
|
||||
Ok::<Option<String>, LauncherError>(None)
|
||||
Ok::<Option<String>, crate::Error>(None)
|
||||
})
|
||||
.await??)
|
||||
.await
|
||||
.unwrap()?)
|
||||
}
|
||||
|
||||
@@ -1,205 +1,8 @@
|
||||
pub mod api {
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct GameProfile {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UserProperty {
|
||||
pub name: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct User {
|
||||
pub id: String,
|
||||
pub username: String,
|
||||
pub properties: Option<Vec<UserProperty>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AuthenticateResponse {
|
||||
pub user: Option<User>,
|
||||
pub client_token: Uuid,
|
||||
pub access_token: String,
|
||||
pub available_profiles: Vec<GameProfile>,
|
||||
pub selected_profile: Option<GameProfile>,
|
||||
}
|
||||
|
||||
pub async fn login(
|
||||
username: &str,
|
||||
password: &str,
|
||||
request_user: bool,
|
||||
) -> Result<AuthenticateResponse, reqwest::Error> {
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
client
|
||||
.post("https://authserver.mojang.com/authenticate")
|
||||
.header(reqwest::header::CONTENT_TYPE, "application/json")
|
||||
.body(
|
||||
serde_json::json!(
|
||||
{
|
||||
"agent": {
|
||||
"name": "Minecraft",
|
||||
"version": 1
|
||||
},
|
||||
"username": username,
|
||||
"password": password,
|
||||
"clientToken": Uuid::new_v4(),
|
||||
"requestUser": request_user
|
||||
}
|
||||
)
|
||||
.to_string(),
|
||||
)
|
||||
.send()
|
||||
.await?
|
||||
.json()
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn sign_out(username: &str, password: &str) -> Result<(), reqwest::Error> {
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
client
|
||||
.post("https://authserver.mojang.com/signout")
|
||||
.header(reqwest::header::CONTENT_TYPE, "application/json")
|
||||
.body(
|
||||
serde_json::json!(
|
||||
{
|
||||
"username": username,
|
||||
"password": password
|
||||
}
|
||||
)
|
||||
.to_string(),
|
||||
)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn validate(access_token: &str, client_token: &str) -> Result<(), reqwest::Error> {
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
client
|
||||
.post("https://authserver.mojang.com/validate")
|
||||
.header(reqwest::header::CONTENT_TYPE, "application/json")
|
||||
.body(
|
||||
serde_json::json!(
|
||||
{
|
||||
"accessToken": access_token,
|
||||
"clientToken": client_token
|
||||
}
|
||||
)
|
||||
.to_string(),
|
||||
)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn invalidate(access_token: &str, client_token: &str) -> Result<(), reqwest::Error> {
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
client
|
||||
.post("https://authserver.mojang.com/invalidate")
|
||||
.header(reqwest::header::CONTENT_TYPE, "application/json")
|
||||
.body(
|
||||
serde_json::json!(
|
||||
{
|
||||
"accessToken": access_token,
|
||||
"clientToken": client_token
|
||||
}
|
||||
)
|
||||
.to_string(),
|
||||
)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RefreshResponse {
|
||||
pub user: Option<User>,
|
||||
pub client_token: Uuid,
|
||||
pub access_token: String,
|
||||
pub selected_profile: Option<GameProfile>,
|
||||
}
|
||||
|
||||
pub async fn refresh(
|
||||
access_token: &str,
|
||||
client_token: &str,
|
||||
selected_profile: &GameProfile,
|
||||
request_user: bool,
|
||||
) -> Result<RefreshResponse, reqwest::Error> {
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
client
|
||||
.post("https://authserver.mojang.com/refresh")
|
||||
.header(reqwest::header::CONTENT_TYPE, "application/json")
|
||||
.body(
|
||||
serde_json::json!(
|
||||
{
|
||||
"accessToken": access_token,
|
||||
"clientToken": client_token,
|
||||
"selectedProfile": {
|
||||
"id": selected_profile.id,
|
||||
"name": selected_profile.name,
|
||||
},
|
||||
"requestUser": request_user,
|
||||
}
|
||||
)
|
||||
.to_string(),
|
||||
)
|
||||
.send()
|
||||
.await?
|
||||
.json()
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
//! Authentication flow
|
||||
// TODO: Implement authentication
|
||||
#[derive(Debug)]
|
||||
pub struct Credentials {
|
||||
pub id: uuid::Uuid,
|
||||
pub username: String,
|
||||
pub access_token: String,
|
||||
}
|
||||
|
||||
@@ -1,362 +1,282 @@
|
||||
//! Downloader for Minecraft data
|
||||
|
||||
use crate::{
|
||||
data::{DataError, Settings},
|
||||
launcher::LauncherError,
|
||||
state::State,
|
||||
util::{fetch::*, platform::OsExt},
|
||||
};
|
||||
use daedalus::get_path_from_artifact;
|
||||
use daedalus::minecraft::{
|
||||
fetch_assets_index, fetch_version_info, Asset, AssetsIndex, DownloadType,
|
||||
Library, Os, Version, VersionInfo,
|
||||
};
|
||||
use daedalus::modded::{
|
||||
fetch_partial_version, merge_partial_version, LoaderVersion,
|
||||
};
|
||||
use futures::future;
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
use tokio::{
|
||||
fs::File,
|
||||
io::AsyncWriteExt,
|
||||
sync::{OnceCell, Semaphore},
|
||||
use daedalus::{
|
||||
self as d,
|
||||
minecraft::{
|
||||
Asset, AssetsIndex, Library, Os, Version as GameVersion,
|
||||
VersionInfo as GameVersionInfo,
|
||||
},
|
||||
modded::LoaderVersion,
|
||||
};
|
||||
use futures::prelude::*;
|
||||
use std::sync::Arc;
|
||||
use tokio::{fs, sync::OnceCell};
|
||||
|
||||
static DOWNLOADS_SEMAPHORE: OnceCell<Semaphore> = OnceCell::const_new();
|
||||
pub async fn download_minecraft(
|
||||
st: &State,
|
||||
version: &GameVersionInfo,
|
||||
) -> crate::Result<()> {
|
||||
log::info!("Downloading Minecraft version {}", version.id);
|
||||
let assets_index = download_assets_index(st, version).await?;
|
||||
|
||||
pub async fn init() -> Result<(), DataError> {
|
||||
DOWNLOADS_SEMAPHORE
|
||||
.get_or_try_init(|| async {
|
||||
let settings = Settings::get().await?;
|
||||
Ok::<_, DataError>(Semaphore::new(
|
||||
settings.max_concurrent_downloads,
|
||||
))
|
||||
})
|
||||
.await?;
|
||||
tokio::try_join! {
|
||||
download_client(st, version),
|
||||
download_assets(st, version.assets == "legacy", &assets_index),
|
||||
download_libraries(st, version.libraries.as_slice(), &version.id)
|
||||
}?;
|
||||
|
||||
log::info!("Done downloading Minecraft!");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn download_version_info(
|
||||
client_path: &Path,
|
||||
version: &Version,
|
||||
loader_version: Option<&LoaderVersion>,
|
||||
) -> Result<VersionInfo, LauncherError> {
|
||||
let id = match loader_version {
|
||||
Some(x) => &x.id,
|
||||
None => &version.id,
|
||||
};
|
||||
st: &State,
|
||||
version: &GameVersion,
|
||||
loader: Option<&LoaderVersion>,
|
||||
) -> crate::Result<GameVersionInfo> {
|
||||
let version_id = loader.map_or(&version.id, |it| &it.id);
|
||||
log::debug!("Loading version info for Minecraft {version_id}");
|
||||
let path = st
|
||||
.directories
|
||||
.version_dir(version_id)
|
||||
.join(format!("{version_id}.json"));
|
||||
|
||||
let mut path = client_path.join(id);
|
||||
path.push(&format!("{id}.json"));
|
||||
|
||||
if path.exists() {
|
||||
let contents = std::fs::read_to_string(path)?;
|
||||
Ok(serde_json::from_str(&contents)?)
|
||||
let res = if path.exists() {
|
||||
fs::read(path)
|
||||
.err_into::<crate::Error>()
|
||||
.await
|
||||
.and_then(|ref it| Ok(serde_json::from_slice(it)?))
|
||||
} else {
|
||||
let mut info = fetch_version_info(version).await?;
|
||||
log::info!("Downloading version info for version {}", &version.id);
|
||||
let mut info = d::minecraft::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();
|
||||
if let Some(loader) = loader {
|
||||
let partial = d::modded::fetch_partial_version(&loader.url).await?;
|
||||
info = d::modded::merge_partial_version(partial, info);
|
||||
info.id = loader.id.clone();
|
||||
}
|
||||
let info_s = serde_json::to_string(&info)?;
|
||||
save_file(&path, &bytes::Bytes::from(info_s)).await?;
|
||||
|
||||
let permit = st.io_semaphore.acquire().await.unwrap();
|
||||
write(&path, &serde_json::to_vec(&info)?, &permit).await?;
|
||||
Ok(info)
|
||||
}
|
||||
}?;
|
||||
|
||||
log::debug!("Loaded version info for Minecraft {version_id}");
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub async fn download_client(
|
||||
client_path: &Path,
|
||||
version_info: &VersionInfo,
|
||||
) -> Result<(), LauncherError> {
|
||||
let version = &version_info.id;
|
||||
st: &State,
|
||||
version_info: &GameVersionInfo,
|
||||
) -> crate::Result<()> {
|
||||
let ref version = version_info.id;
|
||||
log::debug!("Locating client for version {version}");
|
||||
let client_download = version_info
|
||||
.downloads
|
||||
.get(&DownloadType::Client)
|
||||
.ok_or_else(|| {
|
||||
LauncherError::InvalidInput(format!(
|
||||
"Version {version} does not have any client downloads"
|
||||
))
|
||||
})?;
|
||||
.get(&d::minecraft::DownloadType::Client)
|
||||
.ok_or(crate::Error::LauncherError(format!(
|
||||
"No client downloads exist for version {version}"
|
||||
)))?;
|
||||
let path = st
|
||||
.directories
|
||||
.version_dir(version)
|
||||
.join(format!("{version}.jar"));
|
||||
|
||||
let mut path = client_path.join(version);
|
||||
path.push(&format!("{version}.jar"));
|
||||
if !path.exists() {
|
||||
let permit = st.io_semaphore.acquire().await.unwrap();
|
||||
let bytes =
|
||||
fetch(&client_download.url, Some(&client_download.sha1), &permit)
|
||||
.await?;
|
||||
write(&path, &bytes, &permit).await?;
|
||||
log::info!("Fetched client version {version}");
|
||||
}
|
||||
|
||||
save_and_download_file(
|
||||
&path,
|
||||
&client_download.url,
|
||||
Some(&client_download.sha1),
|
||||
)
|
||||
.await?;
|
||||
log::debug!("Client loaded for version {version}!");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn download_assets_index(
|
||||
assets_path: &Path,
|
||||
version: &VersionInfo,
|
||||
) -> Result<AssetsIndex, LauncherError> {
|
||||
let path =
|
||||
assets_path.join(format!("indexes/{}.json", &version.asset_index.id));
|
||||
st: &State,
|
||||
version: &GameVersionInfo,
|
||||
) -> crate::Result<AssetsIndex> {
|
||||
log::debug!("Loading assets index");
|
||||
let path = st
|
||||
.directories
|
||||
.assets_index_dir()
|
||||
.join(format!("{}.json", &version.asset_index.id));
|
||||
|
||||
if path.exists() {
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
Ok(serde_json::from_str(&content)?)
|
||||
let res = if path.exists() {
|
||||
fs::read(path)
|
||||
.err_into::<crate::Error>()
|
||||
.await
|
||||
.and_then(|ref it| Ok(serde_json::from_slice(it)?))
|
||||
} else {
|
||||
let index = fetch_assets_index(version).await?;
|
||||
|
||||
save_file(&path, &bytes::Bytes::from(serde_json::to_string(&index)?))
|
||||
.await?;
|
||||
|
||||
let index = d::minecraft::fetch_assets_index(version).await?;
|
||||
let permit = st.io_semaphore.acquire().await.unwrap();
|
||||
write(&path, &serde_json::to_vec(&index)?, &permit).await?;
|
||||
log::info!("Fetched assets index");
|
||||
Ok(index)
|
||||
}
|
||||
}?;
|
||||
|
||||
log::debug!("Assets index successfully loaded!");
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub async fn download_assets(
|
||||
assets_path: &Path,
|
||||
legacy_path: Option<&Path>,
|
||||
st: &State,
|
||||
with_legacy: bool,
|
||||
index: &AssetsIndex,
|
||||
) -> Result<(), LauncherError> {
|
||||
future::join_all(index.objects.iter().map(|(name, asset)| {
|
||||
download_asset(assets_path, legacy_path, name, asset)
|
||||
}))
|
||||
.await
|
||||
.into_iter()
|
||||
.collect::<Result<Vec<()>, LauncherError>>()?;
|
||||
) -> crate::Result<()> {
|
||||
log::debug!("Loading assets");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
stream::iter(index.objects.iter())
|
||||
.map(Ok::<(&String, &Asset), crate::Error>)
|
||||
.try_for_each_concurrent(None, |(name, asset)| async move {
|
||||
let ref hash = asset.hash;
|
||||
let resource_path = st.directories.object_dir(hash);
|
||||
let url = format!(
|
||||
"https://resources.download.minecraft.net/{sub_hash}/{hash}",
|
||||
sub_hash = &hash[..2]
|
||||
);
|
||||
|
||||
async fn download_asset(
|
||||
assets_path: &Path,
|
||||
legacy_path: Option<&Path>,
|
||||
name: &str,
|
||||
asset: &Asset,
|
||||
) -> Result<(), LauncherError> {
|
||||
let hash = &asset.hash;
|
||||
let sub_hash = &hash[..2];
|
||||
let fetch_cell = OnceCell::<bytes::Bytes>::new();
|
||||
tokio::try_join! {
|
||||
async {
|
||||
if !resource_path.exists() {
|
||||
let permit = st.io_semaphore.acquire().await.unwrap();
|
||||
let resource = fetch_cell
|
||||
.get_or_try_init(|| fetch(&url, Some(hash), &permit))
|
||||
.await?;
|
||||
write(&resource_path, &resource, &permit).await?;
|
||||
log::info!("Fetched asset with hash {hash}");
|
||||
}
|
||||
Ok::<_, crate::Error>(())
|
||||
},
|
||||
async {
|
||||
if with_legacy {
|
||||
let permit = st.io_semaphore.acquire().await.unwrap();
|
||||
let resource = fetch_cell
|
||||
.get_or_try_init(|| fetch(&url, Some(hash), &permit))
|
||||
.await?;
|
||||
let resource_path = st.directories.legacy_assets_dir().join(
|
||||
name.replace('/', &String::from(std::path::MAIN_SEPARATOR))
|
||||
);
|
||||
write(&resource_path, &resource, &permit).await?;
|
||||
log::info!("Fetched legacy asset with hash {hash}");
|
||||
}
|
||||
Ok::<_, crate::Error>(())
|
||||
},
|
||||
}?;
|
||||
|
||||
let mut resource_path = assets_path.join("objects");
|
||||
resource_path.push(sub_hash);
|
||||
resource_path.push(hash);
|
||||
|
||||
let url =
|
||||
format!("https://resources.download.minecraft.net/{sub_hash}/{hash}");
|
||||
|
||||
let resource =
|
||||
save_and_download_file(&resource_path, &url, Some(hash)).await?;
|
||||
|
||||
if let Some(legacy_path) = legacy_path {
|
||||
let resource_path = legacy_path
|
||||
.join(name.replace('/', &std::path::MAIN_SEPARATOR.to_string()));
|
||||
save_file(resource_path.as_path(), &resource).await?;
|
||||
}
|
||||
log::debug!("Loaded asset with hash {hash}");
|
||||
Ok(())
|
||||
}).await?;
|
||||
|
||||
log::debug!("Done loading assets!");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn download_libraries(
|
||||
libraries_path: &Path,
|
||||
natives_path: &Path,
|
||||
st: &State,
|
||||
libraries: &[Library],
|
||||
) -> Result<(), LauncherError> {
|
||||
future::join_all(libraries.iter().map(|library| {
|
||||
download_library(libraries_path, natives_path, library)
|
||||
}))
|
||||
.await
|
||||
.into_iter()
|
||||
.collect::<Result<Vec<()>, LauncherError>>()?;
|
||||
version: &str,
|
||||
) -> crate::Result<()> {
|
||||
log::debug!("Loading libraries");
|
||||
let (libraries_dir, natives_dir) = (
|
||||
Arc::new(st.directories.libraries_dir()),
|
||||
Arc::new(st.directories.version_natives_dir(version)),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
tokio::try_join! {
|
||||
fs::create_dir_all(st.directories.libraries_dir()),
|
||||
fs::create_dir_all(st.directories.version_natives_dir(version))
|
||||
}?;
|
||||
|
||||
async fn download_library(
|
||||
libraries_path: &Path,
|
||||
natives_path: &Path,
|
||||
library: &Library,
|
||||
) -> Result<(), LauncherError> {
|
||||
if let Some(rules) = &library.rules {
|
||||
if !super::rules::parse_rules(rules) {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
stream::iter(libraries.iter())
|
||||
.map(Ok::<&Library, crate::Error>)
|
||||
.try_for_each_concurrent(None, |library| async move {
|
||||
if let Some(rules) = &library.rules {
|
||||
if !rules.iter().all(super::parse_rule) {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
tokio::try_join! {
|
||||
async {
|
||||
let artifact_path = d::get_path_from_artifact(&library.name)?;
|
||||
let path = st.directories.libraries_dir().join(&artifact_path);
|
||||
|
||||
future::try_join(
|
||||
download_library_jar(libraries_path, library),
|
||||
download_native(natives_path, library),
|
||||
)
|
||||
.await?;
|
||||
match library.downloads {
|
||||
_ if path.exists() => Ok(()),
|
||||
Some(d::minecraft::LibraryDownloads {
|
||||
artifact: Some(ref artifact),
|
||||
..
|
||||
}) => {
|
||||
let permit = st.io_semaphore.acquire().await.unwrap();
|
||||
let bytes = fetch(&artifact.url, Some(&artifact.sha1), &permit)
|
||||
.await?;
|
||||
write(&path, &bytes, &permit).await?;
|
||||
log::info!("Fetched library {}", &library.name);
|
||||
Ok::<_, crate::Error>(())
|
||||
}
|
||||
None => {
|
||||
let url = [
|
||||
library
|
||||
.url
|
||||
.as_deref()
|
||||
.unwrap_or("https://libraries.minecraft.net"),
|
||||
&artifact_path
|
||||
].concat();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
let permit = st.io_semaphore.acquire().await.unwrap();
|
||||
let bytes = fetch(&url, None, &permit).await?;
|
||||
write(&path, &bytes, &permit).await?;
|
||||
log::info!("Fetched library {}", &library.name);
|
||||
Ok::<_, crate::Error>(())
|
||||
}
|
||||
_ => Ok(())
|
||||
}
|
||||
},
|
||||
async {
|
||||
// HACK: pseudo try block using or else
|
||||
if let Some((os_key, classifiers)) = None.or_else(|| Some((
|
||||
library
|
||||
.natives
|
||||
.as_ref()?
|
||||
.get(&Os::native())?,
|
||||
library
|
||||
.downloads
|
||||
.as_ref()?
|
||||
.classifiers
|
||||
.as_ref()?
|
||||
))) {
|
||||
let parsed_key = os_key.replace(
|
||||
"${arch}",
|
||||
crate::util::platform::ARCH_WIDTH,
|
||||
);
|
||||
|
||||
async fn download_library_jar(
|
||||
libraries_path: &Path,
|
||||
library: &Library,
|
||||
) -> Result<(), LauncherError> {
|
||||
let artifact_path = get_path_from_artifact(&library.name)?;
|
||||
let path = libraries_path.join(&artifact_path);
|
||||
|
||||
if let Some(downloads) = &library.downloads {
|
||||
if let Some(library) = &downloads.artifact {
|
||||
save_and_download_file(&path, &library.url, Some(&library.sha1))
|
||||
.await?;
|
||||
}
|
||||
} else {
|
||||
let url = format!(
|
||||
"{}{artifact_path}",
|
||||
library
|
||||
.url
|
||||
.as_deref()
|
||||
.unwrap_or("https://libraries.minecraft.net/"),
|
||||
);
|
||||
save_and_download_file(&path, &url, None).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn download_native(
|
||||
natives_path: &Path,
|
||||
library: &Library,
|
||||
) -> Result<(), LauncherError> {
|
||||
use daedalus::minecraft::LibraryDownload;
|
||||
use std::collections::HashMap;
|
||||
|
||||
// Try blocks in stable Rust when?
|
||||
let optional_cascade =
|
||||
|| -> Option<(&String, &HashMap<String, LibraryDownload>)> {
|
||||
let os_key = library.natives.as_ref()?.get(&get_os())?;
|
||||
let classifiers =
|
||||
library.downloads.as_ref()?.classifiers.as_ref()?;
|
||||
Some((os_key, classifiers))
|
||||
};
|
||||
|
||||
if let Some((os_key, classifiers)) = optional_cascade() {
|
||||
#[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 file = download_file(&native.url, Some(&native.sha1)).await?;
|
||||
|
||||
let reader = std::io::Cursor::new(&file);
|
||||
|
||||
let mut archive = zip::ZipArchive::new(reader).unwrap();
|
||||
archive.extract(natives_path).unwrap();
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn save_and_download_file(
|
||||
path: &Path,
|
||||
url: &str,
|
||||
sha1: Option<&str>,
|
||||
) -> Result<bytes::Bytes, LauncherError> {
|
||||
match std::fs::read(path) {
|
||||
Ok(bytes) => Ok(bytes::Bytes::from(bytes)),
|
||||
Err(_) => {
|
||||
let file = download_file(url, sha1).await?;
|
||||
save_file(path, &file).await?;
|
||||
Ok(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn save_file(path: &Path, bytes: &bytes::Bytes) -> std::io::Result<()> {
|
||||
let _save_permit = DOWNLOADS_SEMAPHORE
|
||||
.get()
|
||||
.expect("File operation semaphore not initialized!")
|
||||
.acquire()
|
||||
.await
|
||||
.unwrap();
|
||||
if let Some(parent) = path.parent() {
|
||||
tokio::fs::create_dir_all(parent).await?;
|
||||
}
|
||||
|
||||
let mut file = File::create(path).await?;
|
||||
file.write_all(bytes).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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 _download_permit = DOWNLOADS_SEMAPHORE
|
||||
.get()
|
||||
.expect("File operation semaphore not initialized!")
|
||||
.acquire()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.tcp_keepalive(Some(Duration::from_secs(10)))
|
||||
.build()
|
||||
.map_err(|err| LauncherError::FetchError {
|
||||
inner: err,
|
||||
item: url.to_string(),
|
||||
})?;
|
||||
|
||||
for attempt in 1..=4 {
|
||||
let result = client.get(url).send().await;
|
||||
|
||||
match result {
|
||||
Ok(x) => {
|
||||
let bytes = x.bytes().await;
|
||||
|
||||
if let Ok(bytes) = bytes {
|
||||
if let Some(sha1) = sha1 {
|
||||
if &get_hash(bytes.clone()).await? != sha1 {
|
||||
if attempt <= 3 {
|
||||
continue;
|
||||
} else {
|
||||
return Err(LauncherError::ChecksumFailure {
|
||||
hash: sha1.to_string(),
|
||||
url: url.to_string(),
|
||||
tries: attempt,
|
||||
});
|
||||
}
|
||||
if let Some(native) = classifiers.get(&parsed_key) {
|
||||
let permit = st.io_semaphore.acquire().await.unwrap();
|
||||
let data = fetch(&native.url, Some(&native.sha1), &permit).await?;
|
||||
let reader = std::io::Cursor::new(&data);
|
||||
let mut archive = zip::ZipArchive::new(reader).unwrap();
|
||||
archive.extract(&st.directories.version_natives_dir(version)).unwrap();
|
||||
log::info!("Fetched native {}", &library.name);
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(bytes);
|
||||
} else if attempt <= 3 {
|
||||
continue;
|
||||
} else if let Err(err) = bytes {
|
||||
return Err(LauncherError::FetchError {
|
||||
inner: err,
|
||||
item: url.to_string(),
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Err(_) if attempt <= 3 => continue,
|
||||
Err(err) => {
|
||||
return Err(LauncherError::FetchError {
|
||||
inner: err,
|
||||
item: url.to_string(),
|
||||
})
|
||||
}
|
||||
}?;
|
||||
|
||||
log::debug!("Loaded library {}", library.name);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
unreachable!()
|
||||
}
|
||||
).await?;
|
||||
|
||||
/// Computes a checksum of the input bytes
|
||||
async fn get_hash(bytes: bytes::Bytes) -> Result<String, LauncherError> {
|
||||
let hash =
|
||||
tokio::task::spawn_blocking(|| sha1::Sha1::from(bytes).hexdigest())
|
||||
.await?;
|
||||
|
||||
Ok(hash)
|
||||
log::debug!("Done loading libraries!");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,205 +1,116 @@
|
||||
use daedalus::minecraft::{ArgumentType, VersionInfo};
|
||||
use daedalus::modded::LoaderVersion;
|
||||
use serde::{Deserialize, Serialize};
|
||||
//! Logic for launching Minecraft
|
||||
use crate::state as st;
|
||||
use daedalus as d;
|
||||
use std::{path::Path, process::Stdio};
|
||||
use thiserror::Error;
|
||||
use tokio::process::{Child, Command};
|
||||
|
||||
pub use crate::launcher::auth::provider::Credentials;
|
||||
|
||||
mod args;
|
||||
pub mod auth;
|
||||
|
||||
mod auth;
|
||||
pub use auth::Credentials;
|
||||
|
||||
mod download;
|
||||
mod rules;
|
||||
|
||||
pub(crate) use download::init as init_download_semaphore;
|
||||
pub fn parse_rule(rule: &d::minecraft::Rule) -> bool {
|
||||
use d::minecraft::{Rule, RuleAction};
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum LauncherError {
|
||||
#[error("Failed to validate file checksum at url {url} with hash {hash} after {tries} tries")]
|
||||
ChecksumFailure {
|
||||
hash: String,
|
||||
url: String,
|
||||
tries: u32,
|
||||
},
|
||||
let res = match rule {
|
||||
Rule {
|
||||
os: Some(ref os), ..
|
||||
} => crate::util::platform::os_rule(os),
|
||||
Rule {
|
||||
features: Some(ref features),
|
||||
..
|
||||
} => features.has_demo_resolution.unwrap_or(false),
|
||||
_ => true,
|
||||
};
|
||||
|
||||
#[error("Failed to run processor: {0}")]
|
||||
ProcessorError(String),
|
||||
|
||||
#[error("Invalid input: {0}")]
|
||||
InvalidInput(String),
|
||||
|
||||
#[error("Error while managing asynchronous tasks")]
|
||||
TaskError(#[from] tokio::task::JoinError),
|
||||
|
||||
#[error("Error while reading/writing to the disk: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
#[error("Error while spawning child process {process}")]
|
||||
ProcessError {
|
||||
inner: std::io::Error,
|
||||
process: String,
|
||||
},
|
||||
|
||||
#[error("Error while deserializing JSON")]
|
||||
SerdeError(#[from] serde_json::Error),
|
||||
|
||||
#[error("Unable to fetch {item}")]
|
||||
FetchError { inner: reqwest::Error, item: String },
|
||||
|
||||
#[error("{0}")]
|
||||
ParseError(String),
|
||||
|
||||
#[error("Error while fetching metadata: {0}")]
|
||||
DaedalusError(#[from] daedalus::Error),
|
||||
|
||||
#[error("Error while reading metadata: {0}")]
|
||||
MetaError(#[from] crate::data::DataError),
|
||||
|
||||
#[error("Java error: {0}")]
|
||||
JavaError(String),
|
||||
|
||||
#[error("Command exited with non-zero exit code: {0}")]
|
||||
ExitError(i32),
|
||||
}
|
||||
|
||||
// TODO: this probably should be in crate::data
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Copy, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ModLoader {
|
||||
Vanilla,
|
||||
Forge,
|
||||
Fabric,
|
||||
}
|
||||
|
||||
impl Default for ModLoader {
|
||||
fn default() -> Self {
|
||||
ModLoader::Vanilla
|
||||
match rule.action {
|
||||
RuleAction::Allow => res,
|
||||
RuleAction::Disallow => !res,
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ModLoader {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let repr = match self {
|
||||
&Self::Vanilla => "Vanilla",
|
||||
&Self::Forge => "Forge",
|
||||
&Self::Fabric => "Fabric",
|
||||
};
|
||||
|
||||
f.write_str(repr)
|
||||
macro_rules! processor_rules {
|
||||
($dest:expr; $($name:literal : client => $client:expr, server => $server:expr;)+) => {
|
||||
$(std::collections::HashMap::insert(
|
||||
$dest,
|
||||
String::from($name),
|
||||
daedalus::modded::SidedDataEntry {
|
||||
client: String::from($client),
|
||||
server: String::from($server),
|
||||
},
|
||||
);)+
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn launch_minecraft(
|
||||
game_version: &str,
|
||||
loader_version: &Option<LoaderVersion>,
|
||||
root_dir: &Path,
|
||||
java: &Path,
|
||||
java_args: &Vec<String>,
|
||||
loader_version: &Option<d::modded::LoaderVersion>,
|
||||
instance_path: &Path,
|
||||
java_install: &Path,
|
||||
java_args: &[String],
|
||||
wrapper: &Option<String>,
|
||||
memory: &crate::data::profiles::MemorySettings,
|
||||
resolution: &crate::data::profiles::WindowSize,
|
||||
credentials: &Credentials,
|
||||
) -> Result<Child, LauncherError> {
|
||||
let (metadata, settings) = futures::try_join! {
|
||||
crate::data::Metadata::get(),
|
||||
crate::data::Settings::get(),
|
||||
}?;
|
||||
let root_dir = root_dir.canonicalize()?;
|
||||
let metadata_dir = &settings.metadata_dir;
|
||||
memory: &st::MemorySettings,
|
||||
resolution: &st::WindowSize,
|
||||
credentials: &auth::Credentials,
|
||||
) -> crate::Result<Child> {
|
||||
let state = st::State::get().await?;
|
||||
let instance_path = instance_path.canonicalize()?;
|
||||
|
||||
let (
|
||||
versions_path,
|
||||
libraries_path,
|
||||
assets_path,
|
||||
legacy_assets_path,
|
||||
natives_path,
|
||||
) = (
|
||||
metadata_dir.join("versions"),
|
||||
metadata_dir.join("libraries"),
|
||||
metadata_dir.join("assets"),
|
||||
metadata_dir.join("resources"),
|
||||
metadata_dir.join("natives"),
|
||||
);
|
||||
|
||||
let version = metadata
|
||||
let version = state
|
||||
.metadata
|
||||
.minecraft
|
||||
.versions
|
||||
.iter()
|
||||
.find(|it| it.id == game_version)
|
||||
.ok_or_else(|| {
|
||||
LauncherError::InvalidInput(format!(
|
||||
"Invalid game version: {game_version}",
|
||||
))
|
||||
})?;
|
||||
.ok_or(crate::Error::LauncherError(format!(
|
||||
"Invalid game version: {game_version}"
|
||||
)))?;
|
||||
|
||||
let version_jar = loader_version
|
||||
.as_ref()
|
||||
.map_or(version.id.clone(), |it| it.id.clone());
|
||||
|
||||
let mut version = download::download_version_info(
|
||||
&versions_path,
|
||||
version,
|
||||
let mut version_info = download::download_version_info(
|
||||
&state,
|
||||
&version,
|
||||
loader_version.as_ref(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let client_path = versions_path
|
||||
.join(&version.id)
|
||||
.join(format!("{}.jar", &version_jar));
|
||||
let version_natives_path = natives_path.join(&version.id);
|
||||
let client_path = state
|
||||
.directories
|
||||
.version_dir(&version.id)
|
||||
.join(format!("{version_jar}.jar"));
|
||||
|
||||
download_minecraft(
|
||||
&version,
|
||||
&versions_path,
|
||||
&assets_path,
|
||||
&legacy_assets_path,
|
||||
&libraries_path,
|
||||
&version_natives_path,
|
||||
)
|
||||
.await?;
|
||||
download::download_minecraft(&state, &version_info).await?;
|
||||
st::State::sync().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: game_version.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(),
|
||||
},
|
||||
);
|
||||
if let Some(processors) = &version_info.processors {
|
||||
if let Some(ref mut data) = version_info.data {
|
||||
processor_rules! {
|
||||
data;
|
||||
"SIDE":
|
||||
client => "client",
|
||||
server => "";
|
||||
"MINECRAFT_JAR" :
|
||||
client => client_path.to_string_lossy(),
|
||||
server => "";
|
||||
"MINECRAFT_VERSION":
|
||||
client => game_version,
|
||||
server => "";
|
||||
"ROOT":
|
||||
client => instance_path.to_string_lossy(),
|
||||
server => "";
|
||||
"LIBRARY_DIR":
|
||||
client => state.directories.libraries_dir().to_string_lossy(),
|
||||
server => "";
|
||||
}
|
||||
|
||||
for processor in processors {
|
||||
if let Some(sides) = &processor.sides {
|
||||
if !sides.contains(&"client".to_string()) {
|
||||
if !sides.contains(&String::from("client")) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -209,120 +120,93 @@ pub async fn launch_minecraft(
|
||||
|
||||
let child = Command::new("java")
|
||||
.arg("-cp")
|
||||
.arg(args::get_class_paths_jar(&libraries_path, &cp)?)
|
||||
.arg(args::get_class_paths_jar(
|
||||
&state.directories.libraries_dir(),
|
||||
&cp,
|
||||
)?)
|
||||
.arg(
|
||||
args::get_processor_main_class(args::get_lib_path(
|
||||
&libraries_path,
|
||||
&state.directories.libraries_dir(),
|
||||
&processor.jar,
|
||||
)?)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
LauncherError::ProcessorError(format!(
|
||||
crate::Error::LauncherError(format!(
|
||||
"Could not find processor main class for {}",
|
||||
processor.jar
|
||||
))
|
||||
})?,
|
||||
)
|
||||
.args(args::get_processor_arguments(
|
||||
&libraries_path,
|
||||
&state.directories.libraries_dir(),
|
||||
&processor.args,
|
||||
data,
|
||||
)?)
|
||||
.output()
|
||||
.await
|
||||
.map_err(|err| LauncherError::ProcessError {
|
||||
inner: err,
|
||||
process: "java".to_string(),
|
||||
.map_err(|err| {
|
||||
crate::Error::LauncherError(format!(
|
||||
"Error running processor: {err}",
|
||||
))
|
||||
})?;
|
||||
|
||||
if !child.status.success() {
|
||||
return Err(LauncherError::ProcessorError(
|
||||
String::from_utf8_lossy(&child.stderr).to_string(),
|
||||
));
|
||||
return Err(crate::Error::LauncherError(format!(
|
||||
"Processor error: {}",
|
||||
String::from_utf8_lossy(&child.stderr)
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let arguments = version.arguments.clone().unwrap_or_default();
|
||||
let args = version_info.arguments.clone().unwrap_or_default();
|
||||
let mut command = match wrapper {
|
||||
Some(hook) => {
|
||||
let mut cmd = Command::new(hook);
|
||||
cmd.arg(java);
|
||||
cmd.arg(java_install);
|
||||
cmd
|
||||
}
|
||||
None => Command::new(java.to_string_lossy().to_string()),
|
||||
None => Command::new(String::from(java_install.to_string_lossy())),
|
||||
};
|
||||
|
||||
command
|
||||
.args(args::get_jvm_arguments(
|
||||
arguments.get(&ArgumentType::Jvm).map(|x| x.as_slice()),
|
||||
&version_natives_path,
|
||||
&libraries_path,
|
||||
args.get(&d::minecraft::ArgumentType::Jvm)
|
||||
.map(|x| x.as_slice()),
|
||||
&state.directories.version_natives_dir(&version.id),
|
||||
&state.directories.libraries_dir(),
|
||||
&args::get_class_paths(
|
||||
&libraries_path,
|
||||
version.libraries.as_slice(),
|
||||
&state.directories.libraries_dir(),
|
||||
version_info.libraries.as_slice(),
|
||||
&client_path,
|
||||
)?,
|
||||
&version_jar,
|
||||
*memory,
|
||||
java_args.clone(),
|
||||
Vec::from(java_args),
|
||||
)?)
|
||||
.arg(version.main_class.clone())
|
||||
.arg(version_info.main_class.clone())
|
||||
.args(args::get_minecraft_arguments(
|
||||
arguments.get(&ArgumentType::Game).map(|x| x.as_slice()),
|
||||
version.minecraft_arguments.as_deref(),
|
||||
args.get(&d::minecraft::ArgumentType::Game)
|
||||
.map(|x| x.as_slice()),
|
||||
version_info.minecraft_arguments.as_deref(),
|
||||
credentials,
|
||||
&version.id,
|
||||
&version.asset_index.id,
|
||||
&root_dir,
|
||||
&assets_path,
|
||||
&version_info.asset_index.id,
|
||||
&instance_path,
|
||||
&state.directories.assets_dir(),
|
||||
&version.type_,
|
||||
*resolution,
|
||||
)?)
|
||||
.current_dir(root_dir.clone())
|
||||
.current_dir(instance_path.clone())
|
||||
.stdout(Stdio::inherit())
|
||||
.stderr(Stdio::inherit());
|
||||
|
||||
command.spawn().map_err(|err| LauncherError::ProcessError {
|
||||
inner: err,
|
||||
process: format!("minecraft-{} @ {}", &version.id, root_dir.display()),
|
||||
command.spawn().map_err(|err| {
|
||||
crate::Error::LauncherError(format!(
|
||||
"Error running Minecraft (minecraft-{} @ {}): {err}",
|
||||
&version.id,
|
||||
instance_path.display()
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn download_minecraft(
|
||||
version: &VersionInfo,
|
||||
versions_dir: &Path,
|
||||
assets_dir: &Path,
|
||||
legacy_assets_dir: &Path,
|
||||
libraries_dir: &Path,
|
||||
natives_dir: &Path,
|
||||
) -> Result<(), LauncherError> {
|
||||
let assets_index =
|
||||
download::download_assets_index(assets_dir, version).await?;
|
||||
|
||||
let (a, b, c) = futures::future::join3(
|
||||
download::download_client(versions_dir, version),
|
||||
download::download_assets(
|
||||
assets_dir,
|
||||
if version.assets == "legacy" {
|
||||
Some(legacy_assets_dir)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
&assets_index,
|
||||
),
|
||||
download::download_libraries(
|
||||
libraries_dir,
|
||||
natives_dir,
|
||||
version.libraries.as_slice(),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
a?;
|
||||
b?;
|
||||
c?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
use crate::launcher::download::get_os;
|
||||
use daedalus::minecraft::{OsRule, Rule, RuleAction};
|
||||
use regex::Regex;
|
||||
|
||||
pub fn parse_rules(rules: &[Rule]) -> bool {
|
||||
rules.iter().all(|x| parse_rule(x))
|
||||
}
|
||||
|
||||
pub fn parse_rule(rule: &Rule) -> bool {
|
||||
let result = if let Some(os) = &rule.os {
|
||||
parse_os_rule(os)
|
||||
} else if let Some(features) = &rule.features {
|
||||
features.has_demo_resolution.unwrap_or(false)
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
match rule.action {
|
||||
RuleAction::Allow => result,
|
||||
RuleAction::Disallow => !result,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_os_rule(rule: &OsRule) -> bool {
|
||||
if let Some(arch) = &rule.arch {
|
||||
match arch.as_str() {
|
||||
"x86" => {
|
||||
#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
|
||||
return false;
|
||||
}
|
||||
"arm" => {
|
||||
#[cfg(not(target_arch = "arm"))]
|
||||
return false;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(name) = &rule.name {
|
||||
if &get_os() != name {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if let Some(version) = &rule.version {
|
||||
let regex = Regex::new(version.as_str());
|
||||
|
||||
if let Ok(regex) = regex {
|
||||
if !regex.is_match(&sys_info::os_release().unwrap_or_default()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
Reference in New Issue
Block a user