steamworks/
matchmaking.rs

1use std::{
2    fmt::Display,
3    net::{Ipv4Addr, SocketAddrV4},
4};
5
6use super::*;
7#[cfg(test)]
8use serial_test::serial;
9
10/// Access to the steam matchmaking interface
11pub struct Matchmaking {
12    pub(crate) mm: *mut sys::ISteamMatchmaking,
13    pub(crate) inner: Arc<Inner>,
14}
15
16/// The visibility of a lobby
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
19pub enum LobbyType {
20    Private,
21    FriendsOnly,
22    Public,
23    Invisible,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
27#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
28pub struct LobbyId(pub(crate) u64);
29
30impl LobbyId {
31    /// Creates a `LobbyId` from a raw 64 bit value.
32    ///
33    /// May be useful for deserializing lobby ids from
34    /// a network or save format.
35    pub fn from_raw(id: u64) -> LobbyId {
36        LobbyId(id)
37    }
38
39    /// Returns the raw 64 bit value of the lobby id
40    ///
41    /// May be useful for serializing lobby ids over a
42    /// network or to a save format.
43    pub fn raw(&self) -> u64 {
44        self.0
45    }
46}
47
48impl Matchmaking {
49    pub fn request_lobby_list<F>(&self, cb: F)
50    where
51        F: FnOnce(SResult<Vec<LobbyId>>) + 'static + Send,
52    {
53        unsafe {
54            let api_call = sys::SteamAPI_ISteamMatchmaking_RequestLobbyList(self.mm);
55            register_call_result::<sys::LobbyMatchList_t, _>(
56                &self.inner,
57                api_call,
58                move |v, io_error| {
59                    cb(if io_error {
60                        Err(SteamError::IOFailure)
61                    } else {
62                        let mut out = Vec::with_capacity(v.m_nLobbiesMatching as usize);
63                        for idx in 0..v.m_nLobbiesMatching {
64                            out.push(LobbyId(sys::SteamAPI_ISteamMatchmaking_GetLobbyByIndex(
65                                sys::SteamAPI_SteamMatchmaking_v009(),
66                                idx as _,
67                            )));
68                        }
69                        Ok(out)
70                    })
71                },
72            );
73        }
74    }
75
76    /// Attempts to create a new matchmaking lobby
77    ///
78    /// The lobby with have the visibility of the of the passed
79    /// `LobbyType` and a limit of `max_members` inside it.
80    /// The `max_members` may not be higher than 250.
81    ///
82    /// # Triggers
83    ///
84    /// * `LobbyEnter`
85    /// * `LobbyCreated`
86    pub fn create_lobby<F>(&self, ty: LobbyType, max_members: u32, cb: F)
87    where
88        F: FnOnce(SResult<LobbyId>) + 'static + Send,
89    {
90        assert!(max_members <= 250); // Steam API limits
91        unsafe {
92            let ty = match ty {
93                LobbyType::Private => sys::ELobbyType::k_ELobbyTypePrivate,
94                LobbyType::FriendsOnly => sys::ELobbyType::k_ELobbyTypeFriendsOnly,
95                LobbyType::Public => sys::ELobbyType::k_ELobbyTypePublic,
96                LobbyType::Invisible => sys::ELobbyType::k_ELobbyTypeInvisible,
97            };
98            let api_call =
99                sys::SteamAPI_ISteamMatchmaking_CreateLobby(self.mm, ty, max_members as _);
100            register_call_result::<sys::LobbyCreated_t, _>(
101                &self.inner,
102                api_call,
103                move |v, io_error| {
104                    cb(if io_error {
105                        Err(SteamError::IOFailure)
106                    } else {
107                        crate::to_steam_result(v.m_eResult).map(|_| LobbyId(v.m_ulSteamIDLobby))
108                    })
109                },
110            );
111        }
112    }
113
114    /// Tries to join the lobby with the given ID
115    pub fn join_lobby<F>(&self, lobby: LobbyId, cb: F)
116    where
117        F: FnOnce(Result<LobbyId, ()>) + 'static + Send,
118    {
119        unsafe {
120            let api_call = sys::SteamAPI_ISteamMatchmaking_JoinLobby(self.mm, lobby.0);
121            register_call_result::<sys::LobbyEnter_t, _>(
122                &self.inner,
123                api_call,
124                move |v, io_error| {
125                    cb(if io_error || v.m_EChatRoomEnterResponse != 1 {
126                        Err(())
127                    } else {
128                        Ok(LobbyId(v.m_ulSteamIDLobby))
129                    })
130                },
131            );
132        }
133    }
134
135    /// Returns the number of data keys in the lobby
136    pub fn lobby_data_count(&self, lobby: LobbyId) -> u32 {
137        unsafe { sys::SteamAPI_ISteamMatchmaking_GetLobbyDataCount(self.mm, lobby.0) as _ }
138    }
139
140    /// Returns the lobby metadata associated with the specified key from the
141    /// specified lobby.
142    pub fn lobby_data(&self, lobby: LobbyId, key: &str) -> Option<String> {
143        let key = CString::new(key).unwrap();
144        unsafe {
145            let data = sys::SteamAPI_ISteamMatchmaking_GetLobbyData(self.mm, lobby.0, key.as_ptr());
146            CStr::from_ptr(data)
147        }
148        .to_str()
149        .ok()
150        .filter(|s| !s.is_empty())
151        .map(str::to_owned)
152    }
153
154    /// Returns the lobby metadata associated with the specified index
155    pub fn lobby_data_by_index(&self, lobby: LobbyId, idx: u32) -> Option<(String, String)> {
156        let mut key = [0i8; sys::k_nMaxLobbyKeyLength as usize];
157        let mut value = [0i8; sys::k_cubChatMetadataMax as usize];
158        unsafe {
159            let success = sys::SteamAPI_ISteamMatchmaking_GetLobbyDataByIndex(
160                self.mm,
161                lobby.0,
162                idx as _,
163                key.as_mut_ptr() as _,
164                key.len() as _,
165                value.as_mut_ptr() as _,
166                value.len() as _,
167            );
168            match success {
169                true => Some((
170                    CStr::from_ptr(key.as_ptr()).to_string_lossy().into_owned(),
171                    CStr::from_ptr(value.as_ptr())
172                        .to_string_lossy()
173                        .into_owned(),
174                )),
175                false => None,
176            }
177        }
178    }
179
180    /// Sets the lobby metadata associated with the specified key in the specified lobby.
181    pub fn set_lobby_data(&self, lobby: LobbyId, key: &str, value: &str) -> bool {
182        let key = CString::new(key).unwrap();
183        let value = CString::new(value).unwrap();
184        unsafe {
185            sys::SteamAPI_ISteamMatchmaking_SetLobbyData(
186                self.mm,
187                lobby.0,
188                key.as_ptr(),
189                value.as_ptr(),
190            )
191        }
192    }
193
194    /// Deletes the lobby metadata associated with the specified key in the specified lobby.
195    pub fn delete_lobby_data(&self, lobby: LobbyId, key: &str) -> bool {
196        let key = CString::new(key).unwrap();
197        unsafe { sys::SteamAPI_ISteamMatchmaking_DeleteLobbyData(self.mm, lobby.0, key.as_ptr()) }
198    }
199
200    /// Sets per-user metadata for the local user.
201    ///
202    /// Triggers a LobbyDataUpdate callback.
203    pub fn set_lobby_member_data(&self, lobby: LobbyId, key: &str, value: &str) {
204        let key = CString::new(key).unwrap();
205        let value = CString::new(value).unwrap();
206        unsafe {
207            sys::SteamAPI_ISteamMatchmaking_SetLobbyMemberData(
208                self.mm,
209                lobby.0,
210                key.as_ptr(),
211                value.as_ptr(),
212            )
213        }
214    }
215
216    /// Gets per-user metadata from another player in the specified lobby.
217    ///
218    /// This can only be queried from members in lobbies that you are currently in.
219    /// Returns None if lobby is invalid, user is not in the lobby, or key is not set.
220    pub fn get_lobby_member_data(
221        &self,
222        lobby: LobbyId,
223        user: SteamId,
224        key: &str,
225    ) -> Option<String> {
226        let key = CString::new(key).unwrap();
227        unsafe {
228            let data = sys::SteamAPI_ISteamMatchmaking_GetLobbyMemberData(
229                self.mm,
230                lobby.0,
231                user.0,
232                key.as_ptr(),
233            );
234            CStr::from_ptr(data)
235        }
236        .to_str()
237        .map(str::to_owned)
238        .ok()
239    }
240
241    /// Exits the passed lobby
242    pub fn leave_lobby(&self, lobby: LobbyId) {
243        unsafe {
244            sys::SteamAPI_ISteamMatchmaking_LeaveLobby(self.mm, lobby.0);
245        }
246    }
247
248    /// Returns the current limit on the number of players in a lobby.
249    ///
250    /// Returns `[None]` if no metadata is available for the specified lobby.
251    pub fn lobby_member_limit(&self, lobby: LobbyId) -> Option<usize> {
252        unsafe {
253            let count = sys::SteamAPI_ISteamMatchmaking_GetLobbyMemberLimit(self.mm, lobby.0);
254            match count {
255                0 => None,
256                _ => Some(count as usize),
257            }
258        }
259    }
260
261    /// Returns the steam id of the current owner of the passed lobby
262    pub fn lobby_owner(&self, lobby: LobbyId) -> SteamId {
263        unsafe {
264            SteamId(sys::SteamAPI_ISteamMatchmaking_GetLobbyOwner(
265                self.mm, lobby.0,
266            ))
267        }
268    }
269
270    /// Returns the number of players in a lobby.
271    ///
272    /// Useful if you are not currently in the lobby
273    pub fn lobby_member_count(&self, lobby: LobbyId) -> usize {
274        unsafe {
275            let count = sys::SteamAPI_ISteamMatchmaking_GetNumLobbyMembers(self.mm, lobby.0);
276            count as usize
277        }
278    }
279
280    /// Returns a list of members currently in the lobby
281    pub fn lobby_members(&self, lobby: LobbyId) -> Vec<SteamId> {
282        unsafe {
283            let count = sys::SteamAPI_ISteamMatchmaking_GetNumLobbyMembers(self.mm, lobby.0);
284            let mut members = Vec::with_capacity(count as usize);
285            for idx in 0..count {
286                members.push(SteamId(
287                    sys::SteamAPI_ISteamMatchmaking_GetLobbyMemberByIndex(self.mm, lobby.0, idx),
288                ))
289            }
290            members
291        }
292    }
293
294    /// Sets whether or not a lobby is joinable by other players. This always defaults to enabled
295    /// for a new lobby.
296    ///
297    /// If joining is disabled, then no players can join, even if they are a friend or have been
298    /// invited.
299    ///
300    /// Lobbies with joining disabled will not be returned from a lobby search.
301    ///
302    /// Returns true on success, false if the current user doesn't own the lobby.
303    pub fn set_lobby_joinable(&self, lobby: LobbyId, joinable: bool) -> bool {
304        unsafe { sys::SteamAPI_ISteamMatchmaking_SetLobbyJoinable(self.mm, lobby.0, joinable) }
305    }
306
307    /// Broadcasts a chat message (text or binary data) to all users in the lobby.
308    ///
309    /// # Parameters
310    /// - `lobby`: The Steam ID of the lobby to send the chat message to.
311    /// - `msg`: This can be text or binary data, up to 4 Kilobytes in size.
312    ///
313    /// # Description
314    /// All users in the lobby (including the local user) will receive a `LobbyChatMsg_t` callback
315    /// with the message.
316    ///
317    /// If you're sending binary data, you should prefix a header to the message so that you know
318    /// to treat it as your custom data rather than a plain old text message.
319    ///
320    /// For communication that needs to be arbitrated (e.g., having a user pick from a set of characters),
321    /// you can use the lobby owner as the decision maker. `GetLobbyOwner` returns the current lobby owner.
322    /// There is guaranteed to always be one and only one lobby member who is the owner.
323    /// So for the choose-a-character scenario, the user who is picking a character would send the binary
324    /// message 'I want to be Zoe', the lobby owner would see that message, see if it was OK, and broadcast
325    /// the appropriate result (user X is Zoe).
326    ///
327    /// These messages are sent via the Steam back-end, and so the bandwidth available is limited.
328    /// For higher-volume traffic like voice or game data, you'll want to use the Steam Networking API.
329    ///
330    /// # Returns
331    /// Returns `Ok(())` if the message was successfully sent. Returns an error of type `SteamError` if the
332    /// message is too small or too large, or if no connection to Steam could be made.
333    pub fn send_lobby_chat_message(&self, lobby: LobbyId, msg: &[u8]) -> Result<(), SteamError> {
334        match unsafe {
335            steamworks_sys::SteamAPI_ISteamMatchmaking_SendLobbyChatMsg(
336                self.mm,
337                lobby.0,
338                msg.as_ptr().cast(),
339                msg.len() as i32,
340            )
341        } {
342            true => Ok(()),
343            false => Err(SteamError::IOFailure),
344        }
345    }
346
347    /// Gets the data from a lobby chat message after receiving a `LobbyChatMsg_t` callback.
348    ///
349    /// # Parameters
350    /// - `lobby`: The Steam ID of the lobby to get the chat message from.
351    /// - `chat_id`: The index of the chat entry in the lobby.
352    /// - `buffer`: Buffer to save retrieved message data to. The buffer should be no
353    /// more than 4 Kilobytes.
354    ///
355    /// # Returns
356    /// Returns `&[u8]` A resliced byte array of the message buffer
357    pub fn get_lobby_chat_entry<'a>(
358        &self,
359        lobby: LobbyId,
360        chat_id: i32,
361        buffer: &'a mut [u8],
362    ) -> &'a [u8] {
363        let mut steam_user = sys::CSteamID {
364            m_steamid: sys::CSteamID_SteamID_t { m_unAll64Bits: 0 },
365        };
366        let mut chat_type = steamworks_sys::EChatEntryType::k_EChatEntryTypeInvalid;
367        unsafe {
368            let len = sys::SteamAPI_ISteamMatchmaking_GetLobbyChatEntry(
369                self.mm,
370                lobby.0,
371                chat_id,
372                &mut steam_user,
373                buffer.as_mut_ptr().cast(),
374                buffer.len() as _,
375                &mut chat_type,
376            );
377            return &buffer[0..len as usize];
378        }
379    }
380    /// Adds a string comparison filter to the lobby list request.
381    ///
382    /// This method adds a filter that compares a specific string attribute in lobbies
383    /// with the provided value. Lobbies matching this criterion will be included in the result.
384    ///
385    /// # Arguments
386    ///
387    /// * `key`: The attribute key to compare.
388    /// * `value`: The value to compare against.
389    ///
390    pub fn add_request_lobby_list_string_filter(
391        &self,
392        StringFilter(LobbyKey(key), value, kind): StringFilter,
393    ) -> &Self {
394        let key = CString::new(key).unwrap();
395        let value = CString::new(value).unwrap();
396        unsafe {
397            sys::SteamAPI_ISteamMatchmaking_AddRequestLobbyListStringFilter(
398                self.mm,
399                key.as_ptr(),
400                value.as_ptr(),
401                kind.into(),
402            );
403        }
404        self
405    }
406    /// Adds a numerical comparison filter to the lobby list request.
407    ///
408    /// This method adds a filter that compares a specific numerical attribute in lobbies
409    /// with the provided value. Lobbies matching this criterion will be included in the result.
410    ///
411    /// # Arguments
412    ///
413    /// * `key`: The attribute key to compare.
414    /// * `value`: The value to compare against.
415    ///
416    pub fn add_request_lobby_list_numerical_filter(
417        &self,
418        NumberFilter(LobbyKey(key), value, comparison): NumberFilter,
419    ) -> &Self {
420        let key = CString::new(key).unwrap();
421        unsafe {
422            sys::SteamAPI_ISteamMatchmaking_AddRequestLobbyListNumericalFilter(
423                self.mm,
424                key.as_ptr(),
425                value,
426                comparison.into(),
427            );
428        }
429        self
430    }
431    /// Adds a near value filter to the lobby list request.
432    ///
433    /// This method adds a filter that sorts the lobby results based on their closeness
434    /// to a specific value. No actual filtering is performed; lobbies are sorted based on proximity.
435    ///
436    /// # Arguments
437    ///
438    /// * `key`: The attribute key to use for sorting.
439    /// * `value`: The reference value for sorting.
440    ///
441    pub fn add_request_lobby_list_near_value_filter(
442        &self,
443        NearFilter(LobbyKey(key), value): NearFilter,
444    ) -> &Self {
445        let key = CString::new(key).unwrap();
446        unsafe {
447            sys::SteamAPI_ISteamMatchmaking_AddRequestLobbyListNearValueFilter(
448                self.mm,
449                key.as_ptr(),
450                value,
451            );
452        }
453        self
454    }
455    /// Adds a filter for available open slots to the lobby list request.
456    ///
457    /// This method adds a filter that includes lobbies having a specific number of open slots.
458    ///
459    /// # Arguments
460    ///
461    /// * `open_slots`: The number of open slots in a lobby to filter by.
462    ///
463    pub fn set_request_lobby_list_slots_available_filter(&self, open_slots: u8) -> &Self {
464        unsafe {
465            sys::SteamAPI_ISteamMatchmaking_AddRequestLobbyListFilterSlotsAvailable(
466                self.mm,
467                open_slots as i32,
468            );
469        }
470        self
471    }
472    /// Adds a distance filter to the lobby list request.
473    ///
474    /// This method adds a filter that includes lobbies within a certain distance criterion.
475    ///
476    /// # Arguments
477    ///
478    /// * `distance`: The `DistanceFilter` indicating the distance criterion for the filter.
479    ///
480    pub fn set_request_lobby_list_distance_filter(&self, distance: DistanceFilter) -> &Self {
481        unsafe {
482            sys::SteamAPI_ISteamMatchmaking_AddRequestLobbyListDistanceFilter(
483                self.mm,
484                distance.into(),
485            );
486        }
487        self
488    }
489    /// Adds a result count filter to the lobby list request.
490    ///
491    /// This method adds a filter to limit the number of lobby results returned by the request.
492    ///
493    /// # Arguments
494    ///
495    /// * `count`: The maximum number of lobby results to include in the response.
496    ///
497    pub fn set_request_lobby_list_result_count_filter(&self, count: u64) -> &Self {
498        unsafe {
499            sys::SteamAPI_ISteamMatchmaking_AddRequestLobbyListResultCountFilter(
500                self.mm,
501                count as i32,
502            );
503        }
504        self
505    }
506
507    /// Sets filters for the lobbies to be returned from [`request_lobby_list`].
508    ///
509    /// This method is used to apply various filters to the lobby list retrieval process.
510    /// Call this method before calling `request_lobby_list` to ensure that the specified filters
511    /// are taken into account when fetching the list of available lobbies.
512    ///
513    /// # Arguments
514    ///
515    /// * `filter`: A [`LobbyListFilter`] struct containing the filter criteria to be applied.
516    ///
517    /// [`request_lobby_list`]: #method.request_lobby_list
518    /// [`LobbyListFilter`]: struct.LobbyListFilter.html
519    ///
520    /// # Example
521    ///
522    /// ```no_run
523    /// # use steamworks::*;
524    /// fn main() {
525    ///     let client = Client::init().unwrap();
526    ///     client.matchmaking().set_lobby_list_filter(
527    ///         LobbyListFilter {
528    ///             string: Some(vec![
529    ///                 StringFilter(
530    ///                     LobbyKey::new("name"), "My Lobby", StringFilterKind::Equal
531    ///                 ),
532    ///                 StringFilter(
533    ///                     LobbyKey::new("gamemode"), "ffa", StringFilterKind::Equal
534    ///                 ),
535    ///             ]),
536    ///             number: Some(vec![
537    ///                 NumberFilter(LobbyKey::new("elo"), 1500, ComparisonFilter::GreaterThan),
538    ///                 NumberFilter(LobbyKey::new("elo"), 2000, ComparisonFilter::LessThan)
539    ///             ]),
540    ///             ..Default::default()
541    ///         }
542    ///     ).request_lobby_list(|lobbies| {
543    ///         println!("Lobbies: {:?}", lobbies);
544    ///     });
545    /// }
546    /// ```
547    pub fn set_lobby_list_filter(&self, filter: LobbyListFilter<'_>) -> &Self {
548        filter.string.into_iter().flatten().for_each(|str_filter| {
549            self.add_request_lobby_list_string_filter(str_filter);
550        });
551        filter.number.into_iter().flatten().for_each(|num_filter| {
552            self.add_request_lobby_list_numerical_filter(num_filter);
553        });
554        filter
555            .near_value
556            .into_iter()
557            .flatten()
558            .for_each(|near_filter| {
559                self.add_request_lobby_list_near_value_filter(near_filter);
560            });
561        if let Some(distance) = filter.distance {
562            self.set_request_lobby_list_distance_filter(distance);
563        }
564        if let Some(open_slots) = filter.open_slots {
565            self.set_request_lobby_list_slots_available_filter(open_slots);
566        }
567        if let Some(count) = filter.count {
568            self.set_request_lobby_list_result_count_filter(count);
569        }
570        self
571    }
572
573    /// Sets the game server associated with the lobby.
574    ///
575    /// This is used to tell other lobby members which game server to connect to.
576    ///
577    /// # Parameters
578    /// - `lobby`: The lobby ID
579    /// - `server_addr`: The IP address and port of the game server
580    /// - `server_steam_id`: The Steam ID of the game server (optional)
581    ///
582    /// # Returns
583    /// Returns `true` if successful, `false` otherwise
584    pub fn set_lobby_game_server(
585        &self,
586        lobby: LobbyId,
587        server_addr: SocketAddrV4,
588        server_steam_id: Option<SteamId>,
589    ) -> () {
590        unsafe {
591            sys::SteamAPI_ISteamMatchmaking_SetLobbyGameServer(
592                self.mm,
593                lobby.0,
594                server_addr.ip().to_bits(),
595                server_addr.port(),
596                server_steam_id.map(|id| id.0).unwrap_or(0),
597            )
598        }
599    }
600
601    /// Gets the game server associated with the lobby.
602    ///
603    /// # Parameters
604    /// - `lobby`: The lobby ID
605    ///
606    /// # Returns
607    /// Returns `None` if no game server is associated, otherwise returns a tuple containing:
608    /// - `server_addr`: The IP address and port of the game server
609    /// - `server_steam_id`: The Steam ID of the game server (if available)
610    pub fn get_lobby_game_server(&self, lobby: LobbyId) -> Option<(SocketAddrV4, Option<SteamId>)> {
611        unsafe {
612            let mut server_ip = 0;
613            let mut server_port = 0;
614
615            let mut server_steam_id = sys::CSteamID {
616                m_steamid: sys::CSteamID_SteamID_t { m_unAll64Bits: 0 },
617            };
618
619            let success = sys::SteamAPI_ISteamMatchmaking_GetLobbyGameServer(
620                self.mm,
621                lobby.0,
622                &mut server_ip,
623                &mut server_port,
624                &mut server_steam_id,
625            );
626
627            let server_addr = SocketAddrV4::new(Ipv4Addr::from_bits(server_ip), server_port);
628            let server_id = SteamId::from_raw(server_steam_id.m_steamid.m_unAll64Bits);
629
630            // Return None if no game server is associated with the lobby
631            let server_id = (!server_id.is_invalid()).then_some(server_id);
632
633            if success {
634                Some((server_addr, server_id))
635            } else {
636                None
637            }
638        }
639    }
640}
641
642/// Filters for the lobbies to be returned from `request_lobby_list`.
643///
644/// This struct is designed to be used as part of the filtering process
645/// when calling the [`set_lobby_list_filter`](Matchmaking::set_lobby_list_filter) method.
646///
647/// # Fields
648///
649/// - `string`: A string comparison filter that matches lobby attributes with specific strings.
650/// - `number`: A number comparison filter that matches lobby attributes with specific integer values.
651/// - `near_value`: Specifies a value, and the results will be sorted closest to this value (no actual filtering).
652/// - `open_slots`: Filters lobbies based on the number of open slots they have.
653/// - `distance`: Filters lobbies based on a distance criterion.
654/// - `count`: Specifies the maximum number of lobby results to be returned.
655#[derive(Debug, Clone, Default, PartialEq)]
656#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
657pub struct LobbyListFilter<'a> {
658    /// A string comparison filter that matches lobby attributes with specific strings.
659    //#[cfg_attr(feature = "serde", serde(borrow))]
660    pub string: Option<StringFilters<'a>>,
661    /// A number comparison filter that matches lobby attributes with specific integer values
662    #[cfg_attr(feature = "serde", serde(borrow))]
663    pub number: Option<NumberFilters<'a>>,
664    /// Specifies a value, and the results will be sorted closest to this value (no actual filtering)
665    #[cfg_attr(feature = "serde", serde(borrow))]
666    pub near_value: Option<NearFilters<'a>>,
667    /// Filters lobbies based on the number of open slots they have
668    pub open_slots: Option<u8>,
669    /// Filters lobbies based on a distance criterion
670    pub distance: Option<DistanceFilter>,
671    /// Specifies the maximum number of lobby results to be returned
672    pub count: Option<u64>,
673}
674
675/// A wrapper for a lobby key string.
676///
677/// This struct provides a wrapper for a lobby key string. It is used to validate
678/// constructed keys and to ensure that they do not exceed the maximum allowed length.
679#[derive(Debug, Default, Clone, Copy, PartialEq)]
680#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
681pub struct LobbyKey<'a>(pub(crate) &'a str);
682
683impl<'a> std::ops::Deref for LobbyKey<'a> {
684    type Target = &'a str;
685
686    fn deref(&self) -> &Self::Target {
687        &self.0
688    }
689}
690
691#[derive(Debug, Clone, Copy, PartialEq, Default, Error)]
692#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
693pub struct LobbyKeyTooLongError;
694
695impl Display for LobbyKeyTooLongError {
696    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
697        write!(
698            f,
699            "Lobby key is greater than {} characters",
700            sys::k_nMaxLobbyKeyLength
701        )
702    }
703}
704
705impl<'a> LobbyKey<'a> {
706    /// Attempts to create a new `LobbyKey` from a provided string key.
707    ///
708    /// # Arguments
709    ///
710    /// * `key`: The string key to create a `LobbyKey` from.
711    ///
712    /// # Errors
713    ///
714    /// This function will return an error of type [`LobbyKeyTooLongError`] if the provided key's length
715    /// exceeds k_nMaxLobbyKeyLength (255 characters).
716    pub fn try_new(key: &'a str) -> Result<Self, LobbyKeyTooLongError> {
717        if key.len() > sys::k_nMaxLobbyKeyLength as usize {
718            Err(LobbyKeyTooLongError)
719        } else {
720            Ok(LobbyKey(key))
721        }
722    }
723    /// Creates a new `LobbyKey` from a provided string key.
724    ///
725    /// # Arguments
726    ///
727    /// * `key`: The string key to create a `LobbyKey` from.
728    ///
729    /// # Panics
730    ///
731    /// This function will panic if the provided key's length exceeds 255 characters.
732    pub fn new(key: &'a str) -> Self {
733        Self::try_new(key).unwrap()
734    }
735}
736
737pub type StringFilters<'a> = Vec<StringFilter<'a>>;
738pub type NumberFilters<'a> = Vec<NumberFilter<'a>>;
739pub type NearFilters<'a> = Vec<NearFilter<'a>>;
740
741/// A filter used for string based key value comparisons.
742///
743/// # Fields
744///
745/// * `0`: The attribute key for comparison.
746/// * `1`: The target string value for matching.
747#[derive(Debug, Default, Clone, Copy, PartialEq)]
748#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
749pub struct StringFilter<'a>(
750    #[cfg_attr(feature = "serde", serde(borrow))] pub LobbyKey<'a>,
751    pub &'a str,
752    pub StringFilterKind,
753);
754
755#[derive(Debug, Default, Clone, Copy, PartialEq)]
756#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
757pub enum StringFilterKind {
758    #[default]
759    EqualToOrLessThan,
760    LessThan,
761    Equal,
762    GreaterThan,
763    EqualToOrGreaterThan,
764    NotEqual,
765}
766
767impl From<StringFilterKind> for sys::ELobbyComparison {
768    fn from(filter: StringFilterKind) -> Self {
769        match filter {
770            StringFilterKind::EqualToOrLessThan => {
771                sys::ELobbyComparison::k_ELobbyComparisonEqualToOrLessThan
772            }
773            StringFilterKind::LessThan => sys::ELobbyComparison::k_ELobbyComparisonLessThan,
774            StringFilterKind::Equal => sys::ELobbyComparison::k_ELobbyComparisonEqual,
775            StringFilterKind::GreaterThan => sys::ELobbyComparison::k_ELobbyComparisonGreaterThan,
776            StringFilterKind::EqualToOrGreaterThan => {
777                sys::ELobbyComparison::k_ELobbyComparisonEqualToOrGreaterThan
778            }
779            StringFilterKind::NotEqual => sys::ELobbyComparison::k_ELobbyComparisonNotEqual,
780        }
781    }
782}
783
784/// A filter used for numerical attribute comparison in lobby filtering.
785///
786/// # Fields
787///
788/// * `key`: The attribute key for comparison.
789/// * `value`: The target numerical value for matching.
790/// * `comparison`: The comparison mode indicating how the numerical values should be compared.
791///
792/// # Example
793///
794/// ```no_run
795/// # use steamworks::*;
796/// let elo_filter = NumberFilter(
797///     LobbyKey::new("lobby_elo"),
798///     1500,
799///     ComparisonFilter::GreaterThan,
800/// );
801/// ```
802///
803#[derive(Debug, Default, Clone, Copy, PartialEq)]
804#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
805pub struct NumberFilter<'a>(
806    #[cfg_attr(feature = "serde", serde(borrow))] pub LobbyKey<'a>,
807    pub i32,
808    pub ComparisonFilter,
809);
810
811/// A filter used for near-value sorting in lobby filtering.
812///
813/// This struct enables sorting the lobby results based on their closeness to a reference value.
814/// It includes two fields: the attribute key to use for sorting and the reference numerical value.
815///
816/// This filter does not perform actual filtering but rather sorts the results based on proximity.
817///
818/// # Fields
819///
820/// * `0`: The attribute key to use for sorting.
821/// * `1`: The reference numerical value used for sorting proximity.
822#[derive(Debug, Default, Clone, Copy, PartialEq)]
823#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
824pub struct NearFilter<'a>(
825    #[cfg_attr(feature = "serde", serde(borrow))] pub LobbyKey<'a>,
826    pub i32,
827);
828
829impl<'a> LobbyListFilter<'a> {
830    /// Sets the string comparison filter for the lobby list filter.
831    ///
832    /// # Arguments
833    ///
834    /// * `string`: A tuple containing the attribute name and the target string value to match.
835    ///
836    pub fn set_string(mut self, string: Option<StringFilters<'a>>) -> Self {
837        self.string = string;
838        self
839    }
840
841    /// Sets the number comparison filter for the lobby list filter.
842    ///
843    /// # Arguments
844    ///
845    /// * `number`: A tuple containing the attribute name and the target integer value to match.
846    ///
847    pub fn set_number(mut self, number: Option<NumberFilters<'a>>) -> Self {
848        self.number = number;
849        self
850    }
851
852    /// Sets the near value filter for the lobby list filter.
853    ///
854    /// # Arguments
855    ///
856    /// * `near_value`: A tuple containing the attribute name and the reference integer value.
857    ///                 Lobby results will be sorted based on their closeness to this value.
858    ///
859    pub fn set_near_value(mut self, near_value: Option<NearFilters<'a>>) -> Self {
860        self.near_value = near_value;
861        self
862    }
863
864    /// Sets the open slots filter for the lobby list filter.
865    ///
866    /// # Arguments
867    ///
868    /// * `open_slots`: The number of open slots to filter lobbies by.
869    ///
870    pub fn set_open_slots(mut self, open_slots: Option<u8>) -> Self {
871        self.open_slots = open_slots;
872        self
873    }
874
875    /// Sets the distance filter for the lobby list filter.
876    ///
877    /// # Arguments
878    ///
879    /// * `distance`: A distance filter that specifies a distance criterion for filtering lobbies.
880    ///
881    pub fn set_distance(mut self, distance: Option<DistanceFilter>) -> Self {
882        self.distance = distance;
883        self
884    }
885
886    /// Sets the maximum number of lobby results to be returned.
887    ///
888    /// # Arguments
889    ///
890    /// * `count`: The maximum number of lobby results to retrieve.
891    ///
892    pub fn set_count(mut self, count: Option<u64>) -> Self {
893        self.count = count;
894        self
895    }
896}
897
898#[derive(Debug, Clone, Copy, PartialEq, Default)]
899#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
900pub enum DistanceFilter {
901    Close,
902    #[default]
903    Default,
904    Far,
905    Worldwide,
906}
907
908impl From<DistanceFilter> for sys::ELobbyDistanceFilter {
909    fn from(filter: DistanceFilter) -> Self {
910        match filter {
911            DistanceFilter::Close => sys::ELobbyDistanceFilter::k_ELobbyDistanceFilterClose,
912            DistanceFilter::Default => sys::ELobbyDistanceFilter::k_ELobbyDistanceFilterDefault,
913            DistanceFilter::Far => sys::ELobbyDistanceFilter::k_ELobbyDistanceFilterFar,
914            DistanceFilter::Worldwide => sys::ELobbyDistanceFilter::k_ELobbyDistanceFilterWorldwide,
915        }
916    }
917}
918
919#[derive(Debug, Clone, Copy, PartialEq, Default)]
920#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
921pub enum ComparisonFilter {
922    #[default]
923    Equal,
924    NotEqual,
925    GreaterThan,
926    GreaterThanEqualTo,
927    LessThan,
928    LessThanEqualTo,
929}
930
931impl From<ComparisonFilter> for sys::ELobbyComparison {
932    fn from(filter: ComparisonFilter) -> Self {
933        match filter {
934            ComparisonFilter::Equal => sys::ELobbyComparison::k_ELobbyComparisonEqual,
935            ComparisonFilter::NotEqual => sys::ELobbyComparison::k_ELobbyComparisonNotEqual,
936            ComparisonFilter::GreaterThan => sys::ELobbyComparison::k_ELobbyComparisonGreaterThan,
937            ComparisonFilter::GreaterThanEqualTo => {
938                sys::ELobbyComparison::k_ELobbyComparisonEqualToOrGreaterThan
939            }
940            ComparisonFilter::LessThan => sys::ELobbyComparison::k_ELobbyComparisonLessThan,
941            ComparisonFilter::LessThanEqualTo => {
942                sys::ELobbyComparison::k_ELobbyComparisonEqualToOrLessThan
943            }
944        }
945    }
946}
947
948/// Flags describing how a users lobby state has changed. This is provided from `LobbyChatUpdate`.
949#[derive(Clone, Debug, PartialEq)]
950#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
951pub enum ChatMemberStateChange {
952    /// This user has joined or is joining the lobby.
953    Entered,
954
955    /// This user has left or is leaving the lobby.
956    Left,
957
958    /// User disconnected without leaving the lobby first.
959    Disconnected,
960
961    /// The user has been kicked.
962    Kicked,
963
964    /// The user has been kicked and banned.
965    Banned,
966}
967
968#[derive(Debug, Clone, Copy, PartialEq)]
969#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
970pub enum ChatEntryType {
971    Invalid,
972    ChatMsg,
973    Typing,
974    InviteGame,
975    Emote,
976    LeftConversation,
977    Entered,
978    WasKicked,
979    WasBanned,
980    Disconnected,
981    HistoricalChat,
982    LinkBlocked,
983}
984
985impl From<u8> for ChatEntryType {
986    fn from(value: u8) -> Self {
987        match value {
988            x if x == sys::EChatEntryType::k_EChatEntryTypeInvalid as u8 => ChatEntryType::Invalid,
989            x if x == sys::EChatEntryType::k_EChatEntryTypeChatMsg as u8 => ChatEntryType::ChatMsg,
990            x if x == sys::EChatEntryType::k_EChatEntryTypeTyping as u8 => ChatEntryType::Typing,
991            x if x == sys::EChatEntryType::k_EChatEntryTypeInviteGame as u8 => {
992                ChatEntryType::InviteGame
993            }
994            x if x == sys::EChatEntryType::k_EChatEntryTypeEmote as u8 => ChatEntryType::Emote,
995            x if x == sys::EChatEntryType::k_EChatEntryTypeLeftConversation as u8 => {
996                ChatEntryType::LeftConversation
997            }
998            x if x == sys::EChatEntryType::k_EChatEntryTypeEntered as u8 => ChatEntryType::Entered,
999            x if x == sys::EChatEntryType::k_EChatEntryTypeWasKicked as u8 => {
1000                ChatEntryType::WasKicked
1001            }
1002            x if x == sys::EChatEntryType::k_EChatEntryTypeWasBanned as u8 => {
1003                ChatEntryType::WasBanned
1004            }
1005            x if x == sys::EChatEntryType::k_EChatEntryTypeDisconnected as u8 => {
1006                ChatEntryType::Disconnected
1007            }
1008            x if x == sys::EChatEntryType::k_EChatEntryTypeHistoricalChat as u8 => {
1009                ChatEntryType::HistoricalChat
1010            }
1011            x if x == sys::EChatEntryType::k_EChatEntryTypeLinkBlocked as u8 => {
1012                ChatEntryType::LinkBlocked
1013            }
1014            _ => ChatEntryType::Invalid,
1015        }
1016    }
1017}
1018
1019#[derive(Debug, Clone, Copy, PartialEq)]
1020#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1021pub enum ChatRoomEnterResponse {
1022    Success,
1023    DoesntExist,
1024    NotAllowed,
1025    Full,
1026    Error,
1027    Banned,
1028    Limited,
1029    ClanDisabled,
1030    CommunityBan,
1031    MemberBlockedYou,
1032    YouBlockedMember,
1033    RatelimitExceeded,
1034}
1035
1036impl From<u32> for ChatRoomEnterResponse {
1037    fn from(value: u32) -> Self {
1038        match value {
1039            x if x == sys::EChatRoomEnterResponse::k_EChatRoomEnterResponseSuccess as u32 => {
1040                ChatRoomEnterResponse::Success
1041            }
1042            x if x == sys::EChatRoomEnterResponse::k_EChatRoomEnterResponseDoesntExist as u32 => {
1043                ChatRoomEnterResponse::DoesntExist
1044            }
1045            x if x == sys::EChatRoomEnterResponse::k_EChatRoomEnterResponseNotAllowed as u32 => {
1046                ChatRoomEnterResponse::NotAllowed
1047            }
1048            x if x == sys::EChatRoomEnterResponse::k_EChatRoomEnterResponseFull as u32 => {
1049                ChatRoomEnterResponse::Full
1050            }
1051            x if x == sys::EChatRoomEnterResponse::k_EChatRoomEnterResponseError as u32 => {
1052                ChatRoomEnterResponse::Error
1053            }
1054            x if x == sys::EChatRoomEnterResponse::k_EChatRoomEnterResponseBanned as u32 => {
1055                ChatRoomEnterResponse::Banned
1056            }
1057            x if x == sys::EChatRoomEnterResponse::k_EChatRoomEnterResponseLimited as u32 => {
1058                ChatRoomEnterResponse::Limited
1059            }
1060            x if x == sys::EChatRoomEnterResponse::k_EChatRoomEnterResponseClanDisabled as u32 => {
1061                ChatRoomEnterResponse::ClanDisabled
1062            }
1063            x if x == sys::EChatRoomEnterResponse::k_EChatRoomEnterResponseCommunityBan as u32 => {
1064                ChatRoomEnterResponse::CommunityBan
1065            }
1066            x if x
1067                == sys::EChatRoomEnterResponse::k_EChatRoomEnterResponseMemberBlockedYou as u32 =>
1068            {
1069                ChatRoomEnterResponse::MemberBlockedYou
1070            }
1071            x if x
1072                == sys::EChatRoomEnterResponse::k_EChatRoomEnterResponseYouBlockedMember as u32 =>
1073            {
1074                ChatRoomEnterResponse::YouBlockedMember
1075            }
1076            x if x
1077                == sys::EChatRoomEnterResponse::k_EChatRoomEnterResponseRatelimitExceeded
1078                    as u32 =>
1079            {
1080                ChatRoomEnterResponse::RatelimitExceeded
1081            }
1082            _ => ChatRoomEnterResponse::Error,
1083        }
1084    }
1085}
1086
1087/// A chat (text or binary) message for this lobby has been received. After getting this you must use GetLobbyChatEntry to retrieve the contents of this message.
1088#[derive(Clone, Debug)]
1089#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1090pub struct LobbyChatMsg {
1091    /// The Steam ID of the lobby this message was sent in.
1092    pub lobby: LobbyId,
1093    /// Steam ID of the user who sent this message. Note that it could have been the local user.
1094    pub user: SteamId,
1095    /// Type of message received. This is actually a EChatEntryType.
1096    pub chat_entry_type: ChatEntryType,
1097    /// The index of the chat entry to use with GetLobbyChatEntry, this is not valid outside of the scope of this callback and should never be stored.
1098    pub chat_id: i32,
1099}
1100
1101impl_callback!(cb: LobbyChatMsg_t => LobbyChatMsg {
1102    Self {
1103        lobby: LobbyId(cb.m_ulSteamIDLobby),
1104        user: SteamId(cb.m_ulSteamIDUser),
1105        chat_entry_type: cb.m_eChatEntryType.into(),
1106        chat_id: cb.m_iChatID as i32,
1107    }
1108});
1109
1110/// A lobby chat room state has changed, this is usually sent when a user has joined or left the lobby.
1111#[derive(Clone, Debug)]
1112#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1113pub struct LobbyChatUpdate {
1114    /// The Steam ID of the lobby.
1115    pub lobby: LobbyId,
1116    /// The user who's status in the lobby just changed - can be recipient.
1117    pub user_changed: SteamId,
1118    /// Chat member who made the change. This can be different from `user_changed` if kicking, muting, etc. For example, if one user kicks another from the lobby, this will be set to the id of the user who initiated the kick.
1119    pub making_change: SteamId,
1120    /// "ChatMemberStateChange" values.
1121    pub member_state_change: ChatMemberStateChange,
1122}
1123
1124impl_callback!(cb: LobbyChatUpdate_t => LobbyChatUpdate {
1125    Self {
1126        lobby: LobbyId(cb.m_ulSteamIDLobby),
1127        user_changed: SteamId(cb.m_ulSteamIDUserChanged),
1128        making_change: SteamId(cb.m_ulSteamIDUserChanged),
1129        member_state_change: match cb.m_rgfChatMemberStateChange {
1130            x if x == sys::EChatMemberStateChange::k_EChatMemberStateChangeEntered as u32 => {
1131                ChatMemberStateChange::Entered
1132            }
1133            x if x == sys::EChatMemberStateChange::k_EChatMemberStateChangeLeft as u32 => {
1134                ChatMemberStateChange::Left
1135            }
1136            x if x
1137                == sys::EChatMemberStateChange::k_EChatMemberStateChangeDisconnected as u32 =>
1138            {
1139                ChatMemberStateChange::Disconnected
1140            }
1141            x if x == sys::EChatMemberStateChange::k_EChatMemberStateChangeKicked as u32 => {
1142                ChatMemberStateChange::Kicked
1143            }
1144            x if x == sys::EChatMemberStateChange::k_EChatMemberStateChangeBanned as u32 => {
1145                ChatMemberStateChange::Banned
1146            }
1147            _ => unreachable!(),
1148        },
1149    }
1150});
1151
1152/// Result of our request to create a Lobby. At this point, the lobby has been joined and is ready for use, a LobbyEnter_t callback will also be received (since the local user is joining their own lobby).
1153#[derive(Clone, Debug)]
1154#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1155pub struct LobbyCreated {
1156    /// The result of the operation (EResult). Possible values: k_EResultOK, k_EResultFail, k_EResultTimeout, k_EResultLimitExceeded, k_EResultAccessDenied, k_EResultNoConnection
1157    pub result: u32,
1158    /// The Steam ID of the lobby that was created, 0 if failed.
1159    pub lobby: LobbyId,
1160}
1161
1162impl_callback!(cb: LobbyCreated_t => LobbyCreated {
1163    Self {
1164        result: cb.m_eResult as u32,
1165        lobby: LobbyId(cb.m_ulSteamIDLobby),
1166    }
1167});
1168
1169/// The lobby metadata has changed.
1170/// If m_ulSteamIDMember is a user in the lobby, then use GetLobbyMemberData to access per-user details; otherwise, if m_ulSteamIDMember == m_ulSteamIDLobby, use GetLobbyData to access the lobby metadata.
1171#[derive(Clone, Debug)]
1172#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1173pub struct LobbyDataUpdate {
1174    /// The Steam ID of the Lobby.
1175    pub lobby: LobbyId,
1176    /// Steam ID of either the member whose data changed, or the room itself.
1177    pub member: SteamId,
1178    /// true if the lobby data was successfully changed, otherwise false.
1179    pub success: bool,
1180}
1181
1182impl_callback!(cb: LobbyDataUpdate_t => LobbyDataUpdate {
1183    Self {
1184        lobby: LobbyId(cb.m_ulSteamIDLobby),
1185        member: SteamId(cb.m_ulSteamIDMember),
1186        success: cb.m_bSuccess != 0,
1187    }
1188});
1189
1190/// Recieved upon attempting to enter a lobby. Lobby metadata is available to use immediately after receiving this.
1191#[derive(Clone, Debug)]
1192#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1193pub struct LobbyEnter {
1194    /// The steam ID of the Lobby you have entered.
1195    pub lobby: LobbyId,
1196    /// Unused - Always 0.
1197    pub chat_permissions: u32,
1198    /// If true, then only invited users may join.
1199    pub blocked: bool,
1200    /// This is actually a EChatRoomEnterResponse value. This will be set to k_EChatRoomEnterResponseSuccess if the lobby was successfully joined, otherwise it will be k_EChatRoomEnterResponseError.
1201    pub chat_room_enter_response: ChatRoomEnterResponse,
1202}
1203
1204impl_callback!(cb: LobbyEnter_t => LobbyEnter {
1205    Self {
1206        lobby: LobbyId(cb.m_ulSteamIDLobby),
1207        chat_permissions: cb.m_rgfChatPermissions,
1208        blocked: cb.m_bLocked,
1209        chat_room_enter_response: cb.m_EChatRoomEnterResponse.into(),
1210    }
1211});
1212
1213#[test]
1214#[serial]
1215fn test_lobby() {
1216    let client = Client::init().unwrap();
1217    let mm = client.matchmaking();
1218
1219    mm.request_lobby_list(|v| {
1220        println!("List: {:?}", v);
1221    });
1222
1223    mm.create_lobby(LobbyType::Private, 4, |v| {
1224        println!("Create: {:?}", v);
1225    });
1226
1227    mm.set_lobby_list_filter(LobbyListFilter {
1228        string: Some(vec![StringFilter(
1229            LobbyKey::new("name"),
1230            "My Lobby",
1231            StringFilterKind::Equal,
1232        )]),
1233        ..Default::default()
1234    });
1235
1236    for _ in 0..100 {
1237        client.run_callbacks();
1238        ::std::thread::sleep(::std::time::Duration::from_millis(100));
1239    }
1240}
1241
1242#[test]
1243#[serial]
1244fn test_set_lobby_game_server() {
1245    let client = Client::init().unwrap();
1246    let mm = client.matchmaking();
1247
1248    let client2 = client.clone();
1249    mm.create_lobby(LobbyType::Private, 4, move |v| {
1250        println!("Create: {:?}", v);
1251        let mm2 = client2.matchmaking();
1252
1253        // Test setting and getting game server information
1254        let lobby_id = v.unwrap(); // Example lobby ID, in real test should use actual ID from create_lobby
1255        let test_addr = SocketAddrV4::new(Ipv4Addr::new(192, 168, 1, 1), 27015); // Example IP and port
1256        let test_steam_id = Some(SteamId(76561197960287930)); // Example SteamID
1257
1258        // Set game server information
1259        mm2.set_lobby_game_server(lobby_id, test_addr, test_steam_id);
1260
1261        // Retrieve and verify game server information
1262        if let Some((addr, steam_id)) = mm2.get_lobby_game_server(lobby_id) {
1263            assert_eq!(test_addr, addr, "Server address mismatch");
1264            assert_eq!(steam_id, test_steam_id, "Server SteamID mismatch");
1265            println!("Game server info verified: {addr} {steam_id:?}");
1266        } else {
1267            panic!("Failed to retrieve lobby game server info");
1268        }
1269
1270        // Test case for lobby with no game server set
1271        let empty_lobby = LobbyId(54321);
1272        assert!(
1273            mm2.get_lobby_game_server(empty_lobby).is_none(),
1274            "Expected None for lobby with no game server set"
1275        );
1276    });
1277
1278    for _ in 0..100 {
1279        client.run_callbacks();
1280        ::std::thread::sleep(::std::time::Duration::from_millis(100));
1281    }
1282}