use std::net::SocketAddr;
use anyhow::{Context, Result};
use de_lobby_model::{
Game, GameConfig, GameListing, GameMap, GamePartial, GamePlayer, GamePlayerInfo, GameSetup,
MAP_HASH_LEN, MAX_GAME_NAME_LEN, MAX_MAP_NAME_LEN, MAX_USERNAME_LEN,
};
use futures_util::TryStreamExt;
use log::info;
use sqlx::{query, sqlite::SqliteRow, Pool, Row, Sqlite, SqliteExecutor};
use thiserror::Error;
use crate::{
db::{FromRow, SQLITE_CONSTRAINT_FOREIGNKEY, SQLITE_CONSTRAINT_PRIMARYKEY},
db_error_code, db_error_message,
};
const SERVER_LEN: usize = 45;
#[derive(Clone)]
pub(super) struct Games {
pool: &'static Pool<Sqlite>,
}
impl Games {
pub(super) async fn init(pool: &'static Pool<Sqlite>) -> Result<Self> {
let init_query = format!(
include_str!("init.sql"),
username_len = MAX_USERNAME_LEN,
game_name_len = MAX_GAME_NAME_LEN,
map_name_len = MAX_MAP_NAME_LEN,
map_hash_len = MAP_HASH_LEN,
server_len = SERVER_LEN,
);
info!("Initializing games...");
query(&init_query)
.execute(pool)
.await
.context("DB initialization failed")?;
Ok(Self { pool })
}
pub(super) async fn list(&self) -> Result<GameListing> {
let mut rows = query(
"SELECT games.*, count(players.ordinal) as num_players \
FROM games \
LEFT JOIN players ON (games.name = players.game) \
GROUP BY games.name;",
)
.fetch(self.pool);
let mut games = GameListing::empty();
while let Some(row) = rows
.try_next()
.await
.context("Failed to retrieve a game from the DB")?
{
games.push(GamePartial::try_from_row(row)?);
}
Ok(games)
}
pub(super) async fn get(&self, game: &str) -> Result<Option<Game>> {
let Some(game_row) = query("SELECT * FROM games WHERE name = ?;")
.bind(game)
.fetch_optional(self.pool)
.await
.context("Failed to retrieve a game from the DB")?
else {
return Ok(None);
};
let setup = GameSetup::try_from_row(game_row)?;
let mut players = Vec::new();
let mut player_rows = query("SELECT ordinal, username FROM players WHERE game = ?;")
.bind(game)
.fetch(self.pool);
while let Some(player_row) = player_rows
.try_next()
.await
.context("Failed to retrieve game players from the DB")?
{
players.push(GamePlayer::try_from_row(player_row)?);
}
Ok(Some(Game::new(setup, players)))
}
pub(super) async fn create(&self, game: Game) -> Result<(), CreationError> {
let game_setup = game.setup();
let game_config = game_setup.config();
let mut transaction = self.pool.begin().await.map_err(CreationError::Database)?;
let result =
query("INSERT INTO games (name, max_players, map_hash, map_name, server) VALUES(?, ?, ?, ?, ?);")
.bind(game_config.name())
.bind(game_config.max_players())
.bind(game_config.map().hash())
.bind(game_config.map().name())
.bind(game_setup.server().to_string())
.execute(&mut transaction)
.await;
db_error_code!(
result,
CreationError::NameTaken,
SQLITE_CONSTRAINT_PRIMARYKEY
);
result.map_err(CreationError::Database)?;
let mut author = true;
for username in game.players() {
Self::add_player_inner(&mut transaction, author, username, game_config.name())
.await
.map_err(CreationError::AdditionError)?;
author = false;
}
transaction
.commit()
.await
.map_err(CreationError::Database)?;
Ok(())
}
pub(super) async fn add_player(
&self,
player: &GamePlayer,
game: &str,
) -> Result<(), AdditionError> {
Self::add_player_inner(self.pool, false, player, game).await
}
async fn add_player_inner<'c, E>(
executor: E,
author: bool,
player: &GamePlayer,
game: &str,
) -> Result<(), AdditionError>
where
E: SqliteExecutor<'c>,
{
let result =
query("INSERT INTO players (ordinal, author, username, game) VALUES (?, ?, ?, ?);")
.bind(player.info().ordinal())
.bind(author)
.bind(player.username())
.bind(game)
.execute(executor)
.await;
db_error_code!(
result,
AdditionError::UserOrGameDoesNotExist,
SQLITE_CONSTRAINT_FOREIGNKEY
);
db_error_message!(
result,
AdditionError::AlreadyInAGame,
"UNIQUE constraint failed: players.username"
);
db_error_message!(
result,
AdditionError::OrdinalConflict,
"UNIQUE constraint failed: players.game, players.ordinal"
);
db_error_message!(result, AdditionError::OrdinalTooLarge, "TOO-LARGE-ORDINAL");
result.map_err(AdditionError::Database)?;
Ok(())
}
pub(super) async fn remove_player(
&self,
username: &str,
game: &str,
) -> Result<(), RemovalError> {
let mut transaction = self.pool.begin().await.map_err(RemovalError::Database)?;
let mut rows = query("SELECT author FROM players WHERE username = ? AND game = ?;")
.bind(username)
.bind(game)
.fetch(self.pool);
let action = match rows.try_next().await.map_err(RemovalError::Database)? {
Some(row) => {
let author: bool = row.try_get("author").map_err(RemovalError::Database)?;
if author {
RemovalAction::Abandoned
} else {
RemovalAction::Removed
}
}
None => return Err(RemovalError::NotInTheGame),
};
match action {
RemovalAction::Abandoned => {
query("DELETE FROM games WHERE name = ?;")
.bind(game)
.execute(&mut transaction)
.await
.map_err(RemovalError::Database)?;
}
RemovalAction::Removed => {
Self::remove_player_inner(&mut transaction, username, game).await?;
}
}
transaction.commit().await.map_err(RemovalError::Database)?;
Ok(())
}
async fn remove_player_inner<'c, E>(
executor: E,
username: &str,
game: &str,
) -> Result<(), RemovalError>
where
E: SqliteExecutor<'c>,
{
let query_result = query("DELETE FROM players WHERE username = ? AND game = ?;")
.bind(username)
.bind(game)
.execute(executor)
.await
.map_err(RemovalError::Database)?;
let rows_affected = query_result.rows_affected();
assert!(rows_affected <= 1);
if rows_affected == 0 {
return Err(RemovalError::NotInTheGame);
}
Ok(())
}
}
enum RemovalAction {
Abandoned,
Removed,
}
#[derive(Error, Debug)]
pub(super) enum CreationError {
#[error("Game name is already taken")]
NameTaken,
#[error("Could not add all players to the game")]
AdditionError(#[source] AdditionError),
#[error("A database error encountered")]
Database(#[source] sqlx::Error),
#[error(transparent)]
Other(#[from] anyhow::Error),
}
#[derive(Error, Debug)]
pub(super) enum AdditionError {
#[error("User is already in another game")]
AlreadyInAGame,
#[error("Another player already joined the game with the same ordinal")]
OrdinalConflict,
#[error("Player ordinal is larger than maximum number of players in the game")]
OrdinalTooLarge,
#[error("The user or the game does not exist")]
UserOrGameDoesNotExist,
#[error("A database error encountered")]
Database(#[source] sqlx::Error),
#[error(transparent)]
Other(#[from] anyhow::Error),
}
#[derive(Error, Debug)]
pub(super) enum RemovalError {
#[error("User is not in the game")]
NotInTheGame,
#[error("A database error encountered")]
Database(#[source] sqlx::Error),
}
impl FromRow for GamePlayer {
type Error = anyhow::Error;
fn try_from_row(row: SqliteRow) -> Result<Self, Self::Error> {
let username: String = row.try_get("username")?;
let ordinal: u8 = row.try_get("ordinal")?;
Ok(Self::new(username, GamePlayerInfo::new(ordinal)))
}
}
impl FromRow for GameSetup {
type Error = anyhow::Error;
fn try_from_row(row: SqliteRow) -> Result<Self, Self::Error> {
let server: String = row.try_get("server")?;
let server: SocketAddr = server.parse()?;
let config = GameConfig::try_from_row(row)?;
Ok(Self::new(server, config))
}
}
impl FromRow for GamePartial {
type Error = anyhow::Error;
fn try_from_row(row: SqliteRow) -> Result<Self, Self::Error> {
let num_players: u8 = row.try_get("num_players")?;
let config = GameConfig::try_from_row(row)?;
Ok(Self::new(config, num_players))
}
}
impl FromRow for GameConfig {
type Error = anyhow::Error;
fn try_from_row(row: SqliteRow) -> Result<Self, Self::Error> {
let name: String = row.try_get("name")?;
let max_players: u8 = row.try_get("max_players")?;
let map = GameMap::try_from_row(row)?;
Ok(Self::new(name, max_players, map))
}
}
impl FromRow for GameMap {
type Error = anyhow::Error;
fn try_from_row(row: SqliteRow) -> Result<Self, Self::Error> {
let hash: String = row.try_get("map_hash")?;
let name: String = row.try_get("map_name")?;
Ok(Self::new(hash, name))
}
}