added box pins (#110)

* added box pins

* clippy
This commit is contained in:
Wyatt Verchere 2023-05-09 12:05:52 -07:00 committed by GitHub
parent 1796c48fc7
commit da4fc1c835
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 1146 additions and 1095 deletions

View File

@ -79,97 +79,101 @@ pub async fn install_pack_from_version_id(
title: Option<String>, title: Option<String>,
) -> crate::Result<PathBuf> { ) -> crate::Result<PathBuf> {
let state = State::get().await?; let state = State::get().await?;
Box::pin(async move {
let loading_bar = init_loading(
LoadingBarType::PackFileDownload {
pack_name: title,
pack_version: version_id.clone(),
},
100.0,
"Downloading pack file",
)
.await?;
let loading_bar = init_loading( emit_loading(&loading_bar, 0.0, Some("Fetching version")).await?;
LoadingBarType::PackFileDownload { let version: ModrinthVersion = fetch_json(
pack_name: title, Method::GET,
pack_version: version_id.clone(), &format!("{}version/{}", MODRINTH_API_URL, version_id),
}, None,
100.0, None,
"Downloading pack file", &state.fetch_semaphore,
) )
.await?; .await?;
emit_loading(&loading_bar, 10.0, None).await?;
emit_loading(&loading_bar, 0.0, Some("Fetching version")).await?; let (url, hash) =
let version: ModrinthVersion = fetch_json( if let Some(file) = version.files.iter().find(|x| x.primary) {
Method::GET, Some((file.url.clone(), file.hashes.get("sha1")))
&format!("{}version/{}", MODRINTH_API_URL, version_id), } else {
None, version
None, .files
&state.fetch_semaphore, .first()
) .map(|file| (file.url.clone(), file.hashes.get("sha1")))
.await?; }
emit_loading(&loading_bar, 10.0, None).await?; .ok_or_else(|| {
crate::ErrorKind::InputError(
let (url, hash) = "Specified version has no files".to_string(),
if let Some(file) = version.files.iter().find(|x| x.primary) {
Some((file.url.clone(), file.hashes.get("sha1")))
} else {
version
.files
.first()
.map(|file| (file.url.clone(), file.hashes.get("sha1")))
}
.ok_or_else(|| {
crate::ErrorKind::InputError(
"Specified version has no files".to_string(),
)
})?;
let file = fetch_advanced(
Method::GET,
&url,
hash.map(|x| &**x),
None,
None,
Some((&loading_bar, 70.0)),
&state.fetch_semaphore,
)
.await?;
emit_loading(&loading_bar, 0.0, Some("Fetching project metadata")).await?;
let project: ModrinthProject = fetch_json(
Method::GET,
&format!("{}project/{}", MODRINTH_API_URL, version.project_id),
None,
None,
&state.fetch_semaphore,
)
.await?;
emit_loading(&loading_bar, 10.0, Some("Retrieving icon")).await?;
let icon = if let Some(icon_url) = project.icon_url {
let state = State::get().await?;
let icon_bytes = fetch(&icon_url, None, &state.fetch_semaphore).await?;
let filename = icon_url.rsplit('/').next();
if let Some(filename) = filename {
Some(
write_cached_icon(
filename,
&state.directories.caches_dir(),
icon_bytes,
&state.io_semaphore,
) )
.await?, })?;
)
let file = fetch_advanced(
Method::GET,
&url,
hash.map(|x| &**x),
None,
None,
Some((&loading_bar, 70.0)),
&state.fetch_semaphore,
)
.await?;
emit_loading(&loading_bar, 0.0, Some("Fetching project metadata"))
.await?;
let project: ModrinthProject = fetch_json(
Method::GET,
&format!("{}project/{}", MODRINTH_API_URL, version.project_id),
None,
None,
&state.fetch_semaphore,
)
.await?;
emit_loading(&loading_bar, 10.0, Some("Retrieving icon")).await?;
let icon = if let Some(icon_url) = project.icon_url {
let state = State::get().await?;
let icon_bytes =
fetch(&icon_url, None, &state.fetch_semaphore).await?;
let filename = icon_url.rsplit('/').next();
if let Some(filename) = filename {
Some(
write_cached_icon(
filename,
&state.directories.caches_dir(),
icon_bytes,
&state.io_semaphore,
)
.await?,
)
} else {
None
}
} else { } else {
None None
} };
} else { emit_loading(&loading_bar, 10.0, None).await?;
None
};
emit_loading(&loading_bar, 10.0, None).await?;
install_pack( install_pack(
file, file,
icon, icon,
Some(project.title), Some(project.title),
Some(version.project_id), Some(version.project_id),
Some(version.id), Some(version.id),
Some(loading_bar), Some(loading_bar),
) )
.await
})
.await .await
} }
@ -189,243 +193,259 @@ async fn install_pack(
) -> crate::Result<PathBuf> { ) -> crate::Result<PathBuf> {
let state = &State::get().await?; let state = &State::get().await?;
let reader = Cursor::new(&file); Box::pin(async move {
let reader: Cursor<&bytes::Bytes> = Cursor::new(&file);
// Create zip reader around file // Create zip reader around file
let mut zip_reader = ZipFileReader::new(reader).await.map_err(|_| { let mut zip_reader =
crate::Error::from(crate::ErrorKind::InputError( ZipFileReader::new(reader).await.map_err(|_| {
"Failed to read input modpack zip".to_string(), crate::Error::from(crate::ErrorKind::InputError(
)) "Failed to read input modpack zip".to_string(),
})?; ))
})?;
// Extract index of modrinth.index.json // Extract index of modrinth.index.json
let zip_index_option = zip_reader let zip_index_option = zip_reader
.file()
.entries()
.iter()
.position(|f| f.entry().filename() == "modrinth.index.json");
if let Some(zip_index) = zip_index_option {
let mut manifest = String::new();
let entry = zip_reader
.file() .file()
.entries() .entries()
.get(zip_index) .iter()
.unwrap() .position(|f| f.entry().filename() == "modrinth.index.json");
.entry() if let Some(zip_index) = zip_index_option {
.clone(); let mut manifest = String::new();
let mut reader = zip_reader.entry(zip_index).await?; let entry = zip_reader
reader.read_to_string_checked(&mut manifest, &entry).await?; .file()
.entries()
.get(zip_index)
.unwrap()
.entry()
.clone();
let mut reader = zip_reader.entry(zip_index).await?;
reader.read_to_string_checked(&mut manifest, &entry).await?;
let pack: PackFormat = serde_json::from_str(&manifest)?; let pack: PackFormat = serde_json::from_str(&manifest)?;
if &*pack.game != "minecraft" { if &*pack.game != "minecraft" {
return Err(crate::ErrorKind::InputError( return Err(crate::ErrorKind::InputError(
"Pack does not support Minecraft".to_string(), "Pack does not support Minecraft".to_string(),
) )
.into()); .into());
}
let mut game_version = None;
let mut mod_loader = None;
let mut loader_version = None;
for (key, value) in &pack.dependencies {
match key {
PackDependency::Forge => {
mod_loader = Some(ModLoader::Forge);
loader_version = Some(value);
}
PackDependency::FabricLoader => {
mod_loader = Some(ModLoader::Fabric);
loader_version = Some(value);
}
PackDependency::QuiltLoader => {
mod_loader = Some(ModLoader::Quilt);
loader_version = Some(value);
}
PackDependency::Minecraft => game_version = Some(value),
} }
}
let game_version = if let Some(game_version) = game_version { let mut game_version = None;
game_version let mut mod_loader = None;
} else { let mut loader_version = None;
return Err(crate::ErrorKind::InputError( for (key, value) in &pack.dependencies {
"Pack did not specify Minecraft version".to_string(), match key {
) PackDependency::Forge => {
.into()); mod_loader = Some(ModLoader::Forge);
}; loader_version = Some(value);
let profile_raw = crate::api::profile_create::profile_create(
override_title.unwrap_or_else(|| pack.name.clone()),
game_version.clone(),
mod_loader.unwrap_or(ModLoader::Vanilla),
loader_version.cloned(),
icon,
Some(LinkedData {
project_id: project_id.clone(),
version_id: version_id.clone(),
}),
Some(true),
)
.await?;
let profile = profile_raw.clone();
let result = async {
let loading_bar = init_or_edit_loading(
existing_loading_bar,
LoadingBarType::PackDownload {
pack_name: pack.name.clone(),
pack_id: project_id,
pack_version: version_id,
},
100.0,
"Downloading modpack",
)
.await?;
let num_files = pack.files.len();
use futures::StreamExt;
loading_try_for_each_concurrent(
futures::stream::iter(pack.files.into_iter())
.map(Ok::<PackFile, crate::Error>),
None,
Some(&loading_bar),
70.0,
num_files,
None,
|project| {
let profile = profile.clone();
async move {
//TODO: Future update: prompt user for optional files in a modpack
if let Some(env) = project.env {
if env
.get(&EnvType::Client)
.map(|x| x == &SideType::Unsupported)
.unwrap_or(false)
{
return Ok(());
}
}
let file = fetch_mirrors(
&project
.downloads
.iter()
.map(|x| &**x)
.collect::<Vec<&str>>(),
project
.hashes
.get(&PackFileHash::Sha1)
.map(|x| &**x),
&state.fetch_semaphore,
)
.await?;
let path = std::path::Path::new(&project.path)
.components()
.next();
if let Some(path) = path {
match path {
Component::CurDir | Component::Normal(_) => {
let path = profile.join(project.path);
write(&path, &file, &state.io_semaphore)
.await?;
}
_ => {}
};
}
Ok(())
} }
}, PackDependency::FabricLoader => {
) mod_loader = Some(ModLoader::Fabric);
.await?; loader_version = Some(value);
let extract_overrides = |overrides: String| async {
let reader = Cursor::new(&file);
let mut overrides_zip =
ZipFileReader::new(reader).await.map_err(|_| {
crate::Error::from(crate::ErrorKind::InputError(
"Failed extract overrides Zip".to_string(),
))
})?;
let profile = profile.clone();
async move {
for index in 0..overrides_zip.file().entries().len() {
let file = overrides_zip
.file()
.entries()
.get(index)
.unwrap()
.entry()
.clone();
let file_path = PathBuf::from(file.filename());
if file.filename().starts_with(&overrides)
&& !file.filename().ends_with('/')
{
// Reads the file into the 'content' variable
let mut content = Vec::new();
let mut reader = overrides_zip.entry(index).await?;
reader
.read_to_end_checked(&mut content, &file)
.await?;
let mut new_path = PathBuf::new();
let components = file_path.components().skip(1);
for component in components {
new_path.push(component);
}
if new_path.file_name().is_some() {
write(
&profile.join(new_path),
&content,
&state.io_semaphore,
)
.await?;
}
}
} }
PackDependency::QuiltLoader => {
Ok::<(), crate::Error>(()) mod_loader = Some(ModLoader::Quilt);
loader_version = Some(value);
}
PackDependency::Minecraft => game_version = Some(value),
} }
.await }
let game_version = if let Some(game_version) = game_version {
game_version
} else {
return Err(crate::ErrorKind::InputError(
"Pack did not specify Minecraft version".to_string(),
)
.into());
}; };
emit_loading(&loading_bar, 0.0, Some("Extracting overrides")) let profile_raw = crate::api::profile_create::profile_create(
.await?; override_title.unwrap_or_else(|| pack.name.clone()),
extract_overrides("overrides".to_string()).await?; game_version.clone(),
extract_overrides("client_overrides".to_string()).await?; mod_loader.unwrap_or(ModLoader::Vanilla),
emit_loading(&loading_bar, 29.9, Some("Done extacting overrides")) loader_version.cloned(),
icon,
Some(LinkedData {
project_id: project_id.clone(),
version_id: version_id.clone(),
}),
Some(true),
)
.await?;
let profile = profile_raw.clone();
let result = async {
let loading_bar = init_or_edit_loading(
existing_loading_bar,
LoadingBarType::PackDownload {
pack_name: pack.name.clone(),
pack_id: project_id,
pack_version: version_id,
},
100.0,
"Downloading modpack",
)
.await?; .await?;
if let Some(profile) = crate::api::profile::get(&profile).await? { let num_files = pack.files.len();
tokio::try_join!( use futures::StreamExt;
super::profile::sync(&profile.path), loading_try_for_each_concurrent(
crate::launcher::install_minecraft( futures::stream::iter(pack.files.into_iter())
&profile, .map(Ok::<PackFile, crate::Error>),
Some(loading_bar) None,
), Some(&loading_bar),
)?; 70.0,
num_files,
None,
|project| {
let profile = profile.clone();
async move {
//TODO: Future update: prompt user for optional files in a modpack
if let Some(env) = project.env {
if env
.get(&EnvType::Client)
.map(|x| x == &SideType::Unsupported)
.unwrap_or(false)
{
return Ok(());
}
}
let file = fetch_mirrors(
&project
.downloads
.iter()
.map(|x| &**x)
.collect::<Vec<&str>>(),
project
.hashes
.get(&PackFileHash::Sha1)
.map(|x| &**x),
&state.fetch_semaphore,
)
.await?;
let path = std::path::Path::new(&project.path)
.components()
.next();
if let Some(path) = path {
match path {
Component::CurDir
| Component::Normal(_) => {
let path = profile.join(project.path);
write(
&path,
&file,
&state.io_semaphore,
)
.await?;
}
_ => {}
};
}
Ok(())
}
},
)
.await?;
let extract_overrides = |overrides: String| async {
let reader = Cursor::new(&file);
let mut overrides_zip =
ZipFileReader::new(reader).await.map_err(|_| {
crate::Error::from(crate::ErrorKind::InputError(
"Failed extract overrides Zip".to_string(),
))
})?;
let profile = profile.clone();
async move {
for index in 0..overrides_zip.file().entries().len() {
let file = overrides_zip
.file()
.entries()
.get(index)
.unwrap()
.entry()
.clone();
let file_path = PathBuf::from(file.filename());
if file.filename().starts_with(&overrides)
&& !file.filename().ends_with('/')
{
// Reads the file into the 'content' variable
let mut content = Vec::new();
let mut reader =
overrides_zip.entry(index).await?;
reader
.read_to_end_checked(&mut content, &file)
.await?;
let mut new_path = PathBuf::new();
let components = file_path.components().skip(1);
for component in components {
new_path.push(component);
}
if new_path.file_name().is_some() {
write(
&profile.join(new_path),
&content,
&state.io_semaphore,
)
.await?;
}
}
}
Ok::<(), crate::Error>(())
}
.await
};
emit_loading(&loading_bar, 0.0, Some("Extracting overrides"))
.await?;
extract_overrides("overrides".to_string()).await?;
extract_overrides("client_overrides".to_string()).await?;
emit_loading(
&loading_bar,
29.9,
Some("Done extacting overrides"),
)
.await?;
if let Some(profile) =
crate::api::profile::get(&profile).await?
{
tokio::try_join!(
super::profile::sync(&profile.path),
crate::launcher::install_minecraft(
&profile,
Some(loading_bar)
),
)?;
}
Ok::<PathBuf, crate::Error>(profile)
} }
.await;
Ok::<PathBuf, crate::Error>(profile) match result {
} Ok(profile) => Ok(profile),
.await; Err(err) => {
let _ = crate::api::profile::remove(&profile_raw).await;
match result { Err(err)
Ok(profile) => Ok(profile), }
Err(err) => {
let _ = crate::api::profile::remove(&profile_raw).await;
Err(err)
} }
} else {
Err(crate::Error::from(crate::ErrorKind::InputError(
"No pack manifest found in mrpack".to_string(),
)))
} }
} else { })
Err(crate::Error::from(crate::ErrorKind::InputError( .await
"No pack manifest found in mrpack".to_string(),
)))
}
} }

View File

@ -139,41 +139,44 @@ pub async fn install(path: &Path) -> crate::Result<()> {
pub async fn update_all(profile_path: &Path) -> crate::Result<()> { pub async fn update_all(profile_path: &Path) -> crate::Result<()> {
let state = State::get().await?; let state = State::get().await?;
let mut profiles = state.profiles.write().await; Box::pin(async move {
let mut profiles = state.profiles.write().await;
if let Some(profile) = profiles.0.get_mut(profile_path) { if let Some(profile) = profiles.0.get_mut(profile_path) {
let loading_bar = init_loading( let loading_bar = init_loading(
LoadingBarType::ProfileUpdate { LoadingBarType::ProfileUpdate {
profile_uuid: profile.uuid, profile_uuid: profile.uuid,
profile_name: profile.metadata.name.clone(), profile_name: profile.metadata.name.clone(),
}, },
100.0, 100.0,
"Updating profile", "Updating profile",
) )
.await?; .await?;
use futures::StreamExt; use futures::StreamExt;
loading_try_for_each_concurrent( loading_try_for_each_concurrent(
futures::stream::iter(profile.projects.keys()) futures::stream::iter(profile.projects.keys())
.map(Ok::<&PathBuf, crate::Error>), .map(Ok::<&PathBuf, crate::Error>),
None, None,
Some(&loading_bar), Some(&loading_bar),
100.0, 100.0,
profile.projects.keys().len(), profile.projects.keys().len(),
None, None,
|project| update_project(profile_path, project, Some(true)), |project| update_project(profile_path, project, Some(true)),
) )
.await?; .await?;
profile.sync().await?; profile.sync().await?;
Ok(()) Ok(())
} else { } else {
Err(crate::ErrorKind::UnmanagedProfileError( Err(crate::ErrorKind::UnmanagedProfileError(
profile_path.display().to_string(), profile_path.display().to_string(),
) )
.as_error()) .as_error())
} }
})
.await
} }
pub async fn update_project( pub async fn update_project(
@ -372,146 +375,148 @@ pub async fn run_credentials(
path: &Path, path: &Path,
credentials: &auth::Credentials, credentials: &auth::Credentials,
) -> crate::Result<Arc<RwLock<MinecraftChild>>> { ) -> crate::Result<Arc<RwLock<MinecraftChild>>> {
let state = State::get().await?; Box::pin(async move {
let settings = state.settings.read().await; let state = State::get().await?;
let metadata = state.metadata.read().await; let settings = state.settings.read().await;
let profile = get(path).await?.ok_or_else(|| { let metadata = state.metadata.read().await;
crate::ErrorKind::OtherError(format!( let profile = get(path).await?.ok_or_else(|| {
"Tried to run a nonexistent or unloaded profile at path {}!", crate::ErrorKind::OtherError(format!(
path.display() "Tried to run a nonexistent or unloaded profile at path {}!",
)) path.display()
})?;
let version = metadata
.minecraft
.versions
.iter()
.find(|it| it.id == profile.metadata.game_version)
.ok_or_else(|| {
crate::ErrorKind::LauncherError(format!(
"Invalid or unknown Minecraft version: {}",
profile.metadata.game_version
)) ))
})?; })?;
let version_info = download::download_version_info(
&state,
version,
profile.metadata.loader_version.as_ref(),
None,
None,
)
.await?;
let pre_launch_hooks =
&profile.hooks.as_ref().unwrap_or(&settings.hooks).pre_launch;
if let Some(hook) = pre_launch_hooks {
// TODO: hook parameters
let mut cmd = hook.split(' ');
if let Some(command) = cmd.next() {
let result = Command::new(command)
.args(&cmd.collect::<Vec<&str>>())
.current_dir(path)
.spawn()?
.wait()
.await?;
if !result.success() { let version = metadata
return Err(crate::ErrorKind::LauncherError(format!( .minecraft
"Non-zero exit code for pre-launch hook: {}", .versions
result.code().unwrap_or(-1) .iter()
.find(|it| it.id == profile.metadata.game_version)
.ok_or_else(|| {
crate::ErrorKind::LauncherError(format!(
"Invalid or unknown Minecraft version: {}",
profile.metadata.game_version
)) ))
.as_error()); })?;
let version_info = download::download_version_info(
&state,
version,
profile.metadata.loader_version.as_ref(),
None,
None,
)
.await?;
let pre_launch_hooks =
&profile.hooks.as_ref().unwrap_or(&settings.hooks).pre_launch;
if let Some(hook) = pre_launch_hooks {
// TODO: hook parameters
let mut cmd = hook.split(' ');
if let Some(command) = cmd.next() {
let result = Command::new(command)
.args(&cmd.collect::<Vec<&str>>())
.current_dir(path)
.spawn()?
.wait()
.await?;
if !result.success() {
return Err(crate::ErrorKind::LauncherError(format!(
"Non-zero exit code for pre-launch hook: {}",
result.code().unwrap_or(-1)
))
.as_error());
}
} }
} }
}
let java_version = match profile.java { let java_version = match profile.java {
// Load profile-specific Java implementation choice // Load profile-specific Java implementation choice
// (This defaults to Daedalus-decided key on init, but can be changed by the user) // (This defaults to Daedalus-decided key on init, but can be changed by the user)
Some(JavaSettings { Some(JavaSettings {
jre_key: Some(ref jre_key), jre_key: Some(ref jre_key),
.. ..
}) => settings.java_globals.get(jre_key), }) => settings.java_globals.get(jre_key),
// Fall back to Daedalus-decided key if no profile-specific key is set // Fall back to Daedalus-decided key if no profile-specific key is set
_ => { _ => {
match version_info match version_info
.java_version .java_version
.as_ref() .as_ref()
.map(|it| it.major_version) .map(|it| it.major_version)
.unwrap_or(0) .unwrap_or(0)
{ {
0..=16 => settings 0..=16 => settings
.java_globals .java_globals
.get(&crate::jre::JAVA_8_KEY.to_string()), .get(&crate::jre::JAVA_8_KEY.to_string()),
17 => settings 17 => settings
.java_globals .java_globals
.get(&crate::jre::JAVA_17_KEY.to_string()), .get(&crate::jre::JAVA_17_KEY.to_string()),
_ => settings _ => settings
.java_globals .java_globals
.get(&crate::jre::JAVA_18PLUS_KEY.to_string()), .get(&crate::jre::JAVA_18PLUS_KEY.to_string()),
}
} }
};
let java_version = java_version.as_ref().ok_or_else(|| {
crate::ErrorKind::LauncherError(format!(
"No Java stored for version {}",
version_info.java_version.map_or(8, |it| it.major_version),
))
})?;
// Get the path to the Java executable from the chosen Java implementation key
let java_install: &Path = &PathBuf::from(&java_version.path);
if !java_install.exists() {
return Err(crate::ErrorKind::LauncherError(format!(
"Could not find Java install: {}",
java_install.display()
))
.as_error());
} }
}; let java_args = profile
let java_version = java_version.as_ref().ok_or_else(|| { .java
crate::ErrorKind::LauncherError(format!( .as_ref()
"No Java stored for version {}", .and_then(|it| it.extra_arguments.as_ref())
version_info.java_version.map_or(8, |it| it.major_version), .unwrap_or(&settings.custom_java_args);
))
})?;
// Get the path to the Java executable from the chosen Java implementation key let wrapper = profile
let java_install: &Path = &PathBuf::from(&java_version.path); .hooks
if !java_install.exists() { .as_ref()
return Err(crate::ErrorKind::LauncherError(format!( .map_or(&settings.hooks.wrapper, |it| &it.wrapper);
"Could not find Java install: {}",
java_install.display()
))
.as_error());
}
let java_args = profile
.java
.as_ref()
.and_then(|it| it.extra_arguments.as_ref())
.unwrap_or(&settings.custom_java_args);
let wrapper = profile let memory = profile.memory.unwrap_or(settings.memory);
.hooks let resolution = profile.resolution.unwrap_or(settings.game_resolution);
.as_ref()
.map_or(&settings.hooks.wrapper, |it| &it.wrapper);
let memory = profile.memory.unwrap_or(settings.memory); let env_args = &settings.custom_env_args;
let resolution = profile.resolution.unwrap_or(settings.game_resolution);
let env_args = &settings.custom_env_args; // Post post exit hooks
let post_exit_hook =
&profile.hooks.as_ref().unwrap_or(&settings.hooks).post_exit;
// Post post exit hooks let post_exit_hook = if let Some(hook) = post_exit_hook {
let post_exit_hook = let mut cmd = hook.split(' ');
&profile.hooks.as_ref().unwrap_or(&settings.hooks).post_exit; if let Some(command) = cmd.next() {
let mut command = Command::new(command);
let post_exit_hook = if let Some(hook) = post_exit_hook { command.args(&cmd.collect::<Vec<&str>>()).current_dir(path);
let mut cmd = hook.split(' '); Some(command)
if let Some(command) = cmd.next() { } else {
let mut command = Command::new(command); None
command.args(&cmd.collect::<Vec<&str>>()).current_dir(path); }
Some(command)
} else { } else {
None None
} };
} else {
None
};
let mc_process = crate::launcher::launch_minecraft( let mc_process = crate::launcher::launch_minecraft(
java_install, java_install,
java_args, java_args,
env_args, env_args,
wrapper, wrapper,
&memory, &memory,
&resolution, &resolution,
credentials, credentials,
post_exit_hook, post_exit_hook,
&profile, &profile,
) )
.await?; .await?;
Ok(mc_process)
Ok(mc_process) })
.await
} }

View File

@ -51,153 +51,156 @@ pub async fn profile_create(
) -> crate::Result<PathBuf> { ) -> crate::Result<PathBuf> {
trace!("Creating new profile. {}", name); trace!("Creating new profile. {}", name);
let state = State::get().await?; let state = State::get().await?;
let metadata = state.metadata.read().await; Box::pin(async move {
let metadata = state.metadata.read().await;
let uuid = Uuid::new_v4(); let uuid = Uuid::new_v4();
let path = state.directories.profiles_dir().join(uuid.to_string()); let path = state.directories.profiles_dir().join(uuid.to_string());
if path.exists() { if path.exists() {
if !path.is_dir() { if !path.is_dir() {
return Err(ProfileCreationError::NotFolder.into()); return Err(ProfileCreationError::NotFolder.into());
}
if path.join("profile.json").exists() {
return Err(ProfileCreationError::ProfileExistsError(
path.join("profile.json"),
)
.into());
}
if ReadDirStream::new(fs::read_dir(&path).await?)
.next()
.await
.is_some()
{
return Err(ProfileCreationError::NotEmptyFolder.into());
}
} else {
fs::create_dir_all(&path).await?;
}
info!(
"Creating profile at path {}",
&canonicalize(&path)?.display()
);
let loader = modloader;
let loader = if loader != ModLoader::Vanilla {
let version = loader_version.unwrap_or_else(|| "latest".to_string());
let filter = |it: &LoaderVersion| match version.as_str() {
"latest" => true,
"stable" => it.stable,
id => it.id == *id || format!("{}-{}", game_version, id) == it.id,
};
let loader_data = match loader {
ModLoader::Forge => &metadata.forge,
ModLoader::Fabric => &metadata.fabric,
_ => {
return Err(ProfileCreationError::NoManifest(
loader.to_string(),
)
.into())
} }
if path.join("profile.json").exists() {
return Err(ProfileCreationError::ProfileExistsError(
path.join("profile.json"),
)
.into());
}
if ReadDirStream::new(fs::read_dir(&path).await?)
.next()
.await
.is_some()
{
return Err(ProfileCreationError::NotEmptyFolder.into());
}
} else {
fs::create_dir_all(&path).await?;
}
info!(
"Creating profile at path {}",
&canonicalize(&path)?.display()
);
let loader = modloader;
let loader = if loader != ModLoader::Vanilla {
let version = loader_version.unwrap_or_else(|| "latest".to_string());
let filter = |it: &LoaderVersion| match version.as_str() {
"latest" => true,
"stable" => it.stable,
id => it.id == *id || format!("{}-{}", game_version, id) == it.id,
};
let loader_data = match loader {
ModLoader::Forge => &metadata.forge,
ModLoader::Fabric => &metadata.fabric,
_ => {
return Err(ProfileCreationError::NoManifest(
loader.to_string(),
)
.into())
}
};
let loaders = &loader_data
.game_versions
.iter()
.find(|it| {
it.id.replace(
daedalus::modded::DUMMY_REPLACE_STRING,
&game_version,
) == game_version
})
.ok_or_else(|| {
ProfileCreationError::ModloaderUnsupported(
loader.to_string(),
game_version.clone(),
)
})?
.loaders;
let loader_version = loaders
.iter()
.cloned()
.find(filter)
.or(
// If stable was searched for but not found, return latest by default
if version == "stable" {
loaders.iter().next().cloned()
} else {
None
},
)
.ok_or_else(|| {
ProfileCreationError::InvalidVersionModloader(
version,
loader.to_string(),
)
})?;
Some((loader_version, loader))
} else {
None
}; };
let loaders = &loader_data // Fully canonicalize now that its created for storing purposes
.game_versions let path = canonicalize(&path)?;
.iter() let mut profile =
.find(|it| { Profile::new(uuid, name, game_version, path.clone()).await?;
it.id.replace( if let Some(ref icon) = icon {
daedalus::modded::DUMMY_REPLACE_STRING, let bytes = tokio::fs::read(icon).await?;
&game_version, profile
) == game_version .set_icon(
}) &state.directories.caches_dir(),
.ok_or_else(|| { &state.io_semaphore,
ProfileCreationError::ModloaderUnsupported( bytes::Bytes::from(bytes),
loader.to_string(), &icon.to_string_lossy(),
game_version.clone(),
) )
})? .await?;
.loaders; }
if let Some((loader_version, loader)) = loader {
profile.metadata.loader = loader;
profile.metadata.loader_version = Some(loader_version);
}
let loader_version = loaders profile.metadata.linked_data = linked_data;
.iter()
.cloned()
.find(filter)
.or(
// If stable was searched for but not found, return latest by default
if version == "stable" {
loaders.iter().next().cloned()
} else {
None
},
)
.ok_or_else(|| {
ProfileCreationError::InvalidVersionModloader(
version,
loader.to_string(),
)
})?;
Some((loader_version, loader)) // Attempts to find optimal JRE for the profile from the JavaGlobals
} else { // Finds optimal key, and see if key has been set in JavaGlobals
None let settings = state.settings.read().await;
}; let optimal_version_key = jre::get_optimal_jre_key(&profile).await?;
if settings.java_globals.get(&optimal_version_key).is_some() {
profile.java = Some(JavaSettings {
jre_key: Some(optimal_version_key),
extra_arguments: None,
});
} else {
emit_warning(&format!("Could not detect optimal JRE: {optimal_version_key}, falling back to system default.")).await?;
}
// Fully canonicalize now that its created for storing purposes emit_profile(
let path = canonicalize(&path)?; uuid,
let mut profile = path.clone(),
Profile::new(uuid, name, game_version, path.clone()).await?; &profile.metadata.name,
if let Some(ref icon) = icon { ProfilePayloadType::Created,
let bytes = tokio::fs::read(icon).await?; )
profile .await?;
.set_icon(
&state.directories.caches_dir(),
&state.io_semaphore,
bytes::Bytes::from(bytes),
&icon.to_string_lossy(),
)
.await?;
}
if let Some((loader_version, loader)) = loader {
profile.metadata.loader = loader;
profile.metadata.loader_version = Some(loader_version);
}
profile.metadata.linked_data = linked_data; {
let mut profiles = state.profiles.write().await;
profiles.insert(profile.clone()).await?;
}
// Attempts to find optimal JRE for the profile from the JavaGlobals if !skip_install_profile.unwrap_or(false) {
// Finds optimal key, and see if key has been set in JavaGlobals crate::launcher::install_minecraft(&profile, None).await?;
let settings = state.settings.read().await; }
let optimal_version_key = jre::get_optimal_jre_key(&profile).await?; State::sync().await?;
if settings.java_globals.get(&optimal_version_key).is_some() {
profile.java = Some(JavaSettings {
jre_key: Some(optimal_version_key),
extra_arguments: None,
});
} else {
emit_warning(&format!("Could not detect optimal JRE: {optimal_version_key}, falling back to system default.")).await?;
}
emit_profile( Ok(path)
uuid,
path.clone(),
&profile.metadata.name,
ProfilePayloadType::Created,
)
.await?;
{ }).await
let mut profiles = state.profiles.write().await;
profiles.insert(profile.clone()).await?;
}
if !skip_install_profile.unwrap_or(false) {
crate::launcher::install_minecraft(&profile, None).await?;
}
State::sync().await?;
Ok(path)
} }
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]

View File

@ -49,53 +49,61 @@ pub async fn download_version_info(
force: Option<bool>, force: Option<bool>,
loading_bar: Option<&LoadingBarId>, loading_bar: Option<&LoadingBarId>,
) -> crate::Result<GameVersionInfo> { ) -> crate::Result<GameVersionInfo> {
let version_id = loader Box::pin(async move {
.map_or(version.id.clone(), |it| format!("{}-{}", version.id, it.id)); let version_id = loader.map_or(version.id.clone(), |it| {
tracing::debug!("Loading version info for Minecraft {version_id}"); format!("{}-{}", version.id, it.id)
let path = st });
.directories tracing::debug!("Loading version info for Minecraft {version_id}");
.version_dir(&version_id) let path = st
.join(format!("{version_id}.json")); .directories
.version_dir(&version_id)
.join(format!("{version_id}.json"));
let res = if path.exists() && !force.unwrap_or(false) { let res = if path.exists() && !force.unwrap_or(false) {
fs::read(path) fs::read(path)
.err_into::<crate::Error>() .err_into::<crate::Error>()
.await .await
.and_then(|ref it| Ok(serde_json::from_slice(it)?)) .and_then(|ref it| Ok(serde_json::from_slice(it)?))
} else { } else {
tracing::info!("Downloading version info for version {}", &version.id); tracing::info!(
let mut info = d::minecraft::fetch_version_info(version).await?; "Downloading version info for version {}",
&version.id
);
let mut info = d::minecraft::fetch_version_info(version).await?;
if let Some(loader) = loader { if let Some(loader) = loader {
let partial = d::modded::fetch_partial_version(&loader.url).await?; let partial =
info = d::modded::merge_partial_version(partial, info); 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,
if res
.processors
.as_ref()
.map(|x| !x.is_empty())
.unwrap_or(false)
{
5.0
} else {
15.0
},
None,
)
.await?;
} }
info.id = version_id.clone();
write(&path, &serde_json::to_vec(&info)?, &st.io_semaphore).await?; tracing::debug!("Loaded version info for Minecraft {version_id}");
Ok(info) Ok(res)
}?; })
.await
if let Some(loading_bar) = loading_bar {
emit_loading(
loading_bar,
if res
.processors
.as_ref()
.map(|x| !x.is_empty())
.unwrap_or(false)
{
5.0
} else {
15.0
},
None,
)
.await?;
}
tracing::debug!("Loaded version info for Minecraft {version_id}");
Ok(res)
} }
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
@ -104,38 +112,41 @@ pub async fn download_client(
version_info: &GameVersionInfo, version_info: &GameVersionInfo,
loading_bar: Option<&LoadingBarId>, loading_bar: Option<&LoadingBarId>,
) -> crate::Result<()> { ) -> crate::Result<()> {
let version = &version_info.id; Box::pin(async move {
tracing::debug!("Locating client for version {version}"); let version = &version_info.id;
let client_download = version_info tracing::debug!("Locating client for version {version}");
.downloads let client_download = version_info
.get(&d::minecraft::DownloadType::Client) .downloads
.ok_or( .get(&d::minecraft::DownloadType::Client)
crate::ErrorKind::LauncherError(format!( .ok_or(
"No client downloads exist for version {version}" crate::ErrorKind::LauncherError(format!(
)) "No client downloads exist for version {version}"
.as_error(), ))
)?; .as_error(),
let path = st )?;
.directories let path = st
.version_dir(version) .directories
.join(format!("{version}.jar")); .version_dir(version)
.join(format!("{version}.jar"));
if !path.exists() { if !path.exists() {
let bytes = fetch( let bytes = fetch(
&client_download.url, &client_download.url,
Some(&client_download.sha1), Some(&client_download.sha1),
&st.fetch_semaphore, &st.fetch_semaphore,
) )
.await?; .await?;
write(&path, &bytes, &st.io_semaphore).await?; write(&path, &bytes, &st.io_semaphore).await?;
tracing::trace!("Fetched client version {version}"); tracing::trace!("Fetched client version {version}");
} }
if let Some(loading_bar) = loading_bar { if let Some(loading_bar) = loading_bar {
emit_loading(loading_bar, 10.0, None).await?; emit_loading(loading_bar, 10.0, None).await?;
} }
tracing::debug!("Client loaded for version {version}!"); tracing::debug!("Client loaded for version {version}!");
Ok(()) Ok(())
})
.await
} }
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
@ -176,58 +187,61 @@ pub async fn download_assets(
index: &AssetsIndex, index: &AssetsIndex,
loading_bar: Option<&LoadingBarId>, loading_bar: Option<&LoadingBarId>,
) -> crate::Result<()> { ) -> crate::Result<()> {
tracing::debug!("Loading assets"); Box::pin(async move {
let num_futs = index.objects.len(); tracing::debug!("Loading assets");
let assets = stream::iter(index.objects.iter()) let num_futs = index.objects.len();
.map(Ok::<(&String, &Asset), crate::Error>); let assets = stream::iter(index.objects.iter())
.map(Ok::<(&String, &Asset), crate::Error>);
loading_try_for_each_concurrent(assets, loading_try_for_each_concurrent(assets,
None, None,
loading_bar, loading_bar,
35.0, 35.0,
num_futs, num_futs,
None, None,
|(name, asset)| async move { |(name, asset)| async move {
let hash = &asset.hash; let hash = &asset.hash;
let resource_path = st.directories.object_dir(hash); let resource_path = st.directories.object_dir(hash);
let url = format!( let url = format!(
"https://resources.download.minecraft.net/{sub_hash}/{hash}", "https://resources.download.minecraft.net/{sub_hash}/{hash}",
sub_hash = &hash[..2] sub_hash = &hash[..2]
); );
let fetch_cell = OnceCell::<bytes::Bytes>::new(); let fetch_cell = OnceCell::<bytes::Bytes>::new();
tokio::try_join! { tokio::try_join! {
async { async {
if !resource_path.exists() { if !resource_path.exists() {
let resource = fetch_cell let resource = fetch_cell
.get_or_try_init(|| fetch(&url, Some(hash), &st.fetch_semaphore)) .get_or_try_init(|| fetch(&url, Some(hash), &st.fetch_semaphore))
.await?; .await?;
write(&resource_path, resource, &st.io_semaphore).await?; write(&resource_path, resource, &st.io_semaphore).await?;
tracing::trace!("Fetched asset with hash {hash}"); tracing::trace!("Fetched asset with hash {hash}");
} }
Ok::<_, crate::Error>(()) Ok::<_, crate::Error>(())
}, },
async { async {
if with_legacy { if with_legacy {
let resource = fetch_cell let resource = fetch_cell
.get_or_try_init(|| fetch(&url, Some(hash), &st.fetch_semaphore)) .get_or_try_init(|| fetch(&url, Some(hash), &st.fetch_semaphore))
.await?; .await?;
let resource_path = st.directories.legacy_assets_dir().join( let resource_path = st.directories.legacy_assets_dir().join(
name.replace('/', &String::from(std::path::MAIN_SEPARATOR)) name.replace('/', &String::from(std::path::MAIN_SEPARATOR))
); );
write(&resource_path, resource, &st.io_semaphore).await?; write(&resource_path, resource, &st.io_semaphore).await?;
tracing::trace!("Fetched legacy asset with hash {hash}"); tracing::trace!("Fetched legacy asset with hash {hash}");
} }
Ok::<_, crate::Error>(()) Ok::<_, crate::Error>(())
}, },
}?; }?;
tracing::trace!("Loaded asset with hash {hash}"); tracing::trace!("Loaded asset with hash {hash}");
Ok(()) Ok(())
}).await?; }).await?;
tracing::debug!("Done loading assets!"); tracing::debug!("Done loading assets!");
Ok(()) Ok(())
}).await
} }
#[tracing::instrument(skip(st, libraries))] #[tracing::instrument(skip(st, libraries))]
@ -237,96 +251,99 @@ pub async fn download_libraries(
version: &str, version: &str,
loading_bar: Option<&LoadingBarId>, loading_bar: Option<&LoadingBarId>,
) -> crate::Result<()> { ) -> crate::Result<()> {
tracing::debug!("Loading libraries"); Box::pin(async move {
tracing::debug!("Loading libraries");
tokio::try_join! { tokio::try_join! {
fs::create_dir_all(st.directories.libraries_dir()), fs::create_dir_all(st.directories.libraries_dir()),
fs::create_dir_all(st.directories.version_natives_dir(version)) fs::create_dir_all(st.directories.version_natives_dir(version))
}?; }?;
let num_files = libraries.len(); let num_files = libraries.len();
loading_try_for_each_concurrent( loading_try_for_each_concurrent(
stream::iter(libraries.iter()) stream::iter(libraries.iter())
.map(Ok::<&Library, crate::Error>), None, loading_bar,35.0,num_files, None,|library| async move { .map(Ok::<&Library, crate::Error>), None, loading_bar,35.0,num_files, None,|library| async move {
if let Some(rules) = &library.rules { if let Some(rules) = &library.rules {
if !rules.iter().all(super::parse_rule) { if !rules.iter().all(super::parse_rule) {
return Ok(()); return Ok(());
}
}
tokio::try_join! {
async {
let artifact_path = d::get_path_from_artifact(&library.name)?;
let path = st.directories.libraries_dir().join(&artifact_path);
match library.downloads {
_ if path.exists() => Ok(()),
Some(d::minecraft::LibraryDownloads {
artifact: Some(ref artifact),
..
}) => {
let bytes = fetch(&artifact.url, Some(&artifact.sha1), &st.fetch_semaphore)
.await?;
write(&path, &bytes, &st.io_semaphore).await?;
tracing::trace!("Fetched library {}", &library.name);
Ok::<_, crate::Error>(())
}
None => {
let url = [
library
.url
.as_deref()
.unwrap_or("https://libraries.minecraft.net"),
&artifact_path
].concat();
let bytes = fetch(&url, None, &st.fetch_semaphore).await?;
write(&path, &bytes, &st.io_semaphore).await?;
tracing::trace!("Fetched library {}", &library.name);
Ok::<_, crate::Error>(())
}
_ => Ok(())
} }
}, }
async { tokio::try_join! {
// HACK: pseudo try block using or else async {
if let Some((os_key, classifiers)) = None.or_else(|| Some(( let artifact_path = d::get_path_from_artifact(&library.name)?;
library let path = st.directories.libraries_dir().join(&artifact_path);
.natives
.as_ref()?
.get(&Os::native_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) { match library.downloads {
let data = fetch(&native.url, Some(&native.sha1), &st.fetch_semaphore).await?; _ if path.exists() => Ok(()),
let reader = std::io::Cursor::new(&data); Some(d::minecraft::LibraryDownloads {
if let Ok(mut archive) = zip::ZipArchive::new(reader) { artifact: Some(ref artifact),
match archive.extract(&st.directories.version_natives_dir(version)) { ..
Ok(_) => tracing::info!("Fetched native {}", &library.name), }) => {
Err(err) => tracing::error!("Failed extracting native {}. err: {}", &library.name, err) let bytes = fetch(&artifact.url, Some(&artifact.sha1), &st.fetch_semaphore)
.await?;
write(&path, &bytes, &st.io_semaphore).await?;
tracing::trace!("Fetched library {}", &library.name);
Ok::<_, crate::Error>(())
}
None => {
let url = [
library
.url
.as_deref()
.unwrap_or("https://libraries.minecraft.net"),
&artifact_path
].concat();
let bytes = fetch(&url, None, &st.fetch_semaphore).await?;
write(&path, &bytes, &st.io_semaphore).await?;
tracing::trace!("Fetched library {}", &library.name);
Ok::<_, crate::Error>(())
}
_ => Ok(())
}
},
async {
// HACK: pseudo try block using or else
if let Some((os_key, classifiers)) = None.or_else(|| Some((
library
.natives
.as_ref()?
.get(&Os::native_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).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)) {
Ok(_) => tracing::info!("Fetched native {}", &library.name),
Err(err) => tracing::error!("Failed extracting native {}. err: {}", &library.name, err)
}
} else {
tracing::error!("Failed extracting native {}", &library.name)
} }
} else {
tracing::error!("Failed extracting native {}", &library.name)
} }
} }
Ok(())
} }
}?;
Ok(()) tracing::debug!("Loaded library {}", library.name);
} Ok(())
}?; }
).await?;
tracing::debug!("Loaded library {}", library.name); tracing::debug!("Done loading libraries!");
Ok(()) Ok(())
}
).await?;
tracing::debug!("Done loading libraries!"); }).await
Ok(())
} }

View File

@ -58,158 +58,161 @@ pub async fn install_minecraft(
profile: &Profile, profile: &Profile,
existing_loading_bar: Option<LoadingBarId>, existing_loading_bar: Option<LoadingBarId>,
) -> crate::Result<()> { ) -> crate::Result<()> {
let state = State::get().await?; Box::pin(async move {
let instance_path = &canonicalize(&profile.path)?; let state = State::get().await?;
let metadata = state.metadata.read().await; let instance_path = &canonicalize(&profile.path)?;
let metadata = state.metadata.read().await;
let version = metadata let version = metadata
.minecraft .minecraft
.versions .versions
.iter() .iter()
.find(|it| it.id == profile.metadata.game_version) .find(|it| it.id == profile.metadata.game_version)
.ok_or(crate::ErrorKind::LauncherError(format!( .ok_or(crate::ErrorKind::LauncherError(format!(
"Invalid game version: {}", "Invalid game version: {}",
profile.metadata.game_version profile.metadata.game_version
)))?; )))?;
let version_jar = profile let version_jar = profile
.metadata .metadata
.loader_version .loader_version
.as_ref() .as_ref()
.map_or(version.id.clone(), |it| { .map_or(version.id.clone(), |it| {
format!("{}-{}", version.id.clone(), it.id.clone()) format!("{}-{}", version.id.clone(), it.id.clone())
}); });
let loading_bar = init_or_edit_loading( let loading_bar = init_or_edit_loading(
existing_loading_bar, existing_loading_bar,
LoadingBarType::MinecraftDownload { LoadingBarType::MinecraftDownload {
// If we are downloading minecraft for a profile, provide its name and uuid // If we are downloading minecraft for a profile, provide its name and uuid
profile_name: profile.metadata.name.clone(), profile_name: profile.metadata.name.clone(),
profile_uuid: profile.uuid, profile_uuid: profile.uuid,
}, },
100.0, 100.0,
"Downloading Minecraft", "Downloading Minecraft",
) )
.await?; .await?;
// Download version info // Download version info
let mut version_info = download::download_version_info( let mut version_info = download::download_version_info(
&state, &state,
version, version,
profile.metadata.loader_version.as_ref(), profile.metadata.loader_version.as_ref(),
None, None,
Some(&loading_bar), Some(&loading_bar),
) )
.await?; .await?;
// Download minecraft (5-90) // Download minecraft (5-90)
download::download_minecraft(&state, &version_info, &loading_bar).await?; download::download_minecraft(&state, &version_info, &loading_bar).await?;
let client_path = state let client_path = state
.directories .directories
.version_dir(&version_jar) .version_dir(&version_jar)
.join(format!("{version_jar}.jar")); .join(format!("{version_jar}.jar"));
if let Some(processors) = &version_info.processors { if let Some(processors) = &version_info.processors {
if let Some(ref mut data) = version_info.data { if let Some(ref mut data) = version_info.data {
processor_rules! { processor_rules! {
data; data;
"SIDE": "SIDE":
client => "client", client => "client",
server => ""; server => "";
"MINECRAFT_JAR" : "MINECRAFT_JAR" :
client => client_path.to_string_lossy(), client => client_path.to_string_lossy(),
server => ""; server => "";
"MINECRAFT_VERSION": "MINECRAFT_VERSION":
client => profile.metadata.game_version.clone(), client => profile.metadata.game_version.clone(),
server => ""; server => "";
"ROOT": "ROOT":
client => instance_path.to_string_lossy(), client => instance_path.to_string_lossy(),
server => ""; server => "";
"LIBRARY_DIR": "LIBRARY_DIR":
client => state.directories.libraries_dir().to_string_lossy(), client => state.directories.libraries_dir().to_string_lossy(),
server => ""; 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() {
emit_loading(
&loading_bar,
10.0 / total_length as f64,
Some(&format!(
"Running forge processor {}/{}",
index, total_length
)),
)
.await?;
if let Some(sides) = &processor.sides {
if !sides.contains(&String::from("client")) {
continue;
}
} }
let cp = wrap_ref_builder!(cp = processor.classpath.clone() => { emit_loading(&loading_bar, 0.0, Some("Running forge processors"))
cp.push(processor.jar.clone()) .await?;
}); let total_length = processors.len();
let child = Command::new("java") // Forge processors (90-100)
.arg("-cp") for (index, processor) in processors.iter().enumerate() {
.arg(args::get_class_paths_jar( emit_loading(
&state.directories.libraries_dir(), &loading_bar,
&cp, 10.0 / total_length as f64,
)?) Some(&format!(
.arg( "Running forge processor {}/{}",
args::get_processor_main_class(args::get_lib_path( index, total_length
&state.directories.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( .await?;
&state.directories.libraries_dir(),
&processor.args,
data,
)?)
.output()
.await
.map_err(|err| {
crate::ErrorKind::LauncherError(format!(
"Error running processor: {err}",
))
})?;
if !child.status.success() { if let Some(sides) = &processor.sides {
return Err(crate::ErrorKind::LauncherError(format!( if !sides.contains(&String::from("client")) {
"Processor error: {}", continue;
String::from_utf8_lossy(&child.stderr) }
)) }
.as_error());
let cp = wrap_ref_builder!(cp = processor.classpath.clone() => {
cp.push(processor.jar.clone())
});
let child = Command::new("java")
.arg("-cp")
.arg(args::get_class_paths_jar(
&state.directories.libraries_dir(),
&cp,
)?)
.arg(
args::get_processor_main_class(args::get_lib_path(
&state.directories.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(
&state.directories.libraries_dir(),
&processor.args,
data,
)?)
.output()
.await
.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());
}
} }
} }
} }
}
crate::api::profile::edit(&profile.path, |prof| { crate::api::profile::edit(&profile.path, |prof| {
prof.installed = true; prof.installed = true;
async { Ok(()) } async { Ok(()) }
}) })
.await?; .await?;
State::sync().await?; State::sync().await?;
Ok(()) Ok(())
}).await
} }
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
@ -224,140 +227,143 @@ pub async fn launch_minecraft(
post_exit_hook: Option<Command>, post_exit_hook: Option<Command>,
profile: &Profile, profile: &Profile,
) -> crate::Result<Arc<tokio::sync::RwLock<MinecraftChild>>> { ) -> crate::Result<Arc<tokio::sync::RwLock<MinecraftChild>>> {
if !profile.installed { Box::pin(async move {
install_minecraft(profile, None).await?; if !profile.installed {
} install_minecraft(profile, None).await?;
let state = State::get().await?;
let metadata = state.metadata.read().await;
let instance_path = &canonicalize(&profile.path)?;
let version = metadata
.minecraft
.versions
.iter()
.find(|it| it.id == profile.metadata.game_version)
.ok_or(crate::ErrorKind::LauncherError(format!(
"Invalid game version: {}",
profile.metadata.game_version
)))?;
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 client_path = state
.directories
.version_dir(&version_jar)
.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_install)})
} }
None => Command::new(String::from(java_install.to_string_lossy())),
};
let env_args = Vec::from(env_args); let state = State::get().await?;
let metadata = state.metadata.read().await;
let instance_path = &canonicalize(&profile.path)?;
// Check if profile has a running profile, and reject running the command if it does let version = metadata
// Done late so a quick double call doesn't launch two instances .minecraft
let existing_processes = .versions
process::get_uuids_by_profile_path(instance_path).await?; .iter()
if let Some(uuid) = existing_processes.first() { .find(|it| it.id == profile.metadata.game_version)
return Err(crate::ErrorKind::LauncherError(format!( .ok_or(crate::ErrorKind::LauncherError(format!(
"Profile {} is already running at UUID: {uuid}", "Invalid game version: {}",
instance_path.display() profile.metadata.game_version
)) )))?;
.as_error());
}
command let version_jar = profile
.args( .metadata
args::get_jvm_arguments( .loader_version
args.get(&d::minecraft::ArgumentType::Jvm) .as_ref()
.map(|x| x.as_slice()), .map_or(version.id.clone(), |it| {
&state.directories.version_natives_dir(&version_jar), format!("{}-{}", version.id.clone(), it.id.clone())
&state.directories.libraries_dir(), });
&args::get_class_paths(
let version_info = download::download_version_info(
&state,
version,
profile.metadata.loader_version.as_ref(),
None,
None,
)
.await?;
let client_path = state
.directories
.version_dir(&version_jar)
.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_install)})
}
None => Command::new(String::from(java_install.to_string_lossy())),
};
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(instance_path).await?;
if let Some(uuid) = existing_processes.first() {
return Err(crate::ErrorKind::LauncherError(format!(
"Profile {} is already running at UUID: {uuid}",
instance_path.display()
))
.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),
&state.directories.libraries_dir(), &state.directories.libraries_dir(),
version_info.libraries.as_slice(), &args::get_class_paths(
&client_path, &state.directories.libraries_dir(),
)?, version_info.libraries.as_slice(),
&version_jar, &client_path,
*memory, )?,
Vec::from(java_args), &version_jar,
)? *memory,
.into_iter() Vec::from(java_args),
.collect::<Vec<_>>(), )?
) .into_iter()
.arg(version_info.main_class.clone()) .collect::<Vec<_>>(),
.args( )
args::get_minecraft_arguments( .arg(version_info.main_class.clone())
args.get(&d::minecraft::ArgumentType::Game) .args(
.map(|x| x.as_slice()), args::get_minecraft_arguments(
version_info.minecraft_arguments.as_deref(), args.get(&d::minecraft::ArgumentType::Game)
credentials, .map(|x| x.as_slice()),
&version.id, version_info.minecraft_arguments.as_deref(),
&version_info.asset_index.id, credentials,
instance_path, &version.id,
&state.directories.assets_dir(), &version_info.asset_index.id,
&version.type_, instance_path,
*resolution, &state.directories.assets_dir(),
)? &version.type_,
.into_iter() *resolution,
.collect::<Vec<_>>(), )?
) .into_iter()
.current_dir(instance_path.clone()) .collect::<Vec<_>>(),
.stdout(Stdio::piped()) )
.stderr(Stdio::piped()); .current_dir(instance_path.clone())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
// CARGO-set DYLD_LIBRARY_PATH breaks Minecraft on macOS during testing on playground // CARGO-set DYLD_LIBRARY_PATH breaks Minecraft on macOS during testing on playground
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
if std::env::var("CARGO").is_ok() { if std::env::var("CARGO").is_ok() {
command.env_remove("DYLD_FALLBACK_LIBRARY_PATH"); command.env_remove("DYLD_FALLBACK_LIBRARY_PATH");
} }
command.envs(env_args); command.envs(env_args);
// Get Modrinth logs directories // Get Modrinth logs directories
let datetime_string = let datetime_string =
chrono::Local::now().format("%Y%m%y_%H%M%S").to_string(); chrono::Local::now().format("%Y%m%y_%H%M%S").to_string();
let logs_dir = { let logs_dir = {
let st = State::get().await?; let st = State::get().await?;
st.directories st.directories
.profile_logs_dir(profile.uuid) .profile_logs_dir(profile.uuid)
.join(&datetime_string) .join(&datetime_string)
}; };
fs::create_dir_all(&logs_dir)?; fs::create_dir_all(&logs_dir)?;
let stdout_log_path = logs_dir.join("stdout.log"); let stdout_log_path = logs_dir.join("stdout.log");
let stderr_log_path = logs_dir.join("stderr.log"); let stderr_log_path = logs_dir.join("stderr.log");
// Create Minecraft child by inserting it into the state // Create Minecraft child by inserting it into the state
// This also spawns the process and prepares the subsequent processes // This also spawns the process and prepares the subsequent processes
let mut state_children = state.children.write().await; let mut state_children = state.children.write().await;
state_children state_children
.insert_process( .insert_process(
Uuid::new_v4(), Uuid::new_v4(),
instance_path.to_path_buf(), instance_path.to_path_buf(),
stdout_log_path, stdout_log_path,
stderr_log_path, stderr_log_path,
command, command,
post_exit_hook, post_exit_hook,
) )
.await .await
}).await
} }

View File

@ -6,7 +6,7 @@
use dunce::canonicalize; use dunce::canonicalize;
use theseus::jre::autodetect_java_globals; use theseus::jre::autodetect_java_globals;
use theseus::prelude::*; use theseus::prelude::*;
use theseus::profile_create::profile_create;
use tokio::time::{sleep, Duration}; use tokio::time::{sleep, Duration};
use tracing_error::ErrorLayer; use tracing_error::ErrorLayer;
use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::layer::SubscriberExt;