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