From 904f8e3716a1169f5d437dc4fe66b40537c9f5f2 Mon Sep 17 00:00:00 2001 From: myOmikron Date: Wed, 29 Mar 2023 02:26:06 +0200 Subject: [PATCH] Added endpoint to start a game from an existing lobby. Closes #9 --- src/chan/ws_manager_chan.rs | 12 ++- src/server/handler/chats.rs | 19 ++-- src/server/handler/lobbies.rs | 174 +++++++++++++++++++++++++++++++- src/server/handler/websocket.rs | 20 ---- src/server/mod.rs | 6 +- src/server/swagger.rs | 2 + 6 files changed, 197 insertions(+), 36 deletions(-) diff --git a/src/chan/ws_manager_chan.rs b/src/chan/ws_manager_chan.rs index 8158d60..72aab50 100644 --- a/src/chan/ws_manager_chan.rs +++ b/src/chan/ws_manager_chan.rs @@ -56,12 +56,16 @@ pub enum WsMessage { /// This can occur, if the server can not deserialize the message, the message has a wrong /// type or a message, that should only be sent from the server, is received InvalidMessage, - /// This variant is sent from the client that has finished its turn - FinishedTurn { + /// The notification for the clients that a new game has started + GameStarted { /// Identifier of the game game_uuid: Uuid, - /// Data of the game - game_data: Box, + /// Chatroom for the game + game_chat_uuid: Uuid, + /// The lobby the game originated from + lobby_uuid: Uuid, + /// The lobby chatroom the game chat room originated from + lobby_chat_uuid: Uuid, }, /// An update of the game data. /// diff --git a/src/server/handler/chats.rs b/src/server/handler/chats.rs index 8689b7d..0a498e7 100644 --- a/src/server/handler/chats.rs +++ b/src/server/handler/chats.rs @@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize}; use utoipa::ToSchema; use uuid::Uuid; -use crate::models::{ChatRoom, ChatRoomMember, ChatRoomMessage, Friend, LobbyAccount}; +use crate::models::{ChatRoom, ChatRoomMember, ChatRoomMessage, Friend, GameAccount, LobbyAccount}; use crate::server::handler::{AccountResponse, ApiError, ApiResult, PathUuid}; /// The message of a chatroom @@ -63,7 +63,7 @@ pub struct GetChatResponse { /// Retrieve the messages of a chatroom /// /// `messages` should be sorted by the datetime of `message.created_at`. -/// `message.id` should be used to uniquely identify chat messages. +/// `message.uuid` should be used to uniquely identify chat messages. /// This is needed as new messages are delivered via websocket /// /// `members` holds information about all members that are currently in the chat room (including @@ -179,6 +179,7 @@ pub async fn get_chat( pub struct GetAllChatsResponse { friend_chat_rooms: Vec, lobby_chat_rooms: Vec, + game_chat_rooms: Vec, } /// Retrieve all chats the executing user has access to. @@ -203,7 +204,7 @@ pub async fn get_all_chats( let mut tx = db.start_transaction().await?; - let friend_chat_room_ids = query!(&mut tx, (Friend::F.chat_room.uuid,)) + let friend_chat_room_uuids = query!(&mut tx, (Friend::F.chat_room.uuid,)) .condition(and!( Friend::F.is_request.equals(false), Friend::F.from.uuid.equals(uuid.as_ref()) @@ -211,15 +212,21 @@ pub async fn get_all_chats( .all() .await?; - let lobby_chat_room_ids = query!(&mut tx, (LobbyAccount::F.lobby.chat_room.uuid)) + let lobby_chat_room_uuids = query!(&mut tx, (LobbyAccount::F.lobby.chat_room.uuid)) .condition(LobbyAccount::F.player.uuid.equals(uuid.as_ref())) .all() .await?; + let game_chat_room_uuids = query!(&mut tx, (GameAccount::F.game.chat_room.uuid,)) + .condition(GameAccount::F.uuid.equals(uuid.as_ref())) + .all() + .await?; + tx.commit().await?; Ok(Json(GetAllChatsResponse { - lobby_chat_rooms: lobby_chat_room_ids.into_iter().map(|(x,)| x).collect(), - friend_chat_rooms: friend_chat_room_ids.into_iter().map(|(x,)| x).collect(), + lobby_chat_rooms: lobby_chat_room_uuids.into_iter().map(|x| x.0).collect(), + friend_chat_rooms: friend_chat_room_uuids.into_iter().map(|x| x.0).collect(), + game_chat_rooms: game_chat_room_uuids.into_iter().map(|x| x.0).collect(), })) } diff --git a/src/server/handler/lobbies.rs b/src/server/handler/lobbies.rs index a855a8f..01c92b1 100644 --- a/src/server/handler/lobbies.rs +++ b/src/server/handler/lobbies.rs @@ -1,20 +1,23 @@ use actix_toolbox::tb_middleware::Session; -use actix_web::web::{Data, Json}; +use actix_web::web::{Data, Json, Path}; use actix_web::{get, post}; use argon2::password_hash::SaltString; use argon2::{Argon2, PasswordHasher}; use chrono::{DateTime, Utc}; +use log::error; use rand::thread_rng; use rorm::fields::{BackRef, ForeignModelByField}; -use rorm::{insert, query, Database, Model}; +use rorm::{delete, insert, query, update, Database, Model}; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; use uuid::Uuid; +use crate::chan::{WsManagerChan, WsManagerMessage, WsMessage}; use crate::models::{ - Account, ChatRoomInsert, ChatRoomMemberInsert, Lobby, LobbyAccount, LobbyInsert, + Account, ChatRoomInsert, ChatRoomMember, ChatRoomMemberInsert, ChatRoomMessage, + GameAccountInsert, GameInsert, Lobby, LobbyAccount, LobbyInsert, }; -use crate::server::handler::{AccountResponse, ApiError, ApiResult}; +use crate::server::handler::{AccountResponse, ApiError, ApiResult, PathUuid}; /// A single lobby #[derive(Serialize, ToSchema)] @@ -266,3 +269,166 @@ pub async fn create_lobby( lobby_chat_room_uuid: chat_room_uuid, })) } + +/// The response when starting a game +#[derive(Serialize, ToSchema)] +pub struct StartGameResponse { + game_uuid: Uuid, + game_chat_uuid: Uuid, +} + +/// Start a game from an existing lobby. +/// +/// The executing user must be the owner of the lobby. +/// +/// The lobby is deleted in the process, a new chatroom is created and all messages from the +/// lobby chatroom are attached to the game chatroom. +/// +/// This will invoke a [WsMessage::GameStarted] message that is sent via websocket to all +/// members of the lobby to inform them which lobby was started. It also contains the the new and +/// old chatroom uuids to make mapping for the clients easier. +/// +/// After the game started, the lobby owner must use the `PUT /api/v2/games/{uuid}` endpoint to +/// upload the initial game state. +/// +/// **Note**: +/// This behaviour is subject to change. +/// The server should be set the order in which players are allowed to make their turns. +/// This allows the server to detect malicious players trying to update the game state before +/// its their turn. +#[utoipa::path( + tag = "Lobbies", + context_path = "/api/v2", + responses( + (status = 200, description = "Lobby got created", body = StartGameResponse), + (status = 400, description = "Client error", body = ApiErrorResponse), + (status = 500, description = "Server error", body = ApiErrorResponse), + ), + params(PathUuid), + security(("session_cookie" = [])) +)] +#[post("/lobbies/{uuid}/start")] +pub async fn start_game( + path: Path, + db: Data, + session: Session, + ws_manager_chan: Data, +) -> ApiResult> { + let uuid: Uuid = session.get("uuid")?.ok_or(ApiError::SessionCorrupt)?; + + let mut tx = db.start_transaction().await?; + + let mut lobby = query!(&mut tx, Lobby) + .condition(Lobby::F.uuid.equals(path.uuid.as_ref())) + .optional() + .await? + .ok_or(ApiError::InvalidUuid)?; + + Lobby::F + .current_player + .populate(&mut tx, &mut lobby) + .await?; + + // Check if the executing user owns the lobby + if *lobby.owner.key() != uuid { + return Err(ApiError::MissingPrivileges); + } + + // Create chatroom for the game + let game_chat_uuid = insert!(&mut tx, ChatRoomInsert) + .return_primary_key() + .single(&ChatRoomInsert { + uuid: Uuid::new_v4(), + }) + .await?; + + // Move messages from lobby chat to game chat + update!(&mut tx, ChatRoomMessage) + .condition( + ChatRoomMessage::F + .chat_room + .equals(lobby.chat_room.key().as_ref()), + ) + .set(ChatRoomMessage::F.chat_room, game_chat_uuid.as_ref()) + .exec() + .await?; + + // Move chatroom member to new chatroom + update!(&mut tx, ChatRoomMember) + .condition( + ChatRoomMember::F + .chat_room + .equals(lobby.chat_room.key().as_ref()), + ) + .set(ChatRoomMember::F.chat_room, game_chat_uuid.as_ref()) + .exec() + .await?; + + // Create new game and attach lobby chat + let game_uuid = insert!(&mut tx, GameInsert) + .return_primary_key() + .single(&GameInsert { + uuid: Uuid::new_v4(), + chat_room: ForeignModelByField::Key(game_chat_uuid), + max_players: lobby.max_player, + name: lobby.name, + updated_by: ForeignModelByField::Key(uuid), + }) + .await?; + + // Retrieve players from lobby + let player: Vec = if let Some(lobby_player) = lobby.current_player.cached { + lobby_player + .into_iter() + .map(|x: LobbyAccount| *x.player.key()) + .collect() + } else { + error!("Cache of populated field current_player was empty"); + return Err(ApiError::InternalServerError); + }; + + // Attach all players from lobby to game + insert!(&mut tx, GameAccountInsert) + .return_nothing() + .bulk( + &player + .iter() + .map(|x| GameAccountInsert { + uuid: Uuid::new_v4(), + game: ForeignModelByField::Key(game_uuid), + player: ForeignModelByField::Key(*x), + }) + .collect::>(), + ) + .await?; + + // Delete lobby + delete!(&mut tx, Lobby) + .condition(Lobby::F.uuid.equals(uuid.as_ref())) + .await?; + + tx.commit().await?; + + // Send notifications to all players + let msg = WsMessage::GameStarted { + game_uuid, + game_chat_uuid, + lobby_uuid: lobby.uuid, + lobby_chat_uuid: *lobby.chat_room.key(), + }; + + for p in player { + if let Err(err) = ws_manager_chan + .send(WsManagerMessage::SendMessage(p, msg.clone())) + .await + { + error!("Could not send to ws manager chan: {err}"); + return Err(ApiError::InternalServerError); + } + } + + Ok(Json(StartGameResponse { + game_uuid, + game_chat_uuid, + })) +} diff --git a/src/server/handler/websocket.rs b/src/server/handler/websocket.rs index 4e6aa85..920124f 100644 --- a/src/server/handler/websocket.rs +++ b/src/server/handler/websocket.rs @@ -79,26 +79,6 @@ pub async fn websocket( while let Some(res) = rx.recv().await { match res { Ok(msg) => match msg { - Message::Text(data) => { - let message: WsMessage = match serde_json::from_str(&String::from(data)) { - Ok(v) => v, - Err(err) => { - debug!("Could not deserialize message: {err}"); - invalid_msg!(rx_tx); - continue; - } - }; - - match message { - WsMessage::FinishedTurn { - game_uuid, - game_data, - } => { - debug!("Received Finished turn: {game_uuid}: {game_data}"); - } - _ => invalid_msg!(rx_tx), - } - } Message::Ping(req) => send_to_ws!(rx_tx, Message::Pong(req)), Message::Pong(_) => { let mut r = last_hb.lock().await; diff --git a/src/server/mod.rs b/src/server/mod.rs index ebe23ab..8ea8041 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -25,7 +25,8 @@ use crate::server::handler::{ accept_friend_request, create_friend_request, create_invite, create_lobby, delete_friend, delete_me, get_all_chats, get_chat, get_friends, get_game, get_invites, get_lobbies, get_me, get_open_games, health, login, logout, lookup_account_by_username, lookup_account_by_uuid, - push_game_update, register_account, set_password, update_me, version, websocket, welcome_page, + push_game_update, register_account, set_password, start_game, update_me, version, websocket, + welcome_page, }; use crate::server::middleware::{ handle_not_found, json_extractor_error, AuthenticationRequired, TokenRequired, @@ -132,7 +133,8 @@ pub async fn start_server( .service(get_invites) .service(get_game) .service(get_open_games) - .service(push_game_update), + .service(push_game_update) + .service(start_game), ) }) .bind(s_addr)? diff --git a/src/server/swagger.rs b/src/server/swagger.rs index 86c70bf..db0d69a 100644 --- a/src/server/swagger.rs +++ b/src/server/swagger.rs @@ -46,6 +46,7 @@ impl Modify for CookieSecurity { handler::get_open_games, handler::get_game, handler::push_game_update, + handler::start_game, ), components(schemas( handler::AccountRegistrationRequest, @@ -78,6 +79,7 @@ impl Modify for CookieSecurity { handler::GetGameOverviewResponse, handler::GameUploadResponse, handler::GameUploadRequest, + handler::StartGameResponse, )), modifiers(&CookieSecurity) )]