team04_server/lobby/state/
mod.rs1pub mod clients;
3pub mod handle_messages;
4pub mod matchups;
5pub mod players;
8
9use std::sync::Arc;
10
11use crate::{
12 LOCK_WAIT_TIME_MS,
13 config::ConfigSet,
14 log,
15 messages::lightsaber_options::LightsaberOptions,
16 server::state::{LobbyId, SyncedServerState},
17 unit::UnitType,
18};
19use clients::{Clients, PlayerId, ReconnectToken, Spectator};
20use matchups::Matchups;
21use players::Player;
22use serde::Serialize;
23use strum::VariantArray;
24use tokio::sync::{Mutex, MutexGuard};
25
26use crate::messages::{
27 MessageTx, player_character::PlayerCharacter, text_broadcast::TextBroadcast,
28};
29
30#[derive(Clone, Debug)]
33pub struct SharedLobbyState(Arc<Mutex<LobbyState>>, LobbyId);
34pub type LockedLobbyState<'a> = MutexGuard<'a, LobbyState>;
35impl SharedLobbyState {
36 pub fn new(lobby_state: LobbyState) -> Self {
37 let id = lobby_state.id;
38 Self(Arc::new(Mutex::new(lobby_state)), id)
39 }
40 pub async fn lock(&self) -> LockedLobbyState {
41 let id = self.id();
42 let now = tokio::time::Instant::now();
43 let task = tokio::spawn(async move {
44 tokio::time::sleep(tokio::time::Duration::from_millis(LOCK_WAIT_TIME_MS.0)).await;
45 log::info!("[lock] getting lobby lock took more than {} ms ({})", LOCK_WAIT_TIME_MS.0, id; &log::pfx());
46 tokio::time::sleep(tokio::time::Duration::from_millis(LOCK_WAIT_TIME_MS.1)).await;
47 let _ondrop = Ondrop(now, id);
48 log::warning!("[lock] getting lobby lock took more than {} ms ({})", LOCK_WAIT_TIME_MS.1, id; &log::pfx());
49 tokio::time::sleep(tokio::time::Duration::MAX).await;
50 struct Ondrop(tokio::time::Instant, LobbyId);
51 impl Drop for Ondrop {
52 fn drop(&mut self) {
53 log::warning!("[lock] finally got lobby lock after {} ms ({})", self.0.elapsed().as_millis(), self.1; &log::pfx());
54 }
55 }
56 });
57 let lock = self.0.lock().await;
58 task.abort();
59 lock
60 }
61 pub fn id(&self) -> LobbyId {
62 self.1
63 }
64 pub async fn remove_lobby_from_server(&self) {
65 let lock = self.lock().await;
66 let server = Arc::clone(&lock.server);
67 drop(lock);
68 server.lock().await.remove_lobby(self.id()).await;
69 }
70}
71
72#[derive(Debug)]
74pub struct LobbyState {
75 id: LobbyId,
76 server: Arc<SyncedServerState>,
77 pub configs: ConfigSet,
78
79 game_started: bool,
80
81 pub(crate) round: u64,
82 pub(crate) phase: LobbyPhase,
83 pub(crate) paused: tokio::sync::watch::Sender<bool>,
84 pub(crate) pause_requested: bool,
85
86 pub clients: Clients,
87 pub matchups: Vec<(PlayerId, PlayerId, bool)>,
89
90 pub(crate) hist_lightsaber_options: Vec<LightsaberOptions>,
92 pub(crate) hist_unit_options: Vec<[UnitType; 3]>,
94 pub(crate) prev_matchups: Matchups,
96
97 pub on_game_start: TxRx<bool>,
98 pub(super) game_start_timer_id: u32,
99}
100#[derive(Debug, Serialize, Clone, Copy, PartialEq, Eq)]
101#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
102pub enum LobbyPhase {
103 LightsaberShopPhase,
104 UnitShopPhase,
105 PlacementPhase,
106 FightPhase,
107 CompletionPhase,
108}
109impl LobbyState {
110 pub fn new(
111 id: LobbyId,
112 clients: Clients,
113 configs: ConfigSet,
114 server: Arc<SyncedServerState>,
115 ) -> Self {
116 let mut pfx = log::pfx();
117 pfx.lobby(id);
118 log::info!("created lobby"; &pfx);
119 log::debug!(
120 "game config: {}",
121 serde_json::to_string_pretty(&configs.game_config).unwrap();
122 &pfx
123 );
124 log::debug!(
125 "board config: {}",
126 serde_json::to_string_pretty(&configs.board_config).unwrap();
127 &pfx
128 );
129 log::debug!(
130 "unit config: {}",
131 serde_json::to_string_pretty(&configs.unit_config).unwrap();
132 &pfx
133 );
134 Self {
135 id,
136 server,
137 configs,
138
139 game_started: false,
140
141 round: 0,
142 phase: LobbyPhase::CompletionPhase,
144 paused: tokio::sync::watch::Sender::new(false),
145 pause_requested: false,
146
147 clients,
148 matchups: vec![],
149
150 hist_lightsaber_options: vec![],
151 hist_unit_options: vec![],
152 prev_matchups: Matchups::new(0),
153
154 on_game_start: TxRx::new(false),
155 game_start_timer_id: 0,
156 }
157 }
158 pub fn id(&self) -> LobbyId {
159 self.id
160 }
161 pub fn paused(&self) -> bool {
162 *self.paused.borrow()
163 }
164}
165
166impl LobbyState {
167 pub async fn player_join<F, Fut>(
168 &mut self,
169 mut player: Player,
170 player_identification: F,
171 ) -> Result<(PlayerId, ReconnectToken), PlayerJoinError>
172 where
173 F: FnOnce() -> Fut,
174 Fut: Future<Output = (PlayerId, ReconnectToken)>,
175 {
176 if self.game_started() {
177 return Err(PlayerJoinError::GameAlreadyStarted);
178 }
179 if self.clients.players.len() >= self.max_players() {
180 return Err(PlayerJoinError::LobbyFull);
181 }
182 if self
183 .clients
184 .players
185 .players_all()
186 .any(|p| p.name() == player.name())
187 {
188 return Err(PlayerJoinError::NameInUse);
189 }
190 let (player_id, reconnect_token) = player_identification().await;
191 assert!(!self.clients.players.contains(&player_id));
192 player.reconnect_token = reconnect_token;
193 log::info!("player '{}' joined", player.name(); log::pfx().lobby(self.id()).player(player_id));
194 self.clients.players.add(player_id, player);
195 Ok((player_id, reconnect_token))
196 }
197
198 pub async fn client_leave(&mut self, name: &str, player_id: Option<PlayerId>) -> bool {
199 if let Some(player_id) = player_id {
200 if !self.game_started() {
201 if self.clients.players.remove(player_id).is_some() {
203 log::info!("player '{}' left", name; log::pfx().lobby(self.id()).player(player_id));
204 self.broadcast_lobby_info().await;
205 true
206 } else {
207 log::warning!(
208 "attempted to remove player '{name}', but no such player exists in the lobby.";
209 log::pfx().lobby(self.id).player(player_id)
210 );
211 false
212 }
213 } else {
214 false
216 }
217 } else {
218 if self.clients.spectators.remove_by_name(&name).is_some() {
220 log::info!("spectator '{}' left", name; log::pfx().lobby(self.id()));
221 self.broadcast_lobby_info().await;
222 true
223 } else {
224 log::warning!(
225 "[WARN] attempted to remove spectator '{name}' by name, but no such spectator exists in the lobby.";
226 log::pfx().lobby(self.id)
227 );
228 false
229 }
230 }
231 }
232
233 pub fn spectator_join(&mut self, spectator: Spectator) -> Result<(), SpectatorJoinError> {
234 if self.game_started() {
235 }
237 log::info!("spectator '{}' joined", spectator.name(); log::pfx().lobby(self.id()));
239 self.clients.spectators.add(spectator);
240 Ok(())
241 }
242}
243
244#[derive(Debug)]
245pub enum PlayerJoinError {
246 GameAlreadyStarted,
247 LobbyFull,
248 NameInUse,
249}
250
251pub enum SpectatorJoinError {}
252
253#[derive(Debug)]
254pub struct TxRx<T>(
255 tokio::sync::watch::Sender<T>,
256 tokio::sync::watch::Receiver<T>,
257);
258impl<T> TxRx<T> {
259 fn new(init: T) -> Self {
260 let channel = tokio::sync::watch::channel(init);
261 Self(channel.0, channel.1)
262 }
263 fn send(&mut self, v: T) -> bool {
264 self.0.send(v).is_ok()
265 }
266 pub fn recv(&self) -> tokio::sync::watch::Receiver<T> {
267 self.1.clone()
268 }
269}
270
271impl LobbyState {
272 pub fn min_players(&self) -> usize {
273 self.configs.board_config.min_players as usize
274 }
275 pub fn max_players(&self) -> usize {
276 self.configs.board_config.max_players as usize
277 }
278 pub fn game_started(&self) -> bool {
279 self.game_started
280 }
281 pub(super) fn set_game_started(&mut self) {
282 if !self.game_started {
283 #[cfg(not(all(test, debug_assertions)))]
284 {
285 let server_state = Arc::clone(&self.server);
291 let conf = self.configs.clone();
292 tokio::spawn(async move {
293 server_state
294 .lock()
295 .await
296 .add_lobby(conf, Arc::clone(&server_state), true);
297 });
298 }
299 self.game_started = true;
300 self.on_game_start.send(true);
301 }
302 }
303}
304
305impl LobbyState {
306 pub async fn broadcast_chat_message(&mut self, name: &str, message: &str) {
307 self.clients
308 .broadcast_message(&TextBroadcast::new(name, message).serialize())
309 .await;
310 }
311}
312
313impl LobbyState {
314 pub fn character_taken(&self, character: PlayerCharacter) -> bool {
315 self.clients
316 .players
317 .players_all()
318 .any(|player| player.character.is_some_and(|ch| ch == character))
319 }
320
321 pub fn available_characters(&self) -> Vec<PlayerCharacter> {
322 PlayerCharacter::VARIANTS
323 .iter()
324 .copied()
325 .filter(|ch| !self.character_taken(*ch))
326 .collect()
327 }
328
329 pub async fn broadcast_lobby_info(&mut self) {
330 self.clients
331 .broadcast_message(
332 &crate::messages::lobby_info::LobbyInfo::new_from_lobby_state(self).serialize(),
333 )
334 .await;
335 }
336
337 pub async fn broadcast_gamestate(&mut self) {
338 self.clients
339 .broadcast_message(
340 &crate::messages::game_state::GameState::new_from_lobby_state(self).serialize(),
341 )
342 .await;
343 }
344}
345
346#[cfg(test)]
347mod test {
348 use crate::{
349 lobby::{
350 state::clients::{PlayerId, Spectator},
351 test::{
352 config_set_modified, get_server_and_lobby, get_server_and_lobby_with_config,
353 player_join,
354 },
355 },
356 server::Connection,
357 };
358
359 #[tokio::test]
360 #[should_panic]
361 async fn player_join_too_many() {
362 let (server, lobby) = get_server_and_lobby_with_config(config_set_modified(
363 |_| {},
364 |_| {},
365 |board| {
366 board.max_players = 4;
367 },
368 ));
369 for _ in 0..5 {
370 player_join(&server, &lobby).await;
371 }
372 }
373
374 #[tokio::test]
375 #[should_panic]
376 async fn player_join_after_game_start() {
377 let (server, lobby) = get_server_and_lobby_with_config(config_set_modified(
378 |_| {},
379 |_| {},
380 |board| {
381 board.max_players = 4;
382 },
383 ));
384 for _ in 0..3 {
385 player_join(&server, &lobby).await;
386 }
387 lobby.lock().await.start_game_now().await;
388 player_join(&server, &lobby).await;
389 }
390
391 #[tokio::test]
392 async fn player_leave() {
393 let (server, lobby) = get_server_and_lobby();
394 let (p1id, _, _) = player_join(&server, &lobby).await;
395 assert_eq!(lobby.lock().await.clients.players.len(), 1);
396 assert!(lobby.lock().await.client_leave("", Some(p1id)).await);
397 assert_eq!(lobby.lock().await.clients.players.len(), 0);
398 }
399
400 #[tokio::test]
401 async fn player_leave_after_game_start() {
402 let (server, lobby) = get_server_and_lobby();
403 let (p1id, _, _) = player_join(&server, &lobby).await;
404 lobby.lock().await.start_game_now().await;
405 assert_eq!(lobby.lock().await.clients.players.len(), 1);
406 assert!(!lobby.lock().await.client_leave("", Some(p1id)).await);
407 assert!(
408 !lobby
409 .lock()
410 .await
411 .client_leave("", Some(PlayerId::random()))
412 .await
413 );
414 assert_eq!(lobby.lock().await.clients.players.len(), 1);
415 }
416
417 #[tokio::test]
418 async fn spectator_leave() {
419 let (_, lobby) = get_server_and_lobby();
420 let mut lock = lobby.lock().await;
421 assert!(
422 lock.spectator_join(Spectator::new("Spec".to_owned(), Connection::fake().0,))
423 .is_ok()
424 );
425 assert!(
426 lock.spectator_join(Spectator::new("Spec2".to_owned(), Connection::fake().0,))
427 .is_ok()
428 );
429 assert_eq!(lock.clients.spectators.len(), 2);
430 assert!(lock.client_leave("Spec", None).await);
431 assert_eq!(lock.clients.spectators.len(), 1);
432 assert!(!lock.client_leave("Spec", None).await);
433 assert_eq!(lock.clients.spectators.len(), 1);
434 }
435}