use bcrypt::{hash, verify, DEFAULT_COST}; use rusqlite::Connection; use crate::server::database; use crate::server::games::registry::GameRegistry; use crate::server::games::session::SessionManager; use crate::server::games::traits::GameError; #[derive(Debug, Default)] pub struct ConnectionState { user: Option, } impl ConnectionState { pub fn user(&self) -> Option<&str> { self.user.as_deref() } } pub struct ServerCore { conn: Connection, sessions: SessionManager, registry: GameRegistry, bcrypt_cost: u32, } impl ServerCore { pub fn new(conn: Connection) -> Self { Self::with_registry(conn, GameRegistry::with_defaults(), DEFAULT_COST) } pub fn with_registry( conn: Connection, registry: GameRegistry, bcrypt_cost: u32, ) -> Self { Self { conn, sessions: SessionManager::new(), registry, bcrypt_cost, } } pub fn open(path: &str) -> rusqlite::Result { let conn = Connection::open(path)?; database::init_db(&conn); Ok(Self::new(conn)) } pub fn handle_message(&mut self, state: &mut ConnectionState, msg: &str) -> String { let parts: Vec<&str> = msg.splitn(5, ' ').collect(); match parts[0] { "GAME_LIST" => { let mut games = self.registry.list_games(); games.sort(); let info: Vec = games .iter() .filter_map(|game_type| { self.registry .get_game(game_type) .map(|game| format!("{}:{}", game_type, game.get_name())) }) .collect(); if info.is_empty() { "ERROR No games available".to_string() } else { format!("GAME_LIST {}", info.join(",")) } } "GAME_INFO" => { if parts.len() != 2 { "ERROR Invalid GAME_INFO format. Usage: GAME_INFO " .to_string() } else { match self.registry.get_game(parts[1]) { Some(game) => format!( "GAME_INFO {} {} {}", parts[1], game.get_name(), game.get_description() ), None => format!("ERROR Unknown game type: {}", parts[1]), } } } "PLAY_START" => { if parts.len() != 3 { "ERROR Invalid PLAY_START format. Usage: PLAY_START " .to_string() } else { self.handle_play_start(state, parts[1], parts[2]) } } "REGISTER" => { if parts.len() != 3 { "ERROR Invalid REGISTER format".to_string() } else { self.handle_register(parts[1], parts[2]) } } "AUTH" => { if parts.len() != 3 { "ERROR Invalid AUTH format".to_string() } else { self.handle_auth(state, parts[1], parts[2]) } } "PING" => "PONG".to_string(), "INFO" => self.handle_info(state), _ => msg.to_string(), } } fn handle_register(&mut self, username: &str, credential: &str) -> String { if database::get_hashed_password(&self.conn, username).is_some() { return "ERROR User already exists".to_string(); } let hashed = match hash(credential, self.bcrypt_cost) { Ok(hashed) => hashed, Err(error) => { eprintln!("bcrypt failed for {username}: {error}"); return "ERROR Could not hash password".to_string(); } }; match database::add_user(&self.conn, username, &hashed) { Ok(()) => format!("REGISTERED {}", username), Err(_) => "ERROR User already exists".to_string(), } } fn handle_auth( &mut self, state: &mut ConnectionState, username: &str, credential: &str, ) -> String { let Some(stored) = database::get_hashed_password(&self.conn, username) else { return "ERROR User not found".to_string(); }; let valid = if stored.starts_with("$2") { verify(credential, &stored).unwrap_or(false) } else if stored == credential { // Legacy unsalted SHA-256 row: accept once, then upgrade in place. match hash(credential, self.bcrypt_cost) { Ok(upgraded) => { if let Err(error) = database::set_hashed_password(&self.conn, username, &upgraded) { eprintln!("failed to upgrade password for {username}: {error}"); } } Err(error) => eprintln!("bcrypt failed for {username}: {error}"), } true } else { false }; if valid { state.user = Some(username.to_string()); format!("OK {}", username) } else { "ERROR Invalid credentials".to_string() } } fn handle_info(&self, state: &ConnectionState) -> String { let Some(user) = state.user.as_deref() else { return "ERROR Not logged in".to_string(); }; match ( database::get_account_id(&self.conn, user), database::get_balance(&self.conn, user), ) { (Some(id), Some(balance)) => format!("INFO {} {} {}", user, id, balance), _ => "ERROR Error retrieving user info".to_string(), } } fn handle_play_start( &mut self, state: &ConnectionState, game_type: &str, bet_amount_str: &str, ) -> String { let Some(user) = state.user.as_deref() else { return "ERROR Not logged in".to_string(); }; let bet_amount: i64 = match bet_amount_str.parse() { Ok(amount) => amount, Err(_) => return "ERROR Invalid bet amount".to_string(), }; if bet_amount <= 0 { return "ERROR Invalid bet amount".to_string(); } let Some(game) = self.registry.get_game(game_type) else { return format!("ERROR Unknown game type: {}", game_type); }; let Some(balance) = database::get_balance(&self.conn, user) else { return "ERROR Could not retrieve balance".to_string(); }; if balance < 0 { return "ERROR Invalid balance".to_string(); } if let Err(error) = game.validate_bet(bet_amount as u64, balance as u64) { return game_error(error); } let game_state = game.prepare_state(user, bet_amount as u64); let new_state = match game.process_action(&game_state, "spin", &[]) { Ok(state) => state, Err(error) => return game_error(error), }; let result = match game.calculate_result(&new_state) { Ok(result) => result, Err(error) => return game_error(error), }; // The session is the audit guard for the deduction and is created // before the balance changes. The durable audit row is `plays`. let session_id = self.sessions .create_session(user, game_type.to_string(), new_state.clone()); let winnings = match i64::try_from(result.winnings) { Ok(winnings) => winnings, Err(_) => { let _ = self.sessions.remove_session(&session_id); return "ERROR Winnings out of range".to_string(); } }; let reels = result .result_data .get("reels") .cloned() .unwrap_or_default(); let win_type = result .result_data .get("win_type") .cloned() .unwrap_or_default(); let win_lines = result .result_data .get("win_lines") .cloned() .unwrap_or_else(|| "-".to_string()); let win_cells = result .result_data .get("win_cells") .cloned() .unwrap_or_else(|| "-".to_string()); match database::apply_spin( &mut self.conn, &session_id, user, game_type, bet_amount, winnings, &reels, &win_type, ) { Ok(new_balance) => { let _ = self.sessions.remove_session(&session_id); format!( "GAME_RESULT {session_id} {winnings} {bet_amount} {new_balance} {reels} {win_type} {win_lines} {win_cells}" ) } Err(error) => { let _ = self.sessions.remove_session(&session_id); eprintln!("Balance transaction failed for {user}: {error}"); "ERROR Failed to update balance".to_string() } } } pub fn disconnect(&mut self, state: &ConnectionState) { if let Some(user) = state.user.as_deref() { for session in self.sessions.get_user_sessions(user) { let _ = self.sessions.remove_session(&session.session_id); } } } pub fn balance(&self, username: &str) -> Option { database::get_balance(&self.conn, username) } pub fn play_count(&self, username: &str) -> i64 { database::count_plays(&self.conn, username) } pub fn session_count(&self) -> usize { self.sessions.len() } } fn game_error(error: GameError) -> String { match error { GameError::InsufficientBalance => "ERROR Insufficient balance".to_string(), GameError::InvalidBet => "ERROR Invalid bet amount".to_string(), GameError::InvalidAction(message) => format!("ERROR {message}"), GameError::GameOver => "ERROR Game over".to_string(), GameError::SessionNotFound => "ERROR Session not found".to_string(), GameError::InternalError(message) => format!("ERROR {message}"), } }