bones_framework/networking/
online.rs

1#![doc = include_str!("./online.md")]
2// TODO
3#![allow(missing_docs)]
4
5use crate::{
6    networking::{get_network_endpoint, NetworkMatchSocket},
7    prelude::*,
8};
9pub use bones_matchmaker_proto::{
10    GameID, LobbyId, LobbyInfo, LobbyListItem, MatchInfo, PlayerIdxAssignment, MATCH_ALPN,
11};
12use iroh::{endpoint::Connection, Endpoint, NodeId};
13use once_cell::sync::Lazy;
14use tracing::{info, warn};
15
16/// The number of bytes to use for read_to_end()
17pub const READ_TO_END_BYTE_COUNT: usize = 256;
18
19/// Struct that holds a channel which exchange messages with the matchmaking server.
20#[derive(DerefMut, Deref)]
21pub struct OnlineMatchmaker(BiChannelClient<OnlineMatchmakerRequest, OnlineMatchmakerResponse>);
22
23/// Online matchmaker request
24#[derive(Debug)]
25pub enum OnlineMatchmakerRequest {
26    SearchForGame {
27        id: NodeId,
28        player_count: u32,
29        game_id: GameID,
30        match_data: Vec<u8>,
31        player_idx_assignment: PlayerIdxAssignment,
32    },
33    StopSearch {
34        id: NodeId,
35    },
36    ListLobbies {
37        id: NodeId,
38        game_id: GameID,
39    },
40    CreateLobby {
41        id: NodeId,
42        lobby_info: LobbyInfo,
43    },
44    JoinLobby {
45        id: NodeId,
46        game_id: GameID,
47        lobby_id: LobbyId,
48        password: Option<String>,
49    },
50}
51
52impl OnlineMatchmakerRequest {
53    /// Returns the NodeId associated with the request.
54    pub fn node_id(&self) -> NodeId {
55        match self {
56            OnlineMatchmakerRequest::SearchForGame { id, .. } => *id,
57            OnlineMatchmakerRequest::StopSearch { id } => *id,
58            OnlineMatchmakerRequest::ListLobbies { id, .. } => *id,
59            OnlineMatchmakerRequest::CreateLobby { id, .. } => *id,
60            OnlineMatchmakerRequest::JoinLobby { id, .. } => *id,
61        }
62    }
63}
64
65/// Online matchmaker response
66#[derive(Serialize, Clone)]
67pub enum OnlineMatchmakerResponse {
68    /// Searching for matchmaking in progress
69    Searching,
70    /// Response that specifies updates about the current matchmaking (ie. player count updates)
71    MatchmakingUpdate { player_count: u32 },
72    /// The desired client count has been reached, and the match may start.
73    GameStarting {
74        #[serde(skip_serializing, skip_deserializing)]
75        socket: NetworkMatchSocket,
76        player_idx: usize,
77        player_count: usize,
78        random_seed: u64,
79    },
80    /// Response that specifies updates about the current lobby (ie. player count updates)
81    LobbyUpdate { player_count: u32 },
82    /// A list of available lobbies
83    LobbiesList(Vec<LobbyListItem>),
84    /// Confirmation that a lobby has been created
85    LobbyCreated(LobbyId),
86    /// Confirmation that a client has joined a lobby
87    LobbyJoined {
88        lobby_id: LobbyId,
89        player_count: usize,
90    },
91    /// An error message response
92    Error(String),
93}
94
95impl std::fmt::Debug for OnlineMatchmakerResponse {
96    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97        let serialized =
98            serde_yaml::to_string(self).expect("Failed to serialize OnlineMatchmakerResponse");
99        write!(f, "{:?}", serialized)
100    }
101}
102
103/// Online matchmaker channel
104pub static ONLINE_MATCHMAKER: Lazy<OnlineMatchmaker> = Lazy::new(|| {
105    let (client, server) = bi_channel();
106
107    RUNTIME.spawn(async move {
108        if let Err(err) = process_matchmaker_requests(server).await {
109            warn!("online matchmaker failed: {err:?}");
110        }
111    });
112
113    OnlineMatchmaker(client)
114});
115
116/// Internal struct used to keep track of the connection with the matchmaker
117pub struct MatchmakerConnectionState {
118    ep: Option<Endpoint>,
119    conn: Option<Connection>,
120    node_id: Option<NodeId>,
121}
122
123impl Default for MatchmakerConnectionState {
124    fn default() -> Self {
125        Self::new()
126    }
127}
128
129impl MatchmakerConnectionState {
130    /// Initialize a new MatchmakerConnectionState
131    pub fn new() -> Self {
132        Self {
133            ep: None,
134            conn: None,
135            node_id: None,
136        }
137    }
138
139    /// Acquires the matchmaker connection, either establishing from scratch if none exists
140    /// or fetching the currently held connection.
141    pub async fn acquire_connection(&mut self) -> anyhow::Result<&Connection> {
142        if let Some(id) = self.node_id {
143            if self.conn.is_none() {
144                info!("Connecting to online matchmaker");
145                let ep = get_network_endpoint().await;
146                let conn = ep.connect(id, MATCH_ALPN).await?;
147                self.ep = Some(ep.clone());
148                self.conn = Some(conn);
149                info!("Connected to online matchmaker");
150            }
151
152            self.conn
153                .as_ref()
154                .ok_or_else(|| anyhow::anyhow!("Failed to establish connection"))
155        } else {
156            Err(anyhow::anyhow!("NodeId not set"))
157        }
158    }
159
160    /// Closes the connection with the matchmaker, and removes the conn/ep from self.
161    pub fn close_connection(&mut self) {
162        if let Some(conn) = self.conn.take() {
163            conn.close(0u32.into(), b"Closing matchmaker connection");
164        }
165        self.ep = None;
166    }
167
168    /// Returns true if a connection with the matchmaker currently exists
169    pub fn is_connected(&self) -> bool {
170        self.conn.is_some()
171    }
172
173    /// Sets the iroh NodeId that will be used to establish connection with the matchmaker
174    pub fn set_node_id(&mut self, id: NodeId) {
175        self.node_id = Some(id);
176    }
177}
178
179/// Core communication processing for the matchmaker
180async fn process_matchmaker_requests(
181    user_channel: BiChannelServer<OnlineMatchmakerRequest, OnlineMatchmakerResponse>,
182) -> anyhow::Result<()> {
183    let mut matchmaker_connection_state = MatchmakerConnectionState::new();
184
185    while let Ok(message) = user_channel.recv().await {
186        match message {
187            OnlineMatchmakerRequest::SearchForGame {
188                id,
189                player_count,
190                game_id,
191                match_data,
192                player_idx_assignment,
193            } => {
194                matchmaker_connection_state.set_node_id(id);
195                let match_info = MatchInfo {
196                    max_players: player_count,
197                    match_data,
198                    game_id,
199                    player_idx_assignment,
200                };
201
202                if let Err(err) = crate::networking::online_matchmaking::resolve_search_for_match(
203                    &user_channel,
204                    &mut matchmaker_connection_state,
205                    match_info.clone(),
206                )
207                .await
208                {
209                    warn!("Start Matchmaking Search failed: {err:?}");
210                }
211            }
212            OnlineMatchmakerRequest::ListLobbies { id, game_id } => {
213                matchmaker_connection_state.set_node_id(id);
214                if let Err(err) = crate::networking::online_lobby::resolve_list_lobbies(
215                    &user_channel,
216                    &mut matchmaker_connection_state,
217                    game_id,
218                )
219                .await
220                {
221                    warn!("Listing lobbies failed: {err:?}");
222                }
223            }
224            OnlineMatchmakerRequest::CreateLobby { id, lobby_info } => {
225                matchmaker_connection_state.set_node_id(id);
226                if let Err(err) = crate::networking::online_lobby::resolve_create_lobby(
227                    &user_channel,
228                    &mut matchmaker_connection_state,
229                    lobby_info,
230                )
231                .await
232                {
233                    warn!("Creating lobby failed: {err:?}");
234                }
235            }
236            OnlineMatchmakerRequest::JoinLobby {
237                id,
238                game_id,
239                lobby_id,
240                password,
241            } => {
242                matchmaker_connection_state.set_node_id(id);
243                if let Err(err) = crate::networking::online_lobby::resolve_join_lobby(
244                    &user_channel,
245                    &mut matchmaker_connection_state,
246                    game_id,
247                    lobby_id,
248                    password,
249                )
250                .await
251                {
252                    warn!("Joining lobby failed: {err:?}");
253                }
254            }
255            // Otherwise do nothing as the requests will be dealt with in the above functions
256            _ => {}
257        }
258    }
259
260    Ok(())
261}
262
263// Interface for interacting with the matchmaker from a game
264impl OnlineMatchmaker {
265    /// Read and return the latest matchmaker response, if one exists.
266    pub fn read_matchmaker_response() -> Option<OnlineMatchmakerResponse> {
267        ONLINE_MATCHMAKER.try_recv().ok()
268    }
269
270    /// Sends a request to the matchmaking server to start searching for a match. Response is read via `read_matchmaker_response()`.
271    pub fn start_search_for_match(
272        matchmaking_server: NodeId,
273        game_id: GameID,
274        player_count: u32,
275        match_data: Vec<u8>,
276        player_idx_assignment: PlayerIdxAssignment,
277    ) -> anyhow::Result<()> {
278        ONLINE_MATCHMAKER
279            .try_send(OnlineMatchmakerRequest::SearchForGame {
280                id: matchmaking_server,
281                player_count,
282                game_id,
283                match_data,
284                player_idx_assignment,
285            })
286            .map_err(|e| anyhow::anyhow!("Failed to send matchmaker request: {}", e))?;
287        Ok(())
288    }
289
290    /// Stops searching for a match.
291    pub fn stop_search_for_match(matchmaking_server: NodeId) -> anyhow::Result<()> {
292        ONLINE_MATCHMAKER
293            .try_send(OnlineMatchmakerRequest::StopSearch {
294                id: matchmaking_server,
295            })
296            .map_err(|e| anyhow::anyhow!("Failed to send matchmaker request: {}", e))?;
297        Ok(())
298    }
299
300    /// Sends a request to the matchmaking server to provide a list of all available lobbies for game_id. Response is read via `read_matchmaker_response()`.
301    pub fn list_lobbies(matchmaking_server: NodeId, game_id: GameID) -> anyhow::Result<()> {
302        ONLINE_MATCHMAKER
303            .try_send(OnlineMatchmakerRequest::ListLobbies {
304                id: matchmaking_server,
305                game_id,
306            })
307            .map_err(|e| anyhow::anyhow!("Failed to send list lobbies request: {}", e))?;
308        Ok(())
309    }
310
311    /// Sends a request to the matchmaking server to create a new lobby with the specified lobby_info.
312    pub fn create_lobby(matchmaking_server: NodeId, lobby_info: LobbyInfo) -> anyhow::Result<()> {
313        ONLINE_MATCHMAKER
314            .try_send(OnlineMatchmakerRequest::CreateLobby {
315                id: matchmaking_server,
316                lobby_info,
317            })
318            .map_err(|e| anyhow::anyhow!("Failed to send create lobby request: {}", e))?;
319        Ok(())
320    }
321
322    /// Sends a request to the matchmaking server to join a lobby with the specified game_id, lobby_id, and optional password.
323    pub fn join_lobby(
324        matchmaking_server: NodeId,
325        game_id: GameID,
326        lobby_id: LobbyId,
327        password: Option<String>,
328    ) -> anyhow::Result<()> {
329        ONLINE_MATCHMAKER
330            .try_send(OnlineMatchmakerRequest::JoinLobby {
331                id: matchmaking_server,
332                game_id,
333                lobby_id,
334                password,
335            })
336            .map_err(|e| anyhow::anyhow!("Failed to send join lobby request: {}", e))?;
337        Ok(())
338    }
339}