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,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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user