You've already forked AstralRinth
forked from didirus/AstralRinth
Migrate to Turborepo (#1251)
This commit is contained in:
436
packages/app-lib/src/launcher/args.rs
Normal file
436
packages/app-lib/src/launcher/args.rs
Normal file
@@ -0,0 +1,436 @@
|
||||
//! Minecraft CLI argument logic
|
||||
use crate::launcher::parse_rules;
|
||||
use crate::state::Credentials;
|
||||
use crate::{
|
||||
state::{MemorySettings, WindowSize},
|
||||
util::{io::IOError, platform::classpath_separator},
|
||||
};
|
||||
use daedalus::{
|
||||
get_path_from_artifact,
|
||||
minecraft::{Argument, ArgumentValue, Library, VersionType},
|
||||
modded::SidedDataEntry,
|
||||
};
|
||||
use dunce::canonicalize;
|
||||
use std::collections::HashSet;
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::{collections::HashMap, path::Path};
|
||||
use uuid::Uuid;
|
||||
|
||||
// Replaces the space separator with a newline character, as to not split the arguments
|
||||
const TEMPORARY_REPLACE_CHAR: &str = "\n";
|
||||
|
||||
pub fn get_class_paths(
|
||||
libraries_path: &Path,
|
||||
libraries: &[Library],
|
||||
client_path: &Path,
|
||||
java_arch: &str,
|
||||
minecraft_updated: bool,
|
||||
) -> crate::Result<String> {
|
||||
let mut cps = libraries
|
||||
.iter()
|
||||
.filter_map(|library| {
|
||||
if let Some(rules) = &library.rules {
|
||||
if !parse_rules(rules, java_arch, minecraft_updated) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
if !library.include_in_classpath {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(get_lib_path(libraries_path, &library.name, false))
|
||||
})
|
||||
.collect::<Result<HashSet<_>, _>>()?;
|
||||
|
||||
cps.insert(
|
||||
canonicalize(client_path)
|
||||
.map_err(|_| {
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Specified class path {} does not exist",
|
||||
client_path.to_string_lossy()
|
||||
))
|
||||
.as_error()
|
||||
})?
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
Ok(cps
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>()
|
||||
.join(classpath_separator(java_arch)))
|
||||
}
|
||||
|
||||
pub fn get_class_paths_jar<T: AsRef<str>>(
|
||||
libraries_path: &Path,
|
||||
libraries: &[T],
|
||||
java_arch: &str,
|
||||
) -> crate::Result<String> {
|
||||
let cps = libraries
|
||||
.iter()
|
||||
.map(|library| get_lib_path(libraries_path, library.as_ref(), false))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
Ok(cps.join(classpath_separator(java_arch)))
|
||||
}
|
||||
|
||||
pub fn get_lib_path(
|
||||
libraries_path: &Path,
|
||||
lib: &str,
|
||||
allow_not_exist: bool,
|
||||
) -> crate::Result<String> {
|
||||
let mut path = libraries_path.to_path_buf();
|
||||
|
||||
path.push(get_path_from_artifact(lib)?);
|
||||
|
||||
if !path.exists() && allow_not_exist {
|
||||
return Ok(path.to_string_lossy().to_string());
|
||||
}
|
||||
|
||||
let path = &canonicalize(&path).map_err(|_| {
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Library file at path {} does not exist",
|
||||
path.to_string_lossy()
|
||||
))
|
||||
.as_error()
|
||||
})?;
|
||||
|
||||
Ok(path.to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn get_jvm_arguments(
|
||||
arguments: Option<&[Argument]>,
|
||||
natives_path: &Path,
|
||||
libraries_path: &Path,
|
||||
class_paths: &str,
|
||||
version_name: &str,
|
||||
memory: MemorySettings,
|
||||
custom_args: Vec<String>,
|
||||
java_arch: &str,
|
||||
) -> crate::Result<Vec<String>> {
|
||||
let mut parsed_arguments = Vec::new();
|
||||
|
||||
if let Some(args) = arguments {
|
||||
parse_arguments(
|
||||
args,
|
||||
&mut parsed_arguments,
|
||||
|arg| {
|
||||
parse_jvm_argument(
|
||||
arg.to_string(),
|
||||
natives_path,
|
||||
libraries_path,
|
||||
class_paths,
|
||||
version_name,
|
||||
java_arch,
|
||||
)
|
||||
},
|
||||
java_arch,
|
||||
)?;
|
||||
} else {
|
||||
parsed_arguments.push(format!(
|
||||
"-Djava.library.path={}",
|
||||
canonicalize(natives_path)
|
||||
.map_err(|_| crate::ErrorKind::LauncherError(format!(
|
||||
"Specified natives path {} does not exist",
|
||||
natives_path.to_string_lossy()
|
||||
))
|
||||
.as_error())?
|
||||
.to_string_lossy()
|
||||
));
|
||||
parsed_arguments.push("-cp".to_string());
|
||||
parsed_arguments.push(class_paths.to_string());
|
||||
}
|
||||
parsed_arguments.push(format!("-Xmx{}M", memory.maximum));
|
||||
for arg in custom_args {
|
||||
if !arg.is_empty() {
|
||||
parsed_arguments.push(arg);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(parsed_arguments)
|
||||
}
|
||||
|
||||
fn parse_jvm_argument(
|
||||
mut argument: String,
|
||||
natives_path: &Path,
|
||||
libraries_path: &Path,
|
||||
class_paths: &str,
|
||||
version_name: &str,
|
||||
java_arch: &str,
|
||||
) -> crate::Result<String> {
|
||||
argument.retain(|c| !c.is_whitespace());
|
||||
Ok(argument
|
||||
.replace(
|
||||
"${natives_directory}",
|
||||
&canonicalize(natives_path)
|
||||
.map_err(|_| {
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Specified natives path {} does not exist",
|
||||
natives_path.to_string_lossy()
|
||||
))
|
||||
.as_error()
|
||||
})?
|
||||
.to_string_lossy(),
|
||||
)
|
||||
.replace(
|
||||
"${library_directory}",
|
||||
&canonicalize(libraries_path)
|
||||
.map_err(|_| {
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Specified libraries path {} does not exist",
|
||||
libraries_path.to_string_lossy()
|
||||
))
|
||||
.as_error()
|
||||
})?
|
||||
.to_string_lossy(),
|
||||
)
|
||||
.replace("${classpath_separator}", classpath_separator(java_arch))
|
||||
.replace("${launcher_name}", "theseus")
|
||||
.replace("${launcher_version}", env!("CARGO_PKG_VERSION"))
|
||||
.replace("${version_name}", version_name)
|
||||
.replace("${classpath}", class_paths))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn get_minecraft_arguments(
|
||||
arguments: Option<&[Argument]>,
|
||||
legacy_arguments: Option<&str>,
|
||||
credentials: &Credentials,
|
||||
version: &str,
|
||||
asset_index_name: &str,
|
||||
game_directory: &Path,
|
||||
assets_directory: &Path,
|
||||
version_type: &VersionType,
|
||||
resolution: WindowSize,
|
||||
java_arch: &str,
|
||||
) -> crate::Result<Vec<String>> {
|
||||
if let Some(arguments) = arguments {
|
||||
let mut parsed_arguments = Vec::new();
|
||||
|
||||
parse_arguments(
|
||||
arguments,
|
||||
&mut parsed_arguments,
|
||||
|arg| {
|
||||
parse_minecraft_argument(
|
||||
arg,
|
||||
&credentials.access_token,
|
||||
&credentials.username,
|
||||
credentials.id,
|
||||
version,
|
||||
asset_index_name,
|
||||
game_directory,
|
||||
assets_directory,
|
||||
version_type,
|
||||
resolution,
|
||||
)
|
||||
},
|
||||
java_arch,
|
||||
)?;
|
||||
|
||||
Ok(parsed_arguments)
|
||||
} else if let Some(legacy_arguments) = legacy_arguments {
|
||||
let mut parsed_arguments = Vec::new();
|
||||
for x in legacy_arguments.split(' ') {
|
||||
parsed_arguments.push(parse_minecraft_argument(
|
||||
&x.replace(' ', TEMPORARY_REPLACE_CHAR),
|
||||
&credentials.access_token,
|
||||
&credentials.username,
|
||||
credentials.id,
|
||||
version,
|
||||
asset_index_name,
|
||||
game_directory,
|
||||
assets_directory,
|
||||
version_type,
|
||||
resolution,
|
||||
)?);
|
||||
}
|
||||
Ok(parsed_arguments)
|
||||
} else {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn parse_minecraft_argument(
|
||||
argument: &str,
|
||||
access_token: &str,
|
||||
username: &str,
|
||||
uuid: Uuid,
|
||||
version: &str,
|
||||
asset_index_name: &str,
|
||||
game_directory: &Path,
|
||||
assets_directory: &Path,
|
||||
version_type: &VersionType,
|
||||
resolution: WindowSize,
|
||||
) -> crate::Result<String> {
|
||||
Ok(argument
|
||||
.replace("${accessToken}", access_token)
|
||||
.replace("${auth_access_token}", access_token)
|
||||
.replace("${auth_session}", access_token)
|
||||
.replace("${auth_player_name}", username)
|
||||
// TODO: add auth xuid eventually
|
||||
.replace("${auth_xuid}", "0")
|
||||
.replace("${auth_uuid}", &uuid.simple().to_string())
|
||||
.replace("${uuid}", &uuid.simple().to_string())
|
||||
.replace("${clientid}", "c4502edb-87c6-40cb-b595-64a280cf8906")
|
||||
.replace("${user_properties}", "{}")
|
||||
.replace("${user_type}", "msa")
|
||||
.replace("${version_name}", version)
|
||||
.replace("${assets_index_name}", asset_index_name)
|
||||
.replace(
|
||||
"${game_directory}",
|
||||
&canonicalize(game_directory)
|
||||
.map_err(|_| {
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Specified game directory {} does not exist",
|
||||
game_directory.to_string_lossy()
|
||||
))
|
||||
.as_error()
|
||||
})?
|
||||
.to_string_lossy(),
|
||||
)
|
||||
.replace(
|
||||
"${assets_root}",
|
||||
&canonicalize(assets_directory)
|
||||
.map_err(|_| {
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Specified assets directory {} does not exist",
|
||||
assets_directory.to_string_lossy()
|
||||
))
|
||||
.as_error()
|
||||
})?
|
||||
.to_string_lossy(),
|
||||
)
|
||||
.replace(
|
||||
"${game_assets}",
|
||||
&canonicalize(assets_directory)
|
||||
.map_err(|_| {
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Specified assets directory {} does not exist",
|
||||
assets_directory.to_string_lossy()
|
||||
))
|
||||
.as_error()
|
||||
})?
|
||||
.to_string_lossy(),
|
||||
)
|
||||
.replace("${version_type}", version_type.as_str())
|
||||
.replace("${resolution_width}", &resolution.0.to_string())
|
||||
.replace("${resolution_height}", &resolution.1.to_string()))
|
||||
}
|
||||
|
||||
fn parse_arguments<F>(
|
||||
arguments: &[Argument],
|
||||
parsed_arguments: &mut Vec<String>,
|
||||
parse_function: F,
|
||||
java_arch: &str,
|
||||
) -> crate::Result<()>
|
||||
where
|
||||
F: Fn(&str) -> crate::Result<String>,
|
||||
{
|
||||
for argument in arguments {
|
||||
match argument {
|
||||
Argument::Normal(arg) => {
|
||||
let parsed =
|
||||
parse_function(&arg.replace(' ', TEMPORARY_REPLACE_CHAR))?;
|
||||
for arg in parsed.split(TEMPORARY_REPLACE_CHAR) {
|
||||
parsed_arguments.push(arg.to_string());
|
||||
}
|
||||
}
|
||||
Argument::Ruled { rules, value } => {
|
||||
if parse_rules(rules, java_arch, true) {
|
||||
match value {
|
||||
ArgumentValue::Single(arg) => {
|
||||
parsed_arguments.push(parse_function(
|
||||
&arg.replace(' ', TEMPORARY_REPLACE_CHAR),
|
||||
)?);
|
||||
}
|
||||
ArgumentValue::Many(args) => {
|
||||
for arg in args {
|
||||
parsed_arguments.push(parse_function(
|
||||
&arg.replace(' ', TEMPORARY_REPLACE_CHAR),
|
||||
)?);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_processor_arguments<T: AsRef<str>>(
|
||||
libraries_path: &Path,
|
||||
arguments: &[T],
|
||||
data: &HashMap<String, SidedDataEntry>,
|
||||
) -> crate::Result<Vec<String>> {
|
||||
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],
|
||||
true,
|
||||
)?
|
||||
} else {
|
||||
entry.client.clone()
|
||||
})
|
||||
}
|
||||
} else if argument.as_ref().starts_with('[') {
|
||||
new_arguments.push(get_lib_path(libraries_path, trimmed_arg, true)?)
|
||||
} else {
|
||||
new_arguments.push(argument.as_ref().to_string())
|
||||
}
|
||||
}
|
||||
|
||||
Ok(new_arguments)
|
||||
}
|
||||
|
||||
pub async fn get_processor_main_class(
|
||||
path: String,
|
||||
) -> crate::Result<Option<String>> {
|
||||
let main_class = tokio::task::spawn_blocking(move || {
|
||||
let zipfile = std::fs::File::open(&path)
|
||||
.map_err(|e| IOError::with_path(e, &path))?;
|
||||
let mut archive = zip::ZipArchive::new(zipfile).map_err(|_| {
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Cannot read processor at {}",
|
||||
path
|
||||
))
|
||||
.as_error()
|
||||
})?;
|
||||
|
||||
let file = archive.by_name("META-INF/MANIFEST.MF").map_err(|_| {
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Cannot read processor manifest at {}",
|
||||
path
|
||||
))
|
||||
.as_error()
|
||||
})?;
|
||||
|
||||
let reader = BufReader::new(file);
|
||||
|
||||
for line in reader.lines() {
|
||||
let mut line = line.map_err(IOError::from)?;
|
||||
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>, crate::Error>(None)
|
||||
})
|
||||
.await??;
|
||||
|
||||
Ok(main_class)
|
||||
}
|
||||
353
packages/app-lib/src/launcher/download.rs
Normal file
353
packages/app-lib/src/launcher/download.rs
Normal file
@@ -0,0 +1,353 @@
|
||||
//! Downloader for Minecraft data
|
||||
|
||||
use crate::launcher::parse_rules;
|
||||
use crate::state::CredentialsStore;
|
||||
use crate::{
|
||||
event::{
|
||||
emit::{emit_loading, loading_try_for_each_concurrent},
|
||||
LoadingBarId,
|
||||
},
|
||||
state::State,
|
||||
util::{fetch::*, io, platform::OsExt},
|
||||
};
|
||||
use daedalus::{
|
||||
self as d,
|
||||
minecraft::{
|
||||
Asset, AssetsIndex, Library, Os, Version as GameVersion,
|
||||
VersionInfo as GameVersionInfo,
|
||||
},
|
||||
modded::LoaderVersion,
|
||||
};
|
||||
use futures::prelude::*;
|
||||
use tokio::sync::OnceCell;
|
||||
|
||||
#[tracing::instrument(skip(st, version))]
|
||||
pub async fn download_minecraft(
|
||||
st: &State,
|
||||
version: &GameVersionInfo,
|
||||
loading_bar: &LoadingBarId,
|
||||
java_arch: &str,
|
||||
force: bool,
|
||||
minecraft_updated: bool,
|
||||
) -> crate::Result<()> {
|
||||
tracing::info!("Downloading Minecraft version {}", version.id);
|
||||
// 5
|
||||
let assets_index =
|
||||
download_assets_index(st, version, Some(loading_bar), force).await?;
|
||||
|
||||
let amount = if version
|
||||
.processors
|
||||
.as_ref()
|
||||
.map(|x| !x.is_empty())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
25.0
|
||||
} else {
|
||||
40.0
|
||||
};
|
||||
|
||||
tokio::try_join! {
|
||||
// Total loading sums to 90/60
|
||||
download_client(st, version, Some(loading_bar), force), // 10
|
||||
download_assets(st, version.assets == "legacy", &assets_index, Some(loading_bar), amount, force), // 40
|
||||
download_libraries(st, version.libraries.as_slice(), &version.id, Some(loading_bar), amount, java_arch, force, minecraft_updated) // 40
|
||||
}?;
|
||||
|
||||
tracing::info!("Done downloading Minecraft!");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all, fields(version = version.id.as_str(), loader = ?loader))]
|
||||
#[theseus_macros::debug_pin]
|
||||
pub async fn download_version_info(
|
||||
st: &State,
|
||||
version: &GameVersion,
|
||||
loader: Option<&LoaderVersion>,
|
||||
force: Option<bool>,
|
||||
loading_bar: Option<&LoadingBarId>,
|
||||
) -> crate::Result<GameVersionInfo> {
|
||||
let version_id = loader
|
||||
.map_or(version.id.clone(), |it| format!("{}-{}", version.id, it.id));
|
||||
tracing::debug!("Loading version info for Minecraft {version_id}");
|
||||
let path = st
|
||||
.directories
|
||||
.version_dir(&version_id)
|
||||
.await
|
||||
.join(format!("{version_id}.json"));
|
||||
|
||||
let res = if path.exists() && !force.unwrap_or(false) {
|
||||
io::read(path)
|
||||
.err_into::<crate::Error>()
|
||||
.await
|
||||
.and_then(|ref it| Ok(serde_json::from_slice(it)?))
|
||||
} else {
|
||||
tracing::info!("Downloading version info for version {}", &version.id);
|
||||
let mut info = d::minecraft::fetch_version_info(version).await?;
|
||||
|
||||
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 = version_id.clone();
|
||||
|
||||
write(&path, &serde_json::to_vec(&info)?, &st.io_semaphore).await?;
|
||||
Ok(info)
|
||||
}?;
|
||||
|
||||
if let Some(loading_bar) = loading_bar {
|
||||
emit_loading(loading_bar, 5.0, None).await?;
|
||||
}
|
||||
|
||||
tracing::debug!("Loaded version info for Minecraft {version_id}");
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
#[theseus_macros::debug_pin]
|
||||
pub async fn download_client(
|
||||
st: &State,
|
||||
version_info: &GameVersionInfo,
|
||||
loading_bar: Option<&LoadingBarId>,
|
||||
force: bool,
|
||||
) -> crate::Result<()> {
|
||||
let version = &version_info.id;
|
||||
tracing::debug!("Locating client for version {version}");
|
||||
let client_download = version_info
|
||||
.downloads
|
||||
.get(&d::minecraft::DownloadType::Client)
|
||||
.ok_or(
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"No client downloads exist for version {version}"
|
||||
))
|
||||
.as_error(),
|
||||
)?;
|
||||
let path = st
|
||||
.directories
|
||||
.version_dir(version)
|
||||
.await
|
||||
.join(format!("{version}.jar"));
|
||||
|
||||
if !path.exists() || force {
|
||||
let bytes = fetch(
|
||||
&client_download.url,
|
||||
Some(&client_download.sha1),
|
||||
&st.fetch_semaphore,
|
||||
&CredentialsStore(None),
|
||||
)
|
||||
.await?;
|
||||
write(&path, &bytes, &st.io_semaphore).await?;
|
||||
tracing::trace!("Fetched client version {version}");
|
||||
}
|
||||
if let Some(loading_bar) = loading_bar {
|
||||
emit_loading(loading_bar, 9.0, None).await?;
|
||||
}
|
||||
|
||||
tracing::debug!("Client loaded for version {version}!");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
#[theseus_macros::debug_pin]
|
||||
pub async fn download_assets_index(
|
||||
st: &State,
|
||||
version: &GameVersionInfo,
|
||||
loading_bar: Option<&LoadingBarId>,
|
||||
force: bool,
|
||||
) -> crate::Result<AssetsIndex> {
|
||||
tracing::debug!("Loading assets index");
|
||||
let path = st
|
||||
.directories
|
||||
.assets_index_dir()
|
||||
.await
|
||||
.join(format!("{}.json", &version.asset_index.id));
|
||||
|
||||
let res = if path.exists() && !force {
|
||||
io::read(path)
|
||||
.err_into::<crate::Error>()
|
||||
.await
|
||||
.and_then(|ref it| Ok(serde_json::from_slice(it)?))
|
||||
} else {
|
||||
let index = d::minecraft::fetch_assets_index(version).await?;
|
||||
write(&path, &serde_json::to_vec(&index)?, &st.io_semaphore).await?;
|
||||
tracing::info!("Fetched assets index");
|
||||
Ok(index)
|
||||
}?;
|
||||
|
||||
if let Some(loading_bar) = loading_bar {
|
||||
emit_loading(loading_bar, 5.0, None).await?;
|
||||
}
|
||||
tracing::debug!("Assets index successfully loaded!");
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(st, index))]
|
||||
#[theseus_macros::debug_pin]
|
||||
pub async fn download_assets(
|
||||
st: &State,
|
||||
with_legacy: bool,
|
||||
index: &AssetsIndex,
|
||||
loading_bar: Option<&LoadingBarId>,
|
||||
loading_amount: f64,
|
||||
force: bool,
|
||||
) -> crate::Result<()> {
|
||||
tracing::debug!("Loading assets");
|
||||
let num_futs = index.objects.len();
|
||||
let assets = stream::iter(index.objects.iter())
|
||||
.map(Ok::<(&String, &Asset), crate::Error>);
|
||||
|
||||
loading_try_for_each_concurrent(assets,
|
||||
None,
|
||||
loading_bar,
|
||||
loading_amount,
|
||||
num_futs,
|
||||
None,
|
||||
|(name, asset)| async move {
|
||||
let hash = &asset.hash;
|
||||
let resource_path = st.directories.object_dir(hash).await;
|
||||
let url = format!(
|
||||
"https://resources.download.minecraft.net/{sub_hash}/{hash}",
|
||||
sub_hash = &hash[..2]
|
||||
);
|
||||
|
||||
let fetch_cell = OnceCell::<bytes::Bytes>::new();
|
||||
tokio::try_join! {
|
||||
async {
|
||||
if !resource_path.exists() || force {
|
||||
let resource = fetch_cell
|
||||
.get_or_try_init(|| fetch(&url, Some(hash), &st.fetch_semaphore, &CredentialsStore(None)))
|
||||
.await?;
|
||||
write(&resource_path, resource, &st.io_semaphore).await?;
|
||||
tracing::trace!("Fetched asset with hash {hash}");
|
||||
}
|
||||
Ok::<_, crate::Error>(())
|
||||
},
|
||||
async {
|
||||
let resource_path = st.directories.legacy_assets_dir().await.join(
|
||||
name.replace('/', &String::from(std::path::MAIN_SEPARATOR))
|
||||
);
|
||||
|
||||
if with_legacy && !resource_path.exists() || force {
|
||||
let resource = fetch_cell
|
||||
.get_or_try_init(|| fetch(&url, Some(hash), &st.fetch_semaphore, &CredentialsStore(None)))
|
||||
.await?;
|
||||
write(&resource_path, resource, &st.io_semaphore).await?;
|
||||
tracing::trace!("Fetched legacy asset with hash {hash}");
|
||||
}
|
||||
Ok::<_, crate::Error>(())
|
||||
},
|
||||
}?;
|
||||
|
||||
tracing::trace!("Loaded asset with hash {hash}");
|
||||
Ok(())
|
||||
}).await?;
|
||||
tracing::debug!("Done loading assets!");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(st, libraries))]
|
||||
#[theseus_macros::debug_pin]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn download_libraries(
|
||||
st: &State,
|
||||
libraries: &[Library],
|
||||
version: &str,
|
||||
loading_bar: Option<&LoadingBarId>,
|
||||
loading_amount: f64,
|
||||
java_arch: &str,
|
||||
force: bool,
|
||||
minecraft_updated: bool,
|
||||
) -> crate::Result<()> {
|
||||
tracing::debug!("Loading libraries");
|
||||
|
||||
tokio::try_join! {
|
||||
io::create_dir_all(st.directories.libraries_dir().await),
|
||||
io::create_dir_all(st.directories.version_natives_dir(version).await)
|
||||
}?;
|
||||
let num_files = libraries.len();
|
||||
loading_try_for_each_concurrent(
|
||||
stream::iter(libraries.iter())
|
||||
.map(Ok::<&Library, crate::Error>), None, loading_bar,loading_amount,num_files, None,|library| async move {
|
||||
if let Some(rules) = &library.rules {
|
||||
if !parse_rules(rules, java_arch, minecraft_updated) {
|
||||
tracing::trace!("Skipped library {}", &library.name);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
tokio::try_join! {
|
||||
async {
|
||||
let artifact_path = d::get_path_from_artifact(&library.name)?;
|
||||
let path = st.directories.libraries_dir().await.join(&artifact_path);
|
||||
|
||||
match library.downloads {
|
||||
_ if path.exists() && !force => Ok(()),
|
||||
Some(d::minecraft::LibraryDownloads {
|
||||
artifact: Some(ref artifact),
|
||||
..
|
||||
}) => {
|
||||
let bytes = fetch(&artifact.url, Some(&artifact.sha1), &st.fetch_semaphore, &CredentialsStore(None))
|
||||
.await?;
|
||||
write(&path, &bytes, &st.io_semaphore).await?;
|
||||
tracing::trace!("Fetched library {} to path {:?}", &library.name, &path);
|
||||
Ok::<_, crate::Error>(())
|
||||
}
|
||||
_ => {
|
||||
let url = [
|
||||
library
|
||||
.url
|
||||
.as_deref()
|
||||
.unwrap_or("https://libraries.minecraft.net/"),
|
||||
&artifact_path
|
||||
].concat();
|
||||
|
||||
let bytes = fetch(&url, None, &st.fetch_semaphore, &CredentialsStore(None)).await?;
|
||||
write(&path, &bytes, &st.io_semaphore).await?;
|
||||
tracing::trace!("Fetched library {} to path {:?}", &library.name, &path);
|
||||
Ok::<_, crate::Error>(())
|
||||
}
|
||||
}
|
||||
},
|
||||
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_arch(java_arch))?,
|
||||
library
|
||||
.downloads
|
||||
.as_ref()?
|
||||
.classifiers
|
||||
.as_ref()?
|
||||
))) {
|
||||
let parsed_key = os_key.replace(
|
||||
"${arch}",
|
||||
crate::util::platform::ARCH_WIDTH,
|
||||
);
|
||||
|
||||
if let Some(native) = classifiers.get(&parsed_key) {
|
||||
let data = fetch(&native.url, Some(&native.sha1), &st.fetch_semaphore, &CredentialsStore(None)).await?;
|
||||
let reader = std::io::Cursor::new(&data);
|
||||
if let Ok(mut archive) = zip::ZipArchive::new(reader) {
|
||||
match archive.extract(st.directories.version_natives_dir(version).await) {
|
||||
Ok(_) => tracing::debug!("Fetched native {}", &library.name),
|
||||
Err(err) => tracing::error!("Failed extracting native {}. err: {}", &library.name, err)
|
||||
}
|
||||
} else {
|
||||
tracing::error!("Failed extracting native {}", &library.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}?;
|
||||
|
||||
tracing::debug!("Loaded library {}", library.name);
|
||||
Ok(())
|
||||
}
|
||||
).await?;
|
||||
|
||||
tracing::debug!("Done loading libraries!");
|
||||
Ok(())
|
||||
}
|
||||
633
packages/app-lib/src/launcher/mod.rs
Normal file
633
packages/app-lib/src/launcher/mod.rs
Normal file
@@ -0,0 +1,633 @@
|
||||
//! Logic for launching Minecraft
|
||||
use crate::event::emit::{emit_loading, init_or_edit_loading};
|
||||
use crate::event::{LoadingBarId, LoadingBarType};
|
||||
use crate::launcher::io::IOError;
|
||||
use crate::prelude::JavaVersion;
|
||||
use crate::state::{Credentials, ProfileInstallStage};
|
||||
use crate::util::io;
|
||||
use crate::{
|
||||
process,
|
||||
state::{self as st, MinecraftChild},
|
||||
State,
|
||||
};
|
||||
use chrono::Utc;
|
||||
use daedalus as d;
|
||||
use daedalus::minecraft::{RuleAction, VersionInfo};
|
||||
use st::Profile;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tokio::process::Command;
|
||||
use uuid::Uuid;
|
||||
|
||||
mod args;
|
||||
|
||||
pub mod download;
|
||||
|
||||
// All nones -> disallowed
|
||||
// 1+ true -> allowed
|
||||
// 1+ false -> disallowed
|
||||
#[tracing::instrument]
|
||||
pub fn parse_rules(
|
||||
rules: &[d::minecraft::Rule],
|
||||
java_version: &str,
|
||||
minecraft_updated: bool,
|
||||
) -> bool {
|
||||
let mut x = rules
|
||||
.iter()
|
||||
.map(|x| parse_rule(x, java_version, minecraft_updated))
|
||||
.collect::<Vec<Option<bool>>>();
|
||||
|
||||
if rules
|
||||
.iter()
|
||||
.all(|x| matches!(x.action, RuleAction::Disallow))
|
||||
{
|
||||
x.push(Some(true))
|
||||
}
|
||||
|
||||
!(x.iter().any(|x| x == &Some(false)) || x.iter().all(|x| x.is_none()))
|
||||
}
|
||||
|
||||
// if anything is disallowed, it should NOT be included
|
||||
// if anything is not disallowed, it shouldn't factor in final result
|
||||
// if anything is not allowed, it shouldn't factor in final result
|
||||
// if anything is allowed, it should be included
|
||||
#[tracing::instrument]
|
||||
pub fn parse_rule(
|
||||
rule: &d::minecraft::Rule,
|
||||
java_version: &str,
|
||||
minecraft_updated: bool,
|
||||
) -> Option<bool> {
|
||||
use d::minecraft::{Rule, RuleAction};
|
||||
|
||||
let res = match rule {
|
||||
Rule {
|
||||
os: Some(ref os), ..
|
||||
} => {
|
||||
crate::util::platform::os_rule(os, java_version, minecraft_updated)
|
||||
}
|
||||
Rule {
|
||||
features: Some(ref features),
|
||||
..
|
||||
} => {
|
||||
!features.is_demo_user.unwrap_or(true)
|
||||
|| features.has_custom_resolution.unwrap_or(false)
|
||||
|| !features.has_quick_plays_support.unwrap_or(true)
|
||||
|| !features.is_quick_play_multiplayer.unwrap_or(true)
|
||||
|| !features.is_quick_play_realms.unwrap_or(true)
|
||||
|| !features.is_quick_play_singleplayer.unwrap_or(true)
|
||||
}
|
||||
_ => return Some(true),
|
||||
};
|
||||
|
||||
match rule.action {
|
||||
RuleAction::Allow => {
|
||||
if res {
|
||||
Some(true)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
RuleAction::Disallow => {
|
||||
if res {
|
||||
Some(false)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 get_java_version_from_profile(
|
||||
profile: &Profile,
|
||||
version_info: &VersionInfo,
|
||||
) -> crate::Result<Option<JavaVersion>> {
|
||||
if let Some(java) = profile.java.clone().and_then(|x| x.override_version) {
|
||||
Ok(Some(java))
|
||||
} else {
|
||||
let key = version_info
|
||||
.java_version
|
||||
.as_ref()
|
||||
.map(|it| it.major_version)
|
||||
.unwrap_or(8);
|
||||
|
||||
let state = State::get().await?;
|
||||
let settings = state.settings.read().await;
|
||||
|
||||
if let Some(java) = settings.java_globals.get(&format!("JAVA_{key}")) {
|
||||
return Ok(Some(java.clone()));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(profile))]
|
||||
#[theseus_macros::debug_pin]
|
||||
pub async fn install_minecraft(
|
||||
profile: &Profile,
|
||||
existing_loading_bar: Option<LoadingBarId>,
|
||||
repairing: bool,
|
||||
) -> crate::Result<()> {
|
||||
let sync_projects = existing_loading_bar.is_some();
|
||||
let loading_bar = init_or_edit_loading(
|
||||
existing_loading_bar,
|
||||
LoadingBarType::MinecraftDownload {
|
||||
// If we are downloading minecraft for a profile, provide its name and uuid
|
||||
profile_name: profile.metadata.name.clone(),
|
||||
profile_path: profile.get_profile_full_path().await?,
|
||||
},
|
||||
100.0,
|
||||
"Downloading Minecraft",
|
||||
)
|
||||
.await?;
|
||||
|
||||
crate::api::profile::edit(&profile.profile_id(), |prof| {
|
||||
prof.install_stage = ProfileInstallStage::Installing;
|
||||
|
||||
async { Ok(()) }
|
||||
})
|
||||
.await?;
|
||||
State::sync().await?;
|
||||
|
||||
if sync_projects {
|
||||
Profile::sync_projects_task(profile.profile_id(), true);
|
||||
}
|
||||
|
||||
let state = State::get().await?;
|
||||
let instance_path =
|
||||
&io::canonicalize(profile.get_profile_full_path().await?)?;
|
||||
let metadata = state.metadata.read().await;
|
||||
|
||||
let version_index = metadata
|
||||
.minecraft
|
||||
.versions
|
||||
.iter()
|
||||
.position(|it| it.id == profile.metadata.game_version)
|
||||
.ok_or(crate::ErrorKind::LauncherError(format!(
|
||||
"Invalid game version: {}",
|
||||
profile.metadata.game_version
|
||||
)))?;
|
||||
let version = &metadata.minecraft.versions[version_index];
|
||||
let minecraft_updated = version_index
|
||||
<= metadata
|
||||
.minecraft
|
||||
.versions
|
||||
.iter()
|
||||
.position(|x| x.id == "22w16a")
|
||||
.unwrap_or(0);
|
||||
|
||||
let version_jar = profile
|
||||
.metadata
|
||||
.loader_version
|
||||
.as_ref()
|
||||
.map_or(version.id.clone(), |it| {
|
||||
format!("{}-{}", version.id.clone(), it.id.clone())
|
||||
});
|
||||
|
||||
// Download version info (5)
|
||||
let mut version_info = download::download_version_info(
|
||||
&state,
|
||||
version,
|
||||
profile.metadata.loader_version.as_ref(),
|
||||
Some(repairing),
|
||||
Some(&loading_bar),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// TODO: check if java exists, if not install it add to install step
|
||||
|
||||
let key = version_info
|
||||
.java_version
|
||||
.as_ref()
|
||||
.map(|it| it.major_version)
|
||||
.unwrap_or(8);
|
||||
let (java_version, set_java) = if let Some(java_version) =
|
||||
get_java_version_from_profile(profile, &version_info).await?
|
||||
{
|
||||
(std::path::PathBuf::from(java_version.path), false)
|
||||
} else {
|
||||
let path = crate::api::jre::auto_install_java(key).await?;
|
||||
|
||||
(path, true)
|
||||
};
|
||||
|
||||
// Test jre version
|
||||
let java_version = crate::api::jre::check_jre(java_version.clone())
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Java path invalid or non-functional: {:?}",
|
||||
java_version
|
||||
))
|
||||
})?;
|
||||
|
||||
if set_java {
|
||||
{
|
||||
let mut settings = state.settings.write().await;
|
||||
settings
|
||||
.java_globals
|
||||
.insert(format!("JAVA_{key}"), java_version.clone());
|
||||
}
|
||||
State::sync().await?;
|
||||
}
|
||||
|
||||
// Download minecraft (5-90)
|
||||
download::download_minecraft(
|
||||
&state,
|
||||
&version_info,
|
||||
&loading_bar,
|
||||
&java_version.architecture,
|
||||
repairing,
|
||||
minecraft_updated,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(processors) = &version_info.processors {
|
||||
let client_path = state
|
||||
.directories
|
||||
.version_dir(&version_jar)
|
||||
.await
|
||||
.join(format!("{version_jar}.jar"));
|
||||
|
||||
let libraries_dir = state.directories.libraries_dir().await;
|
||||
|
||||
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 => profile.metadata.game_version.clone(),
|
||||
server => "";
|
||||
"ROOT":
|
||||
client => instance_path.to_string_lossy(),
|
||||
server => "";
|
||||
"LIBRARY_DIR":
|
||||
client => libraries_dir.to_string_lossy(),
|
||||
server => "";
|
||||
}
|
||||
|
||||
emit_loading(&loading_bar, 0.0, Some("Running forge processors"))
|
||||
.await?;
|
||||
let total_length = processors.len();
|
||||
|
||||
// Forge processors (90-100)
|
||||
for (index, processor) in processors.iter().enumerate() {
|
||||
if let Some(sides) = &processor.sides {
|
||||
if !sides.contains(&String::from("client")) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let cp = wrap_ref_builder!(cp = processor.classpath.clone() => {
|
||||
cp.push(processor.jar.clone())
|
||||
});
|
||||
|
||||
let child = Command::new(&java_version.path)
|
||||
.arg("-cp")
|
||||
.arg(args::get_class_paths_jar(
|
||||
&libraries_dir,
|
||||
&cp,
|
||||
&java_version.architecture,
|
||||
)?)
|
||||
.arg(
|
||||
args::get_processor_main_class(args::get_lib_path(
|
||||
&libraries_dir,
|
||||
&processor.jar,
|
||||
false,
|
||||
)?)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Could not find processor main class for {}",
|
||||
processor.jar
|
||||
))
|
||||
})?,
|
||||
)
|
||||
.args(args::get_processor_arguments(
|
||||
&libraries_dir,
|
||||
&processor.args,
|
||||
data,
|
||||
)?)
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| IOError::with_path(e, &java_version.path))
|
||||
.map_err(|err| {
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Error running processor: {err}",
|
||||
))
|
||||
})?;
|
||||
|
||||
if !child.status.success() {
|
||||
return Err(crate::ErrorKind::LauncherError(format!(
|
||||
"Processor error: {}",
|
||||
String::from_utf8_lossy(&child.stderr)
|
||||
))
|
||||
.as_error());
|
||||
}
|
||||
|
||||
emit_loading(
|
||||
&loading_bar,
|
||||
30.0 / total_length as f64,
|
||||
Some(&format!(
|
||||
"Running forge processor {}/{}",
|
||||
index, total_length
|
||||
)),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
crate::api::profile::edit(&profile.profile_id(), |prof| {
|
||||
prof.install_stage = ProfileInstallStage::Installed;
|
||||
|
||||
async { Ok(()) }
|
||||
})
|
||||
.await?;
|
||||
State::sync().await?;
|
||||
emit_loading(&loading_bar, 1.0, Some("Finished installing")).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
#[theseus_macros::debug_pin]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn launch_minecraft(
|
||||
java_args: &[String],
|
||||
env_args: &[(String, String)],
|
||||
mc_set_options: &[(String, String)],
|
||||
wrapper: &Option<String>,
|
||||
memory: &st::MemorySettings,
|
||||
resolution: &st::WindowSize,
|
||||
credentials: &Credentials,
|
||||
post_exit_hook: Option<String>,
|
||||
profile: &Profile,
|
||||
) -> crate::Result<Arc<tokio::sync::RwLock<MinecraftChild>>> {
|
||||
if profile.install_stage == ProfileInstallStage::PackInstalling
|
||||
|| profile.install_stage == ProfileInstallStage::Installing
|
||||
{
|
||||
return Err(crate::ErrorKind::LauncherError(
|
||||
"Profile is still installing".to_string(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
if profile.install_stage != ProfileInstallStage::Installed {
|
||||
install_minecraft(profile, None, false).await?;
|
||||
}
|
||||
|
||||
let state = State::get().await?;
|
||||
let metadata = state.metadata.read().await;
|
||||
|
||||
let instance_path = profile.get_profile_full_path().await?;
|
||||
let instance_path = &io::canonicalize(instance_path)?;
|
||||
|
||||
let version_index = metadata
|
||||
.minecraft
|
||||
.versions
|
||||
.iter()
|
||||
.position(|it| it.id == profile.metadata.game_version)
|
||||
.ok_or(crate::ErrorKind::LauncherError(format!(
|
||||
"Invalid game version: {}",
|
||||
profile.metadata.game_version
|
||||
)))?;
|
||||
let version = &metadata.minecraft.versions[version_index];
|
||||
let minecraft_updated = version_index
|
||||
<= metadata
|
||||
.minecraft
|
||||
.versions
|
||||
.iter()
|
||||
.position(|x| x.id == "22w16a")
|
||||
.unwrap_or(0);
|
||||
|
||||
let version_jar = profile
|
||||
.metadata
|
||||
.loader_version
|
||||
.as_ref()
|
||||
.map_or(version.id.clone(), |it| {
|
||||
format!("{}-{}", version.id.clone(), it.id.clone())
|
||||
});
|
||||
|
||||
let version_info = download::download_version_info(
|
||||
&state,
|
||||
version,
|
||||
profile.metadata.loader_version.as_ref(),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let java_version = get_java_version_from_profile(profile, &version_info)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::LauncherError(
|
||||
"Missing correct java installation".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
// Test jre version
|
||||
let java_version =
|
||||
crate::api::jre::check_jre(java_version.path.clone().into())
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Java path invalid or non-functional: {}",
|
||||
java_version.path
|
||||
))
|
||||
})?;
|
||||
|
||||
let client_path = state
|
||||
.directories
|
||||
.version_dir(&version_jar)
|
||||
.await
|
||||
.join(format!("{version_jar}.jar"));
|
||||
|
||||
let args = version_info.arguments.clone().unwrap_or_default();
|
||||
let mut command = match wrapper {
|
||||
Some(hook) => {
|
||||
wrap_ref_builder!(it = Command::new(hook) => {it.arg(&java_version.path)})
|
||||
}
|
||||
None => Command::new(&java_version.path),
|
||||
};
|
||||
|
||||
let env_args = Vec::from(env_args);
|
||||
|
||||
// Check if profile has a running profile, and reject running the command if it does
|
||||
// Done late so a quick double call doesn't launch two instances
|
||||
let existing_processes =
|
||||
process::get_uuids_by_profile_path(profile.profile_id()).await?;
|
||||
if let Some(uuid) = existing_processes.first() {
|
||||
return Err(crate::ErrorKind::LauncherError(format!(
|
||||
"Profile {} is already running at UUID: {uuid}",
|
||||
profile.profile_id()
|
||||
))
|
||||
.as_error());
|
||||
}
|
||||
command
|
||||
.args(
|
||||
args::get_jvm_arguments(
|
||||
args.get(&d::minecraft::ArgumentType::Jvm)
|
||||
.map(|x| x.as_slice()),
|
||||
&state.directories.version_natives_dir(&version_jar).await,
|
||||
&state.directories.libraries_dir().await,
|
||||
&args::get_class_paths(
|
||||
&state.directories.libraries_dir().await,
|
||||
version_info.libraries.as_slice(),
|
||||
&client_path,
|
||||
&java_version.architecture,
|
||||
minecraft_updated,
|
||||
)?,
|
||||
&version_jar,
|
||||
*memory,
|
||||
Vec::from(java_args),
|
||||
&java_version.architecture,
|
||||
)?
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.arg(version_info.main_class.clone())
|
||||
.args(
|
||||
args::get_minecraft_arguments(
|
||||
args.get(&d::minecraft::ArgumentType::Game)
|
||||
.map(|x| x.as_slice()),
|
||||
version_info.minecraft_arguments.as_deref(),
|
||||
credentials,
|
||||
&version.id,
|
||||
&version_info.asset_index.id,
|
||||
instance_path,
|
||||
&state.directories.assets_dir().await,
|
||||
&version.type_,
|
||||
*resolution,
|
||||
&java_version.architecture,
|
||||
)?
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.current_dir(instance_path.clone());
|
||||
|
||||
// CARGO-set DYLD_LIBRARY_PATH breaks Minecraft on macOS during testing on playground
|
||||
#[cfg(target_os = "macos")]
|
||||
if std::env::var("CARGO").is_ok() {
|
||||
command.env_remove("DYLD_FALLBACK_LIBRARY_PATH");
|
||||
}
|
||||
// Java options should be set in instance options (the existence of _JAVA_OPTIONS overwites them)
|
||||
command.env_remove("_JAVA_OPTIONS");
|
||||
|
||||
command.envs(env_args);
|
||||
|
||||
// Overwrites the minecraft options.txt file with the settings from the profile
|
||||
// Uses 'a:b' syntax which is not quite yaml
|
||||
use regex::Regex;
|
||||
|
||||
if !mc_set_options.is_empty() {
|
||||
let options_path = instance_path.join("options.txt");
|
||||
let mut options_string = String::new();
|
||||
if options_path.exists() {
|
||||
options_string = io::read_to_string(&options_path).await?;
|
||||
}
|
||||
for (key, value) in mc_set_options {
|
||||
let re = Regex::new(&format!(r"(?m)^{}:.*$", regex::escape(key)))?;
|
||||
// check if the regex exists in the file
|
||||
if !re.is_match(&options_string) {
|
||||
// The key was not found in the file, so append it
|
||||
options_string.push_str(&format!("\n{}:{}", key, value));
|
||||
} else {
|
||||
let replaced_string = re
|
||||
.replace_all(&options_string, &format!("{}:{}", key, value))
|
||||
.to_string();
|
||||
options_string = replaced_string;
|
||||
}
|
||||
}
|
||||
|
||||
io::write(&options_path, options_string).await?;
|
||||
}
|
||||
|
||||
crate::api::profile::edit(&profile.profile_id(), |prof| {
|
||||
prof.metadata.last_played = Some(Utc::now());
|
||||
|
||||
async { Ok(()) }
|
||||
})
|
||||
.await?;
|
||||
State::sync().await?;
|
||||
|
||||
let mut censor_strings = HashMap::new();
|
||||
let username = whoami::username();
|
||||
censor_strings.insert(
|
||||
format!("/{}/", username),
|
||||
"/{COMPUTER_USERNAME}/".to_string(),
|
||||
);
|
||||
censor_strings.insert(
|
||||
format!("\\{}\\", username),
|
||||
"\\{COMPUTER_USERNAME}\\".to_string(),
|
||||
);
|
||||
censor_strings.insert(
|
||||
credentials.access_token.clone(),
|
||||
"{MINECRAFT_ACCESS_TOKEN}".to_string(),
|
||||
);
|
||||
censor_strings.insert(
|
||||
credentials.username.clone(),
|
||||
"{MINECRAFT_USERNAME}".to_string(),
|
||||
);
|
||||
censor_strings.insert(
|
||||
credentials.id.as_simple().to_string(),
|
||||
"{MINECRAFT_UUID}".to_string(),
|
||||
);
|
||||
censor_strings.insert(
|
||||
credentials.id.as_hyphenated().to_string(),
|
||||
"{MINECRAFT_UUID}".to_string(),
|
||||
);
|
||||
|
||||
// If in tauri, and the 'minimize on launch' setting is enabled, minimize the window
|
||||
#[cfg(feature = "tauri")]
|
||||
{
|
||||
use crate::EventState;
|
||||
|
||||
let window = EventState::get_main_window().await?;
|
||||
if let Some(window) = window {
|
||||
let settings = state.settings.read().await;
|
||||
if settings.hide_on_process {
|
||||
window.minimize()?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !*state.offline.read().await {
|
||||
// Add game played to discord rich presence
|
||||
let _ = state
|
||||
.discord_rpc
|
||||
.set_activity(&format!("Playing {}", profile.metadata.name), true)
|
||||
.await;
|
||||
}
|
||||
|
||||
// Create Minecraft child by inserting it into the state
|
||||
// This also spawns the process and prepares the subsequent processes
|
||||
let mut state_children = state.children.write().await;
|
||||
state_children
|
||||
.insert_new_process(
|
||||
Uuid::new_v4(),
|
||||
profile.profile_id(),
|
||||
command,
|
||||
post_exit_hook,
|
||||
censor_strings,
|
||||
)
|
||||
.await
|
||||
}
|
||||
Reference in New Issue
Block a user