Skip to main content

nil_server/
app.rs

1// Copyright (C) Call of Nil contributors
2// SPDX-License-Identifier: AGPL-3.0-only
3
4use crate::error::{Error, Result};
5use crate::response::{MaybeResponse, from_err};
6use crate::server::{remote, spawn_round_duration_task};
7use crate::{VERSION, env, res};
8use dashmap::DashMap;
9use either::Either;
10use jiff::Zoned;
11use nil_core::chat::Chat;
12use nil_core::continent::Continent;
13use nil_core::military::Military;
14use nil_core::npc::bot::BotManager;
15use nil_core::npc::precursor::PrecursorManager;
16use nil_core::player::PlayerManager;
17use nil_core::ranking::Ranking;
18use nil_core::report::ReportManager;
19use nil_core::round::Round;
20use nil_core::world::config::WorldId;
21use nil_core::world::{World, WorldOptions};
22use nil_crypto::password::Password;
23use nil_server_database::Database;
24use nil_server_database::model::game::{GameWithBlob, NewGame};
25use nil_server_database::sql_types::player_id::db_PlayerId;
26use nil_server_types::ServerKind;
27use nil_server_types::round::RoundDuration;
28use semver::{Prerelease, Version};
29use std::num::NonZeroU16;
30use std::sync::{Arc, Weak};
31use std::time::Duration;
32use tap::TryConv;
33use tokio::sync::RwLock;
34use tokio::task::{spawn, spawn_blocking};
35
36#[derive(Clone)]
37pub struct App {
38  server_kind: ServerKind,
39  database: Option<Database>,
40  worlds: Arc<DashMap<WorldId, Arc<RwLock<World>>>>,
41  world_limit: NonZeroU16,
42  world_limit_per_user: NonZeroU16,
43}
44
45#[bon::bon]
46impl App {
47  pub fn new_local(world: World) -> Self {
48    let id = world.config().id();
49    let app = Self {
50      server_kind: ServerKind::Local { id },
51      database: None,
52      worlds: Arc::new(DashMap::new()),
53      world_limit: NonZeroU16::MIN,
54      world_limit_per_user: NonZeroU16::MIN,
55    };
56
57    app
58      .worlds
59      .insert(id, Arc::new(RwLock::new(world)));
60
61    app
62  }
63
64  pub async fn new_remote(database_url: &str) -> Result<Self> {
65    let worlds = Arc::new(DashMap::new());
66    let database = Database::new(database_url).await?;
67
68    let mut invalid_games = Vec::new();
69
70    for game_id in database.get_game_ids().await? {
71      if let Ok(game) = database.get_game_with_blob(game_id).await
72        && has_valid_version(&game)
73        && has_valid_age(&game)
74        && let Ok(world) = game.to_world()
75      {
76        let world_id = world.config().id();
77        let round_id = world.round().id();
78        let is_round_idle = world.round().is_idle();
79
80        let database = database.clone();
81        let world = Arc::new(RwLock::new(world));
82        let weak_world = Arc::downgrade(&world);
83
84        if let Some(round_duration) = game.round_duration
85          && !is_round_idle
86        {
87          spawn(spawn_round_duration_task(
88            round_id,
89            Weak::clone(&weak_world),
90            round_duration.into(),
91          ));
92        }
93
94        world.write().await.on_next_round(
95          remote::on_next_round()
96            .database(database)
97            .weak_world(weak_world)
98            .maybe_round_duration(game.round_duration)
99            .call(),
100        );
101
102        worlds.insert(world_id, world);
103      } else {
104        tracing::warn!(invalid_game = %game_id);
105        invalid_games.push(game_id);
106      }
107    }
108
109    database.delete_games(invalid_games).await?;
110
111    Ok(Self {
112      server_kind: ServerKind::Remote,
113      database: Some(database),
114      worlds,
115      world_limit: env::remote_world_limit(),
116      world_limit_per_user: env::remote_world_limit_per_user(),
117    })
118  }
119
120  #[inline]
121  pub fn server_kind(&self) -> ServerKind {
122    self.server_kind
123  }
124
125  /// # Panics
126  ///
127  /// Panics if the server is not remote.
128  pub fn database(&self) -> Database {
129    if let ServerKind::Remote = self.server_kind
130      && let Some(database) = &self.database
131    {
132      database.clone()
133    } else {
134      panic!("Not a remote server")
135    }
136  }
137
138  pub fn world_ids(&self) -> Vec<WorldId> {
139    self
140      .worlds
141      .iter()
142      .map(|entry| *entry.key())
143      .collect()
144  }
145
146  #[inline]
147  pub fn world_limit(&self) -> u16 {
148    self.world_limit.get()
149  }
150
151  #[inline]
152  pub fn world_limit_per_user(&self) -> u16 {
153    self.world_limit_per_user.get()
154  }
155
156  /// Creates a new remote world with the given options.
157  ///
158  /// # Panics
159  ///
160  /// Panics if the server is not remote.
161  #[builder]
162  pub(crate) async fn create_remote(
163    &self,
164    #[builder(start_fn)] options: &WorldOptions,
165    #[builder(into)] player_id: db_PlayerId,
166    #[builder(into)] world_description: Option<String>,
167    #[builder(into)] world_password: Option<Password>,
168    #[builder(into)] round_duration: Option<RoundDuration>,
169    server_version: Version,
170  ) -> Result<WorldId> {
171    self
172      .check_remote_world_limit(player_id.clone())
173      .await?;
174
175    let database = self.database();
176    let user = database.get_user(player_id).await?;
177
178    let world = World::try_from(options)?;
179    let world_id = world.config().id();
180    let blob = world.to_bytes()?;
181
182    NewGame::builder(world_id, blob)
183      .created_by(user.id)
184      .maybe_description(world_description)
185      .maybe_password(world_password)
186      .maybe_round_duration(round_duration)
187      .server_version(server_version)
188      .build()
189      .await?
190      .create(&database)
191      .await?;
192
193    let database = database.clone();
194    let world = Arc::new(RwLock::new(world));
195
196    world.write().await.on_next_round(
197      remote::on_next_round()
198        .database(database)
199        .weak_world(Arc::downgrade(&world))
200        .maybe_round_duration(round_duration)
201        .call(),
202    );
203
204    self.worlds.insert(world_id, world);
205
206    Ok(world_id)
207  }
208
209  /// Checks if the player can create a new remote world.
210  async fn check_remote_world_limit(&self, player: db_PlayerId) -> Result<()> {
211    let database = self.database();
212
213    let limit = i64::from(self.world_limit.get());
214    if database.count_games().await? >= limit {
215      return Err(Error::WorldLimitReached);
216    }
217
218    let limit_per_user = i64::from(self.world_limit_per_user.get());
219    if database.count_games_by_user(player).await? >= limit_per_user {
220      return Err(Error::WorldLimitReached);
221    }
222
223    Ok(())
224  }
225
226  pub(crate) fn get(&self, id: WorldId) -> Result<Arc<RwLock<World>>> {
227    self
228      .worlds
229      .get(&id)
230      .map(|world| Arc::clone(&world))
231      .ok_or_else(|| Error::WorldNotFound(id))
232  }
233
234  pub(crate) fn remove(&self, id: WorldId) -> Option<Arc<RwLock<World>>> {
235    self.worlds.remove(&id).map(|it| it.1)
236  }
237
238  pub async fn world<F, T>(&self, id: WorldId, f: F) -> MaybeResponse<T>
239  where
240    F: FnOnce(&World) -> T,
241  {
242    match self.get(id) {
243      Ok(world) => Either::Left(f(&*world.read().await)),
244      Err(err) => Either::Right(from_err(err)),
245    }
246  }
247
248  pub async fn world_mut<F, T>(&self, id: WorldId, f: F) -> MaybeResponse<T>
249  where
250    F: FnOnce(&mut World) -> T,
251  {
252    match self.get(id) {
253      Ok(world) => Either::Left(f(&mut *world.write().await)),
254      Err(err) => Either::Right(from_err(err)),
255    }
256  }
257
258  pub async fn world_blocking<F, T>(&self, id: WorldId, f: F) -> MaybeResponse<T>
259  where
260    F: FnOnce(&World) -> T + Send + Sync + 'static,
261    T: Send + Sync + 'static,
262  {
263    match self.get(id) {
264      Ok(world) => {
265        match spawn_blocking(move || f(&world.blocking_read())).await {
266          Ok(value) => Either::Left(value),
267          Err(err) => {
268            tracing::error!(message = %err, error = ?err);
269            Either::Right(res!(INTERNAL_SERVER_ERROR))
270          }
271        }
272      }
273      Err(err) => Either::Right(from_err(err)),
274    }
275  }
276
277  pub async fn world_blocking_mut<F, T>(&self, id: WorldId, f: F) -> MaybeResponse<T>
278  where
279    F: FnOnce(&mut World) -> T + Send + Sync + 'static,
280    T: Send + Sync + 'static,
281  {
282    match self.get(id) {
283      Ok(world) => {
284        match spawn_blocking(move || f(&mut world.blocking_write())).await {
285          Ok(value) => Either::Left(value),
286          Err(err) => {
287            tracing::error!(message = %err, error = ?err);
288            Either::Right(res!(INTERNAL_SERVER_ERROR))
289          }
290        }
291      }
292      Err(err) => Either::Right(from_err(err)),
293    }
294  }
295
296  pub async fn bot_manager<F, T>(&self, id: WorldId, f: F) -> MaybeResponse<T>
297  where
298    F: FnOnce(&BotManager) -> T,
299  {
300    self
301      .world(id, |world| f(world.bot_manager()))
302      .await
303  }
304
305  pub async fn chat<F, T>(&self, id: WorldId, f: F) -> MaybeResponse<T>
306  where
307    F: FnOnce(&Chat) -> T,
308  {
309    self.world(id, |world| f(world.chat())).await
310  }
311
312  pub async fn continent<F, T>(&self, id: WorldId, f: F) -> MaybeResponse<T>
313  where
314    F: FnOnce(&Continent) -> T,
315  {
316    self
317      .world(id, |world| f(world.continent()))
318      .await
319  }
320
321  pub async fn military<F, T>(&self, id: WorldId, f: F) -> MaybeResponse<T>
322  where
323    F: FnOnce(&Military) -> T,
324  {
325    self
326      .world(id, |world| f(world.military()))
327      .await
328  }
329
330  pub async fn player_manager<F, T>(&self, id: WorldId, f: F) -> MaybeResponse<T>
331  where
332    F: FnOnce(&PlayerManager) -> T,
333  {
334    self
335      .world(id, |world| f(world.player_manager()))
336      .await
337  }
338
339  pub async fn precursor_manager<F, T>(&self, id: WorldId, f: F) -> MaybeResponse<T>
340  where
341    F: FnOnce(&PrecursorManager) -> T,
342  {
343    self
344      .world(id, |world| f(world.precursor_manager()))
345      .await
346  }
347
348  pub async fn ranking<F, T>(&self, id: WorldId, f: F) -> MaybeResponse<T>
349  where
350    F: FnOnce(&Ranking) -> T,
351  {
352    self
353      .world(id, |world| f(world.ranking()))
354      .await
355  }
356
357  pub async fn report_manager<F, T>(&self, id: WorldId, f: F) -> MaybeResponse<T>
358  where
359    F: FnOnce(&ReportManager) -> T,
360  {
361    self
362      .world(id, |world| f(world.report_manager()))
363      .await
364  }
365
366  pub async fn round<F, T>(&self, id: WorldId, f: F) -> MaybeResponse<T>
367  where
368    F: FnOnce(&Round) -> T,
369  {
370    self
371      .world(id, |world| f(world.round()))
372      .await
373  }
374}
375
376fn has_valid_version(game: &GameWithBlob) -> bool {
377  let Ok(version) = Version::parse(VERSION) else {
378    unreachable!("Current version should always be valid")
379  };
380
381  let minor = if version.major == 0 { version.minor } else { 0 };
382  let version_cmp = semver::Comparator {
383    op: semver::Op::Caret,
384    major: version.major,
385    minor: Some(minor),
386    patch: Some(0),
387    pre: Prerelease::EMPTY,
388  };
389
390  version_cmp.matches(&game.server_version)
391}
392
393fn has_valid_age(game: &GameWithBlob) -> bool {
394  let Ok(duration) = game
395    .updated_at
396    .duration_until(&Zoned::now())
397    .try_conv::<Duration>()
398  else {
399    return false;
400  };
401
402  duration <= Duration::from_days(30)
403}