293 lines
8.6 KiB
Rust
293 lines
8.6 KiB
Rust
use eyre::bail;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::sync::{
|
|
LazyLock,
|
|
atomic::{AtomicBool, Ordering},
|
|
};
|
|
use tokio::sync::Mutex;
|
|
|
|
use super::{hongshi, terracotta};
|
|
|
|
static ACTIVE_PROVIDER: LazyLock<Mutex<Option<MultiplayerProvider>>> =
|
|
LazyLock::new(|| Mutex::new(None));
|
|
static MULTIPLAYER_OPERATION: LazyLock<Mutex<()>> =
|
|
LazyLock::new(|| Mutex::new(()));
|
|
static SHUTTING_DOWN: AtomicBool = AtomicBool::new(false);
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum MultiplayerProvider {
|
|
Terracotta,
|
|
Hongshi,
|
|
}
|
|
|
|
impl std::fmt::Display for MultiplayerProvider {
|
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::Terracotta => formatter.write_str("terracotta"),
|
|
Self::Hongshi => formatter.write_str("hongshi"),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize)]
|
|
pub struct MultiplayerProviderCapabilities {
|
|
pub provider: MultiplayerProvider,
|
|
pub supported: bool,
|
|
pub can_host: bool,
|
|
pub can_join: bool,
|
|
pub requires_local_port: bool,
|
|
pub unsupported_reason: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize)]
|
|
pub struct MultiplayerState {
|
|
pub active_provider: Option<MultiplayerProvider>,
|
|
pub providers: Vec<MultiplayerProviderCapabilities>,
|
|
pub terracotta: terracotta::TerracottaState,
|
|
pub hongshi: hongshi::HongshiState,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize)]
|
|
#[serde(tag = "provider", rename_all = "snake_case")]
|
|
pub enum MultiplayerHostRequest {
|
|
Terracotta {
|
|
player_name: String,
|
|
room_code: Option<String>,
|
|
},
|
|
Hongshi {
|
|
local_port: u16,
|
|
node_name: Option<String>,
|
|
instance_id: Option<String>,
|
|
},
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize)]
|
|
pub struct MultiplayerJoinRequest {
|
|
pub provider: MultiplayerProvider,
|
|
pub player_name: String,
|
|
pub room_code: String,
|
|
}
|
|
|
|
pub async fn claim_provider(provider: MultiplayerProvider) -> eyre::Result<()> {
|
|
if SHUTTING_DOWN.load(Ordering::Relaxed) {
|
|
bail!("the launcher is shutting down");
|
|
}
|
|
|
|
let mut active = ACTIVE_PROVIDER.lock().await;
|
|
if let Some(current) = *active
|
|
&& current != provider
|
|
{
|
|
bail!("{current} multiplayer is already running");
|
|
}
|
|
*active = Some(provider);
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn release_provider(provider: MultiplayerProvider) {
|
|
let mut active = ACTIVE_PROVIDER.lock().await;
|
|
if *active == Some(provider) {
|
|
*active = None;
|
|
}
|
|
}
|
|
|
|
pub async fn get_state() -> MultiplayerState {
|
|
let active_provider = *ACTIVE_PROVIDER.lock().await;
|
|
let terracotta = terracotta::get_state().await;
|
|
let hongshi = hongshi::get_state().await;
|
|
MultiplayerState {
|
|
active_provider,
|
|
providers: vec![
|
|
MultiplayerProviderCapabilities {
|
|
provider: MultiplayerProvider::Terracotta,
|
|
supported: terracotta::terracotta_platform_key()
|
|
!= "unsupported",
|
|
can_host: true,
|
|
can_join: true,
|
|
requires_local_port: false,
|
|
unsupported_reason: None,
|
|
},
|
|
MultiplayerProviderCapabilities {
|
|
provider: MultiplayerProvider::Hongshi,
|
|
supported: hongshi.supported,
|
|
can_host: true,
|
|
can_join: false,
|
|
requires_local_port: true,
|
|
unsupported_reason: (!hongshi.supported).then(|| {
|
|
"RedStone is not supported on this platform".to_string()
|
|
}),
|
|
},
|
|
],
|
|
terracotta,
|
|
hongshi,
|
|
}
|
|
}
|
|
|
|
async fn stop_provider(provider: MultiplayerProvider) -> eyre::Result<()> {
|
|
match provider {
|
|
MultiplayerProvider::Terracotta => terracotta::stop_terracotta().await,
|
|
MultiplayerProvider::Hongshi => hongshi::stop().await,
|
|
}
|
|
}
|
|
|
|
pub async fn switch_provider(
|
|
provider: MultiplayerProvider,
|
|
) -> eyre::Result<()> {
|
|
let _operation = MULTIPLAYER_OPERATION.lock().await;
|
|
let active = *ACTIVE_PROVIDER.lock().await;
|
|
if let Some(current) = active
|
|
&& current != provider
|
|
{
|
|
stop_provider(current).await?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn prepare_terracotta() -> eyre::Result<()> {
|
|
prepare_terracotta_with_options(None, true).await
|
|
}
|
|
|
|
pub async fn prepare_terracotta_with_options(
|
|
binary_path: Option<String>,
|
|
auto_download: bool,
|
|
) -> eyre::Result<()> {
|
|
let _operation = MULTIPLAYER_OPERATION.lock().await;
|
|
claim_provider(MultiplayerProvider::Terracotta).await?;
|
|
if terracotta::get_state().await.http_port.is_some() {
|
|
return Ok(());
|
|
}
|
|
if let Err(error) =
|
|
terracotta::start_terracotta(binary_path, auto_download).await
|
|
{
|
|
release_provider(MultiplayerProvider::Terracotta).await;
|
|
return Err(error);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn stop_terracotta_compat() -> eyre::Result<()> {
|
|
let _operation = MULTIPLAYER_OPERATION.lock().await;
|
|
terracotta::stop_terracotta().await
|
|
}
|
|
|
|
pub async fn reset_terracotta_compat() -> eyre::Result<()> {
|
|
let _operation = MULTIPLAYER_OPERATION.lock().await;
|
|
terracotta::reset_state().await
|
|
}
|
|
|
|
pub async fn host(request: MultiplayerHostRequest) -> eyre::Result<()> {
|
|
let _operation = MULTIPLAYER_OPERATION.lock().await;
|
|
match request {
|
|
MultiplayerHostRequest::Terracotta {
|
|
player_name,
|
|
room_code,
|
|
} => {
|
|
let already_running =
|
|
terracotta::get_state().await.http_port.is_some();
|
|
claim_provider(MultiplayerProvider::Terracotta).await?;
|
|
let result = async {
|
|
if !already_running {
|
|
terracotta::start_terracotta(None, true).await?;
|
|
}
|
|
terracotta::start_hosting(room_code, player_name).await
|
|
}
|
|
.await;
|
|
if result.is_err() && !already_running {
|
|
let _ = terracotta::stop_terracotta().await;
|
|
}
|
|
result
|
|
}
|
|
MultiplayerHostRequest::Hongshi {
|
|
local_port,
|
|
node_name,
|
|
instance_id,
|
|
} => hongshi::start(local_port, node_name, instance_id).await,
|
|
}
|
|
}
|
|
|
|
pub async fn join(request: MultiplayerJoinRequest) -> eyre::Result<()> {
|
|
let _operation = MULTIPLAYER_OPERATION.lock().await;
|
|
if request.provider != MultiplayerProvider::Terracotta {
|
|
bail!(
|
|
"RedStone guests connect directly with the public server address"
|
|
);
|
|
}
|
|
let already_running = terracotta::get_state().await.http_port.is_some();
|
|
claim_provider(MultiplayerProvider::Terracotta).await?;
|
|
let result = async {
|
|
if !already_running {
|
|
terracotta::start_terracotta(None, true).await?;
|
|
}
|
|
terracotta::start_joining(request.room_code, request.player_name).await
|
|
}
|
|
.await;
|
|
if result.is_err() && !already_running {
|
|
let _ = terracotta::stop_terracotta().await;
|
|
}
|
|
result
|
|
}
|
|
|
|
pub async fn stop() -> eyre::Result<()> {
|
|
let _operation = MULTIPLAYER_OPERATION.lock().await;
|
|
let provider = { *ACTIVE_PROVIDER.lock().await };
|
|
if let Some(provider) = provider {
|
|
stop_provider(provider).await?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn reset() -> eyre::Result<()> {
|
|
let _operation = MULTIPLAYER_OPERATION.lock().await;
|
|
let provider = { *ACTIVE_PROVIDER.lock().await };
|
|
match provider {
|
|
Some(MultiplayerProvider::Terracotta) => {
|
|
terracotta::reset_state().await
|
|
}
|
|
Some(MultiplayerProvider::Hongshi) => hongshi::stop().await,
|
|
None => Ok(()),
|
|
}
|
|
}
|
|
|
|
pub async fn observe_minecraft_log(
|
|
instance_id: &str,
|
|
instance_name: &str,
|
|
process_id: &str,
|
|
message: &str,
|
|
) {
|
|
hongshi::observe_minecraft_log(
|
|
instance_id,
|
|
instance_name,
|
|
process_id,
|
|
message,
|
|
)
|
|
.await;
|
|
}
|
|
|
|
pub async fn minecraft_process_finished(instance_id: &str) {
|
|
hongshi::minecraft_process_finished(instance_id).await;
|
|
}
|
|
|
|
pub async fn shutdown() -> eyre::Result<()> {
|
|
SHUTTING_DOWN.store(true, Ordering::Relaxed);
|
|
stop().await
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[tokio::test]
|
|
async fn active_provider_snapshot_does_not_hold_the_mutex() {
|
|
*ACTIVE_PROVIDER.lock().await = Some(MultiplayerProvider::Hongshi);
|
|
let provider = { *ACTIVE_PROVIDER.lock().await };
|
|
let mut active = tokio::time::timeout(
|
|
std::time::Duration::from_millis(100),
|
|
ACTIVE_PROVIDER.lock(),
|
|
)
|
|
.await
|
|
.expect("provider mutex should be released before stopping");
|
|
assert_eq!(provider, Some(MultiplayerProvider::Hongshi));
|
|
*active = None;
|
|
}
|
|
}
|