From 7a13df6ef4132f9e7372e2acbde110b7d7593dd7 Mon Sep 17 00:00:00 2001 From: "huy.nguyen@setel.com" Date: Mon, 11 Dec 2023 21:31:49 +0700 Subject: [PATCH 1/8] chore: caching room session w/ redis --- client/examples/chaser.rs | 3 +- client/examples/complex/src/main.rs | 3 +- client/examples/jakebot.rs | 8 ++- client/examples/simple.rs | 7 +- client/src/lib.rs | 9 +-- server/Cargo.toml | 1 + server/src/actors/client_ws_actor.rs | 18 +++++- server/src/actors/mod.rs | 2 + server/src/actors/redis_actor.rs | 97 ++++++++++++++++++++++++++++ server/src/controllers/api.rs | 29 +++++++-- server/src/main.rs | 7 ++ server/src/models/messages.rs | 44 +++++++++++++ tokyo.toml | 1 + 13 files changed, 210 insertions(+), 19 deletions(-) create mode 100644 server/src/actors/redis_actor.rs diff --git a/client/examples/chaser.rs b/client/examples/chaser.rs index c5adfe6..9f1d743 100644 --- a/client/examples/chaser.rs +++ b/client/examples/chaser.rs @@ -54,7 +54,8 @@ impl Handler for Player { fn main() { let api_key = &env::var("API_KEY").unwrap_or("a".into()); let team_name = &env::var("TEAM_NAME").unwrap_or("a".into()); + let room_token = &env::var("ROOM_TOKEN").unwrap_or("a".into()); println!("starting up..."); - tokyo::run(api_key, team_name, Player::default()).unwrap(); + tokyo::run(api_key, team_name, room_token, Player::default()).unwrap(); } diff --git a/client/examples/complex/src/main.rs b/client/examples/complex/src/main.rs index dd34cfe..2de20cc 100644 --- a/client/examples/complex/src/main.rs +++ b/client/examples/complex/src/main.rs @@ -102,7 +102,8 @@ fn main() { // TODO: Substitute with your API key and team name. let api_key = &env::var("API_KEY").unwrap_or("a".into()); let team_name = &env::var("TEAM_NAME").unwrap_or("ThucUnbelievale".into()); + let room_token = &env::var("ROOM_TOKEN").unwrap_or("a".into()); println!("starting up..."); - tokyo::run(api_key, team_name, Player::new()).unwrap(); + tokyo::run(api_key, team_name, room_token, Player::new()).unwrap(); } diff --git a/client/examples/jakebot.rs b/client/examples/jakebot.rs index 58d48e3..afda655 100644 --- a/client/examples/jakebot.rs +++ b/client/examples/jakebot.rs @@ -73,8 +73,10 @@ impl Handler for Player { } fn main() { + let api_key = &env::var("API_KEY").unwrap_or("player1".into()); + let team_name = &env::var("TEAM_NAME").unwrap_or("player1".into()); + let room_token = &env::var("ROOM_TOKEN").unwrap_or("OBx4o6Iq".into()); + println!("starting up..."); - let api_key = &env::var("API_KEY").unwrap_or("a".into()); - let team_name = &env::var("TEAM_NAME").unwrap_or("a".into()); - tokyo::run(api_key, team_name, Player::default()).unwrap(); + tokyo::run(api_key, team_name, room_token, Player::default()).unwrap(); } diff --git a/client/examples/simple.rs b/client/examples/simple.rs index 24282ca..59b905c 100644 --- a/client/examples/simple.rs +++ b/client/examples/simple.rs @@ -46,9 +46,10 @@ impl Handler for Player { fn main() { // TODO: Substitute with your API key and team name. - let api_key = &env::var("API_KEY").unwrap_or("a".into()); - let team_name = &env::var("TEAM_NAME").unwrap_or("a".into()); + let api_key = &env::var("API_KEY").unwrap_or("player0".into()); + let team_name = &env::var("TEAM_NAME").unwrap_or("player0".into()); + let room_token = &env::var("ROOM_TOKEN").unwrap_or("OBx4o6Iq".into()); println!("starting up..."); - tokyo::run(api_key, team_name, Player::default()).unwrap(); + tokyo::run(api_key, team_name, room_token, Player::default()).unwrap(); } diff --git a/client/src/lib.rs b/client/src/lib.rs index 4f9951a..d08098c 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -107,16 +107,17 @@ where /// Begin the client-side game loop, using the provided struct that implements `Handler` /// to act on behalf of the player. -pub fn run(key: &str, name: &str, handler: H) -> Result<(), Error> +pub fn run(key: &str, name: &str, room_token: &str, handler: H) -> Result<(), Error> where H: Handler + Send + 'static, { - let host = env::var("SERVER_HOST").unwrap_or("192.168.0.199".into()); + let host = env::var("SERVER_HOST").unwrap_or("127.0.0.1:8080".into()); let url = Url::parse(&format!( - "wss://{}/socket?key={}&name={}", + "ws://{}/socket?key={}&name={}&room_token={}", host, key, - utf8_percent_encode(name, DEFAULT_ENCODE_SET).to_string() + utf8_percent_encode(name, DEFAULT_ENCODE_SET).to_string(), + room_token, ))?; let client_state = diff --git a/server/Cargo.toml b/server/Cargo.toml index 5e48715..bfd4325 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -23,3 +23,4 @@ listenfd = "0.3" failure = "0.1" futures = "0.1" url = "1.7" +redis = "0.24.0" diff --git a/server/src/actors/client_ws_actor.rs b/server/src/actors/client_ws_actor.rs index f2b952a..8c315ff 100644 --- a/server/src/actors/client_ws_actor.rs +++ b/server/src/actors/client_ws_actor.rs @@ -1,5 +1,5 @@ use crate::{ - actors::GameActor, + actors::{GameActor, RedisActor}, models::messages::{ClientStop, PlayerGameCommand}, AppState, }; @@ -13,18 +13,20 @@ const ACTIONS_PER_SECOND: u32 = 22; #[derive(Debug)] pub struct ClientWsActor { game_addr: Addr, + redis_actor_addr: Addr, + room_token: String, api_key: String, team_name: String, rate_limiter: DirectRateLimiter, } impl ClientWsActor { - pub fn new(game_addr: Addr, api_key: String, team_name: String) -> ClientWsActor { + pub fn new(game_addr: Addr, redis_actor_addr: Addr, api_key: String, team_name: String, room_token: String) -> ClientWsActor { let rate_limiter = DirectRateLimiter::::per_second( std::num::NonZeroU32::new(ACTIONS_PER_SECOND).unwrap(), ); - ClientWsActor { game_addr, api_key, team_name, rate_limiter } + ClientWsActor { game_addr, redis_actor_addr, api_key, team_name, room_token, rate_limiter } } } @@ -37,6 +39,12 @@ impl Actor for ClientWsActor { self.team_name.clone(), ctx.address(), )); + if self.api_key != "SPECTATOR" { + self.redis_actor_addr.do_send(crate::models::messages::AddRoomPlayerCommand{ + room_token: self.room_token.clone(), + player_key: self.api_key.clone(), + }); + } } fn stopped(&mut self, ctx: &mut Self::Context) { @@ -45,6 +53,10 @@ impl Actor for ClientWsActor { self.api_key.clone(), ctx.address(), )); + self.redis_actor_addr.do_send(crate::models::messages::RemoveRoomPlayerCommand{ + room_token: self.room_token.clone(), + player_key: self.api_key.clone(), + }); } } diff --git a/server/src/actors/mod.rs b/server/src/actors/mod.rs index ad6d464..4181166 100644 --- a/server/src/actors/mod.rs +++ b/server/src/actors/mod.rs @@ -1,8 +1,10 @@ pub mod client_ws_actor; pub mod game_actor; +pub mod redis_actor; pub use client_ws_actor::ClientWsActor; pub use game_actor::GameActor; pub mod room_manager_actor; pub use room_manager_actor::{CreateRoom, JoinRoom, ListRooms, RoomManagerActor}; +pub use redis_actor::RedisActor; \ No newline at end of file diff --git a/server/src/actors/redis_actor.rs b/server/src/actors/redis_actor.rs new file mode 100644 index 0000000..e275a84 --- /dev/null +++ b/server/src/actors/redis_actor.rs @@ -0,0 +1,97 @@ +use actix::prelude::*; +use redis::{Client, Commands, Connection, RedisResult}; + +use crate::models::messages::{GetRoomFieldCommand, UpdateRoomFieldCommand, SetRoomCommand, GetRoomSizeCommand, AddRoomPlayerCommand, RemoveRoomPlayerCommand}; + +#[derive(Debug)] +pub struct RedisActor { + client: Client, +} + +impl Actor for RedisActor { + type Context = Context; +} + +impl Handler for RedisActor { + type Result = Result; + + fn handle(&mut self, msg: GetRoomFieldCommand, _: &mut Self::Context) -> Self::Result { + let mut con: Connection = self.client.get_connection()?; + let query_key = format!("room:{}", msg.room_token); + let result: RedisResult = con.hget(query_key, msg.field); + result.map_err(|e| e.into()) + } +} + +impl Handler for RedisActor { + type Result = Result; + + fn handle(&mut self, msg: UpdateRoomFieldCommand, _: &mut Self::Context) -> Self::Result { + let mut con: Connection = self.client.get_connection()?; + + // Clone the values before moving them into the Redis `hset` command + let query_key = format!("room:{}", msg.room_token.clone()); + let field = msg.field.clone(); + let value = msg.value.clone(); + + let result: RedisResult<()> = con.hset(query_key, field, value); + result + .map_err(|e| e.into()) + .map(|_| format!("Field {} set for room {}", msg.field, msg.room_token)) + } +} + +impl Handler for RedisActor { + type Result = Result; + + fn handle(&mut self, msg: SetRoomCommand, _: &mut Self::Context) -> Self::Result { + let mut con: Connection = self.client.get_connection()?; + + // Use hset_multiple to set multiple fields at the same time + let query_key = format!("room:{}", msg.room_token.clone()); + let fields: Vec<(&str, &str)> = msg.fields.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect(); + let result: RedisResult<()> = con.hset_multiple(query_key, &fields); + + result.map_err(|e| e.into()) + .map(|_| format!("Fields set for room {}", msg.room_token)) + } +} + +impl Handler for RedisActor { + type Result = Result; + + fn handle(&mut self, msg: GetRoomSizeCommand, _: &mut Self::Context) -> Self::Result { + let mut con: Connection = self.client.get_connection()?; + let query_key = format!("room:{}:players", msg.room_token); + let result: RedisResult = con.scard(query_key); + result.map_err(|e| e.into()) + } +} + +impl Handler for RedisActor { + type Result = Result; + + fn handle(&mut self, msg: AddRoomPlayerCommand, _: &mut Self::Context) -> Self::Result { + let mut con: Connection = self.client.get_connection()?; + let query_key = format!("room:{}:players", msg.room_token); + let result: RedisResult = con.sadd(query_key, msg.player_key); + result.map_err(|e| e.into()) + } +} + +impl Handler for RedisActor { + type Result = Result; + + fn handle(&mut self, msg: RemoveRoomPlayerCommand, _: &mut Self::Context) -> Self::Result { + let mut con: Connection = self.client.get_connection()?; + let query_key = format!("room:{}:players", msg.room_token); + let result: RedisResult = con.srem(query_key, msg.player_key); + result.map_err(|e| e.into()) + } +} + +pub fn create_redis_actor(redis_url: String) -> Addr { + let client = Client::open(redis_url).expect("Failed to create Redis client"); + let actor = RedisActor { client }; + Actor::start(actor) +} diff --git a/server/src/controllers/api.rs b/server/src/controllers/api.rs index 1b03175..fe2b757 100644 --- a/server/src/controllers/api.rs +++ b/server/src/controllers/api.rs @@ -1,6 +1,8 @@ +use std::collections::HashMap; + use crate::{ - actors::{ClientWsActor, CreateRoom, JoinRoom, ListRooms}, - models::messages::ServerCommand, + actors::{ClientWsActor, CreateRoom, JoinRoom, ListRooms, room_manager_actor::RoomCreated}, + models::messages::{ServerCommand, SetRoomCommand}, AppState, }; use actix_web::{http::StatusCode, HttpRequest, Query, State}; @@ -25,7 +27,7 @@ pub fn socket_handler( match r { Ok(room) => actix_web::ws::start( &req, - ClientWsActor::new(room.game_addr, query.key.clone(), query.name.clone()), + ClientWsActor::new(room.game_addr, state.redis_actor_addr.clone(), query.key.clone(), query.name.clone(), query.room_token.clone()), ), Err(err) => Err(actix_web::error::ErrorBadRequest(err.to_string())), } @@ -51,7 +53,7 @@ pub fn spectate_handler( match r { Ok(room) => actix_web::ws::start( &req, - ClientWsActor::new(room.game_addr, "SPECTATOR".to_string(), "SPECTATOR".to_string()), + ClientWsActor::new(room.game_addr, state.redis_actor_addr.clone(), "SPECTATOR".to_string(), "SPECTATOR".to_string(), query.room_token.clone()), ), Err(err) => Err(actix_web::error::ErrorBadRequest(err.to_string())), } @@ -89,6 +91,13 @@ pub fn create_room_handler( match r { Ok(room) => { let body = serde_json::to_string(&room).unwrap(); + + // cache created room info + let cache_fields = create_room_fields(&room); + let _ = state + .redis_actor_addr + .send(SetRoomCommand { room_token: room.token.clone(), fields: cache_fields }) + .wait()?; Ok(actix_web::HttpResponse::with_body(StatusCode::OK, body)) }, Err(_) => Err(actix_web::error::ErrorBadRequest("Failed to create room")), @@ -107,3 +116,15 @@ pub fn list_rooms_handler( Err(_) => Err(actix_web::error::ErrorBadRequest("Failed to list rooms")), } } + +fn create_room_fields(room: &RoomCreated) -> HashMap { + let mut fields = HashMap::new(); + fields.insert("name".to_string(), room.name.clone()); + fields.insert("time_limit_seconds".to_string(), room.time_limit_seconds.to_string()); + fields.insert("max_players".to_string(), room.max_players.to_string()); + + // TODO(haongo) - Handle room's max_players check + // fields.insert("status".to_string(), format!("{}", RoomStatus::Ready)); + + fields +} diff --git a/server/src/main.rs b/server/src/main.rs index bce8456..1cc5f72 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -14,6 +14,7 @@ mod models; use crate::actors::{GameActor, RoomManagerActor}; use actix::{Actor, Addr, System}; use actix_web::{http::Method, middleware::Logger, server, App}; +use actors::redis_actor::{RedisActor, create_redis_actor}; use lazy_static::lazy_static; use listenfd::ListenFd; use std::collections::HashSet; @@ -25,10 +26,12 @@ pub struct AppConfig { api_keys: HashSet, dev_mode: bool, game_config: GameConfig, + redis_uri: Option, } pub struct AppState { game_addr: Addr, + redis_actor_addr: Addr, room_manager_addr: Addr, } @@ -52,6 +55,9 @@ fn main() -> Result<(), String> { let actor_system = System::new("meetup-server"); + let redis_uri = APP_CONFIG.redis_uri.clone().unwrap_or("redis://127.0.0.1/".into()); + let redis_actor_addr = create_redis_actor(redis_uri); + let game_actor = GameActor::new(APP_CONFIG.game_config, 0, 0); let game_actor_addr = game_actor.start(); @@ -61,6 +67,7 @@ fn main() -> Result<(), String> { let mut server = server::new(move || { let app_state = AppState { game_addr: game_actor_addr.clone(), + redis_actor_addr: redis_actor_addr.clone(), room_manager_addr: room_manager_addr.clone(), }; diff --git a/server/src/models/messages.rs b/server/src/models/messages.rs index b6ebc9e..9df9177 100644 --- a/server/src/models/messages.rs +++ b/server/src/models/messages.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use actix::Message; use tokyo::models::GameCommand; @@ -14,3 +16,45 @@ pub struct ClientStop {} pub enum ServerCommand { Reset } + +#[derive(Message)] +#[rtype(result = "Result")] +pub struct GetRoomFieldCommand { + pub room_token: String, + pub field: String, +} + +#[derive(Message)] +#[rtype(result = "Result")] +pub struct UpdateRoomFieldCommand { + pub room_token: String, + pub field: String, + pub value: String, +} + +#[derive(Message)] +#[rtype(result = "Result")] +pub struct SetRoomCommand { + pub room_token: String, + pub fields: HashMap, // Use a HashMap to represent multiple fields +} + +#[derive(Message)] +#[rtype(result = "Result")] +pub struct GetRoomSizeCommand { + pub room_token: String, +} + +#[derive(Message)] +#[rtype(result = "Result")] +pub struct AddRoomPlayerCommand { + pub room_token: String, + pub player_key: String, +} + +#[derive(Message)] +#[rtype(result = "Result")] +pub struct RemoveRoomPlayerCommand { + pub room_token: String, + pub player_key: String, +} \ No newline at end of file diff --git a/tokyo.toml b/tokyo.toml index 24bdc8b..71995cc 100644 --- a/tokyo.toml +++ b/tokyo.toml @@ -1,6 +1,7 @@ server_port = 8080 api_keys = ["webuild"] dev_mode = true +redis_uri = "redis://127.0.0.1/" [game_config] bound_x = 3500 From a915c49a08e8cdfa59fe18f2a257e4a3513c8756 Mon Sep 17 00:00:00 2001 From: "huy.nguyen@setel.com" Date: Mon, 11 Dec 2023 21:39:48 +0700 Subject: [PATCH 2/8] chore: cache room's status --- server/src/controllers/api.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/server/src/controllers/api.rs b/server/src/controllers/api.rs index fe2b757..3e0a60c 100644 --- a/server/src/controllers/api.rs +++ b/server/src/controllers/api.rs @@ -122,9 +122,7 @@ fn create_room_fields(room: &RoomCreated) -> HashMap { fields.insert("name".to_string(), room.name.clone()); fields.insert("time_limit_seconds".to_string(), room.time_limit_seconds.to_string()); fields.insert("max_players".to_string(), room.max_players.to_string()); - - // TODO(haongo) - Handle room's max_players check - // fields.insert("status".to_string(), format!("{}", RoomStatus::Ready)); + fields.insert("status".to_string(), String::from("READY")); // TODO(haongo) - Handle room's max_players check & room start based on this field fields } From dd4e78a5d924e052608e15ff8a4b7e9a46976ce7 Mon Sep 17 00:00:00 2001 From: "huy.nguyen@setel.com" Date: Wed, 13 Dec 2023 13:31:49 +0700 Subject: [PATCH 3/8] chore: refactor based on lastest feat --- client/examples/jakebot.rs | 6 +- client/examples/simple.rs | 6 +- client/src/lib.rs | 4 +- server/src/actors/client_ws_actor.rs | 18 +--- server/src/actors/game_actor.rs | 26 +++++- server/src/actors/redis_actor.rs | 106 +++++++----------------- server/src/actors/room_manager_actor.rs | 2 +- server/src/controllers/api.rs | 59 ++++++++----- server/src/main.rs | 10 ++- server/src/models/messages.rs | 41 ++------- 10 files changed, 115 insertions(+), 163 deletions(-) diff --git a/client/examples/jakebot.rs b/client/examples/jakebot.rs index afda655..125c836 100644 --- a/client/examples/jakebot.rs +++ b/client/examples/jakebot.rs @@ -73,9 +73,9 @@ impl Handler for Player { } fn main() { - let api_key = &env::var("API_KEY").unwrap_or("player1".into()); - let team_name = &env::var("TEAM_NAME").unwrap_or("player1".into()); - let room_token = &env::var("ROOM_TOKEN").unwrap_or("OBx4o6Iq".into()); + let api_key = &env::var("API_KEY").unwrap_or("a".into()); + let team_name = &env::var("TEAM_NAME").unwrap_or("a".into()); + let room_token = &env::var("ROOM_TOKEN").unwrap_or("a".into()); println!("starting up..."); tokyo::run(api_key, team_name, room_token, Player::default()).unwrap(); diff --git a/client/examples/simple.rs b/client/examples/simple.rs index 59b905c..e84946a 100644 --- a/client/examples/simple.rs +++ b/client/examples/simple.rs @@ -46,9 +46,9 @@ impl Handler for Player { fn main() { // TODO: Substitute with your API key and team name. - let api_key = &env::var("API_KEY").unwrap_or("player0".into()); - let team_name = &env::var("TEAM_NAME").unwrap_or("player0".into()); - let room_token = &env::var("ROOM_TOKEN").unwrap_or("OBx4o6Iq".into()); + let api_key = &env::var("API_KEY").unwrap_or("a".into()); + let team_name = &env::var("TEAM_NAME").unwrap_or("a".into()); + let room_token = &env::var("ROOM_TOKEN").unwrap_or("a".into()); println!("starting up..."); tokyo::run(api_key, team_name, room_token, Player::default()).unwrap(); diff --git a/client/src/lib.rs b/client/src/lib.rs index d08098c..f5984dd 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -111,9 +111,9 @@ pub fn run(key: &str, name: &str, room_token: &str, handler: H) -> Result<(), where H: Handler + Send + 'static, { - let host = env::var("SERVER_HOST").unwrap_or("127.0.0.1:8080".into()); + let host = env::var("SERVER_HOST").unwrap_or("192.168.0.199".into()); let url = Url::parse(&format!( - "ws://{}/socket?key={}&name={}&room_token={}", + "wss://{}/socket?key={}&name={}&room_token={}", host, key, utf8_percent_encode(name, DEFAULT_ENCODE_SET).to_string(), diff --git a/server/src/actors/client_ws_actor.rs b/server/src/actors/client_ws_actor.rs index 8c315ff..f2b952a 100644 --- a/server/src/actors/client_ws_actor.rs +++ b/server/src/actors/client_ws_actor.rs @@ -1,5 +1,5 @@ use crate::{ - actors::{GameActor, RedisActor}, + actors::GameActor, models::messages::{ClientStop, PlayerGameCommand}, AppState, }; @@ -13,20 +13,18 @@ const ACTIONS_PER_SECOND: u32 = 22; #[derive(Debug)] pub struct ClientWsActor { game_addr: Addr, - redis_actor_addr: Addr, - room_token: String, api_key: String, team_name: String, rate_limiter: DirectRateLimiter, } impl ClientWsActor { - pub fn new(game_addr: Addr, redis_actor_addr: Addr, api_key: String, team_name: String, room_token: String) -> ClientWsActor { + pub fn new(game_addr: Addr, api_key: String, team_name: String) -> ClientWsActor { let rate_limiter = DirectRateLimiter::::per_second( std::num::NonZeroU32::new(ACTIONS_PER_SECOND).unwrap(), ); - ClientWsActor { game_addr, redis_actor_addr, api_key, team_name, room_token, rate_limiter } + ClientWsActor { game_addr, api_key, team_name, rate_limiter } } } @@ -39,12 +37,6 @@ impl Actor for ClientWsActor { self.team_name.clone(), ctx.address(), )); - if self.api_key != "SPECTATOR" { - self.redis_actor_addr.do_send(crate::models::messages::AddRoomPlayerCommand{ - room_token: self.room_token.clone(), - player_key: self.api_key.clone(), - }); - } } fn stopped(&mut self, ctx: &mut Self::Context) { @@ -53,10 +45,6 @@ impl Actor for ClientWsActor { self.api_key.clone(), ctx.address(), )); - self.redis_actor_addr.do_send(crate::models::messages::RemoveRoomPlayerCommand{ - room_token: self.room_token.clone(), - player_key: self.api_key.clone(), - }); } } diff --git a/server/src/actors/game_actor.rs b/server/src/actors/game_actor.rs index 7238052..05b57c4 100644 --- a/server/src/actors/game_actor.rs +++ b/server/src/actors/game_actor.rs @@ -1,7 +1,7 @@ use crate::{ actors::ClientWsActor, game::{Game, TICKS_PER_SECOND}, - models::messages::{ClientStop, PlayerGameCommand, ServerCommand}, + models::messages::{ClientStop, PlayerGameCommand, ServerCommand, SetScoreboardCommand}, }; use actix::{Actor, Addr, AsyncContext, Context, Handler, Message}; use futures::sync::oneshot; @@ -13,8 +13,11 @@ use std::{ }; use tokyo::models::*; +use super::RedisActor; + #[derive(Debug)] pub struct GameActor { + redis_actor_addr: Addr, connections: HashMap>, spectators: HashSet>, team_names: HashMap, @@ -26,6 +29,7 @@ pub struct GameActor { game_config: GameConfig, max_players: u32, time_limit_seconds: u32, + room_token: String, } #[derive(Debug)] @@ -37,10 +41,15 @@ pub enum GameLoopCommand { } impl GameActor { - pub fn new(config: GameConfig, max_players: u32, time_limit_seconds: u32) -> GameActor { + pub fn new(config: GameConfig, max_players: u32, time_limit_seconds: u32, room_token: String) -> GameActor { let (msg_tx, msg_rx) = channel(); + + let redis_uri = crate::APP_CONFIG.redis_uri.clone().unwrap_or("redis://127.0.0.1/".into()); + let redis_actor = RedisActor::new(redis_uri); + let redis_actor_addr = redis_actor.start(); GameActor { + redis_actor_addr, connections: HashMap::new(), spectators: HashSet::new(), team_names: HashMap::new(), @@ -52,17 +61,20 @@ impl GameActor { game_config: config, max_players, time_limit_seconds, + room_token, } } } fn game_loop( game_actor: Addr, + redis_actor: Addr, msg_chan: Receiver, mut cancel_chan: oneshot::Receiver<()>, config: GameConfig, max_players: u32, time_limit_seconds: u32, + room_token: String, ) { let mut loop_helper = LoopHelper::builder().build_with_target_rate(TICKS_PER_SECOND); @@ -119,6 +131,12 @@ fn game_loop( println!("Ending game!"); status = GameStatus::Finished; game_over_at = None; + + // store scoreboard on game end + redis_actor.do_send(SetScoreboardCommand { + room_token: room_token.clone(), + scoreboard: game.state.scoreboard.clone(), + }); } if status.is_running() { @@ -180,6 +198,8 @@ impl Actor for GameActor { info!("Game Actor started!"); let (cancel_tx, cancel_rx) = oneshot::channel(); let addr = ctx.address(); + let redis_actor_addr = self.redis_actor_addr.clone(); + let room_token = self.room_token.clone(); // "Take" the receiving end of the channel and give it // to the game loop thread @@ -189,7 +209,7 @@ impl Actor for GameActor { let max_players = self.max_players; let time_limit_seconds = self.time_limit_seconds; std::thread::spawn(move || { - game_loop(addr, msg_rx, cancel_rx, config, max_players, time_limit_seconds); + game_loop(addr, redis_actor_addr, msg_rx, cancel_rx, config, max_players, time_limit_seconds, room_token); }); self.cancel_chan = Some(cancel_tx); diff --git a/server/src/actors/redis_actor.rs b/server/src/actors/redis_actor.rs index e275a84..563469a 100644 --- a/server/src/actors/redis_actor.rs +++ b/server/src/actors/redis_actor.rs @@ -1,97 +1,53 @@ +use std::collections::HashMap; + use actix::prelude::*; -use redis::{Client, Commands, Connection, RedisResult}; +use redis::{Client, Commands, Connection}; -use crate::models::messages::{GetRoomFieldCommand, UpdateRoomFieldCommand, SetRoomCommand, GetRoomSizeCommand, AddRoomPlayerCommand, RemoveRoomPlayerCommand}; +use crate::models::messages::{SetScoreboardCommand, GetScoreboardCommand}; #[derive(Debug)] pub struct RedisActor { client: Client, } -impl Actor for RedisActor { - type Context = Context; -} - -impl Handler for RedisActor { - type Result = Result; - - fn handle(&mut self, msg: GetRoomFieldCommand, _: &mut Self::Context) -> Self::Result { - let mut con: Connection = self.client.get_connection()?; - let query_key = format!("room:{}", msg.room_token); - let result: RedisResult = con.hget(query_key, msg.field); - result.map_err(|e| e.into()) - } -} - -impl Handler for RedisActor { - type Result = Result; - - fn handle(&mut self, msg: UpdateRoomFieldCommand, _: &mut Self::Context) -> Self::Result { - let mut con: Connection = self.client.get_connection()?; - - // Clone the values before moving them into the Redis `hset` command - let query_key = format!("room:{}", msg.room_token.clone()); - let field = msg.field.clone(); - let value = msg.value.clone(); - - let result: RedisResult<()> = con.hset(query_key, field, value); - result - .map_err(|e| e.into()) - .map(|_| format!("Field {} set for room {}", msg.field, msg.room_token)) - } -} - -impl Handler for RedisActor { - type Result = Result; - - fn handle(&mut self, msg: SetRoomCommand, _: &mut Self::Context) -> Self::Result { - let mut con: Connection = self.client.get_connection()?; - - // Use hset_multiple to set multiple fields at the same time - let query_key = format!("room:{}", msg.room_token.clone()); - let fields: Vec<(&str, &str)> = msg.fields.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect(); - let result: RedisResult<()> = con.hset_multiple(query_key, &fields); - - result.map_err(|e| e.into()) - .map(|_| format!("Fields set for room {}", msg.room_token)) +impl RedisActor { + pub fn new(redis_url: String) -> RedisActor { + let client = Client::open(redis_url).expect("Failed to create Redis client"); + RedisActor { client } } } -impl Handler for RedisActor { - type Result = Result; - - fn handle(&mut self, msg: GetRoomSizeCommand, _: &mut Self::Context) -> Self::Result { - let mut con: Connection = self.client.get_connection()?; - let query_key = format!("room:{}:players", msg.room_token); - let result: RedisResult = con.scard(query_key); - result.map_err(|e| e.into()) - } +impl Actor for RedisActor { + type Context = Context; } -impl Handler for RedisActor { - type Result = Result; +impl Handler for RedisActor { + type Result = Result<(), redis::RedisError>; - fn handle(&mut self, msg: AddRoomPlayerCommand, _: &mut Self::Context) -> Self::Result { + fn handle(&mut self, msg: SetScoreboardCommand, _: &mut Self::Context) -> Self::Result { let mut con: Connection = self.client.get_connection()?; - let query_key = format!("room:{}:players", msg.room_token); - let result: RedisResult = con.sadd(query_key, msg.player_key); - result.map_err(|e| e.into()) + let query_key = format!("room:{}:scoreboard", msg.room_token); + for (player_id, points) in &msg.scoreboard { + con.zadd(query_key.clone(), *points as f64, *player_id)?; + } + Ok(()) } } -impl Handler for RedisActor { - type Result = Result; +impl Handler for RedisActor { + type Result = Result, redis::RedisError>; - fn handle(&mut self, msg: RemoveRoomPlayerCommand, _: &mut Self::Context) -> Self::Result { + fn handle(&mut self, msg: GetScoreboardCommand, _: &mut Self::Context) -> Self::Result { let mut con: Connection = self.client.get_connection()?; - let query_key = format!("room:{}:players", msg.room_token); - let result: RedisResult = con.srem(query_key, msg.player_key); - result.map_err(|e| e.into()) + let query_key = format!("room:{}:scoreboard", msg.0); + let scoreboard: Vec<(String, String)> = con.zrevrange_withscores(query_key, 0, -1)?; + let mut result = HashMap::new(); + for (total_points_str, player_id_str) in scoreboard { + let player_id = player_id_str.parse::().unwrap_or_default(); + let total_points = total_points_str.parse::().unwrap_or_default() as u32; + result.insert(player_id, total_points); + } + + Ok(result) } } - -pub fn create_redis_actor(redis_url: String) -> Addr { - let client = Client::open(redis_url).expect("Failed to create Redis client"); - let actor = RedisActor { client }; - Actor::start(actor) -} diff --git a/server/src/actors/room_manager_actor.rs b/server/src/actors/room_manager_actor.rs index 96cd445..57b6330 100644 --- a/server/src/actors/room_manager_actor.rs +++ b/server/src/actors/room_manager_actor.rs @@ -37,7 +37,7 @@ impl Room { ) -> Room { let game_cfg = GameConfig { bound_x: config.bound_x, bound_y: config.bound_y }; - let game_actor = GameActor::new(game_cfg, max_players, time_limit_seconds); + let game_actor = GameActor::new(game_cfg, max_players, time_limit_seconds, token.clone()); let game_actor_addr = game_actor.start(); Room { id, name, max_players, time_limit_seconds, token, game: game_actor_addr } } diff --git a/server/src/controllers/api.rs b/server/src/controllers/api.rs index 3e0a60c..bc375cd 100644 --- a/server/src/controllers/api.rs +++ b/server/src/controllers/api.rs @@ -1,11 +1,9 @@ -use std::collections::HashMap; - use crate::{ - actors::{ClientWsActor, CreateRoom, JoinRoom, ListRooms, room_manager_actor::RoomCreated}, - models::messages::{ServerCommand, SetRoomCommand}, + actors::{ClientWsActor, CreateRoom, JoinRoom, ListRooms}, + models::messages::{GetScoreboardCommand, ServerCommand}, AppState, }; -use actix_web::{http::StatusCode, HttpRequest, Query, State}; +use actix_web::{http::StatusCode, HttpRequest, Path, Query, State}; use futures::Future; #[derive(Debug, Deserialize)] @@ -15,6 +13,17 @@ pub struct QueryString { name: String, } +#[derive(Serialize, Deserialize)] +struct ScoreboardEntry { + player_id: u32, + total_points: u32, +} + +#[derive(Serialize, Deserialize)] +struct ScoreboardResponse { + scoreboard: Vec, +} + pub fn socket_handler( (req, state, query): (HttpRequest, State, Query), ) -> Result { @@ -27,7 +36,7 @@ pub fn socket_handler( match r { Ok(room) => actix_web::ws::start( &req, - ClientWsActor::new(room.game_addr, state.redis_actor_addr.clone(), query.key.clone(), query.name.clone(), query.room_token.clone()), + ClientWsActor::new(room.game_addr, query.key.clone(), query.name.clone()), ), Err(err) => Err(actix_web::error::ErrorBadRequest(err.to_string())), } @@ -53,7 +62,7 @@ pub fn spectate_handler( match r { Ok(room) => actix_web::ws::start( &req, - ClientWsActor::new(room.game_addr, state.redis_actor_addr.clone(), "SPECTATOR".to_string(), "SPECTATOR".to_string(), query.room_token.clone()), + ClientWsActor::new(room.game_addr, "SPECTATOR".to_string(), "SPECTATOR".to_string()), ), Err(err) => Err(actix_web::error::ErrorBadRequest(err.to_string())), } @@ -91,13 +100,6 @@ pub fn create_room_handler( match r { Ok(room) => { let body = serde_json::to_string(&room).unwrap(); - - // cache created room info - let cache_fields = create_room_fields(&room); - let _ = state - .redis_actor_addr - .send(SetRoomCommand { room_token: room.token.clone(), fields: cache_fields }) - .wait()?; Ok(actix_web::HttpResponse::with_body(StatusCode::OK, body)) }, Err(_) => Err(actix_web::error::ErrorBadRequest("Failed to create room")), @@ -117,12 +119,25 @@ pub fn list_rooms_handler( } } -fn create_room_fields(room: &RoomCreated) -> HashMap { - let mut fields = HashMap::new(); - fields.insert("name".to_string(), room.name.clone()); - fields.insert("time_limit_seconds".to_string(), room.time_limit_seconds.to_string()); - fields.insert("max_players".to_string(), room.max_players.to_string()); - fields.insert("status".to_string(), String::from("READY")); // TODO(haongo) - Handle room's max_players check & room start based on this field - - fields +pub fn get_room_scoreboard( + (_req, state, path): (HttpRequest, State, Path), +) -> Result { + let room_token = path.into_inner(); + let result = state.redis_actor_addr.send(GetScoreboardCommand(room_token)).wait().unwrap(); + match result { + Ok(scoreboard) => { + let scoreboard_response: ScoreboardResponse = ScoreboardResponse { + scoreboard: scoreboard + .into_iter() + .map(|(player_id, total_points)| ScoreboardEntry { player_id, total_points }) + .collect(), + }; + let body = serde_json::to_string(&scoreboard_response)?; + Ok(actix_web::HttpResponse::with_body(StatusCode::OK, body)) + }, + Err(e) => Err(actix_web::error::ErrorBadRequest(format!( + "Failed to get room's scoreboard: {}", + e + ))), + } } diff --git a/server/src/main.rs b/server/src/main.rs index 1cc5f72..ef3e774 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -14,7 +14,7 @@ mod models; use crate::actors::{GameActor, RoomManagerActor}; use actix::{Actor, Addr, System}; use actix_web::{http::Method, middleware::Logger, server, App}; -use actors::redis_actor::{RedisActor, create_redis_actor}; +use actors::RedisActor; use lazy_static::lazy_static; use listenfd::ListenFd; use std::collections::HashSet; @@ -56,9 +56,10 @@ fn main() -> Result<(), String> { let actor_system = System::new("meetup-server"); let redis_uri = APP_CONFIG.redis_uri.clone().unwrap_or("redis://127.0.0.1/".into()); - let redis_actor_addr = create_redis_actor(redis_uri); + let redis_actor = RedisActor::new(redis_uri); + let redis_actor_addr = redis_actor.start(); - let game_actor = GameActor::new(APP_CONFIG.game_config, 0, 0); + let game_actor = GameActor::new(APP_CONFIG.game_config, 0, 0, String::from("")); let game_actor_addr = game_actor.start(); let room_manager_actor = actors::RoomManagerActor::new(APP_CONFIG.game_config); @@ -77,6 +78,9 @@ fn main() -> Result<(), String> { r.method(Method::POST).with(controllers::api::create_room_handler); r.method(Method::GET).with(controllers::api::list_rooms_handler); }) + .resource("/rooms/{room_token}/scoreboard", |r| { + r.method(Method::GET).with(controllers::api::get_room_scoreboard); + }) .resource("/socket", |r| { r.method(Method::GET).with(controllers::api::socket_handler); }) diff --git a/server/src/models/messages.rs b/server/src/models/messages.rs index 9df9177..cc95699 100644 --- a/server/src/models/messages.rs +++ b/server/src/models/messages.rs @@ -18,43 +18,12 @@ pub enum ServerCommand { } #[derive(Message)] -#[rtype(result = "Result")] -pub struct GetRoomFieldCommand { +#[rtype(result = "Result<(), redis::RedisError>")] +pub struct SetScoreboardCommand { pub room_token: String, - pub field: String, + pub scoreboard: HashMap, } #[derive(Message)] -#[rtype(result = "Result")] -pub struct UpdateRoomFieldCommand { - pub room_token: String, - pub field: String, - pub value: String, -} - -#[derive(Message)] -#[rtype(result = "Result")] -pub struct SetRoomCommand { - pub room_token: String, - pub fields: HashMap, // Use a HashMap to represent multiple fields -} - -#[derive(Message)] -#[rtype(result = "Result")] -pub struct GetRoomSizeCommand { - pub room_token: String, -} - -#[derive(Message)] -#[rtype(result = "Result")] -pub struct AddRoomPlayerCommand { - pub room_token: String, - pub player_key: String, -} - -#[derive(Message)] -#[rtype(result = "Result")] -pub struct RemoveRoomPlayerCommand { - pub room_token: String, - pub player_key: String, -} \ No newline at end of file +#[rtype(result = "Result, redis::RedisError>")] +pub struct GetScoreboardCommand(pub String); From 40bc5cb781555f35d1378fac5433c0e41d35e6eb Mon Sep 17 00:00:00 2001 From: "huy.nguyen@setel.com" Date: Wed, 13 Dec 2023 14:09:51 +0700 Subject: [PATCH 4/8] chore: revert /client changes --- client/examples/chaser.rs | 3 +-- client/examples/complex/src/main.rs | 3 +-- client/examples/jakebot.rs | 6 ++---- client/examples/simple.rs | 3 +-- client/src/lib.rs | 7 +++---- 5 files changed, 8 insertions(+), 14 deletions(-) diff --git a/client/examples/chaser.rs b/client/examples/chaser.rs index 9f1d743..c5adfe6 100644 --- a/client/examples/chaser.rs +++ b/client/examples/chaser.rs @@ -54,8 +54,7 @@ impl Handler for Player { fn main() { let api_key = &env::var("API_KEY").unwrap_or("a".into()); let team_name = &env::var("TEAM_NAME").unwrap_or("a".into()); - let room_token = &env::var("ROOM_TOKEN").unwrap_or("a".into()); println!("starting up..."); - tokyo::run(api_key, team_name, room_token, Player::default()).unwrap(); + tokyo::run(api_key, team_name, Player::default()).unwrap(); } diff --git a/client/examples/complex/src/main.rs b/client/examples/complex/src/main.rs index 2de20cc..dd34cfe 100644 --- a/client/examples/complex/src/main.rs +++ b/client/examples/complex/src/main.rs @@ -102,8 +102,7 @@ fn main() { // TODO: Substitute with your API key and team name. let api_key = &env::var("API_KEY").unwrap_or("a".into()); let team_name = &env::var("TEAM_NAME").unwrap_or("ThucUnbelievale".into()); - let room_token = &env::var("ROOM_TOKEN").unwrap_or("a".into()); println!("starting up..."); - tokyo::run(api_key, team_name, room_token, Player::new()).unwrap(); + tokyo::run(api_key, team_name, Player::new()).unwrap(); } diff --git a/client/examples/jakebot.rs b/client/examples/jakebot.rs index 125c836..58d48e3 100644 --- a/client/examples/jakebot.rs +++ b/client/examples/jakebot.rs @@ -73,10 +73,8 @@ impl Handler for Player { } fn main() { + println!("starting up..."); let api_key = &env::var("API_KEY").unwrap_or("a".into()); let team_name = &env::var("TEAM_NAME").unwrap_or("a".into()); - let room_token = &env::var("ROOM_TOKEN").unwrap_or("a".into()); - - println!("starting up..."); - tokyo::run(api_key, team_name, room_token, Player::default()).unwrap(); + tokyo::run(api_key, team_name, Player::default()).unwrap(); } diff --git a/client/examples/simple.rs b/client/examples/simple.rs index e84946a..24282ca 100644 --- a/client/examples/simple.rs +++ b/client/examples/simple.rs @@ -48,8 +48,7 @@ fn main() { // TODO: Substitute with your API key and team name. let api_key = &env::var("API_KEY").unwrap_or("a".into()); let team_name = &env::var("TEAM_NAME").unwrap_or("a".into()); - let room_token = &env::var("ROOM_TOKEN").unwrap_or("a".into()); println!("starting up..."); - tokyo::run(api_key, team_name, room_token, Player::default()).unwrap(); + tokyo::run(api_key, team_name, Player::default()).unwrap(); } diff --git a/client/src/lib.rs b/client/src/lib.rs index f5984dd..4f9951a 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -107,17 +107,16 @@ where /// Begin the client-side game loop, using the provided struct that implements `Handler` /// to act on behalf of the player. -pub fn run(key: &str, name: &str, room_token: &str, handler: H) -> Result<(), Error> +pub fn run(key: &str, name: &str, handler: H) -> Result<(), Error> where H: Handler + Send + 'static, { let host = env::var("SERVER_HOST").unwrap_or("192.168.0.199".into()); let url = Url::parse(&format!( - "wss://{}/socket?key={}&name={}&room_token={}", + "wss://{}/socket?key={}&name={}", host, key, - utf8_percent_encode(name, DEFAULT_ENCODE_SET).to_string(), - room_token, + utf8_percent_encode(name, DEFAULT_ENCODE_SET).to_string() ))?; let client_state = From 9125dd04545931135c563791a899a442d0146f25 Mon Sep 17 00:00:00 2001 From: "huy.nguyen@setel.com" Date: Wed, 13 Dec 2023 14:30:27 +0700 Subject: [PATCH 5/8] chore: initalize only single redis connection --- server/src/actors/game_actor.rs | 10 ++-------- server/src/actors/room_manager_actor.rs | 11 +++++++---- server/src/main.rs | 4 ++-- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/server/src/actors/game_actor.rs b/server/src/actors/game_actor.rs index 05b57c4..2670267 100644 --- a/server/src/actors/game_actor.rs +++ b/server/src/actors/game_actor.rs @@ -1,5 +1,5 @@ use crate::{ - actors::ClientWsActor, + actors::{ClientWsActor, RedisActor}, game::{Game, TICKS_PER_SECOND}, models::messages::{ClientStop, PlayerGameCommand, ServerCommand, SetScoreboardCommand}, }; @@ -13,8 +13,6 @@ use std::{ }; use tokyo::models::*; -use super::RedisActor; - #[derive(Debug)] pub struct GameActor { redis_actor_addr: Addr, @@ -41,13 +39,9 @@ pub enum GameLoopCommand { } impl GameActor { - pub fn new(config: GameConfig, max_players: u32, time_limit_seconds: u32, room_token: String) -> GameActor { + pub fn new(config: GameConfig, redis_actor_addr: Addr, max_players: u32, time_limit_seconds: u32, room_token: String) -> GameActor { let (msg_tx, msg_rx) = channel(); - let redis_uri = crate::APP_CONFIG.redis_uri.clone().unwrap_or("redis://127.0.0.1/".into()); - let redis_actor = RedisActor::new(redis_uri); - let redis_actor_addr = redis_actor.start(); - GameActor { redis_actor_addr, connections: HashMap::new(), diff --git a/server/src/actors/room_manager_actor.rs b/server/src/actors/room_manager_actor.rs index 57b6330..15d0673 100644 --- a/server/src/actors/room_manager_actor.rs +++ b/server/src/actors/room_manager_actor.rs @@ -1,4 +1,4 @@ -use crate::actors::GameActor; +use crate::actors::{GameActor, RedisActor}; use actix::prelude::*; use rand::{distributions::Alphanumeric, Rng}; use std::{ @@ -12,6 +12,7 @@ const TOKEN_LENGTH: usize = 8; // RoomManagerActor is responsible for creating and managing rooms pub struct RoomManagerActor { config: GameConfig, + redis_actor_addr: Addr, id_counter: u32, rooms: HashMap, } @@ -29,6 +30,7 @@ struct Room { impl Room { pub fn new( config: &GameConfig, + redis_actor_addr: Addr, id: u32, name: String, max_players: u32, @@ -37,15 +39,15 @@ impl Room { ) -> Room { let game_cfg = GameConfig { bound_x: config.bound_x, bound_y: config.bound_y }; - let game_actor = GameActor::new(game_cfg, max_players, time_limit_seconds, token.clone()); + let game_actor = GameActor::new(game_cfg, redis_actor_addr, max_players, time_limit_seconds, token.clone()); let game_actor_addr = game_actor.start(); Room { id, name, max_players, time_limit_seconds, token, game: game_actor_addr } } } impl RoomManagerActor { - pub fn new(cfg: GameConfig) -> RoomManagerActor { - RoomManagerActor { config: cfg, id_counter: 0, rooms: HashMap::new() } + pub fn new(cfg: GameConfig, redis_actor_addr: Addr) -> RoomManagerActor { + RoomManagerActor { config: cfg, id_counter: 0, rooms: HashMap::new(), redis_actor_addr: redis_actor_addr.clone() } } pub fn create_room( @@ -66,6 +68,7 @@ impl RoomManagerActor { token.clone(), Room::new( &self.config, + self.redis_actor_addr.clone(), self.id_counter, name.to_string(), max_players, diff --git a/server/src/main.rs b/server/src/main.rs index ef3e774..4dda832 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -59,10 +59,10 @@ fn main() -> Result<(), String> { let redis_actor = RedisActor::new(redis_uri); let redis_actor_addr = redis_actor.start(); - let game_actor = GameActor::new(APP_CONFIG.game_config, 0, 0, String::from("")); + let game_actor = GameActor::new(APP_CONFIG.game_config, redis_actor_addr.clone(), 0, 0, String::from("")); let game_actor_addr = game_actor.start(); - let room_manager_actor = actors::RoomManagerActor::new(APP_CONFIG.game_config); + let room_manager_actor = actors::RoomManagerActor::new(APP_CONFIG.game_config, redis_actor_addr.clone()); let room_manager_addr = room_manager_actor.start(); let mut server = server::new(move || { From fe1638f752730caadf779659bd7f6143f9ed43fe Mon Sep 17 00:00:00 2001 From: Hao Ngo Date: Thu, 14 Dec 2023 11:22:38 +0700 Subject: [PATCH 6/8] chore: update redis_actor to more abstraction name --- server/src/actors/game_actor.rs | 16 ++++++++-------- server/src/actors/mod.rs | 4 ++-- server/src/actors/room_manager_actor.rs | 14 +++++++------- .../actors/{redis_actor.rs => store_actor.rs} | 14 +++++++------- server/src/controllers/api.rs | 2 +- server/src/main.rs | 14 +++++++------- 6 files changed, 32 insertions(+), 32 deletions(-) rename server/src/actors/{redis_actor.rs => store_actor.rs} (85%) diff --git a/server/src/actors/game_actor.rs b/server/src/actors/game_actor.rs index 2670267..9380e50 100644 --- a/server/src/actors/game_actor.rs +++ b/server/src/actors/game_actor.rs @@ -1,5 +1,5 @@ use crate::{ - actors::{ClientWsActor, RedisActor}, + actors::{ClientWsActor, StoreActor}, game::{Game, TICKS_PER_SECOND}, models::messages::{ClientStop, PlayerGameCommand, ServerCommand, SetScoreboardCommand}, }; @@ -15,7 +15,7 @@ use tokyo::models::*; #[derive(Debug)] pub struct GameActor { - redis_actor_addr: Addr, + store_actor_addr: Addr, connections: HashMap>, spectators: HashSet>, team_names: HashMap, @@ -39,11 +39,11 @@ pub enum GameLoopCommand { } impl GameActor { - pub fn new(config: GameConfig, redis_actor_addr: Addr, max_players: u32, time_limit_seconds: u32, room_token: String) -> GameActor { + pub fn new(config: GameConfig, store_actor_addr: Addr, max_players: u32, time_limit_seconds: u32, room_token: String) -> GameActor { let (msg_tx, msg_rx) = channel(); GameActor { - redis_actor_addr, + store_actor_addr, connections: HashMap::new(), spectators: HashSet::new(), team_names: HashMap::new(), @@ -62,7 +62,7 @@ impl GameActor { fn game_loop( game_actor: Addr, - redis_actor: Addr, + store_actor: Addr, msg_chan: Receiver, mut cancel_chan: oneshot::Receiver<()>, config: GameConfig, @@ -127,7 +127,7 @@ fn game_loop( game_over_at = None; // store scoreboard on game end - redis_actor.do_send(SetScoreboardCommand { + store_actor.do_send(SetScoreboardCommand { room_token: room_token.clone(), scoreboard: game.state.scoreboard.clone(), }); @@ -192,7 +192,7 @@ impl Actor for GameActor { info!("Game Actor started!"); let (cancel_tx, cancel_rx) = oneshot::channel(); let addr = ctx.address(); - let redis_actor_addr = self.redis_actor_addr.clone(); + let store_actor_addr = self.store_actor_addr.clone(); let room_token = self.room_token.clone(); // "Take" the receiving end of the channel and give it @@ -203,7 +203,7 @@ impl Actor for GameActor { let max_players = self.max_players; let time_limit_seconds = self.time_limit_seconds; std::thread::spawn(move || { - game_loop(addr, redis_actor_addr, msg_rx, cancel_rx, config, max_players, time_limit_seconds, room_token); + game_loop(addr, store_actor_addr, msg_rx, cancel_rx, config, max_players, time_limit_seconds, room_token); }); self.cancel_chan = Some(cancel_tx); diff --git a/server/src/actors/mod.rs b/server/src/actors/mod.rs index 4181166..4a75e20 100644 --- a/server/src/actors/mod.rs +++ b/server/src/actors/mod.rs @@ -1,10 +1,10 @@ pub mod client_ws_actor; pub mod game_actor; -pub mod redis_actor; +pub mod store_actor; pub use client_ws_actor::ClientWsActor; pub use game_actor::GameActor; pub mod room_manager_actor; pub use room_manager_actor::{CreateRoom, JoinRoom, ListRooms, RoomManagerActor}; -pub use redis_actor::RedisActor; \ No newline at end of file +pub use store_actor::StoreActor; \ No newline at end of file diff --git a/server/src/actors/room_manager_actor.rs b/server/src/actors/room_manager_actor.rs index 15d0673..04b53d4 100644 --- a/server/src/actors/room_manager_actor.rs +++ b/server/src/actors/room_manager_actor.rs @@ -1,4 +1,4 @@ -use crate::actors::{GameActor, RedisActor}; +use crate::actors::{GameActor, StoreActor}; use actix::prelude::*; use rand::{distributions::Alphanumeric, Rng}; use std::{ @@ -12,7 +12,7 @@ const TOKEN_LENGTH: usize = 8; // RoomManagerActor is responsible for creating and managing rooms pub struct RoomManagerActor { config: GameConfig, - redis_actor_addr: Addr, + store_actor_addr: Addr, id_counter: u32, rooms: HashMap, } @@ -30,7 +30,7 @@ struct Room { impl Room { pub fn new( config: &GameConfig, - redis_actor_addr: Addr, + store_actor_addr: Addr, id: u32, name: String, max_players: u32, @@ -39,15 +39,15 @@ impl Room { ) -> Room { let game_cfg = GameConfig { bound_x: config.bound_x, bound_y: config.bound_y }; - let game_actor = GameActor::new(game_cfg, redis_actor_addr, max_players, time_limit_seconds, token.clone()); + let game_actor = GameActor::new(game_cfg, store_actor_addr, max_players, time_limit_seconds, token.clone()); let game_actor_addr = game_actor.start(); Room { id, name, max_players, time_limit_seconds, token, game: game_actor_addr } } } impl RoomManagerActor { - pub fn new(cfg: GameConfig, redis_actor_addr: Addr) -> RoomManagerActor { - RoomManagerActor { config: cfg, id_counter: 0, rooms: HashMap::new(), redis_actor_addr: redis_actor_addr.clone() } + pub fn new(cfg: GameConfig, store_actor_addr: Addr) -> RoomManagerActor { + RoomManagerActor { config: cfg, id_counter: 0, rooms: HashMap::new(), store_actor_addr } } pub fn create_room( @@ -68,7 +68,7 @@ impl RoomManagerActor { token.clone(), Room::new( &self.config, - self.redis_actor_addr.clone(), + self.store_actor_addr.clone(), self.id_counter, name.to_string(), max_players, diff --git a/server/src/actors/redis_actor.rs b/server/src/actors/store_actor.rs similarity index 85% rename from server/src/actors/redis_actor.rs rename to server/src/actors/store_actor.rs index 563469a..7c544cf 100644 --- a/server/src/actors/redis_actor.rs +++ b/server/src/actors/store_actor.rs @@ -6,22 +6,22 @@ use redis::{Client, Commands, Connection}; use crate::models::messages::{SetScoreboardCommand, GetScoreboardCommand}; #[derive(Debug)] -pub struct RedisActor { +pub struct StoreActor { client: Client, } -impl RedisActor { - pub fn new(redis_url: String) -> RedisActor { +impl StoreActor { + pub fn new(redis_url: String) -> StoreActor { let client = Client::open(redis_url).expect("Failed to create Redis client"); - RedisActor { client } + StoreActor { client } } } -impl Actor for RedisActor { +impl Actor for StoreActor { type Context = Context; } -impl Handler for RedisActor { +impl Handler for StoreActor { type Result = Result<(), redis::RedisError>; fn handle(&mut self, msg: SetScoreboardCommand, _: &mut Self::Context) -> Self::Result { @@ -34,7 +34,7 @@ impl Handler for RedisActor { } } -impl Handler for RedisActor { +impl Handler for StoreActor { type Result = Result, redis::RedisError>; fn handle(&mut self, msg: GetScoreboardCommand, _: &mut Self::Context) -> Self::Result { diff --git a/server/src/controllers/api.rs b/server/src/controllers/api.rs index bc375cd..fef1d3c 100644 --- a/server/src/controllers/api.rs +++ b/server/src/controllers/api.rs @@ -123,7 +123,7 @@ pub fn get_room_scoreboard( (_req, state, path): (HttpRequest, State, Path), ) -> Result { let room_token = path.into_inner(); - let result = state.redis_actor_addr.send(GetScoreboardCommand(room_token)).wait().unwrap(); + let result = state.store_actor_addr.send(GetScoreboardCommand(room_token)).wait().unwrap(); match result { Ok(scoreboard) => { let scoreboard_response: ScoreboardResponse = ScoreboardResponse { diff --git a/server/src/main.rs b/server/src/main.rs index 4dda832..9c60e0a 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -14,7 +14,7 @@ mod models; use crate::actors::{GameActor, RoomManagerActor}; use actix::{Actor, Addr, System}; use actix_web::{http::Method, middleware::Logger, server, App}; -use actors::RedisActor; +use actors::StoreActor; use lazy_static::lazy_static; use listenfd::ListenFd; use std::collections::HashSet; @@ -31,7 +31,7 @@ pub struct AppConfig { pub struct AppState { game_addr: Addr, - redis_actor_addr: Addr, + store_actor_addr: Addr, room_manager_addr: Addr, } @@ -56,19 +56,19 @@ fn main() -> Result<(), String> { let actor_system = System::new("meetup-server"); let redis_uri = APP_CONFIG.redis_uri.clone().unwrap_or("redis://127.0.0.1/".into()); - let redis_actor = RedisActor::new(redis_uri); - let redis_actor_addr = redis_actor.start(); + let store_actor = StoreActor::new(redis_uri); + let store_actor_addr = store_actor.start(); - let game_actor = GameActor::new(APP_CONFIG.game_config, redis_actor_addr.clone(), 0, 0, String::from("")); + let game_actor = GameActor::new(APP_CONFIG.game_config, store_actor_addr.clone(), 0, 0, String::from("")); let game_actor_addr = game_actor.start(); - let room_manager_actor = actors::RoomManagerActor::new(APP_CONFIG.game_config, redis_actor_addr.clone()); + let room_manager_actor = actors::RoomManagerActor::new(APP_CONFIG.game_config, store_actor_addr.clone()); let room_manager_addr = room_manager_actor.start(); let mut server = server::new(move || { let app_state = AppState { game_addr: game_actor_addr.clone(), - redis_actor_addr: redis_actor_addr.clone(), + store_actor_addr: store_actor_addr.clone(), room_manager_addr: room_manager_addr.clone(), }; From 190ad90e447991a154e038c931bf07e5cfadff03 Mon Sep 17 00:00:00 2001 From: Hao Ngo Date: Fri, 15 Dec 2023 01:38:56 +0700 Subject: [PATCH 7/8] chore: extract more of player info into scoreboard --- server/src/actors/game_actor.rs | 10 +++++- server/src/actors/store_actor.rs | 41 ++++++++++++++++++++-- server/src/controllers/api.rs | 60 +++++++++++++++++++++++++++++--- server/src/models/messages.rs | 13 +++++++ 4 files changed, 116 insertions(+), 8 deletions(-) diff --git a/server/src/actors/game_actor.rs b/server/src/actors/game_actor.rs index 9380e50..fd771d4 100644 --- a/server/src/actors/game_actor.rs +++ b/server/src/actors/game_actor.rs @@ -1,7 +1,7 @@ use crate::{ actors::{ClientWsActor, StoreActor}, game::{Game, TICKS_PER_SECOND}, - models::messages::{ClientStop, PlayerGameCommand, ServerCommand, SetScoreboardCommand}, + models::messages::{ClientStop, PlayerGameCommand, ServerCommand, SetScoreboardCommand, SetPlayerInfoCommand}, }; use actix::{Actor, Addr, AsyncContext, Context, Handler, Message}; use futures::sync::oneshot; @@ -224,6 +224,8 @@ impl Handler for GameActor { SocketEvent::Join(api_key, team_name, addr) => { let key_clone = api_key.clone(); let addr_clone = addr.clone(); + let cache_api_key = api_key.clone(); + let cache_team_name = team_name.clone(); info!("person joined - {:?}", api_key); @@ -264,6 +266,12 @@ impl Handler for GameActor { for addr in self.connections.values().chain(self.spectators.iter()) { addr.do_send(ServerToClient::TeamNames(self.team_names.clone())); } + + // Store player info to DB + let mut fields = HashMap::new(); + fields.insert("api_key".to_string(), cache_api_key); + fields.insert("team_name".to_string(), cache_team_name); + self.store_actor_addr.do_send(SetPlayerInfoCommand { player_id, fields }); } }, SocketEvent::Leave(api_key, addr) => { diff --git a/server/src/actors/store_actor.rs b/server/src/actors/store_actor.rs index 7c544cf..ea06540 100644 --- a/server/src/actors/store_actor.rs +++ b/server/src/actors/store_actor.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use actix::prelude::*; use redis::{Client, Commands, Connection}; -use crate::models::messages::{SetScoreboardCommand, GetScoreboardCommand}; +use crate::models::messages::{GetScoreboardCommand, SetPlayerInfoCommand, SetScoreboardCommand, GetMultiplePlayerInfo}; #[derive(Debug)] pub struct StoreActor { @@ -47,7 +47,44 @@ impl Handler for StoreActor { let total_points = total_points_str.parse::().unwrap_or_default() as u32; result.insert(player_id, total_points); } - + Ok(result) } } + +impl Handler for StoreActor { + type Result = Result; + + fn handle(&mut self, msg: SetPlayerInfoCommand, _: &mut Self::Context) -> Self::Result { + let mut con: Connection = self.client.get_connection()?; + + // Use hset_multiple to set multiple fields at the same time + let query_key = format!("player:{}:info", msg.player_id); + let fields: Vec<(&str, &str)> = + msg.fields.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect(); + let result: redis::RedisResult<()> = con.hset_multiple(query_key, &fields); + + result + .map_err(|e| e.into()) + .map(|_| format!("Fields are set for player_id {}", msg.player_id)) + } +} + +impl Handler for StoreActor { + type Result = Result, redis::RedisError>; + + fn handle(&mut self, msg: GetMultiplePlayerInfo, _: &mut Self::Context) -> Self::Result { + let mut con: Connection = self.client.get_connection()?; + let mut results = HashMap::new(); + for key in msg.player_ids { + let hash_key: String = format!("player:{}:info", key); + let player_info: HashMap = con.hgetall(&hash_key)?; + + // If you need to filter out empty rooms, you can add a condition here. + // For example, you can check if room_data is not empty before inserting into results. + results.insert(key.clone(), serde_json::to_string(&player_info).unwrap()); + } + + Ok(results) + } +} diff --git a/server/src/controllers/api.rs b/server/src/controllers/api.rs index fef1d3c..fcff2ba 100644 --- a/server/src/controllers/api.rs +++ b/server/src/controllers/api.rs @@ -1,8 +1,11 @@ +use std::collections::HashMap; + use crate::{ - actors::{ClientWsActor, CreateRoom, JoinRoom, ListRooms}, - models::messages::{GetScoreboardCommand, ServerCommand}, + actors::{ClientWsActor, CreateRoom, JoinRoom, ListRooms, StoreActor}, + models::messages::{GetMultiplePlayerInfo, GetScoreboardCommand, ServerCommand}, AppState, }; +use actix::Addr; use actix_web::{http::StatusCode, HttpRequest, Path, Query, State}; use futures::Future; @@ -13,10 +16,18 @@ pub struct QueryString { name: String, } +#[derive(Debug, Deserialize)] +pub struct PlayerInfo { + api_key: String, + team_name: String, +} + #[derive(Serialize, Deserialize)] -struct ScoreboardEntry { +pub struct ScoreboardEntry { player_id: u32, total_points: u32, + api_key: String, + team_name: String, } #[derive(Serialize, Deserialize)] @@ -119,6 +130,23 @@ pub fn list_rooms_handler( } } +fn get_scoreboard_player_info(player_ids: Vec, addr: Addr) -> Result, actix_web::Error> { + let result: Result, redis::RedisError> = + addr.send(GetMultiplePlayerInfo { player_ids }).wait().unwrap(); + + match result { + Ok(players) => { + let mut player_infos: HashMap = HashMap::new(); + for (id, player_data_json) in players { + let player_info: PlayerInfo = serde_json::from_str(&player_data_json).expect("Failed to deserialize JSON"); + player_infos.insert(id, player_info); + } + Ok(player_infos) + }, + Err(_) => Err(actix_web::error::ErrorBadRequest(String::from("failed to query player info data"))) + } +} + pub fn get_room_scoreboard( (_req, state, path): (HttpRequest, State, Path), ) -> Result { @@ -126,12 +154,34 @@ pub fn get_room_scoreboard( let result = state.store_actor_addr.send(GetScoreboardCommand(room_token)).wait().unwrap(); match result { Ok(scoreboard) => { + let player_ids: Vec = scoreboard.keys().cloned().collect(); + let player_info_map = get_scoreboard_player_info(player_ids, state.store_actor_addr.clone()).unwrap(); let scoreboard_response: ScoreboardResponse = ScoreboardResponse { scoreboard: scoreboard .into_iter() - .map(|(player_id, total_points)| ScoreboardEntry { player_id, total_points }) + .map(|(player_id, total_points)| { + player_info_map + .get(&player_id) + .map_or_else( + || { + info!("Failed to query player info by player_id"); + ScoreboardEntry { + player_id, + total_points, + api_key: String::from(""), + team_name: String::from(""), + } + }, + |info| ScoreboardEntry { + player_id, + total_points, + api_key: info.api_key.clone(), + team_name: info.team_name.clone(), + }, + ) + }) .collect(), - }; + }; let body = serde_json::to_string(&scoreboard_response)?; Ok(actix_web::HttpResponse::with_body(StatusCode::OK, body)) }, diff --git a/server/src/models/messages.rs b/server/src/models/messages.rs index cc95699..79f85a3 100644 --- a/server/src/models/messages.rs +++ b/server/src/models/messages.rs @@ -27,3 +27,16 @@ pub struct SetScoreboardCommand { #[derive(Message)] #[rtype(result = "Result, redis::RedisError>")] pub struct GetScoreboardCommand(pub String); + +#[derive(Message)] +#[rtype(result = "Result")] +pub struct SetPlayerInfoCommand { + pub player_id: u32, + pub fields: HashMap, // Use a HashMap to represent multiple fields +} + +#[derive(Message)] +#[rtype(result = "Result, redis::RedisError>")] +pub struct GetMultiplePlayerInfo { + pub player_ids: Vec, +} \ No newline at end of file From 43f12680248327f74fdb2d2100ac2cde1c2eecbf Mon Sep 17 00:00:00 2001 From: Hao Ngo Date: Fri, 15 Dec 2023 01:48:17 +0700 Subject: [PATCH 8/8] chore: remove unnecessary comments --- server/src/actors/store_actor.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/server/src/actors/store_actor.rs b/server/src/actors/store_actor.rs index ea06540..091d65f 100644 --- a/server/src/actors/store_actor.rs +++ b/server/src/actors/store_actor.rs @@ -79,9 +79,6 @@ impl Handler for StoreActor { for key in msg.player_ids { let hash_key: String = format!("player:{}:info", key); let player_info: HashMap = con.hgetall(&hash_key)?; - - // If you need to filter out empty rooms, you can add a condition here. - // For example, you can check if room_data is not empty before inserting into results. results.insert(key.clone(), serde_json::to_string(&player_info).unwrap()); }