diff --git a/theseus/src/api/pack.rs b/theseus/src/api/pack.rs index 73fda0538..155e69849 100644 --- a/theseus/src/api/pack.rs +++ b/theseus/src/api/pack.rs @@ -79,97 +79,101 @@ pub async fn install_pack_from_version_id( title: Option, ) -> crate::Result { 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( - LoadingBarType::PackFileDownload { - pack_name: title, - pack_version: version_id.clone(), - }, - 100.0, - "Downloading pack file", - ) - .await?; + emit_loading(&loading_bar, 0.0, Some("Fetching version")).await?; + let version: ModrinthVersion = fetch_json( + Method::GET, + &format!("{}version/{}", MODRINTH_API_URL, version_id), + None, + None, + &state.fetch_semaphore, + ) + .await?; + emit_loading(&loading_bar, 10.0, None).await?; - emit_loading(&loading_bar, 0.0, Some("Fetching version")).await?; - let version: ModrinthVersion = fetch_json( - Method::GET, - &format!("{}version/{}", MODRINTH_API_URL, version_id), - None, - None, - &state.fetch_semaphore, - ) - .await?; - emit_loading(&loading_bar, 10.0, None).await?; - - let (url, hash) = - 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, + let (url, hash) = + 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(), ) - .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 { None - } - } else { - None - }; - emit_loading(&loading_bar, 10.0, None).await?; + }; + emit_loading(&loading_bar, 10.0, None).await?; - install_pack( - file, - icon, - Some(project.title), - Some(version.project_id), - Some(version.id), - Some(loading_bar), - ) + install_pack( + file, + icon, + Some(project.title), + Some(version.project_id), + Some(version.id), + Some(loading_bar), + ) + .await + }) .await } @@ -189,243 +193,259 @@ async fn install_pack( ) -> crate::Result { 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 - let mut zip_reader = ZipFileReader::new(reader).await.map_err(|_| { - crate::Error::from(crate::ErrorKind::InputError( - "Failed to read input modpack zip".to_string(), - )) - })?; + // Create zip reader around file + let mut zip_reader = + ZipFileReader::new(reader).await.map_err(|_| { + crate::Error::from(crate::ErrorKind::InputError( + "Failed to read input modpack zip".to_string(), + )) + })?; - // Extract index of modrinth.index.json - 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 + // Extract index of modrinth.index.json + let zip_index_option = zip_reader .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?; + .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() + .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" { - return Err(crate::ErrorKind::InputError( - "Pack does not support Minecraft".to_string(), - ) - .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), + if &*pack.game != "minecraft" { + return Err(crate::ErrorKind::InputError( + "Pack does not support Minecraft".to_string(), + ) + .into()); } - } - 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()); - }; - - 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::), - 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::>(), - 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(()) + 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); } - }, - ) - .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?; - } - } + PackDependency::FabricLoader => { + mod_loader = Some(ModLoader::Fabric); + loader_version = Some(value); } - - Ok::<(), crate::Error>(()) + PackDependency::QuiltLoader => { + 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")) - .await?; - extract_overrides("overrides".to_string()).await?; - extract_overrides("client_overrides".to_string()).await?; - emit_loading(&loading_bar, 29.9, Some("Done extacting overrides")) + 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?; - 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) - ), - )?; + let num_files = pack.files.len(); + use futures::StreamExt; + loading_try_for_each_concurrent( + futures::stream::iter(pack.files.into_iter()) + .map(Ok::), + 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::>(), + 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::(profile) } + .await; - Ok::(profile) - } - .await; + match result { + Ok(profile) => Ok(profile), + Err(err) => { + let _ = crate::api::profile::remove(&profile_raw).await; - match result { - Ok(profile) => Ok(profile), - Err(err) => { - let _ = crate::api::profile::remove(&profile_raw).await; - - Err(err) + 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( - "No pack manifest found in mrpack".to_string(), - ))) - } + }) + .await } diff --git a/theseus/src/api/profile.rs b/theseus/src/api/profile.rs index 0162d6279..9ed210d02 100644 --- a/theseus/src/api/profile.rs +++ b/theseus/src/api/profile.rs @@ -139,41 +139,44 @@ pub async fn install(path: &Path) -> crate::Result<()> { pub async fn update_all(profile_path: &Path) -> crate::Result<()> { 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) { - let loading_bar = init_loading( - LoadingBarType::ProfileUpdate { - profile_uuid: profile.uuid, - profile_name: profile.metadata.name.clone(), - }, - 100.0, - "Updating profile", - ) - .await?; + if let Some(profile) = profiles.0.get_mut(profile_path) { + let loading_bar = init_loading( + LoadingBarType::ProfileUpdate { + profile_uuid: profile.uuid, + profile_name: profile.metadata.name.clone(), + }, + 100.0, + "Updating profile", + ) + .await?; - use futures::StreamExt; - loading_try_for_each_concurrent( - futures::stream::iter(profile.projects.keys()) - .map(Ok::<&PathBuf, crate::Error>), - None, - Some(&loading_bar), - 100.0, - profile.projects.keys().len(), - None, - |project| update_project(profile_path, project, Some(true)), - ) - .await?; + use futures::StreamExt; + loading_try_for_each_concurrent( + futures::stream::iter(profile.projects.keys()) + .map(Ok::<&PathBuf, crate::Error>), + None, + Some(&loading_bar), + 100.0, + profile.projects.keys().len(), + None, + |project| update_project(profile_path, project, Some(true)), + ) + .await?; - profile.sync().await?; + profile.sync().await?; - Ok(()) - } else { - Err(crate::ErrorKind::UnmanagedProfileError( - profile_path.display().to_string(), - ) - .as_error()) - } + Ok(()) + } else { + Err(crate::ErrorKind::UnmanagedProfileError( + profile_path.display().to_string(), + ) + .as_error()) + } + }) + .await } pub async fn update_project( @@ -372,146 +375,148 @@ pub async fn run_credentials( path: &Path, credentials: &auth::Credentials, ) -> crate::Result>> { - let state = State::get().await?; - let settings = state.settings.read().await; - let metadata = state.metadata.read().await; - let profile = get(path).await?.ok_or_else(|| { - crate::ErrorKind::OtherError(format!( - "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 + Box::pin(async move { + let state = State::get().await?; + let settings = state.settings.read().await; + let metadata = state.metadata.read().await; + let profile = get(path).await?.ok_or_else(|| { + crate::ErrorKind::OtherError(format!( + "Tried to run a nonexistent or unloaded profile at path {}!", + path.display() )) })?; - 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::>()) - .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) + 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 )) - .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::>()) + .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 { - // Load profile-specific Java implementation choice - // (This defaults to Daedalus-decided key on init, but can be changed by the user) - Some(JavaSettings { - jre_key: Some(ref jre_key), - .. - }) => settings.java_globals.get(jre_key), - // Fall back to Daedalus-decided key if no profile-specific key is set - _ => { - match version_info - .java_version - .as_ref() - .map(|it| it.major_version) - .unwrap_or(0) - { - 0..=16 => settings - .java_globals - .get(&crate::jre::JAVA_8_KEY.to_string()), - 17 => settings - .java_globals - .get(&crate::jre::JAVA_17_KEY.to_string()), - _ => settings - .java_globals - .get(&crate::jre::JAVA_18PLUS_KEY.to_string()), + let java_version = match profile.java { + // Load profile-specific Java implementation choice + // (This defaults to Daedalus-decided key on init, but can be changed by the user) + Some(JavaSettings { + jre_key: Some(ref jre_key), + .. + }) => settings.java_globals.get(jre_key), + // Fall back to Daedalus-decided key if no profile-specific key is set + _ => { + match version_info + .java_version + .as_ref() + .map(|it| it.major_version) + .unwrap_or(0) + { + 0..=16 => settings + .java_globals + .get(&crate::jre::JAVA_8_KEY.to_string()), + 17 => settings + .java_globals + .get(&crate::jre::JAVA_17_KEY.to_string()), + _ => settings + .java_globals + .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_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), - )) - })?; + let java_args = profile + .java + .as_ref() + .and_then(|it| it.extra_arguments.as_ref()) + .unwrap_or(&settings.custom_java_args); - // 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 - .java - .as_ref() - .and_then(|it| it.extra_arguments.as_ref()) - .unwrap_or(&settings.custom_java_args); + let wrapper = profile + .hooks + .as_ref() + .map_or(&settings.hooks.wrapper, |it| &it.wrapper); - let wrapper = profile - .hooks - .as_ref() - .map_or(&settings.hooks.wrapper, |it| &it.wrapper); + let memory = profile.memory.unwrap_or(settings.memory); + let resolution = profile.resolution.unwrap_or(settings.game_resolution); - let memory = profile.memory.unwrap_or(settings.memory); - let resolution = profile.resolution.unwrap_or(settings.game_resolution); + let env_args = &settings.custom_env_args; - 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 = - &profile.hooks.as_ref().unwrap_or(&settings.hooks).post_exit; - - let post_exit_hook = if let Some(hook) = post_exit_hook { - let mut cmd = hook.split(' '); - if let Some(command) = cmd.next() { - let mut command = Command::new(command); - command.args(&cmd.collect::>()).current_dir(path); - Some(command) + let post_exit_hook = if let Some(hook) = post_exit_hook { + let mut cmd = hook.split(' '); + if let Some(command) = cmd.next() { + let mut command = Command::new(command); + command.args(&cmd.collect::>()).current_dir(path); + Some(command) + } else { + None + } } else { None - } - } else { - None - }; + }; - let mc_process = crate::launcher::launch_minecraft( - java_install, - java_args, - env_args, - wrapper, - &memory, - &resolution, - credentials, - post_exit_hook, - &profile, - ) - .await?; - - Ok(mc_process) + let mc_process = crate::launcher::launch_minecraft( + java_install, + java_args, + env_args, + wrapper, + &memory, + &resolution, + credentials, + post_exit_hook, + &profile, + ) + .await?; + Ok(mc_process) + }) + .await } diff --git a/theseus/src/api/profile_create.rs b/theseus/src/api/profile_create.rs index c929962a3..3d12197b8 100644 --- a/theseus/src/api/profile_create.rs +++ b/theseus/src/api/profile_create.rs @@ -51,153 +51,156 @@ pub async fn profile_create( ) -> crate::Result { trace!("Creating new profile. {}", name); 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 path = state.directories.profiles_dir().join(uuid.to_string()); - if path.exists() { - if !path.is_dir() { - 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()) + let uuid = Uuid::new_v4(); + let path = state.directories.profiles_dir().join(uuid.to_string()); + if path.exists() { + if !path.is_dir() { + 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()) + } + }; + + 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 - .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(), + + // Fully canonicalize now that its created for storing purposes + let path = canonicalize(&path)?; + let mut profile = + Profile::new(uuid, name, game_version, path.clone()).await?; + if let Some(ref icon) = icon { + let bytes = tokio::fs::read(icon).await?; + profile + .set_icon( + &state.directories.caches_dir(), + &state.io_semaphore, + bytes::Bytes::from(bytes), + &icon.to_string_lossy(), ) - })? - .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 - }; - - // Fully canonicalize now that its created for storing purposes - let path = canonicalize(&path)?; - let mut profile = - Profile::new(uuid, name, game_version, path.clone()).await?; - if let Some(ref icon) = icon { - let bytes = tokio::fs::read(icon).await?; - profile - .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; - - // Attempts to find optimal JRE for the profile from the JavaGlobals - // Finds optimal key, and see if key has been set in JavaGlobals - 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?; - } - - emit_profile( - uuid, - path.clone(), - &profile.metadata.name, - ProfilePayloadType::Created, - ) - .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) + .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; + + // Attempts to find optimal JRE for the profile from the JavaGlobals + // Finds optimal key, and see if key has been set in JavaGlobals + 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?; + } + + emit_profile( + uuid, + path.clone(), + &profile.metadata.name, + ProfilePayloadType::Created, + ) + .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) + + }).await } #[derive(thiserror::Error, Debug)] diff --git a/theseus/src/launcher/download.rs b/theseus/src/launcher/download.rs index 0fbfaf846..db7baddfb 100644 --- a/theseus/src/launcher/download.rs +++ b/theseus/src/launcher/download.rs @@ -49,53 +49,61 @@ pub async fn download_version_info( force: Option, loading_bar: Option<&LoadingBarId>, ) -> crate::Result { - let version_id = loader - .map_or(version.id.clone(), |it| format!("{}-{}", version.id, it.id)); - tracing::debug!("Loading version info for Minecraft {version_id}"); - let path = st - .directories - .version_dir(&version_id) - .join(format!("{version_id}.json")); + Box::pin(async move { + let version_id = loader.map_or(version.id.clone(), |it| { + format!("{}-{}", version.id, it.id) + }); + tracing::debug!("Loading version info for Minecraft {version_id}"); + let path = st + .directories + .version_dir(&version_id) + .join(format!("{version_id}.json")); - let res = if path.exists() && !force.unwrap_or(false) { - fs::read(path) - .err_into::() - .await - .and_then(|ref it| Ok(serde_json::from_slice(it)?)) - } else { - tracing::info!("Downloading version info for version {}", &version.id); - let mut info = d::minecraft::fetch_version_info(version).await?; + let res = if path.exists() && !force.unwrap_or(false) { + fs::read(path) + .err_into::() + .await + .and_then(|ref it| Ok(serde_json::from_slice(it)?)) + } else { + tracing::info!( + "Downloading version info for version {}", + &version.id + ); + let mut info = d::minecraft::fetch_version_info(version).await?; - if let Some(loader) = loader { - let partial = d::modded::fetch_partial_version(&loader.url).await?; - info = d::modded::merge_partial_version(partial, info); + if let Some(loader) = loader { + let partial = + d::modded::fetch_partial_version(&loader.url).await?; + info = d::modded::merge_partial_version(partial, info); + } + info.id = version_id.clone(); + + write(&path, &serde_json::to_vec(&info)?, &st.io_semaphore).await?; + Ok(info) + }?; + + if let Some(loading_bar) = loading_bar { + emit_loading( + loading_bar, + 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?; - 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?; - } - - tracing::debug!("Loaded version info for Minecraft {version_id}"); - Ok(res) + tracing::debug!("Loaded version info for Minecraft {version_id}"); + Ok(res) + }) + .await } #[tracing::instrument(skip_all)] @@ -104,38 +112,41 @@ pub async fn download_client( version_info: &GameVersionInfo, loading_bar: Option<&LoadingBarId>, ) -> crate::Result<()> { - let version = &version_info.id; - tracing::debug!("Locating client for version {version}"); - let client_download = version_info - .downloads - .get(&d::minecraft::DownloadType::Client) - .ok_or( - crate::ErrorKind::LauncherError(format!( - "No client downloads exist for version {version}" - )) - .as_error(), - )?; - let path = st - .directories - .version_dir(version) - .join(format!("{version}.jar")); + Box::pin(async move { + let version = &version_info.id; + tracing::debug!("Locating client for version {version}"); + let client_download = version_info + .downloads + .get(&d::minecraft::DownloadType::Client) + .ok_or( + crate::ErrorKind::LauncherError(format!( + "No client downloads exist for version {version}" + )) + .as_error(), + )?; + let path = st + .directories + .version_dir(version) + .join(format!("{version}.jar")); - if !path.exists() { - let bytes = fetch( - &client_download.url, - Some(&client_download.sha1), - &st.fetch_semaphore, - ) - .await?; - write(&path, &bytes, &st.io_semaphore).await?; - tracing::trace!("Fetched client version {version}"); - } - if let Some(loading_bar) = loading_bar { - emit_loading(loading_bar, 10.0, None).await?; - } + if !path.exists() { + let bytes = fetch( + &client_download.url, + Some(&client_download.sha1), + &st.fetch_semaphore, + ) + .await?; + write(&path, &bytes, &st.io_semaphore).await?; + tracing::trace!("Fetched client version {version}"); + } + if let Some(loading_bar) = loading_bar { + emit_loading(loading_bar, 10.0, None).await?; + } - tracing::debug!("Client loaded for version {version}!"); - Ok(()) + tracing::debug!("Client loaded for version {version}!"); + Ok(()) + }) + .await } #[tracing::instrument(skip_all)] @@ -176,58 +187,61 @@ pub async fn download_assets( index: &AssetsIndex, loading_bar: Option<&LoadingBarId>, ) -> crate::Result<()> { - tracing::debug!("Loading assets"); - let num_futs = index.objects.len(); - let assets = stream::iter(index.objects.iter()) - .map(Ok::<(&String, &Asset), crate::Error>); - - loading_try_for_each_concurrent(assets, - None, - loading_bar, - 35.0, - num_futs, - None, - |(name, asset)| async move { - let hash = &asset.hash; - let resource_path = st.directories.object_dir(hash); - let url = format!( - "https://resources.download.minecraft.net/{sub_hash}/{hash}", - sub_hash = &hash[..2] - ); - - let fetch_cell = OnceCell::::new(); - tokio::try_join! { - async { - if !resource_path.exists() { - let resource = fetch_cell - .get_or_try_init(|| fetch(&url, Some(hash), &st.fetch_semaphore)) - .await?; - write(&resource_path, resource, &st.io_semaphore).await?; - tracing::trace!("Fetched asset with hash {hash}"); - } - Ok::<_, crate::Error>(()) - }, - async { - if with_legacy { - let resource = fetch_cell - .get_or_try_init(|| fetch(&url, Some(hash), &st.fetch_semaphore)) - .await?; - let resource_path = st.directories.legacy_assets_dir().join( - name.replace('/', &String::from(std::path::MAIN_SEPARATOR)) - ); - write(&resource_path, resource, &st.io_semaphore).await?; - tracing::trace!("Fetched legacy asset with hash {hash}"); - } - Ok::<_, crate::Error>(()) - }, - }?; - - tracing::trace!("Loaded asset with hash {hash}"); - Ok(()) - }).await?; - - tracing::debug!("Done loading assets!"); - Ok(()) + Box::pin(async move { + tracing::debug!("Loading assets"); + let num_futs = index.objects.len(); + let assets = stream::iter(index.objects.iter()) + .map(Ok::<(&String, &Asset), crate::Error>); + + loading_try_for_each_concurrent(assets, + None, + loading_bar, + 35.0, + num_futs, + None, + |(name, asset)| async move { + let hash = &asset.hash; + let resource_path = st.directories.object_dir(hash); + let url = format!( + "https://resources.download.minecraft.net/{sub_hash}/{hash}", + sub_hash = &hash[..2] + ); + + let fetch_cell = OnceCell::::new(); + tokio::try_join! { + async { + if !resource_path.exists() { + let resource = fetch_cell + .get_or_try_init(|| fetch(&url, Some(hash), &st.fetch_semaphore)) + .await?; + write(&resource_path, resource, &st.io_semaphore).await?; + tracing::trace!("Fetched asset with hash {hash}"); + } + Ok::<_, crate::Error>(()) + }, + async { + if with_legacy { + let resource = fetch_cell + .get_or_try_init(|| fetch(&url, Some(hash), &st.fetch_semaphore)) + .await?; + let resource_path = st.directories.legacy_assets_dir().join( + name.replace('/', &String::from(std::path::MAIN_SEPARATOR)) + ); + write(&resource_path, resource, &st.io_semaphore).await?; + tracing::trace!("Fetched legacy asset with hash {hash}"); + } + Ok::<_, crate::Error>(()) + }, + }?; + + tracing::trace!("Loaded asset with hash {hash}"); + Ok(()) + }).await?; + + tracing::debug!("Done loading assets!"); + Ok(()) + + }).await } #[tracing::instrument(skip(st, libraries))] @@ -237,96 +251,99 @@ pub async fn download_libraries( version: &str, loading_bar: Option<&LoadingBarId>, ) -> crate::Result<()> { - tracing::debug!("Loading libraries"); + Box::pin(async move { + tracing::debug!("Loading libraries"); - tokio::try_join! { - fs::create_dir_all(st.directories.libraries_dir()), - fs::create_dir_all(st.directories.version_natives_dir(version)) - }?; - let num_files = libraries.len(); - loading_try_for_each_concurrent( - stream::iter(libraries.iter()) - .map(Ok::<&Library, crate::Error>), None, loading_bar,35.0,num_files, None,|library| async move { - if let Some(rules) = &library.rules { - if !rules.iter().all(super::parse_rule) { - return Ok(()); - } - } - tokio::try_join! { - async { - let artifact_path = d::get_path_from_artifact(&library.name)?; - let path = st.directories.libraries_dir().join(&artifact_path); - - 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(()) + tokio::try_join! { + fs::create_dir_all(st.directories.libraries_dir()), + fs::create_dir_all(st.directories.version_natives_dir(version)) + }?; + let num_files = libraries.len(); + loading_try_for_each_concurrent( + stream::iter(libraries.iter()) + .map(Ok::<&Library, crate::Error>), None, loading_bar,35.0,num_files, None,|library| async move { + if let Some(rules) = &library.rules { + if !rules.iter().all(super::parse_rule) { + return 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) + } + 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 { + // 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!("Done loading libraries!"); - Ok(()) + }?; + + tracing::debug!("Loaded library {}", library.name); + Ok(()) + } + ).await?; + + tracing::debug!("Done loading libraries!"); + Ok(()) + + }).await } diff --git a/theseus/src/launcher/mod.rs b/theseus/src/launcher/mod.rs index 7ea40019f..ea5780842 100644 --- a/theseus/src/launcher/mod.rs +++ b/theseus/src/launcher/mod.rs @@ -58,158 +58,161 @@ pub async fn install_minecraft( profile: &Profile, existing_loading_bar: Option, ) -> crate::Result<()> { - let state = State::get().await?; - let instance_path = &canonicalize(&profile.path)?; - let metadata = state.metadata.read().await; - - 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 loading_bar = init_or_edit_loading( - existing_loading_bar, - LoadingBarType::MinecraftDownload { - // If we are downloading minecraft for a profile, provide its name and uuid - profile_name: profile.metadata.name.clone(), - profile_uuid: profile.uuid, - }, - 100.0, - "Downloading Minecraft", - ) - .await?; - - // Download version info - let mut version_info = download::download_version_info( - &state, - version, - profile.metadata.loader_version.as_ref(), - None, - Some(&loading_bar), - ) - .await?; - - // Download minecraft (5-90) - download::download_minecraft(&state, &version_info, &loading_bar).await?; - - let client_path = state - .directories - .version_dir(&version_jar) - .join(format!("{version_jar}.jar")); - - if let Some(processors) = &version_info.processors { - if let Some(ref mut data) = version_info.data { - processor_rules! { - data; - "SIDE": - client => "client", - server => ""; - "MINECRAFT_JAR" : - client => client_path.to_string_lossy(), - server => ""; - "MINECRAFT_VERSION": - client => profile.metadata.game_version.clone(), - server => ""; - "ROOT": - client => instance_path.to_string_lossy(), - server => ""; - "LIBRARY_DIR": - client => state.directories.libraries_dir().to_string_lossy(), - server => ""; - } - - emit_loading(&loading_bar, 0.0, Some("Running forge processors")) - .await?; - let total_length = processors.len(); - - // Forge processors (90-100) - for (index, processor) in processors.iter().enumerate() { - 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; - } + Box::pin(async move { + let state = State::get().await?; + let instance_path = &canonicalize(&profile.path)?; + let metadata = state.metadata.read().await; + + 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 loading_bar = init_or_edit_loading( + existing_loading_bar, + LoadingBarType::MinecraftDownload { + // If we are downloading minecraft for a profile, provide its name and uuid + profile_name: profile.metadata.name.clone(), + profile_uuid: profile.uuid, + }, + 100.0, + "Downloading Minecraft", + ) + .await?; + + // Download version info + let mut version_info = download::download_version_info( + &state, + version, + profile.metadata.loader_version.as_ref(), + None, + Some(&loading_bar), + ) + .await?; + + // Download minecraft (5-90) + download::download_minecraft(&state, &version_info, &loading_bar).await?; + + let client_path = state + .directories + .version_dir(&version_jar) + .join(format!("{version_jar}.jar")); + + if let Some(processors) = &version_info.processors { + if let Some(ref mut data) = version_info.data { + processor_rules! { + data; + "SIDE": + client => "client", + server => ""; + "MINECRAFT_JAR" : + client => client_path.to_string_lossy(), + server => ""; + "MINECRAFT_VERSION": + client => profile.metadata.game_version.clone(), + server => ""; + "ROOT": + client => instance_path.to_string_lossy(), + server => ""; + "LIBRARY_DIR": + client => state.directories.libraries_dir().to_string_lossy(), + server => ""; } - - 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 - )) - })?, + + 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 + )), ) - .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}", + .await?; + + if let Some(sides) = &processor.sides { + if !sides.contains(&String::from("client")) { + continue; + } + } + + let cp = wrap_ref_builder!(cp = processor.classpath.clone() => { + cp.push(processor.jar.clone()) + }); + + let child = Command::new("java") + .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) )) - })?; - - if !child.status.success() { - return Err(crate::ErrorKind::LauncherError(format!( - "Processor error: {}", - String::from_utf8_lossy(&child.stderr) - )) - .as_error()); + .as_error()); + } } } } - } - - crate::api::profile::edit(&profile.path, |prof| { - prof.installed = true; - - async { Ok(()) } - }) - .await?; - State::sync().await?; - - Ok(()) + + crate::api::profile::edit(&profile.path, |prof| { + prof.installed = true; + + async { Ok(()) } + }) + .await?; + State::sync().await?; + + Ok(()) + + }).await } #[allow(clippy::too_many_arguments)] @@ -224,140 +227,143 @@ pub async fn launch_minecraft( post_exit_hook: Option, profile: &Profile, ) -> crate::Result>> { - 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)}) + Box::pin(async move { + if !profile.installed { + install_minecraft(profile, None).await?; } - 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(), - &args::get_class_paths( + + 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); + + // 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(), - version_info.libraries.as_slice(), - &client_path, - )?, - &version_jar, - *memory, - Vec::from(java_args), - )? - .into_iter() - .collect::>(), - ) - .arg(version_info.main_class.clone()) - .args( - args::get_minecraft_arguments( - args.get(&d::minecraft::ArgumentType::Game) - .map(|x| x.as_slice()), - version_info.minecraft_arguments.as_deref(), - credentials, - &version.id, - &version_info.asset_index.id, - instance_path, - &state.directories.assets_dir(), - &version.type_, - *resolution, - )? - .into_iter() - .collect::>(), - ) - .current_dir(instance_path.clone()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()); - - // CARGO-set DYLD_LIBRARY_PATH breaks Minecraft on macOS during testing on playground - #[cfg(target_os = "macos")] - if std::env::var("CARGO").is_ok() { - command.env_remove("DYLD_FALLBACK_LIBRARY_PATH"); - } - command.envs(env_args); - - // Get Modrinth logs directories - let datetime_string = - chrono::Local::now().format("%Y%m%y_%H%M%S").to_string(); - let logs_dir = { - let st = State::get().await?; - st.directories - .profile_logs_dir(profile.uuid) - .join(&datetime_string) - }; - fs::create_dir_all(&logs_dir)?; - - let stdout_log_path = logs_dir.join("stdout.log"); - let stderr_log_path = logs_dir.join("stderr.log"); - - // Create Minecraft child by inserting it into the state - // This also spawns the process and prepares the subsequent processes - let mut state_children = state.children.write().await; - state_children - .insert_process( - Uuid::new_v4(), - instance_path.to_path_buf(), - stdout_log_path, - stderr_log_path, - command, - post_exit_hook, - ) - .await + &args::get_class_paths( + &state.directories.libraries_dir(), + version_info.libraries.as_slice(), + &client_path, + )?, + &version_jar, + *memory, + Vec::from(java_args), + )? + .into_iter() + .collect::>(), + ) + .arg(version_info.main_class.clone()) + .args( + args::get_minecraft_arguments( + args.get(&d::minecraft::ArgumentType::Game) + .map(|x| x.as_slice()), + version_info.minecraft_arguments.as_deref(), + credentials, + &version.id, + &version_info.asset_index.id, + instance_path, + &state.directories.assets_dir(), + &version.type_, + *resolution, + )? + .into_iter() + .collect::>(), + ) + .current_dir(instance_path.clone()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + + // CARGO-set DYLD_LIBRARY_PATH breaks Minecraft on macOS during testing on playground + #[cfg(target_os = "macos")] + if std::env::var("CARGO").is_ok() { + command.env_remove("DYLD_FALLBACK_LIBRARY_PATH"); + } + command.envs(env_args); + + // Get Modrinth logs directories + let datetime_string = + chrono::Local::now().format("%Y%m%y_%H%M%S").to_string(); + let logs_dir = { + let st = State::get().await?; + st.directories + .profile_logs_dir(profile.uuid) + .join(&datetime_string) + }; + fs::create_dir_all(&logs_dir)?; + + let stdout_log_path = logs_dir.join("stdout.log"); + let stderr_log_path = logs_dir.join("stderr.log"); + + // Create Minecraft child by inserting it into the state + // This also spawns the process and prepares the subsequent processes + let mut state_children = state.children.write().await; + state_children + .insert_process( + Uuid::new_v4(), + instance_path.to_path_buf(), + stdout_log_path, + stderr_log_path, + command, + post_exit_hook, + ) + .await + + }).await } diff --git a/theseus_playground/src/main.rs b/theseus_playground/src/main.rs index 5b966f861..747f8f79b 100644 --- a/theseus_playground/src/main.rs +++ b/theseus_playground/src/main.rs @@ -6,7 +6,7 @@ use dunce::canonicalize; use theseus::jre::autodetect_java_globals; use theseus::prelude::*; -use theseus::profile_create::profile_create; + use tokio::time::{sleep, Duration}; use tracing_error::ErrorLayer; use tracing_subscriber::layer::SubscriberExt;