Skip to content

Commit

Permalink
Merge pull request #41 from pilksoc/sf-be-removeplayers
Browse files Browse the repository at this point in the history
BE: Remove players from grid on disconnect
  • Loading branch information
djpiper28 authored Mar 16, 2024
2 parents e21dbb3 + b0f5c61 commit 881a5ed
Show file tree
Hide file tree
Showing 7 changed files with 102 additions and 60 deletions.
1 change: 0 additions & 1 deletion backend/src/grid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions backend/src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ use crate::{ws, Clients, Result};
use warp::Reply;

pub async fn ws_handler(ws: warp::ws::Ws, clients: Clients) -> Result<impl Reply> {
//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<impl Reply> {
Expand Down
14 changes: 13 additions & 1 deletion backend/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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<mpsc::UnboundedSender<std::result::Result<Message, warp::Error>>>,
pub last_position: Coordinate,
}

pub type Coordinate = [u64; 2];
pub type Clients = Arc<Mutex<HashMap<String, Client>>>;

static SIZE: u64 = 2048;

lazy_static! {
pub static ref WORLD: Mutex<Grid> = Mutex::new(Grid::new(SIZE, SIZE));
pub static ref CLIENTS: Clients = Arc::new(Mutex::new(HashMap::new()));
}

20 changes: 6 additions & 14 deletions backend/src/main.rs
Original file line number Diff line number Diff line change
@@ -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<mpsc::UnboundedSender<std::result::Result<Message, warp::Error>>>,
}

// type aliases!
type Clients = Arc<Mutex<HashMap<String, Client>>>;
type Result<T> = std::result::Result<T, Rejection>;


#[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());
Expand Down
47 changes: 33 additions & 14 deletions backend/src/modify_gamestate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,31 @@ 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
/// 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<Action>, // 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<Action>
}

/// 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,
}

Expand All @@ -40,39 +49,49 @@ 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);
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 {
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,
ActionType::Place => kube_result = SpaceKind::Kube(action.kube),
}

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
54 changes: 36 additions & 18 deletions backend/src/ws.rs
Original file line number Diff line number Diff line change
@@ -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, Coordinate};
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.
Expand All @@ -30,15 +32,24 @@ pub async fn client_connection(ws: WebSocket, clients: Clients) {
// this might be clapped
let uuid = Uuid::new_v4().simple().to_string();

// 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),
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.
clients.lock().await.insert(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 {
Expand All @@ -48,35 +59,42 @@ 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);
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
}

async fn add_player(uuid: String, new_client: Client) {
CLIENTS.lock().await.insert(uuid, new_client);
}

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
// 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() {
Ok(v) => v,
Err(_) => return,
};
let Ok(message) = msg.to_str() else { return };

//println!("{}", message);
//println!("{message}");

let locked = clients.lock().await;
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))));
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;
}
22 changes: 12 additions & 10 deletions backend/src/ws/gen_json.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -18,16 +17,20 @@ 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;
let player_location = state.coordinates;

modify_gamestate(state);
modify_gamestate(state).await;

let new_grid: LocalGrid =
LocalGrid::from_grid_and_coord(&WORLD.lock().unwrap(), player_location, 48);
// 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 {
Expand All @@ -36,19 +39,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 async fn create_response(message: &str, client_id: &str) -> String {
match serde_json::from_str::<PlayerInfo>(message) {
Ok(info) => recalculate_game(info),
Ok(info) => recalculate_game(info, client_id).await,
Err(_) => "Ding Dong!!! your json is WRONG".to_string(),
}
}

0 comments on commit 881a5ed

Please sign in to comment.