steamworks/
matchmaking_servers.rs

1use std::net::Ipv4Addr;
2use std::ptr;
3use std::rc::Rc;
4use std::time::Duration;
5
6use super::*;
7
8macro_rules! matchmaking_servers_callback {
9    (
10        $name:ident;
11        $self:ident;
12        ($($additional_name:ident : $additional_type:ty where $additional_content:block),*);
13        $(
14            $fn_name:ident($clear_after_call:tt): ( $( $fn_arg_name:ident: $cpp_fn_arg:ty => $rust_fn_arg:ty where $normalize:tt ),* )
15        ),*
16    ) => {
17        paste::item! {
18            $(
19                #[allow(unused_variables)]
20                extern "C" fn [<$name:lower _ $fn_name _virtual>]($self: *mut [<$name CallbacksReal>] $(, $fn_arg_name: $cpp_fn_arg)*) {
21                    unsafe {
22                        $(
23                            #[allow(unused_parens)]
24                            let [<$fn_arg_name _norm>]: $rust_fn_arg = $normalize;
25                        )*
26                        // In case of dropping rust_callbacks inside $fn_name
27                        let rc_fn = Rc::clone(&(*(*$self).rust_callbacks).$fn_name);
28                        (*rc_fn)($([<$fn_arg_name _norm>]),*);
29                        $clear_after_call;
30                    }
31                }
32            )*
33
34            pub struct [<$name Callbacks>] {
35                $(
36                    pub (self) $fn_name: Rc<Box<dyn Fn($($rust_fn_arg),*)>>,
37                )*
38                $(
39                    pub (self) $additional_name: $additional_type,
40                )*
41            }
42
43            impl [<$name Callbacks>] {
44                // Arc can also be here, without Box
45                pub fn new($($fn_name: Box<dyn Fn($($rust_fn_arg),*)>),*) -> Self {
46                    Self {
47                        $($fn_name: Rc::new($fn_name),)*
48                        $($additional_name: $additional_content,)*
49                    }
50                }
51            }
52
53            #[repr(C)]
54            struct [<$name CallbacksReal>] {
55                pub vtable: *mut [<$name CallbacksVirtual>],
56                pub rust_callbacks: *mut [<$name Callbacks>],
57            }
58
59            #[repr(C)]
60            struct [<$name CallbacksVirtual>] {
61                $(
62                    pub $fn_name: extern "C" fn(*mut [<$name CallbacksReal>] $(, $cpp_fn_arg)*)
63                ),*
64            }
65
66            unsafe fn [<create_ $name:lower>](rust_callbacks: [<$name Callbacks>]) -> *mut [<$name CallbacksReal>] {
67                let rust_callbacks = Box::into_raw(Box::new(rust_callbacks));
68                let vtable = Box::into_raw(Box::new([<$name CallbacksVirtual>] {
69                    $(
70                        $fn_name: [<$name:lower _ $fn_name _virtual>]
71                    ),*
72                }));
73                let real = Box::into_raw(Box::new([<$name CallbacksReal>] {
74                    vtable,
75                    rust_callbacks,
76                }));
77
78                real
79            }
80
81            unsafe fn [<free_ $name:lower>](real: *mut [<$name CallbacksReal>]) {
82                drop(Box::from_raw((*real).rust_callbacks));
83                drop(Box::from_raw((*real).vtable));
84                drop(Box::from_raw(real));
85            }
86        }
87    };
88}
89
90macro_rules! gen_server_list_fn {
91    (
92        $name:ident, $sys_method:ident
93    ) => {
94        /// # Usage
95        ///
96        /// Request must be released at the end of using. For more details see [`ServerListRequest::release`]
97        ///
98        /// # Arguments
99        ///
100        /// * app_id: The app to request the server list of.
101        /// * filters: An array of filters to only retrieve servers the user cares about.
102        /// A list of the keys & values can be found
103        /// [here](https://partner.steamgames.com/doc/api/ISteamMatchmakingServers#MatchMakingKeyValuePair_t).
104        ///
105        /// # Errors
106        ///
107        /// Every filter's key and value must take 255 bytes or under, otherwise `Err` is returned.
108        pub fn $name<ID: Into<AppId>>(
109            &self,
110            app_id: ID,
111            filters: &HashMap<&str, &str>,
112            callbacks: ServerListCallbacks,
113        ) -> Result<Arc<Mutex<ServerListRequest>>, ()> {
114            let app_id = app_id.into().0;
115            let mut filters = {
116                let mut vec = Vec::with_capacity(filters.len());
117                for i in filters {
118                    let key_bytes = i.0.as_bytes();
119                    let value_bytes = i.1.as_bytes();
120
121                    // Max length is 255, so 256th byte will always be nul-terminator
122                    if key_bytes.len() >= 256 || value_bytes.len() >= 256 {
123                        return Err(());
124                    }
125
126                    let mut key = [0i8; 256];
127                    let mut value = [0i8; 256];
128
129                    unsafe {
130                        key.as_mut_ptr()
131                            .copy_from(key_bytes.as_ptr().cast(), key_bytes.len());
132                        value
133                            .as_mut_ptr()
134                            .copy_from(value_bytes.as_ptr().cast(), value_bytes.len());
135                    }
136
137                    vec.push(sys::MatchMakingKeyValuePair_t {
138                        m_szKey: key,
139                        m_szValue: value,
140                    });
141                }
142                vec.shrink_to_fit();
143
144                vec
145            };
146
147            unsafe {
148                let callbacks = create_serverlist(callbacks);
149
150                let request_arc = ServerListRequest::get(callbacks);
151                let mut request = request_arc.lock().unwrap();
152
153                let handle = sys::$sys_method(
154                    self.mms,
155                    app_id,
156                    &mut filters.as_mut_ptr().cast(),
157                    filters.len().try_into().unwrap(),
158                    callbacks.cast(),
159                );
160
161                request.mms = self.mms;
162                request.real = callbacks;
163                request.h_req = handle;
164
165                drop(request);
166
167                Ok(request_arc)
168            }
169        }
170    };
171}
172
173pub struct GameServerItem {
174    pub appid: u32,
175    pub players: i32,
176    pub do_not_refresh: bool,
177    pub successful_response: bool,
178    pub have_password: bool,
179    pub secure: bool,
180    pub bot_players: i32,
181    pub ping: Duration,
182    pub max_players: i32,
183    pub server_version: i32,
184    pub steamid: u64,
185    pub last_time_played: Duration,
186    pub addr: Ipv4Addr,
187    pub query_port: u16,
188    pub connection_port: u16,
189    pub game_description: String,
190    pub server_name: String,
191    pub game_dir: String,
192    pub map: String,
193    pub tags: String,
194}
195
196impl GameServerItem {
197    unsafe fn from_ptr(raw: *const sys::gameserveritem_t) -> Self {
198        let raw = *raw;
199        Self {
200            appid: raw.m_nAppID,
201            players: raw.m_nPlayers,
202            bot_players: raw.m_nBotPlayers,
203            ping: Duration::from_millis(raw.m_nPing.try_into().unwrap()),
204            max_players: raw.m_nMaxPlayers,
205            server_version: raw.m_nServerVersion,
206            steamid: raw.m_steamID.m_steamid.m_unAll64Bits,
207
208            do_not_refresh: raw.m_bDoNotRefresh,
209            successful_response: raw.m_bHadSuccessfulResponse,
210            have_password: raw.m_bPassword,
211            secure: raw.m_bSecure,
212
213            addr: Ipv4Addr::from(raw.m_NetAdr.m_unIP),
214            query_port: raw.m_NetAdr.m_usQueryPort,
215            connection_port: raw.m_NetAdr.m_usConnectionPort,
216
217            game_description: CStr::from_ptr(raw.m_szGameDescription.as_ptr())
218                .to_string_lossy()
219                .into_owned(),
220            server_name: CStr::from_ptr(raw.m_szServerName.as_ptr())
221                .to_string_lossy()
222                .into_owned(),
223            game_dir: CStr::from_ptr(raw.m_szGameDir.as_ptr())
224                .to_string_lossy()
225                .into_owned(),
226            map: CStr::from_ptr(raw.m_szMap.as_ptr())
227                .to_string_lossy()
228                .into_owned(),
229            tags: CStr::from_ptr(raw.m_szGameTags.as_ptr())
230                .to_string_lossy()
231                .into_owned(),
232
233            last_time_played: Duration::from_secs(raw.m_ulTimeLastPlayed.into()),
234        }
235    }
236}
237
238matchmaking_servers_callback!(
239    Ping;
240    _self;
241    ();
242    responded({}): (info: *const sys::gameserveritem_t => GameServerItem where { GameServerItem::from_ptr(info) }),
243    failed({ free_ping(_self) }): ()
244);
245
246matchmaking_servers_callback!(
247    PlayerDetails;
248    _self;
249    ();
250    add_player({}): (
251        name: *const std::os::raw::c_char => &CStr where { CStr::from_ptr(name) },
252        score: i32 => i32 where {score},
253        time_played: f32 => f32 where {time_played}
254    ),
255    failed({ free_playerdetails(_self) }): (),
256    refresh_complete({ free_playerdetails(_self) }): ()
257);
258
259matchmaking_servers_callback!(
260    ServerRules;
261    _self;
262    ();
263    add_rule({}): (
264        rule: *const std::os::raw::c_char => &CStr where { CStr::from_ptr(rule) },
265        value: *const std::os::raw::c_char => &CStr where { CStr::from_ptr(value) }
266    ),
267    failed({ free_serverrules(_self) }): (),
268    refresh_complete({ free_serverrules(_self) }): ()
269);
270
271matchmaking_servers_callback!(
272    ServerList;
273    _self;
274    (
275        req: Arc<Mutex<ServerListRequest>> where {
276            Arc::new(Mutex::new(ServerListRequest {
277                h_req: ptr::null_mut(),
278                released: false,
279                mms: ptr::null_mut(),
280                real: ptr::null_mut(),
281            }))
282        }
283    );
284    responded({}): (
285        request: sys::HServerListRequest => Arc<Mutex<ServerListRequest>> where { ServerListRequest::get(_self) },
286        server: i32 => i32 where {server}
287    ),
288    failed({}): (
289        request: sys::HServerListRequest => Arc<Mutex<ServerListRequest>> where { ServerListRequest::get(_self) },
290        server: i32 => i32 where {server}
291    ),
292    refresh_complete({}): (
293        request: sys::HServerListRequest => Arc<Mutex<ServerListRequest>> where { ServerListRequest::get(_self) },
294        response: ServerResponse => ServerResponse where {response}
295    )
296);
297
298#[repr(u32)]
299#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
300pub enum ServerResponse {
301    ServerResponded = 0,
302    ServerFailedToRespond = 1,
303    NoServersListedOnMasterServer = 2,
304}
305
306#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
307pub enum ReleaseError {
308    /// Further using methods on this request after `release`
309    /// called will always result in `Err(ReleseError::Released)`.
310    Released,
311    /// Due to wrapper limitations releasing request while query
312    /// is still refreshing (`is_refreshing()`) is impossible.
313    /// `Err(ReleaseError::Refreshing)` will be returned.
314    Refreshing,
315}
316
317pub struct ServerListRequest {
318    pub(self) h_req: sys::HServerListRequest,
319    pub(self) released: bool,
320    pub(self) mms: *mut sys::ISteamMatchmakingServers,
321    pub(self) real: *mut ServerListCallbacksReal,
322}
323
324unsafe impl Send for ServerListRequest {}
325
326impl ServerListRequest {
327    pub(self) unsafe fn get(_self: *mut ServerListCallbacksReal) -> Arc<Mutex<Self>> {
328        let rust_callbacks = &*(*_self).rust_callbacks;
329        Arc::clone(&rust_callbacks.req)
330    }
331
332    /// # Usage
333    ///
334    /// Cancels any pending query on it if there's a pending
335    /// query in progress. Releasing all heap allocated
336    /// structures used for callbacks. The `refresh_complete`
337    /// callback will not be posted when request is released.
338    ///
339    /// Further using methods on this request after `release`
340    /// called will always result in `Err(ReleseError::Released)`.
341    ///
342    /// Due to wrapper limitations releasing request while query
343    /// is still refreshing (`is_refreshing()`) is impossible.
344    /// `Err(ReleaseError::Refreshing)` will be returned.
345    pub fn release(&mut self) -> Result<(), ReleaseError> {
346        unsafe {
347            if self.released {
348                return Err(ReleaseError::Released);
349            }
350            if sys::SteamAPI_ISteamMatchmakingServers_IsRefreshing(self.mms, self.h_req) {
351                return Err(ReleaseError::Refreshing);
352            }
353
354            self.released = true;
355            sys::SteamAPI_ISteamMatchmakingServers_ReleaseRequest(self.mms, self.h_req);
356
357            free_serverlist(self.real);
358
359            Ok(())
360        }
361    }
362
363    fn released(&self) -> Result<(), ()> {
364        if self.released {
365            Err(())
366        } else {
367            Ok(())
368        }
369    }
370
371    /// # Errors
372    ///
373    /// Err if called on the released request
374    pub fn get_server_count(&self) -> Result<i32, ()> {
375        unsafe {
376            self.released()?;
377
378            Ok(sys::SteamAPI_ISteamMatchmakingServers_GetServerCount(
379                self.mms, self.h_req,
380            ))
381        }
382    }
383
384    /// # Errors
385    ///
386    /// Err if called on the released request
387    pub fn get_server_details(&self, server: i32) -> Result<GameServerItem, ()> {
388        unsafe {
389            self.released()?;
390
391            // Should we then free this pointer?
392            let server_item = sys::SteamAPI_ISteamMatchmakingServers_GetServerDetails(
393                self.mms, self.h_req, server,
394            );
395
396            Ok(GameServerItem::from_ptr(server_item))
397        }
398    }
399
400    /// # Errors
401    ///
402    /// Err if called on the released request
403    pub fn refresh_query(&self) -> Result<(), ()> {
404        unsafe {
405            self.released()?;
406
407            sys::SteamAPI_ISteamMatchmakingServers_RefreshQuery(self.mms, self.h_req);
408
409            Ok(())
410        }
411    }
412
413    /// # Errors
414    ///
415    /// Err if called on the released request
416    pub fn refresh_server(&self, server: i32) -> Result<(), ()> {
417        unsafe {
418            self.released()?;
419
420            sys::SteamAPI_ISteamMatchmakingServers_RefreshServer(self.mms, self.h_req, server);
421
422            Ok(())
423        }
424    }
425
426    /// # Errors
427    ///
428    /// Err if called on the released request
429    pub fn is_refreshing(&self) -> Result<bool, ()> {
430        unsafe {
431            self.released()?;
432
433            Ok(sys::SteamAPI_ISteamMatchmakingServers_IsRefreshing(
434                self.mms, self.h_req,
435            ))
436        }
437    }
438}
439
440/// Access to the steam MatchmakingServers interface
441pub struct MatchmakingServers {
442    pub(crate) mms: *mut sys::ISteamMatchmakingServers,
443    pub(crate) _inner: Arc<Inner>,
444}
445
446impl MatchmakingServers {
447    pub fn ping_server(&self, ip: std::net::Ipv4Addr, port: u16, callbacks: PingCallbacks) {
448        unsafe {
449            let callbacks = create_ping(callbacks);
450
451            sys::SteamAPI_ISteamMatchmakingServers_PingServer(
452                self.mms,
453                ip.into(),
454                port,
455                callbacks.cast(),
456            );
457        }
458    }
459
460    pub fn player_details(
461        &self,
462        ip: std::net::Ipv4Addr,
463        port: u16,
464        callbacks: PlayerDetailsCallbacks,
465    ) {
466        unsafe {
467            let callbacks = create_playerdetails(callbacks);
468
469            sys::SteamAPI_ISteamMatchmakingServers_PlayerDetails(
470                self.mms,
471                ip.into(),
472                port,
473                callbacks.cast(),
474            );
475        }
476    }
477
478    pub fn server_rules(&self, ip: std::net::Ipv4Addr, port: u16, callbacks: ServerRulesCallbacks) {
479        unsafe {
480            let callbacks = create_serverrules(callbacks);
481
482            sys::SteamAPI_ISteamMatchmakingServers_ServerRules(
483                self.mms,
484                ip.into(),
485                port,
486                callbacks.cast(),
487            );
488        }
489    }
490
491    /// # Usage
492    ///
493    /// Request must be released at the end of using. For more details see [`ServerListRequest::release`]
494    ///
495    /// # Arguments
496    ///
497    /// * app_id: The app to request the server list of.
498    pub fn lan_server_list<ID: Into<AppId>>(
499        &self,
500        app_id: ID,
501        callbacks: ServerListCallbacks,
502    ) -> Arc<Mutex<ServerListRequest>> {
503        unsafe {
504            let app_id = app_id.into().0;
505
506            let callbacks = create_serverlist(callbacks);
507
508            let request_arc = ServerListRequest::get(callbacks);
509            let mut request = request_arc.lock().unwrap();
510
511            let handle = sys::SteamAPI_ISteamMatchmakingServers_RequestLANServerList(
512                self.mms,
513                app_id,
514                callbacks.cast(),
515            );
516
517            request.mms = self.mms;
518            request.real = callbacks;
519            request.h_req = handle;
520
521            drop(request);
522
523            request_arc
524        }
525    }
526
527    gen_server_list_fn!(
528        internet_server_list,
529        SteamAPI_ISteamMatchmakingServers_RequestInternetServerList
530    );
531    gen_server_list_fn!(
532        favorites_server_list,
533        SteamAPI_ISteamMatchmakingServers_RequestFavoritesServerList
534    );
535    gen_server_list_fn!(
536        history_server_list,
537        SteamAPI_ISteamMatchmakingServers_RequestHistoryServerList
538    );
539    gen_server_list_fn!(
540        friends_server_list,
541        SteamAPI_ISteamMatchmakingServers_RequestFriendsServerList
542    );
543}
544
545#[test]
546#[serial_test::serial]
547fn test_internet_servers() {
548    let client = Client::init_app(304930).unwrap();
549
550    let data = std::rc::Rc::new(Mutex::new(0));
551    let data2 = std::rc::Rc::clone(&data);
552    let data3 = std::rc::Rc::clone(&data);
553    let callbacks = ServerListCallbacks::new(
554        Box::new(move |list, server| {
555            let details = list.lock().unwrap().get_server_details(server).unwrap();
556            println!("{} : {}", details.server_name, details.map);
557            *data.lock().unwrap() += 1;
558        }),
559        Box::new(move |_list, _server| {
560            *data2.lock().unwrap() += 1;
561        }),
562        Box::new(move |list, _response| {
563            list.lock().unwrap().release().unwrap();
564            println!("{}", data3.lock().unwrap());
565        }),
566    );
567
568    let mut map = HashMap::new();
569    map.insert("map", "PEI");
570    let _ = client
571        .matchmaking_servers()
572        .internet_server_list(304930, &map, callbacks)
573        .unwrap();
574
575    for _ in 0..2000 {
576        client.run_callbacks();
577        std::thread::sleep(std::time::Duration::from_millis(10));
578    }
579}