Skip to content

Commit

Permalink
Added endpoint to start a game from an existing lobby. Closes #9
Browse files Browse the repository at this point in the history
  • Loading branch information
myOmikron committed Mar 29, 2023
1 parent 8d4ed59 commit 904f8e3
Show file tree
Hide file tree
Showing 6 changed files with 197 additions and 36 deletions.
12 changes: 8 additions & 4 deletions src/chan/ws_manager_chan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<RawValue>,
/// 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.
///
Expand Down
19 changes: 13 additions & 6 deletions src/server/handler/chats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -179,6 +179,7 @@ pub async fn get_chat(
pub struct GetAllChatsResponse {
friend_chat_rooms: Vec<Uuid>,
lobby_chat_rooms: Vec<Uuid>,
game_chat_rooms: Vec<Uuid>,
}

/// Retrieve all chats the executing user has access to.
Expand All @@ -203,23 +204,29 @@ 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())
))
.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(),
}))
}
174 changes: 170 additions & 4 deletions src/server/handler/lobbies.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand Down Expand Up @@ -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<PathUuid>,
db: Data<Database>,
session: Session,
ws_manager_chan: Data<WsManagerChan>,
) -> ApiResult<Json<StartGameResponse>> {
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<Uuid> = 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::<Vec<_>>(),
)
.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,
}))
}
20 changes: 0 additions & 20 deletions src/server/handler/websocket.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
6 changes: 4 additions & 2 deletions src/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)?
Expand Down
2 changes: 2 additions & 0 deletions src/server/swagger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -78,6 +79,7 @@ impl Modify for CookieSecurity {
handler::GetGameOverviewResponse,
handler::GameUploadResponse,
handler::GameUploadRequest,
handler::StartGameResponse,
)),
modifiers(&CookieSecurity)
)]
Expand Down

0 comments on commit 904f8e3

Please sign in to comment.