use bcrypt::{hash, verify, DEFAULT_COST}; use rusqlite::Connection; use std::collections::HashMap; use std::sync::Arc; use crate::blackjack::BlackjackState; use crate::crash::CrashState; use crate::server::database; use crate::server::games::blackjack::{BlackjackGame, BlackjackHand}; use crate::server::games::crash::{Clock, CrashGame, CrashRound, RealClock}; use crate::server::games::registry::GameRegistry; use crate::server::games::session::SessionManager; use crate::server::games::traits::{GameError, GamePhase, GameState}; #[derive(Debug, Default)] pub struct ConnectionState { user: Option, } /// Settle reservations left behind by a previous process. /// /// Call this **once at server startup, before accepting connections**. A /// per-connection sweep would be wrong: it would settle another player's live /// hand. Only one process may own the database. pub fn recover_orphaned_reservations(db_path: &str) -> rusqlite::Result { let mut conn = Connection::open(db_path)?; database::init_db(&conn); database::abandon_pending(&mut conn) } impl ConnectionState { pub fn user(&self) -> Option<&str> { self.user.as_deref() } } pub struct ServerCore { conn: Connection, sessions: SessionManager, registry: GameRegistry, bcrypt_cost: u32, blackjack: BlackjackGame, crash: CrashGame, clock: Arc, } 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, blackjack: BlackjackGame::new(), crash: CrashGame::new(), clock: Arc::new(RealClock::new()), } } /// Test/utility hook: use a fixed shoe for the next hand(s). pub fn with_blackjack_deck(mut self, deck: Vec) -> Self { self.blackjack = BlackjackGame::with_deck(deck); self } /// Test/utility hook: queue the next crash-point draws. pub fn with_crash_uniforms(mut self, uniforms: impl IntoIterator) -> Self { self.crash = CrashGame::with_uniforms(uniforms); self } pub fn with_crash_uniform(self, uniform: f64) -> Self { self.with_crash_uniforms([uniform]) } /// Test/utility hook: replace the monotonic clock driving crash rounds. pub fn with_clock(mut self, clock: Arc) -> Self { self.clock = clock; self } 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 && parts[1] == "blackjack" { self.handle_blackjack_start(state, parts[2]) } else if parts.len() == 3 && parts[1] == "crash" { self.handle_crash_start(state, parts[2]) } else if parts.len() != 3 && parts.len() != 4 { "ERROR Invalid PLAY_START format. Usage: PLAY_START [payload]" .to_string() } else { self.handle_play_start(state, parts[1], parts[2], parts.get(3).copied()) } } "CRASH_CASHOUT" => { if parts.len() != 2 { "ERROR Invalid CRASH_CASHOUT format. Usage: CRASH_CASHOUT ".to_string() } else { self.handle_crash_end(state, parts[1], false) } } "CRASH_FORFEIT" => { if parts.len() != 2 { "ERROR Invalid CRASH_FORFEIT format. Usage: CRASH_FORFEIT ".to_string() } else { self.handle_crash_end(state, parts[1], true) } } "BLACKJACK_ACTION" => { if parts.len() != 3 { "ERROR Invalid BLACKJACK_ACTION format. Usage: BLACKJACK_ACTION " .to_string() } else { self.handle_blackjack_action(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]) } } "HISTORY" => { if parts.len() > 2 { "ERROR Invalid HISTORY format. Usage: HISTORY [count]".to_string() } else { match parts.get(1) { Some(raw) => match raw.parse::() { Ok(count) if count > 0 => { self.handle_history(state, count.min(crate::history::MAX_HISTORY)) } _ => "ERROR Invalid HISTORY count".to_string(), }, None => self.handle_history(state, crate::history::DEFAULT_HISTORY), } } } "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_history(&mut self, state: &ConnectionState, limit: u32) -> String { let Some(user) = state.user.as_deref() else { return "ERROR Not logged in".to_string(); }; match database::recent_plays(&self.conn, user, limit) { Ok(records) => match serde_json::to_string(&records) { Ok(json) => format!("HISTORY {json}"), Err(error) => { eprintln!("History serialization failed for {user}: {error}"); "ERROR Could not read history".to_string() } }, Err(error) => { eprintln!("History query failed for {user}: {error}"); "ERROR Could not read history".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_blackjack_start(&mut self, state: &ConnectionState, bet_str: &str) -> String { let Some(user) = state.user.as_deref() else { return "ERROR Not logged in".to_string(); }; if self .sessions .get_user_sessions(user) .iter() .any(|session| session.game_type == "blackjack") { return "ERROR Blackjack hand already in progress".to_string(); } let bet: i64 = match bet_str.parse() { Ok(bet) => bet, Err(_) => return "ERROR Invalid bet amount".to_string(), }; if bet <= 0 { return "ERROR Invalid bet amount".to_string(); } let Some(balance) = database::get_balance(&self.conn, user) else { return "ERROR Could not retrieve balance".to_string(); }; if balance < bet { return "ERROR Insufficient balance".to_string(); } let hand = self.blackjack.deal(bet as u64); let session_id = self.create_blackjack_session(user, &hand); let balance = match database::reserve_play( &mut self.conn, &session_id, user, "blackjack", bet, &hand.to_json(), ) { Ok(balance) => balance, Err(_) => { let _ = self.sessions.remove_session(&session_id); return "ERROR Insufficient balance".to_string(); } }; self.blackjack_response(user, &session_id, hand, balance) } fn create_blackjack_session(&mut self, user: &str, hand: &BlackjackHand) -> String { let mut data = HashMap::new(); data.insert("hand".to_string(), hand.to_json()); let game_state = GameState { session_id: String::new(), game_type: "blackjack".to_string(), bet_amount: hand.bet, phase: GamePhase::Playing, data, }; self.sessions .create_session(user, "blackjack".to_string(), game_state) } fn handle_blackjack_action( &mut self, state: &ConnectionState, session_id: &str, action: &str, ) -> String { let Some(user) = state.user.as_deref() else { return "ERROR Not logged in".to_string(); }; let Ok(session) = self.sessions.get_session(session_id) else { return "ERROR Session not found".to_string(); }; if session.username != user || session.game_type != "blackjack" { return "ERROR Invalid session".to_string(); } let Some(raw) = session.state.data.get("hand") else { return "ERROR Missing hand".to_string(); }; let Ok(mut hand) = BlackjackHand::from_json(raw) else { return "ERROR Invalid hand".to_string(); }; if hand.is_done() { return "ERROR Hand is over".to_string(); } let balance = database::get_balance(&self.conn, user).unwrap_or(0); match action { "hit" => { if let Err(error) = hand.hit() { return format!("ERROR {error}"); } } "stand" => hand.stand(), "double" => { if hand.player.len() != 2 { return "ERROR Double is only allowed on the first two cards".to_string(); } if database::extend_reservation(&mut self.conn, session_id, hand.base_bet as i64) .is_err() { return "ERROR Insufficient balance".to_string(); } if let Err(error) = hand.double() { let _ = database::refund_reservation( &mut self.conn, session_id, hand.base_bet as i64, ); return format!("ERROR {error}"); } } "forfeit" | "cancel" => { hand.forfeit(); let state = hand.state(session_id, balance); let details = serde_json::to_string(&state).unwrap_or_else(|_| "{}".to_string()); let balance = match database::settle_play( &mut self.conn, session_id, user, "blackjack", hand.bet as i64, 0, &details, ) { Ok(balance) => balance, Err(error) => { eprintln!("Blackjack forfeit settlement failed for {user}: {error}"); let _ = self.sessions.remove_session(session_id); return "ERROR Failed to update balance".to_string(); } }; let _ = self.sessions.remove_session(session_id); let state = hand.state(session_id, balance); return blackjack_wire(&state); } _ => return "ERROR Unknown action".to_string(), } let balance = database::get_balance(&self.conn, user).unwrap_or(balance); self.blackjack_response(user, session_id, hand, balance) } fn blackjack_response( &mut self, user: &str, session_id: &str, hand: BlackjackHand, balance: i64, ) -> String { if hand.is_done() { let state = hand.state(session_id, balance); let details = serde_json::to_string(&state).unwrap_or_else(|_| "{}".to_string()); let balance = match database::settle_play( &mut self.conn, session_id, user, "blackjack", hand.bet as i64, hand.winnings as i64, &details, ) { Ok(balance) => balance, Err(error) => { eprintln!("Blackjack settlement failed for {user}: {error}"); let _ = self.sessions.remove_session(session_id); return "ERROR Failed to update balance".to_string(); } }; let _ = self.sessions.remove_session(session_id); let state = hand.state(session_id, balance); blackjack_wire(&state) } else { if let Ok(mut session) = self.sessions.get_session(session_id) { session.state.bet_amount = hand.bet; session .state .data .insert("hand".to_string(), hand.to_json()); let _ = self.sessions.update_session(session_id, session.state); } let state = hand.state(session_id, balance); blackjack_wire(&state) } } fn handle_crash_start(&mut self, state: &ConnectionState, bet_str: &str) -> String { let Some(user) = state.user.as_deref() else { return "ERROR Not logged in".to_string(); }; if self .sessions .get_user_sessions(user) .iter() .any(|session| session.game_type == "crash") { return "ERROR Crash round already in progress".to_string(); } let bet: i64 = match bet_str.parse() { Ok(bet) => bet, Err(_) => return "ERROR Invalid bet amount".to_string(), }; if bet <= 0 { return "ERROR Invalid bet amount".to_string(); } let Some(balance) = database::get_balance(&self.conn, user) else { return "ERROR Could not retrieve balance".to_string(); }; if balance < bet { return "ERROR Insufficient balance".to_string(); } // The session is the audit guard for the deduction, so it exists before // the balance is touched. let crash_point = self.crash.next_crash_point(); let round = CrashRound::new(bet as u64, crash_point, self.clock.now()); let mut data = HashMap::new(); data.insert("round".to_string(), round.to_json()); data.insert("balance".to_string(), balance.to_string()); let game_state = GameState { session_id: String::new(), game_type: "crash".to_string(), bet_amount: round.bet, phase: GamePhase::Playing, data, }; let session_id = self .sessions .create_session(user, "crash".to_string(), game_state); let balance = match database::reserve_play( &mut self.conn, &session_id, user, "crash", bet, &round.to_json(), ) { Ok(balance) => balance, Err(_) => { let _ = self.sessions.remove_session(&session_id); return "ERROR Insufficient balance".to_string(); } }; if let Ok(mut session) = self.sessions.get_session(&session_id) { session .state .data .insert("balance".to_string(), balance.to_string()); let _ = self.sessions.update_session(&session_id, session.state); } let state = round.state(&session_id, balance); crash_wire("CRASH_START", &state) } /// Cash out or forfeit. The multiplier comes from the server clock at the /// moment the request is handled; the client never supplies one. fn handle_crash_end( &mut self, state: &ConnectionState, session_id: &str, forfeit: bool, ) -> String { let Some(user) = state.user.as_deref() else { return "ERROR Not logged in".to_string(); }; let Ok(session) = self.sessions.get_session(session_id) else { return "ERROR Session not found".to_string(); }; if session.username != user || session.game_type != "crash" { return "ERROR Invalid session".to_string(); } let Some(raw) = session.state.data.get("round") else { return "ERROR Missing round".to_string(); }; let Ok(mut round) = CrashRound::from_json(raw) else { return "ERROR Invalid round".to_string(); }; if !round.is_running() { return "ERROR Round already ended".to_string(); } let elapsed = (self.clock.now() - round.started_at).max(0.0); let multiplier = crate::crash::multiplier_at_secs(elapsed); let payout = if forfeit { round.multiplier = multiplier; round.forfeit(); 0 } else if multiplier < round.crash_point { round.cash_out(multiplier) } else { round.crash(); 0 }; match self.settle_crash(user, session_id, &round, payout) { Ok(balance) => { let state = round.state(session_id, balance); crash_wire("CRASH_STATE", &state) } Err(message) => { eprintln!("Crash settlement failed for {user}: {message}"); "ERROR Failed to update balance".to_string() } } } /// Server-driven end of a round. `handler` calls this on every socket /// timeout and forwards whatever lines come back. pub fn poll(&mut self, state: &ConnectionState) -> Vec { let Some(user) = state.user.as_deref() else { return Vec::new(); }; let mut out = Vec::new(); for session in self.sessions.get_user_sessions(user) { if session.game_type != "crash" { continue; } let Some(raw) = session.state.data.get("round") else { continue; }; let Ok(mut round) = CrashRound::from_json(raw) else { continue; }; if !round.is_running() { continue; } let elapsed = (self.clock.now() - round.started_at).max(0.0); if crate::crash::multiplier_at_secs(elapsed) < round.crash_point { continue; } round.crash(); match self.settle_crash(user, &session.session_id, &round, 0) { Ok(balance) => { let state = round.state(&session.session_id, balance); out.push(crash_wire("CRASH_BUSTED", &state)); } Err(message) => { eprintln!("Crash push settlement failed for {user}: {message}"); } } } out } fn settle_crash( &mut self, user: &str, session_id: &str, round: &CrashRound, payout: u64, ) -> Result { let details = round.to_json(); let result = database::settle_play( &mut self.conn, session_id, user, "crash", round.bet as i64, payout as i64, &details, ); let _ = self.sessions.remove_session(session_id); result.map_err(|error| error.to_string()) } fn handle_play_start( &mut self, state: &ConnectionState, game_type: &str, bet_amount_str: &str, payload: Option<&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 params: Vec<&str> = payload.into_iter().collect(); let new_state = match game.process_action(&game_state, "spin", ¶ms) { 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 details = serde_json::to_string(&result.result_data).unwrap_or_else(|_| "{}".to_string()); match database::apply_play( &mut self.conn, &session_id, user, game_type, bet_amount, winnings, &details, ) { Ok(new_balance) => { let _ = self.sessions.remove_session(&session_id); if game_type == "roulette" { let number = result .result_data .get("number") .cloned() .unwrap_or_default(); let color = result.result_data.get("color").cloned().unwrap_or_default(); let bets = result .result_data .get("bets") .cloned() .unwrap_or_else(|| "{}".to_string()); format!( "ROULETTE_RESULT {session_id} {winnings} {bet_amount} {new_balance} {number} {color} {bets}" ) } else { 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()); 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) { if session.game_type == "blackjack" { if let Some(raw) = session.state.data.get("hand") { if let Ok(mut hand) = BlackjackHand::from_json(raw) { if !hand.is_done() { hand.forfeit(); let balance = database::get_balance(&self.conn, user).unwrap_or(0); let final_state = hand.state(&session.session_id, balance); let details = serde_json::to_string(&final_state) .unwrap_or_else(|_| "{}".to_string()); if let Err(error) = database::settle_play( &mut self.conn, &session.session_id, user, "blackjack", hand.bet as i64, 0, &details, ) { eprintln!( "Blackjack disconnect forfeit failed for {user}: {error}" ); } } } } } if session.game_type == "crash" { if let Some(raw) = session.state.data.get("round") { if let Ok(mut round) = CrashRound::from_json(raw) { if round.is_running() { round.forfeit(); let details = round.to_json(); if let Err(error) = database::settle_play( &mut self.conn, &session.session_id, user, "crash", round.bet as i64, 0, &details, ) { eprintln!( "Crash disconnect forfeit failed for {user}: {error}" ); } } } } } 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}"), } } fn blackjack_wire(state: &BlackjackState) -> String { format!( "BLACKJACK_STATE {}", serde_json::to_string(state).unwrap_or_else(|_| "{}".to_string()) ) } fn crash_wire(prefix: &str, state: &CrashState) -> String { format!( "{prefix} {}", serde_json::to_string(state).unwrap_or_else(|_| "{}".to_string()) ) }