364 lines
11 KiB
Rust
364 lines
11 KiB
Rust
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<BTreeSet<ClusterId>> = 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<MicrosoftJavaAnswer> {
|
|
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<LauncherState>,
|
|
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<LauncherState>) -> bool {
|
|
state.settings.read().microsoft_java_migrated
|
|
}
|
|
|
|
async fn mark_migrated(state: &Arc<LauncherState>) {
|
|
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<LauncherState>) -> 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<LauncherState>, 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<LauncherState>) -> Option<ClusterId> {
|
|
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<LauncherState>, cluster_id: ClusterId) -> Option<JavaRuntime> {
|
|
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()?
|
|
}
|