From ed11799bf5bced06c3ea4fda3f66002a156eb604 Mon Sep 17 00:00:00 2001 From: ElnuDev Date: Wed, 26 Apr 2023 18:23:52 -0700 Subject: [PATCH 1/2] Restructuring, prepare for room, Discord support --- src/client.rs | 113 +++++++++++++++ src/database.rs | 27 ++-- src/dictionary.rs | 27 +++- src/main.rs | 19 ++- src/response.rs | 62 +++++++++ src/room.rs | 92 +++++++++++++ src/server.rs | 344 +++++++++++++++++----------------------------- src/utils.rs | 47 +++++++ 8 files changed, 492 insertions(+), 239 deletions(-) create mode 100644 src/client.rs create mode 100644 src/response.rs create mode 100644 src/room.rs create mode 100644 src/utils.rs diff --git a/src/client.rs b/src/client.rs new file mode 100644 index 0000000..9d04b61 --- /dev/null +++ b/src/client.rs @@ -0,0 +1,113 @@ +// --------------- +// Module overview +// --------------- +// +// DiscordInfo: describes a Discord user's information +// +// ClientInfo: describes a client's identifying information, +// both for websocket and Discord connections. +// Webscoket connections have the option to store +// DiscordInfo as well for verified connections. +// +// Client: describes a client, holding ClientInfo and Rc + +use crate::room::Room; + +use std::cmp::{PartialEq, Eq}; +use std::hash::{Hash, Hasher}; +use std::rc::Rc; +use std::cell::RefCell; +use simple_websockets::{Responder, Message}; + +pub struct DiscordInfo { + pub username: String, + pub discriminator: u16, + pub id: u64, +} + +impl PartialEq for DiscordInfo { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl Eq for DiscordInfo {} + +pub enum ClientInfo { + Ws { + id: u64, + responder: Responder, + discord_info: Option, + }, + Discord { + discord_info: DiscordInfo, + }, +} + +impl ClientInfo { + fn id(&self) -> u64 { + match self { + Self::Ws { id, discord_info, .. } => match discord_info { + // Discord-verified websocket connection + Some(discord_info) => discord_info.id, + // Anonymous websocket connection + None => *id, + }, + // Discord connection + Self::Discord { discord_info } => discord_info.id, + } + } +} + +impl PartialEq for ClientInfo { + fn eq(&self, other: &Self) -> bool { + self.id() == other.id() + } +} + +impl Eq for ClientInfo {} + +impl Hash for ClientInfo { + fn hash(&self, state: &mut H) { + self.id().hash(state) + } +} + +pub struct Client { + info: ClientInfo, + pub room: Rc>, +} + +impl Client { + pub fn new(info: ClientInfo, room: Rc>) -> Self { + Self { info, room } + } + + pub fn id(&self) -> u64 { + match self.info { + ClientInfo::Ws { id, .. } => id, + ClientInfo::Discord { .. } => unimplemented!("no id for Discord connections"), + } + } + + pub fn send(&self, message: Message) -> bool { + match &self.info { + ClientInfo::Ws { responder, .. } => responder.send(message), + ClientInfo::Discord { .. } => unimplemented!("no networking implementation for Discord connections"), + } + } +} + +impl PartialEq for Client { + fn eq(&self, other: &Self) -> bool { + self.info.eq(&other.info) + } +} + +impl Eq for Client {} + +impl Hash for Client { + fn hash(&self, state: &mut H) { + self.info.hash(state) + } +} diff --git a/src/database.rs b/src/database.rs index 19a3256..deb77b6 100644 --- a/src/database.rs +++ b/src/database.rs @@ -1,4 +1,4 @@ -use crate::Word; +use crate::word::Word; use derive_more::From; use std::path::PathBuf; @@ -15,16 +15,23 @@ pub enum DatabaseCreationError { IoError(std::io::Error), } +#[derive(Default)] +pub enum DatabaseType { + #[default] + InMemory, + OnDisk(String), +} + +#[derive(Default)] +pub struct DatabaseSettings { + pub db_type: DatabaseType, +} + impl Database { - pub fn new( - testing: bool, - ) -> Result { - let conn = if testing { - Connection::open_in_memory() - } else { - let path = PathBuf::from("shiritori.db"); - //fs::create_dir_all(path.parent().unwrap())?; - Connection::open(path) + pub fn new(settings: DatabaseSettings) -> Result { + let conn = match settings.db_type { + DatabaseType::InMemory => Connection::open_in_memory(), + DatabaseType::OnDisk(path) => Connection::open(PathBuf::from(path)), }?; conn.execute( "CREATE TABLE IF NOT EXISTS word ( diff --git a/src/dictionary.rs b/src/dictionary.rs index 6399e6c..836afc8 100644 --- a/src/dictionary.rs +++ b/src/dictionary.rs @@ -1,6 +1,6 @@ use wana_kana::{ConvertJapanese, IsJapaneseStr}; -pub fn lookup(input: &str) -> Option<&jisho::Entry> { +fn raw_lookup(input: &str) -> Option<&jisho::Entry> { let input = input.trim(); jisho::lookup(input).into_iter().find(|&word| ( // If input has no kanji, @@ -13,3 +13,28 @@ pub fn lookup(input: &str) -> Option<&jisho::Entry> { // For example, 照り焼き will be accepted but 照焼 won't. (input == word.kanji))) } + +pub fn lookup(query: &str) -> Option { + match raw_lookup(&query) { + Some(result) => Some(result.clone()), + None => { + if query.is_hiragana() { + // looking up ごりら doesn't return ゴリラ + // jisho::lookup for some reason refers to input string, + // so cloning is required + match raw_lookup(&query.to_katakana()) { + Some(entry) => Some(entry.clone()), + None => None, + } + } else if query.is_katakana() { + // looking up シリトリ doesn't return 尻取り + match raw_lookup(&query.to_hiragana()) { + Some(entry) => Some(entry.clone()), + None => None, + } + } else { + None + } + } + } +} diff --git a/src/main.rs b/src/main.rs index 4fea4ce..4e0281a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,18 +1,17 @@ +mod client; mod database; -pub use database::*; - mod dictionary; -pub use dictionary::*; - +mod response; +mod room; mod server; -pub use server::*; - +mod utils; mod word; -pub use word::*; + +use crate::server::{Server, ServerSettings}; fn main() { - const PORT: u16 = 8080; - let mut server = Server::new(PORT, true) - .unwrap_or_else(|error| panic!("Failed to start server at port {PORT}: {:?}", error)); + let settings = ServerSettings::default(); + let mut server = Server::new(&settings) + .unwrap_or_else(|error| panic!("Failed to start server at port {}: {:?}", settings.port, error)); server.run(); } diff --git a/src/response.rs b/src/response.rs new file mode 100644 index 0000000..6df3c5c --- /dev/null +++ b/src/response.rs @@ -0,0 +1,62 @@ +use crate::word::Word; + +use serde::Serialize; +use simple_websockets::Message; + +#[derive(Serialize)] +pub struct MessageResponse { + event: String, + data: MessageResponseData, +} + +impl MessageResponse { + pub fn to_message(&self) -> Message { + Message::Text(serde_json::to_string(&self).unwrap()) + } +} + +#[derive(Serialize)] +#[serde(untagged)] +pub enum MessageResponseData { + Greeting { + id: u64, + next_mora: Option, + }, + Word { + author: u64, + word: Word, + next_mora: String, + }, + History { + words: Vec, + }, + PlayerCount { + players: u64, + }, + Error { + message: String, + }, +} + +impl MessageResponseData { + pub fn get_name(&self) -> String { + String::from(match self { + Self::Greeting { .. } => "greeting", + Self::Word { .. } => "word", + Self::History { .. } => "history", + Self::PlayerCount { .. } => "playerCount", + Self::Error { .. } => "error", + }) + } + + pub fn into_response(self) -> MessageResponse { + MessageResponse { + event: self.get_name(), + data: self, + } + } + + pub fn into_message(self) -> Message { + self.into_response().to_message() + } +} diff --git a/src/room.rs b/src/room.rs new file mode 100644 index 0000000..85221c2 --- /dev/null +++ b/src/room.rs @@ -0,0 +1,92 @@ +// --------------- +// Module overview +// --------------- +// +// RoomSettings: Stores database settings for room. +// In future, could store passphrase, etc. +// +// Room: Game state abstracted away from any networking. +// Any events that aren't game-related are handled by Server. + +use crate::database::{Database, DatabaseCreationError, DatabaseSettings}; +use crate::dictionary::lookup; +use crate::response::MessageResponseData; +use crate::utils::{get_starting_mora, get_final_mora}; +use crate::word::Word; + +use wana_kana::{IsJapaneseStr, ConvertJapanese}; + +#[derive(Default)] +pub struct RoomSettings { + pub database_settings: DatabaseSettings, +} + +pub struct Room { + database: Database, + next_mora: Option, + last_client_id: Option, +} + +impl Room { + pub fn new(settings: RoomSettings) -> Result { + Ok(Self { + database: Database::new(settings.database_settings)?, + next_mora: None, + last_client_id: None, + }) + } + + pub fn next_mora(&self) -> &Option { + &self.next_mora + } + + pub fn get_history(&self) -> rusqlite::Result> { + self.database.load_words_before(self.database.last_word_id + 1) + } + + // Err(&str) will be converted to MessageResponseData::Error + pub fn handle_query(&mut self, query: &str, client_id: u64) -> Result { + // Ensure query isn't from last player + if Some(client_id) == self.last_client_id { + return Err("It's not your turn!"); + }; + + // Ensure query is in Japanese + let query = query.to_string(); + let query = if query.is_japanese() { + query + } else { + let kana = query.to_kana(); + if kana.is_japanese() { + kana + } else { + return Err("Not Japanese!"); + } + }; + + // Look up word in dictionary + let dictionary_result = lookup(&query); + + // Send result + match dictionary_result { + Some(entry) => { + if entry.reading.chars().last().unwrap().to_string().to_hiragana() == "ん" { + Err("Can't end with ん!") + } else if self.next_mora.is_none() || get_starting_mora(&entry.reading).eq(self.next_mora.as_deref().unwrap()) { + let word: Word = entry.clone().into(); + self.next_mora = Some(get_final_mora(&word.reading)); + self.database.add_word(&word).unwrap(); // TODO: replace .unwrap() with ? + self.last_client_id = Some(client_id); + Ok(MessageResponseData::Word { + author: client_id, + word, + next_mora: self.next_mora.as_deref().unwrap().to_owned(), + }) + } else { + Err("Wrong starting mora!") + } + } + None => Err("Not in dictionary!"), + } + } +} diff --git a/src/server.rs b/src/server.rs index c5d4f1f..45985b6 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,80 +1,14 @@ -use crate::DatabaseCreationError; -use crate::lookup; -use crate::Word; -use crate::Database; +use crate::client::{Client, ClientInfo}; +use crate::database::{DatabaseSettings, DatabaseType, DatabaseCreationError}; +use crate::response::MessageResponseData; +use crate::room::{Room, RoomSettings}; -use simple_websockets::{Event, Responder, Message, EventHub}; use std::collections::HashMap; -use serde::Serialize; -use wana_kana::{ConvertJapanese, IsJapaneseStr}; +use std::rc::Rc; +use std::cell::RefCell; +use simple_websockets::{Event, Responder, Message, EventHub}; use derive_more::From; -#[derive(Serialize)] -struct MessageResponse { - event: String, - data: MessageResponseData, -} - -impl MessageResponse { - fn to_message(&self) -> Message { - Message::Text(serde_json::to_string(&self).unwrap()) - } -} - -#[derive(Serialize)] -#[serde(untagged)] -enum MessageResponseData { - Greeting { - id: u64, - next_mora: Option, - }, - Word { - author: u64, - word: Word, - next_mora: String, - }, - History { - words: Vec, - }, - PlayerCount { - players: u64, - }, - Error { - message: String, - }, -} - -impl MessageResponseData { - fn get_name(&self) -> String { - String::from(match self { - Self::Greeting { .. } => "greeting", - Self::Word { .. } => "word", - Self::History { .. } => "history", - Self::PlayerCount { .. } => "playerCount", - Self::Error { .. } => "error", - }) - } - - fn into_response(self) -> MessageResponse { - MessageResponse { - event: self.get_name(), - data: self, - } - } - - fn into_message(self) -> Message { - self.into_response().to_message() - } -} - -pub struct Server { - event_hub: EventHub, - database: Database, - clients: HashMap, - next_mora: Option, - last_client_id: Option, -} - #[derive(From, Debug)] pub enum ServerCreationError { SimpleWebsocketsError(simple_websockets::Error), @@ -86,63 +20,49 @@ pub enum ServerError { DatabaseError(rusqlite::Error), } -fn get_starting_mora(word: &str) -> String { - if word.is_empty() { - return String::from(""); - } - let word = word.to_hiragana(); - let mut iter = word.chars(); - let starting_char = iter.next().unwrap(); - let second_char = iter.next().unwrap(); - match second_char { - 'ゃ' | 'ゅ' | 'ょ' => format!("{starting_char}{second_char}"), - _ => starting_char.to_string(), - } +pub struct ServerSettings { + pub port: u16, + pub testing: bool, } -// Trim off lengtheners and non-kana that are irrelevant to shiritori -// TODO: Use slices -fn trim_irrelevant_chars(word: &str) -> String { - let mut iter = word.chars().rev().peekable(); - while let Some(c) = iter.peek() { - if *c == 'ー' || !c.to_string().is_kana() { - iter.next(); - } else { - break; +impl Default for ServerSettings { + fn default() -> Self { + Self { + port: 8080, + testing: true, } } - iter.rev().collect() } -// Get final mora, which could be multiple chars e.g. しゃ -// TODO: Use slices -fn get_final_mora(word: &str) -> String { - let word = trim_irrelevant_chars(word).to_hiragana(); - if word.is_empty() { - return String::from(""); - } - let mut iter = word.chars().rev(); - let final_char = iter.next().unwrap(); - match final_char { - 'ゃ' | 'ゅ' | 'ょ' => format!("{}{final_char}", match iter.next() { - Some(c) => c.to_string(), - None => String::from(""), - }), - _ => final_char.to_string(), - } +pub struct Server { + event_hub: EventHub, + lobby: Rc>, + rooms: HashMap, + clients: HashMap, } impl Server { - pub fn new(port: u16, testing: bool) -> Result { - Ok(Server { - event_hub: simple_websockets::launch(port)?, - database: Database::new(testing)?, + pub fn new(settings: &ServerSettings) -> Result { + Ok(Self { + event_hub: simple_websockets::launch(settings.port)?, + lobby: Rc::new(RefCell::new(Room::new(RoomSettings { + database_settings: DatabaseSettings { + db_type: match settings.testing { + true => DatabaseType::InMemory, + false => DatabaseType::OnDisk("shiritori.sb".to_string()), + }, + }, + ..Default::default() + })?)), + rooms: HashMap::new(), clients: HashMap::new(), - next_mora: None, - last_client_id: None, }) } + pub fn get_client(&self, id: u64) -> Option<&Client> { + self.clients.get(&id) + } + pub fn run(&mut self) { loop { match match self.event_hub.poll_event() { @@ -156,126 +76,114 @@ impl Server { } } - fn broadcast_player_count(&self) { - let response = MessageResponseData::PlayerCount { players: self.clients.len() as u64 }.into_response(); - for (_client, responder) in self.clients.iter() { - responder.send(response.to_message()); - } + fn new_client(&mut self, client_id: u64, responder: Responder) -> &Client { + let client = Client::new(ClientInfo::Ws { + id: client_id, + responder, + discord_info: None, + }, self.lobby.clone()); + self.clients.insert(client_id, client); + self.clients.get(&client_id).unwrap() } fn handle_connection(&mut self, client_id: u64, responder: Responder) -> Result<(), ServerError> { + // Debug println!("A client connected with id #{}", client_id); - responder.send(MessageResponseData::Greeting { - id: client_id, - next_mora: self.next_mora.clone(), - }.into_message()); - responder.send(MessageResponseData::History { - words: self.database.load_words_before(self.database.last_word_id + 1)? - }.into_message()); - self.clients.insert(client_id, responder); - self.broadcast_player_count(); + + { + // Initialize client + let client = self.new_client(client_id, responder); + + // Get immutable access to room + let room = client.room.borrow(); + + // Send client greeting + client.send(MessageResponseData::Greeting { + id: client_id, + next_mora: room.next_mora().clone(), + }.into_message()); + + // Sent recent message history + client.send(MessageResponseData::History { + words: room.get_history()?, + }.into_message()); + } + + // Number of clients on Rc> reference counter will be one more + self.broadcast_player_count(&self.lobby); + Ok(()) } fn handle_disconnection(&mut self, client_id: u64) -> Result<(), ServerError> { + // Debug println!("Client #{} disconnected.", client_id); - self.clients.remove(&client_id); - self.broadcast_player_count(); + + // Remove client + // At this point, client should be dropped + let client = self.clients.remove(&client_id).unwrap(); + + // Get room + let room = client.room; + + // Number of clients on Rc> reference counter will be one less + self.broadcast_player_count(&room); + Ok(()) } + fn for_client_in_room(&self, room: &Rc>, mut closure: impl FnMut(&Client) -> ()) { + for (_id, client) in self.clients.iter().filter(|(_id, client)| Rc::>::ptr_eq(room, &client.room)) { + closure(client); + } + } + + fn client_count_in_room(&self, room: &Rc>) -> usize { + let mut count: usize = 0; + self.for_client_in_room(room, |_| count += 1); + count + } + + fn broadcast_player_count(&self, room: &Rc>) { + let response = MessageResponseData::PlayerCount { players: self.client_count_in_room(room) as u64 }.into_response(); + for (_id, client) in self.clients.iter() { + client.send(response.to_message()); + } + } + + fn announce_to_room(&self, room: &Rc>, response_data: MessageResponseData) { + let response = response_data.into_response(); + self.for_client_in_room(room, |client| { + client.send(response.to_message()); + }); + } + fn handle_message(&mut self, client_id: u64, message: Message) -> Result<(), ServerError> { // Ignore binary messages let message = match message { - Message::Text(message) => message, - Message::Binary(_) => return Ok(()), + Message::Text(message) => { + // Debug + println!("Received a message from client #{}: {:?}", client_id, message); + + message + } + Message::Binary(message) => { + // Debug + println!("Received a binary message from client #{}: {:?}", client_id, message); + + return Ok(()); + } }; - // Debug - println!("Received a message from client #{}: {:?}", client_id, message); + let client = self.get_client(client_id).unwrap(); - let response = if Some(client_id) == self.last_client_id { - MessageResponseData::Error { - message: String::from("It's not your turn!"), - } - } else { - let query = if message.is_japanese() { - Some(message) - } else { - let kana = message.to_kana(); - if kana.is_japanese() { - Some(kana) - } else { - None - } - }; - // For some reason result will keep a reference to - // katakana_query and hiragana_query, meaning that - // these need to be kept around as a variable - // to prevent them from being dropped. - let (katakana_query, hiragana_query) = match &query { - Some(query) => ( - Some(query.to_katakana()), - Some(query.to_hiragana()) - ), - None => (None, None), - }; - let result = match &query { - Some(query) => match lookup(query) { - Some(result) => Some(result), - None => { - if query.is_hiragana() { - // looking up ごりら doesn't return ゴリラ - lookup(katakana_query.as_ref().unwrap()) - } else if query.is_katakana() { - // looking up シリトリ doesn't return 尻取り - lookup(hiragana_query.as_ref().unwrap()) - } else { - None - } - } - }, - None => None, - }; - match result { - Some(entry) => { - if entry.reading.chars().last().unwrap().to_string().to_hiragana() == "ん" { - MessageResponseData::Error { - message: String::from("Can't end with ん!"), - } - } else if self.next_mora.is_none() || get_starting_mora(&entry.reading).eq(self.next_mora.as_deref().unwrap()) { - let word: Word = entry.clone().into(); - self.next_mora = Some(get_final_mora(&word.reading)); - self.database.add_word(&word)?; - MessageResponseData::Word { - author: client_id, - word, - next_mora: self.next_mora.as_deref().unwrap().to_owned(), - } - } else { - MessageResponseData::Error { - message: String::from("Wrong starting mora!"), - } - } - } - None => MessageResponseData::Error { - message: String::from("Not in dictionary!"), - }, - } - }.into_response(); - - match response.data { + match client.room.borrow_mut().handle_query(&message, client_id) { + // Broadcast new words to all clients in same room + Ok(response) => self.announce_to_room(&client.room, response), // Send errors to only this client - MessageResponseData::Error { .. } => { - self.clients.get(&client_id).unwrap().send(response.to_message()); - }, - // Broadcast everything else to all clients - _ => { - for (_client, responder) in self.clients.iter() { - responder.send(response.to_message()); - } - self.last_client_id = Some(client_id); - }, + Err(message) => { + client.send(MessageResponseData::Error { message: message.to_string() }.into_message()); + } }; Ok(()) } diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..09fa480 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,47 @@ +use wana_kana::{ConvertJapanese, IsJapaneseStr}; + +pub fn get_starting_mora(word: &str) -> String { + if word.is_empty() { + return String::from(""); + } + let word = word.to_hiragana(); + let mut iter = word.chars(); + let starting_char = iter.next().unwrap(); + let second_char = iter.next().unwrap(); + match second_char { + 'ゃ' | 'ゅ' | 'ょ' => format!("{starting_char}{second_char}"), + _ => starting_char.to_string(), + } +} + +// Trim off lengtheners and non-kana that are irrelevant to shiritori +// TODO: Use slices +fn trim_irrelevant_chars(word: &str) -> String { + let mut iter = word.chars().rev().peekable(); + while let Some(c) = iter.peek() { + if *c == 'ー' || !c.to_string().is_kana() { + iter.next(); + } else { + break; + } + } + iter.rev().collect() +} + +// Get final mora, which could be multiple chars e.g. しゃ +// TODO: Use slices +pub fn get_final_mora(word: &str) -> String { + let word = trim_irrelevant_chars(word).to_hiragana(); + if word.is_empty() { + return String::from(""); + } + let mut iter = word.chars().rev(); + let final_char = iter.next().unwrap(); + match final_char { + 'ゃ' | 'ゅ' | 'ょ' => format!("{}{final_char}", match iter.next() { + Some(c) => c.to_string(), + None => String::from(""), + }), + _ => final_char.to_string(), + } +} From c4eece7ffd66725f890b29007b5cf369e2cc9c30 Mon Sep 17 00:00:00 2001 From: ElnuDev Date: Wed, 26 Apr 2023 18:27:54 -0700 Subject: [PATCH 2/2] cargo fmt --- src/client.rs | 14 +++++--- src/database.rs | 16 ++++------ src/dictionary.rs | 7 ++-- src/main.rs | 8 +++-- src/room.rs | 27 ++++++++++++---- src/server.rs | 81 +++++++++++++++++++++++++++++++++-------------- src/utils.rs | 11 ++++--- src/word.rs | 2 +- 8 files changed, 113 insertions(+), 53 deletions(-) diff --git a/src/client.rs b/src/client.rs index 9d04b61..0138ebb 100644 --- a/src/client.rs +++ b/src/client.rs @@ -13,11 +13,11 @@ use crate::room::Room; -use std::cmp::{PartialEq, Eq}; +use simple_websockets::{Message, Responder}; +use std::cell::RefCell; +use std::cmp::{Eq, PartialEq}; use std::hash::{Hash, Hasher}; use std::rc::Rc; -use std::cell::RefCell; -use simple_websockets::{Responder, Message}; pub struct DiscordInfo { pub username: String, @@ -47,7 +47,9 @@ pub enum ClientInfo { impl ClientInfo { fn id(&self) -> u64 { match self { - Self::Ws { id, discord_info, .. } => match discord_info { + Self::Ws { + id, discord_info, .. + } => match discord_info { // Discord-verified websocket connection Some(discord_info) => discord_info.id, // Anonymous websocket connection @@ -93,7 +95,9 @@ impl Client { pub fn send(&self, message: Message) -> bool { match &self.info { ClientInfo::Ws { responder, .. } => responder.send(message), - ClientInfo::Discord { .. } => unimplemented!("no networking implementation for Discord connections"), + ClientInfo::Discord { .. } => { + unimplemented!("no networking implementation for Discord connections") + } } } } diff --git a/src/database.rs b/src/database.rs index deb77b6..e4c217c 100644 --- a/src/database.rs +++ b/src/database.rs @@ -1,8 +1,8 @@ use crate::word::Word; use derive_more::From; +use rusqlite::{params, Connection, Result}; use std::path::PathBuf; -use rusqlite::{Connection, Result, params}; pub struct Database { conn: Connection, @@ -46,10 +46,11 @@ impl Database { .prepare("SELECT id FROM word ORDER BY id DESC LIMIT 1")? .query_map(params![], |row| row.get(0))? .collect::>>()? - .first() { - Some(id) => *id, - None => 0, // first database entry is id 1 - }; + .first() + { + Some(id) => *id, + None => 0, // first database entry is id 1 + }; Ok(Self { conn, last_word_id }) } @@ -70,10 +71,7 @@ impl Database { pub fn add_word(&mut self, word: &Word) -> Result<()> { self.conn.execute( "INSERT INTO word (word, reading) VALUES (?1, ?2)", - params![ - word.word, - word.reading, - ], + params![word.word, word.reading,], )?; self.last_word_id += 1; Ok(()) diff --git a/src/dictionary.rs b/src/dictionary.rs index 836afc8..4d50ee8 100644 --- a/src/dictionary.rs +++ b/src/dictionary.rs @@ -2,7 +2,8 @@ use wana_kana::{ConvertJapanese, IsJapaneseStr}; fn raw_lookup(input: &str) -> Option<&jisho::Entry> { let input = input.trim(); - jisho::lookup(input).into_iter().find(|&word| ( + jisho::lookup(input).into_iter().find(|&word| { + ( // If input has no kanji, // we can just compare the input to the reading verbatim // ensuring both are hiragana @@ -11,7 +12,9 @@ fn raw_lookup(input: &str) -> Option<&jisho::Entry> { // is verbosely the same. // However, this will cause problems for some words. // For example, 照り焼き will be accepted but 照焼 won't. - (input == word.kanji))) + (input == word.kanji) + ) + }) } pub fn lookup(query: &str) -> Option { diff --git a/src/main.rs b/src/main.rs index 4e0281a..ddc4eb2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,7 +11,11 @@ use crate::server::{Server, ServerSettings}; fn main() { let settings = ServerSettings::default(); - let mut server = Server::new(&settings) - .unwrap_or_else(|error| panic!("Failed to start server at port {}: {:?}", settings.port, error)); + let mut server = Server::new(&settings).unwrap_or_else(|error| { + panic!( + "Failed to start server at port {}: {:?}", + settings.port, error + ) + }); server.run(); } diff --git a/src/room.rs b/src/room.rs index 85221c2..224611d 100644 --- a/src/room.rs +++ b/src/room.rs @@ -11,10 +11,10 @@ use crate::database::{Database, DatabaseCreationError, DatabaseSettings}; use crate::dictionary::lookup; use crate::response::MessageResponseData; -use crate::utils::{get_starting_mora, get_final_mora}; +use crate::utils::{get_final_mora, get_starting_mora}; use crate::word::Word; -use wana_kana::{IsJapaneseStr, ConvertJapanese}; +use wana_kana::{ConvertJapanese, IsJapaneseStr}; #[derive(Default)] pub struct RoomSettings { @@ -41,11 +41,16 @@ impl Room { } pub fn get_history(&self) -> rusqlite::Result> { - self.database.load_words_before(self.database.last_word_id + 1) + self.database + .load_words_before(self.database.last_word_id + 1) } // Err(&str) will be converted to MessageResponseData::Error - pub fn handle_query(&mut self, query: &str, client_id: u64) -> Result { + pub fn handle_query( + &mut self, + query: &str, + client_id: u64, + ) -> Result { // Ensure query isn't from last player if Some(client_id) == self.last_client_id { return Err("It's not your turn!"); @@ -70,9 +75,19 @@ impl Room { // Send result match dictionary_result { Some(entry) => { - if entry.reading.chars().last().unwrap().to_string().to_hiragana() == "ん" { + if entry + .reading + .chars() + .last() + .unwrap() + .to_string() + .to_hiragana() + == "ん" + { Err("Can't end with ん!") - } else if self.next_mora.is_none() || get_starting_mora(&entry.reading).eq(self.next_mora.as_deref().unwrap()) { + } else if self.next_mora.is_none() + || get_starting_mora(&entry.reading).eq(self.next_mora.as_deref().unwrap()) + { let word: Word = entry.clone().into(); self.next_mora = Some(get_final_mora(&word.reading)); self.database.add_word(&word).unwrap(); // TODO: replace .unwrap() with ? diff --git a/src/server.rs b/src/server.rs index 45985b6..597c102 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,13 +1,13 @@ use crate::client::{Client, ClientInfo}; -use crate::database::{DatabaseSettings, DatabaseType, DatabaseCreationError}; +use crate::database::{DatabaseCreationError, DatabaseSettings, DatabaseType}; use crate::response::MessageResponseData; use crate::room::{Room, RoomSettings}; +use derive_more::From; +use simple_websockets::{Event, EventHub, Message, Responder}; +use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; -use std::cell::RefCell; -use simple_websockets::{Event, Responder, Message, EventHub}; -use derive_more::From; #[derive(From, Debug)] pub enum ServerCreationError { @@ -66,27 +66,36 @@ impl Server { pub fn run(&mut self) { loop { match match self.event_hub.poll_event() { - Event::Connect(client_id, responder) => self.handle_connection(client_id, responder), + Event::Connect(client_id, responder) => { + self.handle_connection(client_id, responder) + } Event::Disconnect(client_id) => self.handle_disconnection(client_id), Event::Message(client_id, message) => self.handle_message(client_id, message), } { - Ok(()) => {}, + Ok(()) => {} Err(error) => println!("{:?}", error), } } } fn new_client(&mut self, client_id: u64, responder: Responder) -> &Client { - let client = Client::new(ClientInfo::Ws { - id: client_id, - responder, - discord_info: None, - }, self.lobby.clone()); + let client = Client::new( + ClientInfo::Ws { + id: client_id, + responder, + discord_info: None, + }, + self.lobby.clone(), + ); self.clients.insert(client_id, client); self.clients.get(&client_id).unwrap() } - fn handle_connection(&mut self, client_id: u64, responder: Responder) -> Result<(), ServerError> { + fn handle_connection( + &mut self, + client_id: u64, + responder: Responder, + ) -> Result<(), ServerError> { // Debug println!("A client connected with id #{}", client_id); @@ -98,15 +107,21 @@ impl Server { let room = client.room.borrow(); // Send client greeting - client.send(MessageResponseData::Greeting { - id: client_id, - next_mora: room.next_mora().clone(), - }.into_message()); + client.send( + MessageResponseData::Greeting { + id: client_id, + next_mora: room.next_mora().clone(), + } + .into_message(), + ); // Sent recent message history - client.send(MessageResponseData::History { - words: room.get_history()?, - }.into_message()); + client.send( + MessageResponseData::History { + words: room.get_history()?, + } + .into_message(), + ); } // Number of clients on Rc> reference counter will be one more @@ -133,7 +148,11 @@ impl Server { } fn for_client_in_room(&self, room: &Rc>, mut closure: impl FnMut(&Client) -> ()) { - for (_id, client) in self.clients.iter().filter(|(_id, client)| Rc::>::ptr_eq(room, &client.room)) { + for (_id, client) in self + .clients + .iter() + .filter(|(_id, client)| Rc::>::ptr_eq(room, &client.room)) + { closure(client); } } @@ -145,7 +164,10 @@ impl Server { } fn broadcast_player_count(&self, room: &Rc>) { - let response = MessageResponseData::PlayerCount { players: self.client_count_in_room(room) as u64 }.into_response(); + let response = MessageResponseData::PlayerCount { + players: self.client_count_in_room(room) as u64, + } + .into_response(); for (_id, client) in self.clients.iter() { client.send(response.to_message()); } @@ -163,13 +185,19 @@ impl Server { let message = match message { Message::Text(message) => { // Debug - println!("Received a message from client #{}: {:?}", client_id, message); + println!( + "Received a message from client #{}: {:?}", + client_id, message + ); message } Message::Binary(message) => { // Debug - println!("Received a binary message from client #{}: {:?}", client_id, message); + println!( + "Received a binary message from client #{}: {:?}", + client_id, message + ); return Ok(()); } @@ -182,7 +210,12 @@ impl Server { Ok(response) => self.announce_to_room(&client.room, response), // Send errors to only this client Err(message) => { - client.send(MessageResponseData::Error { message: message.to_string() }.into_message()); + client.send( + MessageResponseData::Error { + message: message.to_string(), + } + .into_message(), + ); } }; Ok(()) diff --git a/src/utils.rs b/src/utils.rs index 09fa480..0684417 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -38,10 +38,13 @@ pub fn get_final_mora(word: &str) -> String { let mut iter = word.chars().rev(); let final_char = iter.next().unwrap(); match final_char { - 'ゃ' | 'ゅ' | 'ょ' => format!("{}{final_char}", match iter.next() { - Some(c) => c.to_string(), - None => String::from(""), - }), + 'ゃ' | 'ゅ' | 'ょ' => format!( + "{}{final_char}", + match iter.next() { + Some(c) => c.to_string(), + None => String::from(""), + } + ), _ => final_char.to_string(), } } diff --git a/src/word.rs b/src/word.rs index cfa1701..8414f87 100644 --- a/src/word.rs +++ b/src/word.rs @@ -1,6 +1,6 @@ use chrono::{DateTime, Utc}; -use serde::Serialize; use jisho::Entry; +use serde::Serialize; use std::convert::From; #[derive(Serialize)]