bones_matchmaker/
matchmaking.rs

1use super::matchmaker::{start_game, MATCHMAKER_STATE};
2use anyhow::Result;
3use bones_matchmaker_proto::{MatchInfo, MatchmakerResponse};
4use iroh::{endpoint::Connection, Endpoint};
5use tokio::time::{sleep, Duration};
6
7/// Handles a stop matchmaking request from a client
8pub async fn handle_stop_matchmaking(
9    conn: Connection,
10    match_info: MatchInfo,
11    send: &mut iroh::endpoint::SendStream,
12) -> Result<()> {
13    let stable_id = conn.stable_id();
14    info!("[{}] Handling stop matchmaking request", stable_id);
15    let state = MATCHMAKER_STATE.lock().await;
16
17    let removed = state
18        .matchmaking_rooms
19        .update(&match_info, |_, members| {
20            if let Some(pos) = members
21                .iter()
22                .position(|member| member.stable_id() == stable_id)
23            {
24                members.remove(pos);
25                true
26            } else {
27                false
28            }
29        })
30        .unwrap_or(false);
31
32    let response = if removed {
33        info!(
34            "[{}] Player successfully removed from matchmaking queue",
35            stable_id
36        );
37        MatchmakerResponse::Accepted
38    } else {
39        info!("[{}] Player not found in matchmaking queue", stable_id);
40        MatchmakerResponse::Error("Not found in matchmaking queue".to_string())
41    };
42
43    let message = postcard::to_allocvec(&response)?;
44    send.write_all(&message).await?;
45    send.finish()?;
46    send.stopped().await?;
47
48    // If we removed a player, send update to the other players in the room
49    if removed {
50        drop(state); // Release the lock before calling send_matchmaking_updates
51        if let Ok(active_connections) = send_matchmaking_updates(&match_info, 0).await {
52            let player_count = active_connections.len();
53            info!(
54                "[{}] Sent updated matchmaking status to other players in the matchmaking room. Current player count: {}",
55                stable_id, player_count
56            );
57        } else {
58            info!(
59                "[{}] Failed to send update to other players in the matchmaking room",
60                stable_id
61            );
62        }
63    }
64
65    Ok(())
66}
67
68/// Handles a start matchmaking request from a client
69pub async fn handle_request_matchaking(
70    ep: &Endpoint,
71    conn: Connection,
72    match_info: MatchInfo,
73    send: &mut iroh::endpoint::SendStream,
74) -> Result<()> {
75    let stable_id = conn.stable_id();
76    info!("[{}] Handling start matchmaking search request", stable_id);
77    let mut state = MATCHMAKER_STATE.lock().await;
78
79    // Wait for up to 20 seconds if the matchmaking room is full
80    for i in 0..200 {
81        let room_is_full = state
82            .matchmaking_rooms
83            .get(&match_info)
84            .map(|room| room.get().len() >= match_info.max_players as usize)
85            .unwrap_or(false);
86
87        if !room_is_full {
88            info!(
89                "[{}] Found available space in matchmaking room after waiting for {} milliseconds",
90                stable_id,
91                i as f64 * 100.0
92            );
93            break;
94        } else if i == 0 {
95            info!(
96                "[{}] Matchmaking room is full, waiting for current room to clear...)",
97                stable_id
98            );
99        }
100        // Temporarily release the lock while waiting
101        drop(state);
102        sleep(Duration::from_millis(100)).await;
103        state = MATCHMAKER_STATE.lock().await;
104    }
105
106    // Final check if the room can be joined
107    let can_join = state
108        .matchmaking_rooms
109        .get(&match_info)
110        .map(|room| room.get().len() < match_info.max_players as usize)
111        .unwrap_or(true);
112
113    // Send error if room is still full
114    // TODO: If this happens often in practice, rework flow so that when matchmaking room is full and all connections are alive
115    // then have the final connection hand-off be concurrently run without needing to be awaited.
116    if !can_join {
117        info!(
118            "[{}] Matchmaking room is full after waiting, rejecting request",
119            stable_id
120        );
121        let error_message = postcard::to_allocvec(&MatchmakerResponse::Error(
122            "Matchmaking room is full. Please try matchmaking again shortly.".to_string(),
123        ))?;
124        send.write_all(&error_message).await?;
125        send.finish()?;
126        send.stopped().await?;
127        return Ok(());
128    }
129
130    // Accept the matchmaking request
131    let message = postcard::to_allocvec(&MatchmakerResponse::Accepted)?;
132    send.write_all(&message).await?;
133    send.finish()?;
134    send.stopped().await?;
135
136    // Add the connection to the matchmaking room
137    let new_player_count = state
138        .matchmaking_rooms
139        .update(&match_info, |_, members| {
140            members.push(conn.clone());
141            info!(
142                "[{}] Added player to matchmaking room. New count: {}",
143                stable_id,
144                members.len()
145            );
146            members.len() as u32
147        })
148        .unwrap_or_else(|| {
149            let members = vec![conn.clone()];
150            info!("[{}] Created new matchmaking room with 1 player", stable_id);
151            if let Err(e) = state.matchmaking_rooms.insert(match_info.clone(), members) {
152                warn!(
153                    "[{}] Failed to insert new matchmaking room: {:?}",
154                    stable_id, e
155                );
156            }
157            1_u32
158        });
159
160    // Release the lock after adding the new player
161    drop(state);
162
163    // Update all players and get active connections
164    info!(
165        "[{}] Sending update to all players & cleaning connections in the matchmaking room ",
166        stable_id
167    );
168    let active_connections = send_matchmaking_updates(&match_info, new_player_count).await?;
169
170    let player_count = active_connections.len();
171    info!(
172        "[{}] Active connections after cleaning/sending update: {}",
173        stable_id, player_count
174    );
175
176    // Start the game if room is full
177    if player_count >= match_info.max_players as usize {
178        info!(
179            "[{}] Matchmaking room is full. Starting the game.",
180            stable_id
181        );
182        start_matchmaked_game_if_ready(ep, &match_info).await?;
183    } else {
184        info!(
185            "[{}] Matchmaking room is not full yet. Waiting for more players.",
186            stable_id
187        );
188    }
189
190    Ok(())
191}
192
193/// Sends matchmaking updates to all players in a room.
194/// Actively checks if all connections are still alive before sending out new_player_count.
195/// Returns the list of active connections.
196async fn send_matchmaking_updates(
197    match_info: &MatchInfo,
198    new_player_count: u32,
199) -> Result<Vec<Connection>> {
200    let connections = {
201        let state = MATCHMAKER_STATE.lock().await;
202        state
203            .matchmaking_rooms
204            .get(match_info)
205            .map(|room| room.get().clone())
206            .unwrap_or_default()
207    };
208
209    let current_count = connections.len() as u32;
210    let mut active_connections = Vec::new();
211
212    // Prepare first update message
213    let first_update_message = postcard::to_allocvec(&MatchmakerResponse::MatchmakingUpdate {
214        player_count: current_count,
215    })?;
216
217    // Send first update and check active connections
218    for conn in connections.into_iter() {
219        if let Ok(mut send) = conn.open_uni().await {
220            if send.write_all(&first_update_message).await.is_ok()
221                && send.finish().is_ok()
222                && send.stopped().await.is_ok()
223            {
224                active_connections.push(conn);
225            }
226        }
227    }
228
229    // Send second update if active connections count changed
230    if active_connections.len() as u32 != new_player_count {
231        let second_update_message =
232            postcard::to_allocvec(&MatchmakerResponse::MatchmakingUpdate {
233                player_count: active_connections.len() as u32,
234            })?;
235
236        for (index, member) in active_connections.iter().enumerate() {
237            if let Ok(mut send) = member.open_uni().await {
238                if let Err(e) = send.write_all(&second_update_message).await {
239                    warn!("Connection to client {} has closed. {:?}", index, e);
240                } else if let Err(e) = send.finish() {
241                    warn!("Connection to client {} has closed. {:?}", index, e);
242                } else if let Err(e) = send.stopped().await {
243                    warn!("Connection to client {} has closed. {:?}", index, e);
244                }
245            }
246        }
247    }
248
249    // Update stored connections
250    {
251        let state = MATCHMAKER_STATE.lock().await;
252        if state.matchmaking_rooms.remove(match_info).is_none() {
253            warn!("Failed to remove matchmaking room: {:?}", &match_info);
254        }
255        if let Err(e) = state
256            .matchmaking_rooms
257            .insert(match_info.clone(), active_connections.clone())
258        {
259            warn!(
260                "Failed to insert updated matchmaking room: {:?}. Error: {:?}",
261                &match_info, e
262            );
263        }
264    }
265
266    Ok(active_connections)
267}
268
269/// Starts a matchmade game if the room is ready with sufficient players
270async fn start_matchmaked_game_if_ready(ep: &Endpoint, match_info: &MatchInfo) -> Result<()> {
271    let members = {
272        let state = MATCHMAKER_STATE.lock().await;
273        state
274            .matchmaking_rooms
275            .remove(match_info)
276            .map(|(_, connections)| connections)
277    };
278
279    if let Some(members) = members {
280        let cloned_match_info = match_info.clone();
281        let players_len = members.len();
282        let ep = ep.clone();
283        tokio::spawn(async move {
284            match start_game(ep, members, &cloned_match_info).await {
285                Ok(_) => info!("Starting matchmaked game with {} players", players_len),
286                Err(e) => error!("Error starting match: {:?}", e),
287            }
288        });
289    } else {
290        warn!("Failed to remove matchmaking room when starting game");
291    }
292
293    Ok(())
294}