Skip to main content

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}