Skip to main content

steam_client/services/
gameservers.rs

1//! Game server queries for Steam client.
2//!
3//! This module provides functionality for querying Steam game servers.
4
5use std::{collections::HashMap, net::Ipv4Addr};
6
7use steamid::SteamID;
8
9use crate::{error::SteamError, SteamClient};
10
11/// Information about a game server.
12#[derive(Debug, Clone)]
13pub struct GameServer {
14    /// Server IP address.
15    pub ip: String,
16    /// Server port.
17    pub port: u16,
18    /// Number of authenticated players.
19    pub players: u32,
20    /// Server SteamID (if available).
21    pub steamid: Option<SteamID>,
22    /// Server name.
23    pub name: Option<String>,
24    /// Current map.
25    pub map: Option<String>,
26    /// Game directory.
27    pub gamedir: Option<String>,
28    /// Game description.
29    pub gamedesc: Option<String>,
30    /// App ID.
31    pub appid: Option<u32>,
32    /// Number of bots.
33    pub bots: Option<u32>,
34    /// Max players.
35    pub max_players: Option<u32>,
36    /// Whether password protected.
37    pub password: Option<bool>,
38    /// Whether VAC secured.
39    pub secure: Option<bool>,
40    /// Game version.
41    pub version: Option<String>,
42}
43
44impl SteamClient {
45    /// Query the GMS for a list of game server IPs and their current player
46    /// counts.
47    ///
48    /// # Arguments
49    /// * `filter` - A filter string. See: https://developer.valvesoftware.com/wiki/Master_Server_Query_Protocol#Filter
50    ///
51    /// # Example filters
52    /// - `\appid\730` - CS2 servers
53    /// - `\gametype\competitive` - Competitive servers
54    /// - `\map\de_dust2` - Servers on de_dust2
55    pub async fn server_query(&mut self, filter: &str) -> Result<Vec<GameServer>, SteamError> {
56        if !self.is_logged_in() {
57            return Err(SteamError::NotLoggedOn);
58        }
59
60        let msg = steam_protos::CMsgClientGMSServerQuery { filter_text: Some(filter.to_string()), ..Default::default() };
61
62        let response: steam_protos::CMsgGMSClientServerQueryResponse = self.send_request_and_wait(steam_enums::EMsg::ClientGMSServerQuery, &msg).await?;
63
64        if let Some(error) = response.error {
65            return Err(SteamError::Other(error));
66        }
67
68        let mut servers = Vec::new();
69        for server in response.servers {
70            let ip_str = if let Some(ip_addr) = server.server_ip {
71                if let Some(steam_protos::cmsg_ip_address::Ip::V4(v4)) = ip_addr.ip {
72                    Ipv4Addr::from(v4).to_string()
73                } else {
74                    continue; // Skip IPv6 for now as per Node implementation
75                }
76            } else {
77                continue;
78            };
79
80            servers.push(GameServer {
81                ip: ip_str,
82                port: server.query_port.unwrap_or(0) as u16,
83                players: server.auth_players.unwrap_or(0),
84                steamid: None,
85                name: None,
86                map: None,
87                gamedir: None,
88                gamedesc: None,
89                appid: None,
90                bots: None,
91                max_players: None,
92                password: None,
93                secure: None,
94                version: None,
95            });
96        }
97
98        Ok(servers)
99    }
100
101    /// Get a list of servers including full game data.
102    ///
103    /// # Arguments
104    /// * `filter` - A filter string
105    /// * `limit` - Maximum number of servers to return (max ~20,000)
106    pub async fn get_server_list(&mut self, filter: &str, limit: u32) -> Result<Vec<GameServer>, SteamError> {
107        if !self.is_logged_in() {
108            return Err(SteamError::NotLoggedOn);
109        }
110
111        let msg = steam_protos::CGameServersGetServerListRequest { filter: Some(filter.to_string()), limit: Some(limit) };
112
113        let response: steam_protos::CGameServersGetServerListResponse = self.send_service_method_and_wait("GameServers.GetServerList#1", &msg).await?;
114
115        let mut servers = Vec::new();
116        for server in response.servers {
117            let parts: Vec<&str> = server.addr.as_deref().unwrap_or("").split(':').collect();
118            let ip = parts.first().unwrap_or(&"").to_string();
119            let port = parts.get(1).and_then(|p| p.parse().ok()).unwrap_or(0);
120
121            servers.push(GameServer {
122                ip,
123                port,
124                players: server.players.unwrap_or(0) as u32,
125                steamid: server.steamid.map(SteamID::from),
126                name: server.name,
127                map: server.map,
128                gamedir: server.gamedir,
129                gamedesc: server.gametype, // Using gametype as gamedesc based on proto definition
130                appid: server.appid,
131                bots: server.bots.map(|b| b as u32),
132                max_players: server.max_players.map(|p| p as u32),
133                password: None, // Not directly in response? Node implementation doesn't map it explicitly
134                secure: server.secure,
135                version: server.version,
136            });
137        }
138
139        Ok(servers)
140    }
141
142    /// Get the associated SteamIDs for given server IPs.
143    ///
144    /// # Arguments
145    /// * `ips` - List of server IP:port strings (e.g., "192.168.1.1:27015")
146    pub async fn get_server_steam_ids_by_ip(&mut self, ips: Vec<String>) -> Result<HashMap<String, SteamID>, SteamError> {
147        if !self.is_logged_in() {
148            return Err(SteamError::NotLoggedOn);
149        }
150
151        let msg = steam_protos::CGameServersGetServerSteamIDsByIPRequest { server_ips: ips };
152
153        let response: steam_protos::CGameServersIPsWithSteamIDsResponse = self.send_service_method_and_wait("GameServers.GetServerSteamIDsByIP#1", &msg).await?;
154
155        let mut result = HashMap::new();
156        for server in response.servers {
157            if let (Some(addr), Some(steamid)) = (server.addr, server.steamid) {
158                result.insert(addr, SteamID::from(steamid));
159            }
160        }
161
162        Ok(result)
163    }
164
165    /// Get the associated IPs for given server SteamIDs.
166    ///
167    /// # Arguments
168    /// * `steamids` - List of server SteamIDs
169    pub async fn get_server_ips_by_steam_id(&mut self, steamids: Vec<SteamID>) -> Result<HashMap<SteamID, String>, SteamError> {
170        if !self.is_logged_in() {
171            return Err(SteamError::NotLoggedOn);
172        }
173
174        let msg = steam_protos::CGameServersGetServerIPsBySteamIDRequest { server_steamids: steamids.iter().map(|s| s.steam_id64()).collect() };
175
176        let response: steam_protos::CGameServersIPsWithSteamIDsResponse = self.send_service_method_and_wait("GameServers.GetServerIPsBySteamID#1", &msg).await?;
177
178        let mut result = HashMap::new();
179        for server in response.servers {
180            if let (Some(addr), Some(steamid)) = (server.addr, server.steamid) {
181                result.insert(SteamID::from(steamid), addr);
182            }
183        }
184
185        Ok(result)
186    }
187}