Skip to main content

steam_client/services/
apps.rs

1//! Apps and games information.
2//!
3//! This module provides functionality for interacting with Steam apps:
4//! - Requesting product info (PICS)
5//! - Setting games as playing
6//! - Getting player counts
7//! - Managing access tokens
8
9use steam_enums::{EAppType, ELicenseFlags};
10
11use crate::{error::SteamError, SteamClient};
12
13/// Special game ID used for non-Steam games in the games_played list.
14/// This value signals to Steam that the game_extra_info field contains
15/// a custom game name rather than an app ID.
16const NON_STEAM_GAME_ID: u64 = 15190414816125648896;
17
18/// App/game information.
19#[derive(Debug, Clone)]
20pub struct AppInfo {
21    /// App ID.
22    pub app_id: u32,
23    /// App name.
24    pub name: String,
25    /// App type.
26    pub app_type: EAppType,
27    /// Developer.
28    pub developer: Option<String>,
29    /// Publisher.
30    pub publisher: Option<String>,
31    /// Icon hash.
32    pub icon_hash: Option<String>,
33    /// Logo hash.
34    pub logo_hash: Option<String>,
35}
36
37/// Package/license information.
38#[derive(Debug, Clone)]
39pub struct PackageInfo {
40    /// Package ID.
41    pub package_id: u32,
42    /// Package name.
43    pub name: Option<String>,
44    /// App IDs included.
45    pub app_ids: Vec<u32>,
46}
47
48/// Owned app license.
49#[derive(Debug, Clone)]
50pub struct OwnedApp {
51    /// App ID.
52    pub app_id: u32,
53    /// Package ID.
54    pub package_id: u32,
55    /// Time purchased.
56    pub time_created: u32,
57    /// License type.
58    pub license_type: u32,
59    /// License flags.
60    pub flags: u32,
61}
62
63/// App info request with optional access token.
64#[derive(Debug, Clone, Default)]
65pub struct AppInfoRequest {
66    /// App ID.
67    pub app_id: u32,
68    /// Access token (if needed for restricted apps).
69    pub access_token: Option<u64>,
70}
71
72impl From<u32> for AppInfoRequest {
73    fn from(app_id: u32) -> Self {
74        Self { app_id, access_token: None }
75    }
76}
77
78/// Package info request with optional access token.
79#[derive(Debug, Clone, Default)]
80pub struct PackageInfoRequest {
81    /// Package ID.
82    pub package_id: u32,
83    /// Access token (if needed for restricted packages).
84    pub access_token: Option<u64>,
85}
86
87impl From<u32> for PackageInfoRequest {
88    fn from(package_id: u32) -> Self {
89        Self { package_id, access_token: None }
90    }
91}
92
93impl SteamClient {
94    /// Get a list of owned apps for the logged-in user.
95    ///
96    /// This information comes from the ClientLicenseList message received
97    /// after login. The licenses property contains package info.
98    pub fn get_owned_apps(&self) -> Vec<OwnedApp> {
99        // This would be populated from ClientLicenseList messages
100        // For now, return empty - would need message handling loop
101        Vec::new()
102    }
103
104    /// Request product/app info for one or more app IDs.
105    ///
106    /// The response will arrive as a `ProductInfoResponse` event.
107    ///
108    /// # Arguments
109    /// * `app_ids` - The app IDs to get info for
110    pub async fn get_product_info(&mut self, app_ids: Vec<u32>) -> Result<(), SteamError> {
111        let requests: Vec<AppInfoRequest> = app_ids.into_iter().map(Into::into).collect();
112        self.get_product_info_with_tokens(requests, Vec::new()).await
113    }
114
115    /// Request product info for apps and packages with optional access tokens.
116    ///
117    /// The response will arrive as a `ProductInfoResponse` event.
118    ///
119    /// # Arguments
120    /// * `apps` - App info requests with optional tokens
121    /// * `packages` - Package info requests with optional tokens
122    pub async fn get_product_info_with_tokens(&mut self, apps: Vec<AppInfoRequest>, packages: Vec<PackageInfoRequest>) -> Result<(), SteamError> {
123        if !self.is_logged_in() {
124            return Err(SteamError::NotLoggedOn);
125        }
126
127        let msg = steam_protos::CMsgClientPICSProductInfoRequest {
128            apps: apps.iter().map(|req| steam_protos::cmsg_client_pics_product_info_request::AppInfo { appid: Some(req.app_id), access_token: req.access_token, only_public_obsolete: None }).collect(),
129            packages: packages.iter().map(|req| steam_protos::cmsg_client_pics_product_info_request::PackageInfo { packageid: Some(req.package_id), access_token: req.access_token }).collect(),
130            meta_data_only: Some(false),
131            ..Default::default()
132        };
133
134        self.send_message(steam_enums::EMsg::ClientPICSProductInfoRequest, &msg).await
135    }
136
137    /// Request package info for one or more package IDs.
138    ///
139    /// The response will arrive as a `ProductInfoResponse` event.
140    ///
141    /// # Arguments
142    /// * `package_ids` - The package IDs to get info for
143    pub async fn get_package_info(&mut self, package_ids: Vec<u32>) -> Result<(), SteamError> {
144        let packages: Vec<PackageInfoRequest> = package_ids.into_iter().map(Into::into).collect();
145        self.get_product_info_with_tokens(Vec::new(), packages).await
146    }
147
148    /// Request access tokens for app and/or package IDs.
149    ///
150    /// Access tokens are needed to retrieve info for some restricted
151    /// apps/packages. The response will arrive as an `AccessTokensResponse`
152    /// event.
153    ///
154    /// # Arguments
155    /// * `app_ids` - The app IDs to get tokens for
156    /// * `package_ids` - The package IDs to get tokens for
157    pub async fn get_access_tokens(&mut self, app_ids: Vec<u32>, package_ids: Vec<u32>) -> Result<(), SteamError> {
158        if !self.is_logged_in() {
159            return Err(SteamError::NotLoggedOn);
160        }
161
162        let msg = steam_protos::CMsgClientPICSAccessTokenRequest { appids: app_ids, packageids: package_ids };
163
164        self.send_message(steam_enums::EMsg::ClientPICSAccessTokenRequest, &msg).await
165    }
166
167    /// Request access tokens for app IDs (needed for some operations).
168    ///
169    /// # Arguments
170    /// * `app_ids` - The app IDs to get tokens for
171    pub async fn get_product_access_tokens(&mut self, app_ids: Vec<u32>) -> Result<(), SteamError> {
172        self.get_access_tokens(app_ids, Vec::new()).await
173    }
174
175    /// Get a list of apps/packages that have changed since a given change
176    /// number.
177    ///
178    /// The response will arrive as a `ProductChangesResponse` event.
179    /// Use change number 0 to get the current change number without any
180    /// changes.
181    ///
182    /// # Arguments
183    /// * `since_change_number` - Get changes since this change number
184    pub async fn get_product_changes(&mut self, since_change_number: u32) -> Result<(), SteamError> {
185        if !self.is_logged_in() {
186            return Err(SteamError::NotLoggedOn);
187        }
188
189        let msg = steam_protos::CMsgClientPICSChangesSinceRequest {
190            since_change_number: Some(since_change_number),
191            send_app_info_changes: Some(true),
192            send_package_info_changes: Some(true),
193            ..Default::default()
194        };
195
196        self.send_message(steam_enums::EMsg::ClientPICSChangesSinceRequest, &msg).await
197    }
198
199    /// Get the number of players currently playing a game.
200    ///
201    /// Use app ID 0 to get the total number of users connected to Steam.
202    ///
203    /// # Arguments
204    /// * `app_id` - The app ID to get player count for
205    pub async fn get_player_count(&mut self, app_id: u32) -> Result<(), SteamError> {
206        if !self.is_logged_in() {
207            return Err(SteamError::NotLoggedOn);
208        }
209
210        let msg = steam_protos::CMsgDpGetNumberOfCurrentPlayers { appid: Some(app_id) };
211
212        self.send_message(steam_enums::EMsg::ClientGetNumberOfCurrentPlayersDP, &msg).await
213    }
214
215    /// Kick any other session logged into this account that is playing a game.
216    ///
217    /// Use this if you receive a `playingBlocked` event and want to force
218    /// playing on this session.
219    pub async fn kick_playing_session(&mut self) -> Result<(), SteamError> {
220        if !self.is_logged_in() {
221            return Err(SteamError::NotLoggedOn);
222        }
223
224        let msg = steam_protos::CMsgClientKickPlayingSession::default();
225        self.send_message(steam_enums::EMsg::ClientKickPlayingSession, &msg).await
226    }
227
228    /// Set the games currently being played.
229    ///
230    /// # Arguments
231    /// * `app_ids` - Up to 32 app IDs to set as playing (empty to stop)
232    pub async fn games_played(&mut self, app_ids: Vec<u32>) -> Result<(), SteamError> {
233        self.games_played_with_extra(app_ids, None).await
234    }
235
236    /// Set the games currently being played with a custom game name.
237    ///
238    /// # Arguments
239    /// * `app_ids` - Up to 32 app IDs to set as playing (empty to stop)
240    /// * `custom_game` - Optional custom/non-Steam game name
241    pub async fn games_played_with_extra(&mut self, app_ids: Vec<u32>, custom_game: Option<String>) -> Result<(), SteamError> {
242        if !self.is_logged_in() {
243            return Err(SteamError::NotLoggedOn);
244        }
245
246        let mut msg = steam_protos::CMsgClientGamesPlayed::default();
247
248        // Add regular games
249        let mut games: Vec<steam_protos::cmsg_client_games_played::GamePlayed> = app_ids.iter().take(32).map(|&id| steam_protos::cmsg_client_games_played::GamePlayed { game_id: Some(id as u64), ..Default::default() }).collect();
250
251        // Add custom game if specified
252        if let Some(ref name) = custom_game {
253            // Non-Steam game uses a special game ID
254            games.push(steam_protos::cmsg_client_games_played::GamePlayed { game_id: Some(NON_STEAM_GAME_ID), game_extra_info: Some(name.clone()), ..Default::default() });
255        }
256
257        msg.games_played = games;
258
259        // Record for session recovery
260        self.session_recovery.record_playing(app_ids, custom_game);
261
262        self.send_message(steam_enums::EMsg::ClientGamesPlayedWithDataBlob, &msg).await
263    }
264
265    /// Redeem a product code on this account.
266    ///
267    /// # Arguments
268    /// * `key` - The product code to redeem
269    pub async fn redeem_key(&mut self, key: String) -> Result<(), SteamError> {
270        if !self.is_logged_in() {
271            return Err(SteamError::NotLoggedOn);
272        }
273
274        let msg = steam_protos::CMsgClientRegisterKey { key: Some(key) };
275
276        self.send_message(steam_enums::EMsg::ClientRegisterKey, &msg).await
277    }
278
279    /// Request licenses for one or more free-on-demand apps.
280    ///
281    /// # Arguments
282    /// * `app_ids` - The app IDs to request licenses for
283    pub async fn request_free_license(&mut self, app_ids: Vec<u32>) -> Result<steam_protos::CMsgClientRequestFreeLicenseResponse, SteamError> {
284        if !self.is_logged_in() {
285            return Err(SteamError::NotLoggedOn);
286        }
287
288        let msg = steam_protos::CMsgClientRequestFreeLicense { app_ids };
289
290        self.send_request_and_wait(steam_enums::EMsg::ClientRequestFreeLicense, &msg).await
291    }
292
293    /// Automatically requests any missing licenses from a given list ("free
294    /// apps").
295    ///
296    /// # Arguments
297    /// * `free_app_list` - List of all possibly free app IDs.
298    /// * `max_limit` - Maximum number to request in one go.
299    pub async fn auto_request_free_license(&mut self, free_app_list: Vec<u32>, max_limit: usize) -> Result<steam_protos::CMsgClientRequestFreeLicenseResponse, SteamError> {
300        use rand::seq::SliceRandom;
301
302        if !self.is_logged_in() {
303            return Err(SteamError::NotLoggedOn);
304        }
305
306        // Filter out apps we already own (assuming AppID == PackageID for free apps
307        // check or we just check if we have a package with that ID)
308        let mut needed_apps: Vec<u32> = free_app_list.into_iter().filter(|&app_id| !self.owns_package(app_id)).collect();
309
310        // Shuffle to avoid hammering the same apps if we are rate limited or doing
311        // partial batches
312        let mut rng = rand::rng();
313        needed_apps.shuffle(&mut rng);
314
315        // Limit the number of apps to request
316        let request_apps: Vec<u32> = needed_apps.into_iter().take(max_limit).collect();
317
318        if request_apps.is_empty() {
319            // Return empty response if nothing to request
320            return Ok(steam_protos::CMsgClientRequestFreeLicenseResponse { eresult: Some(steam_enums::EResult::OK as u32), granted_packageids: vec![], granted_appids: vec![] });
321        }
322
323        self.request_free_license(request_apps).await
324    }
325
326    /// Get a legacy CD key for a game.
327    ///
328    /// # Arguments
329    /// * `app_id` - The app ID to get the key for
330    pub async fn get_legacy_game_key(&mut self, app_id: u32) -> Result<(), SteamError> {
331        if !self.is_logged_in() {
332            return Err(SteamError::NotLoggedOn);
333        }
334
335        let msg = steam_protos::CMsgClientGetLegacyGameKey { app_id: Some(app_id) };
336
337        self.send_message(steam_enums::EMsg::ClientGetLegacyGameKey, &msg).await
338    }
339
340    /// Get a list of package IDs owned by the user.
341    ///
342    /// This filters out expired licenses.
343    pub fn get_owned_packages(&self) -> Vec<u32> {
344        self.apps.read().licenses.iter().filter(|l| (l.flags & ELicenseFlags::Expired as u32) == 0).map(|l| l.package_id).collect()
345    }
346
347    /// Check if the user owns a specific package.
348    pub fn owns_package(&self, package_id: u32) -> bool {
349        self.get_owned_packages().contains(&package_id)
350    }
351}