You've already forked AstralRinth
forked from didirus/AstralRinth
Initial draft of profile metadata format & CLI (#17)
* Initial draft of profile metadata format * Remove records, add Clippy to Nix, fix Clippy error * Work on profile definition * BREAKING: Make global settings consistent with profile settings * Add builder methods & format * Integrate launching with profiles * Add profile loading * Launching via profile, API tweaks, and yak shaving * Incremental update, committing everything due to personal system maintainance * Prepare for review cycle * Remove reminents of experimental work * CLI: allow people to override the non-empty directory check * Fix mistake in previous commit * Handle trailing whitespace and newlines in prompts * Revamp prompt to use dialoguer and support defaults * Make requested changes
This commit is contained in:
@@ -22,7 +22,8 @@ impl Metadata {
|
||||
let meta_path = Path::new(LAUNCHER_WORK_DIR).join(META_FILE);
|
||||
|
||||
if meta_path.exists() {
|
||||
let meta_data = std::fs::read_to_string(meta_path).ok()
|
||||
let meta_data = std::fs::read_to_string(meta_path)
|
||||
.ok()
|
||||
.and_then(|x| serde_json::from_str::<Metadata>(&x).ok());
|
||||
|
||||
if let Some(metadata) = meta_data {
|
||||
@@ -77,8 +78,14 @@ impl Metadata {
|
||||
"{}/minecraft/v0/manifest.json",
|
||||
META_URL
|
||||
))),
|
||||
daedalus::modded::fetch_manifest(&format!("{}/forge/v0/manifest.json", META_URL)),
|
||||
daedalus::modded::fetch_manifest(&format!("{}/fabric/v0/manifest.json", META_URL)),
|
||||
daedalus::modded::fetch_manifest(&format!(
|
||||
"{}/forge/v0/manifest.json",
|
||||
META_URL
|
||||
)),
|
||||
daedalus::modded::fetch_manifest(&format!(
|
||||
"{}/fabric/v0/manifest.json",
|
||||
META_URL
|
||||
)),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -90,10 +97,12 @@ impl Metadata {
|
||||
}
|
||||
|
||||
pub async fn get<'a>() -> Result<RwLockReadGuard<'a, Self>, DataError> {
|
||||
Ok(METADATA
|
||||
let res = METADATA
|
||||
.get()
|
||||
.ok_or_else(|| DataError::InitializedError("metadata".to_string()))?
|
||||
.read()
|
||||
.await)
|
||||
.await;
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use std::io;
|
||||
|
||||
pub use meta::Metadata;
|
||||
pub use profiles::{Profile, Profiles};
|
||||
pub use settings::Settings;
|
||||
|
||||
mod meta;
|
||||
pub mod profiles;
|
||||
mod settings;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
@@ -14,6 +16,9 @@ pub enum DataError {
|
||||
#[error("Daedalus error: {0}")]
|
||||
DaedalusError(#[from] daedalus::Error),
|
||||
|
||||
#[error("Data format error: {0}")]
|
||||
FormatError(String),
|
||||
|
||||
#[error("Attempted to access {0} without initializing it!")]
|
||||
InitializedError(String),
|
||||
|
||||
|
||||
502
theseus/src/data/profiles.rs
Normal file
502
theseus/src/data/profiles.rs
Normal file
@@ -0,0 +1,502 @@
|
||||
use super::DataError;
|
||||
use crate::launcher::ModLoader;
|
||||
use daedalus::modded::LoaderVersion;
|
||||
use futures::*;
|
||||
use once_cell::sync::OnceCell;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use tokio::{
|
||||
fs,
|
||||
process::{Child, Command},
|
||||
sync::{Mutex, RwLock, RwLockReadGuard},
|
||||
};
|
||||
|
||||
static PROFILES: OnceCell<RwLock<Profiles>> = OnceCell::new();
|
||||
pub const PROFILE_JSON_PATH: &str = "profile.json";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Profiles(pub HashMap<PathBuf, Profile>);
|
||||
|
||||
// TODO: possibly add defaults to some of these values
|
||||
pub const CURRENT_FORMAT_VERSION: u32 = 1;
|
||||
pub const SUPPORTED_ICON_FORMATS: &[&'static str] = &[
|
||||
"bmp", "gif", "jpeg", "jpg", "jpe", "png", "svg", "svgz", "webp", "rgb",
|
||||
"mp4",
|
||||
];
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct Profile {
|
||||
#[serde(skip)]
|
||||
pub path: PathBuf,
|
||||
pub metadata: Metadata,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub java: Option<JavaSettings>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub memory: Option<MemorySettings>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub resolution: Option<WindowSize>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub hooks: Option<ProfileHooks>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct Metadata {
|
||||
pub name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub icon: Option<PathBuf>,
|
||||
pub game_version: String,
|
||||
#[serde(default)]
|
||||
pub loader: ModLoader,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub loader_version: Option<LoaderVersion>,
|
||||
pub format_version: u32,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct JavaSettings {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub install: Option<PathBuf>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub extra_arguments: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
|
||||
pub struct MemorySettings {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub minimum: Option<u32>,
|
||||
pub maximum: u32,
|
||||
}
|
||||
|
||||
impl Default for MemorySettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
minimum: None,
|
||||
maximum: 2048,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
|
||||
pub struct WindowSize(pub u16, pub u16);
|
||||
|
||||
impl Default for WindowSize {
|
||||
fn default() -> Self {
|
||||
Self(854, 480)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct ProfileHooks {
|
||||
#[serde(skip_serializing_if = "HashSet::is_empty", default)]
|
||||
pub pre_launch: HashSet<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub wrapper: Option<String>,
|
||||
#[serde(skip_serializing_if = "HashSet::is_empty", default)]
|
||||
pub post_exit: HashSet<String>,
|
||||
}
|
||||
|
||||
impl Default for ProfileHooks {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
pre_launch: HashSet::<String>::new(),
|
||||
wrapper: None,
|
||||
post_exit: HashSet::<String>::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Profile {
|
||||
pub async fn new(
|
||||
name: String,
|
||||
version: String,
|
||||
path: PathBuf,
|
||||
) -> Result<Self, DataError> {
|
||||
if name.trim().is_empty() {
|
||||
return Err(DataError::FormatError(String::from(
|
||||
"Empty name for instance!",
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
path: path.canonicalize()?,
|
||||
metadata: Metadata {
|
||||
name,
|
||||
icon: None,
|
||||
game_version: version,
|
||||
loader: ModLoader::Vanilla,
|
||||
loader_version: None,
|
||||
format_version: CURRENT_FORMAT_VERSION,
|
||||
},
|
||||
java: None,
|
||||
memory: None,
|
||||
resolution: None,
|
||||
hooks: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn run(
|
||||
&self,
|
||||
credentials: &crate::launcher::Credentials,
|
||||
) -> Result<Child, crate::launcher::LauncherError> {
|
||||
use crate::launcher::LauncherError;
|
||||
let (settings, version_info) = tokio::try_join! {
|
||||
super::Settings::get(),
|
||||
super::Metadata::get()
|
||||
.and_then(|manifest| async move {
|
||||
let version = manifest
|
||||
.minecraft
|
||||
.versions
|
||||
.iter()
|
||||
.find(|it| it.id == self.metadata.game_version.as_ref())
|
||||
.ok_or_else(|| DataError::FormatError(format!(
|
||||
"invalid or unknown version: {}",
|
||||
self.metadata.game_version
|
||||
)))?;
|
||||
|
||||
Ok(daedalus::minecraft::fetch_version_info(version)
|
||||
.await?)
|
||||
})
|
||||
}?;
|
||||
|
||||
let ref pre_launch_hooks =
|
||||
self.hooks.as_ref().unwrap_or(&settings.hooks).pre_launch;
|
||||
for hook in pre_launch_hooks.iter() {
|
||||
// TODO: hook parameters
|
||||
let mut cmd = hook.split(' ');
|
||||
let result = Command::new(cmd.next().unwrap())
|
||||
.args(&cmd.collect::<Vec<&str>>())
|
||||
.current_dir(&self.path)
|
||||
.spawn()?
|
||||
.wait()
|
||||
.await?;
|
||||
|
||||
if !result.success() {
|
||||
return Err(LauncherError::ExitError(
|
||||
result.code().unwrap_or(-1),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let java_install = match self.java {
|
||||
Some(JavaSettings {
|
||||
install: Some(ref install),
|
||||
..
|
||||
}) => install,
|
||||
_ => if version_info
|
||||
.java_version
|
||||
.as_ref()
|
||||
.filter(|it| it.major_version >= 16)
|
||||
.is_some()
|
||||
{
|
||||
settings.java_17_path.as_ref()
|
||||
} else {
|
||||
settings.java_8_path.as_ref()
|
||||
}
|
||||
.ok_or_else(|| {
|
||||
LauncherError::JavaError(format!(
|
||||
"No Java installed for version {}",
|
||||
version_info.java_version.map_or(8, |it| it.major_version),
|
||||
))
|
||||
})?,
|
||||
};
|
||||
|
||||
if !java_install.exists() {
|
||||
return Err(LauncherError::JavaError(format!(
|
||||
"Could not find java install: {}",
|
||||
java_install.display()
|
||||
)));
|
||||
}
|
||||
|
||||
let java_args = &self
|
||||
.java
|
||||
.as_ref()
|
||||
.and_then(|it| it.extra_arguments.as_ref())
|
||||
.unwrap_or(&settings.custom_java_args);
|
||||
|
||||
let wrapper = self
|
||||
.hooks
|
||||
.as_ref()
|
||||
.map_or(&settings.hooks.wrapper, |it| &it.wrapper);
|
||||
|
||||
let ref memory = self.memory.unwrap_or(settings.memory);
|
||||
let ref resolution =
|
||||
self.resolution.unwrap_or(settings.game_resolution);
|
||||
|
||||
crate::launcher::launch_minecraft(
|
||||
&self.metadata.game_version,
|
||||
&self.metadata.loader_version,
|
||||
&self.path,
|
||||
&java_install,
|
||||
&java_args,
|
||||
&wrapper,
|
||||
memory,
|
||||
resolution,
|
||||
credentials,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn kill(
|
||||
&self,
|
||||
running: &mut Child,
|
||||
) -> Result<(), crate::launcher::LauncherError> {
|
||||
running.kill().await?;
|
||||
self.wait_for(running).await
|
||||
}
|
||||
|
||||
pub async fn wait_for(
|
||||
&self,
|
||||
running: &mut Child,
|
||||
) -> Result<(), crate::launcher::LauncherError> {
|
||||
let result = running.wait().await.map_err(|err| {
|
||||
crate::launcher::LauncherError::ProcessError {
|
||||
inner: err,
|
||||
process: String::from("minecraft"),
|
||||
}
|
||||
})?;
|
||||
|
||||
match result.success() {
|
||||
false => Err(crate::launcher::LauncherError::ExitError(
|
||||
result.code().unwrap_or(-1),
|
||||
)),
|
||||
true => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: deduplicate these builder methods
|
||||
// They are flat like this in order to allow builder-style usage
|
||||
pub fn with_name(&mut self, name: String) -> &mut Self {
|
||||
self.metadata.name = name;
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn with_icon(
|
||||
&mut self,
|
||||
icon: &Path,
|
||||
) -> Result<&mut Self, DataError> {
|
||||
let ext = icon
|
||||
.extension()
|
||||
.and_then(std::ffi::OsStr::to_str)
|
||||
.unwrap_or("");
|
||||
|
||||
if SUPPORTED_ICON_FORMATS.contains(&ext) {
|
||||
let file_name = format!("icon.{ext}");
|
||||
fs::copy(icon, &self.path.join(&file_name)).await?;
|
||||
self.metadata.icon =
|
||||
Some(Path::new(&format!("./{file_name}")).to_owned());
|
||||
|
||||
Ok(self)
|
||||
} else {
|
||||
Err(DataError::FormatError(format!(
|
||||
"Unsupported image type: {ext}"
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_game_version(&mut self, version: String) -> &mut Self {
|
||||
self.metadata.game_version = version;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_loader(
|
||||
&mut self,
|
||||
loader: ModLoader,
|
||||
version: Option<LoaderVersion>,
|
||||
) -> &mut Self {
|
||||
self.metadata.loader = loader;
|
||||
self.metadata.loader_version = version;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_java_settings(
|
||||
&mut self,
|
||||
settings: Option<JavaSettings>,
|
||||
) -> &mut Self {
|
||||
self.java = settings;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_memory(
|
||||
&mut self,
|
||||
settings: Option<MemorySettings>,
|
||||
) -> &mut Self {
|
||||
self.memory = settings;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_resolution(
|
||||
&mut self,
|
||||
resolution: Option<WindowSize>,
|
||||
) -> &mut Self {
|
||||
self.resolution = resolution;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_hooks(&mut self, hooks: Option<ProfileHooks>) -> &mut Self {
|
||||
self.hooks = hooks;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Profiles {
|
||||
pub async fn init() -> Result<(), DataError> {
|
||||
let settings = super::Settings::get().await?;
|
||||
let profiles = Arc::new(Mutex::new(HashMap::new()));
|
||||
|
||||
let futures = settings.profiles.clone().into_iter().map(|path| async {
|
||||
let profiles = Arc::clone(&profiles);
|
||||
tokio::spawn(async move {
|
||||
// TODO: handle missing profiles
|
||||
let mut profiles = profiles.lock().await;
|
||||
let profile = Self::read_profile_from_dir(path.clone()).await?;
|
||||
|
||||
profiles.insert(path, profile);
|
||||
Ok(()) as Result<_, DataError>
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
});
|
||||
futures::future::try_join_all(futures).await?;
|
||||
|
||||
PROFILES.get_or_init(|| {
|
||||
RwLock::new(Profiles(
|
||||
Arc::try_unwrap(profiles).unwrap().into_inner(),
|
||||
))
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn insert(profile: Profile) -> Result<(), DataError> {
|
||||
let mut profiles = PROFILES
|
||||
.get()
|
||||
.ok_or_else(|| {
|
||||
DataError::InitializedError(String::from("profiles"))
|
||||
})?
|
||||
.write()
|
||||
.await;
|
||||
|
||||
super::Settings::get_mut()
|
||||
.await?
|
||||
.profiles
|
||||
.insert(profile.path.clone());
|
||||
profiles.0.insert(profile.path.clone(), profile);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn insert_from(path: PathBuf) -> Result<(), DataError> {
|
||||
Self::read_profile_from_dir(path)
|
||||
.and_then(Self::insert)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn remove(path: &Path) -> Result<Option<Profile>, DataError> {
|
||||
let path = path.canonicalize()?;
|
||||
let mut profiles = PROFILES.get().unwrap().write().await;
|
||||
super::Settings::get_mut().await?.profiles.remove(&path);
|
||||
Ok(profiles.0.remove(&path))
|
||||
}
|
||||
|
||||
pub async fn save() -> Result<(), DataError> {
|
||||
let profiles = Self::get().await?;
|
||||
|
||||
let futures = profiles.0.clone().into_iter().map(|(path, profile)| {
|
||||
tokio::spawn(async move {
|
||||
let json = tokio::task::spawn_blocking(move || {
|
||||
serde_json::to_vec_pretty(&profile)
|
||||
})
|
||||
.await
|
||||
.unwrap()?;
|
||||
|
||||
let profile_json_path = path.join(PROFILE_JSON_PATH);
|
||||
fs::write(profile_json_path, json).await?;
|
||||
Ok(()) as Result<(), DataError>
|
||||
})
|
||||
});
|
||||
futures::future::try_join_all(futures)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.collect::<Result<_, DataError>>()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get<'a>() -> Result<RwLockReadGuard<'a, Self>, DataError> {
|
||||
Ok(PROFILES
|
||||
.get()
|
||||
.ok_or_else(|| DataError::InitializedError("profiles".to_string()))?
|
||||
.read()
|
||||
.await)
|
||||
}
|
||||
|
||||
async fn read_profile_from_dir(
|
||||
path: PathBuf,
|
||||
) -> Result<Profile, DataError> {
|
||||
let json = fs::read(path.join(PROFILE_JSON_PATH)).await?;
|
||||
let mut profile = serde_json::from_slice::<Profile>(&json)?;
|
||||
profile.path = path.clone();
|
||||
Ok(profile)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||
|
||||
#[test]
|
||||
fn profile_test() -> Result<(), serde_json::Error> {
|
||||
let profile = Profile {
|
||||
path: PathBuf::from("/tmp/nunya/beeswax"),
|
||||
metadata: Metadata {
|
||||
name: String::from("Example Pack"),
|
||||
icon: None,
|
||||
game_version: String::from("1.18.2"),
|
||||
loader: ModLoader::Vanilla,
|
||||
loader_version: None,
|
||||
format_version: CURRENT_FORMAT_VERSION,
|
||||
},
|
||||
java: JavaSettings {
|
||||
install: PathBuf::from("/usr/bin/java"),
|
||||
extra_arguments: Vec::new(),
|
||||
},
|
||||
memory: MemorySettings {
|
||||
minimum: None,
|
||||
maximum: 8192,
|
||||
},
|
||||
resolution: WindowSize(1920, 1080),
|
||||
hooks: ProfileHooks {
|
||||
pre_launch: HashSet::new(),
|
||||
wrapper: None,
|
||||
post_exit: HashSet::new(),
|
||||
},
|
||||
};
|
||||
let json = serde_json::json!({
|
||||
"path": "/tmp/nunya/beeswax",
|
||||
"metadata": {
|
||||
"name": "Example Pack",
|
||||
"game_version": "1.18.2",
|
||||
"format_version": 1u32,
|
||||
},
|
||||
"java": {
|
||||
"install": "/usr/bin/java",
|
||||
},
|
||||
"memory": {
|
||||
"maximum": 8192u32,
|
||||
},
|
||||
"resolution": (1920u16, 1080u16),
|
||||
"hooks": {},
|
||||
});
|
||||
|
||||
assert_eq!(serde_json::to_value(profile.clone())?, json.clone());
|
||||
assert_str_eq!(
|
||||
format!("{:?}", serde_json::from_value::<Profile>(json)?),
|
||||
format!("{:?}", profile),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,33 +1,51 @@
|
||||
use std::path::Path;
|
||||
use super::profiles::*;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use crate::{data::DataError, LAUNCHER_WORK_DIR};
|
||||
use once_cell::sync;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::{RwLock, RwLockReadGuard};
|
||||
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||
|
||||
const SETTINGS_FILE: &str = "settings.json";
|
||||
const ICONS_PATH: &str = "icons";
|
||||
const METADATA_DIR: &str = "meta";
|
||||
|
||||
static SETTINGS: sync::OnceCell<RwLock<Settings>> = sync::OnceCell::new();
|
||||
pub const FORMAT_VERSION: u32 = 1;
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
#[serde(default)]
|
||||
pub struct Settings {
|
||||
pub memory: i32,
|
||||
pub game_resolution: (i32, i32),
|
||||
pub custom_java_args: String,
|
||||
pub java_8_path: Option<String>,
|
||||
pub java_17_path: Option<String>,
|
||||
pub wrapper_command: Option<String>,
|
||||
pub memory: MemorySettings,
|
||||
pub game_resolution: WindowSize,
|
||||
pub custom_java_args: Vec<String>,
|
||||
pub java_8_path: Option<PathBuf>,
|
||||
pub java_17_path: Option<PathBuf>,
|
||||
pub hooks: ProfileHooks,
|
||||
pub icon_path: PathBuf,
|
||||
pub metadata_dir: PathBuf,
|
||||
pub profiles: HashSet<PathBuf>,
|
||||
pub max_concurrent_downloads: usize,
|
||||
pub version: u32,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
memory: 2048,
|
||||
game_resolution: (854, 480),
|
||||
custom_java_args: "".to_string(),
|
||||
memory: MemorySettings::default(),
|
||||
game_resolution: WindowSize::default(),
|
||||
custom_java_args: Vec::new(),
|
||||
java_8_path: None,
|
||||
java_17_path: None,
|
||||
wrapper_command: None,
|
||||
hooks: ProfileHooks::default(),
|
||||
icon_path: Path::new(LAUNCHER_WORK_DIR).join(ICONS_PATH),
|
||||
metadata_dir: Path::new(LAUNCHER_WORK_DIR).join(METADATA_DIR),
|
||||
profiles: HashSet::new(),
|
||||
max_concurrent_downloads: 32,
|
||||
version: FORMAT_VERSION,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,10 +68,14 @@ impl Settings {
|
||||
if SETTINGS.get().is_none() {
|
||||
let new = Self::default();
|
||||
|
||||
std::fs::write(
|
||||
tokio::fs::rename(SETTINGS_FILE, format!("{SETTINGS_FILE}.bak"))
|
||||
.await?;
|
||||
|
||||
tokio::fs::write(
|
||||
Path::new(LAUNCHER_WORK_DIR).join(SETTINGS_FILE),
|
||||
&serde_json::to_string(&new)?,
|
||||
)?;
|
||||
)
|
||||
.await?;
|
||||
|
||||
SETTINGS.get_or_init(|| RwLock::new(new));
|
||||
}
|
||||
@@ -66,7 +88,7 @@ impl Settings {
|
||||
Path::new(LAUNCHER_WORK_DIR).join(SETTINGS_FILE),
|
||||
)?)?;
|
||||
|
||||
let write = &mut *SETTINGS
|
||||
let mut write = SETTINGS
|
||||
.get()
|
||||
.ok_or_else(|| DataError::InitializedError("settings".to_string()))?
|
||||
.write()
|
||||
@@ -82,17 +104,24 @@ impl Settings {
|
||||
|
||||
std::fs::write(
|
||||
Path::new(LAUNCHER_WORK_DIR).join(SETTINGS_FILE),
|
||||
&serde_json::to_string(&*settings)?,
|
||||
&serde_json::to_string_pretty(&*settings)?,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get<'a>() -> Result<RwLockReadGuard<'a, Self>, DataError> {
|
||||
Ok(SETTINGS
|
||||
Ok(Self::get_or_uninit::<'a>()?.read().await)
|
||||
}
|
||||
|
||||
pub async fn get_mut<'a>() -> Result<RwLockWriteGuard<'a, Self>, DataError>
|
||||
{
|
||||
Ok(Self::get_or_uninit::<'a>()?.write().await)
|
||||
}
|
||||
|
||||
fn get_or_uninit<'a>() -> Result<&'a RwLock<Self>, DataError> {
|
||||
SETTINGS
|
||||
.get()
|
||||
.ok_or_else(|| DataError::InitializedError("settings".to_string()))?
|
||||
.read()
|
||||
.await)
|
||||
.ok_or_else(|| DataError::InitializedError("settings".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::data::profiles::*;
|
||||
use crate::launcher::auth::provider::Credentials;
|
||||
use crate::launcher::rules::parse_rules;
|
||||
use crate::launcher::LauncherError;
|
||||
@@ -21,19 +22,22 @@ pub fn get_class_paths(
|
||||
libraries: &[Library],
|
||||
client_path: &Path,
|
||||
) -> Result<String, LauncherError> {
|
||||
let mut class_paths = libraries.iter().filter_map(|library| {
|
||||
if let Some(rules) = &library.rules {
|
||||
if !super::rules::parse_rules(rules.as_slice()) {
|
||||
let mut class_paths = libraries
|
||||
.iter()
|
||||
.filter_map(|library| {
|
||||
if let Some(rules) = &library.rules {
|
||||
if !super::rules::parse_rules(rules.as_slice()) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
if !library.include_in_classpath {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
if !library.include_in_classpath {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(get_lib_path(libraries_path, &library.name))
|
||||
}).collect::<Result<Vec<_>, _>>()?;
|
||||
Some(get_lib_path(libraries_path, &library.name))
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
class_paths.push(
|
||||
crate::util::absolute_path(&client_path)
|
||||
@@ -54,9 +58,10 @@ pub fn get_class_paths_jar<T: AsRef<str>>(
|
||||
libraries_path: &Path,
|
||||
libraries: &[T],
|
||||
) -> Result<String, LauncherError> {
|
||||
let class_paths = libraries.iter().map(|library| {
|
||||
get_lib_path(libraries_path, library.as_ref())
|
||||
}).collect::<Result<Vec<_>, _>>()?;
|
||||
let class_paths = libraries
|
||||
.iter()
|
||||
.map(|library| get_lib_path(libraries_path, library.as_ref()))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
Ok(class_paths.join(get_cp_separator()))
|
||||
}
|
||||
@@ -90,7 +95,7 @@ pub fn get_jvm_arguments(
|
||||
libraries_path: &Path,
|
||||
class_paths: &str,
|
||||
version_name: &str,
|
||||
memory: i32,
|
||||
memory: MemorySettings,
|
||||
custom_args: Vec<String>,
|
||||
) -> Result<Vec<String>, LauncherError> {
|
||||
let mut parsed_arguments = Vec::new();
|
||||
@@ -120,7 +125,10 @@ pub fn get_jvm_arguments(
|
||||
parsed_arguments.push(class_paths.to_string());
|
||||
}
|
||||
|
||||
parsed_arguments.push(format!("-Xmx{}M", memory));
|
||||
if let Some(minimum) = memory.minimum {
|
||||
parsed_arguments.push(format!("-Xms{minimum}M"));
|
||||
}
|
||||
parsed_arguments.push(format!("-Xmx{}M", memory.maximum));
|
||||
for arg in custom_args {
|
||||
if !arg.is_empty() {
|
||||
parsed_arguments.push(arg);
|
||||
@@ -148,8 +156,7 @@ fn parse_jvm_argument(
|
||||
natives_path.to_string_lossy()
|
||||
))
|
||||
})?
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
.to_string_lossy(),
|
||||
)
|
||||
.replace(
|
||||
"${library_directory}",
|
||||
@@ -180,7 +187,7 @@ pub fn get_minecraft_arguments(
|
||||
game_directory: &Path,
|
||||
assets_directory: &Path,
|
||||
version_type: &VersionType,
|
||||
resolution: (i32, i32),
|
||||
resolution: WindowSize,
|
||||
) -> Result<Vec<String>, LauncherError> {
|
||||
if let Some(arguments) = arguments {
|
||||
let mut parsed_arguments = Vec::new();
|
||||
@@ -234,7 +241,7 @@ fn parse_minecraft_argument(
|
||||
game_directory: &Path,
|
||||
assets_directory: &Path,
|
||||
version_type: &VersionType,
|
||||
resolution: (i32, i32),
|
||||
resolution: WindowSize,
|
||||
) -> Result<String, LauncherError> {
|
||||
Ok(argument
|
||||
.replace("${auth_access_token}", access_token)
|
||||
@@ -255,7 +262,7 @@ fn parse_minecraft_argument(
|
||||
))
|
||||
})?
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
.to_owned(),
|
||||
)
|
||||
.replace(
|
||||
"${assets_root}",
|
||||
@@ -267,7 +274,7 @@ fn parse_minecraft_argument(
|
||||
))
|
||||
})?
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
.to_owned(),
|
||||
)
|
||||
.replace(
|
||||
"${game_assets}",
|
||||
@@ -279,7 +286,7 @@ fn parse_minecraft_argument(
|
||||
))
|
||||
})?
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
.to_owned(),
|
||||
)
|
||||
.replace("${version_type}", version_type.as_str())
|
||||
.replace("${resolution_width}", &resolution.0.to_string())
|
||||
|
||||
@@ -1,15 +1,38 @@
|
||||
use crate::launcher::LauncherError;
|
||||
use crate::{
|
||||
data::{DataError, Settings},
|
||||
launcher::LauncherError,
|
||||
};
|
||||
use daedalus::get_path_from_artifact;
|
||||
use daedalus::minecraft::{
|
||||
fetch_assets_index, fetch_version_info, Asset, AssetsIndex, DownloadType, Library, Os, Version,
|
||||
VersionInfo,
|
||||
fetch_assets_index, fetch_version_info, Asset, AssetsIndex, DownloadType,
|
||||
Library, Os, Version, VersionInfo,
|
||||
};
|
||||
use daedalus::modded::{
|
||||
fetch_partial_version, merge_partial_version, LoaderVersion,
|
||||
};
|
||||
use daedalus::modded::{fetch_partial_version, merge_partial_version, LoaderVersion};
|
||||
use futures::future;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
use tokio::{
|
||||
fs::File,
|
||||
io::AsyncWriteExt,
|
||||
sync::{OnceCell, Semaphore},
|
||||
};
|
||||
|
||||
static DOWNLOADS_SEMAPHORE: OnceCell<Semaphore> = OnceCell::const_new();
|
||||
|
||||
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?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn download_version_info(
|
||||
client_path: &Path,
|
||||
@@ -22,8 +45,7 @@ pub async fn download_version_info(
|
||||
};
|
||||
|
||||
let mut path = client_path.join(id);
|
||||
path.push(id);
|
||||
path.set_extension("json");
|
||||
path.push(&format!("{id}.json"));
|
||||
|
||||
if path.exists() {
|
||||
let contents = std::fs::read_to_string(path)?;
|
||||
@@ -37,7 +59,7 @@ pub async fn download_version_info(
|
||||
info.id = loader_version.id.clone();
|
||||
}
|
||||
let info_s = serde_json::to_string(&info)?;
|
||||
save_file(&path, &bytes::Bytes::from(info_s))?;
|
||||
save_file(&path, &bytes::Bytes::from(info_s)).await?;
|
||||
|
||||
Ok(info)
|
||||
}
|
||||
@@ -58,10 +80,14 @@ pub async fn download_client(
|
||||
})?;
|
||||
|
||||
let mut path = client_path.join(version);
|
||||
path.push(version);
|
||||
path.set_extension("jar");
|
||||
path.push(&format!("{version}.jar"));
|
||||
|
||||
save_and_download_file(&path, &client_download.url, Some(&client_download.sha1)).await?;
|
||||
save_and_download_file(
|
||||
&path,
|
||||
&client_download.url,
|
||||
Some(&client_download.sha1),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -69,7 +95,8 @@ 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));
|
||||
let path =
|
||||
assets_path.join(format!("indexes/{}.json", &version.asset_index.id));
|
||||
|
||||
if path.exists() {
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
@@ -77,7 +104,8 @@ pub async fn download_assets_index(
|
||||
} else {
|
||||
let index = fetch_assets_index(version).await?;
|
||||
|
||||
save_file(&path, &bytes::Bytes::from(serde_json::to_string(&index)?))?;
|
||||
save_file(&path, &bytes::Bytes::from(serde_json::to_string(&index)?))
|
||||
.await?;
|
||||
|
||||
Ok(index)
|
||||
}
|
||||
@@ -88,12 +116,9 @@ pub async fn download_assets(
|
||||
legacy_path: Option<&Path>,
|
||||
index: &AssetsIndex,
|
||||
) -> Result<(), LauncherError> {
|
||||
future::join_all(
|
||||
index
|
||||
.objects
|
||||
.iter()
|
||||
.map(|(name, asset)| download_asset(assets_path, legacy_path, name, asset)),
|
||||
)
|
||||
future::join_all(index.objects.iter().map(|(name, asset)| {
|
||||
download_asset(assets_path, legacy_path, name, asset)
|
||||
}))
|
||||
.await
|
||||
.into_iter()
|
||||
.collect::<Result<Vec<()>, LauncherError>>()?;
|
||||
@@ -114,14 +139,16 @@ async fn download_asset(
|
||||
resource_path.push(sub_hash);
|
||||
resource_path.push(hash);
|
||||
|
||||
let url = format!("https://resources.download.minecraft.net/{sub_hash}/{hash}");
|
||||
let url =
|
||||
format!("https://resources.download.minecraft.net/{sub_hash}/{hash}");
|
||||
|
||||
let resource = save_and_download_file(&resource_path, &url, Some(hash)).await?;
|
||||
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)?;
|
||||
let resource_path = legacy_path
|
||||
.join(name.replace('/', &std::path::MAIN_SEPARATOR.to_string()));
|
||||
save_file(resource_path.as_path(), &resource).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -132,11 +159,9 @@ pub async fn download_libraries(
|
||||
natives_path: &Path,
|
||||
libraries: &[Library],
|
||||
) -> Result<(), LauncherError> {
|
||||
future::join_all(
|
||||
libraries
|
||||
.iter()
|
||||
.map(|library| download_library(libraries_path, natives_path, library)),
|
||||
)
|
||||
future::join_all(libraries.iter().map(|library| {
|
||||
download_library(libraries_path, natives_path, library)
|
||||
}))
|
||||
.await
|
||||
.into_iter()
|
||||
.collect::<Result<Vec<()>, LauncherError>>()?;
|
||||
@@ -173,7 +198,8 @@ async fn download_library_jar(
|
||||
|
||||
if let Some(downloads) = &library.downloads {
|
||||
if let Some(library) = &downloads.artifact {
|
||||
save_and_download_file(&path, &library.url, Some(&library.sha1)).await?;
|
||||
save_and_download_file(&path, &library.url, Some(&library.sha1))
|
||||
.await?;
|
||||
}
|
||||
} else {
|
||||
let url = format!(
|
||||
@@ -189,16 +215,21 @@ async fn download_library_jar(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn download_native(natives_path: &Path, library: &Library) -> Result<(), LauncherError> {
|
||||
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))
|
||||
};
|
||||
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")]
|
||||
@@ -227,19 +258,25 @@ async fn save_and_download_file(
|
||||
Ok(bytes) => Ok(bytes::Bytes::from(bytes)),
|
||||
Err(_) => {
|
||||
let file = download_file(url, sha1).await?;
|
||||
save_file(path, &file)?;
|
||||
save_file(path, &file).await?;
|
||||
Ok(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn save_file(path: &Path, bytes: &bytes::Bytes) -> std::io::Result<()> {
|
||||
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() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
tokio::fs::create_dir_all(parent).await?;
|
||||
}
|
||||
|
||||
let mut file = File::create(path)?;
|
||||
file.write_all(bytes)?;
|
||||
let mut file = File::create(path).await?;
|
||||
file.write_all(bytes).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -252,7 +289,17 @@ pub fn get_os() -> Os {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn download_file(url: &str, sha1: Option<&str>) -> Result<bytes::Bytes, LauncherError> {
|
||||
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()
|
||||
@@ -307,7 +354,9 @@ pub async fn download_file(url: &str, sha1: Option<&str>) -> Result<bytes::Bytes
|
||||
|
||||
/// 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?;
|
||||
let hash =
|
||||
tokio::task::spawn_blocking(|| sha1::Sha1::from(bytes).hexdigest())
|
||||
.await?;
|
||||
|
||||
Ok(hash)
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
use crate::launcher::LauncherError;
|
||||
use std::process::Command;
|
||||
|
||||
|
||||
pub fn check_java() -> Result<String, LauncherError> {
|
||||
let child = Command::new("java")
|
||||
.arg("-version")
|
||||
.output()
|
||||
.map_err(|inner| LauncherError::ProcessError {
|
||||
inner,
|
||||
process: "java".into(),
|
||||
})?;
|
||||
|
||||
let output = String::from_utf8_lossy(&child.stderr);
|
||||
let output = output.trim_matches('\"');
|
||||
Ok(output.into())
|
||||
}
|
||||
@@ -1,20 +1,22 @@
|
||||
use daedalus::minecraft::{ArgumentType, VersionInfo};
|
||||
use daedalus::modded::LoaderVersion;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::{path::Path, process::Stdio};
|
||||
use thiserror::Error;
|
||||
use tokio::process::{Child, Command};
|
||||
|
||||
pub use crate::launcher::auth::provider::Credentials;
|
||||
|
||||
mod args;
|
||||
mod auth;
|
||||
pub mod auth;
|
||||
mod download;
|
||||
mod java;
|
||||
mod rules;
|
||||
|
||||
pub(crate) use download::init as init_download_semaphore;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum LauncherError {
|
||||
#[error("Failed to violate file checksum at url {url} with hash {hash} after {tries} tries")]
|
||||
#[error("Failed to validate file checksum at url {url} with hash {hash} after {tries} tries")]
|
||||
ChecksumFailure {
|
||||
hash: String,
|
||||
url: String,
|
||||
@@ -56,8 +58,12 @@ pub enum LauncherError {
|
||||
|
||||
#[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 {
|
||||
@@ -72,80 +78,64 @@ impl Default for ModLoader {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn launch_minecraft(
|
||||
version_name: &str,
|
||||
mod_loader: Option<ModLoader>,
|
||||
root_dir: &Path,
|
||||
credentials: &Credentials,
|
||||
) -> Result<(), LauncherError> {
|
||||
let metadata = crate::data::Metadata::get().await?;
|
||||
let settings = crate::data::Settings::get().await?;
|
||||
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",
|
||||
};
|
||||
|
||||
let versions_path = crate::util::absolute_path(root_dir.join("versions"))?;
|
||||
let libraries_path = crate::util::absolute_path(root_dir.join("libraries"))?;
|
||||
let assets_path = crate::util::absolute_path(root_dir.join("assets"))?;
|
||||
let legacy_assets_path = crate::util::absolute_path(root_dir.join("resources"))?;
|
||||
f.write_str(repr)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn launch_minecraft(
|
||||
game_version: &str,
|
||||
loader_version: &Option<LoaderVersion>,
|
||||
root_dir: &Path,
|
||||
java: &Path,
|
||||
java_args: &Vec<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;
|
||||
|
||||
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
|
||||
.minecraft
|
||||
.versions
|
||||
.iter()
|
||||
.find(|x| x.id == version_name)
|
||||
.find(|it| it.id == game_version)
|
||||
.ok_or_else(|| {
|
||||
LauncherError::InvalidInput(format!("Version {} does not exist", version_name))
|
||||
LauncherError::InvalidInput(format!(
|
||||
"Invalid game version: {game_version}",
|
||||
))
|
||||
})?;
|
||||
|
||||
let loader_version = match mod_loader.unwrap_or_default() {
|
||||
ModLoader::Vanilla => None,
|
||||
ModLoader::Forge | ModLoader::Fabric => {
|
||||
let loaders = if mod_loader.unwrap_or_default() == ModLoader::Forge {
|
||||
&metadata
|
||||
.forge
|
||||
.game_versions
|
||||
.iter()
|
||||
.find(|x| x.id == version_name)
|
||||
.ok_or_else(|| {
|
||||
LauncherError::InvalidInput(format!(
|
||||
"Version {} for mod loader Forge does not exist",
|
||||
version_name
|
||||
))
|
||||
})?
|
||||
.loaders
|
||||
} else {
|
||||
&metadata
|
||||
.fabric
|
||||
.game_versions
|
||||
.iter()
|
||||
.find(|x| x.id == version_name)
|
||||
.ok_or_else(|| {
|
||||
LauncherError::InvalidInput(format!(
|
||||
"Version {} for mod loader Fabric does not exist",
|
||||
version_name
|
||||
))
|
||||
})?
|
||||
.loaders
|
||||
};
|
||||
|
||||
let loader = if let Some(version) = loaders.iter().find(|x| x.stable) {
|
||||
Some(version.clone())
|
||||
} else {
|
||||
loaders.first().cloned()
|
||||
};
|
||||
|
||||
Some(loader.ok_or_else(|| {
|
||||
LauncherError::InvalidInput(format!(
|
||||
"No mod loader version found for version {}",
|
||||
version_name
|
||||
))
|
||||
})?)
|
||||
}
|
||||
};
|
||||
|
||||
let version_jar_name = if let Some(loader) = &loader_version {
|
||||
loader.id.clone()
|
||||
} else {
|
||||
version.id.clone()
|
||||
};
|
||||
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,
|
||||
@@ -154,23 +144,10 @@ pub async fn launch_minecraft(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let java_path = if let Some(java) = &version.java_version {
|
||||
if java.major_version == 17 || java.major_version == 16 {
|
||||
settings.java_17_path.as_deref().ok_or_else(|| LauncherError::JavaError("Please install Java 17 or select your Java 17 installation settings before launching this version!".to_string()))?
|
||||
} else {
|
||||
&settings.java_8_path.as_deref().ok_or_else(|| LauncherError::JavaError("Please install Java 8 or select your Java 8 installation settings before launching this version!".to_string()))?
|
||||
}
|
||||
} else {
|
||||
&settings.java_8_path.as_deref().ok_or_else(|| LauncherError::JavaError("Please install Java 8 or select your Java 8 installation settings before launching this version!".to_string()))?
|
||||
};
|
||||
|
||||
let client_path = crate::util::absolute_path(
|
||||
root_dir
|
||||
.join("versions")
|
||||
.join(&version.id)
|
||||
.join(format!("{}.jar", &version.id)),
|
||||
)?;
|
||||
let natives_path = crate::util::absolute_path(root_dir.join("natives").join(&version.id))?;
|
||||
let client_path = versions_path
|
||||
.join(&version.id)
|
||||
.join(format!("{}.jar", &version_jar));
|
||||
let version_natives_path = natives_path.join(&version.id);
|
||||
|
||||
download_minecraft(
|
||||
&version,
|
||||
@@ -178,7 +155,7 @@ pub async fn launch_minecraft(
|
||||
&assets_path,
|
||||
&legacy_assets_path,
|
||||
&libraries_path,
|
||||
&natives_path,
|
||||
&version_natives_path,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -201,7 +178,7 @@ pub async fn launch_minecraft(
|
||||
data.insert(
|
||||
"MINECRAFT_VERSION".to_string(),
|
||||
daedalus::modded::SidedDataEntry {
|
||||
client: version_name.to_string(),
|
||||
client: game_version.to_string(),
|
||||
server: "".to_string(),
|
||||
},
|
||||
);
|
||||
@@ -252,6 +229,7 @@ pub async fn launch_minecraft(
|
||||
data,
|
||||
)?)
|
||||
.output()
|
||||
.await
|
||||
.map_err(|err| LauncherError::ProcessError {
|
||||
inner: err,
|
||||
process: "java".to_string(),
|
||||
@@ -266,60 +244,50 @@ pub async fn launch_minecraft(
|
||||
}
|
||||
}
|
||||
|
||||
let arguments = version.arguments.unwrap_or_default();
|
||||
|
||||
let mut command = Command::new(if let Some(wrapper) = &settings.wrapper_command {
|
||||
wrapper.clone()
|
||||
} else {
|
||||
java_path.to_string()
|
||||
});
|
||||
|
||||
if settings.wrapper_command.is_some() {
|
||||
command.arg(java_path);
|
||||
}
|
||||
let arguments = version.arguments.clone().unwrap_or_default();
|
||||
let mut command = match wrapper {
|
||||
Some(hook) => {
|
||||
let mut cmd = Command::new(hook);
|
||||
cmd.arg(java);
|
||||
cmd
|
||||
}
|
||||
None => Command::new(java.to_string_lossy().to_string()),
|
||||
};
|
||||
|
||||
command
|
||||
.args(args::get_jvm_arguments(
|
||||
arguments.get(&ArgumentType::Jvm).map(|x| x.as_slice()),
|
||||
&natives_path,
|
||||
&version_natives_path,
|
||||
&libraries_path,
|
||||
&args::get_class_paths(&libraries_path, version.libraries.as_slice(), &client_path)?,
|
||||
&version_jar_name,
|
||||
settings.memory,
|
||||
settings
|
||||
.custom_java_args
|
||||
.split(" ")
|
||||
.into_iter()
|
||||
.map(|x| x.to_string())
|
||||
.collect(),
|
||||
&args::get_class_paths(
|
||||
&libraries_path,
|
||||
version.libraries.as_slice(),
|
||||
&client_path,
|
||||
)?,
|
||||
&version_jar,
|
||||
*memory,
|
||||
java_args.clone(),
|
||||
)?)
|
||||
.arg(version.main_class)
|
||||
.arg(version.main_class.clone())
|
||||
.args(args::get_minecraft_arguments(
|
||||
arguments.get(&ArgumentType::Game).map(|x| x.as_slice()),
|
||||
version.minecraft_arguments.as_deref(),
|
||||
credentials,
|
||||
&version.id,
|
||||
&version.asset_index.id,
|
||||
root_dir,
|
||||
&root_dir,
|
||||
&assets_path,
|
||||
&version.type_,
|
||||
settings.game_resolution,
|
||||
*resolution,
|
||||
)?)
|
||||
.current_dir(root_dir)
|
||||
.current_dir(root_dir.clone())
|
||||
.stdout(Stdio::inherit())
|
||||
.stderr(Stdio::inherit());
|
||||
|
||||
let mut child = command.spawn().map_err(|err| LauncherError::ProcessError {
|
||||
command.spawn().map_err(|err| LauncherError::ProcessError {
|
||||
inner: err,
|
||||
process: "minecraft".to_string(),
|
||||
})?;
|
||||
|
||||
child.wait().map_err(|err| LauncherError::ProcessError {
|
||||
inner: err,
|
||||
process: "minecraft".to_string(),
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
process: format!("minecraft-{} @ {}", &version.id, root_dir.display()),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn download_minecraft(
|
||||
@@ -330,7 +298,8 @@ pub async fn download_minecraft(
|
||||
libraries_dir: &Path,
|
||||
natives_dir: &Path,
|
||||
) -> Result<(), LauncherError> {
|
||||
let assets_index = download::download_assets_index(assets_dir, version).await?;
|
||||
let assets_index =
|
||||
download::download_assets_index(assets_dir, version).await?;
|
||||
|
||||
let (a, b, c) = futures::future::join3(
|
||||
download::download_client(versions_dir, version),
|
||||
@@ -343,7 +312,11 @@ pub async fn download_minecraft(
|
||||
},
|
||||
&assets_index,
|
||||
),
|
||||
download::download_libraries(libraries_dir, natives_dir, version.libraries.as_slice()),
|
||||
download::download_libraries(
|
||||
libraries_dir,
|
||||
natives_dir,
|
||||
version.libraries.as_slice(),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
//! Theseus is a library which provides utilities for launching minecraft, creating Modrinth mod packs,
|
||||
//! and launching Modrinth mod packs
|
||||
|
||||
#![warn(missing_docs, unused_import_braces, missing_debug_implementations)]
|
||||
#![warn(unused_import_braces, missing_debug_implementations)]
|
||||
|
||||
static LAUNCHER_WORK_DIR: &'static str = "./launcher";
|
||||
|
||||
mod data;
|
||||
pub mod data;
|
||||
pub mod launcher;
|
||||
pub mod modpack;
|
||||
mod util;
|
||||
@@ -25,9 +25,29 @@ pub enum Error {
|
||||
}
|
||||
|
||||
pub async fn init() -> Result<(), Error> {
|
||||
std::fs::create_dir_all(LAUNCHER_WORK_DIR).expect("Unable to create launcher root directory!");
|
||||
crate::data::Metadata::init().await?;
|
||||
crate::data::Settings::init().await?;
|
||||
std::fs::create_dir_all(LAUNCHER_WORK_DIR)
|
||||
.expect("Unable to create launcher root directory!");
|
||||
|
||||
use crate::data::*;
|
||||
Metadata::init().await?;
|
||||
|
||||
Settings::init().await?;
|
||||
|
||||
tokio::try_join! {
|
||||
launcher::init_download_semaphore(),
|
||||
Profiles::init(),
|
||||
}?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn save() -> Result<(), Error> {
|
||||
use crate::data::*;
|
||||
|
||||
tokio::try_join! {
|
||||
Settings::save(),
|
||||
Profiles::save(),
|
||||
}?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -22,10 +22,12 @@ pub const COMPILED_ZIP: &str = "compiled.mrpack";
|
||||
pub const MANIFEST_PATH: &str = "modrinth.index.json";
|
||||
pub const OVERRIDES_PATH: &str = "overrides/";
|
||||
pub const PACK_JSON5_PATH: &str = "modpack.json5";
|
||||
const PACK_GITIGNORE: &'static str = const_format::formatcp!(r#"
|
||||
const PACK_GITIGNORE: &'static str = const_format::formatcp!(
|
||||
r#"
|
||||
{COMPILED_PATH}
|
||||
{COMPILED_ZIP}
|
||||
"#);
|
||||
"#
|
||||
);
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum ModpackError {
|
||||
|
||||
@@ -21,7 +21,10 @@ pub trait ModrinthAPI {
|
||||
channel: &str,
|
||||
game: &ModpackGame,
|
||||
) -> ModpackResult<HashSet<ModpackFile>>;
|
||||
async fn get_version(&self, version: &str) -> ModpackResult<HashSet<ModpackFile>>;
|
||||
async fn get_version(
|
||||
&self,
|
||||
version: &str,
|
||||
) -> ModpackResult<HashSet<ModpackFile>>;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -93,6 +96,8 @@ impl ModrinthAPI for ModrinthV1 {
|
||||
String::from("Modrinth V1 does not support vanilla projects"),
|
||||
)),
|
||||
ModpackGame::Minecraft(ref version, ref loader) => Ok((version, loader)),
|
||||
// This guard is here for when Modrinth does support other games.
|
||||
#[allow(unreachable_patterns)]
|
||||
_ => Err(ModpackError::VersionError(String::from(
|
||||
"Attempted to use Modrinth API V1 to install a non-Minecraft project!",
|
||||
))),
|
||||
@@ -131,7 +136,8 @@ impl ModrinthAPI for ModrinthV1 {
|
||||
.map(ModpackFile::from)
|
||||
.collect::<HashSet<ModpackFile>>();
|
||||
|
||||
let dep_futures = version.dependencies.iter().map(|it| self.get_version(it));
|
||||
let dep_futures =
|
||||
version.dependencies.iter().map(|it| self.get_version(it));
|
||||
let deps = try_join_all(dep_futures)
|
||||
.await?
|
||||
.into_iter()
|
||||
@@ -148,12 +154,17 @@ impl ModrinthAPI for ModrinthV1 {
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn get_version(&self, version: &str) -> ModpackResult<HashSet<ModpackFile>> {
|
||||
async fn get_version(
|
||||
&self,
|
||||
version: &str,
|
||||
) -> ModpackResult<HashSet<ModpackFile>> {
|
||||
let domain = &self.0;
|
||||
let version_json = try_get_json(format!("{domain}/api/v1/version/{version}")).await?;
|
||||
let mut version_deserializer = serde_json::Deserializer::from_slice(&version_json);
|
||||
let version = ModrinthV1ProjectVersion::deserialize(&mut version_deserializer)?;
|
||||
let base_path = PathBuf::from("mods/");
|
||||
let version_json =
|
||||
try_get_json(format!("{domain}/api/v1/version/{version}")).await?;
|
||||
let mut version_deserializer =
|
||||
serde_json::Deserializer::from_slice(&version_json);
|
||||
let version =
|
||||
ModrinthV1ProjectVersion::deserialize(&mut version_deserializer)?;
|
||||
|
||||
Ok(version
|
||||
.files
|
||||
|
||||
@@ -159,6 +159,7 @@ pub struct ModpackFile {
|
||||
pub downloads: HashSet<String>,
|
||||
}
|
||||
|
||||
#[allow(clippy::derive_hash_xor_eq)]
|
||||
impl Hash for ModpackFile {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
if let Some(ref hashes) = self.hashes {
|
||||
|
||||
Reference in New Issue
Block a user