use rusqlite::Connection; use std::io::{stdout, BufRead, BufReader, Write}; use std::sync::{Arc, Mutex}; use std::time::Instant; use crate::connection::TlsStream; use crate::database; use crate::games::manager::GameManager; use crate::games::traits::GamePhase; use crossterm::{ cursor::{Hide, MoveToColumn, Show}, event::{read, Event, KeyCode, KeyModifiers}, execute, terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType}, }; use sha2::{Digest, Sha256}; pub struct ClientState { pub stream: Option, pub conn: Connection, pub addr: Option, pub default_port: u16, pub history: Vec, pub history_index: usize, pub logged_in_user: Option, pub game_manager: GameManager, cleanup_done: Arc>, in_game_mode: bool, } impl ClientState { pub fn new() -> Self { let conn = rusqlite::Connection::open( std::env::var("CLIENT_DB").unwrap_or_else(|_| "client.db".to_string()), ) .unwrap(); database::init_db(&conn); let default_ip = "127.0.0.1".to_string(); let default_port = 11525; let last_host = database::get_last_host(&conn); let default_host = format!("{}:{}", default_ip, default_port); let addr = last_host.or(Some(default_host)); Self { stream: None, conn, addr, default_port, history: vec![], history_index: 0, logged_in_user: None, game_manager: GameManager::new(), cleanup_done: Arc::new(Mutex::new(false)), in_game_mode: false, } } pub fn enter_game_mode(&mut self) { self.in_game_mode = true; } pub fn exit_game_mode(&mut self) { self.in_game_mode = false; self.game_manager.end_game(); } pub fn handle_game_key(&mut self, key_code: KeyCode) -> bool { if !self.game_manager.is_in_game() { return false; } match key_code { KeyCode::Esc | KeyCode::Char('q') => { self.exit_game_mode(); println!("\nGame exited."); return true; } KeyCode::Char(' ') => { let can_spin = self.game_manager.get_current_state() .map(|s| s.phase == GamePhase::WaitingToStart || s.phase == GamePhase::Finished) .unwrap_or(false); if can_spin { if let Some(mut stream) = self.stream.take() { match self.game_manager.start_game(&mut stream) { Ok(()) => { let response = self.read_server_response(&mut stream); if response.trim().starts_with("ERROR") { println!("\n{}", response.trim()); } else { self.play_spin_animation(&response); } self.game_manager.reset_phase_to_waiting(); } Err(e) => println!("\nError starting game: {:?}", e), } self.stream = Some(stream); } } return true; } KeyCode::Char('+') => { // Increase bet match self.game_manager.adjust_bet(10) { Ok(_new_bet) => { if let Some(state) = self.game_manager.get_current_state() { if let Some(game) = self.game_manager.get_current_game() { let lines = self.game_manager.get_current_render_lines().unwrap_or(8); redraw_game_lines(&game.render(state), lines); } } } Err(e) => println!("\nError: {:?}", e), } return true; } KeyCode::Char('-') => { // Decrease bet match self.game_manager.adjust_bet(-10) { Ok(_new_bet) => { if let Some(state) = self.game_manager.get_current_state() { if let Some(game) = self.game_manager.get_current_game() { let lines = self.game_manager.get_current_render_lines().unwrap_or(8); redraw_game_lines(&game.render(state), lines); } } } Err(e) => println!("\nError: {:?}", e), } return true; } _ => return true, // Lock into game mode: ignore unknown keys } } pub fn connect_to(&mut self, addr: &str) { self.addr = Some(addr.to_string()); match crate::connection::connect(addr, &self.conn) { Ok((s, authed_user)) => { self.stream = Some(s); self.logged_in_user = authed_user; database::save_host(&self.conn, addr); } Err(e) => println!("\nFailed to connect: {}", e), } } pub fn run(&mut self) { // Set up cleanup handlers let cleanup_done = self.cleanup_done.clone(); // Set up panic handler let panic_cleanup = cleanup_done.clone(); std::panic::set_hook(Box::new(move |_| { restore_terminal(&panic_cleanup); })); // Set up Ctrl+C handler let ctrlc_cleanup = cleanup_done.clone(); if let Err(e) = ctrlc::set_handler(move || { restore_terminal(&ctrlc_cleanup); std::process::exit(0); }) { eprintln!("Warning: Could not set Ctrl+C handler: {}", e); } enable_raw_mode().unwrap(); 'outer: loop { let mut current_input = String::new(); self.history_index = self.history.len(); redraw(""); loop { let event = read().unwrap(); match event { Event::Key(key) => { // Only handle key press events, not release events if key.kind == crossterm::event::KeyEventKind::Press { // Handle game mode keys first if self.game_manager.is_in_game() && self.handle_game_key(key.code) { // Game key was handled, continue loop if !self.game_manager.is_in_game() { // Game ended, redraw command prompt redraw(""); } continue; } // Handle normal command mode keys match key.code { KeyCode::Up => { if self.history_index > 0 { self.history_index -= 1; current_input = self.history[self.history_index].clone(); redraw(¤t_input); } } KeyCode::Down => { if self.history_index < self.history.len() { self.history_index += 1; current_input = if self.history_index == self.history.len() { "".to_string() } else { self.history[self.history_index].clone() }; redraw(¤t_input); } } KeyCode::Enter => { let line = current_input.trim().to_string(); if !line.is_empty() { self.history.push(line.clone()); } let line_trimmed = current_input.trim().to_string(); if !line_trimmed.is_empty() { self.history.push(line_trimmed.clone()); } // Process normal commands let parts: Vec<&str> = line_trimmed.splitn(3, ' ').collect(); match parts[0] { "play" => { if self.stream.is_none() { println!("\nNot connected. Use 'connect :' first."); continue 'outer; } if self.logged_in_user.is_none() { println!("\nNot logged in. Use 'login' first."); continue 'outer; } if parts.len() < 2 { let games = self.game_manager.get_available_games(); println!("\nAvailable games:"); for (gt, name) in games { println!(" - {} ({})", gt, name); } println!("\nUsage: play "); continue 'outer; } let game_type = parts[1]; if self.game_manager.is_in_game() { println!("\nAlready in a game. Use 'quit' to exit current game."); continue 'outer; } match self.game_manager.enter_game(game_type) { Ok(()) => { self.enter_game_mode(); if let Some(game) = self.game_manager.get_current_game() { if let Some(state) = self.game_manager.get_current_state() { redraw_game(&game.render(state)); } } } Err(e) => { println!("\nFailed to enter game: {:?}", e); } } } "games" => { let games = self.game_manager.get_available_games(); println!("\nAvailable games:"); for (gt, name) in games { println!(" - {} ({})", gt, name); } } "connect" => { if self.stream.is_some() { println!("\nAlready connected. Disconnect first."); continue 'outer; } let addr_str = if parts.len() < 2 { if let Some(h) = &self.addr { h.clone() } else { println!("\nNo host specified. Usage: connect :"); continue 'outer; } } else { let mut a = parts[1].to_string(); if a == "localhost" { a = "127.0.0.1".to_string(); } if !a.contains(':') { a = format!("{}:{}", a, self.default_port); } a }; self.connect_to(&addr_str); } "send" => { if self.stream.is_none() { println!("\nNot connected. Use 'connect :' first."); continue 'outer; } if parts.len() < 2 { println!("\nUsage: send "); continue 'outer; } let msg = parts[1]; let s = self.stream.as_mut().unwrap(); let writer_stream = s.try_clone().unwrap(); let mut writer = writer_stream; let response_reader_stream = s.try_clone().unwrap(); let mut response_reader = BufReader::new(response_reader_stream); if let Err(_) = writeln!(writer, "{}", msg) { self.stream = None; println!("\nConnection lost"); continue 'outer; } let mut response = String::new(); if let Err(_) = response_reader.read_line(&mut response) { self.stream = None; println!("\nConnection lost"); } else { println!("\nEcho: {}", response.trim()); } } "disconnect" => { self.stream = None; self.logged_in_user = None; println!("\nDisconnected"); } "ping" => { if self.stream.is_none() { println!("\nNot connected. Use 'connect :' first."); continue 'outer; } let s = self.stream.as_mut().unwrap(); let writer_stream = s.try_clone().unwrap(); let mut writer = writer_stream; let start = Instant::now(); let response_reader_stream = s.try_clone().unwrap(); let mut response_reader = BufReader::new(response_reader_stream); if let Err(_) = writeln!(writer, "PING") { self.stream = None; println!("\nConnection lost"); continue 'outer; } let mut response = String::new(); if let Err(_) = response_reader.read_line(&mut response) { self.stream = None; println!("\nConnection lost"); } else { if response.trim() == "PONG" { let elapsed = start.elapsed(); println!("\nPong ({:?})", elapsed); } else { self.stream = None; println!("\nConnection lost"); } } } "register" => { if self.stream.is_none() { println!("\nNot connected. Use 'connect :' first."); continue 'outer; } let username = read_line("\nUsername: "); if username.is_empty() { continue 'outer; } let password = read_password("Password: "); if password.is_empty() { continue 'outer; } let hashed = format!( "{:x}", Sha256::digest(password.as_bytes()) ); let msg = format!("REGISTER {} {}", username, hashed); let s = self.stream.as_mut().unwrap(); let writer_stream = s.try_clone().unwrap(); let mut writer = writer_stream; let response_reader_stream = s.try_clone().unwrap(); let mut response_reader = BufReader::new(response_reader_stream); if let Err(_) = writeln!(writer, "{}", msg) { self.stream = None; println!("\nConnection lost"); continue 'outer; } let mut response = String::new(); if let Err(_) = response_reader.read_line(&mut response) { self.stream = None; println!("\nConnection lost"); } else { let resp = response.trim(); if resp == format!("REGISTERED {}", username) { println!( "\nSuccessfully registered as {}", username ); } else if resp.starts_with("ERROR ") { println!( "\nRegistration failed: {}", &resp[6..] ); } else { println!("\n{}", resp); } } } "login" => { if self.stream.is_none() { println!("\nNot connected. Use 'connect :' first."); continue 'outer; } if self.logged_in_user.is_some() { println!( "\nAlready logged in as {}", self.logged_in_user.as_ref().unwrap() ); continue 'outer; } loop { let username = read_line("\nUsername: "); if username.is_empty() { break; } let password = read_password("Password: "); if password.is_empty() { break; } let hashed = format!( "{:x}", Sha256::digest(password.as_bytes()) ); let msg = format!("AUTH {} {}", username, hashed); let s = self.stream.as_mut().unwrap(); let writer_stream = s.try_clone().unwrap(); let mut writer = writer_stream; let response_reader_stream = s.try_clone().unwrap(); let mut response_reader = BufReader::new(response_reader_stream); if let Err(_) = writeln!(writer, "{}", msg) { self.stream = None; println!("\nConnection lost"); break; } let mut response = String::new(); if let Err(_) = response_reader.read_line(&mut response) { self.stream = None; println!("\nConnection lost"); break; } else { let resp = response.trim(); if resp.starts_with("OK") { // Save credentials if let Some(addr) = &self.addr { if let Err(e) = database::save_credentials( &self.conn, addr, &username, &hashed, ) { println!("Failed to save credentials: {}", e); } } self.logged_in_user = Some(username.clone()); println!("\nLogged in as {}", username); break; } else if resp.starts_with("ERROR ") { println!( "\nAuthentication failed: {}", &resp[6..] ); println!( "Try again or press Enter to cancel." ); } else { println!("\n{}", resp); } } } } "setcred" => { if self.addr.is_none() { println!("\nNo host connected. Use 'connect :' first."); continue 'outer; } let username = read_line("\nUsername: "); if username.is_empty() { continue 'outer; } let password = read_password("Password: "); if password.is_empty() { continue 'outer; } let hashed = format!( "{:x}", Sha256::digest(password.as_bytes()) ); if let Some(addr) = &self.addr { if let Err(e) = database::save_credentials( &self.conn, addr, &username, &hashed, ) { println!("Failed to save credentials: {}", e); } else { println!("Credentials saved for {}", addr); } } } "logout" => { self.logged_in_user = None; println!("\nLogged out"); } "info" => { if let Some(addr) = &self.addr { let fp = database::get_fingerprint(&self.conn, addr); println!("\nConnected to: {}", addr); match fp { Some(fp) => println!("Fingerprint: {}", fp), None => println!("Fingerprint: (not trusted)"), } } else { println!("\nNot connected to any host."); } if let Some(s) = &self.stream { let mut s = s.try_clone().unwrap(); let _ = writeln!(s, "INFO"); let mut reader = BufReader::new(s); let mut response = String::new(); let _ = reader.read_line(&mut response); if !response.is_empty() { println!("{}", response.trim()); } } } "help" => { if self.game_manager.is_in_game() { if let Some(game) = self.game_manager.get_current_game() { println!("\n{}", game.get_help()); } } else { help(); } } "quit" => { if self.game_manager.is_in_game() { self.game_manager.end_game(); println!("\nGame exited."); } else { restore_terminal(&self.cleanup_done); break 'outer; } } "forget-host" => { let target = if parts.len() > 1 { parts[1].to_string() } else if let Some(addr) = &self.addr { addr.clone() } else { println!( "\nUsage: forget-host : (or connect first)" ); continue 'outer; }; match database::forget_host(&self.conn, &target) { Ok(_) => println!( "\nCleared trust and stored credentials for {}.", target ), Err(e) => println!("\nFailed to forget host: {}", e), } } "hosts" => { let hosts = database::get_all_hosts(&self.conn); if hosts.is_empty() { println!("\nNo remembered hosts."); } else { println!("\nRemembered hosts:"); for (host, fp) in &hosts { match fp { Some(fp) => println!(" {} - {}", host, fp), None => println!(" {} - (no fingerprint)", host), } } } } _ => help(), } break; } KeyCode::Backspace => { if !current_input.is_empty() { current_input.pop(); execute!( stdout(), MoveToColumn(2), Clear(ClearType::UntilNewLine) ) .unwrap(); print!("{}", current_input); stdout().flush().unwrap(); } } KeyCode::Char(c) => { if key.modifiers.contains(KeyModifiers::CONTROL) && c == 'c' { restore_terminal(&self.cleanup_done); std::process::exit(0); } else if key.modifiers.contains(KeyModifiers::CONTROL) && c == 'd' { restore_terminal(&self.cleanup_done); std::process::exit(0); } else if c == '\t' { self.handle_tab_completion(&mut current_input); } else { current_input.push(c); print!("{}", c); stdout().flush().unwrap(); } } KeyCode::Tab => { self.handle_tab_completion(&mut current_input); } _ => {} } } } _ => {} } } } restore_terminal(&self.cleanup_done); } fn handle_tab_completion(&mut self, current_input: &mut String) { let prefix = current_input.trim(); let commands = get_commands(); let matches: Vec<_> = commands.iter().filter(|c| c.starts_with(prefix)).collect(); if matches.len() == 1 { *current_input = matches[0].clone(); redraw(current_input); } else if matches.len() > 1 { execute!(stdout(), Show).unwrap(); println!("\nPossible completions:"); for m in &matches { print!("{} ", m); } println!(); execute!(stdout(), Hide).unwrap(); redraw(current_input); } // else do nothing } /// Read the server response after PLAY_START. fn read_server_response(&self, stream: &mut TlsStream) -> String { let mut response = String::new(); let mut reader = std::io::BufReader::new(stream.try_clone().unwrap()); let _ = reader.read_line(&mut response); response } /// Run the spinning animation and display the final result. fn play_spin_animation(&mut self, response: &str) { use std::io::Write; use std::thread; use std::time::Duration; let symbols = ['C', 'L', 'O', 'G', 'D', 'S']; let frames = 20; let delay = Duration::from_millis(50); for _ in 0..frames { let reels: String = (0..3) .map(|_| symbols[rand::random::() % symbols.len()]) .collect(); execute!(stdout(), Clear(ClearType::CurrentLine), MoveToColumn(0)).unwrap(); print!( "[ {} ] [ {} ] [ {} ]", reels.chars().nth(0).unwrap(), reels.chars().nth(1).unwrap(), reels.chars().nth(2).unwrap() ); stdout().flush().unwrap(); thread::sleep(delay); } let response = response.trim(); if response.starts_with("GAME_RESULT") { let parts: Vec<&str> = response.split_whitespace().collect(); if parts.len() >= 7 { let reels = parts[5]; let win_type = parts[6]; execute!(stdout(), Clear(ClearType::CurrentLine), MoveToColumn(0)).unwrap(); print!( "[ {} ] [ {} ] [ {} ]", reels.chars().nth(0).unwrap_or('?'), reels.chars().nth(1).unwrap_or('?'), reels.chars().nth(2).unwrap_or('?') ); println!(); if win_type == "jackpot" { println!("\n🎉 JACKPOT! 🎉"); } else if win_type == "two" { println!("\n✨ Two of a kind! ✨"); } else { println!("\nNo win."); } println!("Winnings: {} PLN", parts[2]); println!("New Balance: {} PLN", parts[4]); stdout().flush().unwrap(); } } else if response.starts_with("ERROR") { println!("\n{}", response); } } } fn restore_terminal(cleanup_done: &Arc>) { let mut done = cleanup_done.lock().unwrap(); if !*done { let _ = disable_raw_mode(); let _ = execute!(stdout(), Show); *done = true; } } fn get_commands() -> Vec { vec![ "connect".to_string(), "send".to_string(), "disconnect".to_string(), "ping".to_string(), "login".to_string(), "register".to_string(), "setcred".to_string(), "logout".to_string(), "info".to_string(), "play".to_string(), "help".to_string(), "quit".to_string(), "forget-host".to_string(), "hosts".to_string(), ] } fn help() { println!( "\nAvailable commands:\r connect : - connect to the server\r send - send a message to the server\r disconnect - disconnect from the server\r ping - ping the server\r login - login to the server\r register - register a new user\r setcred - set credentials for a host\r logout - logout from the server\r info - show login info\r play [bet] - start a game (e.g., play slots 50)\r help - show this message\r forget-host [ip:port] - clear saved trust fingerprint and credentials\r quit - quit the program" ); } fn redraw_game(game_output: &str) { execute!( stdout(), Clear(ClearType::CurrentLine), Clear(ClearType::FromCursorDown), MoveToColumn(0) ) .unwrap(); println!("{}", game_output); stdout().flush().unwrap(); } fn redraw_game_lines(game_output: &str, lines: usize) { use crossterm::cursor::MoveUp; if lines > 0 { execute!(stdout(), MoveUp(lines as u16)).unwrap(); } execute!( stdout(), Clear(ClearType::CurrentLine), Clear(ClearType::FromCursorDown), MoveToColumn(0) ) .unwrap(); println!("{}", game_output); stdout().flush().unwrap(); } fn redraw(current_input: &str) { execute!(stdout(), Clear(ClearType::CurrentLine), MoveToColumn(0)).unwrap(); print!("> {}", current_input); stdout().flush().unwrap(); execute!(stdout(), MoveToColumn((2 + current_input.len()) as u16)).unwrap(); } fn read_line(prompt: &str) -> String { execute!(stdout(), Show).unwrap(); printsame!("{}", prompt); stdout().flush().unwrap(); let mut input = String::new(); let prompt_len = prompt.len(); loop { match read().unwrap() { Event::Key(key) => { if key.kind == crossterm::event::KeyEventKind::Press { match key.code { KeyCode::Enter => { println!(); break; } KeyCode::Backspace => { if !input.is_empty() { input.pop(); // Move cursor back and clear from current position to end of line execute!( stdout(), MoveToColumn((prompt_len + input.len() + 1) as u16), Clear(ClearType::UntilNewLine) ) .unwrap(); stdout().flush().unwrap(); } } KeyCode::Char(c) => { input.push(c); print!("{}", c); stdout().flush().unwrap(); } _ => {} } } } _ => {} } } input.trim().to_string() } fn read_password(prompt: &str) -> String { execute!(stdout(), Show).unwrap(); print!("{}", prompt); stdout().flush().unwrap(); let mut input = String::new(); let prompt_len = prompt.len(); loop { match read().unwrap() { Event::Key(key) => { if key.kind == crossterm::event::KeyEventKind::Press { match key.code { KeyCode::Enter => { println!(); break; } KeyCode::Backspace => { if !input.is_empty() { input.pop(); // Move cursor back and clear from current position to end of line execute!( stdout(), MoveToColumn((prompt_len + input.len() + 1) as u16), Clear(ClearType::UntilNewLine) ) .unwrap(); stdout().flush().unwrap(); } } KeyCode::Char(c) => { input.push(c); print!("*"); stdout().flush().unwrap(); } _ => {} } } } _ => {} } } input.trim().to_string() }