diff --git a/demo/index.html b/demo/index.html index 5285963..7ce3b9c 100644 --- a/demo/index.html +++ b/demo/index.html @@ -11,11 +11,13 @@
- +

+ + Change room: diff --git a/demo/shiritori.js b/demo/shiritori.js index 44ff7a7..8c5f211 100644 --- a/demo/shiritori.js +++ b/demo/shiritori.js @@ -1,9 +1,9 @@ ws = new WebSocket("ws://localhost:8080"); const div = document.querySelector("#shiritori"); const input = document.querySelector("#shiritori-input"); +const roomInput = document.querySelector("#room-input"); const players = document.querySelector("#shiritori-players"); const iframe = document.querySelector("iframe"); -console.log("yo"); let id = null; function displayWord(_word, end, delay) { @@ -29,6 +29,9 @@ function updateInput(data) { input.placeholder = waiting ? "Waiting for other players..." : `${data.next_mora}…`; input.disabled = waiting; if (!waiting) input.focus(); + } else { + input.placeholder = ""; + input.focus(); } } @@ -40,9 +43,11 @@ ws.onmessage = e => { const { event, data } = JSON.parse(e.data); switch (event) { case "greeting": - id = data.id; + div.innerHTML = ""; updateInput(data); - lookUpWord(data.word.word); + if (typeof data.word !== "undefined") { + lookUpWord(data.word.word); + } break; case "word": displayWord(data.word, true, 0); @@ -50,7 +55,6 @@ ws.onmessage = e => { lookUpWord(data.word.word); break; case "history": - console.log(data); for (let i = 0; i < data.words.length; i++) { displayWord(data.words[i], false, 0.1 * i); } @@ -67,7 +71,26 @@ ws.onmessage = e => { input.addEventListener('keypress', e => { if (e.key === 'Enter') { - ws.send(input.value); + ws.send(JSON.stringify({ + word: { + word: input.value + } + })); input.value = ""; } }); + +function changeRoom(room) { + ws.send(JSON.stringify({ + changeRoom: { + name: room + } + })) +} + +roomInput.addEventListener('keypress', e => { + if (e.key === 'Enter') { + changeRoom(roomInput.value); + roomInput.value = ""; + } + }) diff --git a/demo/style.css b/demo/style.css index 0478275..efd85c7 100644 --- a/demo/style.css +++ b/demo/style.css @@ -9,6 +9,7 @@ body { height: 100vh; align-items: center; justify-content: center; + text-align: center; } div#content { width: 32em; @@ -36,7 +37,6 @@ div#shiritori { padding: 2px; height: 24em; overflow-y: scroll; - text-align: center; } div#shiritori, input#shiritori-input, #split > :last-child { background: rgba(0, 0, 0, 0.25); @@ -49,6 +49,9 @@ div#shiritori, input#shiritori-input { } input { font-size: 1em; + background: rgba(0, 0, 0, 0.25); + border: none; + padding: 0.25em; } input#shiritori-input { width: 100%; @@ -66,9 +69,6 @@ div#shiritori { border-bottom-left-radius: 0; border-bottom-right-radius: 0; } -p#shiritori-players { - text-align: center; -} #split { display: flex; gap: 0.5em; @@ -83,3 +83,10 @@ a { a:hover { border-bottom: 2px solid; } +button { + background: rgba(0, 0, 0, 0.25); + font-size: inherit; + cursor: pointer; + padding: 1em; + border: 0; +} diff --git a/src/client.rs b/src/client.rs index 0138ebb..f36c6ff 100644 --- a/src/client.rs +++ b/src/client.rs @@ -100,6 +100,11 @@ impl Client { } } } + + // Returns (old_room, &new_room) + pub fn switch_rooms(&mut self, new_room: Rc>) -> (Rc>, Rc>) { + (std::mem::replace(&mut self.room, new_room), self.room.clone()) + } } impl PartialEq for Client { diff --git a/src/dictionary.rs b/src/dictionary.rs index 4d50ee8..1192f88 100644 --- a/src/dictionary.rs +++ b/src/dictionary.rs @@ -3,38 +3,30 @@ use wana_kana::{ConvertJapanese, IsJapaneseStr}; fn raw_lookup(input: &str) -> Option<&jisho::Entry> { let input = input.trim(); 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 - (input.is_kana() && input.to_hiragana() == word.reading.to_hiragana()) || - // Otherwise, we have to ensure that the input - // is verbosely the same. - // However, this will cause problems for some words. - // For example, 照り焼き will be accepted but 照焼 won't. - (input == word.kanji) - ) + // If input has no kanji, + // we can just compare the input to the reading verbatim + // ensuring both are hiragana + (input.is_kana() && input.to_hiragana() == word.reading.to_hiragana()) || + // Otherwise, we have to ensure that the input + // is verbosely the same. + // However, this will cause problems for some words. + // For example, 照り焼き will be accepted but 照焼 won't. + (input == word.kanji) }) } pub fn lookup(query: &str) -> Option { - match raw_lookup(&query) { + 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, - } + raw_lookup(&query.to_katakana()).cloned() } else if query.is_katakana() { // looking up シリトリ doesn't return 尻取り - match raw_lookup(&query.to_hiragana()) { - Some(entry) => Some(entry.clone()), - None => None, - } + raw_lookup(&query.to_hiragana()).cloned() } else { None } diff --git a/src/main.rs b/src/main.rs index ddc4eb2..3d0ab16 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ mod client; mod database; mod dictionary; mod response; +mod request; mod room; mod server; mod utils; diff --git a/src/request.rs b/src/request.rs new file mode 100644 index 0000000..35c30f5 --- /dev/null +++ b/src/request.rs @@ -0,0 +1,12 @@ +use serde::Deserialize; + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum MessageRequest { + Word { + word: String, + }, + ChangeRoom { + name: String, + }, +} diff --git a/src/room.rs b/src/room.rs index 224611d..2c1f119 100644 --- a/src/room.rs +++ b/src/room.rs @@ -16,12 +16,13 @@ use crate::word::Word; use wana_kana::{ConvertJapanese, IsJapaneseStr}; -#[derive(Default)] pub struct RoomSettings { + pub name: String, pub database_settings: DatabaseSettings, } pub struct Room { + name: String, database: Database, next_mora: Option, last_client_id: Option, @@ -30,12 +31,17 @@ pub struct Room { impl Room { pub fn new(settings: RoomSettings) -> Result { Ok(Self { + name: settings.name, database: Database::new(settings.database_settings)?, next_mora: None, last_client_id: None, }) } + pub fn name(&self) -> &str { + &self.name + } + pub fn next_mora(&self) -> &Option { &self.next_mora } @@ -88,7 +94,7 @@ impl Room { } 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(); + let word: Word = entry.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); diff --git a/src/server.rs b/src/server.rs index 597c102..f821729 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,13 +1,14 @@ use crate::client::{Client, ClientInfo}; use crate::database::{DatabaseCreationError, DatabaseSettings, DatabaseType}; use crate::response::MessageResponseData; +use crate::request::MessageRequest; use crate::room::{Room, RoomSettings}; use derive_more::From; use simple_websockets::{Event, EventHub, Message, Responder}; -use std::cell::RefCell; +use std::cell::{RefCell, Ref}; use std::collections::HashMap; -use std::rc::Rc; +use std::rc::{Rc, Weak}; #[derive(From, Debug)] pub enum ServerCreationError { @@ -18,6 +19,7 @@ pub enum ServerCreationError { #[derive(From, Debug)] pub enum ServerError { DatabaseError(rusqlite::Error), + RoomCreationError(RoomCreationError), } pub struct ServerSettings { @@ -37,40 +39,92 @@ impl Default for ServerSettings { pub struct Server { event_hub: EventHub, lobby: Rc>, - rooms: HashMap, - clients: HashMap, + rooms: HashMap>>, + clients: HashMap>, } +#[derive(From, Debug)] +pub enum RoomCreationError { + DatabaseCreationError(DatabaseCreationError), + NameConflict, +} + +const LOBBY_NAME: &str = "lobby"; + impl Server { pub fn new(settings: &ServerSettings) -> Result { + let lobby = Rc::new(RefCell::new(Room::new(RoomSettings { + name: LOBBY_NAME.to_string(), + database_settings: DatabaseSettings { + db_type: match settings.testing { + true => DatabaseType::InMemory, + false => DatabaseType::OnDisk("shiritori.sb".to_string()), + }, + }, + })?)); + let lobby_weak = Rc::downgrade(&lobby); 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(), + lobby, + rooms: { + let mut rooms = HashMap::new(); + rooms.insert(LOBBY_NAME.to_string(), lobby_weak); + rooms + }, clients: HashMap::new(), }) } - 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() { Event::Connect(client_id, responder) => { - self.handle_connection(client_id, responder) + let client = self.new_client(client_id, responder); + self.clients.insert(client_id, client); + let client = self.clients.get(&client_id).unwrap().borrow(); // moved, get it again + self.handle_connection(&client) } - Event::Disconnect(client_id) => self.handle_disconnection(client_id), - Event::Message(client_id, message) => self.handle_message(client_id, message), + Event::Disconnect(client_id) => { + let client = self.clients.remove(&client_id).unwrap(); + let client_ref = client.borrow(); + self.handle_disconnection(&client_ref) + }, + Event::Message(client_id, message) => { + println!("Received a message from client #{client_id}: {:?}", message); + let message = match self.process_message(message) { + Ok(message) => message, + Err(error) => { + let client = self.clients.get(&client_id).unwrap(); + let client_ref = client.borrow(); + client_ref.send(MessageResponseData::Error { message: error.to_string() }.into_message()); + continue; + }, + }; + match message { + MessageRequest::Word { word } => { + let client = self.clients.get(&client_id).unwrap(); + let client_ref = client.borrow(); + match client_ref.room.borrow_mut().handle_query(&word, client_id) { + // Broadcast new words to all clients in same room + Ok(response) => self.announce_to_room(&client_ref.room, response), + // Send errors to only this client + Err(message) => { + client_ref.send( + MessageResponseData::Error { + message: message.to_string(), + } + .into_message(), + ); + }, + }; + Ok(()) + }, + MessageRequest::ChangeRoom { name } => { + self.switch_rooms(client_id, name).unwrap(); + Ok(()) + }, + } + }, } { Ok(()) => {} Err(error) => println!("{:?}", error), @@ -78,80 +132,128 @@ impl Server { } } - fn new_client(&mut self, client_id: u64, responder: Responder) -> &Client { - let client = Client::new( + fn new_client(&self, client_id: u64, responder: Responder) -> RefCell { + RefCell::new(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 new_room(&mut self, name: &str) -> Result>, RoomCreationError> { + if self.rooms.contains_key(name) { + return Err(RoomCreationError::NameConflict); + } + let room = Rc::new(RefCell::new(Room::new(RoomSettings { + name: name.to_owned(), + database_settings: DatabaseSettings { + db_type: DatabaseType::InMemory, + }, + })?)); + self.rooms.insert(name.to_owned(), Rc::downgrade(&room)); + Ok(room) + } + + fn switch_rooms(&mut self, client_id: u64, room_name: String) -> Result<(), ServerError> { + let room = self.rooms.get(&room_name) + // upgrade Weak to Rc if exists + .and_then(|weak| weak.upgrade()) // Option>> -> Option>>> + .map(Ok) // Option>> -> Option>>> + // if not exists OR failed to upgrade (value dropped), it will be None. + // in that case, initialize a new room + .unwrap_or_else(|| self.new_room(&room_name))?; + let client = self.clients.get(&client_id).unwrap(); + let (old_room, room) = { + let mut client_mut = client.borrow_mut(); + // Skip logic and return if going into same room + if client_mut.room.borrow().name().eq(&room_name) { + return Ok(()); + } + client_mut.switch_rooms(room) + }; + // Clean up old room to be dropped + // However, lobby will never be dropped + let old_room = if Rc::strong_count(&old_room) <= 1 { + if !Rc::ptr_eq(&old_room, &self.lobby) { + self.rooms.remove(old_room.borrow().name()); + println!("Removing room, {}!", old_room.borrow().name()); + }; + None + } else { + Some(old_room) + }; + // broadcast reference count minus one + // (We still have an Rc hanging around here) + //self.broadcast_offseted_player_count(&old_room, -1); + if let Some(old_room) = old_room { + self.broadcast_player_count(&old_room); + } + self.broadcast_player_count(&room); + + self.room_welcome(&client.borrow())?; + Ok(()) } fn handle_connection( - &mut self, - client_id: u64, - responder: Responder, + &self, + client: &Ref, ) -> Result<(), ServerError> { // Debug - println!("A client connected with id #{}", client_id); + println!("A client connected with id #{}", client.id()); - { - // Initialize client - let client = self.new_client(client_id, responder); + self.room_welcome(client)?; - // Get immutable access to room - let room = client.room.borrow(); + // Number of clients on Rc> reference counter will be one more + self.broadcast_player_count(&client.room); - // Send client greeting - client.send( - MessageResponseData::Greeting { - id: client_id, - next_mora: room.next_mora().clone(), - } - .into_message(), - ); + Ok(()) + } - // Sent recent message history - client.send( - MessageResponseData::History { - words: room.get_history()?, - } - .into_message(), - ); - } + fn room_welcome(&self, client: &Ref) -> Result<(), ServerError> { + // Get immutable access to room + let room = client.room.borrow(); - // Number of clients on Rc> reference counter will be one more - self.broadcast_player_count(&self.lobby); + // 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(), + ); Ok(()) } - fn handle_disconnection(&mut self, client_id: u64) -> Result<(), ServerError> { - // Debug - println!("Client #{} disconnected.", client_id); - - // Remove client - // At this point, client should be dropped - let client = self.clients.remove(&client_id).unwrap(); + fn handle_disconnection(&self, client: &Ref) -> Result<(), ServerError> { + let client_id = client.id(); - // Get room - let room = client.room; + // Debug + println!("Client #{client_id} disconnected."); // Number of clients on Rc> reference counter will be one less - self.broadcast_player_count(&room); + self.broadcast_player_count(&client.room); Ok(()) } - fn for_client_in_room(&self, room: &Rc>, mut closure: impl FnMut(&Client) -> ()) { - for (_id, client) in self + fn for_client_in_room(&self, room: &Rc>, mut closure: impl FnMut(Ref)) { + for client in self .clients .iter() - .filter(|(_id, client)| Rc::>::ptr_eq(room, &client.room)) + .filter(|(_id, client)| Rc::>::ptr_eq(room, &client.borrow().room)) + .map(|(_id, refcell)| refcell.borrow()) { closure(client); } @@ -164,13 +266,14 @@ impl Server { } 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()); - } + self.broadcast_offseted_player_count(room, 0); + } + + fn broadcast_offseted_player_count(&self, room: &Rc>, offset: i32) { + let players = (self.client_count_in_room(room) as i32 + offset) as u64; + println!("Broadcast player count {players} for room {}", room.borrow().name()); + let response = MessageResponseData::PlayerCount { players }; + self.announce_to_room(room, response); } fn announce_to_room(&self, room: &Rc>, response_data: MessageResponseData) { @@ -180,44 +283,16 @@ impl Server { }); } - fn handle_message(&mut self, client_id: u64, message: Message) -> Result<(), ServerError> { + fn process_message(&self, message: Message) -> Result { // Ignore binary messages - let message = match message { - 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(()); - } + let message: MessageRequest = match serde_json::from_str(&match message { + Message::Text(message) => message, + Message::Binary(_message) => return Err("Invalid request."), + }) { + Ok(message) => message, + Err(_) => return Err("Invalid request."), }; - let client = self.get_client(client_id).unwrap(); - - 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 - Err(message) => { - client.send( - MessageResponseData::Error { - message: message.to_string(), - } - .into_message(), - ); - } - }; - Ok(()) + Ok(message) } }