Avancement relatif
Some checks are pending
Rust Checks / checks (push) Waiting to run

This commit is contained in:
Tipragot 2024-04-12 04:36:23 +02:00
parent 01924cdcb8
commit 27e0aad29d
16 changed files with 932 additions and 67 deletions

120
Cargo.lock generated
View file

@ -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",
]

View file

@ -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

View file

@ -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<RefMut<'a, K, V>> for GlobalRefMut<'a, K, V> {
fn from(v: RefMut<'a, K, V>) -> Self {
Self::Single(v)
}
}
impl<'a, K: Eq + Hash, V> From<RefMutMulti<'a, K, V>> 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(),
}
}
}

View file

@ -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<Vec<u8>>);
impl ConnectionSender {
pub async fn send<T: Serialize>(&mut self, message: T) -> anyhow::Result<()> {
Ok(self.0.send(bincode::serialize(&message)?).await?)
}
}
pub struct ConnectionReader(SplitStream<WebSocket>);
impl ConnectionReader {
pub async fn read<T: DeserializeOwned>(&mut self) -> anyhow::Result<T> {
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<Uuid, Lobby> = DashMap::new();
}
#[derive(Serialize, Deserialize)]
enum LoginRequest {
CreateLobby {
username: String,
public: bool,
},
JoinLobby {
lobby_id: Option<Uuid>,
username: String,
},
}
slotmap::new_key_type! {
struct ConnectionId;}
struct Lobby {
id: Uuid,
public: bool,
connections: SlotMap<ConnectionId, LobbyConnection>,
}
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::<IAmReady>().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<GlobalRefMut<'static, Uuid, Lobby>> {
LOBBIES
.iter_mut()
.filter(|lobby| lobby.public)
.min_by_key(|lobby| lobby.connections.len())
.map(GlobalRefMut::from)
}

View file

@ -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<T> {
Refused(String),
Success(T),
}
#[derive(Serialize, Deserialize)]
pub struct CreateResponse {
game_id: GameId,
player_id: PlayerId,
secret: PlayerSecret,
options: Vec<GameSettingField>,
}
#[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<String> },
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<PlayerId, PlayerProfile>,
}
#[derive(Serialize, Deserialize)]
pub struct JoinResponse {
player_id: PlayerId,
secret: PlayerSecret,
players: HashMap<PlayerId, PlayerProfile>,
}
#[derive(Serialize, Deserialize)]
pub struct RejoinResponse {
players: HashMap<PlayerId, PlayerProfile>,
}

View file

@ -0,0 +1,62 @@
use std::marker::PhantomData;
use dashmap::mapref::entry::Entry;
use dashmap::DashMap;
use uuid::Uuid;
pub trait UuidKey: From<Uuid> {
fn to_uuid(&self) -> Uuid;
}
pub struct SlotDashMap<K: UuidKey, V>(DashMap<Uuid, V>, PhantomData<K>);
impl<K: UuidKey, V> SlotDashMap<K, V> {
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<K: UuidKey, V> Default for SlotDashMap<K, V> {
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)*);
};
() => {}
}

View file

@ -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<PlayerId, LobbyPlayer>,
}
#[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<Uuid>,
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;

View file

@ -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<LobbyId, Lobby> = DashMap::new();
// static ref GAMES: DashMap<GameId, Game> = 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(())
}

View file

@ -0,0 +1,73 @@
use std::marker::PhantomData;
use dashmap::mapref::entry::Entry;
use dashmap::DashMap;
use uuid::Uuid;
pub trait UuidKey: From<Uuid> + 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<K: UuidKey, V>(DashMap<Uuid, V>, PhantomData<K>);
impl<K: UuidKey, V> IdDashMap<K, V> {
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<K: UuidKey, V> Default for IdDashMap<K, V> {
fn default() -> Self {
Self::new()
}
}

View file

@ -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<Uuid> {
fn to_uuid(&self) -> Uuid;
#[derive(Serialize, Deserialize)]
pub enum ClientPacket {
Disconnect,
CreateLobby {
username: String,
public: bool,
},
JoinLobby {
lobby_id: Option<Uuid>,
username: String,
},
IAmReady,
IAmNotReady,
}
pub struct SlotDashMap<K: UuidKey, V>(DashMap<Uuid, V>, PhantomData<K>);
impl<K: UuidKey, V> SlotDashMap<K, V> {
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<K: UuidKey, V> Default for SlotDashMap<K, V> {
fn default() -> Self {
Self::new()
}
#[derive(Serialize, Deserialize)]
pub struct Lobby {
pub public: bool,
pub players: HashMap<Uuid, LobbyPlayer>,
}
#[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,
}

View file

@ -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<Uuid, LobbyStatus> = DashMap::new();
}
pub(crate) async fn create_lobby(
client_id: Uuid,
username: String,
public: bool,
) -> anyhow::Result<()> {
todo!()
}

View file

@ -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<Vec<u8>>,
}
enum ClientStatus {
Unauthenticated,
InLobby(Uuid),
InGame(Uuid),
}
lazy_static! {
static ref CLIENTS: RwLock<HashMap<Uuid, Client>> = RwLock::new(HashMap::new());
static ref LOBBIES: RwLock<HashMap<Uuid, Lobby>> = RwLock::new(HashMap::new());
}
pub async fn send_message<'a>(client_id: Uuid, message: impl Into<Cow<'a, [u8]>>) {
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<ServerPacket>) {
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::<ClientPacket>(&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) {}