From 6a0a308bc1d4eb9a4d625dd682d5144a79250bb5 Mon Sep 17 00:00:00 2001 From: Erb3 <49862976+Erb3@users.noreply.github.com> Date: Wed, 5 Jun 2024 15:32:09 +0200 Subject: [PATCH] refactor: complete state refactor Fixes #37 --- Cargo.lock | 146 +++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + frontend/game.js | 11 ++-- src/game.rs | 122 ++++++++++++++------------------------- src/main.rs | 12 ++-- src/packets.rs | 9 ++- src/state.rs | 99 ++++++++++++++++++++++++++++++++ 7 files changed, 306 insertions(+), 94 deletions(-) create mode 100644 src/state.rs diff --git a/Cargo.lock b/Cargo.lock index 5f030c6..a05763f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "async-trait" version = "0.1.80" @@ -134,6 +149,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + [[package]] name = "byteorder" version = "1.5.0" @@ -161,6 +182,26 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets 0.52.5", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + [[package]] name = "cpufeatures" version = "0.2.12" @@ -442,6 +483,29 @@ dependencies = [ "tokio", ] +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "idna" version = "0.5.0" @@ -458,6 +522,15 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -568,6 +641,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.16.0" @@ -942,6 +1024,7 @@ name = "sveio" version = "0.1.0" dependencies = [ "axum", + "chrono", "dotenvy", "geoutils", "rand", @@ -1284,6 +1367,60 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + [[package]] name = "winapi" version = "0.3.9" @@ -1315,6 +1452,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.5", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index aa66609..fbeefb0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ tower = "0.4.13" dotenvy = "0.15.7" geoutils = "0.5.1" regex = "1.10.4" +chrono = "0.4.38" [profile.release] codegen-units = 1 diff --git a/frontend/game.js b/frontend/game.js index 6e4609a..ea4ea20 100644 --- a/frontend/game.js +++ b/frontend/game.js @@ -122,10 +122,13 @@ socket.on("solution", (data) => { mapElement.classList.add("cursor-grab"); }); -socket.on("join-response", (data) => { - if (!data.ok) { - location.href = "/?error=" + data.error; - } +socket.on("join-response", () => { + console.log("Connected!"); +}); + +socket.on("kick", (data) => { + console.log(data); + location.href = "/?error=" + data.message; }); socket.on("connect", () => { diff --git a/src/game.rs b/src/game.rs index 3f92d8e..ca57b47 100644 --- a/src/game.rs +++ b/src/game.rs @@ -1,53 +1,29 @@ -use crate::{datasource, packets, utils}; +use crate::state::GameState; +use crate::{datasource, packets, state, utils}; +use chrono::Utc; use geoutils::Location; use rand::{thread_rng, Rng}; use regex::Regex; -use serde::{Deserialize, Serialize}; use socketioxide::extract::{Data, SocketRef, State}; -use socketioxide::socket::Sid; use socketioxide::SocketIo; -use std::collections::HashMap; -use std::sync::{Arc, Mutex}; use std::time::Duration; use tokio::time; use tracing::info; -#[derive(Deserialize, Serialize, Debug, Clone)] -#[serde(transparent)] -pub struct Username(String); - -#[derive(Serialize, Debug, Clone)] -pub struct Player { - pub username: Username, - pub score: u64, -} - -pub type Guesses = HashMap; -pub type PlayerMap = HashMap; - -pub struct GameState { - pub guesses: Guesses, - pub leaderboard: PlayerMap, -} -type EncapsulatedGameState = Arc>; - pub fn on_connect(socket: SocketRef) { info!("🆕 Client connected with ID {}", socket.id); socket.on( "join", - |socket: SocketRef, - Data::(data), - state: State| { + |socket: SocketRef, Data::(data), state: State| async move { let username_regex = Regex::new(r"^[A-Za-z0-9 _-]{1,32}$").unwrap(); if !username_regex.is_match(&data.username) { socket .emit( - "join-response", - packets::JoinResponsePacket { - ok: false, - error: Some("Bad username".to_string()), + "kick", + packets::DisconnectPacket { + message: "Bad username".to_string(), }, ) .unwrap(); @@ -56,20 +32,12 @@ pub fn on_connect(socket: SocketRef) { return; } - if state - .lock() - .unwrap() - .leaderboard - .clone() - .into_iter() - .any(|v| v.1.username.0 == data.username) - { + if state.is_username_taken(data.username.clone()).await { socket .emit( - "join-response", - packets::JoinResponsePacket { - ok: false, - error: Some("Username taken".to_string()), + "kick", + packets::DisconnectPacket { + message: "Username taken".to_string(), }, ) .unwrap(); @@ -78,24 +46,11 @@ pub fn on_connect(socket: SocketRef) { return; } - state.lock().unwrap().leaderboard.insert( - socket.id, - Player { - username: Username(data.username.clone()), - score: 0, - }, - ); - - socket - .emit( - "join-response", - packets::JoinResponsePacket { - ok: true, - error: None, - }, - ) - .unwrap(); + state + .insert_player(socket.id, state::Player::new(data.username.clone())) + .await; + socket.emit("join-response", "").unwrap(); socket.join("PRIMARY").unwrap(); info!( @@ -107,25 +62,20 @@ pub fn on_connect(socket: SocketRef) { socket.on( "guess", - |socket: SocketRef, - Data::(data), - game_state: State| { + |socket: SocketRef, Data::(data), state: State| async move { info!("📬 Received message: {:?}", data); - game_state.lock().unwrap().guesses.insert(socket.id, data); + state.insert_guess(socket.id, data).await; + state.update_last_packet(socket.id).await; }, ); - socket.on_disconnect(|s: SocketRef, state: State| { - state.lock().unwrap().leaderboard.remove(&s.id); + socket.on_disconnect(|s: SocketRef, state: State| async move { + state.remove_player(s.id).await; info!("🚪 User {} disconnected.", s.id); }); } -pub async fn game_loop( - cities: Vec, - io: SocketIo, - game_state: EncapsulatedGameState, -) { +pub async fn game_loop(cities: Vec, io: SocketIo, state: GameState) { let mut interval = time::interval(Duration::from_secs(5)); let mut last_city: Option<&datasource::City> = None; @@ -133,26 +83,25 @@ pub async fn game_loop( interval.tick().await; if let Some(city) = last_city.cloned() { - let mut state = game_state.lock().unwrap(); let target = Location::new(city.latitude, city.longitude); - for guess in state.guesses.clone() { + for guess in state.get_guesses().await { let packet = guess.1; let distance = target.distance_to(&geoutils::Location::new(packet.lat, packet.long)); let points = utils::calculate_score(distance.unwrap().meters() / 1000.0); - if let Some(existing_player) = state.leaderboard.get(&guess.0) { + if let Some(existing_player) = state.get_player(guess.0).await { let mut p = existing_player.to_owned(); p.score += points; - state.leaderboard.insert(guess.0, p); + state.insert_player(guess.0, p).await; } } let solution = packets::SolutionPacket { location: city, - guesses: state.guesses.clone(), - leaderboard: state.leaderboard.clone(), + guesses: state.get_guesses().await, + leaderboard: state.get_players().await, }; io.to("PRIMARY") @@ -169,11 +118,28 @@ pub async fn game_loop( }; info!("📍 New location: {}, {}", &city.name, &city.country); - game_state.lock().unwrap().guesses.clear(); + state.clear_guesses().await; + io.to("PRIMARY") .emit("newTarget", target_message) .expect("Unable to broadcast new target"); last_city = Some(city); + + for socket in io.sockets().unwrap() { + if let Some(player) = state.get_player(socket.id).await { + if Utc::now().timestamp_millis() > player.last_packet + 30 * 1000 { + socket + .emit( + "kick", + packets::DisconnectPacket { + message: "Automatically removed due to inactivity".to_string(), + }, + ) + .unwrap(); + socket.disconnect().unwrap(); + } + } + } } } diff --git a/src/main.rs b/src/main.rs index bb572a2..7cd0e0f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ mod datasource; mod game; mod packets; +mod state; mod utils; use axum::handler::Handler; use axum::http::{HeaderMap, Method, StatusCode}; @@ -8,7 +9,7 @@ use axum::response::IntoResponse; use axum::routing::get; use dotenvy::dotenv; use socketioxide::SocketIoBuilder; -use std::sync::{Arc, Mutex}; +use state::GameState; use tokio::fs; use tower::ServiceBuilder; use tower_http::cors::{Any, CorsLayer}; @@ -57,13 +58,10 @@ async fn main() -> Result<(), Box> { let cities = datasource::get_cities().await; info!("✨ Loaded {} cities", cities.len()); - let socketio_state = Arc::new(Mutex::new(game::GameState { - guesses: game::Guesses::new(), - leaderboard: game::PlayerMap::new(), - })); + let socketio_state = GameState::new(); let (socketio_layer, io) = SocketIoBuilder::new() - .with_state(Arc::clone(&socketio_state)) + .with_state(socketio_state.clone()) .build_layer(); io.ns("/", game::on_connect); @@ -92,7 +90,7 @@ async fn main() -> Result<(), Box> { info!("🎮 Starting game loop"); - tokio::spawn(async move { + tokio::spawn(async { game::game_loop(cities, io, socketio_state).await; }); diff --git a/src/packets.rs b/src/packets.rs index b049f13..0dae663 100644 --- a/src/packets.rs +++ b/src/packets.rs @@ -1,9 +1,9 @@ use crate::datasource; -use crate::game::{Guesses, PlayerMap}; +use crate::state::{Guesses, PlayerMap}; use serde::{Deserialize, Serialize}; #[derive(Debug, Deserialize, Serialize, Clone)] -pub struct GuessMessage { +pub struct GuessPacket { pub lat: f32, pub long: f32, } @@ -22,7 +22,6 @@ pub struct JoinMessage { } #[derive(Serialize, Debug)] -pub struct JoinResponsePacket { - pub ok: bool, - pub error: Option, +pub struct DisconnectPacket { + pub message: String, } diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..9993a07 --- /dev/null +++ b/src/state.rs @@ -0,0 +1,99 @@ +use crate::packets::{self, GuessPacket}; +use chrono::Utc; +use serde::{Deserialize, Serialize}; +use socketioxide::socket::Sid; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::RwLock; + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(transparent)] +pub struct Username(String); + +#[derive(Debug, Clone, Serialize)] +pub struct Player { + pub username: Username, + pub score: u64, + + #[serde(skip_serializing)] + pub last_packet: i64, +} + +impl Player { + pub fn new(username: String) -> Player { + return Player { + username: Username(username), + score: 0, + last_packet: Utc::now().timestamp_millis(), + }; + } +} + +pub type Guesses = HashMap; +pub type PlayerMap = HashMap; + +#[derive(Clone)] +pub struct GameState { + guesses: Arc>, + players: Arc>, +} + +impl GameState { + pub fn new() -> GameState { + return GameState { + guesses: Arc::new(RwLock::new(Guesses::new())), + players: Arc::new(RwLock::new(PlayerMap::new())), + }; + } + + // Guesses + + pub async fn get_guesses(&self) -> Guesses { + self.guesses.read().await.clone() + } + + pub async fn clear_guesses(&self) { + self.guesses.write().await.clear() + } + + pub async fn insert_guess(&self, sid: Sid, guess: GuessPacket) { + self.guesses.write().await.insert(sid, guess); + } + + // Players + + pub async fn get_players(&self) -> PlayerMap { + self.players.read().await.clone() + } + + pub async fn insert_player(&self, sid: Sid, player: Player) { + self.players.write().await.insert(sid, player); + } + + pub async fn get_player(&self, sid: Sid) -> Option { + match self.players.read().await.get(&sid) { + Some(player) => Some(player.to_owned()), + None => None, + } + } + + pub async fn remove_player(&self, sid: Sid) { + self.players.write().await.remove(&sid); + } + + pub async fn is_username_taken(&self, wanted: String) -> bool { + self.get_players() + .await + .into_iter() + .any(|v| v.1.username.0 == wanted) + } + + pub async fn update_last_packet(&self, sid: Sid) { + let player = self.get_player(sid).await; + + if let Some(mut p) = player { + p.last_packet = Utc::now().timestamp_millis(); + self.players.write().await.insert(sid, p); + } + } +}