Skip to main content

nil_server_database/database/
game.rs

1// Copyright (C) Call of Nil contributors
2// SPDX-License-Identifier: AGPL-3.0-only
3
4use super::BlockingDatabase;
5use crate::error::{Error, Result};
6use crate::model::game::{Game, GameWithBlob, NewGame};
7use crate::sql_types::duration::Duration;
8use crate::sql_types::game_id::GameId;
9use crate::sql_types::hashed_password::HashedPassword;
10use crate::sql_types::id::UserId;
11use crate::sql_types::player_id::PlayerId;
12use crate::sql_types::zoned::Zoned;
13use diesel::prelude::*;
14use diesel::result::Error as DieselError;
15use nil_crypto::password::Password;
16use tap::Pipe;
17
18macro_rules! decl_get {
19  ($fn_name:ident, $model:ident) => {
20    pub fn $fn_name(&self, id: GameId) -> Result<$model> {
21      use $crate::schema::game;
22
23      let result = game::table
24        .find(&id)
25        .select($model::as_select())
26        .first(&mut *self.conn());
27
28      if let Err(DieselError::NotFound) = &result {
29        Err(Error::GameNotFound(id))
30      } else {
31        Ok(result?)
32      }
33    }
34  };
35}
36
37macro_rules! decl_get_all {
38  ($fn_name:ident, $model:ident) => {
39    pub fn $fn_name(&self) -> Result<Vec<$model>> {
40      use crate::schema::game::dsl::*;
41      game
42        .select($model::as_select())
43        .load(&mut *self.conn())
44        .map_err(Into::into)
45    }
46  };
47}
48
49impl BlockingDatabase {
50  decl_get!(get_game, Game);
51  decl_get!(get_game_with_blob, GameWithBlob);
52
53  decl_get_all!(get_games, Game);
54  decl_get_all!(get_games_with_blob, GameWithBlob);
55
56  pub fn count_games(&self) -> Result<i64> {
57    use crate::schema::game;
58
59    game::table
60      .count()
61      .get_result(&mut *self.conn())
62      .map_err(Into::into)
63  }
64
65  pub fn create_game(&self, new: &NewGame) -> Result<usize> {
66    use crate::schema::game;
67
68    diesel::insert_into(game::table)
69      .values(new)
70      .on_conflict(game::id)
71      .do_update()
72      .set((
73        game::world_blob.eq(new.blob()),
74        game::updated_at.eq(Zoned::now()),
75      ))
76      .execute(&mut *self.conn())
77      .map_err(Into::into)
78  }
79
80  pub fn delete_game(&self, id: GameId) -> Result<usize> {
81    use crate::schema::game;
82
83    diesel::delete(game::table.find(id))
84      .execute(&mut *self.conn())
85      .map_err(Into::into)
86  }
87
88  pub fn delete_games(&self, ids: &[GameId]) -> Result<usize> {
89    use crate::schema::game;
90
91    if ids.is_empty() {
92      Ok(0)
93    } else {
94      diesel::delete(game::table.filter(game::id.eq_any(ids)))
95        .execute(&mut *self.conn())
96        .map_err(Into::into)
97    }
98  }
99
100  pub fn game_exists(&self, id: GameId) -> Result<bool> {
101    use crate::schema::game;
102    use diesel::dsl::{exists, select};
103
104    select(exists(game::table.find(id)))
105      .get_result(&mut *self.conn())
106      .map_err(Into::into)
107  }
108
109  pub fn get_game_creator(&self, id: GameId) -> Result<PlayerId> {
110    use crate::schema::game;
111
112    let user_id = game::table
113      .find(&id)
114      .select(game::created_by)
115      .first::<UserId>(&mut *self.conn())?;
116
117    self.get_user_player_id(user_id)
118  }
119
120  pub fn get_game_ids(&self) -> Result<Vec<GameId>> {
121    use crate::schema::game;
122
123    game::table
124      .select(game::id)
125      .load(&mut *self.conn())
126      .map_err(Into::into)
127  }
128
129  pub fn get_game_password(&self, id: GameId) -> Result<Option<HashedPassword>> {
130    use crate::schema::game;
131
132    game::table
133      .find(&id)
134      .select(game::password)
135      .first(&mut *self.conn())
136      .map_err(Into::into)
137  }
138
139  pub fn get_game_round_duration(&self, id: GameId) -> Result<Option<Duration>> {
140    use crate::schema::game;
141
142    game::table
143      .find(&id)
144      .select(game::round_duration)
145      .first(&mut *self.conn())
146      .map_err(Into::into)
147  }
148
149  pub fn update_game_blob(&self, id: GameId, blob: &[u8]) -> Result<usize> {
150    use crate::schema::game;
151
152    diesel::update(game::table.find(&id))
153      .set((game::world_blob.eq(blob), game::updated_at.eq(Zoned::now())))
154      .execute(&mut *self.conn())
155      .map_err(Into::into)
156  }
157
158  pub fn verify_game_password(&self, id: GameId, password: Option<&Password>) -> Result<bool> {
159    if let Some(hash) = self.get_game_password(id)? {
160      password
161        .filter(|it| !it.trim().is_empty())
162        .is_some_and(|it| matches!(hash.verify(it), Ok(true)))
163        .pipe(Ok)
164    } else {
165      Ok(true)
166    }
167  }
168
169  pub fn was_game_created_by(&self, game_id: GameId, player_id: &PlayerId) -> Result<bool> {
170    Ok(&self.get_game_creator(game_id)? == player_id)
171  }
172}