bones_matchmaker/
matchmaker.rs

1use super::lobbies::{handle_create_lobby, handle_join_lobby, handle_list_lobbies};
2use super::matchmaking::{handle_request_matchaking, handle_stop_matchmaking};
3use crate::helpers::generate_random_seed;
4use anyhow::Result;
5use bones_matchmaker_proto::{
6    GameID, LobbyId, LobbyInfo, MatchInfo, MatchmakerRequest, MatchmakerResponse,
7    PlayerIdxAssignment,
8};
9use futures::future::BoxFuture;
10use iroh::{endpoint::Connection, Endpoint, NodeAddr};
11use once_cell::sync::Lazy;
12use rand::{prelude::SliceRandom, SeedableRng};
13use scc::HashMap as SccHashMap;
14use std::cmp::Ordering;
15use std::collections::HashMap;
16use std::sync::Arc;
17use tokio::sync::Mutex;
18
19/// Represents the lobbies for a specific game
20pub struct GameLobbies {
21    #[allow(dead_code)]
22    pub game_id: GameID,
23    pub lobbies: HashMap<LobbyId, LobbyInfo>,
24}
25
26/// Represents the global state of the matchmaker
27#[derive(Default)]
28pub struct State {
29    pub game_lobbies: HashMap<GameID, GameLobbies>,
30    pub lobby_connections: SccHashMap<(GameID, LobbyId), Vec<Connection>>,
31    pub matchmaking_rooms: SccHashMap<MatchInfo, Vec<Connection>>,
32}
33
34/// Global state of the matchmaker
35pub static MATCHMAKER_STATE: Lazy<Arc<Mutex<State>>> =
36    Lazy::new(|| Arc::new(Mutex::new(State::default())));
37
38#[derive(Debug)]
39pub struct Matchmaker {
40    endpoint: Endpoint,
41}
42
43impl iroh::protocol::ProtocolHandler for Matchmaker {
44    fn accept(self: Arc<Self>, conn: iroh::endpoint::Connecting) -> BoxFuture<'static, Result<()>> {
45        Box::pin(async move {
46            let connection = conn.await;
47
48            match connection {
49                Ok(conn) => {
50                    info!(
51                        connection_id = conn.stable_id(),
52                        addr = ?conn.remote_address(),
53                        "Accepted connection from client"
54                    );
55
56                    // Spawn a task to handle the new connection
57                    self.handle_connection(conn).await?;
58                }
59                Err(e) => error!("Error opening client connection: {e:?}"),
60            }
61
62            Ok(())
63        })
64    }
65}
66
67impl Matchmaker {
68    pub fn new(endpoint: Endpoint) -> Self {
69        Matchmaker { endpoint }
70    }
71
72    /// Handles incoming connections and routes requests to appropriate handlers
73    async fn handle_connection(&self, conn: Connection) -> Result<()> {
74        let connection_id = conn.stable_id();
75        loop {
76            tokio::select! {
77                _ = conn.closed() => {
78                    info!("[{}] Client closed connection.", connection_id);
79                    return Ok(());
80                }
81                bi = conn.accept_bi() => {
82                    let (mut send, mut recv) = bi?;
83                    // Parse the incoming request
84                    let request: MatchmakerRequest = postcard::from_bytes(&recv.read_to_end(256).await?)?;
85
86                    // Route the request to the appropriate handler
87                    match request {
88                        MatchmakerRequest::RequestMatchmaking(match_info) => {
89                            handle_request_matchaking(&self.endpoint, conn.clone(), match_info, &mut send).await?;
90                            send.finish()?;
91                            send.stopped().await?;
92                        }
93                        MatchmakerRequest::StopMatchmaking(match_info) => {
94                            handle_stop_matchmaking(conn.clone(), match_info, &mut send).await?;
95                        }
96                        MatchmakerRequest::ListLobbies(game_id) => {
97                            handle_list_lobbies(game_id, &mut send).await?;
98                        }
99                        MatchmakerRequest::CreateLobby(lobby_info) => {
100                            handle_create_lobby(conn.clone(), lobby_info, &mut send).await?;
101                        }
102                        MatchmakerRequest::JoinLobby(game_id, lobby_id, password) => {
103                            handle_join_lobby(&self.endpoint, conn.clone(), game_id, lobby_id, password, &mut send).await?;
104                        }
105                    }
106                }
107            }
108        }
109    }
110}
111
112/// Starts a match/lobby with the given members
113pub async fn start_game(
114    ep: Endpoint,
115    members: Vec<Connection>,
116    match_info: &MatchInfo,
117) -> Result<()> {
118    let random_seed = generate_random_seed();
119    let mut player_ids = Vec::new();
120    let player_count = members.len();
121
122    // Generate player indices based on the PlayerIdxAssignment
123    let player_indices = match &match_info.player_idx_assignment {
124        PlayerIdxAssignment::Ordered => (0..player_count).collect::<Vec<_>>(),
125        PlayerIdxAssignment::Random => {
126            let mut indices: Vec<_> = (0..player_count).collect();
127            let mut rng = rand::rngs::StdRng::seed_from_u64(random_seed);
128            indices.shuffle(&mut rng);
129            indices
130        }
131        PlayerIdxAssignment::SpecifiedOrder(order) => {
132            let mut indices = order.clone();
133            match indices.len().cmp(&player_count) {
134                Ordering::Less => {
135                    indices.extend(indices.len()..player_count);
136                }
137                Ordering::Greater => {
138                    indices.truncate(player_count);
139                }
140                _ => (),
141            }
142            indices
143        }
144    };
145
146    // Collect player IDs and addresses
147    for (conn_idx, conn) in members.iter().enumerate() {
148        let id = iroh::endpoint::get_remote_node_id(conn)?;
149        let mut addr = NodeAddr::new(id);
150        if let Some(info) = ep.remote_info(id) {
151            if let Some(relay_url) = info.relay_url {
152                addr = addr.with_relay_url(relay_url.relay_url);
153            }
154            addr = addr.with_direct_addresses(info.addrs.into_iter().map(|addr| addr.addr));
155        }
156        let player_idx = player_indices[conn_idx];
157        player_ids.push((player_idx as u32, addr));
158    }
159
160    // Sort player_ids by the assigned player index
161    player_ids.sort_by_key(|&(idx, _)| idx);
162
163    // Send match information to each player
164    for (conn_idx, conn) in members.into_iter().enumerate() {
165        let player_idx = player_indices[conn_idx];
166        let message = postcard::to_allocvec(&MatchmakerResponse::Success {
167            random_seed,
168            player_count: player_ids.len() as u32,
169            player_idx: player_idx as u32,
170            player_ids: player_ids.clone(),
171        })?;
172        let mut send = conn.open_uni().await?;
173        send.write_all(&message).await?;
174        send.finish()?;
175        send.stopped().await?;
176        conn.close(0u32.into(), b"done");
177    }
178
179    Ok(())
180}