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::db_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::db_PlayerId;
12use crate::sql_types::zoned::db_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;
41
42      game::table
43        .select($model::as_select())
44        .load(&mut *self.conn())
45        .map_err(Into::into)
46    }
47  };
48}
49
50impl BlockingDatabase {
51  decl_get!(get_game, Game);
52  decl_get!(get_game_with_blob, GameWithBlob);
53
54  decl_get_all!(get_games, Game);
55  decl_get_all!(get_games_with_blob, GameWithBlob);
56
57  pub fn count_games(&self) -> Result<i64> {
58    use crate::schema::game;
59
60    game::table
61      .count()
62      .get_result(&mut *self.conn())
63      .map_err(Into::into)
64  }
65
66  pub fn create_game(&self, new: &NewGame) -> Result<usize> {
67    use crate::schema::game;
68
69    diesel::insert_into(game::table)
70      .values(new)
71      .on_conflict(game::id)
72      .do_update()
73      .set((
74        game::world_blob.eq(new.blob()),
75        game::updated_at.eq(db_Zoned::now()),
76      ))
77      .execute(&mut *self.conn())
78      .map_err(Into::into)
79  }
80
81  pub fn delete_game(&self, id: GameId) -> Result<usize> {
82    use crate::schema::game;
83
84    diesel::delete(game::table.find(id))
85      .execute(&mut *self.conn())
86      .map_err(Into::into)
87  }
88
89  pub fn delete_games(&self, ids: &[GameId]) -> Result<usize> {
90    use crate::schema::game;
91
92    if ids.is_empty() {
93      Ok(0)
94    } else {
95      diesel::delete(game::table.filter(game::id.eq_any(ids)))
96        .execute(&mut *self.conn())
97        .map_err(Into::into)
98    }
99  }
100
101  pub fn game_exists(&self, id: GameId) -> Result<bool> {
102    use crate::schema::game;
103    use diesel::dsl::{exists, select};
104
105    select(exists(game::table.find(id)))
106      .get_result(&mut *self.conn())
107      .map_err(Into::into)
108  }
109
110  pub fn get_game_creator(&self, id: GameId) -> Result<db_PlayerId> {
111    use crate::schema::game;
112
113    let user_id = game::table
114      .find(&id)
115      .select(game::created_by)
116      .first::<UserId>(&mut *self.conn())?;
117
118    self.get_user_player_id(user_id)
119  }
120
121  pub fn get_game_ids(&self) -> Result<Vec<GameId>> {
122    use crate::schema::game;
123
124    game::table
125      .select(game::id)
126      .load(&mut *self.conn())
127      .map_err(Into::into)
128  }
129
130  pub fn get_game_password(&self, id: GameId) -> Result<Option<HashedPassword>> {
131    use crate::schema::game;
132
133    let result = game::table
134      .find(&id)
135      .select(game::password)
136      .first(&mut *self.conn());
137
138    if let Err(DieselError::NotFound) = &result {
139      Err(Error::GameNotFound(id))
140    } else {
141      Ok(result?)
142    }
143  }
144
145  pub fn get_game_round_duration(&self, id: GameId) -> Result<Option<db_Duration>> {
146    use crate::schema::game;
147
148    let result = game::table
149      .find(&id)
150      .select(game::round_duration)
151      .first(&mut *self.conn());
152
153    if let Err(DieselError::NotFound) = &result {
154      Err(Error::GameNotFound(id))
155    } else {
156      Ok(result?)
157    }
158  }
159
160  pub fn update_game_blob(&self, id: GameId, blob: &[u8]) -> Result<usize> {
161    use crate::schema::game;
162
163    let n = diesel::update(game::table.find(&id))
164      .set((
165        game::world_blob.eq(blob),
166        game::updated_at.eq(db_Zoned::now()),
167      ))
168      .execute(&mut *self.conn())?;
169
170    if n == 0 { Err(Error::GameNotFound(id)) } else { Ok(n) }
171  }
172
173  pub fn verify_game_password(&self, id: GameId, password: Option<&Password>) -> Result<bool> {
174    if let Some(hash) = self.get_game_password(id)? {
175      password
176        .filter(|it| !it.trim().is_empty())
177        .is_some_and(|it| matches!(hash.verify(it), Ok(true)))
178        .pipe(Ok)
179    } else {
180      Ok(true)
181    }
182  }
183
184  pub fn was_game_created_by(&self, game_id: GameId, player_id: &db_PlayerId) -> Result<bool> {
185    Ok(&self.get_game_creator(game_id)? == player_id)
186  }
187}