Shared .minecraft folder
This commit is contained in:
parent
700b45dd6b
commit
a645388a40
15 changed files with 234 additions and 13 deletions
|
|
@ -51,6 +51,10 @@ pub enum ClusterAction {
|
|||
content_type: ContentType,
|
||||
path: PathBuf,
|
||||
},
|
||||
SetDedicatedDir {
|
||||
cluster_id: ClusterId,
|
||||
dedicated: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl MutationCapability for ClusterMutation {
|
||||
|
|
@ -99,6 +103,13 @@ impl MutationCapability for ClusterMutation {
|
|||
.notifier
|
||||
.send_info("Imported", &format!("Added {}", row.file_name));
|
||||
}),
|
||||
ClusterAction::SetDedicatedDir {
|
||||
cluster_id,
|
||||
dedicated,
|
||||
} => oneclient_core::clusters::ClusterManager::set_dedicated_dir(
|
||||
&state, *cluster_id, *dedicated,
|
||||
)
|
||||
.await,
|
||||
};
|
||||
result.map_err(|e| e.to_string())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,10 +5,12 @@ use oneclient_core::java::JavaRuntime;
|
|||
use oneclient_core::packages::domain::GameLoader;
|
||||
use oneclient_core::settings::{GameSettingsProfile, ProfileUpdate, Resolution};
|
||||
|
||||
use crate::components::{Dropdown, Icon, IconType, ScrollArea, TextInput, toggle, validate_number};
|
||||
use crate::components::{
|
||||
Dropdown, Icon, IconType, ScrollArea, TextInput, toggle, toggle_controlled, validate_number,
|
||||
};
|
||||
use crate::hooks::{
|
||||
java_runtimes, loader_versions, try_game_profile, use_dispatch, use_game_profile,
|
||||
use_java_runtimes, use_loader_versions, use_settings_snapshot,
|
||||
ClusterAction, java_runtimes, loader_versions, try_game_profile, use_cluster_mutation,
|
||||
use_dispatch, use_game_profile, use_java_runtimes, use_loader_versions, use_settings_snapshot,
|
||||
};
|
||||
use crate::layout::cluster_content;
|
||||
use crate::theme::colors;
|
||||
|
|
@ -98,6 +100,14 @@ impl Component for ClusterSettings {
|
|||
}
|
||||
.into_element(),
|
||||
)
|
||||
.child(section_header("DIRECTORY"))
|
||||
.child(
|
||||
DedicatedDirRow {
|
||||
cluster_id,
|
||||
dedicated: cluster.uses_dedicated_dir(),
|
||||
}
|
||||
.into_element(),
|
||||
)
|
||||
.child(section_header("PROCESS"))
|
||||
.child(text_row(cluster_id, TextField::Pre, &profile, &global))
|
||||
.child(text_row(cluster_id, TextField::Wrapper, &profile, &global))
|
||||
|
|
@ -216,6 +226,35 @@ impl Component for ToggleRow {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
struct DedicatedDirRow {
|
||||
cluster_id: i64,
|
||||
dedicated: bool,
|
||||
}
|
||||
|
||||
impl Component for DedicatedDirRow {
|
||||
fn render(&self) -> impl IntoElement {
|
||||
let cluster_id = self.cluster_id;
|
||||
let dedicated = self.dedicated;
|
||||
let mutation = use_cluster_mutation();
|
||||
|
||||
let on_toggle: EventHandler<()> = (move |()| {
|
||||
mutation.mutate(ClusterAction::SetDedicatedDir {
|
||||
cluster_id,
|
||||
dedicated: !dedicated,
|
||||
});
|
||||
})
|
||||
.into();
|
||||
|
||||
settings_row(
|
||||
IconType::Folder,
|
||||
"Dedicated Directory",
|
||||
"Run this cluster in its own .minecraft instead of the shared one.",
|
||||
toggle_controlled(dedicated, on_toggle),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
struct MemoryRow {
|
||||
cluster_id: i64,
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ use crate::LauncherResult;
|
|||
use super::error::ClusterError;
|
||||
use super::stage::ClusterStage;
|
||||
|
||||
pub const DEDICATED_MARKER: &str = ".dedicated_directory";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Cluster {
|
||||
pub id: ClusterId,
|
||||
|
|
@ -76,6 +78,22 @@ impl Cluster {
|
|||
Ok(paths::clusters_dir()?.join(&self.folder_name))
|
||||
}
|
||||
|
||||
pub fn dedicated_marker(&self) -> LauncherResult<PathBuf> {
|
||||
Ok(self.dir()?.join(DEDICATED_MARKER))
|
||||
}
|
||||
|
||||
pub fn uses_dedicated_dir(&self) -> bool {
|
||||
self.dedicated_marker().map(|p| p.exists()).unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn game_dir(&self) -> LauncherResult<PathBuf> {
|
||||
if self.uses_dedicated_dir() {
|
||||
self.dir()
|
||||
} else {
|
||||
paths::shared_minecraft_dir()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_link_target(&self) -> ClusterLinkTarget<'_> {
|
||||
ClusterLinkTarget {
|
||||
id: self.id,
|
||||
|
|
|
|||
|
|
@ -122,6 +122,33 @@ impl ClusterManager {
|
|||
Ok(Cluster::try_from_row(row)?)
|
||||
}
|
||||
|
||||
pub async fn uses_dedicated_dir(
|
||||
state: &LauncherState,
|
||||
cluster_id: ClusterId,
|
||||
) -> LauncherResult<bool> {
|
||||
Ok(Self::get(state, cluster_id).await?.uses_dedicated_dir())
|
||||
}
|
||||
|
||||
pub async fn set_dedicated_dir(
|
||||
state: &LauncherState,
|
||||
cluster_id: ClusterId,
|
||||
dedicated: bool,
|
||||
) -> LauncherResult<()> {
|
||||
if state.games.is_active(cluster_id) {
|
||||
return Err(crate::game::GameError::AlreadyRunning(cluster_id).into());
|
||||
}
|
||||
|
||||
let cluster = Self::get(state, cluster_id).await?;
|
||||
let marker = cluster.dedicated_marker()?;
|
||||
if dedicated {
|
||||
polyio::create_dir_all(cluster.dir()?).await?;
|
||||
tokio::fs::write(&marker, b"").await.ok();
|
||||
} else if marker.exists() {
|
||||
tokio::fs::remove_file(&marker).await.ok();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn add_playtime(
|
||||
state: &LauncherState,
|
||||
cluster_id: ClusterId,
|
||||
|
|
|
|||
|
|
@ -218,7 +218,7 @@ async fn run_forge_processors(
|
|||
.join(&version_info.id)
|
||||
.join(format!("{}.jar", version_info.id));
|
||||
let libraries = paths::libraries_dir()?;
|
||||
let cluster_dir = cluster.dir()?;
|
||||
let cluster_dir = cluster.game_dir()?;
|
||||
|
||||
let Some(data) = &mut version_info.data else {
|
||||
return Ok(());
|
||||
|
|
|
|||
|
|
@ -30,6 +30,9 @@ pub enum GameError {
|
|||
#[error("cluster {0} is already running")]
|
||||
AlreadyRunning(i64),
|
||||
|
||||
#[error("another cluster ({0}) is already running in the same directory")]
|
||||
DirectoryInUse(i64),
|
||||
|
||||
#[error("failed to spawn the game process: {0}")]
|
||||
Spawn(String),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,6 +55,13 @@ pub async fn launch_cluster(
|
|||
|
||||
let existing = ClusterManager::get(state, cluster_id).await?;
|
||||
|
||||
let game_dir = existing.game_dir()?;
|
||||
if let Some(other) = state.games.dir_in_use_by(&game_dir, cluster_id) {
|
||||
return Err(GameError::DirectoryInUse(other).into());
|
||||
}
|
||||
|
||||
let dedicated = existing.uses_dedicated_dir();
|
||||
|
||||
let progress = GroupedProgressSession::start(
|
||||
&state.services.notifier,
|
||||
format!("Launching {}", existing.name),
|
||||
|
|
@ -188,9 +195,17 @@ pub async fn launch_cluster(
|
|||
|
||||
stage(LaunchStage::Launching);
|
||||
|
||||
let cwd = cluster.dir()?;
|
||||
let cwd = game_dir;
|
||||
tokio::fs::create_dir_all(&cwd).await.ok();
|
||||
|
||||
if !dedicated {
|
||||
if let Err(err) =
|
||||
crate::game::sync_shared_content(&state.services, &cluster, &cwd).await
|
||||
{
|
||||
tracing::warn!(cluster_id, error = %err, "failed to sync shared content");
|
||||
}
|
||||
}
|
||||
|
||||
let client_jar = paths::versions_dir()?
|
||||
.join(&version_name)
|
||||
.join(format!("{version_name}.jar"));
|
||||
|
|
@ -270,6 +285,7 @@ pub async fn launch_cluster(
|
|||
|
||||
stage(LaunchStage::Running);
|
||||
state.games.set_pid(cluster_id, pid);
|
||||
state.games.set_dir(cluster_id, cwd.clone());
|
||||
|
||||
let log_path = paths::logs_dir()?.join(format!("cluster-{cluster_id}.log"));
|
||||
if let Some(parent) = log_path.parent() {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ mod metadata;
|
|||
mod process;
|
||||
mod rules;
|
||||
mod session;
|
||||
mod shared_dir;
|
||||
|
||||
pub use analytics::{
|
||||
Analytics, DayPlaytime, Persona, PlaytimeStats, ServerStat, WEEKDAY_LABELS, aggregate_servers,
|
||||
|
|
@ -26,3 +27,4 @@ pub use metadata::{
|
|||
resolve_minecraft_version,
|
||||
};
|
||||
pub use rules::{validate_rule, validate_rules};
|
||||
pub use shared_dir::sync_shared_content;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Mutex;
|
||||
use std::time::Instant;
|
||||
|
||||
|
|
@ -18,6 +19,7 @@ pub struct GameProcess {
|
|||
pub struct GameProcessManager {
|
||||
inner: Mutex<HashMap<i64, GameProcess>>,
|
||||
kills: Mutex<HashMap<i64, oneshot::Sender<()>>>,
|
||||
dirs: Mutex<HashMap<i64, PathBuf>>,
|
||||
}
|
||||
|
||||
impl GameProcessManager {
|
||||
|
|
@ -60,6 +62,20 @@ impl GameProcessManager {
|
|||
pub fn remove(&self, cluster_id: i64) {
|
||||
self.inner.lock().unwrap().remove(&cluster_id);
|
||||
self.kills.lock().unwrap().remove(&cluster_id);
|
||||
self.dirs.lock().unwrap().remove(&cluster_id);
|
||||
}
|
||||
|
||||
pub fn set_dir(&self, cluster_id: i64, dir: PathBuf) {
|
||||
self.dirs.lock().unwrap().insert(cluster_id, dir);
|
||||
}
|
||||
|
||||
pub fn dir_in_use_by(&self, dir: &Path, exclude: i64) -> Option<i64> {
|
||||
self.dirs
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find(|(id, d)| **id != exclude && d.as_path() == dir)
|
||||
.map(|(id, _)| *id)
|
||||
}
|
||||
|
||||
pub fn is_running(&self, cluster_id: i64) -> bool {
|
||||
|
|
|
|||
83
packages/oneclient_core/src/game/shared_dir.rs
Normal file
83
packages/oneclient_core/src/game/shared_dir.rs
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
|
||||
use std::path::Path;
|
||||
|
||||
use oneclient_db::dao::artifact as artifact_dao;
|
||||
|
||||
use crate::clusters::Cluster;
|
||||
use crate::packages::domain::ContentType;
|
||||
use crate::packages::store::{artifact_absolute_path, link_or_copy};
|
||||
use crate::packages::PackageStore;
|
||||
use crate::state::LauncherServices;
|
||||
use crate::LauncherResult;
|
||||
|
||||
const SWAP_TYPES: [ContentType; 3] =
|
||||
[ContentType::Mod, ContentType::ResourcePack, ContentType::Shader];
|
||||
|
||||
const FABRIC_DEP_OVERRIDES: &str = "config/fabric_loader_dependencies.json";
|
||||
|
||||
pub async fn sync_shared_content(
|
||||
services: &LauncherServices,
|
||||
cluster: &Cluster,
|
||||
game_dir: &Path,
|
||||
) -> LauncherResult<()> {
|
||||
tokio::fs::create_dir_all(game_dir).await.ok();
|
||||
|
||||
for content_type in SWAP_TYPES {
|
||||
let dir = game_dir.join(content_type.folder_name());
|
||||
if dir.exists() {
|
||||
tokio::fs::remove_dir_all(&dir).await.ok();
|
||||
}
|
||||
tokio::fs::create_dir_all(&dir).await.ok();
|
||||
}
|
||||
|
||||
let linked = PackageStore::list_linked_artifacts(cluster.id, services).await?;
|
||||
for link in linked {
|
||||
if !link.enabled || !SWAP_TYPES.contains(&link.content_type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(artifact) =
|
||||
artifact_dao::get_artifact_by_hash(&services.db, &link.hash).await?
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let src = artifact_absolute_path(&artifact.path)?;
|
||||
if !src.exists() {
|
||||
tracing::warn!(hash = %link.hash, "cached artifact missing; skipping shared link");
|
||||
continue;
|
||||
}
|
||||
|
||||
let dest = game_dir
|
||||
.join(link.content_type.folder_name())
|
||||
.join(&link.cluster_file_name);
|
||||
|
||||
if let Err(err) = link_or_copy(&src, &dest).await {
|
||||
tracing::warn!(
|
||||
file = %link.cluster_file_name,
|
||||
error = %err,
|
||||
"failed to link content into shared directory"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
sync_fabric_dep_overrides(cluster, game_dir).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn sync_fabric_dep_overrides(cluster: &Cluster, game_dir: &Path) -> LauncherResult<()> {
|
||||
let src = cluster.dir()?.join(FABRIC_DEP_OVERRIDES);
|
||||
let dest = game_dir.join(FABRIC_DEP_OVERRIDES);
|
||||
|
||||
if src.exists() {
|
||||
if let Some(parent) = dest.parent() {
|
||||
tokio::fs::create_dir_all(parent).await.ok();
|
||||
}
|
||||
tokio::fs::copy(&src, &dest).await?;
|
||||
} else if dest.exists() {
|
||||
tokio::fs::remove_file(&dest).await.ok();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -67,7 +67,7 @@ fn collect_dir(
|
|||
|
||||
pub fn list_cluster_logs(cluster: &Cluster) -> LauncherResult<Vec<LogFileInfo>> {
|
||||
let mut out = Vec::new();
|
||||
let dir = cluster.dir()?;
|
||||
let dir = cluster.game_dir()?;
|
||||
|
||||
collect_dir(
|
||||
&dir.join("logs"),
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ pub async fn link_artifact_to_cluster(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn link_or_copy(src: &Path, dest: &Path) -> LauncherResult<()> {
|
||||
pub async fn link_or_copy(src: &Path, dest: &Path) -> LauncherResult<()> {
|
||||
if dest.exists() {
|
||||
tokio::fs::remove_file(dest).await?;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ mod link;
|
|||
mod paths;
|
||||
|
||||
pub use download::{download_external, download_version_file, ensure_artifact_file};
|
||||
pub use link::{link_artifact_to_cluster, unlink_cluster_file};
|
||||
pub use link::{link_artifact_to_cluster, link_or_copy, unlink_cluster_file};
|
||||
pub use paths::{artifact_absolute_path, cache_file_path, relative_cache_path};
|
||||
|
||||
use oneclient_db::dao::{artifact as artifact_dao, cluster as cluster_dao};
|
||||
|
|
|
|||
|
|
@ -60,6 +60,10 @@ pub fn clusters_dir() -> LauncherResult<PathBuf> {
|
|||
Ok(launcher_dir()?.join("clusters"))
|
||||
}
|
||||
|
||||
pub fn shared_minecraft_dir() -> LauncherResult<PathBuf> {
|
||||
Ok(launcher_dir()?.join("minecraft"))
|
||||
}
|
||||
|
||||
pub fn packages_cache_dir() -> LauncherResult<PathBuf> {
|
||||
Ok(launcher_dir()?.join("metadata").join("packages"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,16 +34,18 @@ fn is_image(name: &str) -> bool {
|
|||
|
||||
fn ensure_in_clusters(path: &Path) -> LauncherResult<PathBuf> {
|
||||
let canon = std::fs::canonicalize(path).map_err(ScreenshotsError::Io)?;
|
||||
if let Ok(root) = std::fs::canonicalize(paths::clusters_dir()?)
|
||||
&& canon.starts_with(&root)
|
||||
{
|
||||
return Ok(canon);
|
||||
for root in [paths::clusters_dir()?, paths::shared_minecraft_dir()?] {
|
||||
if let Ok(root) = std::fs::canonicalize(root)
|
||||
&& canon.starts_with(&root)
|
||||
{
|
||||
return Ok(canon);
|
||||
}
|
||||
}
|
||||
Err(ScreenshotsError::InvalidPath(path.display().to_string()).into())
|
||||
}
|
||||
|
||||
pub fn list_cluster_screenshots(cluster: &Cluster) -> LauncherResult<Vec<ScreenshotInfo>> {
|
||||
let dir = cluster.dir()?.join("screenshots");
|
||||
let dir = cluster.game_dir()?.join("screenshots");
|
||||
let mut out = Vec::new();
|
||||
|
||||
let Ok(entries) = std::fs::read_dir(&dir) else {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue