use crate::error::LobbyError;
use crate::op_payloads::RoomMetadata;
use crate::RoomId;
use async_trait::async_trait;
use parking_lot::Mutex;
use plaza::agent::{Agent, AgentId};
use plaza::controller::{submit_system_ops, CommandSender, ControllerCommand};
use plaza::error::PlazaError;
use std::fmt::Debug;
use std::sync::Arc;
use tokio::task::JoinHandle;
#[async_trait]
pub trait RoomHandle<GameAgentID: AgentId, CustomRoomSettings>: Send + Sync + Debug
where
CustomRoomSettings: Clone + Debug + Send + Sync + 'static,
{
fn id(&self) -> RoomId;
fn metadata(&self) -> RoomMetadata<CustomRoomSettings>;
async fn accept_authorized_player(&self, player_for_game: Agent<GameAgentID>) -> Result<(), LobbyError>;
async fn reserve_seat(&self, _player: &GameAgentID) -> Result<(), LobbyError> {
Err(LobbyError::NotImplemented("this room takes no reservations".to_string()))
}
async fn withdraw_seat(&self, _player: &GameAgentID) -> Result<(), LobbyError> {
Err(LobbyError::NotImplemented("this room takes no reservations".to_string()))
}
async fn notify_player_departed(&self, player_id: &GameAgentID);
async fn request_shutdown(&self);
fn is_finished(&self) -> bool;
fn session_endpoint_info(&self) -> String;
fn password_hash(&self) -> Option<String>;
}
#[derive(Debug)]
pub struct InProcessRoomHandle<GameOp, GameID, GameStateType, CustomRoomSettings>
where
GameOp: Clone + Debug + Send + Sync + 'static,
GameID: AgentId,
GameStateType: Clone + Debug + Send + Sync + 'static,
CustomRoomSettings: Clone + Debug + Send + Sync + 'static,
{
pub room_id: RoomId,
pub command_tx: CommandSender<GameOp, GameID, GameStateType>,
task_join_handle: Arc<Mutex<Option<JoinHandle<Result<GameStateType, PlazaError<GameID>>>>>>,
pub metadata: Arc<Mutex<RoomMetadata<CustomRoomSettings>>>,
pub game_session_endpoint: String,
password_hash: Option<String>,
reservations: Option<ReservationOps<GameOp, GameID>>,
}
struct ReservationOps<GameOp, GameID> {
reserve: Box<dyn Fn(GameID) -> GameOp + Send + Sync>,
withdraw: Box<dyn Fn(GameID) -> GameOp + Send + Sync>,
}
impl<GameOp, GameID> Debug for ReservationOps<GameOp, GameID> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("ReservationOps")
}
}
impl<GameOp, GameID, GameStateType, CustomRoomSettings>
InProcessRoomHandle<GameOp, GameID, GameStateType, CustomRoomSettings>
where
GameOp: Clone + Debug + Send + Sync + 'static,
GameID: AgentId,
GameStateType: Clone + Debug + Send + Sync + 'static,
CustomRoomSettings: Clone + Debug + Send + Sync + 'static,
{
pub fn new(
room_id: RoomId,
initial_metadata: RoomMetadata<CustomRoomSettings>,
command_tx: CommandSender<GameOp, GameID, GameStateType>,
task_join_handle: JoinHandle<Result<GameStateType, PlazaError<GameID>>>,
game_session_endpoint: String,
password_hash: Option<String>,
) -> Self {
Self {
room_id,
command_tx,
task_join_handle: Arc::new(Mutex::new(Some(task_join_handle))),
metadata: Arc::new(Mutex::new(initial_metadata)),
game_session_endpoint,
password_hash,
reservations: None,
}
}
pub fn with_reservations(
mut self,
reserve: impl Fn(GameID) -> GameOp + Send + Sync + 'static,
withdraw: impl Fn(GameID) -> GameOp + Send + Sync + 'static,
) -> Self {
self.reservations = Some(ReservationOps {
reserve: Box::new(reserve),
withdraw: Box::new(withdraw),
});
self
}
pub fn update_player_count_in_metadata(&self, count: u32) {
let mut meta = self.metadata.lock();
meta.current_players = count;
}
async fn submit_system_op(&self, why: &str, op: GameOp) -> Result<(), LobbyError> {
submit_system_ops(&self.command_tx, why, vec![op])
.await
.map_err(|_| LobbyError::InternalOrchestrationError(format!("room {} controller has ended", self.room_id)))
}
}
#[async_trait]
impl<GameOp, GameID, GameStateType, CustomRoomSettings>
RoomHandle<GameID, CustomRoomSettings>
for InProcessRoomHandle<GameOp, GameID, GameStateType, CustomRoomSettings>
where
GameOp: Clone + Debug + Send + Sync + 'static,
GameID: AgentId,
GameStateType: Clone + Debug + Send + Sync + 'static,
CustomRoomSettings: Clone + Debug + Send + Sync + 'static,
{
fn id(&self) -> RoomId {
self.room_id
}
fn metadata(&self) -> RoomMetadata<CustomRoomSettings> {
self.metadata.lock().clone()
}
async fn accept_authorized_player(&self, player_for_game: Agent<GameID>) -> Result<(), LobbyError> {
{
let meta = self.metadata.lock();
if meta.current_players >= meta.max_players {
return Err(LobbyError::JoinRoomFailed("Room is full.".to_string()));
}
}
tracing::info!(
"Player {:?} authorized for room {}; client should connect to {}",
player_for_game.id(),
self.room_id,
self.game_session_endpoint
);
Ok(())
}
async fn reserve_seat(&self, player: &GameID) -> Result<(), LobbyError> {
let Some(ops) = &self.reservations else {
return Err(LobbyError::NotImplemented("this room takes no reservations".to_string()));
};
self.submit_system_op("lobby reservation", (ops.reserve)(player.clone())).await
}
async fn withdraw_seat(&self, player: &GameID) -> Result<(), LobbyError> {
let Some(ops) = &self.reservations else {
return Err(LobbyError::NotImplemented("this room takes no reservations".to_string()));
};
self.submit_system_op("lobby withdrawal", (ops.withdraw)(player.clone())).await
}
async fn notify_player_departed(&self, player_id: &GameID) {
let cmd = ControllerCommand::HandleAgentLeft {
agent_id: player_id.clone(),
};
if self.command_tx.send(cmd).await.is_err() {
tracing::warn!(
"Failed to send HandleAgentLeft to room {}: controller task may have ended.",
self.room_id
);
}
}
async fn request_shutdown(&self) {
if self.command_tx.send(ControllerCommand::Shutdown).await.is_err() {
tracing::warn!(
"Failed to send Shutdown command to room {}: controller task may have already ended.",
self.room_id
);
}
}
fn password_hash(&self) -> Option<String> {
self.password_hash.clone()
}
fn is_finished(&self) -> bool {
match &*self.task_join_handle.lock() {
Some(handle) => handle.is_finished(),
None => true,
}
}
fn session_endpoint_info(&self) -> String {
self.game_session_endpoint.clone()
}
}