From a1f0045476d64e9dd908bb836b17e33014ba97d9 Mon Sep 17 00:00:00 2001 From: Steph Flower Date: Tue, 5 Mar 2024 22:45:27 +0000 Subject: [PATCH 1/3] BE: Remove players from grid on disconnect --- backend/src/handlers.rs | 4 ++-- backend/src/lib.rs | 14 +++++++++++++- backend/src/main.rs | 20 ++++++-------------- backend/src/modify_gamestate.rs | 28 +++++++++++++++++++--------- backend/src/ws.rs | 33 +++++++++++++++++++++++---------- backend/src/ws/gen_json.rs | 14 ++++++-------- 6 files changed, 69 insertions(+), 44 deletions(-) diff --git a/backend/src/handlers.rs b/backend/src/handlers.rs index 479c911..b22c798 100644 --- a/backend/src/handlers.rs +++ b/backend/src/handlers.rs @@ -2,9 +2,9 @@ use crate::{ws, Clients, Result}; use warp::Reply; pub async fn ws_handler(ws: warp::ws::Ws, clients: Clients) -> Result { - //println!("ws_handler"); //debug + println!("ws_handler"); //debug - Ok(ws.on_upgrade(move |socket| ws::client_connection(socket, clients))) + Ok(ws.on_upgrade(move |socket| ws::client_connection(socket))) } pub async fn metrics_handler(clients: Clients) -> Result { diff --git a/backend/src/lib.rs b/backend/src/lib.rs index 55293a0..5149505 100644 --- a/backend/src/lib.rs +++ b/backend/src/lib.rs @@ -1,4 +1,6 @@ -use std::sync::Mutex; +use std::{collections::HashMap, sync::Arc}; +use tokio::sync::{mpsc, Mutex}; +use warp::filters::ws::Message; use crate::grid::Grid; @@ -15,11 +17,21 @@ pub mod modify_gamestate; #[macro_use] extern crate lazy_static; +// type that represents a connecting client +#[derive(Debug, Clone)] +pub struct Client { + pub client_id: String, + pub sender: Option>>, + pub last_position: Coordinate, +} + pub type Coordinate = [u64; 2]; +pub type Clients = Arc>>; static SIZE: u64 = 2048; lazy_static! { pub static ref WORLD: Mutex = Mutex::new(Grid::new(SIZE, SIZE)); + pub static ref CLIENTS: Clients = Arc::new(Mutex::new(HashMap::new())); } diff --git a/backend/src/main.rs b/backend/src/main.rs index 5033bfa..440906a 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -1,33 +1,25 @@ -use std::{collections::HashMap, convert::Infallible, sync::Arc}; -use tokio::sync::{mpsc, Mutex}; -use warp::{filters::ws::Message, Filter, Rejection}; +use std::convert::Infallible; +use cosmic_kube::{Clients, CLIENTS}; +use warp::{Filter, Rejection}; mod handlers; mod ws; -// type that represents a connecting client -#[derive(Debug, Clone)] -pub struct Client { - pub client_id: String, - pub sender: Option>>, -} - // type aliases! -type Clients = Arc>>; type Result = std::result::Result; + #[tokio::main] async fn main() { //initialise a hashmap to store currently connected clients. We may want some more logic here if we want currently connected clients to be stored somewhere - let clients: Clients = Arc::new(Mutex::new(HashMap::new())); println!("Configuring websocket route"); //debug let ws_route = warp::path("ws") .and(warp::ws()) - .and(with_clients(clients.clone())) + .and(with_clients(CLIENTS.clone())) .and_then(handlers::ws_handler) .or(warp::path("metrics") - .and(with_clients(clients.clone())) + .and(with_clients(CLIENTS.clone())) .and_then(handlers::metrics_handler)); let routes = ws_route.with(warp::cors().allow_any_origin()); diff --git a/backend/src/modify_gamestate.rs b/backend/src/modify_gamestate.rs index 4ba16de..7562c61 100644 --- a/backend/src/modify_gamestate.rs +++ b/backend/src/modify_gamestate.rs @@ -5,7 +5,7 @@ use std::fmt; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; -use crate::{kube::Kube, player::Player, space::{Space, SpaceKind}, Coordinate, WORLD}; +use crate::{kube::Kube, player::Player, space::{Space, SpaceKind}, Coordinate, WORLD, CLIENTS}; // this is the data we expect to recieve from the player #[derive(Serialize, Deserialize)] @@ -40,18 +40,18 @@ pub struct Action { pub coordinates: Coordinate, } -pub fn modify_gamestate(player_state: PlayerInfo) { +pub async fn modify_gamestate(player_state: PlayerInfo) { // move the player's position on the grid move_player(player_state.old_coordinates, player_state.coordinates, player_state.player); // then we want to update the grid by performing action match player_state.action { - Some(p) => perform_action(p), - _ => (), + Some(p) => perform_action(p).await, + None => (), } } -pub fn perform_action(action: Action) { +pub async fn perform_action(action: Action) { let kube_result: SpaceKind; match action.kind { ActionType::Pickup => kube_result = SpaceKind::EmptySpace, @@ -59,20 +59,30 @@ pub fn perform_action(action: Action) { } let space_in_question: Space = Space::new(action.coordinates, kube_result); - WORLD.lock().unwrap().insert(space_in_question); + WORLD.lock().await.insert(space_in_question); } -pub fn move_player(old_pos: Option<[u64; 2]>, new_pos: [u64; 2], player: Player) { +pub async fn move_player(old_pos: Option<[u64; 2]>, new_pos: [u64; 2], player: Player) { + + let player_key = player.uuid.to_string(); + //remove the players old location in the world, if provided match old_pos { - Some(c) => WORLD.lock().unwrap().insert(Space::new(c, SpaceKind::EmptySpace)), + Some(c) => WORLD.lock().await.insert(Space::new(c, SpaceKind::EmptySpace)), _ => (), } // store the players location in the world let playerspace: Space = Space::new(new_pos, SpaceKind::Player(player)); - WORLD.lock().unwrap().insert(playerspace); + WORLD.lock().await.insert(playerspace); + + //we now store the player's last known location in the 'active clients' hashmap + CLIENTS.lock().await.entry(player_key).and_modify(|client| client.last_position = new_pos); +} + +pub async fn remove_player(player_location: Coordinate) { + WORLD.lock().await.insert(Space::new(player_location, SpaceKind::EmptySpace)); } // we can write some tests for these methods down here if anyone fancies it diff --git a/backend/src/ws.rs b/backend/src/ws.rs index dd5edc1..a015dc8 100644 --- a/backend/src/ws.rs +++ b/backend/src/ws.rs @@ -1,12 +1,14 @@ -use crate::{ws::gen_json::create_response, Client, Clients}; +use crate::ws::gen_json::create_response; +use cosmic_kube::{modify_gamestate::remove_player, CLIENTS, Client}; use futures::{FutureExt, StreamExt}; +use rand::Rng; use tokio::sync::mpsc; use tokio_stream::wrappers::UnboundedReceiverStream; use uuid::Uuid; use warp::ws::{Message, WebSocket}; mod gen_json; -pub async fn client_connection(ws: WebSocket, clients: Clients) { +pub async fn client_connection(ws: WebSocket) { println!("establishing client connection... {:?}", ws); //debug // splitting the WebSocket stream object into separate 'Sink' and 'Stream' objects. @@ -29,16 +31,19 @@ pub async fn client_connection(ws: WebSocket, clients: Clients) { // creating a new uuid to use as the key in the 'clients' hashmap, and a new instance of a 'client' // this might be clapped let uuid = Uuid::new_v4().simple().to_string(); + let mut rng = rand::thread_rng(); let new_client = Client { client_id: uuid.clone(), //the client_sender object is stored within this new client instance so that we can send messages to this connected client in other parts of the code sender: Some(client_sender), + //we randomly generate the initial position of the player + //reduced to 20 for debugging purposes, for the live game we should set this back to grid size (2048) + last_position: [rng.gen_range(0..20), rng.gen_range(0..20)], }; //obtains a lock on the client list and inserts the new client into the hashmap using the uuid as the key. - clients.lock().await.insert(uuid.clone(), new_client); - + add_player(uuid.clone(), new_client); // creates a loop that handles incoming messages from the client while let Some(result) = client_ws_rcv.next().await { let msg = match result { @@ -48,18 +53,26 @@ pub async fn client_connection(ws: WebSocket, clients: Clients) { break; } }; - client_msg(&uuid, msg, &clients).await; + client_msg(&uuid, msg).await; } - // as the above will keep running as long as the client is active, when we exit the loop we can safely remove this client instance from the hashmap. - clients.lock().await.remove(&uuid); + // as the above will keep running as long as the client is active, when we exit the loop we can safely remove this client instance from the hashmap, after we have removed it's position from the grid. + call_remove_player(&uuid); println!("{} disconnected", uuid); //debug } +fn add_player(uuid: String, new_client: Client) { + CLIENTS.lock().unwrap().insert(uuid, new_client); +} + +fn call_remove_player(uuid: &str) { + remove_player(CLIENTS.lock().unwrap().get(uuid).unwrap().last_position); + CLIENTS.lock().unwrap().remove(uuid); +} // ->recieve client game info <- send back client game state // wwwwwwwwwwwwwwwwwwwww i am so tired -async fn client_msg(client_id: &str, msg: Message, clients: &Clients) { +async fn client_msg(client_id: &str, msg: Message) { //println!("received message from {}: {:?}", client_id, msg); //debug let message = match msg.to_str() { @@ -69,11 +82,11 @@ async fn client_msg(client_id: &str, msg: Message, clients: &Clients) { //println!("{}", message); - let locked = clients.lock().await; + let locked = CLIENTS.lock().unwrap(); match locked.get(client_id) { Some(v) => { if let Some(sender) = &v.sender { - let _ = sender.send(Ok(Message::text(create_response(message)))); + let _ = sender.send(Ok(Message::text(create_response(message, client_id)))); } } None => return, diff --git a/backend/src/ws/gen_json.rs b/backend/src/ws/gen_json.rs index 7189656..815e84b 100644 --- a/backend/src/ws/gen_json.rs +++ b/backend/src/ws/gen_json.rs @@ -1,6 +1,5 @@ use cosmic_kube::local_grid::LocalGrid; use cosmic_kube::modify_gamestate::{modify_gamestate, PlayerInfo}; -use rand::Rng; use serde_json::{json, Value}; use cosmic_kube::WORLD; @@ -18,7 +17,7 @@ fn debug_message(state: &PlayerInfo) { } } -fn recalculate_game(state: PlayerInfo) -> String { +async fn recalculate_game(state: PlayerInfo, id: &str) -> String { debug_message(&state); //debug let player_initialised = state.initialised; @@ -27,7 +26,7 @@ fn recalculate_game(state: PlayerInfo) -> String { modify_gamestate(state); let new_grid: LocalGrid = - LocalGrid::from_grid_and_coord(&WORLD.lock().unwrap(), player_location, 48); + LocalGrid::from_grid_and_coord(&WORLD.lock().await.unwrap(), player_location, 48); let resp: Value; if player_initialised { @@ -36,19 +35,18 @@ fn recalculate_game(state: PlayerInfo) -> String { "grid" : new_grid, }); } else { - let mut rng = rand::thread_rng(); resp = json!({ - //reduced to 20 for debugging purposes, for the live game we should set this back to grid size (2048) - "coordinates" : [rng.gen_range(0..20), rng.gen_range(0..20)] + "coordinates" : player_location, + "uuid" : id }); } resp.to_string() } -pub fn create_response(message: &str) -> String { +pub fn create_response(message: &str, client_id: &str) -> String { match serde_json::from_str::(message) { - Ok(info) => recalculate_game(info), + Ok(info) => recalculate_game(info, client_id), Err(_) => "Ding Dong!!! your json is WRONG".to_string(), } } From 90c67665c65b27db53c40e12f59ac2ac1173af0b Mon Sep 17 00:00:00 2001 From: ettolrach Date: Wed, 6 Mar 2024 01:10:14 +0000 Subject: [PATCH 2/3] Fix handlers.rs and missing awaits --- backend/src/grid.rs | 1 - backend/src/modify_gamestate.rs | 19 +++++++++++----- backend/src/ws.rs | 40 +++++++++++++++++++-------------- backend/src/ws/gen_json.rs | 16 ++++++++----- 4 files changed, 47 insertions(+), 29 deletions(-) diff --git a/backend/src/grid.rs b/backend/src/grid.rs index f2b7606..96bc9a8 100644 --- a/backend/src/grid.rs +++ b/backend/src/grid.rs @@ -80,7 +80,6 @@ impl Grid { else { None } - } /// Checks that a coordinate is not beyond the bounds of the grid. pub fn in_bounds(&self, coordinate: [u64; 2]) -> bool { diff --git a/backend/src/modify_gamestate.rs b/backend/src/modify_gamestate.rs index 7562c61..67e5cc6 100644 --- a/backend/src/modify_gamestate.rs +++ b/backend/src/modify_gamestate.rs @@ -8,19 +8,28 @@ use serde_repr::{Deserialize_repr, Serialize_repr}; use crate::{kube::Kube, player::Player, space::{Space, SpaceKind}, Coordinate, WORLD, CLIENTS}; // this is the data we expect to recieve from the player +/// The data to be received from the player's client. #[derive(Serialize, Deserialize)] pub struct PlayerInfo { + /// Whether the player is new to the game. pub initialised: bool, - pub player: Player, //Player, //the player requesting the data - pub coordinates: [u64; 2], //current player coordinates - old_coordinates: Option<[u64; 2]>, //where the player was previously - pub action: Option, // 0, block picked up 1, block placed + // The player requesting the data. + pub player: Player, + /// Current player coordinates. + pub coordinates: [u64; 2], + /// Where the player was previously. + old_coordinates: Option<[u64; 2]>, + /// The action the player performed, if any. Represented using 0 for block picked up, 1 for block placed. + pub action: Option } +/// The type of action performed. #[derive(Serialize_repr, Deserialize_repr)] #[repr(u8)] pub enum ActionType { + /// The player has picked up a block. Pickup = 0, + /// The player has placed a block. Place = 1, } @@ -42,7 +51,7 @@ pub struct Action { pub async fn modify_gamestate(player_state: PlayerInfo) { // move the player's position on the grid - move_player(player_state.old_coordinates, player_state.coordinates, player_state.player); + move_player(player_state.old_coordinates, player_state.coordinates, player_state.player).await; // then we want to update the grid by performing action match player_state.action { diff --git a/backend/src/ws.rs b/backend/src/ws.rs index a015dc8..c678008 100644 --- a/backend/src/ws.rs +++ b/backend/src/ws.rs @@ -1,5 +1,5 @@ use crate::ws::gen_json::create_response; -use cosmic_kube::{modify_gamestate::remove_player, CLIENTS, Client}; +use cosmic_kube::{modify_gamestate::remove_player, CLIENTS, Client, Coordinate}; use futures::{FutureExt, StreamExt}; use rand::Rng; use tokio::sync::mpsc; @@ -31,19 +31,25 @@ pub async fn client_connection(ws: WebSocket) { // creating a new uuid to use as the key in the 'clients' hashmap, and a new instance of a 'client' // this might be clapped let uuid = Uuid::new_v4().simple().to_string(); - let mut rng = rand::thread_rng(); + + // we randomly generate the initial position of the player. + // reduced to 20 for debugging purposes, for the live game we should set this back to grid size (2048) + // To make it explicit to the compiler that `rng` is only used for a short time, put it in a scope. + let random_initial_pos: Coordinate; + { + let mut rng = rand::thread_rng(); + random_initial_pos = [rng.gen_range(0..20), rng.gen_range(0..20)]; + } let new_client = Client { client_id: uuid.clone(), //the client_sender object is stored within this new client instance so that we can send messages to this connected client in other parts of the code sender: Some(client_sender), - //we randomly generate the initial position of the player - //reduced to 20 for debugging purposes, for the live game we should set this back to grid size (2048) - last_position: [rng.gen_range(0..20), rng.gen_range(0..20)], + last_position: random_initial_pos, }; //obtains a lock on the client list and inserts the new client into the hashmap using the uuid as the key. - add_player(uuid.clone(), new_client); + add_player(uuid.clone(), new_client).await; // creates a loop that handles incoming messages from the client while let Some(result) = client_ws_rcv.next().await { let msg = match result { @@ -56,18 +62,18 @@ pub async fn client_connection(ws: WebSocket) { client_msg(&uuid, msg).await; } - // as the above will keep running as long as the client is active, when we exit the loop we can safely remove this client instance from the hashmap, after we have removed it's position from the grid. - call_remove_player(&uuid); - println!("{} disconnected", uuid); //debug + // as the above will keep running as long as the client is active, when we exit the loop we can safely remove this client instance from the hashmap, after we have removed its position from the grid. + call_remove_player(&uuid).await; + println!("{uuid} disconnected"); //debug } -fn add_player(uuid: String, new_client: Client) { - CLIENTS.lock().unwrap().insert(uuid, new_client); +async fn add_player(uuid: String, new_client: Client) { + CLIENTS.lock().await.insert(uuid, new_client); } -fn call_remove_player(uuid: &str) { - remove_player(CLIENTS.lock().unwrap().get(uuid).unwrap().last_position); - CLIENTS.lock().unwrap().remove(uuid); +async fn call_remove_player(uuid: &str) { + remove_player(CLIENTS.lock().await.get(uuid).unwrap().last_position).await; + CLIENTS.lock().await.remove(uuid); } // ->recieve client game info <- send back client game state @@ -80,13 +86,13 @@ async fn client_msg(client_id: &str, msg: Message) { Err(_) => return, }; - //println!("{}", message); + //println!("{message}"); - let locked = CLIENTS.lock().unwrap(); + let locked = CLIENTS.lock().await; match locked.get(client_id) { Some(v) => { if let Some(sender) = &v.sender { - let _ = sender.send(Ok(Message::text(create_response(message, client_id)))); + let _ = sender.send(Ok(Message::text(create_response(message, client_id).await))); } } None => return, diff --git a/backend/src/ws/gen_json.rs b/backend/src/ws/gen_json.rs index 815e84b..3becd0e 100644 --- a/backend/src/ws/gen_json.rs +++ b/backend/src/ws/gen_json.rs @@ -23,10 +23,14 @@ async fn recalculate_game(state: PlayerInfo, id: &str) -> String { let player_initialised = state.initialised; let player_location = state.coordinates; - modify_gamestate(state); - - let new_grid: LocalGrid = - LocalGrid::from_grid_and_coord(&WORLD.lock().await.unwrap(), player_location, 48); + modify_gamestate(state).await; + + // The dereferencing looks a little weird. Here's what's going on: + // Tokio's Mutex when locking returns a MutexGuard. + // This is the same behaviour as std::sync::Mutex. + // Thus, we first need to dereference it to get to the actual Grid type, + // and then send a reference to the Grid type to the LocalGrid constructor. + let new_grid = LocalGrid::from_grid_and_coord(&(*WORLD.lock().await), player_location, 48); let resp: Value; if player_initialised { @@ -44,9 +48,9 @@ async fn recalculate_game(state: PlayerInfo, id: &str) -> String { resp.to_string() } -pub fn create_response(message: &str, client_id: &str) -> String { +pub async fn create_response(message: &str, client_id: &str) -> String { match serde_json::from_str::(message) { - Ok(info) => recalculate_game(info, client_id), + Ok(info) => recalculate_game(info, client_id).await, Err(_) => "Ding Dong!!! your json is WRONG".to_string(), } } From b0f5c6126921c7de45e237b425c08414ca23f125 Mon Sep 17 00:00:00 2001 From: ettolrach Date: Fri, 15 Mar 2024 21:18:58 +0000 Subject: [PATCH 3/3] Add stderr feedback when client ID not found --- backend/src/ws.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/backend/src/ws.rs b/backend/src/ws.rs index c678008..dc48bcb 100644 --- a/backend/src/ws.rs +++ b/backend/src/ws.rs @@ -81,10 +81,7 @@ async fn call_remove_player(uuid: &str) { async fn client_msg(client_id: &str, msg: Message) { //println!("received message from {}: {:?}", client_id, msg); //debug - let message = match msg.to_str() { - Ok(v) => v, - Err(_) => return, - }; + let Ok(message) = msg.to_str() else { return }; //println!("{message}"); @@ -95,7 +92,9 @@ async fn client_msg(client_id: &str, msg: Message) { let _ = sender.send(Ok(Message::text(create_response(message, client_id).await))); } } - None => return, + None => { + eprintln!("Couldn't find game client in client hashmap! Client ID: {client_id}"); + return + }, } - return; }