team04_server/lobby/state/
mod.rs

1/// Definitions for client handling.
2pub mod clients;
3pub mod handle_messages;
4pub mod matchups;
5/// Extension of the `clients` module specifically
6/// for players, because players are very complex.
7pub 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/// A shared, lockable reference to lobby state,
31/// shared between the lobby's task and the [server's task](crate::server).
32#[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/// The state of a lobby. The game may not have started yet, or it could be in-progress already.
73#[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    /// Matchups. 3rd field is true if 2nd field is a ghost.
88    pub matchups: Vec<(PlayerId, PlayerId, bool)>,
89
90    /// Contains a history of all previous lightsaber options
91    pub(crate) hist_lightsaber_options: Vec<LightsaberOptions>,
92    /// Contains a history of all previous unit options
93    pub(crate) hist_unit_options: Vec<[UnitType; 3]>,
94    /// Is initialized when the game is started
95    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            // initial value shouldn't matter
143            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                // game has not started yet, remove player from lobby (there is no way for them to reconnect via the reconnect token)
202                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                // game has already started, player can't leave
215                false
216            }
217        } else {
218            // remove spectator by name
219            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            // TODO: send data, similar to player reconnect
236        }
237        // TODO: are spectator names unique?
238        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                // create a new lobby with
286                // the same config set.
287                // disabled in tests because
288                // it fills up the log and
289                // makes debugging more confusing.
290                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}