1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
use super::matchmaker::{start_game, MATCHMAKER_STATE};
use anyhow::Result;
use bones_matchmaker_proto::{MatchInfo, MatchmakerResponse};
use iroh::{endpoint::Connection, Endpoint};
use tokio::time::{sleep, Duration};

/// Handles a stop matchmaking request from a client
pub async fn handle_stop_matchmaking(
    conn: Connection,
    match_info: MatchInfo,
    send: &mut iroh::endpoint::SendStream,
) -> Result<()> {
    let stable_id = conn.stable_id();
    info!("[{}] Handling stop matchmaking request", stable_id);
    let state = MATCHMAKER_STATE.lock().await;

    let removed = state
        .matchmaking_rooms
        .update(&match_info, |_, members| {
            if let Some(pos) = members
                .iter()
                .position(|member| member.stable_id() == stable_id)
            {
                members.remove(pos);
                true
            } else {
                false
            }
        })
        .unwrap_or(false);

    let response = if removed {
        info!(
            "[{}] Player successfully removed from matchmaking queue",
            stable_id
        );
        MatchmakerResponse::Accepted
    } else {
        info!("[{}] Player not found in matchmaking queue", stable_id);
        MatchmakerResponse::Error("Not found in matchmaking queue".to_string())
    };

    let message = postcard::to_allocvec(&response)?;
    send.write_all(&message).await?;
    send.finish()?;
    send.stopped().await?;

    // If we removed a player, send update to the other players in the room
    if removed {
        drop(state); // Release the lock before calling send_matchmaking_updates
        if let Ok(active_connections) = send_matchmaking_updates(&match_info, 0).await {
            let player_count = active_connections.len();
            info!(
                "[{}] Sent updated matchmaking status to other players in the matchmaking room. Current player count: {}",
                stable_id, player_count
            );
        } else {
            info!(
                "[{}] Failed to send update to other players in the matchmaking room",
                stable_id
            );
        }
    }

    Ok(())
}

/// Handles a start matchmaking request from a client
pub async fn handle_request_matchaking(
    ep: &Endpoint,
    conn: Connection,
    match_info: MatchInfo,
    send: &mut iroh::endpoint::SendStream,
) -> Result<()> {
    let stable_id = conn.stable_id();
    info!("[{}] Handling start matchmaking search request", stable_id);
    let mut state = MATCHMAKER_STATE.lock().await;

    // Wait for up to 20 seconds if the matchmaking room is full
    for i in 0..200 {
        let room_is_full = state
            .matchmaking_rooms
            .get(&match_info)
            .map(|room| room.get().len() >= match_info.max_players as usize)
            .unwrap_or(false);

        if !room_is_full {
            info!(
                "[{}] Found available space in matchmaking room after waiting for {} milliseconds",
                stable_id,
                i as f64 * 100.0
            );
            break;
        } else if i == 0 {
            info!(
                "[{}] Matchmaking room is full, waiting for current room to clear...)",
                stable_id
            );
        }
        // Temporarily release the lock while waiting
        drop(state);
        sleep(Duration::from_millis(100)).await;
        state = MATCHMAKER_STATE.lock().await;
    }

    // Final check if the room can be joined
    let can_join = state
        .matchmaking_rooms
        .get(&match_info)
        .map(|room| room.get().len() < match_info.max_players as usize)
        .unwrap_or(true);

    // Send error if room is still full
    // TODO: If this happens often in practice, rework flow so that when matchmaking room is full and all connections are alive
    // then have the final connection hand-off be concurrently run without needing to be awaited.
    if !can_join {
        info!(
            "[{}] Matchmaking room is full after waiting, rejecting request",
            stable_id
        );
        let error_message = postcard::to_allocvec(&MatchmakerResponse::Error(
            "Matchmaking room is full. Please try matchmaking again shortly.".to_string(),
        ))?;
        send.write_all(&error_message).await?;
        send.finish()?;
        send.stopped().await?;
        return Ok(());
    }

    // Accept the matchmaking request
    let message = postcard::to_allocvec(&MatchmakerResponse::Accepted)?;
    send.write_all(&message).await?;
    send.finish()?;
    send.stopped().await?;

    // Add the connection to the matchmaking room
    let new_player_count = state
        .matchmaking_rooms
        .update(&match_info, |_, members| {
            members.push(conn.clone());
            info!(
                "[{}] Added player to matchmaking room. New count: {}",
                stable_id,
                members.len()
            );
            members.len() as u32
        })
        .unwrap_or_else(|| {
            let members = vec![conn.clone()];
            info!("[{}] Created new matchmaking room with 1 player", stable_id);
            if let Err(e) = state.matchmaking_rooms.insert(match_info.clone(), members) {
                warn!(
                    "[{}] Failed to insert new matchmaking room: {:?}",
                    stable_id, e
                );
            }
            1_u32
        });

    // Release the lock after adding the new player
    drop(state);

    // Update all players and get active connections
    info!(
        "[{}] Sending update to all players & cleaning connections in the matchmaking room ",
        stable_id
    );
    let active_connections = send_matchmaking_updates(&match_info, new_player_count).await?;

    let player_count = active_connections.len();
    info!(
        "[{}] Active connections after cleaning/sending update: {}",
        stable_id, player_count
    );

    // Start the game if room is full
    if player_count >= match_info.max_players as usize {
        info!(
            "[{}] Matchmaking room is full. Starting the game.",
            stable_id
        );
        start_matchmaked_game_if_ready(ep, &match_info).await?;
    } else {
        info!(
            "[{}] Matchmaking room is not full yet. Waiting for more players.",
            stable_id
        );
    }

    Ok(())
}

/// Sends matchmaking updates to all players in a room.
/// Actively checks if all connections are still alive before sending out new_player_count.
/// Returns the list of active connections.
async fn send_matchmaking_updates(
    match_info: &MatchInfo,
    new_player_count: u32,
) -> Result<Vec<Connection>> {
    let connections = {
        let state = MATCHMAKER_STATE.lock().await;
        state
            .matchmaking_rooms
            .get(match_info)
            .map(|room| room.get().clone())
            .unwrap_or_default()
    };

    let current_count = connections.len() as u32;
    let mut active_connections = Vec::new();

    // Prepare first update message
    let first_update_message = postcard::to_allocvec(&MatchmakerResponse::MatchmakingUpdate {
        player_count: current_count,
    })?;

    // Send first update and check active connections
    for (_index, conn) in connections.into_iter().enumerate() {
        if let Ok(mut send) = conn.open_uni().await {
            if send.write_all(&first_update_message).await.is_ok()
                && send.finish().is_ok()
                && send.stopped().await.is_ok()
            {
                active_connections.push(conn);
            }
        }
    }

    // Send second update if active connections count changed
    if active_connections.len() as u32 != new_player_count {
        let second_update_message =
            postcard::to_allocvec(&MatchmakerResponse::MatchmakingUpdate {
                player_count: active_connections.len() as u32,
            })?;

        for (index, member) in active_connections.iter().enumerate() {
            if let Ok(mut send) = member.open_uni().await {
                if let Err(e) = send.write_all(&second_update_message).await {
                    warn!("Connection to client {} has closed. {:?}", index, e);
                } else if let Err(e) = send.finish() {
                    warn!("Connection to client {} has closed. {:?}", index, e);
                } else if let Err(e) = send.stopped().await {
                    warn!("Connection to client {} has closed. {:?}", index, e);
                }
            }
        }
    }

    // Update stored connections
    {
        let state = MATCHMAKER_STATE.lock().await;
        if state.matchmaking_rooms.remove(match_info).is_none() {
            warn!("Failed to remove matchmaking room: {:?}", &match_info);
        }
        if let Err(e) = state
            .matchmaking_rooms
            .insert(match_info.clone(), active_connections.clone())
        {
            warn!(
                "Failed to insert updated matchmaking room: {:?}. Error: {:?}",
                &match_info, e
            );
        }
    }

    Ok(active_connections)
}

/// Starts a matchmade game if the room is ready with sufficient players
async fn start_matchmaked_game_if_ready(ep: &Endpoint, match_info: &MatchInfo) -> Result<()> {
    let members = {
        let state = MATCHMAKER_STATE.lock().await;
        state
            .matchmaking_rooms
            .remove(match_info)
            .map(|(_, connections)| connections)
    };

    if let Some(members) = members {
        let cloned_match_info = match_info.clone();
        let players_len = members.len();
        let ep = ep.clone();
        tokio::spawn(async move {
            match start_game(ep, members, &cloned_match_info).await {
                Ok(_) => info!("Starting matchmaked game with {} players", players_len),
                Err(e) => error!("Error starting match: {:?}", e),
            }
        });
    } else {
        warn!("Failed to remove matchmaking room when starting game");
    }

    Ok(())
}