Multiplayer Lobby Backend Implementation
This document focuses on the server-side implementation details of the lobby system for Rummage's multiplayer Commander games. It covers the networking architecture, server infrastructure, and data flow between clients and servers.
Table of Contents
- Architecture Overview
- Lobby Server Implementation
- Game Server Implementation
- Connection and Protocol
- Data Persistence
- Security Considerations
- Testing and Validation
- Deployment Considerations
Architecture Overview
The multiplayer system uses a dual-server architecture:
- Lobby Server: Manages lobbies, matchmaking, and initial connections
- Game Server: Handles the actual Commander gameplay after a match starts
┌────────────────┐
│ Lobby Server │
└───────┬────────┘
│
┌───────────────┼───────────────┐
│ │ │
┌───────┴─────┐ ┌───────┴─────┐ ┌───────┴─────┐
│ Game Server │ │ Game Server │ │ Game Server │
└─────────────┘ └─────────────┘ └─────────────┘
Components
- Lobby Manager: Tracks active lobbies and their states
- Session Manager: Handles player authentication and persistence
- Game Instance Factory: Creates and configures new game instances
- Message Broker: Routes communications between components
- Persistence Layer: Stores lobby and game data
Lobby Server Implementation
The Lobby Server is responsible for:
- Managing the list of available lobbies
- Handling player authentication
- Processing lobby creation, updates, and deletions
- Facilitating player chat and interactions in lobbies
- Initiating game sessions when a match starts
#![allow(unused)] fn main() { /// Main lobby server resource #[derive(Resource)] pub struct LobbyServer { /// Active lobbies indexed by ID pub lobbies: HashMap<String, LobbyData>, /// Connected clients pub clients: HashMap<ClientId, LobbyClientInfo>, /// Available game servers pub game_servers: Vec<GameServerInfo>, } /// Data structure for tracking a lobby #[derive(Clone, Debug)] pub struct LobbyData { /// Lobby information visible to players pub info: LobbyInfo, /// Detailed lobby settings pub settings: LobbySettings, /// Players in the lobby pub players: HashMap<String, LobbyPlayer>, /// Chat history pub chat_history: Vec<ChatMessage>, /// Creation timestamp pub created_at: f64, /// Last activity timestamp pub last_activity: f64, } /// Systems to handle lobby server operations pub fn handle_lobby_connections( mut server: ResMut<RepliconServer>, mut lobby_server: ResMut<LobbyServer>, mut connection_events: EventReader<ServerEvent>, time: Res<Time>, ) { for event in connection_events.read() { match event { ServerEvent::ClientConnected { client_id } => { // Add client to connected clients lobby_server.clients.insert(*client_id, LobbyClientInfo { client_id: *client_id, state: LobbyClientState::Connected, username: None, lobby_id: None, connected_at: time.elapsed_seconds(), last_activity: time.elapsed_seconds(), }); } ServerEvent::ClientDisconnected { client_id, reason } => { // Handle client disconnection if let Some(client_info) = lobby_server.clients.remove(client_id) { // If client was in a lobby, remove them if let Some(lobby_id) = client_info.lobby_id { if let Some(lobby) = lobby_server.lobbies.get_mut(&lobby_id) { // Remove player from lobby if let Some(username) = &client_info.username { lobby.players.remove(username); // Add system message about player leaving lobby.chat_history.push(ChatMessage { id: generate_uuid(), sender: "System".to_string(), is_system: true, content: format!("{} has left the lobby", username), timestamp: time.elapsed_seconds(), }); // Notify other players in the lobby notify_lobby_update(&mut server, &lobby_id, &lobby_server); // If lobby is empty or host left, handle lobby cleanup handle_potential_lobby_cleanup(&mut lobby_server, &lobby_id); } } } } } } } } /// Process incoming lobby actions pub fn process_lobby_requests( mut server: ResMut<RepliconServer>, mut lobby_server: ResMut<LobbyServer>, mut lobby_requests: EventReader<FromClient<LobbyRequest>>, time: Res<Time>, ) { for FromClient { client_id, event } in lobby_requests.read() { match event { LobbyRequest::ListLobbies(request) => { // Handle lobby list request let filtered_lobbies = filter_lobbies(&lobby_server.lobbies, &request.filters); let response = ServerListResponse { lobbies: filtered_lobbies, total_lobbies: lobby_server.lobbies.len(), }; // Send response to client server.send_message(*client_id, response); } LobbyRequest::CreateLobby(request) => { // Handle lobby creation request let lobby_id = generate_uuid(); let client_info = lobby_server.clients.get_mut(client_id).unwrap(); // Create new lobby let lobby = LobbyData { info: LobbyInfo { id: lobby_id.clone(), name: request.name.clone(), host_name: client_info.username.clone().unwrap_or_default(), player_count: 1, max_players: request.max_players, has_password: request.password.is_some(), format: request.format.clone(), restrictions: request.restrictions.clone(), description: request.description.clone(), }, settings: request.settings.clone(), players: HashMap::new(), chat_history: Vec::new(), created_at: time.elapsed_seconds(), last_activity: time.elapsed_seconds(), }; // Add host to the lobby let player = LobbyPlayer { name: client_info.username.clone().unwrap_or_default(), is_host: true, status: PlayerLobbyState::Joined, deck_info: None, }; let mut players = HashMap::new(); players.insert(player.name.clone(), player); lobby.players = players; // Add lobby to server lobby_server.lobbies.insert(lobby_id.clone(), lobby); // Update client info client_info.lobby_id = Some(lobby_id.clone()); client_info.state = LobbyClientState::InLobby; // Send response to client server.send_message(*client_id, CreateLobbyResponse { success: true, lobby_id: Some(lobby_id), error: None, }); } LobbyRequest::JoinLobby(request) => { // Handle join lobby request if let Some(lobby) = lobby_server.lobbies.get_mut(&request.lobby_id) { // Check if lobby is joinable if lobby.players.len() >= lobby.info.max_players { // Lobby is full server.send_message(*client_id, JoinLobbyResponse { success: false, failure_reason: Some("Lobby is full".to_string()), lobby_details: None, }); continue; } // Check password if required if lobby.info.has_password { // Verify password } // Add player to lobby let client_info = lobby_server.clients.get_mut(client_id).unwrap(); let player_name = client_info.username.clone().unwrap_or_default(); let player = LobbyPlayer { name: player_name.clone(), is_host: false, status: PlayerLobbyState::Joined, deck_info: None, }; lobby.players.insert(player_name, player); lobby.info.player_count = lobby.players.len(); lobby.last_activity = time.elapsed_seconds(); // Update client info client_info.lobby_id = Some(request.lobby_id.clone()); client_info.state = LobbyClientState::InLobby; // Add system message about player joining lobby.chat_history.push(ChatMessage { id: generate_uuid(), sender: "System".to_string(), is_system: true, content: format!("{} has joined the lobby", player_name), timestamp: time.elapsed_seconds(), }); // Notify all players in the lobby notify_lobby_update(&mut server, &request.lobby_id, &lobby_server); // Send response to joining client server.send_message(*client_id, JoinLobbyResponse { success: true, failure_reason: None, lobby_details: Some(convert_to_lobby_details(lobby)), }); } else { // Lobby not found server.send_message(*client_id, JoinLobbyResponse { success: false, failure_reason: Some("Lobby not found".to_string()), lobby_details: None, }); } } LobbyRequest::SendChat(request) => { // Handle chat message process_chat_message(client_id, request, &mut server, &mut lobby_server, &time); } LobbyRequest::UpdateStatus(request) => { // Handle player status update process_status_update(client_id, request, &mut server, &mut lobby_server, &time); } LobbyRequest::ViewDeck(request) => { // Handle deck view request process_deck_view_request(client_id, request, &mut server, &mut lobby_server); } LobbyRequest::LaunchGame(request) => { // Handle game launch request process_game_launch(client_id, request, &mut server, &mut lobby_server); } } } } }
Game Server Implementation
The Game Server handles the actual Commander gameplay after a match is initiated:
#![allow(unused)] fn main() { /// Main game server resource #[derive(Resource)] pub struct GameServer { /// Current game state pub game_state: Option<GameState>, /// Connected players pub players: HashMap<ClientId, Entity>, /// Whether this server is accepting new connections pub accepting_connections: bool, /// Game configuration pub config: GameConfig, } /// Start a new game instance pub fn start_game_instance( mut commands: Commands, mut server: ResMut<RepliconServer>, game_launch: Res<GameLaunchInfo>, ) { // Initialize game state let game_state = initialize_game_state(&game_launch); commands.insert_resource(game_state.clone()); // Configure server to accept connections from players commands.insert_resource(GameServer { game_state: Some(game_state), players: HashMap::new(), accepting_connections: true, config: game_launch.settings.clone(), }); // Start server on specified port server.start_listening(game_launch.port); // Set up turn tracking commands.insert_resource(TurnManager::new()); // Set up zones for cards commands.insert_resource(ZoneManager::new()); // Notify lobby server that this game instance is ready notify_lobby_server_game_ready(game_launch.lobby_id.clone(), game_launch.port); } /// Transfer players from lobby to game pub fn transfer_players_to_game( mut server: ResMut<RepliconServer>, game_server: Res<GameServer>, lobby_connection: Res<LobbyServerConnection>, game_launch: Res<GameLaunchInfo>, ) { // Notify all players in the lobby that the game is ready let connection_details = GameConnectionDetails { server_address: get_server_address(), game_id: game_launch.game_id.clone(), connection_token: generate_connection_tokens(&game_launch.players), }; let notification = GameLaunchNotification { connection_details, players: game_launch.players.clone(), settings: game_launch.settings.clone(), }; // Send notification to lobby server to distribute to players lobby_connection.send_message(LobbyServerMessage::GameReady(notification)); } }
Connection and Protocol
The networking protocol uses WebRTC for UDP-based communication with reliability layers:
#![allow(unused)] fn main() { /// Connection protocol constants pub mod protocol { /// Protocol version pub const PROTOCOL_VERSION: &str = "1.0.0"; /// Maximum message size in bytes pub const MAX_MESSAGE_SIZE: usize = 1024 * 64; /// Heartbeat interval in seconds pub const HEARTBEAT_INTERVAL: f32 = 1.0; /// Connection timeout in seconds pub const CONNECTION_TIMEOUT: f32 = 5.0; } /// Initialize the networking protocol pub fn init_networking(app: &mut App) { app .add_plugins(RepliconPlugin) .add_systems(Startup, setup_network_config) .add_systems(PreUpdate, ( process_connection_events, handle_protocol_messages, )) .add_systems(Update, ( send_heartbeats, check_timeouts, )); } /// Set up network configuration fn setup_network_config(mut commands: Commands) { commands.insert_resource(RepliconConfig { max_message_size: protocol::MAX_MESSAGE_SIZE, max_message_channel_count: 3, ..default() }); } /// Channel types for different message priorities #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] pub enum NetworkChannel { /// Reliable ordered channel for important messages Reliable, /// Unreliable channel for position updates Unreliable, /// Ordered but can drop messages for less critical state ReliableUnordered, } }
Network Optimization
To handle the complexity of Commander games, we implement several optimization techniques:
- Delta Compression: Only send changes to game state
- Interest Management: Only sync relevant parts of the game state to each client
- Batched Updates: Collect multiple updates and send them together
- Prioritized Synchronization: Critical game events take priority over visual updates
#![allow(unused)] fn main() { /// Delta encoder for game state changes pub struct DeltaEncoder { /// Previous game state hash previous_hash: u64, /// Component change tracking component_changes: HashMap<Entity, Vec<ComponentChange>>, } impl DeltaEncoder { /// Encode delta changes between states pub fn encode_delta(&mut self, current_state: &GameState) -> DeltaPacket { // Calculate changes since previous state // ... DeltaPacket { base_hash: self.previous_hash, entities_added: vec![], entities_removed: vec![], component_changes: self.component_changes.clone(), } } } }
Data Persistence
The lobby server persists certain data to provide continuity:
- User Accounts: Player profiles and authentication
- Lobby Templates: Saved lobby configurations
- Match History: Record of played games
- Deck Statistics: Anonymized deck performance data
#![allow(unused)] fn main() { /// Data persistence manager pub struct PersistenceManager { /// Database connection db_connection: DbConnection, /// Cache for frequently accessed data cache: LruCache<String, CachedData>, } impl PersistenceManager { /// Save lobby to database pub async fn save_lobby(&self, lobby: &LobbyData) -> Result<(), DbError> { // Serialize and store lobby data // ... Ok(()) } /// Load lobby from database pub async fn load_lobby(&self, lobby_id: &str) -> Result<Option<LobbyData>, DbError> { // Retrieve and deserialize lobby data // ... Ok(None) } /// Record match results pub async fn record_match_result(&self, results: &MatchResults) -> Result<(), DbError> { // Store match results for history and statistics // ... Ok(()) } } }
Security Considerations
The multiplayer system implements several security measures:
- Authentication: Verify player identities
- Authorization: Control access to lobbies and games
- Input Validation: Sanitize all player input
- Rate Limiting: Prevent spam and DoS attacks
- Encryption: Secure sensitive communications
#![allow(unused)] fn main() { /// Security module for the lobby system pub mod security { /// Validate player input pub fn validate_player_input(input: &str) -> bool { // Check for malicious content // ... true } /// Rate limit tracker pub struct RateLimiter { /// Action counts per client client_actions: HashMap<ClientId, Vec<(f64, ActionType)>>, } impl RateLimiter { /// Check if action is allowed pub fn check_rate_limit(&mut self, client_id: ClientId, action: ActionType, time: f64) -> bool { // Implement rate limiting logic // ... true } } /// Encrypt sensitive data pub fn encrypt_data(data: &[u8], key: &[u8]) -> Vec<u8> { // Encryption implementation // ... Vec::new() } } }
Testing and Validation
The lobby system includes comprehensive testing:
- Unit Tests: Verify individual component behavior
- Integration Tests: Test component interactions
- Load Tests: Ensure system can handle many concurrent lobbies
- Latency Simulation: Test under various network conditions
- Security Tests: Verify system resilience against attacks
#![allow(unused)] fn main() { /// Test suite for the lobby system #[cfg(test)] mod tests { use super::*; /// Test lobby creation and joining #[test] fn test_lobby_lifecycle() { // Set up test environment let mut app = App::new(); app.add_plugins(MinimalPlugins) .add_plugin(LobbyPlugin); // Create a lobby // ... // Join the lobby // ... // Verify state // ... } /// Test chat functionality #[test] fn test_chat_system() { // Set up test environment // ... // Send chat messages // ... // Verify delivery // ... } /// Test game launch process #[test] fn test_game_launch() { // Set up test environment // ... // Ready up players // ... // Launch game // ... // Verify transition // ... } } }
Deployment Considerations
The lobby system supports various deployment scenarios:
- Self-Hosted: Players can host their own servers
- Dedicated Servers: Centralized infrastructure
- Hybrid Model: Official servers plus community hosting
- Cloud Deployment: Scalable containers for peak times
#![allow(unused)] fn main() { /// Deployment configuration pub struct DeploymentConfig { /// Server discovery method pub discovery: ServerDiscoveryMethod, /// Server capacity pub capacity: ServerCapacity, /// Geographic region pub region: String, /// Auto-scaling settings pub scaling: Option<ScalingConfig>, } /// Server capacity configuration pub struct ServerCapacity { /// Maximum concurrent lobbies pub max_lobbies: usize, /// Maximum concurrent games pub max_games: usize, /// Maximum players per server pub max_players: usize, } /// Auto-scaling configuration pub struct ScalingConfig { /// Minimum number of instances pub min_instances: usize, /// Maximum number of instances pub max_instances: usize, /// Scale up threshold (% utilization) pub scale_up_threshold: f32, /// Scale down threshold (% utilization) pub scale_down_threshold: f32, } }
This document provides a comprehensive overview of the server-side implementation for the multiplayer lobby system, including the necessary architectural components and security considerations.