Skip to main content

nil_server_database/model/
game.rs

1// Copyright (C) Call of Nil contributors
2// SPDX-License-Identifier: AGPL-3.0-only
3
4use crate::Database;
5use crate::error::{Error, Result};
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::version::db_Version;
11use crate::sql_types::zoned::db_Zoned;
12use diesel::prelude::*;
13use nil_core::world::World;
14use nil_crypto::password::Password;
15use nil_server_types::round::RoundDuration;
16use nil_util::string::StringExt;
17use std::fmt;
18use tokio::task::spawn_blocking;
19
20#[derive(Identifiable, Queryable, Selectable, Clone)]
21#[diesel(table_name = crate::schema::game)]
22#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
23#[diesel(belongs_to(UserData, foreign_key = created_by))]
24pub struct Game {
25  pub id: GameId,
26  pub password: Option<HashedPassword>,
27  pub description: Option<String>,
28  pub round_duration: Option<db_Duration>,
29  pub created_by: UserId,
30  pub created_at: db_Zoned,
31  pub updated_at: db_Zoned,
32}
33
34impl From<GameWithBlob> for Game {
35  fn from(game: GameWithBlob) -> Self {
36    Self {
37      id: game.id,
38      password: game.password,
39      description: game.description,
40      round_duration: game.round_duration,
41      created_by: game.created_by,
42      created_at: game.created_at,
43      updated_at: game.updated_at,
44    }
45  }
46}
47
48impl fmt::Debug for Game {
49  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50    f.debug_struct("Game")
51      .field("id", &self.id.to_string())
52      .field("round_duration", &self.round_duration)
53      .field("created_by", &self.created_by)
54      .field("created_at", &self.created_at.to_string())
55      .field("updated_at", &self.updated_at.to_string())
56      .finish_non_exhaustive()
57  }
58}
59
60#[derive(Identifiable, Queryable, Selectable, Clone)]
61#[diesel(table_name = crate::schema::game)]
62#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
63#[diesel(belongs_to(UserData, foreign_key = created_by))]
64pub struct GameWithBlob {
65  pub id: GameId,
66  pub password: Option<HashedPassword>,
67  pub description: Option<String>,
68  pub round_duration: Option<db_Duration>,
69  pub server_version: db_Version,
70  pub created_by: UserId,
71  pub created_at: db_Zoned,
72  pub updated_at: db_Zoned,
73  pub world_blob: Vec<u8>,
74}
75
76impl GameWithBlob {
77  #[inline]
78  pub fn to_world(&self) -> Result<World> {
79    Ok(World::load(&self.world_blob)?)
80  }
81}
82
83impl fmt::Debug for GameWithBlob {
84  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85    f.debug_struct("GameWithBlob")
86      .field("id", &self.id.to_string())
87      .field("round_duration", &self.round_duration)
88      .field("created_by", &self.created_by)
89      .field("created_at", &self.created_at.to_string())
90      .field("updated_at", &self.updated_at.to_string())
91      .finish_non_exhaustive()
92  }
93}
94
95#[derive(Insertable, Clone)]
96#[diesel(table_name = crate::schema::game)]
97pub struct NewGame {
98  id: GameId,
99  password: Option<HashedPassword>,
100  description: Option<String>,
101  round_duration: Option<db_Duration>,
102  server_version: db_Version,
103  created_by: UserId,
104  created_at: db_Zoned,
105  updated_at: db_Zoned,
106  world_blob: Vec<u8>,
107}
108
109#[bon::bon]
110impl NewGame {
111  #[builder]
112  pub async fn new(
113    #[builder(start_fn, into)] id: GameId,
114    #[builder(start_fn)] blob: Vec<u8>,
115    #[builder(into)] password: Option<Password>,
116    #[builder(into)] mut description: Option<String>,
117    #[builder(into)] round_duration: Option<RoundDuration>,
118    #[builder(into)] server_version: db_Version,
119    created_by: UserId,
120  ) -> Result<Self> {
121    if let Some(description) = &mut description {
122      description.truncate_chars(1000);
123    }
124
125    let now = db_Zoned::now();
126
127    Ok(Self {
128      id,
129      password: hash_password(password).await?,
130      description,
131      round_duration: round_duration.map(Into::into),
132      server_version,
133      created_by,
134      created_at: now.clone(),
135      updated_at: now,
136      world_blob: blob,
137    })
138  }
139
140  #[inline]
141  pub fn blob(&self) -> &[u8] {
142    &self.world_blob
143  }
144
145  #[inline]
146  pub async fn create(self, database: &Database) -> Result<usize> {
147    database.create_game(self).await
148  }
149}
150
151impl fmt::Debug for NewGame {
152  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
153    f.debug_struct("NewGame")
154      .field("id", &self.id.to_string())
155      .field("round_duration", &self.round_duration)
156      .field("server_version", &self.server_version)
157      .field("created_by", &self.created_by)
158      .field("created_at", &self.created_at.to_string())
159      .field("updated_at", &self.updated_at.to_string())
160      .finish_non_exhaustive()
161  }
162}
163
164async fn hash_password(password: Option<Password>) -> Result<Option<HashedPassword>> {
165  let Some(password) = password else { return Ok(None) };
166  let pass_len = password.trim().chars().count();
167
168  if pass_len == 0 {
169    return Ok(None);
170  } else if !(3..=50).contains(&pass_len) {
171    return Err(Error::InvalidPassword);
172  }
173
174  spawn_blocking(move || HashedPassword::new(&password))
175    .await?
176    .map(Some)
177    .map_err(Into::into)
178}