Skip to main content

nil_server_database/impl/
game.rs

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