1use 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 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 #[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 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}