//! This module handles game rooms where players connect and exchange messages. //! It provides: //! - [`Room`]: A game session with host-to-client broadcast channels //! - [`AppState`]: Global state holding all active rooms and game configurations //! - [`reload_config`]: Hot-reloading of game settings from `GameConfig.json` use bytes::Bytes; use serde::{Deserialize, Serialize}; use deadpool_postgres::Pool; use std::collections::HashMap; use std::sync::Arc; use tokio::fs; use tokio::sync::{Mutex, RwLock}; use tokio::sync::{broadcast, mpsc}; /// The game entry we have for one game. #[derive(Serialize, Deserialize)] pub struct GameEntry { /// The name of the game. pub name: String, /// The maximum amount of players (0 = no limit) pub max_players: u16, } type EntryList = Vec; /// The description of the room, the players play in pub struct Room { /// The next id a client gets, this is consecutively counted. pub next_client_id: u16, // Needs Mutex /// The amount of players currently in the room. pub amount_of_players: u16, // Needs mutex. /// This is a status counter for rule variation in a game (like coop vs semi-coop). pub rule_variation: u16, /// The sender to send messages to the host. pub to_host_sender: mpsc::Sender, // Clone-able no Mutex! /// The broad case sender needed to subscribe for the clients. pub host_to_client_broadcaster: broadcast::Sender, // Clone-able -> no Mutex! /// Reconnect tokens keyed by player id. Used to authenticate reconnect attempts. pub player_tokens: HashMap, /// Whether the host WebSocket is currently active. False during the grace period /// after host disconnect — the grace-period task will clean up the room if the /// host does not reconnect in time. pub host_connected: bool, /// IDs of non-host players whose WebSocket is currently active. /// Used to replay NEW_CLIENT / CLIENT_DISCONNECTS when the host reconnects. pub connected_players: Vec, /// Row id in `game_records` for this session. None when no authenticated player created the room. pub game_record_id: Option, /// Maps in-game player_id → database user_id. None means the player is anonymous. pub user_ids: HashMap>, } /// The application state. pub struct AppState { /// The rooms we associate with several sessions. pub rooms: Mutex>, /// Contains a mapping from game name to the maximum amount of players allowed. pub configs: RwLock>, /// PostgreSQL connection pool — shared across all request handlers. pub db: Pool, } impl AppState { pub fn new(db: Pool) -> Self { Self { rooms: Mutex::new(HashMap::new()), configs: RwLock::new(HashMap::new()), db, } } } /// Reloads the configuration file, that lists the games with the maximum number of players per room. pub async fn reload_config(state: &Arc) -> Result<(), String> { let json_content = fs::read_to_string("GameConfig.json") .await .map_err(|e| format!("Failed to read file: {}", e))?; let raw_data: EntryList = serde_json::from_str(&json_content).map_err(|e| format!("Failed to parse JSON: {}", e))?; let new_configs: HashMap = raw_data .into_iter() .map(|entry| (entry.name, entry.max_players)) .collect(); { let mut configs = state.configs.write().await; *configs = new_configs; // Replace all. } Ok(()) }