use crate::error::LobbyError;
use crate::factory::RoomFactory;
use crate::op_payloads::*;
use crate::room::RoomHandle;
use crate::types::RoomId;
use parking_lot::Mutex;
use plaza::agent::Agent;
use std::collections::HashMap;
use std::sync::Arc;
use tracing::{error, info, warn};
type RoomArc<F> =
Arc<dyn RoomHandle<<F as RoomFactory>::GameID, <F as RoomFactory>::CustomGameSettings>>;
pub type PasswordVerifier = Arc<dyn Fn(&str, &str) -> bool + Send + Sync>;
pub struct InMemoryLobbyManager<F>
where
F: RoomFactory,
{
rooms: Arc<Mutex<HashMap<RoomId, RoomArc<F>>>>,
room_factory: Arc<F>,
player_to_room_map: Arc<Mutex<HashMap<F::GameID, RoomId>>>,
password_verifier: PasswordVerifier,
}
impl<F> std::fmt::Debug for InMemoryLobbyManager<F>
where
F: RoomFactory,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("InMemoryLobbyManager")
.field("rooms", &self.rooms.lock().len())
.finish()
}
}
impl<F> InMemoryLobbyManager<F>
where
F: RoomFactory,
{
pub fn new(room_factory: Arc<F>) -> Self {
Self {
rooms: Arc::new(Mutex::new(HashMap::new())),
room_factory,
player_to_room_map: Arc::new(Mutex::new(HashMap::new())),
password_verifier: Arc::new(|attempt, stored| attempt == stored),
}
}
pub fn with_password_verifier(mut self, verifier: PasswordVerifier) -> Self {
self.password_verifier = verifier;
self
}
pub async fn handle_create_room_request(
&self,
_requester_id: &F::GameID,
settings: RoomSettings<F::CustomGameSettings>,
) -> Result<RoomMetadata<F::CustomGameSettings>, LobbyError> {
let room_id = RoomId::new_v4();
info!(
"Attempting to spawn room {} with settings: {:?}",
room_id, settings.game_mode
);
match self.room_factory.spawn_room(room_id, &settings).await {
Ok(room_handle) => {
let metadata = room_handle.metadata();
info!(
"Room {} spawned successfully. Endpoint: {}",
room_id,
room_handle.session_endpoint_info()
);
self.rooms.lock().insert(room_id, room_handle);
Ok(metadata)
}
Err(e) => {
error!("Failed to spawn room {}: {}", room_id, e);
Err(e)
}
}
}
pub async fn handle_join_room_request(
&self,
player_lobby_agent_id: &F::GameID,
player_game_agent: Agent<F::GameID>,
payload: &JoinRoomRequestPayload,
) -> Result<JoinRoomOutcomePayload, LobbyError> {
let room_id = payload.room_id;
let room_arc = {
let rooms = self.rooms.lock();
match rooms.get(&room_id) {
Some(handle) => Arc::clone(handle),
None => return Err(LobbyError::RoomNotFound(room_id)),
}
};
let metadata = room_arc.metadata();
if metadata.has_password {
let attempt = payload
.password_attempt
.as_deref()
.ok_or_else(|| LobbyError::JoinRoomFailed("Password required.".to_string()))?;
let stored = room_arc
.password_hash()
.ok_or_else(|| LobbyError::JoinRoomFailed("Room is private but has no password set.".to_string()))?;
if !(self.password_verifier)(attempt, &stored) {
warn!(room = %room_id, "Rejected join: incorrect password.");
return Err(LobbyError::JoinRoomFailed("Incorrect password.".to_string()));
}
}
if metadata.current_players >= metadata.max_players {
return Err(LobbyError::JoinRoomFailed("Room is full.".to_string()));
}
if let (Some(allowed), Some(measured)) = (metadata.max_one_way_ms, payload.measured_one_way_ms)
&& measured > allowed
{
warn!(room = %room_id, measured, allowed, "Rejected join: connection cannot meet this room's schedule.");
return Err(LobbyError::UnsuitableConnection { measured_ms: measured, allowed_ms: allowed });
}
match room_arc.accept_authorized_player(player_game_agent).await {
Ok(()) => {
self
.player_to_room_map
.lock()
.insert(player_lobby_agent_id.clone(), room_id);
info!(
"Player {:?} authorized for room {}. Endpoint: {}",
player_lobby_agent_id,
room_id,
room_arc.session_endpoint_info()
);
Ok(JoinRoomOutcomePayload {
room_id,
success: true,
reason_if_fail: None,
room_session_endpoint: Some(room_arc.session_endpoint_info()),
player_game_token: None,
})
}
Err(e) => {
error!(
"Room {} denied player {:?} join attempt: {}",
room_id, player_lobby_agent_id, e
);
Err(e)
}
}
}
pub fn room(&self, room_id: &RoomId) -> Option<RoomArc<F>> {
self.rooms.lock().get(room_id).map(Arc::clone)
}
pub fn rooms(&self) -> Vec<RoomArc<F>> {
self.rooms.lock().values().map(Arc::clone).collect()
}
pub fn list_rooms(&self, filters: Option<&RoomFilters>) -> Vec<RoomMetadata<F::CustomGameSettings>> {
self
.rooms
.lock()
.values()
.map(|room_handle| room_handle.metadata())
.filter(|metadata| match filters {
None => true,
Some(f) => {
let mode_ok = f.game_mode.as_ref().is_none_or(|mode| &metadata.game_mode == mode);
let space_ok = !f.exclude_full.unwrap_or(false) || metadata.current_players < metadata.max_players;
let private_ok = !f.exclude_private_if_no_password_known.unwrap_or(false) || !metadata.has_password;
let latency_ok = f
.playable_at_one_way_ms
.is_none_or(|measured| metadata.max_one_way_ms.is_none_or(|allowed| measured <= allowed));
mode_ok && space_ok && private_ok && latency_ok
}
})
.collect()
}
pub fn rooms_playable_at(&self, one_way_ms: u32) -> Vec<RoomMetadata<F::CustomGameSettings>> {
let rooms: Vec<_> = self.rooms.lock().values().map(|handle| handle.metadata()).collect();
crate::routing::playable_at(one_way_ms, rooms)
}
pub async fn reap_finished_rooms(&self) {
let finished: Vec<(RoomId, RoomArc<F>)> = {
let rooms = self.rooms.lock();
rooms
.iter()
.filter(|(_, handle)| handle.is_finished())
.map(|(id, handle)| (*id, Arc::clone(handle)))
.collect()
};
for (id, handle) in finished {
info!("Reaping finished room: {}", id);
handle.request_shutdown().await;
self.rooms.lock().remove(&id);
self.player_to_room_map.lock().retain(|_player, room| room != &id);
}
}
pub async fn handle_player_leaving_lobby(&self, player_id: &F::GameID) {
let room_id = match self.player_to_room_map.lock().remove(player_id) {
Some(room_id) => room_id,
None => {
info!("Player {:?} left lobby; was not in any active room.", player_id);
return;
}
};
info!(
"Player {:?} left lobby, was in room {}. Notifying room.",
player_id, room_id
);
let room_arc = self.rooms.lock().get(&room_id).map(Arc::clone);
match room_arc {
Some(handle) => handle.notify_player_departed(player_id).await,
None => warn!(
"Player {:?} left lobby, but their room {} was not found.",
player_id, room_id
),
}
}
}