steamworks/user_stats.rs
1mod stat_callback;
2pub mod stats;
3
4pub use self::stat_callback::*;
5use super::*;
6#[cfg(test)]
7use serial_test::serial;
8
9/// Access to the steam user interface
10pub struct UserStats {
11 pub(crate) user_stats: *mut sys::ISteamUserStats,
12 pub(crate) inner: Arc<Inner>,
13}
14
15impl UserStats {
16 pub fn find_leaderboard<F>(&self, name: &str, cb: F)
17 where
18 F: FnOnce(Result<Option<Leaderboard>, SteamError>) + 'static + Send,
19 {
20 unsafe {
21 let name = CString::new(name).unwrap();
22 let api_call =
23 sys::SteamAPI_ISteamUserStats_FindLeaderboard(self.user_stats, name.as_ptr());
24 register_call_result::<sys::LeaderboardFindResult_t, _>(
25 &self.inner,
26 api_call,
27 move |v, io_error| {
28 cb(if io_error {
29 Err(SteamError::IOFailure)
30 } else {
31 Ok(if v.m_bLeaderboardFound != 0 {
32 Some(Leaderboard(v.m_hSteamLeaderboard))
33 } else {
34 None
35 })
36 })
37 },
38 );
39 }
40 }
41
42 pub fn find_or_create_leaderboard<F>(
43 &self,
44 name: &str,
45 sort_method: LeaderboardSortMethod,
46 display_type: LeaderboardDisplayType,
47 cb: F,
48 ) where
49 F: FnOnce(Result<Option<Leaderboard>, SteamError>) + 'static + Send,
50 {
51 unsafe {
52 let name = CString::new(name).unwrap();
53
54 let sort_method = match sort_method {
55 LeaderboardSortMethod::Ascending => {
56 sys::ELeaderboardSortMethod::k_ELeaderboardSortMethodAscending
57 }
58 LeaderboardSortMethod::Descending => {
59 sys::ELeaderboardSortMethod::k_ELeaderboardSortMethodDescending
60 }
61 };
62
63 let display_type = match display_type {
64 LeaderboardDisplayType::Numeric => {
65 sys::ELeaderboardDisplayType::k_ELeaderboardDisplayTypeNumeric
66 }
67 LeaderboardDisplayType::TimeSeconds => {
68 sys::ELeaderboardDisplayType::k_ELeaderboardDisplayTypeTimeSeconds
69 }
70 LeaderboardDisplayType::TimeMilliSeconds => {
71 sys::ELeaderboardDisplayType::k_ELeaderboardDisplayTypeTimeMilliSeconds
72 }
73 };
74
75 let api_call = sys::SteamAPI_ISteamUserStats_FindOrCreateLeaderboard(
76 self.user_stats,
77 name.as_ptr(),
78 sort_method,
79 display_type,
80 );
81 register_call_result::<sys::LeaderboardFindResult_t, _>(
82 &self.inner,
83 api_call,
84 move |v, io_error| {
85 cb(if io_error {
86 Err(SteamError::IOFailure)
87 } else {
88 Ok(if v.m_bLeaderboardFound != 0 {
89 Some(Leaderboard(v.m_hSteamLeaderboard))
90 } else {
91 None
92 })
93 })
94 },
95 );
96 }
97 }
98
99 pub fn upload_leaderboard_score<F>(
100 &self,
101 leaderboard: &Leaderboard,
102 method: UploadScoreMethod,
103 score: i32,
104 details: &[i32],
105 cb: F,
106 ) where
107 F: FnOnce(Result<Option<LeaderboardScoreUploaded>, SteamError>) + 'static + Send,
108 {
109 unsafe {
110 let method = match method {
111 UploadScoreMethod::KeepBest => {
112 sys::ELeaderboardUploadScoreMethod::k_ELeaderboardUploadScoreMethodKeepBest
113 }
114 UploadScoreMethod::ForceUpdate => {
115 sys::ELeaderboardUploadScoreMethod::k_ELeaderboardUploadScoreMethodForceUpdate
116 }
117 };
118 let api_call = sys::SteamAPI_ISteamUserStats_UploadLeaderboardScore(
119 self.user_stats,
120 leaderboard.0,
121 method,
122 score,
123 details.as_ptr(),
124 details.len() as _,
125 );
126 register_call_result::<sys::LeaderboardScoreUploaded_t, _>(
127 &self.inner,
128 api_call,
129 move |v, io_error| {
130 cb(if io_error {
131 Err(SteamError::IOFailure)
132 } else {
133 Ok(if v.m_bSuccess != 0 {
134 Some(LeaderboardScoreUploaded {
135 score: v.m_nScore,
136 was_changed: v.m_bScoreChanged != 0,
137 global_rank_new: v.m_nGlobalRankNew as _,
138 global_rank_previous: v.m_nGlobalRankPrevious as _,
139 })
140 } else {
141 None
142 })
143 })
144 },
145 );
146 }
147 }
148
149 pub fn download_leaderboard_entries<F>(
150 &self,
151 leaderboard: &Leaderboard,
152 request: LeaderboardDataRequest,
153 start: usize,
154 end: usize,
155 max_details_len: usize,
156 cb: F,
157 ) where
158 F: FnOnce(Result<Vec<LeaderboardEntry>, SteamError>) + 'static + Send,
159 {
160 unsafe {
161 let request = match request {
162 LeaderboardDataRequest::Global => {
163 sys::ELeaderboardDataRequest::k_ELeaderboardDataRequestGlobal
164 }
165 LeaderboardDataRequest::GlobalAroundUser => {
166 sys::ELeaderboardDataRequest::k_ELeaderboardDataRequestGlobalAroundUser
167 }
168 LeaderboardDataRequest::Friends => {
169 sys::ELeaderboardDataRequest::k_ELeaderboardDataRequestFriends
170 }
171 };
172 let api_call = sys::SteamAPI_ISteamUserStats_DownloadLeaderboardEntries(
173 self.user_stats,
174 leaderboard.0,
175 request,
176 start as _,
177 end as _,
178 );
179 let user_stats = self.user_stats as isize;
180 register_call_result::<sys::LeaderboardScoresDownloaded_t, _>(
181 &self.inner,
182 api_call,
183 move |v, io_error| {
184 cb(if io_error {
185 Err(SteamError::IOFailure)
186 } else {
187 let len = v.m_cEntryCount;
188 let mut entries = Vec::with_capacity(len as usize);
189 for idx in 0..len {
190 let mut entry: sys::LeaderboardEntry_t = std::mem::zeroed();
191 let mut details = Vec::with_capacity(max_details_len);
192
193 sys::SteamAPI_ISteamUserStats_GetDownloadedLeaderboardEntry(
194 user_stats as *mut _,
195 v.m_hSteamLeaderboardEntries,
196 idx,
197 &mut entry,
198 details.as_mut_ptr(),
199 max_details_len as _,
200 );
201
202 details.set_len(std::cmp::min(
203 entry.m_cDetails as usize,
204 max_details_len as usize,
205 ));
206
207 entries.push(LeaderboardEntry {
208 user: SteamId(entry.m_steamIDUser.m_steamid.m_unAll64Bits),
209 global_rank: entry.m_nGlobalRank,
210 score: entry.m_nScore,
211 details,
212 })
213 }
214 Ok(entries)
215 })
216 },
217 );
218 }
219 }
220
221 /// Returns the display type of a leaderboard handle. Returns `None` if the leaderboard handle is invalid.
222 pub fn get_leaderboard_display_type(
223 &self,
224 leaderboard: &Leaderboard,
225 ) -> Option<LeaderboardDisplayType> {
226 unsafe {
227 match sys::SteamAPI_ISteamUserStats_GetLeaderboardDisplayType(
228 self.user_stats,
229 leaderboard.0,
230 ) {
231 sys::ELeaderboardDisplayType::k_ELeaderboardDisplayTypeNumeric => {
232 Some(LeaderboardDisplayType::Numeric)
233 }
234 sys::ELeaderboardDisplayType::k_ELeaderboardDisplayTypeTimeSeconds => {
235 Some(LeaderboardDisplayType::TimeSeconds)
236 }
237 sys::ELeaderboardDisplayType::k_ELeaderboardDisplayTypeTimeMilliSeconds => {
238 Some(LeaderboardDisplayType::TimeMilliSeconds)
239 }
240 _ => None,
241 }
242 }
243 }
244
245 /// Returns the sort method of a leaderboard handle. Returns `None` if the leaderboard handle is invalid.
246 pub fn get_leaderboard_sort_method(
247 &self,
248 leaderboard: &Leaderboard,
249 ) -> Option<LeaderboardSortMethod> {
250 unsafe {
251 match sys::SteamAPI_ISteamUserStats_GetLeaderboardSortMethod(
252 self.user_stats,
253 leaderboard.0,
254 ) {
255 sys::ELeaderboardSortMethod::k_ELeaderboardSortMethodAscending => {
256 Some(LeaderboardSortMethod::Ascending)
257 }
258 sys::ELeaderboardSortMethod::k_ELeaderboardSortMethodDescending => {
259 Some(LeaderboardSortMethod::Descending)
260 }
261 _ => None,
262 }
263 }
264 }
265
266 /// Returns the name of a leaderboard handle. Returns an empty string if the leaderboard handle is invalid.
267 pub fn get_leaderboard_name(&self, leaderboard: &Leaderboard) -> String {
268 unsafe {
269 let name = CStr::from_ptr(sys::SteamAPI_ISteamUserStats_GetLeaderboardName(
270 self.user_stats,
271 leaderboard.0,
272 ));
273 name.to_string_lossy().into()
274 }
275 }
276
277 /// Returns the total number of entries in a leaderboard. Returns 0 if the leaderboard handle is invalid.
278 pub fn get_leaderboard_entry_count(&self, leaderboard: &Leaderboard) -> i32 {
279 unsafe {
280 sys::SteamAPI_ISteamUserStats_GetLeaderboardEntryCount(self.user_stats, leaderboard.0)
281 }
282 }
283
284 /// Triggers a [`UserStatsReceived`](./struct.UserStatsReceived.html) callback.
285 pub fn request_user_stats(&self, steam_user_id: u64) {
286 unsafe {
287 sys::SteamAPI_ISteamUserStats_RequestUserStats(self.user_stats, steam_user_id);
288 }
289 }
290
291 /// Asynchronously fetch the data for the percentage of players who have received each achievement
292 /// for the current game globally.
293 ///
294 /// You must have called `request_current_stats()` and it needs to return successfully via its
295 /// callback prior to calling this!*
296 ///
297 /// **Note: Not sure if this is applicable, as the other achievement functions requiring
298 /// `request_current_stats()` don't specifically need it to be called in order for them to complete
299 /// successfully. Maybe it autoruns via `Client::init()/init_app()` somehow?*
300 pub fn request_global_achievement_percentages<F>(&self, cb: F)
301 where
302 F: FnOnce(Result<GameId, SteamError>) + 'static + Send,
303 {
304 unsafe {
305 let api_call =
306 sys::SteamAPI_ISteamUserStats_RequestGlobalAchievementPercentages(self.user_stats);
307 register_call_result::<sys::GlobalAchievementPercentagesReady_t, _>(
308 &self.inner,
309 api_call,
310 move |v, io_error| {
311 cb(if io_error {
312 Err(SteamError::IOFailure)
313 } else {
314 Ok(GameId(v.m_nGameID))
315 })
316 },
317 );
318 }
319 }
320
321 /// Asynchronously requests global stats data, which is available for stats marked as "aggregated".
322 ///
323 /// This call is asynchronous, with the results returned in [`GlobalStatsReceived`](crate::GlobalStatsReceived) callback.
324 ///
325 /// # Arguments
326 ///
327 /// * `history_days` - Specifies how many days of day-by-day history to retrieve in addition
328 /// to the overall totals. The limit is 60.
329 ///
330 /// # Example
331 ///
332 /// ```no_run
333 /// # use steamworks::*;
334 /// # let client = steamworks::Client::init().unwrap();
335 /// let user_stats = client.user_stats();
336 ///
337 /// // Request global stats with 7 days of history
338 /// user_stats.request_global_stats(7, |result| {
339 /// match result {
340 /// Ok(game_id) => {
341 /// println!("Global stats received for game: {:?}", game_id);
342 /// }
343 /// Err(e) => {
344 /// println!("Failed to get global stats: {:?}", e);
345 /// }
346 /// }
347 /// });
348 /// ```
349 pub fn request_global_stats<F>(&self, history_days: i32, cb: F)
350 where
351 F: FnOnce(Result<GameId, SteamError>) + 'static + Send,
352 {
353 unsafe {
354 let api_call =
355 sys::SteamAPI_ISteamUserStats_RequestGlobalStats(self.user_stats, history_days);
356 register_call_result::<sys::GlobalStatsReceived_t, _>(
357 &self.inner,
358 api_call,
359 move |v, io_error| {
360 cb(if io_error {
361 Err(SteamError::IOFailure)
362 } else {
363 Ok(GameId(v.m_nGameID))
364 })
365 },
366 );
367 }
368 }
369
370 /// Gets the lifetime total for an aggregated stat as an `i64`.
371 ///
372 /// The specified stat must exist and be marked as "aggregated" in the Steamworks App Admin.
373 ///
374 /// Requires [`request_global_stats()`](Self::request_global_stats) to have been called
375 /// and a successful [`GlobalStatsReceived`](crate::GlobalStatsReceived) callback processed.
376 ///
377 /// # Arguments
378 ///
379 /// * `name` - The 'API Name' of the stat. Must not be longer than `k_cchStatNameMax`.
380 ///
381 /// # Returns
382 ///
383 /// Returns `Ok(i64)` with the stat value if successful, or `Err(())` if the stat doesn't exist
384 /// or hasn't been received yet.
385 pub fn get_global_stat_i64(&self, name: &str) -> Result<i64, ()> {
386 let name = CString::new(name).map_err(|_| ())?;
387 let mut value: i64 = 0;
388 let success = unsafe {
389 sys::SteamAPI_ISteamUserStats_GetGlobalStatInt64(
390 self.user_stats,
391 name.as_ptr(),
392 &mut value,
393 )
394 };
395 if success {
396 Ok(value)
397 } else {
398 Err(())
399 }
400 }
401
402 /// Gets the lifetime total for an aggregated stat as an `f64`.
403 ///
404 /// The specified stat must exist and be marked as "aggregated" in the Steamworks App Admin.
405 ///
406 /// Requires [`request_global_stats()`](Self::request_global_stats) to have been called
407 /// and a successful [`GlobalStatsReceived`](crate::GlobalStatsReceived) callback processed.
408 ///
409 /// # Arguments
410 ///
411 /// * `name` - The 'API Name' of the stat. Must not be longer than `k_cchStatNameMax`.
412 ///
413 /// # Returns
414 ///
415 /// Returns `Ok(f64)` with the stat value if successful, or `Err(())` if the stat doesn't exist
416 /// or hasn't been received yet.
417 pub fn get_global_stat_f64(&self, name: &str) -> Result<f64, ()> {
418 let name = CString::new(name).map_err(|_| ())?;
419 let mut value: f64 = 0.0;
420 let success = unsafe {
421 sys::SteamAPI_ISteamUserStats_GetGlobalStatDouble(
422 self.user_stats,
423 name.as_ptr(),
424 &mut value,
425 )
426 };
427 if success {
428 Ok(value)
429 } else {
430 Err(())
431 }
432 }
433
434 /// Gets history for an aggregated stat as `i64` values.
435 ///
436 /// The data will be filled with daily values, starting with today.
437 /// So when called, `data[0]` will be today, `data[1]` will be yesterday, and `data[2]` will be
438 /// two days ago, etc.
439 ///
440 /// The specified stat must exist and be marked as "aggregated" in the Steamworks App Admin.
441 ///
442 /// Requires [`request_global_stats()`](Self::request_global_stats) to have been called
443 /// and a successful [`GlobalStatsReceived`](crate::GlobalStatsReceived) callback processed.
444 ///
445 /// # Arguments
446 ///
447 /// * `name` - The 'API Name' of the stat. Must not be longer than `k_cchStatNameMax`.
448 /// * `max_days` - The maximum number of days of history to retrieve. This should match
449 /// or be less than the `history_days` value passed to `request_global_stats()`.
450 ///
451 /// # Returns
452 ///
453 /// Returns `Ok(Vec<i64>)` containing the daily values (from today backwards) if successful,
454 /// or `Err(())` if the stat doesn't exist or hasn't been received yet.
455 pub fn get_global_stat_history_i64(&self, name: &str, max_days: usize) -> Result<Vec<i64>, ()> {
456 let name = CString::new(name).map_err(|_| ())?;
457 let mut data = vec![0i64; max_days];
458 let count = unsafe {
459 sys::SteamAPI_ISteamUserStats_GetGlobalStatHistoryInt64(
460 self.user_stats,
461 name.as_ptr(),
462 data.as_mut_ptr(),
463 (max_days * std::mem::size_of::<i64>()) as u32,
464 )
465 };
466 if count >= 0 {
467 data.truncate(count as usize);
468 Ok(data)
469 } else {
470 Err(())
471 }
472 }
473
474 /// Gets history for an aggregated stat as `f64` values.
475 ///
476 /// The data will be filled with daily values, starting with today.
477 /// So when called, `data[0]` will be today, `data[1]` will be yesterday, and `data[2]` will be
478 /// two days ago, etc.
479 ///
480 /// The specified stat must exist and be marked as "aggregated" in the Steamworks App Admin.
481 ///
482 /// Requires [`request_global_stats()`](Self::request_global_stats) to have been called
483 /// and a successful [`GlobalStatsReceived`](crate::GlobalStatsReceived) callback processed.
484 ///
485 /// # Arguments
486 ///
487 /// * `name` - The 'API Name' of the stat. Must not be longer than `k_cchStatNameMax`.
488 /// * `max_days` - The maximum number of days of history to retrieve. This should match
489 /// or be less than the `history_days` value passed to `request_global_stats()`.
490 ///
491 /// # Returns
492 ///
493 /// Returns `Ok(Vec<f64>)` containing the daily values (from today backwards) if successful,
494 /// or `Err(())` if the stat doesn't exist or hasn't been received yet.
495 pub fn get_global_stat_history_f64(&self, name: &str, max_days: usize) -> Result<Vec<f64>, ()> {
496 let name = CString::new(name).map_err(|_| ())?;
497 let mut data = vec![0f64; max_days];
498 let count = unsafe {
499 sys::SteamAPI_ISteamUserStats_GetGlobalStatHistoryDouble(
500 self.user_stats,
501 name.as_ptr(),
502 data.as_mut_ptr(),
503 (max_days * std::mem::size_of::<f64>()) as u32,
504 )
505 };
506 if count >= 0 {
507 data.truncate(count as usize);
508 Ok(data)
509 } else {
510 Err(())
511 }
512 }
513
514 /// Send the changed stats and achievements data to the server for permanent storage.
515 ///
516 /// * Triggers a [`UserStatsStored`](../struct.UserStatsStored.html) callback if successful.
517 /// * Triggers a [`UserAchievementStored`](../struct.UserAchievementStored.html) callback
518 /// if achievements have been unlocked.
519 ///
520 /// Requires [`request_current_stats()`](#method.request_current_stats) to have been called
521 /// and a successful [`UserStatsReceived`](./struct.UserStatsReceived.html) callback processed.
522 pub fn store_stats(&self) -> Result<(), ()> {
523 let success = unsafe { sys::SteamAPI_ISteamUserStats_StoreStats(self.user_stats) };
524 if success {
525 Ok(())
526 } else {
527 Err(())
528 }
529 }
530
531 /// Resets the current users stats and, optionally achievements.
532 pub fn reset_all_stats(&self, achievements_too: bool) -> Result<(), ()> {
533 let success = unsafe {
534 sys::SteamAPI_ISteamUserStats_ResetAllStats(self.user_stats, achievements_too)
535 };
536 if success {
537 Ok(())
538 } else {
539 Err(())
540 }
541 }
542
543 /// Gets the value of a given stat for the current user
544 ///
545 /// The specified stat must exist and match the type set on the Steamworks App Admin website.
546 ///
547 /// Requires [`request_current_stats()`](#method.request_current_stats) to have been called
548 /// and a successful [`UserStatsReceived`](./struct.UserStatsReceived.html) callback processed.
549 pub fn get_stat_i32(&self, name: &str) -> Result<i32, ()> {
550 let name = CString::new(name).unwrap();
551
552 let mut value: i32 = 0;
553 let success = unsafe {
554 sys::SteamAPI_ISteamUserStats_GetStatInt32(self.user_stats, name.as_ptr(), &mut value)
555 };
556 if success {
557 Ok(value)
558 } else {
559 Err(())
560 }
561 }
562
563 /// Sets / updates the value of a given stat for the current user
564 ///
565 /// This call only changes the value in-memory and is very cheap. To commit the stats you
566 /// must call [`store_stats()`](#method.store_stats)
567 ///
568 /// The specified stat must exist and match the type set on the Steamworks App Admin website.
569 ///
570 /// Requires [`request_current_stats()`](#method.request_current_stats) to have been called
571 /// and a successful [`UserStatsReceived`](./struct.UserStatsReceived.html) callback processed.
572 pub fn set_stat_i32(&self, name: &str, stat: i32) -> Result<(), ()> {
573 let name = CString::new(name).unwrap();
574
575 let success = unsafe {
576 sys::SteamAPI_ISteamUserStats_SetStatInt32(self.user_stats, name.as_ptr(), stat)
577 };
578 if success {
579 Ok(())
580 } else {
581 Err(())
582 }
583 }
584
585 /// Gets the value of a given stat for the current user
586 ///
587 /// The specified stat must exist and match the type set on the Steamworks App Admin website.
588 ///
589 /// Requires [`request_current_stats()`](#method.request_current_stats) to have been called
590 /// and a successful [`UserStatsReceived`](./struct.UserStatsReceived.html) callback processed.
591 pub fn get_stat_f32(&self, name: &str) -> Result<f32, ()> {
592 let name = CString::new(name).unwrap();
593
594 let mut value: f32 = 0.0;
595 let success = unsafe {
596 sys::SteamAPI_ISteamUserStats_GetStatFloat(self.user_stats, name.as_ptr(), &mut value)
597 };
598 if success {
599 Ok(value)
600 } else {
601 Err(())
602 }
603 }
604
605 /// Sets / updates the value of a given stat for the current user
606 ///
607 /// This call only changes the value in-memory and is very cheap. To commit the stats you
608 /// must call [`store_stats()`](#method.store_stats)
609 ///
610 /// The specified stat must exist and match the type set on the Steamworks App Admin website.
611 ///
612 /// Requires [`request_current_stats()`](#method.request_current_stats) to have been called
613 /// and a successful [`UserStatsReceived`](./struct.UserStatsReceived.html) callback processed.
614 pub fn set_stat_f32(&self, name: &str, stat: f32) -> Result<(), ()> {
615 let name = CString::new(name).unwrap();
616
617 let success = unsafe {
618 sys::SteamAPI_ISteamUserStats_SetStatFloat(self.user_stats, name.as_ptr(), stat)
619 };
620 if success {
621 Ok(())
622 } else {
623 Err(())
624 }
625 }
626
627 /// Access achievement API for a given achievement 'API Name'.
628 ///
629 /// Requires [`request_current_stats()`](#method.request_current_stats) to have been called
630 /// and a successful [`UserStatsReceived`](./struct.UserStatsReceived.html) callback processed.
631 #[inline]
632 #[must_use]
633 pub fn achievement(&self, name: &str) -> stats::AchievementHelper<'_> {
634 stats::AchievementHelper {
635 name: CString::new(name).unwrap(),
636 parent: self,
637 }
638 }
639
640 /// Get the number of achievements defined in the App Admin panel of the Steamworks website.
641 ///
642 /// This is used for iterating through all of the achievements with GetAchievementName.
643 ///
644 /// Returns 0 if the current App ID has no achievements.
645 ///
646 /// *Note: Returns an error for AppId `480` (Spacewar)!*
647 pub fn get_num_achievements(&self) -> Result<u32, ()> {
648 unsafe {
649 let num = sys::SteamAPI_ISteamUserStats_GetNumAchievements(self.user_stats);
650 if num != 0 {
651 Ok(num)
652 } else {
653 Err(())
654 }
655 }
656 }
657
658 /// Returns an array of all achievement names for the current AppId.
659 ///
660 /// Returns an empty string for an achievement name if `iAchievement` is not a valid index,
661 /// and the current AppId must have achievements.
662 pub fn get_achievement_names(&self) -> Option<Vec<String>> {
663 let num = self
664 .get_num_achievements()
665 .expect("Failed to get number of achievements");
666 let mut names = Vec::new();
667
668 for i in 0..num {
669 unsafe {
670 let name = sys::SteamAPI_ISteamUserStats_GetAchievementName(self.user_stats, i);
671
672 let c_str = CStr::from_ptr(name).to_string_lossy().into_owned();
673
674 names.push(c_str);
675 }
676 }
677 Some(names)
678 }
679}
680
681#[derive(Clone, Debug)]
682#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
683pub struct LeaderboardEntry {
684 pub user: SteamId,
685 pub global_rank: i32,
686 pub score: i32,
687 pub details: Vec<i32>,
688}
689
690pub enum LeaderboardDataRequest {
691 Global,
692 GlobalAroundUser,
693 Friends,
694}
695
696#[derive(Clone, Debug)]
697#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
698pub struct LeaderboardScoreUploaded {
699 pub score: i32,
700 pub was_changed: bool,
701 pub global_rank_new: i32,
702 pub global_rank_previous: i32,
703}
704
705#[derive(Clone, Debug)]
706#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
707pub enum UploadScoreMethod {
708 KeepBest,
709 ForceUpdate,
710}
711
712#[derive(Clone, Debug)]
713#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
714pub enum LeaderboardSortMethod {
715 Ascending,
716 Descending,
717}
718
719#[derive(Clone, Debug)]
720#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
721pub enum LeaderboardDisplayType {
722 Numeric,
723 TimeSeconds,
724 TimeMilliSeconds,
725}
726
727#[derive(Clone, Debug)]
728#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
729pub struct Leaderboard(u64);
730
731impl Leaderboard {
732 /// Returns the raw 64 bit value of the leaderboard id
733 ///
734 /// Useful for serializing leaderboard ids over a
735 /// network or to a save format.
736 pub fn raw(&self) -> u64 {
737 self.0
738 }
739}
740
741#[test]
742#[ignore]
743#[serial]
744fn test() {
745 let client = Client::init().unwrap();
746
747 let stats = client.user_stats();
748
749 stats.find_leaderboard("steamworks_test", |lb| {
750 println!("Got: {:?}", lb);
751 });
752 let c2 = client.clone();
753 stats.find_or_create_leaderboard(
754 "steamworks_test_created",
755 LeaderboardSortMethod::Descending,
756 LeaderboardDisplayType::TimeMilliSeconds,
757 move |lb| {
758 println!("Got: {:?}", lb);
759
760 if let Some(lb) = lb.ok().and_then(|v| v) {
761 c2.user_stats().upload_leaderboard_score(
762 &lb,
763 UploadScoreMethod::ForceUpdate,
764 1337,
765 &[1, 2, 3, 4],
766 |v| {
767 println!("Upload: {:?}", v);
768 },
769 );
770 c2.user_stats().download_leaderboard_entries(
771 &lb,
772 LeaderboardDataRequest::Global,
773 0,
774 200,
775 10,
776 |v| {
777 println!("Download: {:?}", v);
778 },
779 );
780 }
781 },
782 );
783
784 for _ in 0..50 {
785 client.run_callbacks();
786 ::std::thread::sleep(::std::time::Duration::from_millis(100));
787 }
788}
789
790#[test]
791#[ignore]
792#[serial]
793fn test_global_stats() {
794 let client = Client::init().unwrap();
795 let stats = client.user_stats();
796
797 // Get stat name from environment variable, default to "test_stat"
798 let stat_name =
799 std::env::var("TEST_GLOBAL_STAT_NAME").unwrap_or_else(|_| "test_stat".to_string());
800 println!("Using global stat name: {}", stat_name);
801
802 // Test request_global_stats with 7 days of history
803 let c2 = client.clone();
804 let stat_name_clone = stat_name.clone();
805 stats.request_global_stats(7, move |result| {
806 match result {
807 Ok(game_id) => {
808 println!("Global stats received for game: {:?}", game_id);
809
810 // Test get_global_stat_i64
811 match c2.user_stats().get_global_stat_i64(&stat_name_clone) {
812 Ok(value) => println!("Global stat (i64): {}", value),
813 Err(_) => println!(
814 "Failed to get global stat (i64) - stat may not exist or not be aggregated"
815 ),
816 }
817
818 // Test get_global_stat_f64
819 match c2.user_stats().get_global_stat_f64(&stat_name_clone) {
820 Ok(value) => println!("Global stat (f64): {}", value),
821 Err(_) => println!(
822 "Failed to get global stat (f64) - stat may not exist or not be aggregated"
823 ),
824 }
825
826 // Test get_global_stat_history_i64
827 match c2
828 .user_stats()
829 .get_global_stat_history_i64(&stat_name_clone, 7)
830 {
831 Ok(history) => println!("Global stat history (i64): {:?}", history),
832 Err(_) => println!("Failed to get global stat history (i64)"),
833 }
834
835 // Test get_global_stat_history_f64
836 match c2
837 .user_stats()
838 .get_global_stat_history_f64(&stat_name_clone, 7)
839 {
840 Ok(history) => println!("Global stat history (f64): {:?}", history),
841 Err(_) => println!("Failed to get global stat history (f64)"),
842 }
843 }
844 Err(e) => {
845 println!("Failed to get global stats: {:?}", e);
846 }
847 }
848 });
849
850 // Run callbacks to process the async result
851 for _ in 0..50 {
852 client.run_callbacks();
853 ::std::thread::sleep(::std::time::Duration::from_millis(100));
854 }
855}
856
857#[test]
858fn test_global_stat_cstring_error() {
859 // Test that get_global_stat methods properly handle invalid CString input
860 // This test doesn't require Steam to be running
861
862 // Create a string with null byte which is invalid for CString
863 let invalid_name = "test\0stat";
864
865 // We can't actually test the UserStats methods without initializing Steam,
866 // but we can verify the CString conversion behavior
867 assert!(CString::new(invalid_name).is_err());
868}