diff --git a/Cargo.lock b/Cargo.lock index 784630b..dd0dc70 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1289,6 +1289,26 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "thiserror" +version = "1.0.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f11c217e1416d6f036b870f14e0413d480dbf28edbee1f877abaf0206af43bb7" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "thread_local" version = "1.1.7" @@ -1542,6 +1562,7 @@ version = "0.1.0" dependencies = [ "rand", "rocket", + "thiserror", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index eaa353c..f60ac33 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,3 +8,4 @@ edition = "2021" [dependencies] rand = "0.8.5" rocket = { version = "0.5.0", features = ["secrets", "json"] } +thiserror = "1.0.51" diff --git a/src/common.rs b/src/common.rs new file mode 100644 index 0000000..9aa1457 --- /dev/null +++ b/src/common.rs @@ -0,0 +1,51 @@ +pub use crate::PlayerWrapper; +use crate::{ + game::{Game, Lobby, Player, Room}, + lobby, +}; +use std::{ + collections::HashMap, + marker::PhantomData, + sync::{Arc, Mutex, MutexGuard}, +}; + +macro_rules! make_event { + ($message:expr) => {{ + let __msg = $message; + Event::json(&__msg).event(__msg.name()) + }}; +} + +pub(crate) use make_event; + +pub struct GlobalState { + pub lobbys: Mutex>>>, + pub games: Mutex>>>, +} + +impl GlobalState { + pub fn new() -> Self { + Self { + lobbys: Mutex::new(HashMap::new()), + games: Mutex::new(HashMap::new()), + } + } +} + +pub struct Protected>(Arc>, PhantomData); + +impl> Protected { + pub fn new(content: ROOM) -> Self { + Self(Arc::new(Mutex::new(content)), PhantomData) + } + + pub fn lock(&self) -> MutexGuard { + self.0.lock().unwrap() + } +} + +impl> Clone for Protected { + fn clone(&self) -> Self { + Self(Arc::clone(&self.0), PhantomData) + } +} diff --git a/src/game.rs b/src/game.rs index e7d9bb8..98586c2 100644 --- a/src/game.rs +++ b/src/game.rs @@ -2,7 +2,7 @@ use rand::{ seq::{IteratorRandom, SliceRandom}, thread_rng, }; -use rocket::serde::Serialize; +use rocket::serde::{Deserialize, Serialize}; use std::{collections::HashMap, hash::Hash}; macro_rules! repeated_vec { @@ -21,7 +21,7 @@ macro_rules! repeated_vec { }; } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(crate = "rocket::serde")] #[serde(rename_all = "lowercase")] pub enum Team { @@ -29,11 +29,108 @@ pub enum Team { Moriarty, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(crate = "rocket::serde")] +#[serde(rename_all = "lowercase")] +pub enum Cable { + Safe, + Defusing, + Bomb, +} + pub trait Player { type ID: Eq + Hash + Clone + Copy; fn id(&self) -> Self::ID; +} + +pub trait Room { + fn name(&self) -> &str; + fn players(&self) -> &HashMap; + fn get_player(&self, id: PLAYER::ID) -> Option<&PLAYER>; + fn get_player_mut(&mut self, id: PLAYER::ID) -> Option<&mut PLAYER>; +} + +pub trait WaitingPlayer: Player { fn ready(&self) -> bool; +} + +pub struct Lobby { + name: String, + players: HashMap, +} + +impl Lobby { + pub fn new(name: String) -> Self { + Self { + name, + players: HashMap::new(), + } + } + + pub fn add_player(&mut self, player: PLAYER) -> Result<(), errors::Join> { + if self.players.len() >= 8 { + return Err(errors::Join::GameFull); + } + + if self.players.contains_key(&player.id()) { + return Err(errors::Join::AlreadyConnected); + } + self.players.insert(player.id(), player); + + Ok(()) + } + + pub fn remove_player(&mut self, id: PLAYER::ID) { + self.players.remove(&id); + } +} + +impl Room for Lobby { + fn name(&self) -> &str { + &self.name + } + + fn players(&self) -> &HashMap { + &self.players + } + + fn get_player(&self, id: PLAYER::ID) -> Option<&PLAYER> { + self.players.get(&id) + } + + fn get_player_mut(&mut self, id: PLAYER::ID) -> Option<&mut PLAYER> { + self.players.get_mut(&id) + } +} + +pub mod errors { + use thiserror::Error; + + #[derive(Error, Debug, Clone, Copy)] + pub enum Join { + #[error("this game is already full")] + GameFull, + #[error("you are already connected to this game")] + AlreadyConnected, + } +} + +// +// +// +// +// +// +// +// +// +// +// +// +// + +pub trait OldPlayer: WaitingPlayer { fn connected(&self) -> bool; fn team(&self) -> Team; @@ -44,22 +141,13 @@ pub trait Player { fn cables(&self) -> &[Cable]; } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] -#[serde(crate = "rocket::serde")] -#[serde(rename_all = "lowercase")] -pub enum Cable { - Safe, - Defusing, - Bomb, -} - pub enum CutOutcome { Win(Team), RoundEnd, Nothing, } -enum GameState { +enum GameState { Lobby, Ingame { wire_cutters: PLAYER::ID, @@ -68,57 +156,44 @@ enum GameState { }, } -pub struct Game { +pub struct Game { name: String, players: HashMap, state: GameState, } -impl Game { - pub fn new(name: String) -> Self { - Self { - name, - players: HashMap::new(), - state: GameState::Lobby, - } - } - - pub fn name(&self) -> &str { +impl Room for Game { + fn name(&self) -> &str { &self.name } - pub const fn wire_cutters(&self) -> Option { - if let GameState::Ingame { wire_cutters, .. } = self.state { - Some(wire_cutters) - } else { - None - } - } - - pub const fn players(&self) -> &HashMap { + fn players(&self) -> &HashMap { &self.players } - pub fn get_player(&self, id: PLAYER::ID) -> Option<&PLAYER> { + fn get_player(&self, id: PLAYER::ID) -> Option<&PLAYER> { self.players.get(&id) } - pub fn get_player_mut(&mut self, id: PLAYER::ID) -> Option<&mut PLAYER> { + fn get_player_mut(&mut self, id: PLAYER::ID) -> Option<&mut PLAYER> { self.players.get_mut(&id) } +} - pub fn add_player(&mut self, player: PLAYER) -> Result<(), errors::PlayerJoin> { - match self.state { - GameState::Lobby { .. } => { - if self.players.len() >= 8 { - return Err(errors::PlayerJoin::GameFull); - } - - self.players.entry(player.id()).or_insert(player); +impl Game { + pub fn new(name: String) -> Self { + Self { + name, + players: HashMap::new(), + state: GameState::Lobby, + } + } - Ok(()) - } - _ => Err(errors::PlayerJoin::GameAlreadyStarted), + pub const fn wire_cutters(&self) -> Option { + if let GameState::Ingame { wire_cutters, .. } = self.state { + Some(wire_cutters) + } else { + None } } @@ -147,12 +222,12 @@ impl Game { } } - pub fn start(&mut self) -> Result<(), errors::GameStart> { + pub fn start(&mut self) -> Result<(), old_errors::GameStart> { if self.players.len() < 4 { - return Err(errors::GameStart::NotEnoughPlayers); + return Err(old_errors::GameStart::NotEnoughPlayers); } - if !self.players.values().all(Player::ready) { - return Err(errors::GameStart::NotAllPlayersReady); + if !self.players.values().all(WaitingPlayer::ready) { + return Err(old_errors::GameStart::NotAllPlayersReady); } let mut teams = match self.players.len() { @@ -185,7 +260,7 @@ impl Game { &mut self, cutting: PLAYER::ID, cutted: PLAYER::ID, - ) -> Result<(Cable, CutOutcome), errors::Cut> { + ) -> Result<(Cable, CutOutcome), old_errors::Cut> { if let GameState::Ingame { wire_cutters, defusing_remaining, @@ -193,10 +268,10 @@ impl Game { } = &mut self.state { if cutting != *wire_cutters { - return Err(errors::Cut::DontHaveWireCutter); + return Err(old_errors::Cut::DontHaveWireCutter); } if cutted == cutting { - return Err(errors::Cut::CannotSelfCut); + return Err(old_errors::Cut::CannotSelfCut); } let cable = self.players.get_mut(&cutted).unwrap().cut_cable(); @@ -219,7 +294,7 @@ impl Game { Ok((cable, CutOutcome::Nothing)) } } else { - Err(errors::Cut::GameNotStarted) + Err(old_errors::Cut::GameNotStarted) } } @@ -244,12 +319,7 @@ impl Game { } } -pub mod errors { - pub enum PlayerJoin { - GameFull, - GameAlreadyStarted, - } - +pub mod old_errors { pub enum GameStart { NotEnoughPlayers, NotAllPlayersReady, diff --git a/src/lobby.rs b/src/lobby.rs new file mode 100644 index 0000000..7cf1f98 --- /dev/null +++ b/src/lobby.rs @@ -0,0 +1,272 @@ +use crate::{ + common::{make_event, GlobalState, Protected}, + game::{self, Lobby, Room, errors}, +}; +use rand::{ + distributions::{Alphanumeric, DistString}, + random, +}; +use rocket::{ + get, + http::{CookieJar, Status}, + request::{FromRequest, Outcome, Request}, + response::{ + stream::{Event, EventStream}, + Redirect, + }, + routes, + serde::Serialize, + tokio::{ + select, + sync::mpsc::{unbounded_channel, UnboundedSender}, + }, + uri, Shutdown, State, +}; +use std::time::Duration; + +#[derive(Debug, Clone, Serialize)] +#[serde(crate = "rocket::serde")] +pub struct Player { + id: ::ID, + name: String, + ready: bool, + #[serde(skip)] + sender: UnboundedSender, +} + +impl game::Player for Player { + type ID = u32; + + fn id(&self) -> Self::ID { + self.id + } +} + +impl game::WaitingPlayer for Player { + fn ready(&self) -> bool { + self.ready + } +} + +#[derive(Debug, Clone, Serialize)] +#[serde(crate = "rocket::serde")] +#[serde(untagged)] +enum Message { + Error { + reason: &'static str, + }, + Initialize { + lobby: String, + players: Vec, + }, + Join { + player: Player, + }, + Leave { + player: ::ID, + }, + SelfLeave, +} + +impl Message { + const fn name(&self) -> &'static str { + match self { + Self::Error { .. } => "error", + Self::Initialize { .. } => "init", + Self::Join { .. } => "join", + Self::Leave { .. } => "leave", + Self::SelfLeave { .. } => unreachable!(), + } + } +} + +impl Protected> { + #[allow(clippy::significant_drop_in_scrutinee)] + fn broadcast(&self, msg: &Message) { + for player in self.lock().players().values() { + player.sender.send(msg.clone()).unwrap(); + } + } +} + +#[rocket::async_trait] +impl<'r> FromRequest<'r> for Protected> { + type Error = (); + + async fn from_request(request: &'r Request<'_>) -> Outcome { + let Some(lobby) = request.cookies().get_private("lobby") else { + return Outcome::Error((Status::NotFound, ())); + }; + let lobbys = request + .guard::<&State>() + .await + .unwrap() + .lobbys + .lock() + .unwrap(); + + lobbys.get(lobby.value()).map_or_else( + || Outcome::Error((Status::NotFound, ())), + |x| Outcome::Success(Self::clone(x)), + ) + } +} + +struct ConnectionGuard { + lobby: Protected>, + id: ::ID, +} + +impl Drop for ConnectionGuard { + fn drop(&mut self) { + self.lobby.broadcast(&Message::Leave { player: self.id }); + self.lobby.lock().remove_player(self.id); + } +} + +#[get("/lobby/create?&")] +#[must_use] +fn create(id: Option, name: String, state: &State) -> Redirect { + let mut id = id.unwrap_or_else(|| Alphanumeric.sample_string(&mut rand::thread_rng(), 6)); + + { + let mut lobbys = state.lobbys.lock().unwrap(); + + while lobbys.contains_key(&id) { + id = Alphanumeric.sample_string(&mut rand::thread_rng(), 6); + } + id = id.to_uppercase(); + + lobbys.insert(id.clone(), Protected::new(Lobby::new(id.clone()))); + } + + Redirect::to(uri!(join(id, name))) +} + +#[get("/lobby/join?&")] +#[must_use] +fn join(lobby: &str, name: String, state: &State, jar: &CookieJar<'_>) -> Redirect { + let lobby_name = lobby.to_uppercase(); + + let lobbys = state.lobbys.lock().unwrap(); + let Some(lobby) = lobbys.get(&lobby_name).map(Protected::lock) else { + return Redirect::to("/gameMenu.html?error=Lobby%20not%20found"); + }; + + let mut id = random(); + while lobby.players().contains_key(&id) { + id = random(); + } + + jar.add_private(("lobby", lobby_name)); + jar.add_private(("id", id.to_string())); + jar.add_private(("name", name)); + + Redirect::to(uri!("/lobby.html")) +} + +#[get("/lobby/events")] +#[must_use] +fn events<'a>( + lobby: Option>>, + jar: &'a CookieJar<'_>, + mut end: Shutdown, +) -> EventStream![Event + 'a] { + EventStream! { + let Some(lobby) = lobby else { + yield make_event!(Message::Error { + reason: "You are not in a lobby" + }); + return; + }; + + let Some(Ok(id)) = jar.get_private("id").map(|x| x.value().parse::<::ID>()) else { + yield make_event!(Message::Error { + reason: "Invalid player id" + }); + return; + }; + + let Some(name) = jar.get_private("name").map(|x| x.value().to_owned()) else { + yield make_event!(Message::Error { + reason: "Invalid player name" + }); + return; + }; + + let (sender, mut receiver) = unbounded_channel(); + let player = Player { id, name, ready: false, sender }; + + let result = lobby.lock().add_player(player.clone()); + match result { + Ok(()) => (), + Err(errors::Join::GameFull) => { + yield make_event!(Message::Error { + reason: "This lobby is full" + }); + return; + } + Err(errors::Join::AlreadyConnected) => { + yield make_event!(Message::Error { + reason: "You are already connected to this game" + }); + return; + } + } + + let guard = ConnectionGuard { lobby: lobby.clone(), id }; + + let lobby_name = lobby.lock().name().to_owned(); + yield make_event!(Message::Initialize { + lobby: lobby_name, + players: lobby.lock().players().values().cloned().collect(), + }); + + lobby.broadcast(&Message::Join { player }); + + loop { + let Some(msg) = select! { + msg = receiver.recv() => msg, + () = &mut end => { + yield make_event!(Message::Error { + reason: "Server closed", + }); + return; + }, + } else { break; }; + if matches!(msg, Message::SelfLeave) { + break; + } + + yield make_event!(msg); + } + + drop(guard); + }.heartbeat(Duration::from_secs(5)) +} + +#[get("/lobby/leave")] +#[must_use] +#[allow(clippy::needless_pass_by_value)] +fn leave(lobby: Option>>, jar: &CookieJar<'_>) -> Redirect { + if let Some(Ok(id)) = jar + .get_private("id") + .map(|x| x.value().parse::<::ID>()) + { + if let Some(lobby) = lobby { + if let Some(player) = lobby.lock().get_player(id) { + player.sender.send(Message::SelfLeave).unwrap(); + } + } + }; + + jar.remove_private("lobby"); + jar.remove_private("id"); + jar.remove_private("name"); + + Redirect::to("/gameMenu.html") +} + +pub fn routes() -> Vec { + routes![create, join, events, leave] +} diff --git a/src/main.rs b/src/main.rs index 398d612..8013faf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,6 @@ #![allow(clippy::option_if_let_else, clippy::no_effect_underscore_binding)] -use rand::{ - distributions::{Alphanumeric, DistString}, - random, - seq::SliceRandom, - thread_rng, -}; +use rand::{seq::SliceRandom, thread_rng}; use rocket::{ fs::{relative, FileServer}, get, @@ -21,26 +16,26 @@ use rocket::{ serde::Serialize, tokio::{ select, - sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, + sync::mpsc::{UnboundedReceiver, UnboundedSender}, }, - uri, Shutdown, State, -}; -use std::{ - collections::HashMap, - sync::{Arc, Mutex, MutexGuard}, + Shutdown, State, }; +use std::sync::Mutex; +mod common; mod game; +mod lobby; -use game::{ - errors::{self, PlayerJoin}, - Cable, CutOutcome, Game, Player as _, Team, -}; +use common::{GlobalState, Protected}; +use game::{old_errors, Cable, CutOutcome, Game, OldPlayer as _, Room, Team}; + +// TODO: auto-delete created game if nobody joins +// TODO: delete game when it goes empty #[derive(Debug, Clone, Serialize)] #[serde(crate = "rocket::serde")] #[serde(untagged)] -enum Message { +pub enum Message { Error { reason: String, }, @@ -97,7 +92,7 @@ impl Message { #[derive(Debug, Clone, Serialize)] #[serde(crate = "rocket::serde")] -struct Player { +pub struct Player { id: ::ID, pub name: String, pub ready: bool, @@ -119,7 +114,7 @@ impl Player { } #[derive(Debug)] -struct PlayerWrapper { +pub struct PlayerWrapper { pub inner: Player, pub sender: UnboundedSender, // only present if no SSE stream is currently using it @@ -133,11 +128,15 @@ impl game::Player for PlayerWrapper { fn id(&self) -> Self::ID { self.inner.id } +} +impl game::WaitingPlayer for PlayerWrapper { fn ready(&self) -> bool { self.inner.ready } +} +impl game::OldPlayer for PlayerWrapper { fn connected(&self) -> bool { self.receiver.is_none() } @@ -166,38 +165,28 @@ impl game::Player for PlayerWrapper { } } -struct ProtectedGame(Arc>>); - -impl ProtectedGame { - pub fn new(name: String) -> Self { - Self(Arc::new(Mutex::new(Game::new(name)))) - } - - pub fn get(&self) -> MutexGuard> { - self.0.lock().unwrap() - } - +impl Protected> { pub fn broadcast(&self, msg: &Message) { - for player in self.get().players().values() { + for player in self.lock().players().values() { player.sender.send(msg.clone()).unwrap(); } } } -impl Clone for ProtectedGame { - fn clone(&self) -> Self { - Self(Arc::clone(&self.0)) +impl Protected> { + pub fn new_game(name: String) -> Self { + Self::new(Game::new(name)) } } #[rocket::async_trait] -impl<'r> FromRequest<'r> for ProtectedGame { +impl<'r> FromRequest<'r> for Protected> { type Error = (); async fn from_request(request: &'r Request<'_>) -> Outcome { if let Some(lobby) = request.cookies().get_private("lobby") { let games = request - .guard::<&State>() + .guard::<&State>() .await .unwrap() .games @@ -214,81 +203,6 @@ impl<'r> FromRequest<'r> for ProtectedGame { } } -struct Games { - pub games: Mutex>, -} - -impl Games { - pub fn new() -> Self { - Self { - games: Mutex::new(HashMap::new()), - } - } -} - -#[get("/create?&")] -#[must_use] -fn create(name: String, id: Option, games: &State) -> Redirect { - let mut id = id.unwrap_or_else(|| Alphanumeric.sample_string(&mut rand::thread_rng(), 6)); - - { - let mut games = games.games.lock().unwrap(); - - while games.contains_key(&id) { - id = Alphanumeric.sample_string(&mut rand::thread_rng(), 6); - } - id = id.to_uppercase(); - - games.insert(id.clone(), ProtectedGame::new(id.clone())); - } - - Redirect::to(uri!(join(id, name))) -} - -// TODO: if a player join but never call /events, he will be in the game even though he is not really here -#[get("/join?&")] -#[must_use] -fn join(lobby: &str, name: String, games: &State, jar: &CookieJar<'_>) -> Redirect { - let lobby = lobby.to_uppercase(); - - if !games.games.lock().unwrap().contains_key(&lobby) { - return Redirect::to("/gameMenu.html?error=Lobby%20not%20found"); - } - - let mut id = random(); - while games.games.lock().unwrap()[&lobby] - .get() - .players() - .contains_key(&id) - { - id = random(); - } - - let (sender, receiver) = unbounded_channel(); - let player = PlayerWrapper { - inner: Player::new(id, name), - sender, - receiver: Some(Mutex::new(receiver)), - team: None, - }; - - let result = games.games.lock().unwrap()[&lobby].get().add_player(player); - match result { - Ok(()) => (), - Err(PlayerJoin::GameFull) => { - return Redirect::to("/gameMenu.html?error=The%20game%20is%20full") - } - Err(PlayerJoin::GameAlreadyStarted) => { - return Redirect::to("/gameMenu.html?error=This%20game%20already%20started") - } - }; - - jar.add_private(("lobby", lobby)); - jar.add_private(("userid", id.to_string())); - - Redirect::to(uri!("/lobby.html")) -} - fn get_playerid(jar: &CookieJar<'_>) -> Result<::ID, Status> { if let Some(id) = jar.get_private("userid") { id.value().parse().map_err(|_| Status::BadRequest) @@ -298,7 +212,7 @@ fn get_playerid(jar: &CookieJar<'_>) -> Result<:: } struct ConnectionGuard { - game: ProtectedGame, + game: Protected>, playerid: ::ID, receiver: Option>, } @@ -309,14 +223,14 @@ impl Drop for ConnectionGuard { player: self.playerid, }); self.game - .get() + .lock() .get_player_mut(self.playerid) .unwrap() .receiver .replace(Mutex::new(self.receiver.take().unwrap())); - if !self.game.get().started() { - self.game.get().remove_player(self.playerid); + if !self.game.lock().started() { + self.game.lock().remove_player(self.playerid); } } } @@ -325,7 +239,7 @@ impl Drop for ConnectionGuard { #[get("/events")] #[must_use] fn events<'a>( - game: ProtectedGame, + game: Protected>, jar: &'a CookieJar<'a>, mut end: Shutdown, ) -> EventStream![Event + 'a] { @@ -347,14 +261,14 @@ fn events<'a>( } }; - if game.get().get_player(playerid).is_none() { + if game.lock().get_player(playerid).is_none() { yield make_event!(Message::Error { reason: "You are not part of this game".to_owned(), }); return; }; - let player = game.get().get_player(playerid).unwrap().inner.clone(); - let Some(receiver) = game.get().get_player_mut(playerid).unwrap().receiver.take() else { + let player = game.lock().get_player(playerid).unwrap().inner.clone(); + let Some(receiver) = game.lock().get_player_mut(playerid).unwrap().receiver.take() else { yield make_event!(Message::Error { reason: "You are already connected to this game".to_owned(), }); @@ -363,10 +277,10 @@ fn events<'a>( let receiver = receiver.into_inner().unwrap(); game.broadcast(&Message::Join { player: player.clone() }); - let lobby_name = game.get().name().to_owned(); - let player_list = game.get().players().values().map(|p| p.inner.clone()).collect(); - let team = game.get().get_player(playerid).unwrap().team; - let wire_cutters = game.get().wire_cutters(); + let lobby_name = game.lock().name().to_owned(); + let player_list = game.lock().players().values().map(|p| p.inner.clone()).collect(); + let team = game.lock().get_player(playerid).unwrap().team; + let wire_cutters = game.lock().wire_cutters(); yield make_event!(Message::Initialize { lobby: lobby_name, players: player_list, team, wire_cutters }); let mut guard = ConnectionGuard { @@ -380,7 +294,12 @@ fn events<'a>( loop { let msg = select! { msg = receiver.recv() => msg, - _ = &mut end => break, + _ = &mut end => { + yield make_event!(Message::Error { + reason: "Server closed".to_owned(), + }); + break + }, }; let Some(msg) = msg else { break; @@ -400,12 +319,12 @@ fn events<'a>( #[get("/leave")] #[must_use] -fn leave(game: ProtectedGame, jar: &CookieJar<'_>) -> Redirect { +fn leave(game: Protected>, jar: &CookieJar<'_>) -> Redirect { let Ok(playerid) = get_playerid(jar) else { return Redirect::to("/gameMenu.html"); }; - if let Some(player) = game.get().get_player(playerid) { + if let Some(player) = game.lock().get_player(playerid) { player.sender.send(Message::SelfLeave).unwrap(); } @@ -416,13 +335,13 @@ fn leave(game: ProtectedGame, jar: &CookieJar<'_>) -> Redirect { } #[get("/ready?")] -fn ready(state: bool, game: ProtectedGame, jar: &CookieJar<'_>) { +fn ready(state: bool, game: Protected>, jar: &CookieJar<'_>) { let Ok(playerid) = get_playerid(jar) else { return; }; - if game.get().get_player(playerid).is_some() { - game.get().get_player_mut(playerid).unwrap().inner.ready = state; + if game.lock().get_player(playerid).is_some() { + game.lock().get_player_mut(playerid).unwrap().inner.ready = state; game.broadcast(&Message::Ready { player: playerid, state, @@ -431,12 +350,12 @@ fn ready(state: bool, game: ProtectedGame, jar: &CookieJar<'_>) { } #[get("/start")] -fn start(game: ProtectedGame) -> Status { - if game.get().start().is_err() { +fn start(game: Protected>) -> Status { + if game.lock().start().is_err() { return Status::PreconditionRequired; } - for player in game.get().players().values() { + for player in game.lock().players().values() { player .sender .send(Message::Start { @@ -452,31 +371,33 @@ fn start(game: ProtectedGame) -> Status { #[get("/cut?")] fn cut( player: ::ID, - game: ProtectedGame, - games: &State, + game: Protected>, + games: &State, jar: &CookieJar<'_>, ) -> Result<(), BadRequest<&'static str>> { let Ok(playerid) = get_playerid(jar) else { return Err(BadRequest("Invalid player id")); }; - if game.get().get_player(playerid).is_none() { + if game.lock().get_player(playerid).is_none() { return Err(BadRequest("You are not part of this game")); }; - if game.get().get_player(player).is_none() { + if game.lock().get_player(player).is_none() { return Err(BadRequest( "The player you specified is not part of this game", )); }; - let (cable, outcome) = match game.get().cut(playerid, player) { + let (cable, outcome) = match game.lock().cut(playerid, player) { Ok(x) => x, - Err(errors::Cut::GameNotStarted) => return Err(BadRequest("This game hasn't started yet")), - Err(errors::Cut::DontHaveWireCutter) => { + Err(old_errors::Cut::GameNotStarted) => { + return Err(BadRequest("This game hasn't started yet")) + } + Err(old_errors::Cut::DontHaveWireCutter) => { return Err(BadRequest("You don't have the wire cutter")) } - Err(errors::Cut::CannotSelfCut) => { + Err(old_errors::Cut::CannotSelfCut) => { return Err(BadRequest("You can't cut one of your own cables")) } }; @@ -487,7 +408,7 @@ fn cut( CutOutcome::Nothing => (), CutOutcome::Win(team) => game_won(games, &game, team, jar), CutOutcome::RoundEnd => { - if game.get().next_round() { + if game.lock().next_round() { game_won(games, &game, Team::Moriarty, jar); } else { send_round(&game); @@ -498,9 +419,9 @@ fn cut( Ok(()) } -fn send_round(game: &ProtectedGame) { - let wire_cutters = game.get().wire_cutters(); - for player in game.get().players().values() { +fn send_round(game: &Protected>) { + let wire_cutters = game.lock().wire_cutters(); + for player in game.lock().players().values() { player .sender .send(Message::RoundStart { @@ -511,9 +432,14 @@ fn send_round(game: &ProtectedGame) { } } -fn game_won(games: &State, game: &ProtectedGame, team: Team, jar: &CookieJar<'_>) { +fn game_won( + games: &State, + game: &Protected>, + team: Team, + jar: &CookieJar<'_>, +) { let winning_players = game - .get() + .lock() .players() .values() .filter(|p| p.team() == team) @@ -524,7 +450,7 @@ fn game_won(games: &State, game: &ProtectedGame, team: Team, jar: &Cookie players: winning_players, }); - let lobby = &game.get().name().to_owned(); + let lobby = &game.lock().name().to_owned(); games.games.lock().unwrap().remove(lobby); jar.remove_private("lobby"); @@ -539,10 +465,8 @@ fn index() -> Redirect { #[launch] fn rocket() -> _ { rocket::build() - .manage(Games::new()) + .manage(GlobalState::new()) .mount("/", FileServer::from(relative!("static"))) - .mount( - "/", - routes![index, create, join, events, leave, ready, start, cut], - ) + .mount("/", routes![index, events, leave, ready, start, cut]) + .mount("/", lobby::routes()) } diff --git a/static b/static index a1f7d0e..9f09741 160000 --- a/static +++ b/static @@ -1 +1 @@ -Subproject commit a1f7d0e946af79df0e9dc7faf6f95ddfd9a7b6db +Subproject commit 9f09741300c60809ff70e0fe6609922b46e6fb77