bones_matchmaker/
lobbies.rs

1use super::matchmaker::{start_game, GameLobbies, MATCHMAKER_STATE};
2use crate::helpers::{generate_unique_id, hash_password};
3use anyhow::Result;
4use bones_matchmaker_proto::{
5    GameID, LobbyId, LobbyInfo, LobbyListItem, MatchInfo, MatchmakerResponse,
6};
7use iroh::{endpoint::Connection, Endpoint};
8use std::collections::HashMap;
9
10/// Handles a request to list lobbies for a specific game
11pub async fn handle_list_lobbies(
12    game_id: GameID,
13    send: &mut iroh::endpoint::SendStream,
14) -> Result<()> {
15    let state = MATCHMAKER_STATE.lock().await;
16    // Retrieve and format lobby information for the specified game
17    let lobbies = state
18        .game_lobbies
19        .get(&game_id)
20        .map(|game_lobbies| {
21            game_lobbies
22                .lobbies
23                .iter()
24                .map(|(id, lobby_info)| {
25                    let current_players = state
26                        .lobby_connections
27                        .get(&(game_id.clone(), id.clone()))
28                        .map(|entry| entry.get().len() as u32)
29                        .unwrap_or(0);
30                    LobbyListItem {
31                        id: id.clone(),
32                        name: lobby_info.name.clone(),
33                        current_players,
34                        max_players: lobby_info.max_players,
35                        has_password: lobby_info.password_hash.is_some(),
36                        game_id: game_id.clone(),
37                    }
38                })
39                .collect::<Vec<_>>()
40        })
41        .unwrap_or_default();
42
43    // Send the lobby list back to the client
44    let message = postcard::to_allocvec(&MatchmakerResponse::LobbiesList(lobbies))?;
45    send.write_all(&message).await?;
46    send.finish()?;
47    send.stopped().await?;
48
49    Ok(())
50}
51
52/// Handles a request to create a new lobby
53pub async fn handle_create_lobby(
54    conn: Connection,
55    lobby_info: LobbyInfo,
56    send: &mut iroh::endpoint::SendStream,
57) -> Result<()> {
58    let lobby_id = LobbyId(generate_unique_id());
59    let mut state = MATCHMAKER_STATE.lock().await;
60
61    // Create or update the game lobbies and insert the new lobby
62    state
63        .game_lobbies
64        .entry(lobby_info.game_id.clone())
65        .or_insert_with(|| GameLobbies {
66            game_id: lobby_info.game_id.clone(),
67            lobbies: HashMap::new(),
68        })
69        .lobbies
70        .insert(lobby_id.clone(), lobby_info.clone());
71
72    // Add the connection to the lobby
73    if let Err(e) = state
74        .lobby_connections
75        .insert((lobby_info.game_id.clone(), lobby_id.clone()), vec![conn])
76    {
77        error!("Failed to inserting lobby during creation: {:?}", e);
78    }
79
80    // Send confirmation to the client
81    let message = postcard::to_allocvec(&MatchmakerResponse::LobbyCreated(lobby_id))?;
82    send.write_all(&message).await?;
83    send.finish()?;
84    send.stopped().await?;
85
86    Ok(())
87}
88
89/// Handles a request to join an existing lobby
90pub async fn handle_join_lobby(
91    ep: &Endpoint,
92    conn: Connection,
93    game_id: GameID,
94    lobby_id: LobbyId,
95    password: Option<String>,
96    send: &mut iroh::endpoint::SendStream,
97) -> Result<()> {
98    let mut state = MATCHMAKER_STATE.lock().await;
99
100    if let Some(game_lobbies) = state.game_lobbies.get_mut(&game_id) {
101        if let Some(lobby_info) = game_lobbies.lobbies.get(&lobby_id) {
102            // Check password if the lobby is password-protected
103            if let Some(hash) = &lobby_info.password_hash {
104                if password.as_ref().map(|p| hash_password(p)) != Some(hash.clone()) {
105                    let message = postcard::to_allocvec(&MatchmakerResponse::Error(
106                        "Incorrect password".to_string(),
107                    ))?;
108                    send.write_all(&message).await?;
109                    send.finish()?;
110                    send.stopped().await?;
111                    return Ok(());
112                }
113            }
114
115            let max_players = lobby_info.max_players;
116            let match_data = lobby_info.match_data.clone();
117            let player_idx_assignment = lobby_info.player_idx_assignment.clone();
118
119            // Try to add the player to the lobby
120            let join_result = state.lobby_connections.update(
121                &(game_id.clone(), lobby_id.clone()),
122                |_exists, connections| {
123                    if connections.len() < max_players as usize {
124                        connections.push(conn.clone());
125                        Some(connections.len())
126                    } else {
127                        None
128                    }
129                },
130            );
131
132            match join_result {
133                Some(Some(count)) => {
134                    // Successfully joined the lobby
135                    let message =
136                        postcard::to_allocvec(&MatchmakerResponse::LobbyJoined(lobby_id.clone()))?;
137                    send.write_all(&message).await?;
138                    send.finish()?;
139                    send.stopped().await?;
140
141                    // Always notify all players in the lobby about the update
142                    let lobby_update_message =
143                        postcard::to_allocvec(&MatchmakerResponse::LobbyUpdate {
144                            player_count: count as u32,
145                        })?;
146                    if let Some(connections) = state
147                        .lobby_connections
148                        .get(&(game_id.clone(), lobby_id.clone()))
149                    {
150                        for connection in connections.get().iter() {
151                            let mut send = connection.open_uni().await?;
152                            send.write_all(&lobby_update_message).await?;
153                            send.finish()?;
154                            send.stopped().await?;
155                        }
156                    }
157
158                    // Check if the lobby is full and start the match if it is
159                    if count == max_players as usize {
160                        let match_info = MatchInfo {
161                            max_players,
162                            match_data,
163                            game_id: game_id.clone(),
164                            player_idx_assignment,
165                        };
166                        if let Some(connections) = state
167                            .lobby_connections
168                            .remove(&(game_id.clone(), lobby_id.clone()))
169                        {
170                            let members = connections.1;
171                            drop(state);
172                            let ep = ep.clone();
173                            tokio::spawn(async move {
174                                if let Err(e) = start_game(ep, members, &match_info).await {
175                                    error!("Error starting match from full lobby: {:?}", e);
176                                }
177                            });
178                        }
179                    }
180                }
181                _ => {
182                    // Lobby is full
183                    let message = postcard::to_allocvec(&MatchmakerResponse::Error(
184                        "Lobby is full".to_string(),
185                    ))?;
186                    send.write_all(&message).await?;
187                    send.finish()?;
188                    send.stopped().await?;
189                }
190            }
191        } else {
192            let message =
193                postcard::to_allocvec(&MatchmakerResponse::Error("Lobby not found".to_string()))?;
194            send.write_all(&message).await?;
195            send.finish()?;
196            send.stopped().await?;
197        }
198    } else {
199        let message =
200            postcard::to_allocvec(&MatchmakerResponse::Error("Game not found".to_string()))?;
201        send.write_all(&message).await?;
202        send.finish()?;
203        send.stopped().await?;
204    }
205
206    Ok(())
207}