diff --git a/server/src/main.rs b/server/src/main.rs index 9b8b9e4..a44ec82 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -12,21 +12,20 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::{Arc, LazyLock, Weak}; use tokio::select; -use uuid::Uuid; use word_grid::api::{APIGame, ApiState, Update}; use word_grid::dictionary::{Dictionary, DictionaryImpl}; use word_grid::game::{Error, Game, PlayedTile}; use word_grid::player_interaction::ai::Difficulty; +use ws::frame::{CloseCode, CloseFrame}; use ws::stream::DuplexStream; use ws::Message; static DICTIONARY: LazyLock = LazyLock::new(|| DictionaryImpl::create_from_path("../resources/dictionary.csv")); -#[derive(Clone, Debug, Serialize)] +#[derive(Clone, Debug, Serialize, PartialEq)] struct Player { name: String, - id: Uuid, } #[derive(Clone, Serialize, Debug)] @@ -47,7 +46,7 @@ impl PartyInfo { struct Room { party_info: PartyInfo, game: Option, - sender: Sender, + sender: Sender<(Option, InnerRoomMessage)>, } impl Room { @@ -120,7 +119,7 @@ type RoomMap = HashMap>>; async fn incoming_message_handler( message: Option>, - sender: &Sender, + sender: &Sender<(Option, InnerRoomMessage)>, player: &Player, room: &Arc>, stream: &mut DuplexStream, @@ -139,18 +138,14 @@ async fn incoming_message_handler( let message = message.to_text().unwrap(); if message.len() == 0 { println!("Websocket closed"); - // TODO need to handle updating Players, etc. println!("Player {player:#?} is leaving"); let mut room = room.write().await; - if room.game.is_some() { - unimplemented!("Need to handle mid-game someone leaving") - } let new_vec = room .party_info .players .iter() - .filter(|p| !p.id.eq(&player.id)) + .filter(|p| !p.name.eq(&player.name)) .map(|p| p.clone()) .collect_vec(); room.party_info.players = new_vec; @@ -161,7 +156,9 @@ async fn incoming_message_handler( }, info: room.party_info.clone(), }; - sender.send(InnerRoomMessage::PassThrough(event)).unwrap(); + sender + .send((None, InnerRoomMessage::PassThrough(event))) + .unwrap(); // TODO - handle case where there are no players left @@ -172,7 +169,7 @@ async fn incoming_message_handler( let message: ClientToServerMessage = serde_json::from_str(message).unwrap(); // TODO - println!("Received {message:#?} from client {}", player.id); + println!("Received {message:#?} from client {}", player.name); match message { ClientToServerMessage::Load => { return !game_load(player, None, room, stream).await @@ -202,7 +199,9 @@ async fn incoming_message_handler( let game = APIGame::new(game); room.game = Some(game); - sender.send(InnerRoomMessage::GameEvent(None)).unwrap(); + sender + .send((None, InnerRoomMessage::GameEvent(None))) + .unwrap(); } } ClientToServerMessage::GameMove { r#move } => { @@ -246,7 +245,7 @@ async fn incoming_message_handler( match result { Ok(event) => { sender - .send(InnerRoomMessage::GameEvent(event.update)) + .send((None, InnerRoomMessage::GameEvent(event.update))) .unwrap(); } Err(error) => { @@ -265,7 +264,9 @@ async fn incoming_message_handler( event: RoomEvent::AIJoined { difficulty }, info: room.party_info.clone(), }; - sender.send(InnerRoomMessage::PassThrough(event)).unwrap(); + sender + .send((None, InnerRoomMessage::PassThrough(event))) + .unwrap(); } ClientToServerMessage::RemoveAI { index } => { let mut room = room.write().await; @@ -275,7 +276,9 @@ async fn incoming_message_handler( event: RoomEvent::AILeft { index }, info: room.party_info.clone(), }; - sender.send(InnerRoomMessage::PassThrough(event)).unwrap(); + sender + .send((None, InnerRoomMessage::PassThrough(event))) + .unwrap(); } else { let event = ServerToClientMessage::Invalid { reason: format!("{index} is out of bounds"), @@ -321,26 +324,48 @@ async fn game_load( } async fn outgoing_message_handler( - message: Result, - _sender: &Sender, + message: Result<(Option, InnerRoomMessage), E>, + _sender: &Sender<(Option, InnerRoomMessage)>, player: &Player, room: &Arc>, stream: &mut DuplexStream, ) -> bool { - let message = message.unwrap(); + let (target, message) = message.unwrap(); println!("Inner room message - {:#?}", message); - return match message { - InnerRoomMessage::PassThrough(event) => { - let text = serde_json::to_string(&event).unwrap(); - let x = stream.send(text.into()).await; - x.is_err() + if target.is_none() || target.unwrap() == *player { + match message { + InnerRoomMessage::PassThrough(event) => { + let text = serde_json::to_string(&event).unwrap(); + let x = stream.send(text.into()).await; + x.is_err() + } + InnerRoomMessage::GameEvent(update) => !game_load(player, update, room, stream).await, } - InnerRoomMessage::GameEvent(update) => !game_load(player, update, room, stream).await, - }; + } else { + false + } +} + +fn reject_websocket_with_reason( + ws: ws::WebSocket, + close_code: CloseCode, + reason: String, +) -> ws::Channel<'static> { + ws.channel(move |mut stream| { + Box::pin(async move { + let closeframe = CloseFrame { + code: close_code, + reason: reason.into(), + }; + let _ = stream.close(Some(closeframe)).await; + + Ok(()) + }) + }) } #[get("/room/?")] -async fn chat( +async fn room( id: &str, player_name: &str, ws: ws::WebSocket, @@ -349,10 +374,8 @@ async fn chat( let mut rooms = rooms.lock().await; let room = rooms.get(id); - // TODO extract from cookies let player = Player { name: player_name.to_string(), - id: Uuid::new_v4(), }; fn make_join_event(room: &Room, player: &Player) -> ServerToClientMessage { @@ -364,40 +387,80 @@ async fn chat( } } - let (room, mut receiver, sender) = if room.is_none_or(|x| x.strong_count() == 0) { - println!("Creating new room"); - let room = Room::new(player.clone()); - let event = make_join_event(&room, &player); - - let sender = room.sender.clone(); - let receiver = sender.subscribe(); - - let arc = Arc::new(RwLock::new(room)); - - rooms.insert(id.to_string(), Arc::downgrade(&arc)); - sender.send(InnerRoomMessage::PassThrough(event)).unwrap(); - - (arc, receiver, sender) - } else { - let a = room.unwrap(); - let b = a.clone(); - let c = b.upgrade(); - let d = c.unwrap(); - - // need to add player to group - let (sender, event) = { - let mut room = d.write().await; - room.party_info.players.push(player.clone()); - let sender = room.sender.clone(); + let (room, mut receiver, sender, trigger_game_load) = + if room.is_none_or(|x| x.strong_count() == 0) { + println!("Creating new room"); + let room = Room::new(player.clone()); let event = make_join_event(&room, &player); - (sender, event) - }; - let receiver = sender.subscribe(); - sender.send(InnerRoomMessage::PassThrough(event)).unwrap(); + let sender = room.sender.clone(); + let receiver = sender.subscribe(); - (d, receiver, sender) - }; + let arc = Arc::new(RwLock::new(room)); + + rooms.insert(id.to_string(), Arc::downgrade(&arc)); + sender + .send((None, InnerRoomMessage::PassThrough(event))) + .unwrap(); + + (arc, receiver, sender, false) + } else { + let a = room.unwrap(); + let b = a.clone(); + let c = b.upgrade(); + let d = c.unwrap(); + + let mut trigger_game_load = false; + + // need to add player to group + let (sender, event) = { + let mut room = d.write().await; + + // check if player is already in the room. If they are, don't allow the new connection + if room.party_info.players.contains(&player) { + return reject_websocket_with_reason( + ws, + CloseCode::Protocol, + format!("{} is already in the room", player.name), + ); + } + + if let Some(game) = &room.game { + if game + .0 + .player_states + .get_player_state(&player.name) + .is_none() + { + // Game is in progress and our new player isn't a member + return reject_websocket_with_reason( + ws, + CloseCode::Protocol, + "The game is already in-progress".to_string(), + ); + } + trigger_game_load = true; + } + + room.party_info.players.push(player.clone()); + let sender = room.sender.clone(); + let event = make_join_event(&room, &player); + + (sender, event) + }; + let receiver = sender.subscribe(); + sender + .send((None, InnerRoomMessage::PassThrough(event))) + .unwrap(); + + (d, receiver, sender, trigger_game_load) + }; + + if trigger_game_load { + sender + .send((Some(player.clone()), InnerRoomMessage::GameEvent(None))) + .unwrap(); + } ws.channel(move |mut stream| { Box::pin(async move { @@ -429,5 +492,5 @@ async fn chat( fn rocket() -> _ { rocket::build() .manage(Mutex::new(RoomMap::new())) - .mount("/", routes![chat]) + .mount("/", routes![room]) } diff --git a/ui/src/Game.tsx b/ui/src/Game.tsx index 5b5cc7d..11c1e34 100644 --- a/ui/src/Game.tsx +++ b/ui/src/Game.tsx @@ -269,7 +269,7 @@ export function Game(props: { continue } - const letters = state.remaining_tiles.get(name); + const letters = state.remaining_tiles[name]; if (letters.length == 0) { logDispatch(
{name} has no remaining tiles.
); } else { diff --git a/ui/src/api.ts b/ui/src/api.ts index 7e85070..a9071e5 100644 --- a/ui/src/api.ts +++ b/ui/src/api.ts @@ -41,7 +41,7 @@ export enum CellType { Start = "Start", } -export type GameState = { type: "InProgress" } | { type: "Ended"; finisher: string | undefined; remaining_tiles: Map }; +export type GameState = { type: "InProgress" } | { type: "Ended"; finisher: string | undefined; remaining_tiles: {[id: string]: Letter[]} }; export interface APIPlayer { name: string; diff --git a/ui/src/multiplayer.tsx b/ui/src/multiplayer.tsx index 455a289..046e2b8 100644 --- a/ui/src/multiplayer.tsx +++ b/ui/src/multiplayer.tsx @@ -49,7 +49,7 @@ export function Menu(): React.JSX.Element { return

Connecting to {roomName}

} else if (partyInfo != null) { const players = partyInfo.players.map((x) => { - return
  • {x.name}
  • ; + return
  • {x.name}
  • ; }); const ais = partyInfo.ais.map((x, i) => { return
  • @@ -137,6 +137,14 @@ export function Menu(): React.JSX.Element { } console.log("Message from server ", event.data); }); + socket.addEventListener("close", (event) => { + console.log({event}); + setSocket(null); + setGame(null); + if(event.reason != null && event.reason.length > 0) { + alert(`Disconnected with reason "${event.reason} & code ${event.code}"`); + } + }); setSocket(socket); }}>Connect diff --git a/ui/src/utils.ts b/ui/src/utils.ts index 11962ef..44949f3 100644 --- a/ui/src/utils.ts +++ b/ui/src/utils.ts @@ -119,7 +119,7 @@ export function mergeTrays(existing: PlayableLetterData[], newer: (LetterData | } existing.filter((x) => { - return x !== undefined && x !== null; + return x != null; }).forEach((x) => { if (x.location === LocationType.TRAY) { freeSpots[x.index] = null; @@ -138,9 +138,9 @@ export function mergeTrays(existing: PlayableLetterData[], newer: (LetterData | } return newer.map((ld, i) => { - if (ld !== undefined) { + if (ld != null) { - if (existing[i] !== undefined && existing[i] !== null && existing[i].location === LocationType.TRAY) { + if (existing[i] != null && existing[i].location === LocationType.TRAY) { ld["index"] = existing[i].index; } else { ld["index"] = firstNotNull();