use std::collections::BTreeSet; use std::sync::{Arc, Mutex}; use freya::prelude::spawn_forever; use oneclient_common::Patch; use oneclient_core::settings::store::save_settings_and_apply; use oneclient_core::{LauncherState, ProfileUpdate}; use oneclient_db::models::ClusterId; use oneclient_events::{Choice, Prompt, Signal}; use oneclient_java::{JavaRuntime, JavaVendor}; use tokio::sync::mpsc::UnboundedSender; use crate::hooks::Actions; use crate::launcher; /// Front-ends match on these pub const MICROSOFT_JAVA_CHOICE_INSTALL: &str = "java.microsoft.install"; pub const MICROSOFT_JAVA_CHOICE_NEVER: &str = "java.microsoft.never"; /// Cancel works per session, "Don't ask again" is set in settings static ASKED: Mutex> = Mutex::new(BTreeSet::new()); enum MicrosoftJavaAnswer { Install, Never, } pub fn spawn_auto_install() { tokio::spawn(auto_install(ui_refresh_channel())); } fn ui_refresh_channel() -> UnboundedSender<()> { let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<()>(); spawn_forever(async move { while rx.recv().await.is_some() { crate::hooks::invalidate_profile_queries().await; } }); tx } async fn auto_install(refresh: UnboundedSender<()>) { let Ok(state) = launcher::state() else { return; }; if opted_out(&state) || already_migrated(&state) { return; } let Some(cluster_id) = automatic_cluster(&state).await else { return; }; let major = match oneclient_core::required_java_major(&state, cluster_id).await { Ok(Some(major)) => major, Ok(None) => { tracing::info!(cluster_id, "the cluster's manifest names no Java version"); return; } Err(err) => { tracing::warn!( cluster_id, "could not read the cluster's Java version: {err:#}" ); return; } }; match state .java .has_vendor_runtime(&JavaVendor::Microsoft, Some(major)) .await { Ok(true) => return, Ok(false) => {} Err(err) => { tracing::warn!("could not check for a Microsoft Java runtime: {err:#}"); return; } } if !publishes(&state, major).await { return; } tracing::info!( cluster_id, major, "fetching a Microsoft runtime for the cluster on Automatic" ); install_and_unpin(cluster_id, major, refresh); } pub async fn offer_for_pinned_cluster(actions: &Actions, cluster_id: ClusterId) { let Ok(state) = launcher::state() else { return; }; if opted_out(&state) { return; } let Some(pinned) = pinned_runtime(&state, cluster_id).await else { return; }; if pinned.vendor == JavaVendor::Microsoft { return; } let major = match oneclient_core::required_java_major(&state, cluster_id).await { Ok(Some(major)) => major, Ok(None) => { tracing::info!(cluster_id, "the cluster's manifest names no Java version"); return; } Err(err) => { tracing::warn!( cluster_id, "could not read the cluster's Java version: {err:#}" ); return; } }; let installed = match state .java .has_vendor_runtime(&JavaVendor::Microsoft, Some(major)) .await { Ok(installed) => installed, Err(err) => { tracing::warn!("could not check for a Microsoft Java runtime: {err:#}"); false } }; if !installed && !publishes(&state, major).await { return; } if !claim_ask(cluster_id) { return; } match actions.events().ask(offer_prompt(installed)).await { Ok(Some(chosen)) => match chosen.value { MicrosoftJavaAnswer::Install => { if installed { unpin_to_automatic(&state, cluster_id, major, true, ui_refresh_channel()).await; } else { install_and_unpin(cluster_id, major, ui_refresh_channel()); } } MicrosoftJavaAnswer::Never => actions.skip_microsoft_java(), }, Ok(None) => tracing::info!(cluster_id, "Microsoft Java offer dismissed"), Err(err) => tracing::warn!(cluster_id, "Microsoft Java offer failed: {err:#}"), } } fn offer_prompt(installed: bool) -> Prompt { let closing = if installed { "You already have it installed, so this cluster will switch over immediately." } else { "Minecraft will launch with Microsoft OpenJDK next time." }; Prompt::new( "Microsoft Java runtime", format!( "Based on our research, we now recommend Microsoft's OpenJDK \ as they have specific optimizations for Minecraft. \ Would you like to use Microsoft OpenJDK as your default Java installation? \ {closing}" ), ) .option( Choice::new(MICROSOFT_JAVA_CHOICE_NEVER, "Don't ask again"), MicrosoftJavaAnswer::Never, ) .option( Choice::primary(MICROSOFT_JAVA_CHOICE_INSTALL, "Proceed"), MicrosoftJavaAnswer::Install, ) .dismiss("Cancel") } fn install_and_unpin(cluster_id: ClusterId, major: u32, refresh: UnboundedSender<()>) { tokio::spawn(async move { let Ok(state) = launcher::state() else { return }; let events = state.services.events.clone(); let runtime = match state .java .install_runtime_from(&JavaVendor::Microsoft, major) .await { Ok(runtime) => runtime, Err(err) => { events .notify("Java install failed") .body(err.to_string()) .error() .send(); return; } }; events.signal(Signal::JavaChanged); tracing::info!(cluster_id, version = %runtime.version, "installed a Microsoft runtime"); unpin_to_automatic(&state, cluster_id, major, false, refresh).await; }); } async fn unpin_to_automatic( state: &Arc, cluster_id: ClusterId, major: u32, immediate: bool, refresh: UnboundedSender<()>, ) { let events = state.services.events.clone(); mark_migrated(state).await; // Cleared rather than pointed at the new runtime Automatic ranks the // default vendor first so it lands on this one anyway and the cluster // keeps following later Microsoft installs instead of freezing on one let update = ProfileUpdate { java_path: Patch::Clear, ..Default::default() }; match state.clusters.update_profile(cluster_id, update).await { Ok(_) => { let _ = refresh.send(()); let body = if immediate { format!( "This cluster is back on Automatic and will run on the optimized Microsoft \ {major} JDK from now on." ) } else { format!( "This cluster is back on Automatic and will pick the optimized Microsoft \ {major} JDK from its next launch." ) }; events.notify("Java switched").body(body).send(); } // Only when installation was successfull and the cluster is not pointing to the new version Err(err) => events .notify("Cluster not switched") .body(format!( "Optimized Microsoft {major} JDK is ready but the cluster still points at its \ old runtime: {err}. Please report to discord.gg/polyfrost" )) .error() .send(), } } fn already_migrated(state: &Arc) -> bool { state.settings.read().microsoft_java_migrated } async fn mark_migrated(state: &Arc) { let snapshot = { let mut settings = state.settings.write(); if settings.microsoft_java_migrated { return; } settings.microsoft_java_migrated = true; settings.clone() }; if let Err(err) = save_settings_and_apply(&state.services, &snapshot).await { tracing::warn!("could not record the Microsoft Java migration: {err:#}"); } } fn opted_out(state: &Arc) -> bool { let settings = state.settings.read(); settings.skip_microsoft_java || !settings.seen_onboarding } fn claim_ask(cluster_id: ClusterId) -> bool { ASKED .lock() .map(|mut asked| asked.insert(cluster_id)) .unwrap_or(false) } /// If there is not Microsoft build for given host, the offer is abandoned async fn publishes(state: &Arc, major: u32) -> bool { match state .java .latest_package(&JavaVendor::Microsoft, major) .await { Ok(Some(_)) => true, Ok(None) => { tracing::info!(major, "Microsoft publishes no build for this host"); false } Err(err) => { tracing::warn!("could not reach Microsoft's downloads: {err:#}"); false } } } /// Only the cluster the launcher opens on the one the home panel preselects last played first and newest version when nothing has been played yet async fn automatic_cluster(state: &Arc) -> Option { let clusters = match state.clusters.list().await { Ok(clusters) => clusters, Err(err) => { tracing::warn!("could not read the cluster list: {err:#}"); return None; } }; let cluster = crate::utils::sort_clusters_for_home(clusters) .into_iter() .next()?; let global = state.settings.read().global_game_settings.clone(); match state.clusters.resolve_settings(&global, &cluster).await { Ok(profile) => { let automatic = profile.java_path.is_none(); tracing::info!( cluster_id = cluster.id, name = %cluster.name, automatic, "checked the Java of the cluster the launcher opens on" ); automatic.then_some(cluster.id) } Err(err) => { tracing::warn!( cluster_id = cluster.id, "could not resolve cluster settings: {err:#}" ); None } } } /// `None` when the cluster is on Automatic or when its pin points at a runtime that is no longer on disk async fn pinned_runtime(state: &Arc, cluster_id: ClusterId) -> Option { let cluster = state.clusters.get(cluster_id).await.ok()?; let global = state.settings.read().global_game_settings.clone(); let profile = state .clusters .resolve_settings(&global, &cluster) .await .ok()?; let path = profile.java_path?; state.java.runtime_for_profile(Some(&path)).await.ok()? }