summaryrefslogtreecommitdiffstats
path: root/src/bin
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-08-24 14:48:12 +0200
committersalmonik <salmonik@tuta.io>2026-08-24 14:48:12 +0200
commit5d6b5049c2a34a3f8abe725d4a871ba84a0dd714 (patch)
tree57cb03bf59b3358a96742f455d38c21f4f645396 /src/bin
parentstable and working, both platforms (diff)
downloadzagan-bet-5d6b5049c2a34a3f8abe725d4a871ba84a0dd714.tar.gz
zagan-bet-5d6b5049c2a34a3f8abe725d4a871ba84a0dd714.zip
TLS transport and games framework on client and server
Diffstat (limited to '')
-rw-r--r--src/bin/client/connection.rs136
-rw-r--r--src/bin/client/database.rs26
-rw-r--r--src/bin/client/games/manager.rs127
-rw-r--r--src/bin/client/games/mod.rs3
-rw-r--r--src/bin/client/games/slots.rs62
-rw-r--r--src/bin/client/games/traits.rs30
-rw-r--r--src/bin/client/macros.rs2
-rw-r--r--src/bin/client/main.rs14
-rw-r--r--src/bin/client/mod.rs3
-rw-r--r--src/bin/client/repl.rs990
-rw-r--r--src/bin/client/tls.rs150
-rw-r--r--src/bin/server/args.rs11
-rw-r--r--src/bin/server/database.rs26
-rw-r--r--src/bin/server/games/mod.rs4
-rw-r--r--src/bin/server/games/registry.rs55
-rw-r--r--src/bin/server/games/session.rs77
-rw-r--r--src/bin/server/games/slots.rs179
-rw-r--r--src/bin/server/games/traits.rs67
-rw-r--r--src/bin/server/handler.rs177
-rw-r--r--src/bin/server/main.rs34
-rw-r--r--src/bin/server/tls.rs96
-rw-r--r--src/bin/windows-test/main.rs61
22 files changed, 1900 insertions, 430 deletions
diff --git a/src/bin/client/connection.rs b/src/bin/client/connection.rs
index b07bf01..c5d506d 100644
--- a/src/bin/client/connection.rs
+++ b/src/bin/client/connection.rs
@@ -1,46 +1,76 @@
1use std::io::{BufRead, BufReader, Write}; 1use crate::database;
2use native_tls::TlsConnector;
3use rusqlite::Connection;
4use std::io::{Read, Write};
2use std::net::TcpStream; 5use std::net::TcpStream;
3use std::sync::mpsc; 6use std::sync::mpsc;
7use std::sync::Arc;
8use std::sync::Mutex;
4use std::thread; 9use std::thread;
5use std::time::{Duration, Instant}; 10use std::time::{Duration, Instant};
6use rusqlite::Connection;
7use crate::database;
8 11
9pub fn connect(addr: &str, conn: &Connection) -> Result<(TcpStream, Option<String>), Box<dyn std::error::Error>> { 12static UNSAFE_MODE: Mutex<bool> = Mutex::new(false);
13
14pub fn set_unsafe_mode(unsafe_mode: bool) {
15 let mut mode = UNSAFE_MODE.lock().unwrap();
16 *mode = unsafe_mode;
17}
18
19pub fn connect(
20 addr: &str,
21 conn: &Connection,
22) -> Result<(TlsStream, Option<String>), Box<dyn std::error::Error>> {
10 let (tx, rx) = mpsc::channel(); 23 let (tx, rx) = mpsc::channel();
11 let addr_str = addr.to_string(); 24 let addr_str = addr.to_string();
12 thread::spawn(move || { 25 let server_name = addr_str
13 match TcpStream::connect(&addr_str) { 26 .split(':')
14 Ok(s) => tx.send(Ok(s)).unwrap(), 27 .next()
15 Err(e) => tx.send(Err(e)).unwrap(), 28 .unwrap_or("127.0.0.1")
16 } 29 .to_string();
30 thread::spawn(move || match TcpStream::connect(&addr_str) {
31 Ok(s) => tx.send(Ok(s)).unwrap(),
32 Err(e) => tx.send(Err(e)).unwrap(),
17 }); 33 });
18 match rx.recv_timeout(Duration::from_secs(3)) { 34 match rx.recv_timeout(Duration::from_secs(3)) {
19 Ok(Ok(s)) => { 35 Ok(Ok(s)) => {
20 let mut ping_writer = s.try_clone().unwrap(); 36 let unsafe_mode = *UNSAFE_MODE.lock().unwrap();
21 let mut ping_reader = BufReader::new(s.try_clone().unwrap()); 37
38 let connector = if unsafe_mode {
39 TlsConnector::builder()
40 .danger_accept_invalid_certs(true)
41 .danger_accept_invalid_hostnames(true)
42 .build()?
43 } else {
44 TlsConnector::builder().build()?
45 };
46
47 let tls_stream = connector.connect(&server_name, s)?;
48 let mut tls = TlsStream::new(tls_stream);
49
22 let start = Instant::now(); 50 let start = Instant::now();
23 writeln!(ping_writer, "PING").unwrap(); 51 tls.write_line("PING")?;
24 let mut response = String::new(); 52 let mut response = String::new();
25 ping_reader.read_line(&mut response).unwrap(); 53 tls.read_line(&mut response)?;
26 let elapsed = start.elapsed(); 54 let elapsed = start.elapsed();
27 println!("\n\rConnected to {} with ping delay: {:?}", addr, elapsed); 55 println!(
28 56 "\n\rConnected to {} with ping delay: {:?} (TLS)",
29 // Check for credentials and auto-auth 57 addr, elapsed
58 );
59
30 if let Some((username, password)) = database::get_credentials(conn, addr) { 60 if let Some((username, password)) = database::get_credentials(conn, addr) {
31 let auth_msg = format!("AUTH {} {}", username, password); 61 let auth_msg = format!("AUTH {} {}", username, password);
32 writeln!(ping_writer, "{}", auth_msg).unwrap(); 62 tls.write_line(&auth_msg)?;
33 let mut auth_response = String::new(); 63 let mut auth_response = String::new();
34 ping_reader.read_line(&mut auth_response).unwrap(); 64 tls.read_line(&mut auth_response)?;
35 if auth_response.trim().starts_with("OK") { 65 if auth_response.trim().starts_with("OK") {
36 println!("Authenticated as {}", username); 66 println!("Authenticated as {}", username);
37 Ok((s, Some(username))) 67 Ok((tls, Some(username)))
38 } else { 68 } else {
39 println!("Authentication failed: {}", auth_response.trim()); 69 println!("Authentication failed: {}", auth_response.trim());
40 Ok((s, None)) 70 Ok((tls, None))
41 } 71 }
42 } else { 72 } else {
43 Ok((s, None)) 73 Ok((tls, None))
44 } 74 }
45 } 75 }
46 Ok(Err(e)) => Err(Box::new(e)), 76 Ok(Err(e)) => Err(Box::new(e)),
@@ -48,3 +78,67 @@ pub fn connect(addr: &str, conn: &Connection) -> Result<(TcpStream, Option<Strin
48 Err(_) => Err("Connection attempt failed".into()), 78 Err(_) => Err("Connection attempt failed".into()),
49 } 79 }
50} 80}
81
82pub struct TlsStream {
83 stream: Arc<Mutex<native_tls::TlsStream<TcpStream>>>,
84}
85
86unsafe impl Send for TlsStream {}
87unsafe impl Sync for TlsStream {}
88
89impl TlsStream {
90 pub fn new(stream: native_tls::TlsStream<TcpStream>) -> Self {
91 Self {
92 stream: Arc::new(Mutex::new(stream)),
93 }
94 }
95
96 pub fn read_line(&mut self, buf: &mut String) -> std::io::Result<usize> {
97 let mut byte_buf = [0u8; 1024];
98 let mut stream = self.stream.lock().unwrap();
99 loop {
100 let n = stream.read(&mut byte_buf)?;
101 if n == 0 {
102 return Ok(0);
103 }
104 let chunk = std::str::from_utf8(&byte_buf[..n]).unwrap_or("");
105 buf.push_str(chunk);
106 if buf.contains('\n') {
107 return Ok(n);
108 }
109 }
110 }
111
112 pub fn write_line(&mut self, msg: &str) -> std::io::Result<()> {
113 let mut stream = self.stream.lock().unwrap();
114 stream.write_all(msg.as_bytes())?;
115 stream.write_all(b"\n")?;
116 stream.flush()?;
117 Ok(())
118 }
119
120 pub fn try_clone(&self) -> std::io::Result<TlsStream> {
121 Ok(TlsStream {
122 stream: Arc::clone(&self.stream),
123 })
124 }
125}
126
127impl Read for TlsStream {
128 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
129 let mut stream = self.stream.lock().unwrap();
130 stream.read(buf)
131 }
132}
133
134impl Write for TlsStream {
135 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
136 let mut stream = self.stream.lock().unwrap();
137 stream.write(buf)
138 }
139
140 fn flush(&mut self) -> std::io::Result<()> {
141 let mut stream = self.stream.lock().unwrap();
142 stream.flush()
143 }
144}
diff --git a/src/bin/client/database.rs b/src/bin/client/database.rs
index da069a7..a274f31 100644
--- a/src/bin/client/database.rs
+++ b/src/bin/client/database.rs
@@ -8,7 +8,8 @@ pub fn init_db() -> Connection {
8 last_used DATETIME DEFAULT CURRENT_TIMESTAMP 8 last_used DATETIME DEFAULT CURRENT_TIMESTAMP
9 )", 9 )",
10 [], 10 [],
11 ).unwrap(); 11 )
12 .unwrap();
12 conn.execute( 13 conn.execute(
13 "CREATE TABLE IF NOT EXISTS credentials ( 14 "CREATE TABLE IF NOT EXISTS credentials (
14 host TEXT PRIMARY KEY, 15 host TEXT PRIMARY KEY,
@@ -16,12 +17,15 @@ pub fn init_db() -> Connection {
16 password TEXT NOT NULL 17 password TEXT NOT NULL
17 )", 18 )",
18 [], 19 [],
19 ).unwrap(); 20 )
21 .unwrap();
20 conn 22 conn
21} 23}
22 24
23pub fn get_last_host(conn: &Connection) -> Option<String> { 25pub fn get_last_host(conn: &Connection) -> Option<String> {
24 let mut stmt = conn.prepare("SELECT host FROM hosts ORDER BY last_used DESC LIMIT 1").unwrap(); 26 let mut stmt = conn
27 .prepare("SELECT host FROM hosts ORDER BY last_used DESC LIMIT 1")
28 .unwrap();
25 let mut rows = stmt.query([]).unwrap(); 29 let mut rows = stmt.query([]).unwrap();
26 if let Ok(Some(row)) = rows.next() { 30 if let Ok(Some(row)) = rows.next() {
27 row.get(0).ok() 31 row.get(0).ok()
@@ -34,10 +38,16 @@ pub fn save_host(conn: &Connection, host: &str) {
34 conn.execute( 38 conn.execute(
35 "INSERT OR REPLACE INTO hosts (host, last_used) VALUES (?1, CURRENT_TIMESTAMP)", 39 "INSERT OR REPLACE INTO hosts (host, last_used) VALUES (?1, CURRENT_TIMESTAMP)",
36 [host], 40 [host],
37 ).unwrap(); 41 )
42 .unwrap();
38} 43}
39 44
40pub fn save_credentials(conn: &Connection, host: &str, username: &str, password: &str) -> Result<(), Box<dyn std::error::Error>> { 45pub fn save_credentials(
46 conn: &Connection,
47 host: &str,
48 username: &str,
49 password: &str,
50) -> Result<(), Box<dyn std::error::Error>> {
41 conn.execute( 51 conn.execute(
42 "INSERT OR REPLACE INTO credentials (host, username, password) VALUES (?1, ?2, ?3)", 52 "INSERT OR REPLACE INTO credentials (host, username, password) VALUES (?1, ?2, ?3)",
43 (host, username, password), 53 (host, username, password),
@@ -46,7 +56,9 @@ pub fn save_credentials(conn: &Connection, host: &str, username: &str, password:
46} 56}
47 57
48pub fn get_credentials(conn: &Connection, host: &str) -> Option<(String, String)> { 58pub fn get_credentials(conn: &Connection, host: &str) -> Option<(String, String)> {
49 let mut stmt = conn.prepare("SELECT username, password FROM credentials WHERE host = ?1").unwrap(); 59 let mut stmt = conn
60 .prepare("SELECT username, password FROM credentials WHERE host = ?1")
61 .unwrap();
50 let mut rows = stmt.query([host]).unwrap(); 62 let mut rows = stmt.query([host]).unwrap();
51 if let Ok(Some(row)) = rows.next() { 63 if let Ok(Some(row)) = rows.next() {
52 let username: String = row.get(0).unwrap(); 64 let username: String = row.get(0).unwrap();
@@ -55,4 +67,4 @@ pub fn get_credentials(conn: &Connection, host: &str) -> Option<(String, String)
55 } else { 67 } else {
56 None 68 None
57 } 69 }
58} \ No newline at end of file 70}
diff --git a/src/bin/client/games/manager.rs b/src/bin/client/games/manager.rs
new file mode 100644
index 0000000..6959214
--- /dev/null
+++ b/src/bin/client/games/manager.rs
@@ -0,0 +1,127 @@
1use crate::games::slots::SlotsGame;
2use crate::games::traits::{Game, GameError, GamePhase, GameState};
3use std::collections::HashMap;
4use std::io::Write;
5
6pub struct GameManager {
7 current_session: Option<GameState>,
8 pending_game_type: Option<String>,
9 games: HashMap<String, Box<dyn Game>>,
10}
11
12impl GameManager {
13 pub fn new() -> Self {
14 let mut games: HashMap<String, Box<dyn Game>> = HashMap::new();
15 games.insert("slots".to_string(), Box::new(SlotsGame::new()));
16
17 Self {
18 current_session: None,
19 pending_game_type: None,
20 games,
21 }
22 }
23
24 pub fn get_available_games(&self) -> Vec<(&str, &str)> {
25 self.games
26 .iter()
27 .map(|(k, v)| (k.as_str(), v.get_name()))
28 .collect()
29 }
30
31 pub fn enter_game(&mut self, game_type: &str) -> Result<(), GameError> {
32 if self.current_session.is_some() {
33 return Err(GameError::InvalidAction);
34 }
35
36 if !self.games.contains_key(game_type) {
37 return Err(GameError::InvalidAction);
38 }
39
40 self.pending_game_type = Some(game_type.to_string());
41
42 self.current_session = Some(GameState {
43 game_type: game_type.to_string(),
44 bet_amount: 10,
45 phase: GamePhase::WaitingToStart,
46 data: HashMap::new(),
47 });
48
49 Ok(())
50 }
51
52 pub fn start_game(
53 &mut self,
54 stream: &mut crate::connection::TlsStream,
55 ) -> Result<(), GameError> {
56 let game_type = self
57 .pending_game_type
58 .take()
59 .ok_or(GameError::InvalidAction)?;
60
61 let state = self
62 .current_session
63 .as_mut()
64 .ok_or(GameError::InvalidAction)?;
65
66 if state.phase != GamePhase::WaitingToStart {
67 return Err(GameError::InvalidAction);
68 }
69
70 let bet_amount = state.bet_amount;
71
72 let writer = stream;
73 writeln!(writer, "PLAY_START {} {}", game_type, bet_amount)
74 .map_err(|_| GameError::NetworkError)?;
75
76 Ok(())
77 }
78
79 pub fn get_current_game(&self) -> Option<&dyn Game> {
80 self.current_session
81 .as_ref()
82 .and_then(|session| self.games.get(&session.game_type).map(|game| game.as_ref()))
83 }
84
85 pub fn get_current_state(&self) -> Option<&GameState> {
86 self.current_session.as_ref()
87 }
88
89 pub fn get_current_render_lines(&self) -> Option<usize> {
90 let session = self.current_session.as_ref()?;
91 let game = self.games.get(&session.game_type)?;
92 Some(game.render_lines(session))
93 }
94
95 pub fn is_in_game(&self) -> bool {
96 self.current_session.is_some()
97 }
98
99 pub fn is_game_started(&self) -> bool {
100 self.current_session
101 .as_ref()
102 .map(|s| s.phase != GamePhase::WaitingToStart)
103 .unwrap_or(false)
104 }
105
106 pub fn end_game(&mut self) {
107 self.current_session = None;
108 self.pending_game_type = None;
109 }
110
111 pub fn adjust_bet(&mut self, delta: i64) -> Result<u64, GameError> {
112 if let Some(ref session) = self.current_session {
113 if session.phase != GamePhase::WaitingToStart {
114 return Err(GameError::InvalidAction);
115 }
116 }
117
118 if let Some(ref mut session) = self.current_session {
119 let current_bet = session.bet_amount as i64;
120 let new_bet = (current_bet + delta).max(10) as u64;
121 session.bet_amount = new_bet;
122 Ok(new_bet)
123 } else {
124 Err(GameError::InvalidAction)
125 }
126 }
127}
diff --git a/src/bin/client/games/mod.rs b/src/bin/client/games/mod.rs
new file mode 100644
index 0000000..aa74574
--- /dev/null
+++ b/src/bin/client/games/mod.rs
@@ -0,0 +1,3 @@
1pub mod manager;
2pub mod slots;
3pub mod traits;
diff --git a/src/bin/client/games/slots.rs b/src/bin/client/games/slots.rs
new file mode 100644
index 0000000..2f9d63e
--- /dev/null
+++ b/src/bin/client/games/slots.rs
@@ -0,0 +1,62 @@
1use crate::games::traits::{Game, GameState};
2
3pub struct SlotsGame;
4
5impl SlotsGame {
6 pub fn new() -> Self {
7 Self
8 }
9
10 fn render_reels(&self, reels: &str) -> String {
11 if reels.is_empty() {
12 "[ ? ] [ ? ] [ ? ]".to_string()
13 } else {
14 let chars: Vec<char> = reels.chars().collect();
15 format!(
16 "[ {} ] [ {} ] [ {} ]\r",
17 chars.get(0).unwrap_or(&'?'),
18 chars.get(1).unwrap_or(&'?'),
19 chars.get(2).unwrap_or(&'?')
20 )
21 }
22 }
23}
24
25impl Game for SlotsGame {
26 fn get_name(&self) -> &'static str {
27 "Slots"
28 }
29
30 fn render(&self, state: &GameState) -> String {
31 let reels = state.data.get("reels").cloned().unwrap_or_default();
32
33 let mut output = format!("\n=== SLOTS ===\r\n");
34 output += &format!("Bet: {} PLN\r\n", state.bet_amount);
35 output += &format!("{}\r\n", self.render_reels(&reels));
36 output += "\nPress [SPACE] to spin!\r\n";
37 output += "\nControls: [+/-] Adjust Bet [Q] Quit\r\n";
38 output
39 }
40
41 fn render_lines(&self, state: &GameState) -> usize {
42 self.render(state).lines().count()
43 }
44
45 fn get_help(&self) -> String {
46 "SLOTS GAME\n\
47 =========\n\
48 Match symbols to win!\n\
49 \n\
50 Payouts:\n\
51 Three of a kind: 5x-100x bet\n\
52 Two of a kind: 2x-20x bet\n\
53 \n\
54 Symbols: C=Cherry, L=Lemon, O=Orange, G=Grapes, D=Diamond, S=Seven\n\
55 \n\
56 Controls:\n\
57 SPACE - Spin the reels\n\
58 +/- - Adjust bet amount\n\
59 Q - Exit the game"
60 .to_string()
61 }
62}
diff --git a/src/bin/client/games/traits.rs b/src/bin/client/games/traits.rs
new file mode 100644
index 0000000..19f2638
--- /dev/null
+++ b/src/bin/client/games/traits.rs
@@ -0,0 +1,30 @@
1use std::collections::HashMap;
2
3#[derive(Debug, Clone)]
4pub enum GameError {
5 InvalidAction,
6 NetworkError,
7}
8
9#[derive(Debug, Clone, PartialEq)]
10pub enum GamePhase {
11 WaitingToStart,
12}
13
14#[derive(Debug, Clone)]
15pub struct GameState {
16 pub game_type: String,
17 pub bet_amount: u64,
18 pub phase: GamePhase,
19 pub data: HashMap<String, String>,
20}
21
22pub trait Game: Send + Sync {
23 fn get_name(&self) -> &'static str;
24
25 fn render(&self, state: &GameState) -> String;
26
27 fn render_lines(&self, state: &GameState) -> usize;
28
29 fn get_help(&self) -> String;
30}
diff --git a/src/bin/client/macros.rs b/src/bin/client/macros.rs
index e45f2b3..cda73e3 100644
--- a/src/bin/client/macros.rs
+++ b/src/bin/client/macros.rs
@@ -14,4 +14,4 @@ macro_rules! printsame {
14 ($($arg:tt)*) => { 14 ($($arg:tt)*) => {
15 print!("\r{}", format_args!($($arg)*)) 15 print!("\r{}", format_args!($($arg)*))
16 }; 16 };
17} \ No newline at end of file 17}
diff --git a/src/bin/client/main.rs b/src/bin/client/main.rs
index 2ff6f55..c95880d 100644
--- a/src/bin/client/main.rs
+++ b/src/bin/client/main.rs
@@ -1,12 +1,22 @@
1#[macro_use] 1#[macro_use]
2mod macros; 2mod macros;
3mod repl;
4mod database;
5mod connection; 3mod connection;
4mod database;
5mod games;
6mod repl;
6 7
7use repl::ClientState; 8use repl::ClientState;
8 9
9fn main() { 10fn main() {
11 let args: Vec<String> = std::env::args().collect();
12 let unsafe_mode = args.iter().any(|arg| arg == "--unsafe");
13
14 if unsafe_mode {
15 println!("WARNING: Running in UNSAFE mode - certificate verification disabled!");
16 }
17
18 connection::set_unsafe_mode(unsafe_mode);
19
10 let mut state = ClientState::new(); 20 let mut state = ClientState::new();
11 if let Some(host) = state.addr.clone() { 21 if let Some(host) = state.addr.clone() {
12 state.connect_to(&host); 22 state.connect_to(&host);
diff --git a/src/bin/client/mod.rs b/src/bin/client/mod.rs
index 938d862..21d7f27 100644
--- a/src/bin/client/mod.rs
+++ b/src/bin/client/mod.rs
@@ -1,4 +1,5 @@
1pub mod database; 1pub mod database;
2pub mod connection; 2mod connection;
3mod games;
3pub mod platform; 4pub mod platform;
4pub mod repl; 5pub mod repl;
diff --git a/src/bin/client/repl.rs b/src/bin/client/repl.rs
index a0e5b5e..194860b 100644
--- a/src/bin/client/repl.rs
+++ b/src/bin/client/repl.rs
@@ -1,23 +1,32 @@
1use std::io::{BufRead, BufReader, Write, stdout};
2use std::net::TcpStream;
3use rusqlite::Connection; 1use rusqlite::Connection;
2use std::io::{stdout, BufRead, BufReader, Write};
3use std::sync::{Arc, Mutex};
4use std::time::Instant; 4use std::time::Instant;
5 5
6use crate::connection; 6use crate::connection::TlsStream;
7use crate::database; 7use crate::database;
8use crate::games::manager::GameManager;
8 9
9use crossterm::{event::{read, Event, KeyCode, KeyModifiers}, terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType}, cursor::{MoveToColumn, Show, Hide}, execute}; 10use crossterm::{
11 cursor::{Hide, MoveToColumn, Show},
12 event::{read, Event, KeyCode, KeyModifiers},
13 execute,
14 terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType},
15};
10 16
11use sha2::{Sha256, Digest}; 17use sha2::{Digest, Sha256};
12 18
13pub struct ClientState { 19pub struct ClientState {
14 pub stream: Option<TcpStream>, 20 pub stream: Option<TlsStream>,
15 pub conn: Connection, 21 pub conn: Connection,
16 pub addr: Option<String>, 22 pub addr: Option<String>,
17 pub default_port: u16, 23 pub default_port: u16,
18 pub history: Vec<String>, 24 pub history: Vec<String>,
19 pub history_index: usize, 25 pub history_index: usize,
20 pub logged_in_user: Option<String>, 26 pub logged_in_user: Option<String>,
27 pub game_manager: GameManager,
28 cleanup_done: Arc<Mutex<bool>>,
29 in_game_mode: bool,
21} 30}
22 31
23impl ClientState { 32impl ClientState {
@@ -37,12 +46,86 @@ impl ClientState {
37 history: vec![], 46 history: vec![],
38 history_index: 0, 47 history_index: 0,
39 logged_in_user: None, 48 logged_in_user: None,
49 game_manager: GameManager::new(),
50 cleanup_done: Arc::new(Mutex::new(false)),
51 in_game_mode: false,
52 }
53 }
54
55 pub fn enter_game_mode(&mut self) {
56 self.in_game_mode = true;
57 }
58
59 pub fn exit_game_mode(&mut self) {
60 self.in_game_mode = false;
61 self.game_manager.end_game();
62 }
63
64 pub fn handle_game_key(&mut self, key_code: KeyCode) -> bool {
65 if !self.game_manager.is_in_game() {
66 return false;
67 }
68
69 match key_code {
70 KeyCode::Char('q') => {
71 self.exit_game_mode();
72 println!("\nGame exited.");
73 return true;
74 }
75 KeyCode::Char(' ') => {
76 let should_start = !self.game_manager.is_game_started();
77 if should_start {
78 if let Some(mut stream) = self.stream.take() {
79 match self.game_manager.start_game(&mut stream) {
80 Ok(()) => {
81 self.play_spin_animation(&mut stream);
82 }
83 Err(e) => println!("\nError starting game: {:?}", e),
84 }
85 self.stream = Some(stream);
86 }
87 }
88 return true;
89 }
90 KeyCode::Char('+') => {
91 // Increase bet
92 match self.game_manager.adjust_bet(10) {
93 Ok(_new_bet) => {
94 if let Some(state) = self.game_manager.get_current_state() {
95 if let Some(game) = self.game_manager.get_current_game() {
96 let lines =
97 self.game_manager.get_current_render_lines().unwrap_or(8);
98 redraw_game_lines(&game.render(state), lines);
99 }
100 }
101 }
102 Err(e) => println!("\nError: {:?}", e),
103 }
104 return true;
105 }
106 KeyCode::Char('-') => {
107 // Decrease bet
108 match self.game_manager.adjust_bet(-10) {
109 Ok(_new_bet) => {
110 if let Some(state) = self.game_manager.get_current_state() {
111 if let Some(game) = self.game_manager.get_current_game() {
112 let lines =
113 self.game_manager.get_current_render_lines().unwrap_or(8);
114 redraw_game_lines(&game.render(state), lines);
115 }
116 }
117 }
118 Err(e) => println!("\nError: {:?}", e),
119 }
120 return true;
121 }
122 _ => return false,
40 } 123 }
41 } 124 }
42 125
43 pub fn connect_to(&mut self, addr: &str) { 126 pub fn connect_to(&mut self, addr: &str) {
44 self.addr = Some(addr.to_string()); 127 self.addr = Some(addr.to_string());
45 match connection::connect(addr, &self.conn) { 128 match crate::connection::connect(addr, &self.conn) {
46 Ok((s, authed_user)) => { 129 Ok((s, authed_user)) => {
47 self.stream = Some(s); 130 self.stream = Some(s);
48 self.logged_in_user = authed_user; 131 self.logged_in_user = authed_user;
@@ -53,6 +136,24 @@ impl ClientState {
53 } 136 }
54 137
55 pub fn run(&mut self) { 138 pub fn run(&mut self) {
139 // Set up cleanup handlers
140 let cleanup_done = self.cleanup_done.clone();
141
142 // Set up panic handler
143 let panic_cleanup = cleanup_done.clone();
144 std::panic::set_hook(Box::new(move |_| {
145 restore_terminal(&panic_cleanup);
146 }));
147
148 // Set up Ctrl+C handler
149 let ctrlc_cleanup = cleanup_done.clone();
150 if let Err(e) = ctrlc::set_handler(move || {
151 restore_terminal(&ctrlc_cleanup);
152 std::process::exit(0);
153 }) {
154 eprintln!("Warning: Could not set Ctrl+C handler: {}", e);
155 }
156
56 enable_raw_mode().unwrap(); 157 enable_raw_mode().unwrap();
57 'outer: loop { 158 'outer: loop {
58 let mut current_input = String::new(); 159 let mut current_input = String::new();
@@ -64,308 +165,446 @@ impl ClientState {
64 Event::Key(key) => { 165 Event::Key(key) => {
65 // Only handle key press events, not release events 166 // Only handle key press events, not release events
66 if key.kind == crossterm::event::KeyEventKind::Press { 167 if key.kind == crossterm::event::KeyEventKind::Press {
67 match key.code { 168 // Handle game mode keys first
68 KeyCode::Up => { 169 if self.game_manager.is_in_game() && self.handle_game_key(key.code) {
69 if self.history_index > 0 { 170 // Game key was handled, continue loop
70 self.history_index -= 1; 171 if !self.game_manager.is_in_game() {
71 current_input = self.history[self.history_index].clone(); 172 // Game ended, redraw command prompt
72 redraw(&current_input); 173 redraw("");
73 }
74 }
75 KeyCode::Down => {
76 if self.history_index < self.history.len() {
77 self.history_index += 1;
78 current_input = if self.history_index == self.history.len() { "".to_string() } else { self.history[self.history_index].clone() };
79 redraw(&current_input);
80 }
81 }
82 KeyCode::Enter => {
83 let line = current_input.trim().to_string();
84 if !line.is_empty() {
85 self.history.push(line.clone());
86 }
87 // Process command
88 let parts: Vec<&str> = line.splitn(2, ' ').collect();
89 match parts[0] {
90 "connect" => {
91 let addr_str = if parts.len() < 2 {
92 if let Some(h) = &self.addr {
93 h.clone()
94 } else {
95 println!("\nNo host specified and no previous connection. Usage: connect <ip>:<port>");
96 continue 'outer;
97 }
98 } else {
99 let mut a = parts[1].to_string();
100 if !a.contains(':') {
101 a = format!("{}:{}", a, self.default_port);
102 }
103 a
104 };
105 self.connect_to(&addr_str);
106 } 174 }
107 "send" => { 175 continue;
108 if self.stream.is_none() { 176 }
109 println!("\nNot connected. Use 'connect <ip>:<port>' first."); 177
110 continue 'outer; 178 // Handle normal command mode keys
111 } 179 match key.code {
112 if parts.len() < 2 { 180 KeyCode::Up => {
113 println!("\nUsage: send <message>"); 181 if self.history_index > 0 {
114 continue 'outer; 182 self.history_index -= 1;
115 } 183 current_input = self.history[self.history_index].clone();
116 let msg = parts[1]; 184 redraw(&current_input);
117 let s = self.stream.as_mut().unwrap();
118 let writer_stream = s.try_clone().unwrap();
119 let mut writer = writer_stream;
120 let response_reader_stream = s.try_clone().unwrap();
121 let mut response_reader = BufReader::new(response_reader_stream);
122 if let Err(_) = writeln!(writer, "{}", msg) {
123 self.stream = None;
124 println!("\nConnection lost");
125 continue 'outer;
126 }
127 let mut response = String::new();
128 if let Err(_) = response_reader.read_line(&mut response) {
129 self.stream = None;
130 println!("\nConnection lost");
131 } else {
132 println!("\nEcho: {}", response.trim());
133 } 185 }
134 } 186 }
135 "disconnect" => { 187 KeyCode::Down => {
136 self.stream = None; 188 if self.history_index < self.history.len() {
137 self.logged_in_user = None; 189 self.history_index += 1;
138 println!("\nDisconnected"); 190 current_input = if self.history_index == self.history.len()
139 } 191 {
140 "ping" => { 192 "".to_string()
141 if self.stream.is_none() {
142 println!("\nNot connected. Use 'connect <ip>:<port>' first.");
143 continue 'outer;
144 }
145 let s = self.stream.as_mut().unwrap();
146 let writer_stream = s.try_clone().unwrap();
147 let mut writer = writer_stream;
148 let start = Instant::now();
149 let response_reader_stream = s.try_clone().unwrap();
150 let mut response_reader = BufReader::new(response_reader_stream);
151 if let Err(_) = writeln!(writer, "PING") {
152 self.stream = None;
153 println!("\nConnection lost");
154 continue 'outer;
155 }
156 let mut response = String::new();
157 if let Err(_) = response_reader.read_line(&mut response) {
158 self.stream = None;
159 println!("\nConnection lost");
160 } else {
161 if response.trim() == "PONG" {
162 let elapsed = start.elapsed();
163 println!("\nPong ({:?})", elapsed);
164 } else { 193 } else {
165 self.stream = None; 194 self.history[self.history_index].clone()
166 println!("\nConnection lost"); 195 };
167 } 196 redraw(&current_input);
168 } 197 }
169 } 198 }
170 "register" => { 199 KeyCode::Enter => {
171 if self.stream.is_none() { 200 let line = current_input.trim().to_string();
172 println!("\nNot connected. Use 'connect <ip>:<port>' first."); 201 if !line.is_empty() {
173 continue 'outer; 202 self.history.push(line.clone());
174 }
175 let username = read_line("\nUsername: ");
176 if username.is_empty() {
177 continue 'outer;
178 } 203 }
179 let password = read_password("Password: "); 204 let line_trimmed = current_input.trim().to_string();
180 if password.is_empty() { 205 if !line_trimmed.is_empty() {
181 continue 'outer; 206 self.history.push(line_trimmed.clone());
182 } 207 }
183 let hashed = format!("{:x}", Sha256::digest(password.as_bytes())); 208
184 let msg = format!("REGISTER {} {}", username, hashed); 209 // Process normal commands
185 let s = self.stream.as_mut().unwrap(); 210 let parts: Vec<&str> = line_trimmed.splitn(3, ' ').collect();
186 let writer_stream = s.try_clone().unwrap(); 211 match parts[0] {
187 let mut writer = writer_stream; 212 "play" => {
188 let response_reader_stream = s.try_clone().unwrap(); 213 if self.stream.is_none() {
189 let mut response_reader = BufReader::new(response_reader_stream); 214 println!("\nNot connected. Use 'connect <ip>:<port>' first.");
190 if let Err(_) = writeln!(writer, "{}", msg) { 215 continue 'outer;
191 self.stream = None; 216 }
192 println!("\nConnection lost"); 217 if self.logged_in_user.is_none() {
193 continue 'outer; 218 println!("\nNot logged in. Use 'login' first.");
194 } 219 continue 'outer;
195 let mut response = String::new(); 220 }
196 if let Err(_) = response_reader.read_line(&mut response) { 221 if parts.len() < 2 {
197 self.stream = None; 222 let games = self.game_manager.get_available_games();
198 println!("\nConnection lost"); 223 println!("\nAvailable games:");
199 } else { 224 for (gt, name) in games {
200 let resp = response.trim(); 225 println!(" - {} ({})", gt, name);
201 if resp == format!("REGISTERED {}", username) { 226 }
202 println!("\nSuccessfully registered as {}", username); 227 println!("\nUsage: play <game>");
203 } else if resp.starts_with("ERROR ") { 228 continue 'outer;
204 println!("\nRegistration failed: {}", &resp[6..]); 229 }
205 } else { 230 let game_type = parts[1];
206 println!("\n{}", resp); 231
232 if self.game_manager.is_in_game() {
233 println!("\nAlready in a game. Use 'quit' to exit current game.");
234 continue 'outer;
235 }
236
237 match self.game_manager.enter_game(game_type) {
238 Ok(()) => {
239 self.enter_game_mode();
240 if let Some(game) =
241 self.game_manager.get_current_game()
242 {
243 if let Some(state) =
244 self.game_manager.get_current_state()
245 {
246 redraw_game(&game.render(state));
247 }
248 }
249 }
250 Err(e) => {
251 println!("\nFailed to enter game: {:?}", e);
252 }
253 }
207 } 254 }
208 } 255 "games" => {
209 } 256 let games = self.game_manager.get_available_games();
210 "login" => { 257 println!("\nAvailable games:");
211 if self.stream.is_none() { 258 for (gt, name) in games {
212 println!("\nNot connected. Use 'connect <ip>:<port>' first."); 259 println!(" - {} ({})", gt, name);
213 continue 'outer; 260 }
214 }
215 if self.logged_in_user.is_some() {
216 println!("\nAlready logged in as {}", self.logged_in_user.as_ref().unwrap());
217 continue 'outer;
218 }
219 loop {
220 let username = read_line("\nUsername: ");
221 if username.is_empty() {
222 break;
223 } 261 }
224 let password = read_password("Password: "); 262 "connect" => {
225 if password.is_empty() { 263 let addr_str = if parts.len() < 2 {
226 break; 264 if let Some(h) = &self.addr {
265 h.clone()
266 } else {
267 println!("\nNo host specified and no previous connection. Usage: connect <ip>:<port>");
268 continue 'outer;
269 }
270 } else {
271 let mut a = parts[1].to_string();
272 if !a.contains(':') {
273 a = format!("{}:{}", a, self.default_port);
274 }
275 a
276 };
277 self.connect_to(&addr_str);
227 } 278 }
228 let hashed = format!("{:x}", Sha256::digest(password.as_bytes())); 279 "send" => {
229 let msg = format!("AUTH {} {}", username, hashed); 280 if self.stream.is_none() {
230 let s = self.stream.as_mut().unwrap(); 281 println!("\nNot connected. Use 'connect <ip>:<port>' first.");
231 let writer_stream = s.try_clone().unwrap(); 282 continue 'outer;
232 let mut writer = writer_stream; 283 }
233 let response_reader_stream = s.try_clone().unwrap(); 284 if parts.len() < 2 {
234 let mut response_reader = BufReader::new(response_reader_stream); 285 println!("\nUsage: send <message>");
235 if let Err(_) = writeln!(writer, "{}", msg) { 286 continue 'outer;
236 self.stream = None; 287 }
237 println!("\nConnection lost"); 288 let msg = parts[1];
238 break; 289 let s = self.stream.as_mut().unwrap();
290 let writer_stream = s.try_clone().unwrap();
291 let mut writer = writer_stream;
292 let response_reader_stream = s.try_clone().unwrap();
293 let mut response_reader =
294 BufReader::new(response_reader_stream);
295 if let Err(_) = writeln!(writer, "{}", msg) {
296 self.stream = None;
297 println!("\nConnection lost");
298 continue 'outer;
299 }
300 let mut response = String::new();
301 if let Err(_) = response_reader.read_line(&mut response)
302 {
303 self.stream = None;
304 println!("\nConnection lost");
305 } else {
306 println!("\nEcho: {}", response.trim());
307 }
239 } 308 }
240 let mut response = String::new(); 309 "disconnect" => {
241 if let Err(_) = response_reader.read_line(&mut response) {
242 self.stream = None; 310 self.stream = None;
243 println!("\nConnection lost"); 311 self.logged_in_user = None;
244 break; 312 println!("\nDisconnected");
245 } else { 313 }
246 let resp = response.trim(); 314 "ping" => {
247 if resp.starts_with("OK") { 315 if self.stream.is_none() {
248 // Save credentials 316 println!("\nNot connected. Use 'connect <ip>:<port>' first.");
249 if let Some(addr) = &self.addr { 317 continue 'outer;
250 if let Err(e) = database::save_credentials(&self.conn, addr, &username, &hashed) { 318 }
251 println!("Failed to save credentials: {}", e); 319 let s = self.stream.as_mut().unwrap();
320 let writer_stream = s.try_clone().unwrap();
321 let mut writer = writer_stream;
322 let start = Instant::now();
323 let response_reader_stream = s.try_clone().unwrap();
324 let mut response_reader =
325 BufReader::new(response_reader_stream);
326 if let Err(_) = writeln!(writer, "PING") {
327 self.stream = None;
328 println!("\nConnection lost");
329 continue 'outer;
330 }
331 let mut response = String::new();
332 if let Err(_) = response_reader.read_line(&mut response)
333 {
334 self.stream = None;
335 println!("\nConnection lost");
336 } else {
337 if response.trim() == "PONG" {
338 let elapsed = start.elapsed();
339 println!("\nPong ({:?})", elapsed);
340 } else {
341 self.stream = None;
342 println!("\nConnection lost");
343 }
344 }
345 }
346 "register" => {
347 if self.stream.is_none() {
348 println!("\nNot connected. Use 'connect <ip>:<port>' first.");
349 continue 'outer;
350 }
351 let username = read_line("\nUsername: ");
352 if username.is_empty() {
353 continue 'outer;
354 }
355 let password = read_password("Password: ");
356 if password.is_empty() {
357 continue 'outer;
358 }
359 let hashed = format!(
360 "{:x}",
361 Sha256::digest(password.as_bytes())
362 );
363 let msg = format!("REGISTER {} {}", username, hashed);
364 let s = self.stream.as_mut().unwrap();
365 let writer_stream = s.try_clone().unwrap();
366 let mut writer = writer_stream;
367 let response_reader_stream = s.try_clone().unwrap();
368 let mut response_reader =
369 BufReader::new(response_reader_stream);
370 if let Err(_) = writeln!(writer, "{}", msg) {
371 self.stream = None;
372 println!("\nConnection lost");
373 continue 'outer;
374 }
375 let mut response = String::new();
376 if let Err(_) = response_reader.read_line(&mut response)
377 {
378 self.stream = None;
379 println!("\nConnection lost");
380 } else {
381 let resp = response.trim();
382 if resp == format!("REGISTERED {}", username) {
383 println!(
384 "\nSuccessfully registered as {}",
385 username
386 );
387 } else if resp.starts_with("ERROR ") {
388 println!(
389 "\nRegistration failed: {}",
390 &resp[6..]
391 );
392 } else {
393 println!("\n{}", resp);
394 }
395 }
396 }
397 "login" => {
398 if self.stream.is_none() {
399 println!("\nNot connected. Use 'connect <ip>:<port>' first.");
400 continue 'outer;
401 }
402 if self.logged_in_user.is_some() {
403 println!(
404 "\nAlready logged in as {}",
405 self.logged_in_user.as_ref().unwrap()
406 );
407 continue 'outer;
408 }
409 loop {
410 let username = read_line("\nUsername: ");
411 if username.is_empty() {
412 break;
413 }
414 let password = read_password("Password: ");
415 if password.is_empty() {
416 break;
417 }
418 let hashed = format!(
419 "{:x}",
420 Sha256::digest(password.as_bytes())
421 );
422 let msg = format!("AUTH {} {}", username, hashed);
423 let s = self.stream.as_mut().unwrap();
424 let writer_stream = s.try_clone().unwrap();
425 let mut writer = writer_stream;
426 let response_reader_stream = s.try_clone().unwrap();
427 let mut response_reader =
428 BufReader::new(response_reader_stream);
429 if let Err(_) = writeln!(writer, "{}", msg) {
430 self.stream = None;
431 println!("\nConnection lost");
432 break;
433 }
434 let mut response = String::new();
435 if let Err(_) =
436 response_reader.read_line(&mut response)
437 {
438 self.stream = None;
439 println!("\nConnection lost");
440 break;
441 } else {
442 let resp = response.trim();
443 if resp.starts_with("OK") {
444 // Save credentials
445 if let Some(addr) = &self.addr {
446 if let Err(e) =
447 database::save_credentials(
448 &self.conn, addr, &username,
449 &hashed,
450 )
451 {
452 println!("Failed to save credentials: {}", e);
453 }
454 }
455 self.logged_in_user =
456 Some(username.clone());
457 println!("\nLogged in as {}", username);
458 break;
459 } else if resp.starts_with("ERROR ") {
460 println!(
461 "\nAuthentication failed: {}",
462 &resp[6..]
463 );
464 println!(
465 "Try again or press Enter to cancel."
466 );
467 } else {
468 println!("\n{}", resp);
252 } 469 }
253 } 470 }
254 self.logged_in_user = Some(username.clone()); 471 }
255 println!("\nLogged in as {}", username); 472 }
256 break; 473 "setcred" => {
257 } else if resp.starts_with("ERROR ") { 474 if self.addr.is_none() {
258 println!("\nAuthentication failed: {}", &resp[6..]); 475 println!("\nNo host connected. Use 'connect <ip>:<port>' first.");
259 println!("Try again or press Enter to cancel."); 476 continue 'outer;
477 }
478 let username = read_line("\nUsername: ");
479 if username.is_empty() {
480 continue 'outer;
481 }
482 let password = read_password("Password: ");
483 if password.is_empty() {
484 continue 'outer;
485 }
486 let hashed = format!(
487 "{:x}",
488 Sha256::digest(password.as_bytes())
489 );
490 if let Some(addr) = &self.addr {
491 if let Err(e) = database::save_credentials(
492 &self.conn, addr, &username, &hashed,
493 ) {
494 println!("Failed to save credentials: {}", e);
495 } else {
496 println!("Credentials saved for {}", addr);
497 }
498 }
499 }
500 "logout" => {
501 self.logged_in_user = None;
502 println!("\nLogged out");
503 }
504 "info" => {
505 if self.stream.is_none() {
506 println!("\nNot connected. Use 'connect <ip>:<port>' first.");
507 continue 'outer;
508 }
509 let s = self.stream.as_mut().unwrap();
510 let writer_stream = s.try_clone().unwrap();
511 let mut writer = writer_stream;
512 let response_reader_stream = s.try_clone().unwrap();
513 let mut response_reader =
514 BufReader::new(response_reader_stream);
515 if let Err(_) = writeln!(writer, "INFO") {
516 self.stream = None;
517 println!("\nConnection lost");
518 continue 'outer;
519 }
520 let mut response = String::new();
521 if let Err(_) = response_reader.read_line(&mut response)
522 {
523 self.stream = None;
524 println!("\nConnection lost");
260 } else { 525 } else {
261 println!("\n{}", resp); 526 let resp = response.trim();
527 if resp.starts_with("INFO ") {
528 let parts: Vec<&str> =
529 resp.split_whitespace().collect();
530 if parts.len() == 4 {
531 println!("\nLogged in as {} (ID: {})\r\nBalance: {} PLN", parts[1], parts[2], parts[3]);
532 } else {
533 println!("\n{}", resp);
534 }
535 } else if resp.starts_with("ERROR ") {
536 println!("\nError: {}", &resp[6..]);
537 } else {
538 println!("\n{}", resp);
539 }
262 } 540 }
263 } 541 }
264 } 542 "help" => {
265 } 543 if self.game_manager.is_in_game() {
266 "setcred" => { 544 if let Some(game) =
267 if self.addr.is_none() { 545 self.game_manager.get_current_game()
268 println!("\nNo host connected. Use 'connect <ip>:<port>' first."); 546 {
269 continue 'outer; 547 println!("\n{}", game.get_help());
270 } 548 }
271 let username = read_line("\nUsername: "); 549 } else {
272 if username.is_empty() { 550 help();
273 continue 'outer; 551 }
274 }
275 let password = read_password("Password: ");
276 if password.is_empty() {
277 continue 'outer;
278 }
279 let hashed = format!("{:x}", Sha256::digest(password.as_bytes()));
280 if let Some(addr) = &self.addr {
281 if let Err(e) = database::save_credentials(&self.conn, addr, &username, &hashed) {
282 println!("Failed to save credentials: {}", e);
283 } else {
284 println!("Credentials saved for {}", addr);
285 } 552 }
553 "quit" => {
554 if self.game_manager.is_in_game() {
555 self.game_manager.end_game();
556 println!("\nGame exited.");
557 } else {
558 restore_terminal(&self.cleanup_done);
559 break 'outer;
560 }
561 }
562 _ => help(),
286 } 563 }
564 break;
287 } 565 }
288 "logout" => { 566 KeyCode::Backspace => {
289 self.logged_in_user = None; 567 if !current_input.is_empty() {
290 println!("\nLogged out"); 568 current_input.pop();
291 } 569 execute!(
292 "info" => { 570 stdout(),
293 if self.stream.is_none() { 571 MoveToColumn(2),
294 println!("\nNot connected. Use 'connect <ip>:<port>' first."); 572 Clear(ClearType::UntilNewLine)
295 continue 'outer; 573 )
296 } 574 .unwrap();
297 let s = self.stream.as_mut().unwrap(); 575 print!("{}", current_input);
298 let writer_stream = s.try_clone().unwrap(); 576 stdout().flush().unwrap();
299 let mut writer = writer_stream;
300 let response_reader_stream = s.try_clone().unwrap();
301 let mut response_reader = BufReader::new(response_reader_stream);
302 if let Err(_) = writeln!(writer, "INFO") {
303 self.stream = None;
304 println!("\nConnection lost");
305 continue 'outer;
306 } 577 }
307 let mut response = String::new(); 578 }
308 if let Err(_) = response_reader.read_line(&mut response) { 579 KeyCode::Char(c) => {
309 self.stream = None; 580 if key.modifiers.contains(KeyModifiers::CONTROL) && c == 'c' {
310 println!("\nConnection lost"); 581 restore_terminal(&self.cleanup_done);
582 std::process::exit(0);
583 } else if key.modifiers.contains(KeyModifiers::CONTROL)
584 && c == 'd'
585 {
586 restore_terminal(&self.cleanup_done);
587 std::process::exit(0);
588 } else if c == '\t' {
589 self.handle_tab_completion(&mut current_input);
311 } else { 590 } else {
312 let resp = response.trim(); 591 current_input.push(c);
313 if resp.starts_with("INFO ") { 592 print!("{}", c);
314 let parts: Vec<&str> = resp.split_whitespace().collect(); 593 stdout().flush().unwrap();
315 if parts.len() == 4 {
316 println!("\nLogged in as {} (ID: {})\r\nBalance: {} PLN", parts[1], parts[2], parts[3]);
317 } else {
318 println!("\n{}", resp);
319 }
320 } else if resp.starts_with("ERROR ") {
321 println!("\nError: {}", &resp[6..]);
322 } else {
323 println!("\n{}", resp);
324 }
325 } 594 }
326 } 595 }
327 "help" => { 596 KeyCode::Tab => {
328 help(); 597 self.handle_tab_completion(&mut current_input);
329 } 598 }
330 "quit" => break 'outer, 599 _ => {}
331 _ => help(),
332 }
333 break;
334 }
335 KeyCode::Backspace => {
336 if !current_input.is_empty() {
337 current_input.pop();
338 execute!(stdout(), MoveToColumn(2),
339 Clear(ClearType::UntilNewLine)).unwrap();
340 print!("{}", current_input);
341 stdout().flush().unwrap();
342 } 600 }
343 } 601 }
344 KeyCode::Char(c) => {
345 if key.modifiers.contains(KeyModifiers::CONTROL) && c == 'c' {
346 execute!(stdout(), Show).unwrap();
347 return;
348 } else if c == '\t' {
349 self.handle_tab_completion(&mut current_input);
350 } else {
351 current_input.push(c);
352 print!("{}", c);
353 stdout().flush().unwrap();
354 }
355 }
356 KeyCode::Tab => {
357 self.handle_tab_completion(&mut current_input);
358 }
359 _ => {}
360 }
361 } 602 }
362 }
363 _ => {} 603 _ => {}
364 } 604 }
365 } 605 }
366 } 606 }
367 disable_raw_mode().unwrap(); 607 restore_terminal(&self.cleanup_done);
368 execute!(stdout(), Show).unwrap();
369 } 608 }
370 609
371 fn handle_tab_completion(&mut self, current_input: &mut String) { 610 fn handle_tab_completion(&mut self, current_input: &mut String) {
@@ -387,6 +626,77 @@ impl ClientState {
387 } 626 }
388 // else do nothing 627 // else do nothing
389 } 628 }
629
630 fn play_spin_animation(&mut self, stream: &mut TlsStream) {
631 use std::io::Write;
632 use std::thread;
633 use std::time::Duration;
634
635 let symbols = ['C', 'L', 'O', 'G', 'D', 'S'];
636 let frames = 20;
637 let delay = Duration::from_millis(50);
638
639 for _ in 0..frames {
640 let reels: String = (0..3)
641 .map(|_| symbols[rand::random::<usize>() % symbols.len()])
642 .collect();
643
644 execute!(stdout(), Clear(ClearType::CurrentLine), MoveToColumn(0)).unwrap();
645 print!(
646 "[ {} ] [ {} ] [ {} ]",
647 reels.chars().nth(0).unwrap(),
648 reels.chars().nth(1).unwrap(),
649 reels.chars().nth(2).unwrap()
650 );
651 stdout().flush().unwrap();
652
653 thread::sleep(delay);
654 }
655
656 let mut response_str = String::new();
657 let mut reader = std::io::BufReader::new(stream.try_clone().unwrap());
658 if reader.read_line(&mut response_str).is_ok() {
659 let response = response_str.trim();
660
661 if response.starts_with("GAME_RESULT") {
662 let parts: Vec<&str> = response.split_whitespace().collect();
663 if parts.len() >= 7 {
664 let reels = parts[5];
665 let win_type = parts[6];
666
667 execute!(stdout(), Clear(ClearType::CurrentLine), MoveToColumn(0)).unwrap();
668 print!(
669 "[ {} ] [ {} ] [ {} ]",
670 reels.chars().nth(0).unwrap_or('?'),
671 reels.chars().nth(1).unwrap_or('?'),
672 reels.chars().nth(2).unwrap_or('?')
673 );
674 println!();
675
676 if win_type == "jackpot" {
677 println!("\n🎉 JACKPOT! 🎉");
678 } else if win_type == "two" {
679 println!("\n✨ Two of a kind! ✨");
680 } else {
681 println!("\nNo win.");
682 }
683
684 println!("Winnings: {} PLN", parts[2]);
685 println!("New Balance: {} PLN", parts[4]);
686 stdout().flush().unwrap();
687 }
688 }
689 }
690 }
691}
692
693fn restore_terminal(cleanup_done: &Arc<Mutex<bool>>) {
694 let mut done = cleanup_done.lock().unwrap();
695 if !*done {
696 let _ = disable_raw_mode();
697 let _ = execute!(stdout(), Show);
698 *done = true;
699 }
390} 700}
391 701
392fn get_commands() -> Vec<String> { 702fn get_commands() -> Vec<String> {
@@ -400,13 +710,15 @@ fn get_commands() -> Vec<String> {
400 "setcred".to_string(), 710 "setcred".to_string(),
401 "logout".to_string(), 711 "logout".to_string(),
402 "info".to_string(), 712 "info".to_string(),
713 "play".to_string(),
403 "help".to_string(), 714 "help".to_string(),
404 "quit".to_string(), 715 "quit".to_string(),
405 ] 716 ]
406} 717}
407 718
408fn help() { 719fn help() {
409 println!("\nAvailable commands:\r 720 println!(
721 "\nAvailable commands:\r
410 connect <ip>:<port> - connect to the server\r 722 connect <ip>:<port> - connect to the server\r
411 send <message> - send a message to the server\r 723 send <message> - send a message to the server\r
412 disconnect - disconnect from the server\r 724 disconnect - disconnect from the server\r
@@ -416,8 +728,38 @@ fn help() {
416 setcred - set credentials for a host\r 728 setcred - set credentials for a host\r
417 logout - logout from the server\r 729 logout - logout from the server\r
418 info - show login info\r 730 info - show login info\r
731 play <game> [bet] - start a game (e.g., play slots 50)\r
419 help - show this message\r 732 help - show this message\r
420 quit - quit the program"); 733 quit - quit the program"
734 );
735}
736
737fn redraw_game(game_output: &str) {
738 execute!(
739 stdout(),
740 Clear(ClearType::CurrentLine),
741 Clear(ClearType::FromCursorDown),
742 MoveToColumn(0)
743 )
744 .unwrap();
745 println!("{}", game_output);
746 stdout().flush().unwrap();
747}
748
749fn redraw_game_lines(game_output: &str, lines: usize) {
750 use crossterm::cursor::MoveUp;
751 if lines > 0 {
752 execute!(stdout(), MoveUp(lines as u16)).unwrap();
753 }
754 execute!(
755 stdout(),
756 Clear(ClearType::CurrentLine),
757 Clear(ClearType::FromCursorDown),
758 MoveToColumn(0)
759 )
760 .unwrap();
761 println!("{}", game_output);
762 stdout().flush().unwrap();
421} 763}
422 764
423fn redraw(current_input: &str) { 765fn redraw(current_input: &str) {
@@ -431,36 +773,40 @@ fn read_line(prompt: &str) -> String {
431 execute!(stdout(), Show).unwrap(); 773 execute!(stdout(), Show).unwrap();
432 printsame!("{}", prompt); 774 printsame!("{}", prompt);
433 stdout().flush().unwrap(); 775 stdout().flush().unwrap();
434 776
435 let mut input = String::new(); 777 let mut input = String::new();
436 let prompt_len = prompt.len(); 778 let prompt_len = prompt.len();
437 779
438 loop { 780 loop {
439 match read().unwrap() { 781 match read().unwrap() {
440 Event::Key(key) => { 782 Event::Key(key) => {
441 if key.kind == crossterm::event::KeyEventKind::Press { 783 if key.kind == crossterm::event::KeyEventKind::Press {
442 match key.code { 784 match key.code {
443 KeyCode::Enter => { 785 KeyCode::Enter => {
444 println!(); 786 println!();
445 break; 787 break;
446 } 788 }
447 KeyCode::Backspace => { 789 KeyCode::Backspace => {
448 if !input.is_empty() { 790 if !input.is_empty() {
449 input.pop(); 791 input.pop();
450 // Move cursor back and clear from current position to end of line 792 // Move cursor back and clear from current position to end of line
451 execute!(stdout(), MoveToColumn((prompt_len + input.len() + 1) as u16), 793 execute!(
452 Clear(ClearType::UntilNewLine)).unwrap(); 794 stdout(),
453 stdout().flush().unwrap(); 795 MoveToColumn((prompt_len + input.len() + 1) as u16),
796 Clear(ClearType::UntilNewLine)
797 )
798 .unwrap();
799 stdout().flush().unwrap();
800 }
801 }
802 KeyCode::Char(c) => {
803 input.push(c);
804 print!("{}", c);
805 stdout().flush().unwrap();
806 }
807 _ => {}
454 } 808 }
455 } 809 }
456 KeyCode::Char(c) => {
457 input.push(c);
458 print!("{}", c);
459 stdout().flush().unwrap();
460 }
461 _ => {}
462 }
463 }
464 } 810 }
465 _ => {} 811 _ => {}
466 } 812 }
@@ -472,36 +818,40 @@ fn read_password(prompt: &str) -> String {
472 execute!(stdout(), Show).unwrap(); 818 execute!(stdout(), Show).unwrap();
473 print!("{}", prompt); 819 print!("{}", prompt);
474 stdout().flush().unwrap(); 820 stdout().flush().unwrap();
475 821
476 let mut input = String::new(); 822 let mut input = String::new();
477 let prompt_len = prompt.len(); 823 let prompt_len = prompt.len();
478 824
479 loop { 825 loop {
480 match read().unwrap() { 826 match read().unwrap() {
481 Event::Key(key) => { 827 Event::Key(key) => {
482 if key.kind == crossterm::event::KeyEventKind::Press { 828 if key.kind == crossterm::event::KeyEventKind::Press {
483 match key.code { 829 match key.code {
484 KeyCode::Enter => { 830 KeyCode::Enter => {
485 println!(); 831 println!();
486 break; 832 break;
487 } 833 }
488 KeyCode::Backspace => { 834 KeyCode::Backspace => {
489 if !input.is_empty() { 835 if !input.is_empty() {
490 input.pop(); 836 input.pop();
491 // Move cursor back and clear from current position to end of line 837 // Move cursor back and clear from current position to end of line
492 execute!(stdout(), MoveToColumn((prompt_len + input.len() + 1) as u16), 838 execute!(
493 Clear(ClearType::UntilNewLine)).unwrap(); 839 stdout(),
494 stdout().flush().unwrap(); 840 MoveToColumn((prompt_len + input.len() + 1) as u16),
841 Clear(ClearType::UntilNewLine)
842 )
843 .unwrap();
844 stdout().flush().unwrap();
845 }
846 }
847 KeyCode::Char(c) => {
848 input.push(c);
849 print!("*");
850 stdout().flush().unwrap();
851 }
852 _ => {}
495 } 853 }
496 } 854 }
497 KeyCode::Char(c) => {
498 input.push(c);
499 print!("*");
500 stdout().flush().unwrap();
501 }
502 _ => {}
503 }
504 }
505 } 855 }
506 _ => {} 856 _ => {}
507 } 857 }
diff --git a/src/bin/client/tls.rs b/src/bin/client/tls.rs
new file mode 100644
index 0000000..8ab5c7c
--- /dev/null
+++ b/src/bin/client/tls.rs
@@ -0,0 +1,150 @@
1use std::io::{Read, Write};
2use std::net::TcpStream;
3use std::sync::mpsc;
4use std::thread;
5use std::time::{Duration, Instant};
6
7pub struct TlsConnector {
8 connector: rustls::ClientConnection,
9 server_name: String,
10}
11
12impl TlsConnector {
13 pub fn new(server_name: &str) -> Result<Self, Box<dyn std::error::Error>> {
14 let config = rustls::ClientConfig::builder()
15 .dangerous()
16 .with_custom_certificate_verifier(std::sync::Arc::new(InsecureVerifier))
17 .with_no_client_auth();
18
19 let connector =
20 rustls::ClientConnection::new(std::sync::Arc::new(config), server_name.try_into()?);
21
22 Ok(Self {
23 connector,
24 server_name: server_name.to_string(),
25 })
26 }
27
28 pub fn connect(
29 &self,
30 stream: TcpStream,
31 ) -> Result<TlsClientStream, Box<dyn std::error::Error>> {
32 let mut conn = rustls::ClientConnection::new(
33 std::sync::Arc::new(
34 rustls::ClientConfig::builder()
35 .dangerous()
36 .with_custom_verifier(rustls::client::danger::ServerCertVerifier::DEFAULT)
37 .with_no_client_auth(),
38 ),
39 self.server_name.clone().try_into()?,
40 )?;
41
42 conn.complete_io(&mut std::io::BufReader::new(&stream))?;
43
44 Ok(TlsClientStream {
45 stream,
46 conn: std::sync::Arc::new(conn),
47 })
48 }
49}
50
51struct InsecureVerifier;
52
53impl rustls::client::danger::ServerCertVerifier for InsecureVerifier {
54 fn verify_server_cert(
55 &self,
56 _end_entity: &rustls::pki_types::CertificateDer<'_>,
57 _intermediates: &[rustls::pki_types::CertificateDer<'_>],
58 _server_name: &rustls::pki_types::ServerName<'_>,
59 _ocsp_response: &[u8],
60 _now: std::time::SystemTime,
61 ) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
62 Ok(rustls::client::danger::ServerCertVerified::assertion())
63 }
64
65 fn verify_tls12_signature(
66 &self,
67 _message: &[u8],
68 _cert: &rustls::pki_types::CertificateDer<'_>,
69 _dss: &rustls::DigitallySignedStruct,
70 ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
71 Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
72 }
73
74 fn verify_tls13_signature(
75 &self,
76 _message: &[u8],
77 _cert: &rustls::pki_types::CertificateDer<'_>,
78 _dss: &rustls::DigitallySignedStruct,
79 ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
80 Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
81 }
82
83 fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
84 vec![
85 rustls::SignatureScheme::RSA_PKCS1_SHA256,
86 rustls::SignatureScheme::RSA_PKCS1_SHA384,
87 rustls::SignatureScheme::RSA_PKCS1_SHA512,
88 rustls::SignatureScheme::RSA_PSS_SHA256,
89 rustls::SignatureScheme::RSA_PSS_SHA384,
90 rustls::SignatureScheme::RSA_PSS_SHA512,
91 rustls::SignatureScheme::ECDSA_NISTP256_SHA256,
92 rustls::SignatureScheme::ECDSA_NISTP384_SHA384,
93 rustls::SignatureScheme::ECDSA_NISTP521_SHA512,
94 rustls::SignatureScheme::RSA_PSS_SHA256_LEGACY,
95 rustls::SignatureScheme::RSA_PSS_SHA384_LEGACY,
96 rustls::SignatureScheme::RSA_PSS_SHA512_LEGACY,
97 ]
98 }
99}
100
101pub struct TlsClientStream {
102 stream: TcpStream,
103 conn: std::sync::Arc<rustls::ClientConnection>,
104}
105
106impl TlsClientStream {
107 pub fn read_line(&mut self, buf: &mut String) -> std::io::Result<usize> {
108 let mut byte_buf = [0u8; 1024];
109 loop {
110 let mut tls_reader = std::io::BufReader::new(self.conn.clone());
111 let n = tls_reader.read(&mut byte_buf)?;
112 if n == 0 {
113 return Ok(0);
114 }
115 let chunk = std::str::from_utf8(&byte_buf[..n]).unwrap_or("");
116 buf.push_str(chunk);
117 if buf.contains('\n') {
118 return Ok(n);
119 }
120 }
121 }
122
123 pub fn write_line(&mut self, msg: &str) -> std::io::Result<()> {
124 use std::io::Write;
125 self.conn.writer().write_all(msg.as_bytes())?;
126 self.conn.writer().write_all(b"\n")?;
127 self.conn.writer().flush()?;
128 Ok(())
129 }
130
131 pub fn try_clone(&self) -> std::io::Result<TlsClientStream> {
132 let new_stream = self.stream.try_clone()?;
133 let mut conn = rustls::ClientConnection::new(
134 std::sync::Arc::new(
135 rustls::ClientConfig::builder()
136 .dangerous()
137 .with_custom_verifier(rustls::client::danger::ServerCertVerifier::DEFAULT)
138 .with_no_client_auth(),
139 ),
140 self.server_name.clone().try_into().unwrap(),
141 )?;
142
143 conn.complete_io(&mut std::io::BufReader::new(&new_stream))?;
144
145 Ok(TlsClientStream {
146 stream: new_stream,
147 conn: std::sync::Arc::new(conn),
148 })
149 }
150}
diff --git a/src/bin/server/args.rs b/src/bin/server/args.rs
index 31c0acc..fc844ea 100644
--- a/src/bin/server/args.rs
+++ b/src/bin/server/args.rs
@@ -1,12 +1,14 @@
1pub struct Args { 1pub struct Args {
2 pub ip: String, 2 pub ip: String,
3 pub port: u16, 3 pub port: u16,
4 pub unsafe_mode: bool,
4} 5}
5 6
6pub fn parse_args() -> Args { 7pub fn parse_args() -> Args {
7 let args: Vec<String> = std::env::args().collect(); 8 let args: Vec<String> = std::env::args().collect();
8 let mut ip = "127.0.0.1".to_string(); 9 let mut ip = "127.0.0.1".to_string();
9 let mut port = 11525; 10 let mut port = 11525;
11 let mut unsafe_mode = false;
10 let mut i = 1; 12 let mut i = 1;
11 while i < args.len() { 13 while i < args.len() {
12 if args[i] == "-p" { 14 if args[i] == "-p" {
@@ -32,10 +34,17 @@ pub fn parse_args() -> Args {
32 eprintln!("-i requires an IP address"); 34 eprintln!("-i requires an IP address");
33 std::process::exit(1); 35 std::process::exit(1);
34 } 36 }
37 } else if args[i] == "--unsafe" {
38 unsafe_mode = true;
39 i += 1;
35 } else { 40 } else {
36 eprintln!("Unknown argument: {}", args[i]); 41 eprintln!("Unknown argument: {}", args[i]);
37 std::process::exit(1); 42 std::process::exit(1);
38 } 43 }
39 } 44 }
40 Args { ip, port } 45 Args {
46 ip,
47 port,
48 unsafe_mode,
49 }
41} 50}
diff --git a/src/bin/server/database.rs b/src/bin/server/database.rs
index 0fac387..1653071 100644
--- a/src/bin/server/database.rs
+++ b/src/bin/server/database.rs
@@ -11,14 +11,22 @@ pub fn init_db() -> Connection {
11 balance INTEGER DEFAULT 1000 11 balance INTEGER DEFAULT 1000
12 )", 12 )",
13 [], 13 [],
14 ).unwrap(); 14 )
15 .unwrap();
15 // Add column if not exists (for migration) 16 // Add column if not exists (for migration)
16 let _ = conn.execute("ALTER TABLE users ADD COLUMN account_id INTEGER UNIQUE", []); 17 let _ = conn.execute("ALTER TABLE users ADD COLUMN account_id INTEGER UNIQUE", []);
17 let _ = conn.execute("ALTER TABLE users ADD COLUMN balance INTEGER DEFAULT 1000", []); 18 let _ = conn.execute(
19 "ALTER TABLE users ADD COLUMN balance INTEGER DEFAULT 1000",
20 [],
21 );
18 conn 22 conn
19} 23}
20 24
21pub fn add_user(conn: &Connection, username: &str, hashed_password: &str) -> Result<(), Box<dyn std::error::Error>> { 25pub fn add_user(
26 conn: &Connection,
27 username: &str,
28 hashed_password: &str,
29) -> Result<(), Box<dyn std::error::Error>> {
22 loop { 30 loop {
23 let account_id: u32 = rand::random::<u32>(); 31 let account_id: u32 = rand::random::<u32>();
24 match conn.execute( 32 match conn.execute(
@@ -33,7 +41,9 @@ pub fn add_user(conn: &Connection, username: &str, hashed_password: &str) -> Res
33} 41}
34 42
35pub fn get_hashed_password(conn: &Connection, username: &str) -> Option<String> { 43pub fn get_hashed_password(conn: &Connection, username: &str) -> Option<String> {
36 let mut stmt = conn.prepare("SELECT hashed_password FROM users WHERE username = ?1").unwrap(); 44 let mut stmt = conn
45 .prepare("SELECT hashed_password FROM users WHERE username = ?1")
46 .unwrap();
37 let mut rows = stmt.query([username]).unwrap(); 47 let mut rows = stmt.query([username]).unwrap();
38 if let Ok(Some(row)) = rows.next() { 48 if let Ok(Some(row)) = rows.next() {
39 row.get(0).ok() 49 row.get(0).ok()
@@ -43,7 +53,9 @@ pub fn get_hashed_password(conn: &Connection, username: &str) -> Option<String>
43} 53}
44 54
45pub fn get_account_id(conn: &Connection, username: &str) -> Option<u32> { 55pub fn get_account_id(conn: &Connection, username: &str) -> Option<u32> {
46 let mut stmt = conn.prepare("SELECT account_id FROM users WHERE username = ?1").unwrap(); 56 let mut stmt = conn
57 .prepare("SELECT account_id FROM users WHERE username = ?1")
58 .unwrap();
47 let mut rows = stmt.query([username]).unwrap(); 59 let mut rows = stmt.query([username]).unwrap();
48 if let Ok(Some(row)) = rows.next() { 60 if let Ok(Some(row)) = rows.next() {
49 Some(row.get::<_, u32>(0).ok()?) 61 Some(row.get::<_, u32>(0).ok()?)
@@ -53,7 +65,9 @@ pub fn get_account_id(conn: &Connection, username: &str) -> Option<u32> {
53} 65}
54 66
55pub fn get_balance(conn: &Connection, username: &str) -> Option<i64> { 67pub fn get_balance(conn: &Connection, username: &str) -> Option<i64> {
56 let mut stmt = conn.prepare("SELECT balance FROM users WHERE username = ?1").unwrap(); 68 let mut stmt = conn
69 .prepare("SELECT balance FROM users WHERE username = ?1")
70 .unwrap();
57 let mut rows = stmt.query([username]).unwrap(); 71 let mut rows = stmt.query([username]).unwrap();
58 if let Ok(Some(row)) = rows.next() { 72 if let Ok(Some(row)) = rows.next() {
59 Some(row.get::<_, i64>(0).ok()?) 73 Some(row.get::<_, i64>(0).ok()?)
diff --git a/src/bin/server/games/mod.rs b/src/bin/server/games/mod.rs
new file mode 100644
index 0000000..4c62760
--- /dev/null
+++ b/src/bin/server/games/mod.rs
@@ -0,0 +1,4 @@
1pub mod registry;
2pub mod session;
3pub mod slots;
4pub mod traits;
diff --git a/src/bin/server/games/registry.rs b/src/bin/server/games/registry.rs
new file mode 100644
index 0000000..e77a138
--- /dev/null
+++ b/src/bin/server/games/registry.rs
@@ -0,0 +1,55 @@
1#![allow(dead_code)]
2
3use crate::games::traits::Game;
4use std::collections::HashMap;
5use std::sync::{Arc, Mutex};
6
7pub type GameRef = Arc<dyn Game + Send + Sync>;
8
9pub struct GameRegistry {
10 games: Arc<Mutex<HashMap<String, GameRef>>>,
11}
12
13impl GameRegistry {
14 pub fn new() -> Self {
15 Self {
16 games: Arc::new(Mutex::new(HashMap::new())),
17 }
18 }
19
20 pub fn register_game<G: Game + Send + Sync + 'static>(&self, game: G) {
21 let game_type = game.game_type().to_string();
22 let mut games = self.games.lock().unwrap();
23 games.insert(game_type, Arc::new(game));
24 }
25
26 pub fn get_game(&self, game_type: &str) -> Option<GameRef> {
27 let games = self.games.lock().unwrap();
28 games.get(game_type).cloned()
29 }
30
31 pub fn list_games(&self) -> Vec<String> {
32 let games = self.games.lock().unwrap();
33 games.keys().cloned().collect()
34 }
35
36 pub fn game_exists(&self, game_type: &str) -> bool {
37 let games = self.games.lock().unwrap();
38 games.contains_key(game_type)
39 }
40}
41
42// Global registry instance
43use crate::games::slots::SlotsGame;
44use once_cell::sync::Lazy;
45
46static GAME_REGISTRY: Lazy<GameRegistry> = Lazy::new(|| {
47 let registry = GameRegistry::new();
48 // Register games here
49 registry.register_game(SlotsGame::new());
50 registry
51});
52
53pub fn get_game_registry() -> &'static GameRegistry {
54 &GAME_REGISTRY
55}
diff --git a/src/bin/server/games/session.rs b/src/bin/server/games/session.rs
new file mode 100644
index 0000000..595a745
--- /dev/null
+++ b/src/bin/server/games/session.rs
@@ -0,0 +1,77 @@
1#![allow(dead_code)]
2
3use crate::games::traits::{GameError, GameState};
4use std::collections::HashMap;
5use std::sync::{Arc, Mutex};
6use uuid::Uuid;
7
8#[derive(Debug, Clone)]
9pub struct GameSession {
10 pub session_id: String,
11 pub username: String,
12 pub game_type: String,
13 pub bet_amount: u64,
14 pub state: GameState,
15}
16
17pub struct SessionManager {
18 sessions: Arc<Mutex<HashMap<String, GameSession>>>,
19}
20
21impl SessionManager {
22 pub fn new() -> Self {
23 Self {
24 sessions: Arc::new(Mutex::new(HashMap::new())),
25 }
26 }
27
28 pub fn create_session(&self, username: &str, game_type: String, state: GameState) -> String {
29 let session_id = Uuid::new_v4().to_string();
30 let session = GameSession {
31 session_id: session_id.clone(),
32 username: username.to_string(),
33 game_type,
34 bet_amount: state.bet_amount,
35 state,
36 };
37
38 let mut sessions = self.sessions.lock().unwrap();
39 sessions.insert(session_id.clone(), session);
40 session_id
41 }
42
43 pub fn get_session(&self, session_id: &str) -> Result<GameSession, GameError> {
44 let sessions = self.sessions.lock().unwrap();
45 sessions
46 .get(session_id)
47 .cloned()
48 .ok_or(GameError::SessionNotFound)
49 }
50
51 pub fn update_session(&self, session_id: &str, new_state: GameState) -> Result<(), GameError> {
52 let mut sessions = self.sessions.lock().unwrap();
53 if let Some(session) = sessions.get_mut(session_id) {
54 session.state = new_state;
55 Ok(())
56 } else {
57 Err(GameError::SessionNotFound)
58 }
59 }
60
61 pub fn remove_session(&self, session_id: &str) -> Result<(), GameError> {
62 let mut sessions = self.sessions.lock().unwrap();
63 sessions
64 .remove(session_id)
65 .ok_or(GameError::SessionNotFound)
66 .map(|_| ())
67 }
68
69 pub fn get_user_sessions(&self, username: &str) -> Vec<GameSession> {
70 let sessions = self.sessions.lock().unwrap();
71 sessions
72 .values()
73 .filter(|s| s.username == username)
74 .cloned()
75 .collect()
76 }
77}
diff --git a/src/bin/server/games/slots.rs b/src/bin/server/games/slots.rs
new file mode 100644
index 0000000..d84e21f
--- /dev/null
+++ b/src/bin/server/games/slots.rs
@@ -0,0 +1,179 @@
1use crate::games::traits::{Game, GameError, GamePhase, GameResult, GameState};
2use rand::Rng;
3use std::collections::HashMap;
4
5pub struct SlotsGame {
6 symbols: Vec<char>,
7 payouts: HashMap<String, u64>,
8}
9
10impl SlotsGame {
11 pub fn new() -> Self {
12 let symbols = vec!['C', 'L', 'O', 'G', 'D', 'S'];
13 let mut payouts = HashMap::new();
14
15 payouts.insert("CCC".to_string(), 5);
16 payouts.insert("LLL".to_string(), 10);
17 payouts.insert("OOO".to_string(), 15);
18 payouts.insert("GGG".to_string(), 20);
19 payouts.insert("DDD".to_string(), 50);
20 payouts.insert("SSS".to_string(), 100);
21
22 payouts.insert("CC".to_string(), 2);
23 payouts.insert("LL".to_string(), 3);
24 payouts.insert("OO".to_string(), 4);
25 payouts.insert("GG".to_string(), 5);
26 payouts.insert("DD".to_string(), 10);
27 payouts.insert("SS".to_string(), 20);
28
29 Self { symbols, payouts }
30 }
31
32 fn spin_reels(&self) -> Vec<char> {
33 let mut rng = rand::thread_rng();
34 (0..3)
35 .map(|_| self.symbols[rng.gen_range(0..self.symbols.len())])
36 .collect()
37 }
38
39 fn calculate_payout(&self, reels: &[char], bet: u64) -> u64 {
40 let reel_string: String = reels.iter().collect();
41
42 if let Some(&multiplier) = self.payouts.get(&reel_string) {
43 return bet * multiplier;
44 }
45
46 for i in 0..2 {
47 if reels[i] == reels[i + 1] {
48 let two_of_kind = format!("{}{}", reels[i], reels[i]);
49 if let Some(&multiplier) = self.payouts.get(&two_of_kind) {
50 return bet * multiplier;
51 }
52 }
53 }
54
55 0
56 }
57}
58
59impl Game for SlotsGame {
60 fn game_type(&self) -> &'static str {
61 "slots"
62 }
63
64 fn get_name(&self) -> &'static str {
65 "Slots"
66 }
67
68 fn get_description(&self) -> &'static str {
69 "Spin the reels and match symbols to win!"
70 }
71
72 fn prepare_state(&self, username: &str, bet_amount: u64) -> GameState {
73 let mut data = HashMap::new();
74 data.insert("username".to_string(), username.to_string());
75 data.insert("status".to_string(), "ready".to_string());
76
77 GameState {
78 session_id: String::new(),
79 game_type: "slots".to_string(),
80 bet_amount,
81 phase: GamePhase::WaitingToStart,
82 data,
83 }
84 }
85
86 fn process_action(
87 &self,
88 state: &GameState,
89 action: &str,
90 _params: &[&str],
91 ) -> Result<GameState, GameError> {
92 if state.phase == GamePhase::Finished {
93 return Err(GameError::GameOver);
94 }
95
96 match action {
97 "spin" => {
98 if state.phase != GamePhase::WaitingToStart && state.phase != GamePhase::Playing {
99 return Err(GameError::InvalidAction(
100 "Cannot spin in current phase".to_string(),
101 ));
102 }
103
104 let reels = self.spin_reels();
105 let reel_string: String = reels.iter().collect();
106
107 let mut new_data = state.data.clone();
108 new_data.insert("reels".to_string(), reel_string.clone());
109 new_data.insert("status".to_string(), "spun".to_string());
110
111 let winnings = self.calculate_payout(&reels, state.bet_amount);
112 new_data.insert("winnings".to_string(), winnings.to_string());
113
114 let is_jackpot = reels[0] == reels[1] && reels[1] == reels[2];
115 let is_two = reels[0] == reels[1] || reels[1] == reels[2];
116 new_data.insert(
117 "win_type".to_string(),
118 if is_jackpot {
119 "jackpot".to_string()
120 } else if is_two {
121 "two".to_string()
122 } else {
123 "none".to_string()
124 },
125 );
126
127 Ok(GameState {
128 session_id: state.session_id.clone(),
129 game_type: state.game_type.clone(),
130 bet_amount: state.bet_amount,
131 phase: GamePhase::Finished,
132 data: new_data,
133 })
134 }
135 _ => Err(GameError::InvalidAction(format!(
136 "Unknown action: {}",
137 action
138 ))),
139 }
140 }
141
142 fn calculate_result(&self, state: &GameState) -> Result<GameResult, GameError> {
143 if state.phase != GamePhase::Finished {
144 return Err(GameError::InternalError("Game not finished".to_string()));
145 }
146
147 let reel_string = state
148 .data
149 .get("reels")
150 .ok_or(GameError::InternalError("No reels found".to_string()))?;
151
152 let reels: Vec<char> = reel_string.chars().collect();
153 let winnings = self.calculate_payout(&reels, state.bet_amount);
154
155 let mut result_data = HashMap::new();
156 result_data.insert("reels".to_string(), reel_string.clone());
157 result_data.insert(
158 "payout_multiplier".to_string(),
159 if winnings > 0 && state.bet_amount > 0 {
160 (winnings / state.bet_amount).to_string()
161 } else {
162 "0".to_string()
163 },
164 );
165 result_data.insert(
166 "win_type".to_string(),
167 state.data.get("win_type").cloned().unwrap_or_default(),
168 );
169
170 Ok(GameResult {
171 session_id: state.session_id.clone(),
172 game_type: state.game_type.clone(),
173 bet_amount: state.bet_amount,
174 winnings,
175 final_balance: 0,
176 result_data,
177 })
178 }
179}
diff --git a/src/bin/server/games/traits.rs b/src/bin/server/games/traits.rs
new file mode 100644
index 0000000..d56b334
--- /dev/null
+++ b/src/bin/server/games/traits.rs
@@ -0,0 +1,67 @@
1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
5pub enum GameError {
6 InvalidAction(String),
7 InsufficientBalance,
8 InvalidBet,
9 GameOver,
10 SessionNotFound,
11 InternalError(String),
12}
13
14#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
15pub enum GamePhase {
16 WaitingToStart,
17 Playing,
18 Finished,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct GameState {
23 pub session_id: String,
24 pub game_type: String,
25 pub bet_amount: u64,
26 pub phase: GamePhase,
27 pub data: HashMap<String, String>,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct GameResult {
32 pub session_id: String,
33 pub game_type: String,
34 pub bet_amount: u64,
35 pub winnings: u64,
36 pub final_balance: u64,
37 pub result_data: HashMap<String, String>,
38}
39
40pub trait Game: Send + Sync {
41 fn game_type(&self) -> &'static str;
42
43 fn get_name(&self) -> &'static str;
44
45 fn get_description(&self) -> &'static str;
46
47 fn validate_bet(&self, bet_amount: u64, user_balance: u64) -> Result<(), GameError> {
48 if bet_amount == 0 {
49 return Err(GameError::InvalidBet);
50 }
51 if bet_amount > user_balance {
52 return Err(GameError::InsufficientBalance);
53 }
54 Ok(())
55 }
56
57 fn prepare_state(&self, username: &str, bet_amount: u64) -> GameState;
58
59 fn process_action(
60 &self,
61 state: &GameState,
62 action: &str,
63 params: &[&str],
64 ) -> Result<GameState, GameError>;
65
66 fn calculate_result(&self, state: &GameState) -> Result<GameResult, GameError>;
67}
diff --git a/src/bin/server/handler.rs b/src/bin/server/handler.rs
index 01fd9c9..867910e 100644
--- a/src/bin/server/handler.rs
+++ b/src/bin/server/handler.rs
@@ -1,12 +1,16 @@
1use crate::database;
2use crate::games::registry::get_game_registry;
3use crate::games::traits::GameError;
4use crate::tls::TlsStream;
5use once_cell::sync::Lazy;
6use rusqlite::Connection;
1use std::io::{BufRead, BufReader, Write}; 7use std::io::{BufRead, BufReader, Write};
2use std::net::TcpStream; 8use std::net::TcpStream;
3use std::sync::Mutex; 9use std::sync::Mutex;
4use once_cell::sync::Lazy;
5use rusqlite::Connection;
6use crate::database;
7 10
8static DB_CONN: Lazy<Mutex<Connection>> = Lazy::new(|| Mutex::new(database::init_db())); 11static DB_CONN: Lazy<Mutex<Connection>> = Lazy::new(|| Mutex::new(database::init_db()));
9 12
13#[allow(dead_code)]
10pub fn handle_client(stream: TcpStream) { 14pub fn handle_client(stream: TcpStream) {
11 let client_addr = stream.peer_addr().unwrap().to_string(); 15 let client_addr = stream.peer_addr().unwrap().to_string();
12 let mut user = "guest".to_string(); 16 let mut user = "guest".to_string();
@@ -25,8 +29,49 @@ pub fn handle_client(stream: TcpStream) {
25} 29}
26 30
27fn process_message(msg: &str, user: &mut String) -> String { 31fn process_message(msg: &str, user: &mut String) -> String {
28 let parts: Vec<&str> = msg.splitn(3, ' ').collect(); 32 let parts: Vec<&str> = msg.splitn(5, ' ').collect();
29 match parts[0] { 33 match parts[0] {
34 "GAME_LIST" => {
35 let registry = get_game_registry();
36 let games: Vec<String> = registry.list_games();
37 if games.is_empty() {
38 "ERROR No games available".to_string()
39 } else {
40 let game_info: Vec<String> = games
41 .iter()
42 .filter_map(|gt| {
43 registry
44 .get_game(gt)
45 .map(|g| format!("{}:{}", gt, g.get_name()))
46 })
47 .collect();
48 format!("GAME_LIST {}", game_info.join(","))
49 }
50 }
51 "GAME_INFO" => {
52 if parts.len() != 2 {
53 "ERROR Invalid GAME_INFO format. Usage: GAME_INFO <game_type>".to_string()
54 } else {
55 let registry = get_game_registry();
56 match registry.get_game(parts[1]) {
57 Some(game) => format!(
58 "GAME_INFO {} {} {}",
59 parts[1],
60 game.get_name(),
61 game.get_description()
62 ),
63 None => format!("ERROR Unknown game type: {}", parts[1]),
64 }
65 }
66 }
67 "PLAY_START" => {
68 if parts.len() != 3 {
69 "ERROR Invalid PLAY_START format. Usage: PLAY_START <game_type> <bet_amount>"
70 .to_string()
71 } else {
72 handle_play_start(user, parts[1], parts[2])
73 }
74 }
30 "REGISTER" => { 75 "REGISTER" => {
31 if parts.len() != 3 { 76 if parts.len() != 3 {
32 "ERROR Invalid REGISTER format".to_string() 77 "ERROR Invalid REGISTER format".to_string()
@@ -65,13 +110,133 @@ fn process_message(msg: &str, user: &mut String) -> String {
65 "ERROR Not logged in".to_string() 110 "ERROR Not logged in".to_string()
66 } else { 111 } else {
67 let db = DB_CONN.lock().unwrap(); 112 let db = DB_CONN.lock().unwrap();
68 if let (Some(id), Some(balance)) = (database::get_account_id(&db, user), database::get_balance(&db, user)) { 113 if let (Some(id), Some(balance)) = (
114 database::get_account_id(&db, user),
115 database::get_balance(&db, user),
116 ) {
69 format!("INFO {} {} {}", user, id, balance) 117 format!("INFO {} {} {}", user, id, balance)
70 } else { 118 } else {
71 "ERROR Error retrieving user info".to_string() 119 "ERROR Error retrieving user info".to_string()
72 } 120 }
73 } 121 }
74 } 122 }
75 _ => msg.to_string(), // Echo back for compatibility 123 _ => msg.to_string(),
124 }
125}
126
127fn handle_play_start(user: &str, game_type: &str, bet_amount_str: &str) -> String {
128 if user == "guest" {
129 return "ERROR Not logged in".to_string();
130 }
131
132 let bet_amount: u64 = match bet_amount_str.parse() {
133 Ok(amount) => amount,
134 Err(_) => return "ERROR Invalid bet amount".to_string(),
135 };
136
137 let db = DB_CONN.lock().unwrap();
138 let current_balance = match database::get_balance(&db, user) {
139 Some(balance) => balance as u64,
140 None => return "ERROR Could not retrieve balance".to_string(),
141 };
142
143 let registry = get_game_registry();
144 let game = match registry.get_game(game_type) {
145 Some(game) => game,
146 None => return format!("ERROR Unknown game type: {}", game_type),
147 };
148
149 if let Err(e) = game.validate_bet(bet_amount, current_balance) {
150 return match e {
151 GameError::InsufficientBalance => "ERROR Insufficient balance".to_string(),
152 GameError::InvalidBet => "ERROR Invalid bet amount".to_string(),
153 _ => "ERROR Bet validation failed".to_string(),
154 };
155 }
156
157 let game_state = game.prepare_state(user, bet_amount);
158
159 drop(db);
160 let db = DB_CONN.lock().unwrap();
161 if let Err(_) = db.execute(
162 "UPDATE users SET balance = balance - ?1 WHERE username = ?2",
163 (bet_amount as i64, user),
164 ) {
165 return "ERROR Failed to deduct bet".to_string();
166 }
167
168 let new_state = match game.process_action(&game_state, "spin", &[]) {
169 Ok(state) => state,
170 Err(e) => {
171 let _ = db.execute(
172 "UPDATE users SET balance = balance + ?1 WHERE username = ?2",
173 (bet_amount as i64, user),
174 );
175 return format!("ERROR Failed to spin: {:?}", e);
176 }
177 };
178
179 let result = match game.calculate_result(&new_state) {
180 Ok(result) => result,
181 Err(e) => {
182 let _ = db.execute(
183 "UPDATE users SET balance = balance + ?1 WHERE username = ?2",
184 (bet_amount as i64, user),
185 );
186 return format!("ERROR Failed to calculate result: {:?}", e);
187 }
188 };
189
190 let total_return = result.bet_amount + result.winnings;
191 if let Err(_) = db.execute(
192 "UPDATE users SET balance = balance + ?1 WHERE username = ?2",
193 (total_return as i64, user),
194 ) {
195 return "ERROR Failed to update balance".to_string();
196 }
197
198 let new_balance = match database::get_balance(&db, user) {
199 Some(balance) => balance,
200 None => return "ERROR Could not retrieve new balance".to_string(),
201 };
202
203 let reels = new_state.data.get("reels").cloned().unwrap_or_default();
204 let win_type = new_state.data.get("win_type").cloned().unwrap_or_default();
205
206 format!(
207 "GAME_RESULT {} {} {} {} {} {}",
208 result.session_id, result.winnings, result.bet_amount, new_balance, reels, win_type
209 )
210}
211
212pub fn handle_client_tls(mut stream: TlsStream) {
213 let client_addr = stream.peer_addr().unwrap().to_string();
214 let mut user = "guest".to_string();
215 println!("[{} @ {}] TLS Client connected", user, client_addr);
216
217 loop {
218 let mut line = String::new();
219 match stream.read_line(&mut line) {
220 Ok(0) => break,
221 Ok(_) => {}
222 Err(e) => {
223 eprintln!("Read error: {}", e);
224 break;
225 }
226 }
227
228 let line = line.trim();
229 if line.is_empty() {
230 continue;
231 }
232
233 println!("[{} @ {}] Received: {}", user, client_addr, line);
234 let response = process_message(line, &mut user);
235 if let Err(e) = stream.write_line(&response) {
236 eprintln!("Write error: {}", e);
237 break;
238 }
239 println!("[{} @ {}] Sent: {}", user, client_addr, response);
76 } 240 }
241 println!("[{} @ {}] TLS Client disconnected", user, client_addr);
77} 242}
diff --git a/src/bin/server/main.rs b/src/bin/server/main.rs
index 7693069..4328559 100644
--- a/src/bin/server/main.rs
+++ b/src/bin/server/main.rs
@@ -1,8 +1,11 @@
1mod args; 1mod args;
2mod handler;
3mod database; 2mod database;
3mod games;
4mod handler;
5mod tls;
4 6
5use std::net::TcpListener; 7use std::net::TcpListener;
8use std::sync::Arc;
6use std::thread; 9use std::thread;
7 10
8fn main() { 11fn main() {
@@ -14,11 +17,34 @@ fn main() {
14 std::process::exit(1); 17 std::process::exit(1);
15 } 18 }
16 }; 19 };
17 println!("Server listening on {}", listener.local_addr().unwrap()); 20
21 let tls = match tls::TlsServer::new("identity.p12", "key.pem", args.unsafe_mode) {
22 Ok(t) => Arc::new(t),
23 Err(e) => {
24 eprintln!("Failed to initialize TLS: {}", e);
25 std::process::exit(1);
26 }
27 };
28
29 println!(
30 "Server listening on {} (TLS enabled){}",
31 listener.local_addr().unwrap(),
32 if args.unsafe_mode {
33 " [UNSAFE MODE]"
34 } else {
35 ""
36 }
37 );
18 for stream in listener.incoming() { 38 for stream in listener.incoming() {
19 let stream = stream.unwrap(); 39 let stream = stream.unwrap();
20 thread::spawn(move || { 40 let tls = tls.clone();
21 handler::handle_client(stream); 41 thread::spawn(move || match tls.accept(stream) {
42 Ok(tls_stream) => {
43 handler::handle_client_tls(tls_stream);
44 }
45 Err(e) => {
46 eprintln!("TLS handshake failed: {}", e);
47 }
22 }); 48 });
23 } 49 }
24} 50}
diff --git a/src/bin/server/tls.rs b/src/bin/server/tls.rs
new file mode 100644
index 0000000..acf3315
--- /dev/null
+++ b/src/bin/server/tls.rs
@@ -0,0 +1,96 @@
1use std::io::{Read, Write};
2use std::net::TcpStream;
3
4#[derive(Clone)]
5pub struct TlsServer {
6 acceptor: native_tls::TlsAcceptor,
7}
8
9impl TlsServer {
10 pub fn new(
11 cert_path: &str,
12 _key_path: &str,
13 unsafe_mode: bool,
14 ) -> Result<Self, Box<dyn std::error::Error>> {
15 let cert_data = if unsafe_mode {
16 if std::path::Path::new(cert_path).exists() {
17 std::fs::read(cert_path)?
18 } else {
19 return Err(format!(
20 "Certificate file '{}' not found. In --unsafe mode, you must provide a valid PKCS12 certificate file.\n\
21 You can generate one with:\n\
22 openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes\n\
23 openssl pkcs12 -export -out identity.p12 -inkey key.pem -in cert.pem",
24 cert_path
25 ).into());
26 }
27 } else {
28 std::fs::read(cert_path)?
29 };
30
31 let identity = native_tls::Identity::from_pkcs12(&cert_data, "")?;
32 let acceptor = native_tls::TlsAcceptor::new(identity)?;
33
34 Ok(Self { acceptor })
35 }
36
37 pub fn accept(&self, stream: TcpStream) -> Result<TlsStream, Box<dyn std::error::Error>> {
38 let stream = self.acceptor.accept(stream)?;
39 Ok(TlsStream::new(stream))
40 }
41}
42
43pub struct TlsStream {
44 stream: native_tls::TlsStream<TcpStream>,
45}
46
47impl TlsStream {
48 pub fn new(stream: native_tls::TlsStream<TcpStream>) -> Self {
49 Self { stream }
50 }
51
52 pub fn read_line(&mut self, s: &mut String) -> std::io::Result<usize> {
53 let mut buf = [0u8; 1024];
54 loop {
55 match self.stream.read(&mut buf) {
56 Ok(0) => return Ok(0),
57 Ok(n) => {
58 let chunk = std::str::from_utf8(&buf[..n]).unwrap_or("");
59 s.push_str(chunk);
60 if s.contains('\n') {
61 return Ok(n);
62 }
63 }
64 Err(e) => return Err(e),
65 }
66 }
67 }
68
69 pub fn write_line(&mut self, msg: &str) -> std::io::Result<()> {
70 use std::io::Write;
71 self.stream.write_all(msg.as_bytes())?;
72 self.stream.write_all(b"\n")?;
73 self.stream.flush()?;
74 Ok(())
75 }
76
77 pub fn peer_addr(&self) -> std::io::Result<std::net::SocketAddr> {
78 self.stream.get_ref().peer_addr()
79 }
80}
81
82impl Read for TlsStream {
83 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
84 self.stream.read(buf)
85 }
86}
87
88impl Write for TlsStream {
89 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
90 self.stream.write(buf)
91 }
92
93 fn flush(&mut self) -> std::io::Result<()> {
94 self.stream.flush()
95 }
96}
diff --git a/src/bin/windows-test/main.rs b/src/bin/windows-test/main.rs
deleted file mode 100644
index b8f84fe..0000000
--- a/src/bin/windows-test/main.rs
+++ /dev/null
@@ -1,61 +0,0 @@
1use std::io::{self, Write};
2use std::ptr;
3use windows_sys::Win32::Foundation::HANDLE;
4use windows_sys::Win32::System::Console::{
5 GetConsoleMode, ReadConsoleW, SetConsoleMode, GetStdHandle, STD_INPUT_HANDLE,
6 ENABLE_ECHO_INPUT, ENABLE_LINE_INPUT,
7};
8
9fn main() -> io::Result<()> {
10 unsafe {
11 let h: HANDLE = GetStdHandle(STD_INPUT_HANDLE);
12 if h == std::ptr::null_mut() || h == -1_i32 as HANDLE { return Err(io::Error::last_os_error()); }
13
14 // save current mode
15 let mut mode: u32 = 0;
16 if GetConsoleMode(h, &mut mode) == 0 { return Err(io::Error::last_os_error()); }
17
18 // disable line input and echo so input is available per-key and not echoed
19 let new_mode = mode & !(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT);
20 if SetConsoleMode(h, new_mode) == 0 { return Err(io::Error::last_os_error()); }
21
22 print!("Press keys (q to exit): ");
23 io::stdout().flush()?;
24
25 // ReadConsoleW reads UTF-16; we'll read one wchar at a time
26 let mut buf: [u16; 1] = [0];
27 loop {
28 let mut read = 0u32;
29 if ReadConsoleW(
30 h,
31 buf.as_mut_ptr() as *mut _,
32 1,
33 &mut read,
34 ptr::null_mut(),
35 ) == 0
36 {
37 // restore mode on error before returning
38 let _ = SetConsoleMode(h, mode);
39 return Err(io::Error::last_os_error());
40 }
41
42 let ch = std::char::from_u32(buf[0] as u32).unwrap_or('\u{FFFD}');
43 if ch == 'q' { break; } // q to exit
44
45 // handle Enter (CR) so output is readable
46 if ch == '\r' {
47 println!();
48 } else {
49 // show code point (no automatic console echo)
50 print!("[{:?}]", ch);
51 io::stdout().flush()?;
52 }
53 }
54
55 // restore original console mode
56 let _ = SetConsoleMode(h, mode);
57 println!("\nexited");
58 }
59
60 Ok(())
61}