diff --git a/Cargo.lock b/Cargo.lock index eeaf18c..84faaa0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -750,7 +750,9 @@ dependencies = [ "dashmap", "futures", "lazy_static", + "rand", "serde", + "slotmap", "tokio", "uuid", ] @@ -775,6 +777,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "slotmap" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" +dependencies = [ + "serde", + "version_check", +] + [[package]] name = "smallvec" version = "1.13.1" @@ -1007,6 +1019,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" dependencies = [ "getrandom", + "serde", ] [[package]] diff --git a/crates/server/Cargo.toml b/crates/server/Cargo.toml index 5bfc93a..147bbb2 100644 --- a/crates/server/Cargo.toml +++ b/crates/server/Cargo.toml @@ -13,9 +13,11 @@ bincode = "1.3.3" dashmap = "5.5.3" futures = "0.3.30" lazy_static = "1.4.0" +rand = "0.8.5" 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"] } +uuid = { version = "1.8.0", features = ["v4", "serde"] } # [lints] # workspace = true diff --git a/crates/server/src/game.rs b/crates/server/src/game.rs new file mode 100644 index 0000000..d2ad7e2 --- /dev/null +++ b/crates/server/src/game.rs @@ -0,0 +1,25 @@ +use dashmap::DashMap; +use lazy_static::lazy_static; +use server::SlotDashMap; +use slotmap::SlotMap; +use uuid::Uuid; + +server::new_key_type! { + struct GameId; +} + +lazy_static! { + static ref GAMES: SlotDashMap = SlotDashMap::new(); +} + +slotmap::new_key_type! { + struct PlayerId; +} + +pub struct Game { + players: SlotMap, +} + +pub struct Player { + connection_secret: Uuid, +} diff --git a/crates/server/src/game_save.rs b/crates/server/src/game_save.rs new file mode 100644 index 0000000..b798aad --- /dev/null +++ b/crates/server/src/game_save.rs @@ -0,0 +1,68 @@ +use std::collections::{HashSet, LinkedList}; +use std::time::Duration; + +use rand::Rng; +use serde::{Deserialize, Serialize}; +use slotmap::SlotMap; +use uuid::Uuid; + +use crate::{receive_message, send_message}; + +slotmap::new_key_type! { + struct ActionId; +} + +#[derive(Serialize, Deserialize)] +struct Action { + title: String, + description: String, + next_actions: SlotMap, + tile_position: Option<(i32, i32)>, + image_id: Uuid, +} + +struct Player { + connection: Option, + secret: Uuid, +} + +impl Player { + fn get_random_action(actions: &SlotMap) -> LinkedList { + if actions.len() == 0 { + return LinkedList::new(); + } + let random_index = rand::thread_rng().gen_range(0..actions.len()); + let action = actions.iter().nth(random_index).unwrap(); + let mut result = LinkedList::new(); + result.push_back(action.0); + result.append(&mut Self::get_random_action(&action.1.next_actions)); + result + } + + async fn make_action( + &mut self, + actions: &SlotMap, + timeout: Duration, + ) -> LinkedList { + let Some(connection) = self.connection else { + return Self::get_random_action(actions); + }; + + let Ok(encoded_message) = bincode::serialize(actions) else { + return Self::get_random_action(actions); + }; + + if !send_message(connection, encoded_message).await { + return Self::get_random_action(actions); + } + + // Get the client response + match tokio::time::timeout(timeout, receive_message(connection)).await { + Ok(Some(response)) => match bincode::deserialize(&response) { + Ok(response) => response, + Err(_) => Self::get_random_action(actions), + }, + _ => Self::get_random_action(actions), + } + } +} diff --git a/crates/server/src/lib.rs b/crates/server/src/lib.rs index 49b6143..f2c5429 100644 --- a/crates/server/src/lib.rs +++ b/crates/server/src/lib.rs @@ -1,26 +1,62 @@ -use std::collections::HashSet; +use std::marker::PhantomData; +use dashmap::mapref::entry::Entry; +use dashmap::DashMap; use uuid::Uuid; -struct Action { - title: String, - description: String, - tile_id: Uuid, - image: +pub trait UuidKey: From { + fn to_uuid(&self) -> Uuid; } -struct ActionType {} +pub struct SlotDashMap(DashMap, PhantomData); -struct Player {} +impl SlotDashMap { + pub fn new() -> Self { + Self(DashMap::new(), PhantomData) + } -impl Player { - async fn make_action(&mut self, actions: HashSet) -> Action { - todo!() + 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); + } } } -async fn border_wars_classic(players: HashSet) { - let player1 = players.iter().next().unwrap(); - - let action = player1.make_action(HashSet::new()).await; +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/login.rs b/crates/server/src/login.rs new file mode 100644 index 0000000..f18e4cf --- /dev/null +++ b/crates/server/src/login.rs @@ -0,0 +1,20 @@ +use anyhow::{bail, Context}; +use axum::extract::ws::{Message, WebSocket}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Serialize, Deserialize)] +pub enum LoginRequest { + Create { username: String }, +} + +pub async fn handle_client_login(socket: &mut 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::Create { username } => todo!(), + } +} diff --git a/crates/server/src/main save.rs b/crates/server/src/main save.rs new file mode 100644 index 0000000..163446d --- /dev/null +++ b/crates/server/src/main save.rs @@ -0,0 +1,127 @@ +use std::collections::LinkedList; + +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 futures::{SinkExt, StreamExt}; +use lazy_static::lazy_static; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; +use slotmap::SlotMap; +use tokio::sync::mpsc::{channel, Receiver, Sender}; +use uuid::Uuid; + +slotmap::new_key_type! { + struct PlayerId; +} + +enum WorldModification { + SetTile { r: i32, s: i32, image_id: Uuid }, +} + +struct Player { + connection_secret: Uuid, + unsent_modifications: LinkedList, +} + +pub struct Game { + players: SlotMap, +} + +trait Request: DeserializeOwned + Serialize { + type Response: Response; +} + +trait Response: DeserializeOwned + Serialize {} + +lazy_static! { + static ref CLIENTS: DashMap>> = DashMap::new(); + static ref GAMES: DashMap = DashMap::new(); +} + +#[derive(Serialize, Deserialize)] +pub enum LoginRequest { + Create { pseudo: String }, + JoinRandom { pseudo: String }, + Join { game_id: Uuid, pseudo: String }, + Rejoin { game_id: Uuid }, +} + +#[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(data) = socket + .recv() + .await + .context("client disconnected before login")?? + else { + bail!("expected login request"); + }; + + let login_request = bincode::deserialize(&data)?; + match login_request { + LoginRequest::Create { pseudo } => todo!(), + LoginRequest::JoinRandom { pseudo } => todo!(), + LoginRequest::Join { game_id, pseudo } => todo!(), + LoginRequest::Rejoin { game_id } => todo!(), + } + + let (mut writer, mut reader) = socket.split(); + + let (global_sender, mut receiver) = channel(128); + let client_id = loop { + let id = Uuid::new_v4(); + let Entry::Vacant(entry) = CLIENTS.entry(id) else { + continue; + }; + entry.insert(global_sender); + break id; + }; + + tokio::spawn(async move { + while let Some(message) = receiver.recv().await { + writer.send(Message::Binary(message)).await?; + } + Ok::<(), axum::Error>(()) + }); + + while let Some(Ok(message)) = reader.next().await { + let Message::Binary(data) = message else { + continue; + }; + + todo!() + } + + CLIENTS.remove(&client_id); + + Ok(()) +} + +async fn send_message(id: Uuid, message: Vec) -> bool { + if let Some(sender) = CLIENTS.get(&id) { + sender.send(message).await.is_ok() + } else { + false + } +} diff --git a/crates/server/src/main.rs b/crates/server/src/main.rs index 4cd6cd3..a131272 100644 --- a/crates/server/src/main.rs +++ b/crates/server/src/main.rs @@ -1,31 +1,22 @@ -use axum::extract::ws::{Message, WebSocket}; +use axum::extract::ws::WebSocket; use axum::extract::WebSocketUpgrade; use axum::routing::get; use axum::Router; -use dashmap::mapref::entry::Entry; -use dashmap::DashMap; -use futures::{SinkExt, StreamExt}; -use lazy_static::lazy_static; -use serde::de::DeserializeOwned; -use serde::Serialize; -use tokio::sync::mpsc::{channel, Sender}; -use uuid::Uuid; -trait Request: DeserializeOwned + Serialize { - type Response: Response; -} - -trait Response: DeserializeOwned + Serialize {} - -lazy_static! { - static ref CLIENTS: DashMap>> = DashMap::new(); -} +mod game; +mod login; #[tokio::main] async fn main() { let app = Router::new().route( "/", - get(|ws: WebSocketUpgrade| async { ws.on_upgrade(|socket| handle(socket)) }), + 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 @@ -33,46 +24,6 @@ async fn main() { axum::serve(listener, app).await.expect("failed to serve"); } -async fn handle(mut socket: WebSocket) { - let (mut writer, mut reader) = socket.split(); - - let (sender, mut receiver) = channel(128); - let client_id = loop { - let id = Uuid::new_v4(); - let Entry::Vacant(entry) = CLIENTS.entry(id) else { - continue; - }; - entry.insert(sender); - break id; - }; - - tokio::spawn(async move { - while let Some(message) = receiver.recv().await { - writer.send(Message::Binary(message)).await?; - } - Ok::<(), axum::Error>(()) - }); - - while let Some(Ok(message)) = reader.next().await { - let Message::Binary(data) = message else { - continue; - }; - if let Err(error) = message_received(client_id, data).await { - println!("Error: {}", error); - break; - }; - } - - CLIENTS.remove(&client_id); -} - -async fn send_message(id: Uuid, message: Vec) -> anyhow::Result<()> { - if let Some(sender) = CLIENTS.get(&id) { - sender.send(message).await?; - } - Ok(()) -} - -async fn message_received(id: Uuid, message: Vec) -> anyhow::Result<()> { +async fn handle(mut socket: WebSocket) -> anyhow::Result<()> { Ok(()) }