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