OneLauncher-Cracked/packages/oneclient_core/src/clusters/manager.rs
2026-07-14 18:06:23 +02:00

335 lines
9 KiB
Rust

use oneclient_db::dao::{cluster as cluster_dao, setting_profile as profile_dao};
use oneclient_db::models::{ClusterId, ClusterPatch, NewCluster};
use crate::packages::domain::ContentType;
use crate::settings::store::{
create_profile_from_global, resolve_cluster_profile, update_named_profile,
};
use crate::patch::Patch;
use crate::settings::{GameSettingsProfile, ProfileUpdate};
use crate::state::LauncherState;
use crate::LauncherResult;
use std::sync::Arc;
use super::cluster::Cluster;
use super::error::ClusterError;
use super::options::{ClusterUpdate, CreateClusterOptions};
use super::prepare;
use super::stage::ClusterStage;
pub struct ClusterManager;
impl ClusterManager {
pub fn sanitize_name(name: &str) -> String {
let mut name = name.to_string();
name.retain(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | ' ' | '.' | '(' | ')'));
name.trim().to_string()
}
#[tracing::instrument(level = "debug", skip(state))]
pub async fn get(state: &LauncherState, cluster_id: ClusterId) -> LauncherResult<Cluster> {
let row = cluster_dao::get_by_id(&state.services.db, cluster_id)
.await?
.ok_or(ClusterError::NotFound(cluster_id))?;
Ok(Cluster::try_from_row(row)?)
}
#[tracing::instrument(level = "debug", skip(state))]
pub async fn list(state: &LauncherState) -> LauncherResult<Vec<Cluster>> {
let rows = cluster_dao::list_all(&state.services.db).await?;
rows.into_iter()
.map(Cluster::try_from_row)
.collect::<Result<Vec<_>, _>>()
.map_err(Into::into)
}
#[tracing::instrument(skip(state))]
pub async fn create(
state: &LauncherState,
options: CreateClusterOptions,
) -> LauncherResult<Cluster> {
let name = Self::sanitize_name(&options.name);
if name.is_empty() {
return Err(ClusterError::EmptyName.into());
}
let folder_name = resolve_unique_folder_name(&name).await?;
let cluster_path = crate::paths::clusters_dir()?.join(&folder_name);
match create_inner(state, &options, &name, &folder_name, &cluster_path).await {
Ok(cluster) => {
tracing::info!(cluster_id = cluster.id, name = %cluster.name, "created cluster");
Ok(cluster)
}
Err(err) => {
tracing::warn!(name = %name, error = %err, "cluster creation failed, cleaning up directory");
let _ = polyio::remove_dir_all(&cluster_path).await;
Err(err)
}
}
}
#[tracing::instrument(level = "debug", skip(state))]
pub async fn update(
state: &LauncherState,
cluster_id: ClusterId,
update: ClusterUpdate,
) -> LauncherResult<Cluster> {
let _ = Self::get(state, cluster_id).await?;
if let Patch::Set(ref profile_name) = update.setting_profile_name {
ensure_profile_exists(&state.services.db, profile_name).await?;
}
let name = update.name.as_deref().map(Self::sanitize_name);
if name.as_deref().is_some_and(str::is_empty) {
return Err(ClusterError::EmptyName.into());
}
let patch = ClusterPatch {
name,
setting_profile_name: update.setting_profile_name.into_db_patch(),
mc_loader_version: update.mc_loader_version.into_db_patch(),
linked_modpack_hash: update.linked_modpack_hash.into_db_patch(),
};
let row = cluster_dao::update(&state.services.db, cluster_id, &patch).await?;
Ok(Cluster::try_from_row(row)?)
}
#[tracing::instrument(skip(state))]
pub async fn delete(
state: &LauncherState,
cluster_id: ClusterId,
remove_files: bool,
) -> LauncherResult<()> {
let cluster = Self::get(state, cluster_id).await?;
if !cluster_dao::delete_by_id(&state.services.db, cluster_id).await? {
return Err(ClusterError::NotFound(cluster_id).into());
}
if remove_files {
let path = cluster.dir()?;
if path.exists() {
polyio::remove_dir_all(&path).await?;
}
}
tracing::info!(cluster_id, remove_files, "deleted cluster");
Ok(())
}
#[tracing::instrument(level = "debug", skip(state))]
pub async fn set_stage(
state: &LauncherState,
cluster_id: ClusterId,
stage: ClusterStage,
) -> LauncherResult<Cluster> {
let row = cluster_dao::set_stage(&state.services.db, cluster_id, stage as i64).await?;
Ok(Cluster::try_from_row(row)?)
}
#[tracing::instrument(level = "debug", skip(state))]
pub async fn uses_dedicated_dir(
state: &LauncherState,
cluster_id: ClusterId,
) -> LauncherResult<bool> {
Ok(Self::get(state, cluster_id).await?.uses_dedicated_dir())
}
#[tracing::instrument(level = "debug", skip(state))]
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(())
}
#[tracing::instrument(level = "debug", skip(state))]
pub async fn add_playtime(
state: &LauncherState,
cluster_id: ClusterId,
duration: std::time::Duration,
) -> LauncherResult<Cluster> {
let seconds = i64::try_from(duration.as_secs()).unwrap_or(i64::MAX);
let row = cluster_dao::add_playtime(&state.services.db, cluster_id, seconds).await?;
Ok(Cluster::try_from_row(row)?)
}
#[tracing::instrument(level = "debug", skip(state, cluster), fields(cluster_id = cluster.id))]
pub async fn resolve_settings(
state: &LauncherState,
cluster: &Cluster,
) -> LauncherResult<GameSettingsProfile> {
let settings = state.settings.read().clone();
resolve_cluster_profile(
&state.services.db,
&settings,
cluster.setting_profile_name.as_deref(),
)
.await
}
#[tracing::instrument(level = "debug", skip(state, update))]
pub async fn update_profile(
state: &LauncherState,
cluster_id: ClusterId,
update: ProfileUpdate,
) -> LauncherResult<GameSettingsProfile> {
let cluster = Self::get(state, cluster_id).await?;
let profile_name = cluster
.setting_profile_name
.ok_or(ClusterError::NoProfile)?;
update_named_profile(&state.services.db, &profile_name, update).await
}
#[tracing::instrument(skip(state, shared_progress))]
pub async fn prepare(
state: &Arc<LauncherState>,
cluster_id: ClusterId,
force: bool,
search_for_java: bool,
auto_install_java: bool,
shared_progress: Option<&crate::notification::GroupedProgressSession>,
) -> LauncherResult<Cluster> {
let mut metadata = state.metadata.lock().await;
prepare::prepare_cluster(
state,
&mut metadata,
cluster_id,
force,
search_for_java,
auto_install_java,
shared_progress,
)
.await
}
#[tracing::instrument(level = "debug", skip(state))]
pub async fn create_and_assign_profile(
state: &LauncherState,
cluster_id: ClusterId,
profile_name: &str,
) -> LauncherResult<GameSettingsProfile> {
let settings = state.settings.read().clone();
let profile = create_profile_from_global(
&state.services.db,
&settings,
profile_name,
None,
None,
)
.await?;
Self::update(
state,
cluster_id,
ClusterUpdate::default().setting_profile(&profile.name),
)
.await?;
Ok(profile)
}
}
#[tracing::instrument(level = "debug", skip(state, options))]
async fn create_inner(
state: &LauncherState,
options: &CreateClusterOptions,
name: &str,
folder_name: &str,
cluster_path: &std::path::Path,
) -> LauncherResult<Cluster> {
polyio::create_dir_all(cluster_path).await?;
ensure_content_dirs(cluster_path).await?;
let settings = state.settings.read().clone();
let profile = create_profile_from_global(
&state.services.db,
&settings,
name,
options.mem_max,
None,
)
.await?;
let row = cluster_dao::insert(
&state.services.db,
&NewCluster {
name,
folder_name,
mc_version: &options.mc_version,
mc_loader: options.mc_loader as i64,
mc_loader_version: options.mc_loader_version.as_deref(),
setting_profile_name: Some(&profile.name),
stage: ClusterStage::NotReady as i64,
},
)
.await?;
Ok(Cluster::try_from_row(row)?)
}
#[tracing::instrument(level = "debug", skip(pool))]
async fn ensure_profile_exists(pool: &oneclient_db::DbPool, name: &str) -> LauncherResult<()> {
if profile_dao::get_by_name(pool, name).await?.is_none() {
return Err(ClusterError::ProfileNotFound(name.to_string()).into());
}
Ok(())
}
#[tracing::instrument(level = "debug")]
async fn resolve_unique_folder_name(name: &str) -> LauncherResult<String> {
let cluster_dir = crate::paths::clusters_dir()?;
let mut folder_name = name.to_string();
let mut path = cluster_dir.join(&folder_name);
if path.exists() {
let mut which = 1;
loop {
let candidate = format!("{folder_name} ({which})");
path = cluster_dir.join(&candidate);
if !path.exists() {
folder_name = candidate;
break;
}
which += 1;
}
}
Ok(folder_name)
}
#[tracing::instrument(level = "debug")]
async fn ensure_content_dirs(cluster_path: &std::path::Path) -> LauncherResult<()> {
for content_type in [
ContentType::Mod,
ContentType::ResourcePack,
ContentType::Shader,
ContentType::DataPack,
ContentType::World,
] {
polyio::create_dir_all(cluster_path.join(content_type.folder_name())).await?;
}
Ok(())
}