1use crate::Client;
2use crate::callbacks::PersonaStateChangeFlags;
3use enum_primitive_derive::Primitive;
4use futures::{Future, StreamExt};
5use num_traits::FromPrimitive;
6use std::cmp::Ordering;
7use std::ffi::CStr;
8use std::fmt::{self, Debug, Display, Formatter};
9use std::hash::{Hash, Hasher};
10use steamworks_sys as sys;
11use steamworks_sys::CSteamID;
12
13#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)]
14pub struct AppId(pub u32);
15
16impl AppId {
17 pub fn new(id: u32) -> Self {
18 AppId(id)
19 }
20}
21
22impl From<u32> for AppId {
23 fn from(x: u32) -> AppId {
24 AppId(x)
25 }
26}
27
28impl From<AppId> for u32 {
29 fn from(x: AppId) -> u32 {
30 x.0
31 }
32}
33
34#[derive(Copy, Clone)]
35pub struct SteamId(pub(crate) u64);
36
37impl SteamId {
38 pub fn new(id: u64) -> Self {
39 id.into()
40 }
41
42 pub fn persona_name(self, client: &Client) -> impl Future<Output = String> + Send + '_ {
43 let mut persona_state_changes = client.on_persona_state_changed();
44 let request_in_progress = unsafe {
45 sys::SteamAPI_ISteamFriends_RequestUserInformation(*client.0.friends, self.0, true)
46 };
47 async move {
48 if request_in_progress {
49 loop {
50 let change = persona_state_changes.next().await.unwrap();
51 if change.steam_id == self
52 && change.change_flags.contains(PersonaStateChangeFlags::NAME)
53 {
54 break;
55 }
56 }
57 }
58
59 unsafe {
60 let name =
61 sys::SteamAPI_ISteamFriends_GetFriendPersonaName(*client.0.friends, self.0);
62
63 CStr::from_ptr(name)
64 .to_owned()
65 .into_string()
66 .expect("persona name contained invalid UTF-8")
67 }
68 }
69 }
70
71 pub fn as_u64(self) -> u64 {
72 self.0
73 }
74}
75
76impl From<u64> for SteamId {
77 fn from(inner: u64) -> Self {
78 SteamId(inner)
79 }
80}
81
82impl From<SteamId> for u64 {
83 fn from(steam_id: SteamId) -> Self {
84 steam_id.0
85 }
86}
87
88impl From<CSteamID> for SteamId {
89 fn from(id: CSteamID) -> Self {
90 unsafe { SteamId(id.m_steamid.m_unAll64Bits) }
91 }
92}
93
94impl Debug for SteamId {
95 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
96 f.debug_tuple("SteamId").field(&self.as_u64()).finish()
97 }
98}
99
100impl PartialEq for SteamId {
101 fn eq(&self, other: &SteamId) -> bool {
102 self.as_u64() == other.as_u64()
103 }
104}
105
106impl Eq for SteamId {}
107
108impl Hash for SteamId {
109 fn hash<H: Hasher>(&self, state: &mut H) {
110 self.as_u64().hash(state);
111 }
112}
113
114impl PartialOrd for SteamId {
115 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
116 Some(self.cmp(other))
117 }
118}
119
120impl Ord for SteamId {
121 fn cmp(&self, other: &Self) -> Ordering {
122 self.as_u64().cmp(&other.as_u64())
123 }
124}
125
126#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Ord, PartialOrd, Primitive)]
127#[repr(i32)]
128pub enum SteamResult {
129 OK = sys::EResult_k_EResultOK as i32,
130 Fail = sys::EResult_k_EResultFail as i32,
131 NoConnection = sys::EResult_k_EResultNoConnection as i32,
132 InvalidPassword = sys::EResult_k_EResultInvalidPassword as i32,
133 LoggedInElsewhere = sys::EResult_k_EResultLoggedInElsewhere as i32,
134 InvalidProtocolVer = sys::EResult_k_EResultInvalidProtocolVer as i32,
135 InvalidParam = sys::EResult_k_EResultInvalidParam as i32,
136 FileNotFound = sys::EResult_k_EResultFileNotFound as i32,
137 Busy = sys::EResult_k_EResultBusy as i32,
138 InvalidState = sys::EResult_k_EResultInvalidState as i32,
139 InvalidName = sys::EResult_k_EResultInvalidName as i32,
140 InvalidEmail = sys::EResult_k_EResultInvalidEmail as i32,
141 DuplicateName = sys::EResult_k_EResultDuplicateName as i32,
142 AccessDenied = sys::EResult_k_EResultAccessDenied as i32,
143 Timeout = sys::EResult_k_EResultTimeout as i32,
144 Banned = sys::EResult_k_EResultBanned as i32,
145 AccountNotFound = sys::EResult_k_EResultAccountNotFound as i32,
146 InvalidSteamID = sys::EResult_k_EResultInvalidSteamID as i32,
147 ServiceUnavailable = sys::EResult_k_EResultServiceUnavailable as i32,
148 NotLoggedOn = sys::EResult_k_EResultNotLoggedOn as i32,
149 Pending = sys::EResult_k_EResultPending as i32,
150 EncryptionFailure = sys::EResult_k_EResultEncryptionFailure as i32,
151 InsufficientPrivilege = sys::EResult_k_EResultInsufficientPrivilege as i32,
152 LimitExceeded = sys::EResult_k_EResultLimitExceeded as i32,
153 Revoked = sys::EResult_k_EResultRevoked as i32,
154 Expired = sys::EResult_k_EResultExpired as i32,
155 AlreadyRedeemed = sys::EResult_k_EResultAlreadyRedeemed as i32,
156 DuplicateRequest = sys::EResult_k_EResultDuplicateRequest as i32,
157 AlreadyOwned = sys::EResult_k_EResultAlreadyOwned as i32,
158 IPNotFound = sys::EResult_k_EResultIPNotFound as i32,
159 PersistFailed = sys::EResult_k_EResultPersistFailed as i32,
160 LockingFailed = sys::EResult_k_EResultLockingFailed as i32,
161 LogonSessionReplaced = sys::EResult_k_EResultLogonSessionReplaced as i32,
162 ConnectFailed = sys::EResult_k_EResultConnectFailed as i32,
163 HandshakeFailed = sys::EResult_k_EResultHandshakeFailed as i32,
164 IOFailure = sys::EResult_k_EResultIOFailure as i32,
165 RemoteDisconnect = sys::EResult_k_EResultRemoteDisconnect as i32,
166 ShoppingCartNotFound = sys::EResult_k_EResultShoppingCartNotFound as i32,
167 Blocked = sys::EResult_k_EResultBlocked as i32,
168 Ignored = sys::EResult_k_EResultIgnored as i32,
169 NoMatch = sys::EResult_k_EResultNoMatch as i32,
170 AccountDisabled = sys::EResult_k_EResultAccountDisabled as i32,
171 ServiceReadOnly = sys::EResult_k_EResultServiceReadOnly as i32,
172 AccountNotFeatured = sys::EResult_k_EResultAccountNotFeatured as i32,
173 AdministratorOK = sys::EResult_k_EResultAdministratorOK as i32,
174 ContentVersion = sys::EResult_k_EResultContentVersion as i32,
175 TryAnotherCM = sys::EResult_k_EResultTryAnotherCM as i32,
176 PasswordRequiredToKickSession = sys::EResult_k_EResultPasswordRequiredToKickSession as i32,
177 AlreadyLoggedInElsewhere = sys::EResult_k_EResultAlreadyLoggedInElsewhere as i32,
178 Suspended = sys::EResult_k_EResultSuspended as i32,
179 Cancelled = sys::EResult_k_EResultCancelled as i32,
180 DataCorruption = sys::EResult_k_EResultDataCorruption as i32,
181 DiskFull = sys::EResult_k_EResultDiskFull as i32,
182 RemoteCallFailed = sys::EResult_k_EResultRemoteCallFailed as i32,
183 PasswordUnset = sys::EResult_k_EResultPasswordUnset as i32,
184 ExternalAccountUnlinked = sys::EResult_k_EResultExternalAccountUnlinked as i32,
185 PSNTicketInvalid = sys::EResult_k_EResultPSNTicketInvalid as i32,
186 ExternalAccountAlreadyLinked = sys::EResult_k_EResultExternalAccountAlreadyLinked as i32,
187 RemoteFileConflict = sys::EResult_k_EResultRemoteFileConflict as i32,
188 IllegalPassword = sys::EResult_k_EResultIllegalPassword as i32,
189 SameAsPreviousValue = sys::EResult_k_EResultSameAsPreviousValue as i32,
190 AccountLogonDenied = sys::EResult_k_EResultAccountLogonDenied as i32,
191 CannotUseOldPassword = sys::EResult_k_EResultCannotUseOldPassword as i32,
192 InvalidLoginAuthCode = sys::EResult_k_EResultInvalidLoginAuthCode as i32,
193 AccountLogonDeniedNoMail = sys::EResult_k_EResultAccountLogonDeniedNoMail as i32,
194 HardwareNotCapableOfIPT = sys::EResult_k_EResultHardwareNotCapableOfIPT as i32,
195 IPTInitError = sys::EResult_k_EResultIPTInitError as i32,
196 ParentalControlRestricted = sys::EResult_k_EResultParentalControlRestricted as i32,
197 FacebookQueryError = sys::EResult_k_EResultFacebookQueryError as i32,
198 ExpiredLoginAuthCode = sys::EResult_k_EResultExpiredLoginAuthCode as i32,
199 IPLoginRestrictionFailed = sys::EResult_k_EResultIPLoginRestrictionFailed as i32,
200 AccountLockedDown = sys::EResult_k_EResultAccountLockedDown as i32,
201 AccountLogonDeniedVerifiedEmailRequired =
202 sys::EResult_k_EResultAccountLogonDeniedVerifiedEmailRequired as i32,
203 NoMatchingURL = sys::EResult_k_EResultNoMatchingURL as i32,
204 BadResponse = sys::EResult_k_EResultBadResponse as i32,
205 RequirePasswordReEntry = sys::EResult_k_EResultRequirePasswordReEntry as i32,
206 ValueOutOfRange = sys::EResult_k_EResultValueOutOfRange as i32,
207 UnexpectedError = sys::EResult_k_EResultUnexpectedError as i32,
208 Disabled = sys::EResult_k_EResultDisabled as i32,
209 InvalidCEGSubmission = sys::EResult_k_EResultInvalidCEGSubmission as i32,
210 RestrictedDevice = sys::EResult_k_EResultRestrictedDevice as i32,
211 RegionLocked = sys::EResult_k_EResultRegionLocked as i32,
212 RateLimitExceeded = sys::EResult_k_EResultRateLimitExceeded as i32,
213 AccountLoginDeniedNeedTwoFactor = sys::EResult_k_EResultAccountLoginDeniedNeedTwoFactor as i32,
214 ItemDeleted = sys::EResult_k_EResultItemDeleted as i32,
215 AccountLoginDeniedThrottle = sys::EResult_k_EResultAccountLoginDeniedThrottle as i32,
216 TwoFactorCodeMismatch = sys::EResult_k_EResultTwoFactorCodeMismatch as i32,
217 TwoFactorActivationCodeMismatch = sys::EResult_k_EResultTwoFactorActivationCodeMismatch as i32,
218 AccountAssociatedToMultiplePartners =
219 sys::EResult_k_EResultAccountAssociatedToMultiplePartners as i32,
220 NotModified = sys::EResult_k_EResultNotModified as i32,
221 NoMobileDevice = sys::EResult_k_EResultNoMobileDevice as i32,
222 TimeNotSynced = sys::EResult_k_EResultTimeNotSynced as i32,
223 SmsCodeFailed = sys::EResult_k_EResultSmsCodeFailed as i32,
224 AccountLimitExceeded = sys::EResult_k_EResultAccountLimitExceeded as i32,
225 AccountActivityLimitExceeded = sys::EResult_k_EResultAccountActivityLimitExceeded as i32,
226 PhoneActivityLimitExceeded = sys::EResult_k_EResultPhoneActivityLimitExceeded as i32,
227 RefundToWallet = sys::EResult_k_EResultRefundToWallet as i32,
228 EmailSendFailure = sys::EResult_k_EResultEmailSendFailure as i32,
229 NotSettled = sys::EResult_k_EResultNotSettled as i32,
230 NeedCaptcha = sys::EResult_k_EResultNeedCaptcha as i32,
231 GSLTDenied = sys::EResult_k_EResultGSLTDenied as i32,
232 GSOwnerDenied = sys::EResult_k_EResultGSOwnerDenied as i32,
233 InvalidItemType = sys::EResult_k_EResultInvalidItemType as i32,
234 IPBanned = sys::EResult_k_EResultIPBanned as i32,
235 GSLTExpired = sys::EResult_k_EResultGSLTExpired as i32,
236 InsufficientFunds = sys::EResult_k_EResultInsufficientFunds as i32,
237 TooManyPending = sys::EResult_k_EResultTooManyPending as i32,
238 NoSiteLicensesFound = sys::EResult_k_EResultNoSiteLicensesFound as i32,
239 WGNetworkSendExceeded = sys::EResult_k_EResultWGNetworkSendExceeded as i32,
240 AccountNotFriends = sys::EResult_k_EResultAccountNotFriends as i32,
241 LimitedUserAccount = sys::EResult_k_EResultLimitedUserAccount as i32,
242 CantRemoveItem = sys::EResult_k_EResultCantRemoveItem as i32,
243}
244
245impl SteamResult {
246 pub(crate) fn from_inner(inner: sys::EResult) -> Self {
247 SteamResult::from_i32(inner as i32)
248 .unwrap_or_else(|| panic!("Unknown EResult variant: {inner}"))
249 }
250}
251
252impl Display for SteamResult {
253 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
254 use SteamResult::*;
255
256 let error_string = match *self {
257 OK => "Success.",
258 Fail => "Generic failure.",
259 NoConnection => "Your Steam client doesn't have a connection to the back-end.",
260 InvalidPassword => "Password/ticket is invalid.",
261 LoggedInElsewhere => "The user is logged in elsewhere.",
262 InvalidProtocolVer => "Protocol version is incorrect.",
263 InvalidParam => "A parameter is incorrect.",
264 FileNotFound => "File was not found.",
265 Busy => "Called method is busy - action not taken.",
266 InvalidState => "Called object was in an invalid state.",
267 InvalidName => "The name was invalid.",
268 InvalidEmail => "The email was invalid.",
269 DuplicateName => "The name is not unique.",
270 AccessDenied => "Access is denied.",
271 Timeout => "Operation timed out.",
272 Banned => "The user is VAC2 banned.",
273 AccountNotFound => "Account not found.",
274 InvalidSteamID => "The Steam ID was invalid.",
275 ServiceUnavailable => "The requested service is currently unavailable.",
276 NotLoggedOn => "The user is not logged on.",
277 Pending => "Request is pending, it may be in process or waiting on third party.",
278 EncryptionFailure => "Encryption or Decryption failed.",
279 InsufficientPrivilege => "Insufficient privilege.",
280 LimitExceeded => "Too much of a good thing.",
281 Revoked => "Access has been revoked (used for revoked guest passes.)",
282 Expired => "License/Guest pass the user is trying to access is expired.",
283 AlreadyRedeemed => {
284 "Guest pass has already been redeemed by account, cannot be used again."
285 }
286 DuplicateRequest => {
287 "The request is a duplicate and the action has already occurred in the past, ignored this time."
288 }
289 AlreadyOwned => {
290 "All the games in this guest pass redemption request are already owned by the user."
291 }
292 IPNotFound => "IP address not found.",
293 PersistFailed => "Failed to write change to the data store.",
294 LockingFailed => "Failed to acquire access lock for this operation.",
295 LogonSessionReplaced => "The logon session has been replaced.",
296 ConnectFailed => "Failed to connect.",
297 HandshakeFailed => "The authentication handshake has failed.",
298 IOFailure => "There has been a generic IO failure.",
299 RemoteDisconnect => "The remote server has disconnected.",
300 ShoppingCartNotFound => "Failed to find the shopping cart requested.",
301 Blocked => "A user blocked the action.",
302 Ignored => "The target is ignoring sender.",
303 NoMatch => "Nothing matching the request found.",
304 AccountDisabled => "The account is disabled.",
305 ServiceReadOnly => "This service is not accepting content changes right now.",
306 AccountNotFeatured => "Account doesn't have value, so this feature isn't available.",
307 AdministratorOK => "Allowed to take this action, but only because requester is admin.",
308 ContentVersion => {
309 "A Version mismatch in content transmitted within the Steam protocol."
310 }
311 TryAnotherCM => {
312 "The current CM can't service the user making a request, user should try another."
313 }
314 PasswordRequiredToKickSession => {
315 "You are already logged in elsewhere, this cached credential login has failed."
316 }
317 AlreadyLoggedInElsewhere => {
318 "The user is logged in elsewhere. (Use k_EResultLoggedInElsewhere instead!)"
319 }
320 Suspended => "Long running operation has suspended/paused. (eg. content download.)",
321 Cancelled => {
322 "Operation has been canceled, typically by user. (eg. a content download.)"
323 }
324 DataCorruption => "Operation canceled because data is ill formed or unrecoverable.",
325 DiskFull => "Operation canceled - not enough disk space.",
326 RemoteCallFailed => "The remote or IPC call has failed.",
327 PasswordUnset => "Password could not be verified as it's unset server side.",
328 ExternalAccountUnlinked => {
329 "External account (PSN, Facebook...) is not linked to a Steam account."
330 }
331 PSNTicketInvalid => "PSN ticket was invalid.",
332 ExternalAccountAlreadyLinked => {
333 "External account (PSN, Facebook...) is already linked to some other account, must explicitly request to replace/delete the link first."
334 }
335 RemoteFileConflict => {
336 "The sync cannot resume due to a conflict between the local and remote files."
337 }
338 IllegalPassword => "The requested new password is not allowed.",
339 SameAsPreviousValue => {
340 "New value is the same as the old one. This is used for secret question and answer."
341 }
342 AccountLogonDenied => "Account login denied due to 2nd factor authentication failure.",
343 CannotUseOldPassword => "The requested new password is not legal.",
344 InvalidLoginAuthCode => "Account login denied due to auth code invalid.",
345 AccountLogonDeniedNoMail => {
346 "Account login denied due to 2nd factor auth failure - and no mail has been sent."
347 }
348 HardwareNotCapableOfIPT => {
349 "The users hardware does not support Intel's Identity Protection Technology (IPT)."
350 }
351 IPTInitError => {
352 "Intel's Identity Protection Technology (IPT) has failed to initialize."
353 }
354 ParentalControlRestricted => {
355 "Operation failed due to parental control restrictions for current user."
356 }
357 FacebookQueryError => "Facebook query returned an error.",
358 ExpiredLoginAuthCode => "Account login denied due to an expired auth code.",
359 IPLoginRestrictionFailed => "The login failed due to an IP restriction.",
360 AccountLockedDown => {
361 "The current users account is currently locked for use. This is likely due to a hijacking and pending ownership verification."
362 }
363 AccountLogonDeniedVerifiedEmailRequired => {
364 "The logon failed because the accounts email is not verified."
365 }
366 NoMatchingURL => "There is no URL matching the provided values.",
367 BadResponse => "Bad Response due to a Parse failure, missing field, etc.",
368 RequirePasswordReEntry => {
369 "The user cannot complete the action until they re-enter their password."
370 }
371 ValueOutOfRange => "The value entered is outside the acceptable range.",
372 UnexpectedError => "Something happened that we didn't expect to ever happen.",
373 Disabled => "The requested service has been configured to be unavailable.",
374 InvalidCEGSubmission => "The files submitted to the CEG server are not valid.",
375 RestrictedDevice => "The device being used is not allowed to perform this action.",
376 RegionLocked => "The action could not be complete because it is region restricted.",
377 RateLimitExceeded => {
378 "Temporary rate limit exceeded, try again later, different from k_EResultLimitExceeded which may be permanent."
379 }
380 AccountLoginDeniedNeedTwoFactor => "Need two-factor code to login.",
381 ItemDeleted => "The thing we're trying to access has been deleted.",
382 AccountLoginDeniedThrottle => {
383 "Login attempt failed, try to throttle response to possible attacker."
384 }
385 TwoFactorCodeMismatch => "Two factor authentication (Steam Guard) code is incorrect.",
386 TwoFactorActivationCodeMismatch => {
387 "The activation code for two-factor authentication (Steam Guard) didn't match."
388 }
389 AccountAssociatedToMultiplePartners => {
390 "The current account has been associated with multiple partners."
391 }
392 NotModified => "The data has not been modified.",
393 NoMobileDevice => "The account does not have a mobile device associated with it.",
394 TimeNotSynced => "The time presented is out of range or tolerance.",
395 SmsCodeFailed => "SMS code failure - no match, none pending, etc.",
396 AccountLimitExceeded => "Too many accounts access this resource.",
397 AccountActivityLimitExceeded => "Too many changes to this account.",
398 PhoneActivityLimitExceeded => "Too many changes to this phone.",
399 RefundToWallet => "Cannot refund to payment method, must use wallet.",
400 EmailSendFailure => "Cannot send an email.",
401 NotSettled => "Can't perform operation until payment has settled.",
402 NeedCaptcha => "The user needs to provide a valid captcha.",
403 GSLTDenied => "A game server login token owned by this token's owner has been banned.",
404 GSOwnerDenied => {
405 "Game server owner is denied for some other reason such as account locked, community ban, vac ban, missing phone, etc."
406 }
407 InvalidItemType => "The type of thing we were requested to act on is invalid.",
408 IPBanned => "The IP address has been banned from taking this action.",
409 GSLTExpired => {
410 "This Game Server Login Token (GSLT) has expired from disuse; it can be reset for use."
411 }
412 InsufficientFunds => "user doesn't have enough wallet funds to complete the action",
413 TooManyPending => "There are too many of this thing pending already",
414 NoSiteLicensesFound => "No site licenses found",
415 WGNetworkSendExceeded => {
416 "the WG couldn't send a response because we exceeded max network send size"
417 }
418 AccountNotFriends => "the user is not mutually friends",
419 LimitedUserAccount => "the user is limited",
420 CantRemoveItem => "item can't be removed",
421 };
422
423 write!(f, "{error_string}")
424 }
425}