feat: multiple minecraft instances (#955)
* feat: mutliple minecraft instances (only dedicated clusters) * changeset
This commit is contained in:
parent
011c4416b1
commit
56d73e3ab1
20 changed files with 481 additions and 86 deletions
5
.changeset/multiple_minecraft_instances.md
Normal file
5
.changeset/multiple_minecraft_instances.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
default: minor
|
||||||
|
---
|
||||||
|
|
||||||
|
# Added safe multiple minecraft instances launching, only dedicated clusters tho
|
||||||
|
|
@ -13,7 +13,7 @@ use crate::hooks::{
|
||||||
use crate::routes::Route;
|
use crate::routes::Route;
|
||||||
use crate::theme::colors;
|
use crate::theme::colors;
|
||||||
use crate::utils::sort_clusters_for_home;
|
use crate::utils::sort_clusters_for_home;
|
||||||
use crate::view::app::{launch_button_state, launch_syncing};
|
use crate::view::app::{launch_button_state, launch_syncing, shared_dir_taken};
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
pub struct ActiveClusterPanel;
|
pub struct ActiveClusterPanel;
|
||||||
|
|
@ -92,6 +92,7 @@ impl Component for ActiveClusterPanel {
|
||||||
};
|
};
|
||||||
let cluster_id = cluster.id;
|
let cluster_id = cluster.id;
|
||||||
let syncing = launch_syncing(&launcher, cluster.uses_bundles());
|
let syncing = launch_syncing(&launcher, cluster.uses_bundles());
|
||||||
|
let shared_dir_taken = shared_dir_taken(&game, &sorted, cluster_id);
|
||||||
|
|
||||||
rect()
|
rect()
|
||||||
.vertical()
|
.vertical()
|
||||||
|
|
@ -127,7 +128,7 @@ impl Component for ActiveClusterPanel {
|
||||||
.child(launch_button(
|
.child(launch_button(
|
||||||
cluster_id,
|
cluster_id,
|
||||||
dispatch,
|
dispatch,
|
||||||
launch_button_state(&game, cluster_id, syncing),
|
launch_button_state(&game, cluster_id, syncing, shared_dir_taken),
|
||||||
))
|
))
|
||||||
.child(cluster_settings_button(cluster_id)),
|
.child(cluster_settings_button(cluster_id)),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -26,12 +26,14 @@ impl Actions {
|
||||||
.take()
|
.take()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `shared` is every other cluster in the shared game dir when this one
|
||||||
|
/// launches there too see [`crate::state::GameState::launch_block`]
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn launch_block(&self, cluster_id: ClusterId) -> Option<LaunchBlock> {
|
pub fn launch_block(&self, cluster_id: ClusterId, shared: &[ClusterId]) -> Option<LaunchBlock> {
|
||||||
let station = self.station();
|
let station = self.station();
|
||||||
let snapshot = station.peek();
|
let snapshot = station.peek();
|
||||||
let parallel = snapshot.settings.settings.allow_parallel_running_clusters;
|
let parallel = snapshot.settings.settings.allow_parallel_running_clusters;
|
||||||
snapshot.game.launch_block(cluster_id, parallel)
|
snapshot.game.launch_block(cluster_id, parallel, shared)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn report_missing_shortcut_target(&self, folder: &str) {
|
pub fn report_missing_shortcut_target(&self, folder: &str) {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ use crate::hooks::{
|
||||||
use crate::routes::Route;
|
use crate::routes::Route;
|
||||||
use crate::theme::colors;
|
use crate::theme::colors;
|
||||||
use crate::ui::entrance_motion_layer;
|
use crate::ui::entrance_motion_layer;
|
||||||
use crate::view::app::{launch_button_state, launch_syncing};
|
use crate::view::app::{launch_button_state, launch_syncing, use_shared_dir_taken};
|
||||||
|
|
||||||
const HEADER_HEIGHT: f32 = 64.;
|
const HEADER_HEIGHT: f32 = 64.;
|
||||||
const TABS_HEIGHT: f32 = 38.;
|
const TABS_HEIGHT: f32 = 38.;
|
||||||
|
|
@ -113,13 +113,14 @@ impl Component for ClusterShell {
|
||||||
let launcher = use_launcher();
|
let launcher = use_launcher();
|
||||||
let cluster = use_cluster(cluster_id);
|
let cluster = use_cluster(cluster_id);
|
||||||
let syncing = launch_syncing(&launcher, cluster.as_ref().is_none_or(|c| c.uses_bundles()));
|
let syncing = launch_syncing(&launcher, cluster.as_ref().is_none_or(|c| c.uses_bundles()));
|
||||||
|
let shared_dir_taken = use_shared_dir_taken(cluster_id);
|
||||||
|
|
||||||
let show_game_log = game.is_active(cluster_id);
|
let show_game_log = game.is_active(cluster_id);
|
||||||
let show_datapacks = cluster
|
let show_datapacks = cluster
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.is_none_or(|c| crate::view::app::cluster::supports_datapacks(&c.mc_version));
|
.is_none_or(|c| crate::view::app::cluster::supports_datapacks(&c.mc_version));
|
||||||
let show_mod_tabs = cluster.as_ref().is_none_or(|c| !c.lacks_mod_loader());
|
let show_mod_tabs = cluster.as_ref().is_none_or(|c| !c.lacks_mod_loader());
|
||||||
let launch_state = launch_button_state(&game, cluster_id, syncing);
|
let launch_state = launch_button_state(&game, cluster_id, syncing, shared_dir_taken);
|
||||||
|
|
||||||
let hidden_tab = !show_mod_tabs
|
let hidden_tab = !show_mod_tabs
|
||||||
&& matches!(
|
&& matches!(
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use freya::prelude::*;
|
use freya::prelude::*;
|
||||||
use freya::router::*;
|
use freya::router::*;
|
||||||
use oneclient_cluster::Cluster;
|
use oneclient_cluster::Cluster;
|
||||||
|
use oneclient_core::LauncherState;
|
||||||
|
|
||||||
use crate::Actions;
|
use crate::Actions;
|
||||||
use crate::components::IconType;
|
use crate::components::IconType;
|
||||||
|
|
@ -65,7 +66,8 @@ async fn launch_shortcut(actions: Actions, router: RouterContext, folder: String
|
||||||
cluster_id: cluster.id,
|
cluster_id: cluster.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
let Some(block) = actions.launch_block(cluster.id) else {
|
let shared = shared_dir_neighbours(&state, &cluster).await;
|
||||||
|
let Some(block) = actions.launch_block(cluster.id, &shared) else {
|
||||||
actions.launch_cluster(cluster.id);
|
actions.launch_cluster(cluster.id);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
@ -74,6 +76,27 @@ async fn launch_shortcut(actions: Actions, router: RouterContext, folder: String
|
||||||
report_already_open(&actions, &cluster, block, blocker.as_deref());
|
report_already_open(&actions, &cluster, block, blocker.as_deref());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The other clusters that launch into the shared game dir empty when this
|
||||||
|
/// cluster has a dedicated one
|
||||||
|
async fn shared_dir_neighbours(state: &LauncherState, cluster: &Cluster) -> Vec<i64> {
|
||||||
|
if cluster.uses_dedicated_dir() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
match state.clusters.list().await {
|
||||||
|
Ok(clusters) => clusters
|
||||||
|
.into_iter()
|
||||||
|
.filter(|other| other.id != cluster.id && !other.uses_dedicated_dir())
|
||||||
|
.map(|other| other.id)
|
||||||
|
.collect(),
|
||||||
|
Err(err) => {
|
||||||
|
// Core refuses a busy shared dir anyway this only words the notice
|
||||||
|
tracing::warn!(cluster_id = cluster.id, "cluster list failed: {err:#}");
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn blocker_name(cluster: &Cluster, block: LaunchBlock) -> Option<String> {
|
async fn blocker_name(cluster: &Cluster, block: LaunchBlock) -> Option<String> {
|
||||||
if block.cluster_id() == cluster.id {
|
if block.cluster_id() == cluster.id {
|
||||||
return None;
|
return None;
|
||||||
|
|
|
||||||
|
|
@ -275,21 +275,25 @@ impl GameState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `shared` lists the other clusters that would share the launch's game dir
|
||||||
|
/// empty for a dedicated cluster with parallel clusters allowed only those
|
||||||
|
/// can be in the way
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn launch_block(&self, cluster_id: i64, parallel: bool) -> Option<LaunchBlock> {
|
pub fn launch_block(
|
||||||
|
&self,
|
||||||
|
cluster_id: i64,
|
||||||
|
parallel: bool,
|
||||||
|
shared: &[i64],
|
||||||
|
) -> Option<LaunchBlock> {
|
||||||
if let Some(block) = self.block_for(cluster_id) {
|
if let Some(block) = self.block_for(cluster_id) {
|
||||||
return Some(block);
|
return Some(block);
|
||||||
}
|
}
|
||||||
|
|
||||||
if parallel {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.stages
|
self.stages
|
||||||
.keys()
|
.keys()
|
||||||
.chain(self.pending.iter())
|
.chain(self.pending.iter())
|
||||||
.copied()
|
.copied()
|
||||||
.filter(|id| *id != cluster_id)
|
.filter(|id| *id != cluster_id && (!parallel || shared.contains(id)))
|
||||||
.find_map(|id| self.block_for(id))
|
.find_map(|id| self.block_for(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -366,9 +370,32 @@ mod tests {
|
||||||
let mut game = GameState::default();
|
let mut game = GameState::default();
|
||||||
game.stages.insert(1, LaunchStage::Running);
|
game.stages.insert(1, LaunchStage::Running);
|
||||||
|
|
||||||
assert_eq!(game.launch_block(1, false), Some(LaunchBlock::Running(1)));
|
assert_eq!(
|
||||||
assert_eq!(game.launch_block(2, false), Some(LaunchBlock::Running(1)));
|
game.launch_block(1, false, &[]),
|
||||||
assert_eq!(game.launch_block(2, true), None);
|
Some(LaunchBlock::Running(1))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
game.launch_block(2, false, &[]),
|
||||||
|
Some(LaunchBlock::Running(1))
|
||||||
|
);
|
||||||
|
assert_eq!(game.launch_block(2, true, &[]), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parallel_clusters_still_hold_one_shared_game() {
|
||||||
|
let mut game = GameState::default();
|
||||||
|
game.stages.insert(1, LaunchStage::Running);
|
||||||
|
|
||||||
|
// 2 shares the game dir with 1 and 3 has its own
|
||||||
|
assert_eq!(
|
||||||
|
game.launch_block(2, true, &[1]),
|
||||||
|
Some(LaunchBlock::Running(1))
|
||||||
|
);
|
||||||
|
assert_eq!(game.launch_block(3, true, &[]), None);
|
||||||
|
assert_eq!(
|
||||||
|
game.launch_block(1, true, &[]),
|
||||||
|
Some(LaunchBlock::Running(1))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -376,8 +403,14 @@ mod tests {
|
||||||
let mut game = GameState::default();
|
let mut game = GameState::default();
|
||||||
game.stages.insert(1, LaunchStage::Downloading);
|
game.stages.insert(1, LaunchStage::Downloading);
|
||||||
|
|
||||||
assert_eq!(game.launch_block(1, false), Some(LaunchBlock::Starting(1)));
|
assert_eq!(
|
||||||
assert_eq!(game.launch_block(2, false), Some(LaunchBlock::Starting(1)));
|
game.launch_block(1, false, &[]),
|
||||||
|
Some(LaunchBlock::Starting(1))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
game.launch_block(2, false, &[]),
|
||||||
|
Some(LaunchBlock::Starting(1))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -385,8 +418,14 @@ mod tests {
|
||||||
let mut game = GameState::default();
|
let mut game = GameState::default();
|
||||||
game.begin_launch(1);
|
game.begin_launch(1);
|
||||||
|
|
||||||
assert_eq!(game.launch_block(1, false), Some(LaunchBlock::Starting(1)));
|
assert_eq!(
|
||||||
assert_eq!(game.launch_block(2, false), Some(LaunchBlock::Starting(1)));
|
game.launch_block(1, false, &[]),
|
||||||
|
Some(LaunchBlock::Starting(1))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
game.launch_block(2, false, &[]),
|
||||||
|
Some(LaunchBlock::Starting(1))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -394,23 +433,62 @@ mod tests {
|
||||||
let mut game = GameState::default();
|
let mut game = GameState::default();
|
||||||
game.stages.insert(1, LaunchStage::Exited);
|
game.stages.insert(1, LaunchStage::Exited);
|
||||||
|
|
||||||
assert_eq!(game.launch_block(2, false), None);
|
assert_eq!(game.launch_block(2, false, &[]), None);
|
||||||
assert_eq!(game.launch_block(1, false), None);
|
assert_eq!(game.launch_block(1, false, &[]), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn the_button_disables_on_the_claim_alone() {
|
fn the_button_disables_on_the_claim_alone() {
|
||||||
let mut game = GameState::default();
|
let mut game = GameState::default();
|
||||||
assert_eq!(launch_button_state(&game, 1, false), ("Launch", true));
|
assert_eq!(
|
||||||
|
launch_button_state(&game, 1, false, false),
|
||||||
|
("Launch", true)
|
||||||
|
);
|
||||||
|
|
||||||
game.begin_launch(1);
|
game.begin_launch(1);
|
||||||
assert_eq!(launch_button_state(&game, 1, false), ("Launching", false));
|
assert_eq!(
|
||||||
|
launch_button_state(&game, 1, false, false),
|
||||||
|
("Launching", false)
|
||||||
|
);
|
||||||
|
|
||||||
// Held past a failure which parks the stage at `Exited`
|
// Held past a failure which parks the stage at `Exited`
|
||||||
game.stages.insert(1, LaunchStage::Exited);
|
game.stages.insert(1, LaunchStage::Exited);
|
||||||
assert_eq!(launch_button_state(&game, 1, false), ("Launching", false));
|
assert_eq!(
|
||||||
|
launch_button_state(&game, 1, false, false),
|
||||||
|
("Launching", false)
|
||||||
|
);
|
||||||
|
|
||||||
game.finish_launch(1);
|
game.finish_launch(1);
|
||||||
assert_eq!(launch_button_state(&game, 1, false), ("Launch", true));
|
assert_eq!(
|
||||||
|
launch_button_state(&game, 1, false, false),
|
||||||
|
("Launch", true)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_queued_launch_says_it_is_waiting() {
|
||||||
|
let mut game = GameState::default();
|
||||||
|
game.stages.insert(1, LaunchStage::Waiting);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
launch_button_state(&game, 1, false, false),
|
||||||
|
("Waiting", false)
|
||||||
|
);
|
||||||
|
assert!(game.is_busy(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_taken_shared_dir_blocks_only_an_idle_cluster() {
|
||||||
|
let mut game = GameState::default();
|
||||||
|
assert_eq!(
|
||||||
|
launch_button_state(&game, 1, false, true),
|
||||||
|
("Folder in use", false)
|
||||||
|
);
|
||||||
|
|
||||||
|
game.stages.insert(1, LaunchStage::Running);
|
||||||
|
assert_eq!(
|
||||||
|
launch_button_state(&game, 1, false, true),
|
||||||
|
("Running", false)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ use crate::utils::{
|
||||||
line_title, resolve_cluster, split_clusters,
|
line_title, resolve_cluster, split_clusters,
|
||||||
};
|
};
|
||||||
use crate::view::app::clusters::CreateInstanceModal;
|
use crate::view::app::clusters::CreateInstanceModal;
|
||||||
use crate::view::app::{launch_button_state, launch_syncing};
|
use crate::view::app::{launch_button_state, launch_syncing, use_shared_dir_taken};
|
||||||
|
|
||||||
const PAGE_PADDING: Gaps = Gaps::new(12., 28., 28., 28.);
|
const PAGE_PADDING: Gaps = Gaps::new(12., 28., 28., 28.);
|
||||||
const COLUMN_GAP_PX: f32 = 24.;
|
const COLUMN_GAP_PX: f32 = 24.;
|
||||||
|
|
@ -723,6 +723,7 @@ impl Component for Sidebar {
|
||||||
|
|
||||||
let cluster_id = self.cluster_id;
|
let cluster_id = self.cluster_id;
|
||||||
let syncing = launch_syncing(&launcher, self.uses_bundles);
|
let syncing = launch_syncing(&launcher, self.uses_bundles);
|
||||||
|
let shared_dir_taken = use_shared_dir_taken(cluster_id);
|
||||||
let art_height = art_height_for(*sidebar_height.read());
|
let art_height = art_height_for(*sidebar_height.read());
|
||||||
let top_padding = if art_height.is_some() { 4. } else { 22. };
|
let top_padding = if art_height.is_some() { 4. } else { 22. };
|
||||||
|
|
||||||
|
|
@ -797,7 +798,7 @@ impl Component for Sidebar {
|
||||||
.child(play_button(
|
.child(play_button(
|
||||||
cluster_id,
|
cluster_id,
|
||||||
dispatch,
|
dispatch,
|
||||||
launch_button_state(&game, cluster_id, syncing),
|
launch_button_state(&game, cluster_id, syncing, shared_dir_taken),
|
||||||
))
|
))
|
||||||
.child(
|
.child(
|
||||||
Button::new()
|
Button::new()
|
||||||
|
|
|
||||||
|
|
@ -9,21 +9,27 @@ pub mod browser;
|
||||||
pub mod cluster;
|
pub mod cluster;
|
||||||
pub mod settings;
|
pub mod settings;
|
||||||
|
|
||||||
|
use crate::hooks::{settled_or_loading, use_clusters, use_game_snapshot};
|
||||||
use crate::state::{GameState as GameSnapshot, LauncherInit};
|
use crate::state::{GameState as GameSnapshot, LauncherInit};
|
||||||
|
use oneclient_core::clusters::Cluster;
|
||||||
use oneclient_events::LaunchStage;
|
use oneclient_events::LaunchStage;
|
||||||
|
|
||||||
pub fn launch_button_state(
|
pub fn launch_button_state(
|
||||||
game: &GameSnapshot,
|
game: &GameSnapshot,
|
||||||
cluster_id: i64,
|
cluster_id: i64,
|
||||||
syncing: bool,
|
syncing: bool,
|
||||||
|
shared_dir_taken: bool,
|
||||||
) -> (&'static str, bool) {
|
) -> (&'static str, bool) {
|
||||||
let state = match game.stage(cluster_id) {
|
let state = match game.stage(cluster_id) {
|
||||||
Some(LaunchStage::Checking) => ("Checking", false),
|
Some(LaunchStage::Checking) => ("Checking", false),
|
||||||
|
Some(LaunchStage::Waiting) => ("Waiting", false),
|
||||||
Some(LaunchStage::Downloading) => ("Downloading", false),
|
Some(LaunchStage::Downloading) => ("Downloading", false),
|
||||||
Some(LaunchStage::Launching) => ("Launching", false),
|
Some(LaunchStage::Launching) => ("Launching", false),
|
||||||
Some(LaunchStage::Running) => ("Running", false),
|
Some(LaunchStage::Running) => ("Running", false),
|
||||||
// The click's own flag held for a beat after a failure so a burst of clicks starts one game
|
// The click's own flag held for a beat after a failure so a burst of clicks starts one game
|
||||||
_ if game.is_launch_pending(cluster_id) => ("Launching", false),
|
_ if game.is_launch_pending(cluster_id) => ("Launching", false),
|
||||||
|
// Core refuses it anyway the shared game dir holds one game at a time
|
||||||
|
_ if shared_dir_taken => ("Folder in use", false),
|
||||||
_ => ("Launch", true),
|
_ => ("Launch", true),
|
||||||
};
|
};
|
||||||
// Block launching while the startup bundle download is still running
|
// Block launching while the startup bundle download is still running
|
||||||
|
|
@ -33,6 +39,29 @@ pub fn launch_button_state(
|
||||||
state
|
state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether another shared cluster is starting or running in the shared game dir
|
||||||
|
/// dedicated clusters never collide so any number of them may run alongside
|
||||||
|
pub fn shared_dir_taken(game: &GameSnapshot, clusters: &[Cluster], cluster_id: i64) -> bool {
|
||||||
|
// Active games are few so filter on them before touching the marker files
|
||||||
|
let mut others = clusters
|
||||||
|
.iter()
|
||||||
|
.filter(|c| c.id != cluster_id && (game.is_active(c.id) || game.is_launch_pending(c.id)))
|
||||||
|
.peekable();
|
||||||
|
|
||||||
|
others.peek().is_some()
|
||||||
|
&& clusters
|
||||||
|
.iter()
|
||||||
|
.find(|c| c.id == cluster_id)
|
||||||
|
.is_some_and(|c| !c.uses_dedicated_dir())
|
||||||
|
&& others.any(|c| !c.uses_dedicated_dir())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn use_shared_dir_taken(cluster_id: i64) -> bool {
|
||||||
|
let game = use_game_snapshot();
|
||||||
|
let clusters = settled_or_loading(&use_clusters()).unwrap_or_default();
|
||||||
|
shared_dir_taken(&game, &clusters, cluster_id)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn launch_syncing(launcher: &LauncherInit, uses_bundles: bool) -> bool {
|
pub fn launch_syncing(launcher: &LauncherInit, uses_bundles: bool) -> bool {
|
||||||
launcher.fetching || (uses_bundles && launcher.syncing_bundles)
|
launcher.fetching || (uses_bundles && launcher.syncing_bundles)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -143,7 +143,16 @@ async fn game_download_bytes(services: &LauncherServices, info: &VersionInfo) ->
|
||||||
let _ = services;
|
let _ = services;
|
||||||
// Runs before a Java runtime is resolved so rules are checked against the
|
// Runs before a Java runtime is resolved so rules are checked against the
|
||||||
// host architecture only natives-related edge cases shift
|
// host architecture only natives-related edge cases shift
|
||||||
match game::plan_downloads(info, assets_index, std::env::consts::ARCH, false, false).await {
|
match game::plan_downloads(
|
||||||
|
info,
|
||||||
|
assets_index,
|
||||||
|
std::env::consts::ARCH,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
game::NativesMode::Extract,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(plan) => plan.total_bytes(),
|
Ok(plan) => plan.total_bytes(),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
tracing::warn!("download plan failed, estimating full size: {err}");
|
tracing::warn!("download plan failed, estimating full size: {err}");
|
||||||
|
|
@ -311,6 +320,8 @@ async fn install_cluster(
|
||||||
.await?
|
.await?
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let natives_mode = crate::game::natives::natives_mode(state, &version_info.id, None);
|
||||||
|
|
||||||
download_minecraft(
|
download_minecraft(
|
||||||
&state.services.mc(),
|
&state.services.mc(),
|
||||||
progress,
|
progress,
|
||||||
|
|
@ -318,6 +329,7 @@ async fn install_cluster(
|
||||||
&java.os_arch,
|
&java.os_arch,
|
||||||
minecraft_updated,
|
minecraft_updated,
|
||||||
force,
|
force,
|
||||||
|
natives_mode,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,8 +45,9 @@ pub async fn launch_cluster(
|
||||||
) -> LauncherResult<LaunchedGame> {
|
) -> LauncherResult<LaunchedGame> {
|
||||||
tracing::info!(cluster_id, search_for_java, "launching cluster");
|
tracing::info!(cluster_id, search_for_java, "launching cluster");
|
||||||
|
|
||||||
let parallel = state.settings.read().allow_parallel_running_clusters;
|
// Parallel clusters or not a cluster runs only once a second game would
|
||||||
if !parallel && state.games.is_active(cluster_id) {
|
// share its directory and its slot in the process manager
|
||||||
|
if state.games.is_active(cluster_id) {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
cluster_id,
|
cluster_id,
|
||||||
"cluster already launching or running; refusing launch"
|
"cluster already launching or running; refusing launch"
|
||||||
|
|
@ -54,13 +55,9 @@ pub async fn launch_cluster(
|
||||||
return Err(GameError::AlreadyRunning(cluster_id).into());
|
return Err(GameError::AlreadyRunning(cluster_id).into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// A parallel attempt shares its entry with the session already under way, so
|
|
||||||
// clearing it here would drop that game's kill sender and pid
|
|
||||||
let adopted = state.games.is_active(cluster_id);
|
|
||||||
|
|
||||||
let result = start(state, cluster_id, account, search_for_java).await;
|
let result = start(state, cluster_id, account, search_for_java).await;
|
||||||
|
|
||||||
if result.is_err() && !adopted {
|
if result.is_err() {
|
||||||
state.games.remove(cluster_id);
|
state.games.remove(cluster_id);
|
||||||
state
|
state
|
||||||
.services
|
.services
|
||||||
|
|
@ -83,7 +80,19 @@ async fn start(
|
||||||
events.game_stage(cluster_id, s);
|
events.game_stage(cluster_id, s);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// The stage goes first so a click while this waits is refused as already
|
||||||
|
// active rather than queued behind it
|
||||||
stage(LaunchStage::Checking);
|
stage(LaunchStage::Checking);
|
||||||
|
let launch_slot = match state.games.try_launch_slot() {
|
||||||
|
Some(slot) => slot,
|
||||||
|
None => {
|
||||||
|
tracing::debug!(cluster_id, "another launch is preparing; waiting for it");
|
||||||
|
stage(LaunchStage::Waiting);
|
||||||
|
let slot = state.games.launch_slot().await;
|
||||||
|
stage(LaunchStage::Checking);
|
||||||
|
slot
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let existing = state.clusters.get(cluster_id).await?;
|
let existing = state.clusters.get(cluster_id).await?;
|
||||||
|
|
||||||
|
|
@ -92,20 +101,15 @@ async fn start(
|
||||||
|
|
||||||
// Non-dedicated clusters share one directory so a second game there would
|
// Non-dedicated clusters share one directory so a second game there would
|
||||||
// materialize over the running one's
|
// materialize over the running one's
|
||||||
// `dir_in_use_by` waves a cluster past
|
if let Err(other) = state.games.claim_dir(cluster_id, &game_dir, !dedicated) {
|
||||||
// its own session which parallel launches make reachable
|
if dedicated {
|
||||||
if !dedicated && let Some(other) = state.games.dir_in_use(&game_dir) {
|
return Err(GameError::DirectoryInUse(other).into());
|
||||||
|
}
|
||||||
tracing::warn!(cluster_id, other, "shared game dir busy; refusing launch");
|
tracing::warn!(cluster_id, other, "shared game dir busy; refusing launch");
|
||||||
let name = running_cluster_name(state, other).await;
|
let name = running_cluster_name(state, other).await;
|
||||||
return Err(GameError::SharedDirectoryBusy(name).into());
|
return Err(GameError::SharedDirectoryBusy(name).into());
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(other) = state.games.dir_in_use_by(&game_dir, cluster_id) {
|
|
||||||
return Err(GameError::DirectoryInUse(other).into());
|
|
||||||
}
|
|
||||||
|
|
||||||
state.games.set_dir(cluster_id, game_dir.clone());
|
|
||||||
|
|
||||||
match state.clusters.mark_played(cluster_id).await {
|
match state.clusters.mark_played(cluster_id).await {
|
||||||
Ok(()) => events.signal(oneclient_events::Signal::ClustersChanged),
|
Ok(()) => events.signal(oneclient_events::Signal::ClustersChanged),
|
||||||
Err(err) => tracing::debug!(cluster_id, error = %err, "could not record the launch time"),
|
Err(err) => tracing::debug!(cluster_id, error = %err, "could not record the launch time"),
|
||||||
|
|
@ -221,6 +225,10 @@ async fn start(
|
||||||
"resolved launch metadata"
|
"resolved launch metadata"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if let Some(dir) = crate::game::natives::natives_dir(&version_name) {
|
||||||
|
state.games.set_natives(cluster_id, dir);
|
||||||
|
}
|
||||||
|
|
||||||
let java = if let Some(runtime) = state
|
let java = if let Some(runtime) = state
|
||||||
.java
|
.java
|
||||||
.runtime_for_profile(profile.java_path.as_deref())
|
.runtime_for_profile(profile.java_path.as_deref())
|
||||||
|
|
@ -248,6 +256,8 @@ async fn start(
|
||||||
.set_stage(cluster_id, ClusterStage::Repairing)
|
.set_stage(cluster_id, ClusterStage::Repairing)
|
||||||
.await;
|
.await;
|
||||||
stage(LaunchStage::Downloading);
|
stage(LaunchStage::Downloading);
|
||||||
|
let natives_mode =
|
||||||
|
crate::game::natives::natives_mode(state, &version_name, Some(cluster_id));
|
||||||
if let Err(err) = download_minecraft(
|
if let Err(err) = download_minecraft(
|
||||||
&state.services.mc(),
|
&state.services.mc(),
|
||||||
&progress,
|
&progress,
|
||||||
|
|
@ -255,6 +265,7 @@ async fn start(
|
||||||
&java.os_arch,
|
&java.os_arch,
|
||||||
updated,
|
updated,
|
||||||
false,
|
false,
|
||||||
|
natives_mode,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
|
|
@ -366,6 +377,8 @@ async fn start(
|
||||||
profile.game_args.as_deref(),
|
profile.game_args.as_deref(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
drop(launch_slot);
|
||||||
|
|
||||||
if let Some(reason) = run_hook(profile.hook_pre.as_deref(), &cwd).await {
|
if let Some(reason) = run_hook(profile.hook_pre.as_deref(), &cwd).await {
|
||||||
events
|
events
|
||||||
.notify("Pre-launch command failed")
|
.notify("Pre-launch command failed")
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ pub mod gpu;
|
||||||
mod heal;
|
mod heal;
|
||||||
mod launch;
|
mod launch;
|
||||||
mod log_replay;
|
mod log_replay;
|
||||||
|
pub(crate) mod natives;
|
||||||
mod process;
|
mod process;
|
||||||
mod reattach;
|
mod reattach;
|
||||||
mod session;
|
mod session;
|
||||||
|
|
@ -28,7 +29,8 @@ pub use fabric::{mods_folder_argument, uses_cluster_mods_folder};
|
||||||
pub use heal::clear_zeroed_files;
|
pub use heal::clear_zeroed_files;
|
||||||
pub use launch::{LaunchedGame, is_running, launch_cluster, offer_repair};
|
pub use launch::{LaunchedGame, is_running, launch_cluster, offer_repair};
|
||||||
pub use oneclient_mc::{
|
pub use oneclient_mc::{
|
||||||
DownloadPlan, GameVersionInfo, GameVersionKind, NativesReport, PlannedLibrary, check_natives,
|
DownloadPlan, GameVersionInfo, GameVersionKind, NativesMode, NativesReport, PlannedLibrary,
|
||||||
|
check_natives,
|
||||||
confirm_incomplete_install, download_minecraft, download_version_info, game_files_missing,
|
confirm_incomplete_install, download_minecraft, download_version_info, game_files_missing,
|
||||||
get_game_versions, get_loader_version, get_loader_versions, get_loaders_for_version,
|
get_game_versions, get_loader_version, get_loader_versions, get_loaders_for_version,
|
||||||
get_version_ids, get_versions_for_loader, is_version_updated, libraries_missing,
|
get_version_ids, get_versions_for_loader, is_version_updated, libraries_missing,
|
||||||
|
|
|
||||||
58
packages/oneclient_core/src/game/natives.rs
Normal file
58
packages/oneclient_core/src/game/natives.rs
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use oneclient_common::domain::GameLoader;
|
||||||
|
use oneclient_common::paths;
|
||||||
|
use oneclient_mc::MetadataStore;
|
||||||
|
|
||||||
|
use crate::clusters::Cluster;
|
||||||
|
use crate::game::NativesMode;
|
||||||
|
use crate::state::LauncherState;
|
||||||
|
|
||||||
|
pub(crate) fn natives_dir(version_name: &str) -> Option<PathBuf> {
|
||||||
|
paths::natives_dir().ok().map(|dir| dir.join(version_name))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn cached_version_name(metadata: &MetadataStore, cluster: &Cluster) -> Option<String> {
|
||||||
|
let mc_version = oneclient_common::version::normalize_mc_version_input(&cluster.mc_version);
|
||||||
|
|
||||||
|
if cluster.mc_loader == GameLoader::Vanilla {
|
||||||
|
return Some(mc_version);
|
||||||
|
}
|
||||||
|
|
||||||
|
let loader = oneclient_mc::get_loader_version_cached(
|
||||||
|
metadata,
|
||||||
|
&mc_version,
|
||||||
|
cluster.mc_loader,
|
||||||
|
cluster.mc_loader_version.as_deref(),
|
||||||
|
)
|
||||||
|
.ok()??;
|
||||||
|
|
||||||
|
Some(format!("{mc_version}-{}", loader.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn natives_holder(
|
||||||
|
state: &LauncherState,
|
||||||
|
version_name: &str,
|
||||||
|
exclude: Option<i64>,
|
||||||
|
) -> Option<i64> {
|
||||||
|
let dir = natives_dir(version_name)?;
|
||||||
|
state.games.natives_in_use_by(&dir, exclude)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn natives_mode(
|
||||||
|
state: &LauncherState,
|
||||||
|
version_name: &str,
|
||||||
|
exclude: Option<i64>,
|
||||||
|
) -> NativesMode {
|
||||||
|
match natives_holder(state, version_name, exclude) {
|
||||||
|
Some(holder) => {
|
||||||
|
tracing::info!(
|
||||||
|
version_name,
|
||||||
|
holder,
|
||||||
|
"natives are loaded by a running game; leaving them in place"
|
||||||
|
);
|
||||||
|
NativesMode::LeaveInPlace
|
||||||
|
}
|
||||||
|
None => NativesMode::Extract,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -63,6 +63,8 @@ pub struct GameProcessManager {
|
||||||
inner: Mutex<HashMap<i64, GameProcess>>,
|
inner: Mutex<HashMap<i64, GameProcess>>,
|
||||||
kills: Mutex<HashMap<i64, oneshot::Sender<()>>>,
|
kills: Mutex<HashMap<i64, oneshot::Sender<()>>>,
|
||||||
dirs: Mutex<HashMap<i64, PathBuf>>,
|
dirs: Mutex<HashMap<i64, PathBuf>>,
|
||||||
|
natives: Mutex<HashMap<i64, PathBuf>>,
|
||||||
|
launching: tokio::sync::Mutex<()>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GameProcessManager {
|
impl GameProcessManager {
|
||||||
|
|
@ -70,6 +72,18 @@ impl GameProcessManager {
|
||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One launch prepares at a time two launches at once would install the
|
||||||
|
/// same Java or download the same game files into the same paths
|
||||||
|
/// Held from the checks until the process spawns not for the whole session
|
||||||
|
pub async fn launch_slot(&self) -> tokio::sync::MutexGuard<'_, ()> {
|
||||||
|
self.launching.lock().await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `None` while another launch holds the slot
|
||||||
|
pub fn try_launch_slot(&self) -> Option<tokio::sync::MutexGuard<'_, ()>> {
|
||||||
|
self.launching.try_lock().ok()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn register_kill(&self, cluster_id: i64, tx: oneshot::Sender<()>) {
|
pub fn register_kill(&self, cluster_id: i64, tx: oneshot::Sender<()>) {
|
||||||
self.kills.lock().unwrap().insert(cluster_id, tx);
|
self.kills.lock().unwrap().insert(cluster_id, tx);
|
||||||
}
|
}
|
||||||
|
|
@ -110,21 +124,44 @@ impl GameProcessManager {
|
||||||
self.inner.lock().unwrap().remove(&cluster_id);
|
self.inner.lock().unwrap().remove(&cluster_id);
|
||||||
self.kills.lock().unwrap().remove(&cluster_id);
|
self.kills.lock().unwrap().remove(&cluster_id);
|
||||||
self.dirs.lock().unwrap().remove(&cluster_id);
|
self.dirs.lock().unwrap().remove(&cluster_id);
|
||||||
|
self.natives.lock().unwrap().remove(&cluster_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_natives(&self, cluster_id: i64, dir: PathBuf) {
|
||||||
|
self.natives.lock().unwrap().insert(cluster_id, dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn natives_in_use_by(&self, dir: &Path, exclude: Option<i64>) -> Option<i64> {
|
||||||
|
self.natives
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.find(|(id, d)| Some(**id) != exclude && d.as_path() == dir)
|
||||||
|
.map(|(id, _)| *id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_dir(&self, cluster_id: i64, dir: PathBuf) {
|
pub fn set_dir(&self, cluster_id: i64, dir: PathBuf) {
|
||||||
self.dirs.lock().unwrap().insert(cluster_id, dir);
|
self.dirs.lock().unwrap().insert(cluster_id, dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Like [`Self::dir_in_use_by`] but counts a cluster's own session too the
|
/// Check and record under one lock so two launches racing on separate
|
||||||
/// shared game directory holds one game at a time whoever it belongs to
|
/// threads cannot both find the directory free
|
||||||
pub fn dir_in_use(&self, dir: &Path) -> Option<i64> {
|
/// `exclusive` is for the shared game directory which holds one game at a
|
||||||
self.dirs
|
/// time whoever it belongs to counting the cluster's own session too
|
||||||
.lock()
|
/// Returns the cluster already holding `dir`
|
||||||
.unwrap()
|
pub fn claim_dir(&self, cluster_id: i64, dir: &Path, exclusive: bool) -> Result<(), i64> {
|
||||||
|
let mut dirs = self.dirs.lock().unwrap();
|
||||||
|
|
||||||
|
let holder = dirs
|
||||||
.iter()
|
.iter()
|
||||||
.find(|(_, d)| d.as_path() == dir)
|
.find(|(id, d)| (exclusive || **id != cluster_id) && d.as_path() == dir)
|
||||||
.map(|(id, _)| *id)
|
.map(|(id, _)| *id);
|
||||||
|
if let Some(holder) = holder {
|
||||||
|
return Err(holder);
|
||||||
|
}
|
||||||
|
|
||||||
|
dirs.insert(cluster_id, dir.to_path_buf());
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn dir_in_use_by(&self, dir: &Path, exclude: i64) -> Option<i64> {
|
pub fn dir_in_use_by(&self, dir: &Path, exclude: i64) -> Option<i64> {
|
||||||
|
|
@ -177,3 +214,43 @@ impl GameProcessManager {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_shared_dir_takes_one_game() {
|
||||||
|
let games = GameProcessManager::new();
|
||||||
|
let shared = Path::new("shared");
|
||||||
|
|
||||||
|
assert_eq!(games.claim_dir(1, shared, true), Ok(()));
|
||||||
|
assert_eq!(games.claim_dir(2, shared, true), Err(1));
|
||||||
|
|
||||||
|
games.remove(1);
|
||||||
|
assert_eq!(games.claim_dir(2, shared, true), Ok(()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dedicated_dirs_do_not_get_in_each_others_way() {
|
||||||
|
let games = GameProcessManager::new();
|
||||||
|
|
||||||
|
assert_eq!(games.claim_dir(1, Path::new("one"), false), Ok(()));
|
||||||
|
assert_eq!(games.claim_dir(2, Path::new("two"), false), Ok(()));
|
||||||
|
assert_eq!(games.claim_dir(3, Path::new("one"), false), Err(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn natives_are_held_until_the_game_is_removed() {
|
||||||
|
let games = GameProcessManager::new();
|
||||||
|
let natives = Path::new("natives/1.8.9");
|
||||||
|
|
||||||
|
games.set_natives(1, natives.to_path_buf());
|
||||||
|
assert_eq!(games.natives_in_use_by(natives, None), Some(1));
|
||||||
|
assert_eq!(games.natives_in_use_by(natives, Some(1)), None);
|
||||||
|
assert_eq!(games.natives_in_use_by(Path::new("natives/1.21"), None), None);
|
||||||
|
|
||||||
|
games.remove(1);
|
||||||
|
assert_eq!(games.natives_in_use_by(natives, None), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -155,6 +155,15 @@ async fn readopt(
|
||||||
state.games.set_stage(cluster_id, LaunchStage::Running);
|
state.games.set_stage(cluster_id, LaunchStage::Running);
|
||||||
state.games.set_pid(cluster_id, Some(pid));
|
state.games.set_pid(cluster_id, Some(pid));
|
||||||
state.games.set_dir(cluster_id, cwd.clone());
|
state.games.set_dir(cluster_id, cwd.clone());
|
||||||
|
let version_name = state
|
||||||
|
.metadata
|
||||||
|
.try_lock()
|
||||||
|
.ok()
|
||||||
|
.and_then(|metadata| crate::game::natives::cached_version_name(&metadata, &cluster));
|
||||||
|
match version_name.as_deref().and_then(crate::game::natives::natives_dir) {
|
||||||
|
Some(dir) => state.games.set_natives(cluster_id, dir),
|
||||||
|
None => tracing::debug!(cluster_id, "could not resolve the re-adopted game's natives"),
|
||||||
|
}
|
||||||
state
|
state
|
||||||
.services
|
.services
|
||||||
.events
|
.events
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,8 @@ pub struct ViewState {
|
||||||
pub sort: Option<String>,
|
pub sort: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const CURRENT_SETTINGS_VERSION: u32 = 2;
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct LauncherSettings {
|
pub struct LauncherSettings {
|
||||||
|
|
@ -93,10 +95,22 @@ impl LauncherSettings {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl LauncherSettings {
|
||||||
|
/// Brings a file written by an older launcher up to the current defaults
|
||||||
|
pub fn migrate(&mut self) {
|
||||||
|
if self.settings_version < 2 {
|
||||||
|
// Had no toggle so a saved `false` was never the user's choice
|
||||||
|
// dedicated clusters plus one shared cluster may now run together
|
||||||
|
self.allow_parallel_running_clusters = true;
|
||||||
|
}
|
||||||
|
self.settings_version = CURRENT_SETTINGS_VERSION;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for LauncherSettings {
|
impl Default for LauncherSettings {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
settings_version: 1,
|
settings_version: CURRENT_SETTINGS_VERSION,
|
||||||
log_debug: false,
|
log_debug: false,
|
||||||
auto_update: true,
|
auto_update: true,
|
||||||
crash_reporting: true,
|
crash_reporting: true,
|
||||||
|
|
@ -104,7 +118,7 @@ impl Default for LauncherSettings {
|
||||||
enable_gamemode: false,
|
enable_gamemode: false,
|
||||||
max_concurrent_requests: 25,
|
max_concurrent_requests: 25,
|
||||||
global_game_settings: GameSettingsProfile::default_global_profile(),
|
global_game_settings: GameSettingsProfile::default_global_profile(),
|
||||||
allow_parallel_running_clusters: false,
|
allow_parallel_running_clusters: true,
|
||||||
launch_behaviour: LaunchBehaviour::default(),
|
launch_behaviour: LaunchBehaviour::default(),
|
||||||
run_in_background: false,
|
run_in_background: false,
|
||||||
show_tray_icon: true,
|
show_tray_icon: true,
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,9 @@ pub async fn load_settings(notify: Option<&EventBus>) -> LauncherSettings {
|
||||||
LauncherSettings::default()
|
LauncherSettings::default()
|
||||||
} else {
|
} else {
|
||||||
let data = polyio::read(&path).await?;
|
let data = polyio::read(&path).await?;
|
||||||
serde_json::from_slice(&data)?
|
let mut settings: LauncherSettings = serde_json::from_slice(&data)?;
|
||||||
|
settings.migrate();
|
||||||
|
settings
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
.await
|
.await
|
||||||
|
|
|
||||||
|
|
@ -156,25 +156,11 @@ async fn natives_in_use(state: &LauncherState) -> Option<HashSet<String>> {
|
||||||
|
|
||||||
let mut in_use = HashSet::new();
|
let mut in_use = HashSet::new();
|
||||||
for cluster in clusters {
|
for cluster in clusters {
|
||||||
let mc_version = oneclient_common::version::normalize_mc_version_input(&cluster.mc_version);
|
match crate::game::natives::cached_version_name(&metadata, &cluster) {
|
||||||
|
Some(version_name) => {
|
||||||
if cluster.mc_loader == oneclient_common::domain::GameLoader::Vanilla {
|
in_use.insert(version_name);
|
||||||
in_use.insert(mc_version);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let resolved = oneclient_mc::get_loader_version_cached(
|
|
||||||
&metadata,
|
|
||||||
&mc_version,
|
|
||||||
cluster.mc_loader,
|
|
||||||
cluster.mc_loader_version.as_deref(),
|
|
||||||
);
|
|
||||||
|
|
||||||
match resolved {
|
|
||||||
Ok(Some(loader)) => {
|
|
||||||
in_use.insert(format!("{mc_version}-{}", loader.id));
|
|
||||||
}
|
}
|
||||||
Ok(None) | Err(_) => {
|
None => {
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
cluster = %cluster.name,
|
cluster = %cluster.name,
|
||||||
loader = %cluster.mc_loader,
|
loader = %cluster.mc_loader,
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ pub struct ClusterVerifyReport {
|
||||||
pub unrepairable: Vec<String>,
|
pub unrepairable: Vec<String>,
|
||||||
/// Game files re-downloaded uncounted because the assets index was gone too
|
/// Game files re-downloaded uncounted because the assets index was gone too
|
||||||
pub reinstalled_game_files: bool,
|
pub reinstalled_game_files: bool,
|
||||||
|
pub natives_in_use_by: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ClusterVerifyReport {
|
impl ClusterVerifyReport {
|
||||||
|
|
@ -38,6 +39,16 @@ impl ClusterVerifyReport {
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn summary(&self) -> String {
|
pub fn summary(&self) -> String {
|
||||||
|
let summary = self.counts_summary();
|
||||||
|
match &self.natives_in_use_by {
|
||||||
|
Some(name) => format!(
|
||||||
|
"{summary} Native libraries were skipped because {name} is running, verify again after closing it."
|
||||||
|
),
|
||||||
|
None => summary,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn counts_summary(&self) -> String {
|
||||||
if self.is_clean() {
|
if self.is_clean() {
|
||||||
return format!("All {} files verified.", self.checked);
|
return format!("All {} files verified.", self.checked);
|
||||||
}
|
}
|
||||||
|
|
@ -155,6 +166,7 @@ async fn run_verify(
|
||||||
// Counted apart from content which repairs itself as it goes one bucket
|
// Counted apart from content which repairs itself as it goes one bucket
|
||||||
// would count the content ones twice
|
// would count the content ones twice
|
||||||
let mut game_broken = 0;
|
let mut game_broken = 0;
|
||||||
|
let mut natives_broken = 0;
|
||||||
|
|
||||||
// The index lives in the assets directory so its absence usually means that
|
// The index lives in the assets directory so its absence usually means that
|
||||||
// whole directory is gone hand the work to prepare
|
// whole directory is gone hand the work to prepare
|
||||||
|
|
@ -174,6 +186,7 @@ async fn run_verify(
|
||||||
report.corrupt += game.corrupt;
|
report.corrupt += game.corrupt;
|
||||||
report.missing += game.missing;
|
report.missing += game.missing;
|
||||||
game_broken = game.corrupt + game.missing;
|
game_broken = game.corrupt + game.missing;
|
||||||
|
natives_broken = game.natives_unextracted;
|
||||||
} else {
|
} else {
|
||||||
tracing::warn!("assets index is missing; re-downloading the game files wholesale");
|
tracing::warn!("assets index is missing; re-downloading the game files wholesale");
|
||||||
}
|
}
|
||||||
|
|
@ -182,9 +195,26 @@ async fn run_verify(
|
||||||
|
|
||||||
// Runs `prepare` directly not the launch path which skips clusters already
|
// Runs `prepare` directly not the launch path which skips clusters already
|
||||||
// marked `Ready` and so never heals one with deleted files
|
// marked `Ready` and so never heals one with deleted files
|
||||||
if game_broken > 0 || reinstalled_game_files {
|
let natives_holder = crate::game::natives::natives_holder(state, &version_info.id, None)
|
||||||
|
.filter(|_| natives_broken > 0);
|
||||||
|
let repairable = if natives_holder.is_some() {
|
||||||
|
game_broken.saturating_sub(natives_broken)
|
||||||
|
} else {
|
||||||
|
game_broken
|
||||||
|
};
|
||||||
|
|
||||||
|
if repairable > 0 || reinstalled_game_files {
|
||||||
prepare_cluster_locked(state, cluster_id, false, true, true, Some(progress)).await?;
|
prepare_cluster_locked(state, cluster_id, false, true, true, Some(progress)).await?;
|
||||||
report.repaired += game_broken;
|
report.repaired += repairable;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(holder) = natives_holder {
|
||||||
|
let name = state
|
||||||
|
.clusters
|
||||||
|
.get(holder)
|
||||||
|
.await
|
||||||
|
.map_or_else(|_| "another cluster".to_string(), |cluster| cluster.name);
|
||||||
|
report.natives_in_use_by = Some(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
report.reinstalled_game_files = reinstalled_game_files;
|
report.reinstalled_game_files = reinstalled_game_files;
|
||||||
|
|
@ -358,6 +388,7 @@ mod tests {
|
||||||
repaired: 4,
|
repaired: 4,
|
||||||
unrepairable: Vec::new(),
|
unrepairable: Vec::new(),
|
||||||
reinstalled_game_files: false,
|
reinstalled_game_files: false,
|
||||||
|
natives_in_use_by: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
assert!(!report.is_clean());
|
assert!(!report.is_clean());
|
||||||
|
|
@ -382,6 +413,20 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn skipped_natives_are_named_rather_than_counted_as_repaired() {
|
||||||
|
let report = ClusterVerifyReport {
|
||||||
|
checked: 40,
|
||||||
|
natives_in_use_by: Some("Hypixel".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
report.summary(),
|
||||||
|
"All 40 files verified. Native libraries were skipped because Hypixel is running, verify again after closing it."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn content_that_cannot_be_replaced_is_still_reported() {
|
fn content_that_cannot_be_replaced_is_still_reported() {
|
||||||
let report = ClusterVerifyReport {
|
let report = ClusterVerifyReport {
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,8 @@ pub enum GameEvent {
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum LaunchStage {
|
pub enum LaunchStage {
|
||||||
Checking,
|
Checking,
|
||||||
|
/// Queued behind another cluster's launch
|
||||||
|
Waiting,
|
||||||
Downloading,
|
Downloading,
|
||||||
Launching,
|
Launching,
|
||||||
Running,
|
Running,
|
||||||
|
|
@ -118,7 +120,10 @@ pub enum LaunchStage {
|
||||||
impl LaunchStage {
|
impl LaunchStage {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn is_busy(self) -> bool {
|
pub fn is_busy(self) -> bool {
|
||||||
matches!(self, Self::Checking | Self::Downloading | Self::Launching)
|
matches!(
|
||||||
|
self,
|
||||||
|
Self::Checking | Self::Waiting | Self::Downloading | Self::Launching
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,12 @@ const ASSET_DOWNLOAD_CONCURRENCY: usize = 32;
|
||||||
/// Libraries are larger and fewer less fan-out is needed to saturate the link
|
/// Libraries are larger and fewer less fan-out is needed to saturate the link
|
||||||
const LIBRARY_DOWNLOAD_CONCURRENCY: usize = 16;
|
const LIBRARY_DOWNLOAD_CONCURRENCY: usize = 16;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum NativesMode {
|
||||||
|
Extract,
|
||||||
|
LeaveInPlace,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct PlannedLibrary {
|
pub struct PlannedLibrary {
|
||||||
pub library: Library,
|
pub library: Library,
|
||||||
|
|
@ -167,6 +173,7 @@ pub async fn plan_downloads(
|
||||||
java_arch: &str,
|
java_arch: &str,
|
||||||
minecraft_updated: bool,
|
minecraft_updated: bool,
|
||||||
force: bool,
|
force: bool,
|
||||||
|
natives_mode: NativesMode,
|
||||||
) -> McResult<DownloadPlan> {
|
) -> McResult<DownloadPlan> {
|
||||||
let legacy = uses_legacy_assets(&version.assets);
|
let legacy = uses_legacy_assets(&version.assets);
|
||||||
let asset_dir = if legacy {
|
let asset_dir = if legacy {
|
||||||
|
|
@ -227,7 +234,12 @@ pub async fn plan_downloads(
|
||||||
&lib_dir.join(&artifact_path),
|
&lib_dir.join(&artifact_path),
|
||||||
library_artifact_size(&lib),
|
library_artifact_size(&lib),
|
||||||
));
|
));
|
||||||
let natives = force || !natives_extracted(&natives_dir, &lib, &java_arch);
|
let natives = match natives_mode {
|
||||||
|
NativesMode::Extract => {
|
||||||
|
force || !natives_extracted(&natives_dir, &lib, &java_arch)
|
||||||
|
}
|
||||||
|
NativesMode::LeaveInPlace => false,
|
||||||
|
};
|
||||||
|
|
||||||
if !artifact && !natives {
|
if !artifact && !natives {
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -269,6 +281,7 @@ pub struct VerifyReport {
|
||||||
/// Mismatched the manifest and were removed
|
/// Mismatched the manifest and were removed
|
||||||
pub corrupt: usize,
|
pub corrupt: usize,
|
||||||
pub missing: usize,
|
pub missing: usize,
|
||||||
|
pub natives_unextracted: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VerifyReport {
|
impl VerifyReport {
|
||||||
|
|
@ -293,6 +306,7 @@ fn arch_segment(java_arch: &str) -> &'static str {
|
||||||
pub struct NativesReport {
|
pub struct NativesReport {
|
||||||
pub repairable: Vec<String>,
|
pub repairable: Vec<String>,
|
||||||
pub unsupported: Vec<String>,
|
pub unsupported: Vec<String>,
|
||||||
|
pub unextracted: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NativesReport {
|
impl NativesReport {
|
||||||
|
|
@ -377,9 +391,10 @@ pub async fn check_natives(
|
||||||
// One scan for the whole sweep the directory is the same for every library
|
// One scan for the whole sweep the directory is the same for every library
|
||||||
let unpacked = any_native_unpacked(&natives_dir).await;
|
let unpacked = any_native_unpacked(&natives_dir).await;
|
||||||
|
|
||||||
let mut families: Vec<(String, Vec<JarVerdict>)> = Vec::new();
|
let mut families: Vec<(String, Vec<(JarVerdict, bool)>)> = Vec::new();
|
||||||
for lib in host_natives {
|
for lib in host_natives {
|
||||||
let verdict = if native_download(lib, java_arch).is_some() {
|
let extracted = native_download(lib, java_arch).is_some();
|
||||||
|
let verdict = if extracted {
|
||||||
if unpacked && natives_extracted(&natives_dir, lib, java_arch) {
|
if unpacked && natives_extracted(&natives_dir, lib, java_arch) {
|
||||||
JarVerdict::Usable
|
JarVerdict::Usable
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -394,17 +409,23 @@ pub async fn check_natives(
|
||||||
|
|
||||||
let family = natives_family(lib);
|
let family = natives_family(lib);
|
||||||
match families.iter_mut().find(|(name, _)| name == &family) {
|
match families.iter_mut().find(|(name, _)| name == &family) {
|
||||||
Some((_, verdicts)) => verdicts.push(verdict),
|
Some((_, verdicts)) => verdicts.push((verdict, extracted)),
|
||||||
None => families.push((family, vec![verdict])),
|
None => families.push((family, vec![(verdict, extracted)])),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut report = NativesReport::default();
|
let mut report = NativesReport::default();
|
||||||
for (family, verdicts) in families {
|
for (family, verdicts) in families {
|
||||||
if verdicts.iter().any(|v| matches!(v, JarVerdict::Usable)) {
|
if verdicts.iter().any(|(v, _)| matches!(v, JarVerdict::Usable)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if verdicts.iter().any(|v| matches!(v, JarVerdict::Unreadable)) {
|
if verdicts.iter().any(|(v, _)| matches!(v, JarVerdict::Unreadable)) {
|
||||||
|
if verdicts
|
||||||
|
.iter()
|
||||||
|
.any(|(v, extracted)| *extracted && matches!(v, JarVerdict::Unreadable))
|
||||||
|
{
|
||||||
|
report.unextracted.push(family.clone());
|
||||||
|
}
|
||||||
report.repairable.push(family);
|
report.repairable.push(family);
|
||||||
} else {
|
} else {
|
||||||
report.unsupported.push(family);
|
report.unsupported.push(family);
|
||||||
|
|
@ -554,6 +575,7 @@ pub async fn verify_game_files(
|
||||||
Err(err) => return Err(err),
|
Err(err) => return Err(err),
|
||||||
};
|
};
|
||||||
report.missing += natives.repairable.len();
|
report.missing += natives.repairable.len();
|
||||||
|
report.natives_unextracted = natives.unextracted.len();
|
||||||
report.checked += natives.repairable.len() + natives.unsupported.len();
|
report.checked += natives.repairable.len() + natives.unsupported.len();
|
||||||
|
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
|
|
@ -577,16 +599,26 @@ pub async fn download_minecraft(
|
||||||
java_arch: &str,
|
java_arch: &str,
|
||||||
minecraft_updated: bool,
|
minecraft_updated: bool,
|
||||||
force: bool,
|
force: bool,
|
||||||
|
natives_mode: NativesMode,
|
||||||
) -> McResult<()> {
|
) -> McResult<()> {
|
||||||
let started = std::time::Instant::now();
|
let started = std::time::Instant::now();
|
||||||
let asset_index = download_assets_index(ctx, progress, version, force).await?;
|
let asset_index = download_assets_index(ctx, progress, version, force).await?;
|
||||||
let plan = plan_downloads(version, asset_index, java_arch, minecraft_updated, force).await?;
|
let plan = plan_downloads(
|
||||||
|
version,
|
||||||
|
asset_index,
|
||||||
|
java_arch,
|
||||||
|
minecraft_updated,
|
||||||
|
force,
|
||||||
|
natives_mode,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
version = %version.id,
|
version = %version.id,
|
||||||
assets = plan.assets.len(),
|
assets = plan.assets.len(),
|
||||||
libraries = plan.libraries.len(),
|
libraries = plan.libraries.len(),
|
||||||
bytes = plan.total_bytes(),
|
bytes = plan.total_bytes(),
|
||||||
|
?natives_mode,
|
||||||
"planned minecraft download"
|
"planned minecraft download"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue