diff --git a/Cargo.lock b/Cargo.lock index 84faaa0..baa815f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,19 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "const-random", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "anyhow" version = "1.0.81" @@ -34,6 +47,12 @@ dependencies = [ "syn", ] +[[package]] +name = "atomic" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" + [[package]] name = "autocfg" version = "1.1.0" @@ -170,6 +189,26 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom", + "once_cell", + "tiny-keccak", +] + [[package]] name = "cpufeatures" version = "0.2.12" @@ -179,6 +218,12 @@ dependencies = [ "libc", ] +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-common" version = "0.1.6" @@ -218,6 +263,18 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "flurry" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7874ce5eeafa5e546227f7c62911e586387bf03d6c9a45ac78aa1c3bc2fedb61" +dependencies = [ + "ahash", + "num_cpus", + "parking_lot", + "seize", +] + [[package]] name = "fnv" version = "1.0.7" @@ -482,9 +539,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.20" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "matchit" @@ -549,6 +606,16 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + [[package]] name = "parking_lot_core" version = "0.9.9" @@ -681,12 +748,28 @@ version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" +[[package]] +name = "scc" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4c10d60d2fd9faf0e8b6540623f5e502343bde2ac585a7d158b3db24d93fcbe" + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "seize" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e5739de653b129b0a59da381599cf17caf24bc586f6a797c52d3d6147c5b85a" +dependencies = [ + "num_cpus", + "once_cell", +] + [[package]] name = "serde" version = "1.0.197" @@ -748,9 +831,12 @@ dependencies = [ "axum", "bincode", "dashmap", + "flurry", "futures", "lazy_static", + "log", "rand", + "scc", "serde", "slotmap", "tokio", @@ -846,6 +932,15 @@ dependencies = [ "syn", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -1018,6 +1113,7 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" dependencies = [ + "atomic", "getrandom", "serde", ] @@ -1165,3 +1261,23 @@ name = "windows_x86_64_msvc" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + +[[package]] +name = "zerocopy" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/crates/server/Cargo.toml b/crates/server/Cargo.toml index 147bbb2..40eb39e 100644 --- a/crates/server/Cargo.toml +++ b/crates/server/Cargo.toml @@ -11,13 +11,16 @@ anyhow = "1.0.81" axum = { version = "0.7.5", features = ["ws"] } bincode = "1.3.3" dashmap = "5.5.3" +flurry = "0.5.0" futures = "0.3.30" lazy_static = "1.4.0" +log = "0.4.21" rand = "0.8.5" +scc = "2.0.19" serde = { version = "1.0.197", features = ["derive"] } slotmap = { version = "1.0.7", features = ["serde"] } tokio = { version = "1.37.0", features = ["macros", "rt-multi-thread"] } -uuid = { version = "1.8.0", features = ["v4", "serde"] } +uuid = { version = "1.8.0", features = ["v4", "serde", "v7"] } # [lints] # workspace = true diff --git a/crates/server/src dada/lib.rs b/crates/server/src dada/lib.rs new file mode 100644 index 0000000..fa0eeff --- /dev/null +++ b/crates/server/src dada/lib.rs @@ -0,0 +1,42 @@ +use std::hash::Hash; +use std::ops::{Deref, DerefMut}; + +use dashmap::mapref::multiple::RefMutMulti; +use dashmap::mapref::one::RefMut; + +pub enum GlobalRefMut<'a, K: Eq + Hash, V> { + Single(RefMut<'a, K, V>), + Multi(RefMutMulti<'a, K, V>), +} + +impl<'a, K: Eq + Hash, V> From> for GlobalRefMut<'a, K, V> { + fn from(v: RefMut<'a, K, V>) -> Self { + Self::Single(v) + } +} + +impl<'a, K: Eq + Hash, V> From> for GlobalRefMut<'a, K, V> { + fn from(v: RefMutMulti<'a, K, V>) -> Self { + Self::Multi(v) + } +} + +impl<'a, K: Eq + Hash, V> Deref for GlobalRefMut<'a, K, V> { + type Target = V; + + fn deref(&self) -> &Self::Target { + match self { + Self::Single(v) => v.value(), + Self::Multi(v) => v.value(), + } + } +} + +impl<'a, K: Eq + Hash, V> DerefMut for GlobalRefMut<'a, K, V> { + fn deref_mut(&mut self) -> &mut Self::Target { + match self { + Self::Single(v) => v.value_mut(), + Self::Multi(v) => v.value_mut(), + } + } +} diff --git a/crates/server/src dada/main.rs b/crates/server/src dada/main.rs new file mode 100644 index 0000000..11e2ed2 --- /dev/null +++ b/crates/server/src dada/main.rs @@ -0,0 +1,182 @@ +use std::collections::{HashMap, HashSet}; +use std::hash::RandomState; +use std::sync::Arc; + +use anyhow::{bail, Context}; +use axum::extract::ws::{Message, WebSocket}; +use axum::extract::WebSocketUpgrade; +use axum::routing::get; +use axum::Router; +use dashmap::mapref::entry::Entry; +use dashmap::mapref::multiple::RefMutMulti; +use dashmap::mapref::one::RefMut; +use dashmap::DashMap; +use futures::stream::{SplitSink, SplitStream}; +use futures::{SinkExt, StreamExt}; +use lazy_static::lazy_static; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; +use server::GlobalRefMut; +use slotmap::SlotMap; +use tokio::sync::mpsc::{channel, Receiver, Sender}; +use tokio::sync::RwLock; +use uuid::Uuid; + +#[derive(Clone)] +pub struct ConnectionSender(Sender>); + +impl ConnectionSender { + pub async fn send(&mut self, message: T) -> anyhow::Result<()> { + Ok(self.0.send(bincode::serialize(&message)?).await?) + } +} + +pub struct ConnectionReader(SplitStream); + +impl ConnectionReader { + pub async fn read(&mut self) -> anyhow::Result { + loop { + let Message::Binary(message) = self.0.next().await.context("no message")?? else { + continue; + }; + return Ok(bincode::deserialize(&message)?); + } + } +} + +#[tokio::main] +async fn main() { + let app = Router::new().route( + "/", + get(|ws: WebSocketUpgrade| async { + ws.on_upgrade(|socket| async { + let (mut sender, receiver) = socket.split(); + let (send_tx, mut send_rx) = channel(16); + tokio::spawn(async move { + while let Some(message) = send_rx.recv().await { + sender.send(Message::Binary(message)).await?; + } + Ok::<(), axum::Error>(()) + }); + if let Err(e) = handle(ConnectionSender(send_tx), ConnectionReader(receiver)).await + { + eprintln!("Error: {}", e); + } + }) + }), + ); + let listener = tokio::net::TcpListener::bind("0.0.0.0:80") + .await + .expect("failed to bind"); + axum::serve(listener, app).await.expect("failed to serve"); +} + +lazy_static! { + static ref LOBBIES: DashMap = DashMap::new(); +} + +#[derive(Serialize, Deserialize)] +enum LoginRequest { + CreateLobby { + username: String, + public: bool, + }, + JoinLobby { + lobby_id: Option, + username: String, + }, +} + +slotmap::new_key_type! { +struct ConnectionId;} + +struct Lobby { + id: Uuid, + public: bool, + connections: SlotMap, +} + +struct LobbyConnection { + sender: ConnectionSender, + username: String, + ready: bool, +} + +#[derive(Serialize, Deserialize)] +struct LobbyJoined(Uuid); + +#[derive(Serialize, Deserialize)] +struct IAmReady; + +async fn handle( + mut sender: ConnectionSender, + mut receiver: ConnectionReader, +) -> anyhow::Result<()> { + // Find or create a lobby + let login_request: LoginRequest = receiver.read().await?; + let (mut lobby, username) = match login_request { + LoginRequest::CreateLobby { username, public } => (create_lobby(public).await, username), + LoginRequest::JoinLobby { lobby_id, username } => ( + match lobby_id { + Some(id) => LOBBIES.get_mut(&id).context("lobby not found")?.into(), + None => match find_random_lobby().await { + Some(lobby) => lobby, + None => create_lobby(true).await, + }, + }, + username, + ), + }; + + // Add the user to the lobby + let lobby_id = lobby.id; + sender.send(LobbyJoined(lobby_id)).await?; + let connection_id = lobby.connections.insert(LobbyConnection { + sender, + username, + ready: false, + }); + drop(lobby); + + // Wait for the user to be ready + let disconnected = receiver.read::().await.is_err(); + + // Check to start the game + let Entry::Occupied(mut lobby) = LOBBIES.entry(lobby_id) else { + bail!("lobby not found"); + }; + if disconnected { + lobby.get_mut().connections.remove(connection_id); + } + + if lobby.get().connections.is_empty() { + LOBBIES.remove(&lobby_id); + return Ok(()); + } + let should_start = lobby.connections.iter().all(|(_, c)| c.ready); + + todo!() +} + +async fn create_lobby(public: bool) -> GlobalRefMut<'static, Uuid, Lobby> { + loop { + let id = Uuid::new_v4(); + if let Entry::Vacant(e) = LOBBIES.entry(id) { + break e + .insert(Lobby { + id, + public, + connections: SlotMap::with_key(), + }) + .into(); + } + } +} + +async fn find_random_lobby() -> Option> { + LOBBIES + .iter_mut() + .filter(|lobby| lobby.public) + .min_by_key(|lobby| lobby.connections.len()) + .map(GlobalRefMut::from) +} diff --git a/crates/server/src/game.rs b/crates/server/src save/game.rs similarity index 100% rename from crates/server/src/game.rs rename to crates/server/src save/game.rs diff --git a/crates/server/src/game_save.rs b/crates/server/src save/game_save.rs similarity index 100% rename from crates/server/src/game_save.rs rename to crates/server/src save/game_save.rs diff --git a/crates/server/src save/lib dada.rs b/crates/server/src save/lib dada.rs new file mode 100644 index 0000000..1c1eb1d --- /dev/null +++ b/crates/server/src save/lib dada.rs @@ -0,0 +1,97 @@ +use std::collections::{HashMap, HashSet}; + +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct GameId(Uuid); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct PlayerId(Uuid); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct PlayerSecret(Uuid); + +#[derive(Serialize, Deserialize)] +pub struct PlayerProfile { + pub username: String, + pub image_id: Uuid, +} + +#[derive(Serialize, Deserialize)] +pub enum LoginRequest { + Create { + username: String, + }, + JoinRandom { + username: String, + }, + Join { + game_id: GameId, + username: String, + }, + Rejoin { + game_id: GameId, + player_id: PlayerId, + secret: PlayerSecret, + }, +} + +#[derive(Serialize, Deserialize)] +pub enum LoginResponse { + Refused(String), + Success(T), +} + +#[derive(Serialize, Deserialize)] +pub struct CreateResponse { + game_id: GameId, + player_id: PlayerId, + secret: PlayerSecret, + options: Vec, +} + +#[derive(Serialize, Deserialize)] +pub struct GameSettingField { + pub name: String, + pub description: String, + pub field_type: GameSettingFieldType, +} + +#[derive(Serialize, Deserialize)] +pub enum GameSettingFieldType { + Integer { min: i32, max: i32 }, + Decimal { min: f32, max: f32 }, + String { min_len: usize, max_len: usize }, + Choice { choices: HashSet }, + Boolean, +} + +#[derive(Serialize, Deserialize)] +pub enum GameSettingFieldValue { + Integer(i32), + Decimal(f32), + String(String), + Choice(String), + Boolean(bool), +} + +#[derive(Serialize, Deserialize)] +pub struct JoinRandomResponse { + game_id: GameId, + player_id: PlayerId, + secret: PlayerSecret, + players: HashMap, +} + +#[derive(Serialize, Deserialize)] +pub struct JoinResponse { + player_id: PlayerId, + secret: PlayerSecret, + players: HashMap, +} + +#[derive(Serialize, Deserialize)] +pub struct RejoinResponse { + players: HashMap, +} diff --git a/crates/server/src save/lib save.rs b/crates/server/src save/lib save.rs new file mode 100644 index 0000000..f2c5429 --- /dev/null +++ b/crates/server/src save/lib save.rs @@ -0,0 +1,62 @@ +use std::marker::PhantomData; + +use dashmap::mapref::entry::Entry; +use dashmap::DashMap; +use uuid::Uuid; + +pub trait UuidKey: From { + fn to_uuid(&self) -> Uuid; +} + +pub struct SlotDashMap(DashMap, PhantomData); + +impl SlotDashMap { + pub fn new() -> Self { + Self(DashMap::new(), PhantomData) + } + + pub fn insert(&self, value: V) -> K { + loop { + let id = Uuid::new_v4(); + let Entry::Vacant(entry) = self.0.entry(id) else { + continue; + }; + entry.insert(value); + return K::from(id); + } + } +} + +impl Default for SlotDashMap { + fn default() -> Self { + Self::new() + } +} + +#[macro_export] +macro_rules! new_key_type { + ( $(#[$outer:meta])* $vis:vis struct $name:ident; $($rest:tt)* ) => { + $(#[$outer])* + #[derive(Copy, Clone, Default, + Eq, PartialEq, Ord, PartialOrd, + Hash, Debug)] + #[repr(transparent)] + $vis struct $name(::uuid::Uuid); + + impl ::core::convert::From<::uuid::Uuid> for $name { + fn from(k: ::uuid::Uuid) -> Self { + $name(k) + } + } + + impl $crate::UuidKey for $name { + fn to_uuid(&self) -> ::uuid::Uuid { + self.0 + } + } + + $crate::new_key_type!($($rest)*); + }; + + () => {} +} diff --git a/crates/server/src save/lib.rs b/crates/server/src save/lib.rs new file mode 100644 index 0000000..6698b14 --- /dev/null +++ b/crates/server/src save/lib.rs @@ -0,0 +1,64 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct LobbyId(Uuid); + +#[derive(Serialize, Deserialize)] +pub struct Lobby { + id: LobbyId, + public: bool, + players: HashMap, +} + +#[derive(Serialize, Deserialize)] +pub struct LobbyPlayer { + username: String, + ready: bool, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct PlayerId(Uuid); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct GameId(Uuid); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct RejoinToken(Uuid); + +#[derive(Serialize, Deserialize)] +pub enum LoginRequest { + CreateLobby { + username: String, + public: bool, + }, + JoinLobby { + lobby_id: Option, + username: String, + }, + RejoinGame { + token: RejoinToken, + }, +} + +#[derive(Serialize, Deserialize)] +pub struct LobbyJoined { + player_id: PlayerId, + lobby: Lobby, +} + +#[derive(Serialize, Deserialize)] +pub enum LobbyClientPacket { + Ready(bool), +} + +#[derive(Serialize, Deserialize)] +pub enum LobbyServerPacket { + LobbyUpdated(Lobby), + GameStarted(GameId, RejoinToken), +} + +#[derive(Serialize, Deserialize)] +pub struct RejoinResponse; diff --git a/crates/server/src/login.rs b/crates/server/src save/login.rs similarity index 100% rename from crates/server/src/login.rs rename to crates/server/src save/login.rs diff --git a/crates/server/src/main save.rs b/crates/server/src save/main save.rs similarity index 100% rename from crates/server/src/main save.rs rename to crates/server/src save/main save.rs diff --git a/crates/server/src save/main.rs b/crates/server/src save/main.rs new file mode 100644 index 0000000..5deffdd --- /dev/null +++ b/crates/server/src save/main.rs @@ -0,0 +1,62 @@ +use std::collections::HashMap; + +use anyhow::{bail, Context}; +use axum::extract::ws::{Message, WebSocket}; +use axum::extract::WebSocketUpgrade; +use axum::routing::get; +use axum::Router; +use dashmap::mapref::entry::Entry; +use dashmap::DashMap; +use lazy_static::lazy_static; +use serde::{Deserialize, Serialize}; +use server::{GameId, Lobby, LobbyId, LoginRequest, PlayerId}; +use uuid::Uuid; + +lazy_static! { + static ref LOBBIES: DashMap = DashMap::new(); + // static ref GAMES: DashMap = DashMap::new(); +} + +#[tokio::main] +async fn main() { + let app = Router::new().route( + "/", + get(|ws: WebSocketUpgrade| async { + ws.on_upgrade(|socket| async { + if let Err(e) = handle(socket).await { + eprintln!("Error: {}", e); + } + }) + }), + ); + let listener = tokio::net::TcpListener::bind("0.0.0.0:80") + .await + .expect("failed to bind"); + axum::serve(listener, app).await.expect("failed to serve"); +} + +async fn handle(mut socket: WebSocket) -> anyhow::Result<()> { + let Message::Binary(login_data) = socket.recv().await.context("client disconnected")?? else { + bail!("expected login request"); + }; + let login_request = bincode::deserialize(&login_data)?; + match login_request { + LoginRequest::CreateLobby { username, public } => { + let lobby_id = loop { + let id = Uuid::new_v4(); + let Entry::Vacant(entry) = LOBBIES.entry(LobbyId(id)) else { + continue; + }; + entry.insert() + } + } + LoginRequest::JoinLobby { lobby_id, username } => todo!(), + LoginRequest::RejoinGame { token } => todo!(), + } + + Ok(()) +} + +async fn handle_game_creation(mut socket: WebSocket, username: String) -> anyhow::Result<()> { + Ok(()) +} diff --git a/crates/server/src save/utils.rs b/crates/server/src save/utils.rs new file mode 100644 index 0000000..cc67605 --- /dev/null +++ b/crates/server/src save/utils.rs @@ -0,0 +1,73 @@ +use std::marker::PhantomData; + +use dashmap::mapref::entry::Entry; +use dashmap::DashMap; +use uuid::Uuid; + +pub trait UuidKey: From + Copy { + fn to_uuid(&self) -> Uuid; +} + +#[macro_export] +macro_rules! new_id_type { + ( $($vis:vis struct $name:ident;)* ) => { + $( + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, ::serde::Serialize, ::serde::Deserialize)] + $vis struct $name(::uuid::Uuid); + + impl ::core::convert::From<::uuid::Uuid> for $name { + fn from(k: ::uuid::Uuid) -> Self { + $name(k) + } + } + + impl $crate::utils::UuidKey for $name { + fn to_uuid(&self) -> ::uuid::Uuid { + self.0 + } + } + )* + }; +} + +pub struct IdDashMap(DashMap, PhantomData); + +impl IdDashMap { + pub fn new() -> Self { + Self(DashMap::new(), PhantomData) + } + + pub fn insert(&self, value: V) -> K { + loop { + let id = Uuid::new_v4(); + let Entry::Vacant(entry) = self.0.entry(id) else { + continue; + }; + entry.insert(value); + return K::from(id); + } + } + + pub fn insert_with_id(&self, create_value: impl FnOnce(K) -> V) -> K { + loop { + let id = Uuid::new_v4(); + let Entry::Vacant(entry) = self.0.entry(id) else { + continue; + }; + let id = K::from(id); + let value = create_value(id); + entry.insert(value); + return id; + } + } + + pub fn remove(&self, id: K) { + self.0.remove(&id.to_uuid()); + } +} + +impl Default for IdDashMap { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/server/src/lib.rs b/crates/server/src/lib.rs index f2c5429..f3b07fc 100644 --- a/crates/server/src/lib.rs +++ b/crates/server/src/lib.rs @@ -1,62 +1,38 @@ -use std::marker::PhantomData; +use std::collections::HashMap; -use dashmap::mapref::entry::Entry; -use dashmap::DashMap; +use serde::{Deserialize, Serialize}; use uuid::Uuid; -pub trait UuidKey: From { - fn to_uuid(&self) -> Uuid; +#[derive(Serialize, Deserialize)] +pub enum ClientPacket { + Disconnect, + CreateLobby { + username: String, + public: bool, + }, + JoinLobby { + lobby_id: Option, + username: String, + }, + IAmReady, + IAmNotReady, } -pub struct SlotDashMap(DashMap, PhantomData); - -impl SlotDashMap { - pub fn new() -> Self { - Self(DashMap::new(), PhantomData) - } - - pub fn insert(&self, value: V) -> K { - loop { - let id = Uuid::new_v4(); - let Entry::Vacant(entry) = self.0.entry(id) else { - continue; - }; - entry.insert(value); - return K::from(id); - } - } +#[derive(Serialize, Deserialize)] +pub enum ServerPacket { + Refused(String), + LobbyJoined(Uuid), + LobbyUpdated(Lobby), } -impl Default for SlotDashMap { - fn default() -> Self { - Self::new() - } +#[derive(Serialize, Deserialize)] +pub struct Lobby { + pub public: bool, + pub players: HashMap, } -#[macro_export] -macro_rules! new_key_type { - ( $(#[$outer:meta])* $vis:vis struct $name:ident; $($rest:tt)* ) => { - $(#[$outer])* - #[derive(Copy, Clone, Default, - Eq, PartialEq, Ord, PartialOrd, - Hash, Debug)] - #[repr(transparent)] - $vis struct $name(::uuid::Uuid); - - impl ::core::convert::From<::uuid::Uuid> for $name { - fn from(k: ::uuid::Uuid) -> Self { - $name(k) - } - } - - impl $crate::UuidKey for $name { - fn to_uuid(&self) -> ::uuid::Uuid { - self.0 - } - } - - $crate::new_key_type!($($rest)*); - }; - - () => {} +#[derive(Serialize, Deserialize)] +pub struct LobbyPlayer { + pub username: String, + pub ready: bool, } diff --git a/crates/server/src/lobby.rs b/crates/server/src/lobby.rs new file mode 100644 index 0000000..2c2e36f --- /dev/null +++ b/crates/server/src/lobby.rs @@ -0,0 +1,18 @@ +use axum::http::header::Entry; +use dashmap::DashMap; +use lazy_static::lazy_static; +use scc::{HashMap, HashSet}; +use server::LobbyStatus; +use uuid::Uuid; + +lazy_static! { + static ref LOBBIES: DashMap = DashMap::new(); +} + +pub(crate) async fn create_lobby( + client_id: Uuid, + username: String, + public: bool, +) -> anyhow::Result<()> { + todo!() +} diff --git a/crates/server/src/main.rs b/crates/server/src/main.rs index a131272..2ee6c66 100644 --- a/crates/server/src/main.rs +++ b/crates/server/src/main.rs @@ -1,22 +1,57 @@ -use axum::extract::ws::WebSocket; +use std::borrow::{Borrow, Cow}; +use std::collections::HashMap; + +use axum::extract::ws::{Message, WebSocket}; use axum::extract::WebSocketUpgrade; use axum::routing::get; use axum::Router; +use dashmap::DashMap; +use futures::{SinkExt, StreamExt}; +use lazy_static::lazy_static; +use log::warn; +use server::{ClientPacket, Lobby, LobbyPlayer, ServerPacket}; +use tokio::sync::mpsc::{channel, Sender}; +use tokio::sync::RwLock; +use uuid::Uuid; -mod game; -mod login; +struct Client { + status: ClientStatus, + sender: Sender>, +} + +enum ClientStatus { + Unauthenticated, + InLobby(Uuid), + InGame(Uuid), +} + +lazy_static! { + static ref CLIENTS: RwLock> = RwLock::new(HashMap::new()); + static ref LOBBIES: RwLock> = RwLock::new(HashMap::new()); +} + +pub async fn send_message<'a>(client_id: Uuid, message: impl Into>) { + if let Some(client) = CLIENTS.read().await.get(&client_id) { + client.sender.send(message.into().into_owned()).await.ok(); + } +} + +pub async fn send_packet(client_id: Uuid, packet: impl Borrow) { + let message = match bincode::serialize(packet.borrow()) { + Ok(message) => message, + Err(error) => { + warn!("failed to serialize packet for {}: {}", client_id, error); + return; + } + }; + send_message(client_id, message).await; +} #[tokio::main] async fn main() { let app = Router::new().route( "/", - get(|ws: WebSocketUpgrade| async { - ws.on_upgrade(|socket| async { - if let Err(e) = handle(socket).await { - eprintln!("Error: {}", e); - } - }) - }), + get(|ws: WebSocketUpgrade| async { ws.on_upgrade(handle_client) }), ); let listener = tokio::net::TcpListener::bind("0.0.0.0:80") .await @@ -24,6 +59,141 @@ async fn main() { axum::serve(listener, app).await.expect("failed to serve"); } -async fn handle(mut socket: WebSocket) -> anyhow::Result<()> { - Ok(()) +async fn handle_client(socket: WebSocket) { + let client_id = Uuid::now_v7(); + + let (mut sender, mut receiver) = socket.split(); + let (send_tx, mut send_rx) = channel(16); + tokio::spawn(async move { + while let Some(message) = send_rx.recv().await { + sender.send(Message::Binary(message)).await?; + } + Ok::<(), axum::Error>(()) + }); + + CLIENTS.write().await.insert( + client_id, + Client { + status: ClientStatus::Unauthenticated, + sender: send_tx, + }, + ); + + while let Some(Ok(message)) = receiver.next().await { + let Message::Binary(message) = message else { + continue; + }; + let Ok(packet) = bincode::deserialize::(&message) else { + warn!("failed to deserialize packet from {}", client_id); + continue; + }; + packet_received(client_id, packet).await; + } + + packet_received(client_id, ClientPacket::Disconnect).await; + CLIENTS.write().await.remove(&client_id); } + +async fn packet_received(client_id: Uuid, packet: ClientPacket) { + let client = &CLIENTS.read().await[&client_id]; + match client.status { + ClientStatus::Unauthenticated => handle_unauthenticated(client_id, packet).await, + ClientStatus::InLobby(lobby_id) => handle_in_lobby(client_id, lobby_id, packet).await, + ClientStatus::InGame(game_id) => handle_in_game(client_id, game_id, packet).await, + } +} + +async fn handle_unauthenticated(client_id: Uuid, packet: ClientPacket) { + match packet { + ClientPacket::CreateLobby { username, public } => { + let lobby_id = Uuid::now_v7(); + let lobby = Lobby { + public, + players: HashMap::from_iter([( + client_id, + LobbyPlayer { + username, + ready: false, + }, + )]), + }; + let mut lobbies = LOBBIES.write().await; + lobbies.insert(lobby_id, lobby); + CLIENTS + .write() + .await + .get_mut(&client_id) + .expect("client not found") + .status = ClientStatus::InLobby(lobby_id); + send_packet(client_id, ServerPacket::LobbyJoined(lobby_id)).await; + } + ClientPacket::JoinLobby { lobby_id, username } => { + let mut lobbies = LOBBIES.write().await; + + let (lobby_id, lobby) = match lobby_id { + Some(id) => { + let Some(lobby) = lobbies.get_mut(&id) else { + return send_packet( + client_id, + ServerPacket::Refused("lobby not found".to_string()), + ) + .await; + }; + (id, lobby) + } + None => { + let random_lobby = lobbies + .iter_mut() + .filter(|(_, lobby)| lobby.public) + .min_by_key(|(_, lobby)| lobby.players.len()); + match random_lobby { + Some((&id, lobby)) => (id, lobby), + None => { + let id = Uuid::now_v7(); + ( + id, + lobbies.entry(id).or_insert(Lobby { + public: true, + players: HashMap::new(), + }), + ) + } + } + } + }; + + lobby.players.insert( + client_id, + LobbyPlayer { + username, + ready: false, + }, + ); + + CLIENTS + .write() + .await + .get_mut(&client_id) + .expect("client not found") + .status = ClientStatus::InLobby(lobby_id); + send_packet(client_id, ServerPacket::LobbyJoined(lobby_id)).await; + + let message = bincode::serialize(&lobby).expect("failed to serialize lobby"); + for player_id in lobby.players.keys() { + send_message(*player_id, &message).await; + } + } + _ => (), + } +} + +async fn handle_in_lobby(client_id: Uuid, lobby_id: Uuid, packet: ClientPacket) { + match packet { + ClientPacket::Disconnect => todo!(), + ClientPacket::IAmReady => todo!(), + ClientPacket::IAmNotReady => todo!(), + _ => (), + } +} + +async fn handle_in_game(client_id: Uuid, game_id: Uuid, packet: ClientPacket) {}