Get rooms working

master
Elnu 2 years ago
parent c4eece7ffd
commit 85cc6c2f35

@ -11,11 +11,13 @@
<div id="split"> <div id="split">
<div> <div>
<div id="shiritori"></div> <div id="shiritori"></div>
<input id="shiritori-input" onfocusout="setTimeout(() => this.focus(), 100)" autofocus> <input id="shiritori-input" autocomplete="off" autofocus>
</div> </div>
<iframe src="https://jisho.org"></iframe> <iframe src="https://jisho.org"></iframe>
</div> </div>
<p id="shiritori-players"></p> <p id="shiritori-players"></p>
<button onclick="changeRoom('lobby')">Back to lobby</button>
Change room: <input id="room-input" autocomplete="off">
</div> </div>
<script src="shiritori.js"></script> <script src="shiritori.js"></script>
</body> </body>

@ -1,9 +1,9 @@
ws = new WebSocket("ws://localhost:8080"); ws = new WebSocket("ws://localhost:8080");
const div = document.querySelector("#shiritori"); const div = document.querySelector("#shiritori");
const input = document.querySelector("#shiritori-input"); const input = document.querySelector("#shiritori-input");
const roomInput = document.querySelector("#room-input");
const players = document.querySelector("#shiritori-players"); const players = document.querySelector("#shiritori-players");
const iframe = document.querySelector("iframe"); const iframe = document.querySelector("iframe");
console.log("yo");
let id = null; let id = null;
function displayWord(_word, end, delay) { function displayWord(_word, end, delay) {
@ -29,6 +29,9 @@ function updateInput(data) {
input.placeholder = waiting ? "Waiting for other players..." : `${data.next_mora}`; input.placeholder = waiting ? "Waiting for other players..." : `${data.next_mora}`;
input.disabled = waiting; input.disabled = waiting;
if (!waiting) input.focus(); if (!waiting) input.focus();
} else {
input.placeholder = "";
input.focus();
} }
} }
@ -40,9 +43,11 @@ ws.onmessage = e => {
const { event, data } = JSON.parse(e.data); const { event, data } = JSON.parse(e.data);
switch (event) { switch (event) {
case "greeting": case "greeting":
id = data.id; div.innerHTML = "";
updateInput(data); updateInput(data);
lookUpWord(data.word.word); if (typeof data.word !== "undefined") {
lookUpWord(data.word.word);
}
break; break;
case "word": case "word":
displayWord(data.word, true, 0); displayWord(data.word, true, 0);
@ -50,7 +55,6 @@ ws.onmessage = e => {
lookUpWord(data.word.word); lookUpWord(data.word.word);
break; break;
case "history": case "history":
console.log(data);
for (let i = 0; i < data.words.length; i++) { for (let i = 0; i < data.words.length; i++) {
displayWord(data.words[i], false, 0.1 * i); displayWord(data.words[i], false, 0.1 * i);
} }
@ -67,7 +71,26 @@ ws.onmessage = e => {
input.addEventListener('keypress', e => { input.addEventListener('keypress', e => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
ws.send(input.value); ws.send(JSON.stringify({
word: {
word: input.value
}
}));
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 = "";
}
})

@ -9,6 +9,7 @@ body {
height: 100vh; height: 100vh;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
text-align: center;
} }
div#content { div#content {
width: 32em; width: 32em;
@ -36,7 +37,6 @@ div#shiritori {
padding: 2px; padding: 2px;
height: 24em; height: 24em;
overflow-y: scroll; overflow-y: scroll;
text-align: center;
} }
div#shiritori, input#shiritori-input, #split > :last-child { div#shiritori, input#shiritori-input, #split > :last-child {
background: rgba(0, 0, 0, 0.25); background: rgba(0, 0, 0, 0.25);
@ -49,6 +49,9 @@ div#shiritori, input#shiritori-input {
} }
input { input {
font-size: 1em; font-size: 1em;
background: rgba(0, 0, 0, 0.25);
border: none;
padding: 0.25em;
} }
input#shiritori-input { input#shiritori-input {
width: 100%; width: 100%;
@ -66,9 +69,6 @@ div#shiritori {
border-bottom-left-radius: 0; border-bottom-left-radius: 0;
border-bottom-right-radius: 0; border-bottom-right-radius: 0;
} }
p#shiritori-players {
text-align: center;
}
#split { #split {
display: flex; display: flex;
gap: 0.5em; gap: 0.5em;
@ -83,3 +83,10 @@ a {
a:hover { a:hover {
border-bottom: 2px solid; border-bottom: 2px solid;
} }
button {
background: rgba(0, 0, 0, 0.25);
font-size: inherit;
cursor: pointer;
padding: 1em;
border: 0;
}

@ -100,6 +100,11 @@ impl Client {
} }
} }
} }
// Returns (old_room, &new_room)
pub fn switch_rooms(&mut self, new_room: Rc<RefCell<Room>>) -> (Rc<RefCell<Room>>, Rc<RefCell<Room>>) {
(std::mem::replace(&mut self.room, new_room), self.room.clone())
}
} }
impl PartialEq for Client { impl PartialEq for Client {

@ -3,38 +3,30 @@ use wana_kana::{ConvertJapanese, IsJapaneseStr};
fn raw_lookup(input: &str) -> Option<&jisho::Entry> { fn raw_lookup(input: &str) -> Option<&jisho::Entry> {
let input = input.trim(); let input = input.trim();
jisho::lookup(input).into_iter().find(|&word| { jisho::lookup(input).into_iter().find(|&word| {
( // If input has no kanji,
// If input has no kanji, // we can just compare the input to the reading verbatim
// we can just compare the input to the reading verbatim // ensuring both are hiragana
// ensuring both are hiragana (input.is_kana() && input.to_hiragana() == word.reading.to_hiragana()) ||
(input.is_kana() && input.to_hiragana() == word.reading.to_hiragana()) || // Otherwise, we have to ensure that the input
// Otherwise, we have to ensure that the input // is verbosely the same.
// is verbosely the same. // However, this will cause problems for some words.
// However, this will cause problems for some words. // For example, 照り焼き will be accepted but 照焼 won't.
// For example, 照り焼き will be accepted but 照焼 won't. (input == word.kanji)
(input == word.kanji)
)
}) })
} }
pub fn lookup(query: &str) -> Option<jisho::Entry> { pub fn lookup(query: &str) -> Option<jisho::Entry> {
match raw_lookup(&query) { match raw_lookup(query) {
Some(result) => Some(result.clone()), Some(result) => Some(result.clone()),
None => { None => {
if query.is_hiragana() { if query.is_hiragana() {
// looking up ごりら doesn't return ゴリラ // looking up ごりら doesn't return ゴリラ
// jisho::lookup for some reason refers to input string, // jisho::lookup for some reason refers to input string,
// so cloning is required // so cloning is required
match raw_lookup(&query.to_katakana()) { raw_lookup(&query.to_katakana()).cloned()
Some(entry) => Some(entry.clone()),
None => None,
}
} else if query.is_katakana() { } else if query.is_katakana() {
// looking up シリトリ doesn't return 尻取り // looking up シリトリ doesn't return 尻取り
match raw_lookup(&query.to_hiragana()) { raw_lookup(&query.to_hiragana()).cloned()
Some(entry) => Some(entry.clone()),
None => None,
}
} else { } else {
None None
} }

@ -2,6 +2,7 @@ mod client;
mod database; mod database;
mod dictionary; mod dictionary;
mod response; mod response;
mod request;
mod room; mod room;
mod server; mod server;
mod utils; mod utils;

@ -0,0 +1,12 @@
use serde::Deserialize;
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum MessageRequest {
Word {
word: String,
},
ChangeRoom {
name: String,
},
}

@ -16,12 +16,13 @@ use crate::word::Word;
use wana_kana::{ConvertJapanese, IsJapaneseStr}; use wana_kana::{ConvertJapanese, IsJapaneseStr};
#[derive(Default)]
pub struct RoomSettings { pub struct RoomSettings {
pub name: String,
pub database_settings: DatabaseSettings, pub database_settings: DatabaseSettings,
} }
pub struct Room { pub struct Room {
name: String,
database: Database, database: Database,
next_mora: Option<String>, next_mora: Option<String>,
last_client_id: Option<u64>, last_client_id: Option<u64>,
@ -30,12 +31,17 @@ pub struct Room {
impl Room { impl Room {
pub fn new(settings: RoomSettings) -> Result<Self, DatabaseCreationError> { pub fn new(settings: RoomSettings) -> Result<Self, DatabaseCreationError> {
Ok(Self { Ok(Self {
name: settings.name,
database: Database::new(settings.database_settings)?, database: Database::new(settings.database_settings)?,
next_mora: None, next_mora: None,
last_client_id: None, last_client_id: None,
}) })
} }
pub fn name(&self) -> &str {
&self.name
}
pub fn next_mora(&self) -> &Option<String> { pub fn next_mora(&self) -> &Option<String> {
&self.next_mora &self.next_mora
} }
@ -88,7 +94,7 @@ impl Room {
} else if self.next_mora.is_none() } else if self.next_mora.is_none()
|| get_starting_mora(&entry.reading).eq(self.next_mora.as_deref().unwrap()) || 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.next_mora = Some(get_final_mora(&word.reading));
self.database.add_word(&word).unwrap(); // TODO: replace .unwrap() with ? self.database.add_word(&word).unwrap(); // TODO: replace .unwrap() with ?
self.last_client_id = Some(client_id); self.last_client_id = Some(client_id);

@ -1,13 +1,14 @@
use crate::client::{Client, ClientInfo}; use crate::client::{Client, ClientInfo};
use crate::database::{DatabaseCreationError, DatabaseSettings, DatabaseType}; use crate::database::{DatabaseCreationError, DatabaseSettings, DatabaseType};
use crate::response::MessageResponseData; use crate::response::MessageResponseData;
use crate::request::MessageRequest;
use crate::room::{Room, RoomSettings}; use crate::room::{Room, RoomSettings};
use derive_more::From; use derive_more::From;
use simple_websockets::{Event, EventHub, Message, Responder}; use simple_websockets::{Event, EventHub, Message, Responder};
use std::cell::RefCell; use std::cell::{RefCell, Ref};
use std::collections::HashMap; use std::collections::HashMap;
use std::rc::Rc; use std::rc::{Rc, Weak};
#[derive(From, Debug)] #[derive(From, Debug)]
pub enum ServerCreationError { pub enum ServerCreationError {
@ -18,6 +19,7 @@ pub enum ServerCreationError {
#[derive(From, Debug)] #[derive(From, Debug)]
pub enum ServerError { pub enum ServerError {
DatabaseError(rusqlite::Error), DatabaseError(rusqlite::Error),
RoomCreationError(RoomCreationError),
} }
pub struct ServerSettings { pub struct ServerSettings {
@ -37,40 +39,92 @@ impl Default for ServerSettings {
pub struct Server { pub struct Server {
event_hub: EventHub, event_hub: EventHub,
lobby: Rc<RefCell<Room>>, lobby: Rc<RefCell<Room>>,
rooms: HashMap<String, Room>, rooms: HashMap<String, Weak<RefCell<Room>>>,
clients: HashMap<u64, Client>, clients: HashMap<u64, RefCell<Client>>,
} }
#[derive(From, Debug)]
pub enum RoomCreationError {
DatabaseCreationError(DatabaseCreationError),
NameConflict,
}
const LOBBY_NAME: &str = "lobby";
impl Server { impl Server {
pub fn new(settings: &ServerSettings) -> Result<Self, ServerCreationError> { pub fn new(settings: &ServerSettings) -> Result<Self, ServerCreationError> {
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 { Ok(Self {
event_hub: simple_websockets::launch(settings.port)?, event_hub: simple_websockets::launch(settings.port)?,
lobby: Rc::new(RefCell::new(Room::new(RoomSettings { lobby,
database_settings: DatabaseSettings { rooms: {
db_type: match settings.testing { let mut rooms = HashMap::new();
true => DatabaseType::InMemory, rooms.insert(LOBBY_NAME.to_string(), lobby_weak);
false => DatabaseType::OnDisk("shiritori.sb".to_string()), rooms
}, },
},
..Default::default()
})?)),
rooms: HashMap::new(),
clients: HashMap::new(), clients: HashMap::new(),
}) })
} }
pub fn get_client(&self, id: u64) -> Option<&Client> {
self.clients.get(&id)
}
pub fn run(&mut self) { pub fn run(&mut self) {
loop { loop {
match match self.event_hub.poll_event() { match match self.event_hub.poll_event() {
Event::Connect(client_id, responder) => { 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::Disconnect(client_id) => {
Event::Message(client_id, message) => self.handle_message(client_id, message), 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(()) => {} Ok(()) => {}
Err(error) => println!("{:?}", error), Err(error) => println!("{:?}", error),
@ -78,80 +132,128 @@ impl Server {
} }
} }
fn new_client(&mut self, client_id: u64, responder: Responder) -> &Client { fn new_client(&self, client_id: u64, responder: Responder) -> RefCell<Client> {
let client = Client::new( RefCell::new(Client::new(
ClientInfo::Ws { ClientInfo::Ws {
id: client_id, id: client_id,
responder, responder,
discord_info: None, discord_info: None,
}, },
self.lobby.clone(), self.lobby.clone(),
); ))
self.clients.insert(client_id, client); }
self.clients.get(&client_id).unwrap()
fn new_room(&mut self, name: &str) -> Result<Rc<RefCell<Room>>, 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<Weak<RefCell<Room>>> -> Option<Option<Rc<RefCell<Room>>>>
.map(Ok) // Option<Rc<RefCell<Room>>> -> Option<Result<Rc<RefCell<Room>>>>
// 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( fn handle_connection(
&mut self, &self,
client_id: u64, client: &Ref<Client>,
responder: Responder,
) -> Result<(), ServerError> { ) -> Result<(), ServerError> {
// Debug // Debug
println!("A client connected with id #{}", client_id); println!("A client connected with id #{}", client.id());
{ self.room_welcome(client)?;
// Initialize client
let client = self.new_client(client_id, responder);
// Get immutable access to room // Number of clients on Rc<RefCell<Room>> reference counter will be one more
let room = client.room.borrow(); self.broadcast_player_count(&client.room);
// Send client greeting Ok(())
client.send( }
MessageResponseData::Greeting {
id: client_id,
next_mora: room.next_mora().clone(),
}
.into_message(),
);
// Sent recent message history fn room_welcome(&self, client: &Ref<Client>) -> Result<(), ServerError> {
client.send( // Get immutable access to room
MessageResponseData::History { let room = client.room.borrow();
words: room.get_history()?,
}
.into_message(),
);
}
// Number of clients on Rc<RefCell<Room>> reference counter will be one more // Send client greeting
self.broadcast_player_count(&self.lobby); 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(()) Ok(())
} }
fn handle_disconnection(&mut self, client_id: u64) -> Result<(), ServerError> { fn handle_disconnection(&self, client: &Ref<Client>) -> Result<(), ServerError> {
// Debug let client_id = client.id();
println!("Client #{} disconnected.", client_id);
// Remove client
// At this point, client should be dropped
let client = self.clients.remove(&client_id).unwrap();
// Get room // Debug
let room = client.room; println!("Client #{client_id} disconnected.");
// Number of clients on Rc<RefCell<Room>> reference counter will be one less // Number of clients on Rc<RefCell<Room>> reference counter will be one less
self.broadcast_player_count(&room); self.broadcast_player_count(&client.room);
Ok(()) Ok(())
} }
fn for_client_in_room(&self, room: &Rc<RefCell<Room>>, mut closure: impl FnMut(&Client) -> ()) { fn for_client_in_room(&self, room: &Rc<RefCell<Room>>, mut closure: impl FnMut(Ref<Client>)) {
for (_id, client) in self for client in self
.clients .clients
.iter() .iter()
.filter(|(_id, client)| Rc::<RefCell<Room>>::ptr_eq(room, &client.room)) .filter(|(_id, client)| Rc::<RefCell<Room>>::ptr_eq(room, &client.borrow().room))
.map(|(_id, refcell)| refcell.borrow())
{ {
closure(client); closure(client);
} }
@ -164,13 +266,14 @@ impl Server {
} }
fn broadcast_player_count(&self, room: &Rc<RefCell<Room>>) { fn broadcast_player_count(&self, room: &Rc<RefCell<Room>>) {
let response = MessageResponseData::PlayerCount { self.broadcast_offseted_player_count(room, 0);
players: self.client_count_in_room(room) as u64, }
}
.into_response(); fn broadcast_offseted_player_count(&self, room: &Rc<RefCell<Room>>, offset: i32) {
for (_id, client) in self.clients.iter() { let players = (self.client_count_in_room(room) as i32 + offset) as u64;
client.send(response.to_message()); 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<RefCell<Room>>, response_data: MessageResponseData) { fn announce_to_room(&self, room: &Rc<RefCell<Room>>, 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<MessageRequest, &str> {
// Ignore binary messages // Ignore binary messages
let message = match message { let message: MessageRequest = match serde_json::from_str(&match message {
Message::Text(message) => { Message::Text(message) => message,
// Debug Message::Binary(_message) => return Err("Invalid request."),
println!( }) {
"Received a message from client #{}: {:?}", Ok(message) => message,
client_id, message Err(_) => return Err("Invalid request."),
);
message
}
Message::Binary(message) => {
// Debug
println!(
"Received a binary message from client #{}: {:?}",
client_id, message
);
return Ok(());
}
}; };
let client = self.get_client(client_id).unwrap(); Ok(message)
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(())
} }
} }

Loading…
Cancel
Save