1use std::convert::TryFrom;
2
3use crate::Client;
4use crate::steam::SteamId;
5use crate::steam::remote_storage::UgcHandle;
6use futures::Future;
7use futures::lock::Mutex;
8use futures_intrusive::sync::Semaphore;
9use once_cell::sync::Lazy;
10use snafu::{ResultExt, ensure};
11use std::convert::TryInto;
12use std::error::Error;
13use std::ffi::CString;
14use std::fmt::{self, Display};
15use std::mem::MaybeUninit;
16use std::{cmp, ptr};
17use steamworks_sys as sys;
18
19#[derive(Debug, Clone)]
27pub struct LeaderboardHandle {
28 pub(crate) client: Client,
29 pub(crate) handle: sys::SteamLeaderboard_t,
30}
31
32impl LeaderboardHandle {
33 pub fn download_global(
42 &self,
43 range_start: u32,
44 range_end: u32,
45 max_details: u8,
46 ) -> impl Future<Output = Vec<LeaderboardEntry>> + Send + '_ {
47 assert!(range_start > 0);
48 assert!(range_end >= range_start);
49
50 self.download_entry_range(
51 sys::ELeaderboardDataRequest_k_ELeaderboardDataRequestGlobal,
52 range_start.try_into().unwrap_or(i32::MAX),
53 range_end.try_into().unwrap_or(i32::MAX),
54 max_details,
55 )
56 }
57
58 pub fn download_global_around_user(
68 &self,
69 range_start: i32,
70 range_end: i32,
71 max_details: u8,
72 ) -> impl Future<Output = Vec<LeaderboardEntry>> + Send + '_ {
73 assert!(range_end >= range_start);
74
75 self.download_entry_range(
76 sys::ELeaderboardDataRequest_k_ELeaderboardDataRequestGlobalAroundUser,
77 range_start,
78 range_end,
79 max_details,
80 )
81 }
82
83 pub fn download_friends(
87 &self,
88 max_details: u8,
89 ) -> impl Future<Output = Vec<LeaderboardEntry>> + Send + '_ {
90 self.download_entry_range(
91 sys::ELeaderboardDataRequest_k_ELeaderboardDataRequestFriends,
92 0,
93 0,
94 max_details,
95 )
96 }
97
98 pub fn upload_leaderboard_score<'a>(
109 &'a self,
110 score: i32,
111 details: Option<&'a [i32]>,
112 force_update: bool,
113 ) -> impl Future<Output = Result<LeaderboardScoreUploaded, UploadLeaderboardScoreError>> + Send + 'a
114 {
115 static LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
117
118 let leaderboard_upload_score_method = if force_update {
119 sys::ELeaderboardUploadScoreMethod_k_ELeaderboardUploadScoreMethodForceUpdate
120 } else {
121 sys::ELeaderboardUploadScoreMethod_k_ELeaderboardUploadScoreMethodKeepBest
122 };
123
124 let details_count = match details {
125 Some(xs) => {
126 let len = xs.len();
127 assert!(
128 len <= 64,
129 "The details passed in to 'upload_leaderboard_score' has a length of {len}, but the limit is 64"
130 );
131 i32::try_from(len).unwrap()
132 }
133 None => 0,
134 };
135
136 async move {
137 let _guard = LOCK.lock().await;
138
139 let response: sys::LeaderboardScoreUploaded_t = unsafe {
140 let handle = sys::SteamAPI_ISteamUserStats_UploadLeaderboardScore(
141 *self.client.0.user_stats,
142 self.handle,
143 leaderboard_upload_score_method,
144 score,
145 details.map(|xs| xs.as_ptr()).unwrap_or(ptr::null()),
146 details_count,
147 );
148
149 self.client.register_for_call_result(handle).await
150 };
151
152 if response.m_bSuccess == 1 {
153 Ok(LeaderboardScoreUploaded {
154 score_changed: response.m_bScoreChanged != 0,
155 global_rank_new: response.m_nGlobalRankNew,
156 global_rank_previous: response.m_nGlobalRankPrevious,
157 })
158 } else {
159 Err(UploadLeaderboardScoreError)
160 }
161 }
162 }
163
164 fn download_entry_range(
165 &self,
166 request_type: sys::ELeaderboardDataRequest,
167 range_start: i32,
168 range_end: i32,
169 max_details: u8,
170 ) -> impl Future<Output = Vec<LeaderboardEntry>> + Send + '_ {
171 let max_details = cmp::min(max_details, 64);
172 async move {
173 let response: sys::LeaderboardScoresDownloaded_t = unsafe {
174 let handle = sys::SteamAPI_ISteamUserStats_DownloadLeaderboardEntries(
175 *self.client.0.user_stats,
176 self.handle,
177 request_type,
178 range_start,
179 range_end,
180 );
181
182 self.client.register_for_call_result(handle).await
183 };
184
185 let mut entries: Vec<LeaderboardEntry> =
186 Vec::with_capacity(response.m_cEntryCount as usize);
187 for i in 0..response.m_cEntryCount {
188 let mut raw_entry: MaybeUninit<sys::LeaderboardEntry_t> = MaybeUninit::uninit();
189 let mut details = vec![0; max_details as usize];
190 let success = unsafe {
191 sys::SteamAPI_ISteamUserStats_GetDownloadedLeaderboardEntry(
192 *self.client.0.user_stats,
193 response.m_hSteamLeaderboardEntries,
194 i,
195 raw_entry.as_mut_ptr(),
196 details.as_mut_ptr(),
197 max_details.into(),
198 )
199 };
200
201 assert!(success, "GetDownloadedLeaderboardEntry failed");
202 let raw_entry = unsafe { raw_entry.assume_init() };
203
204 details.truncate(raw_entry.m_cDetails as usize);
205 entries.push(LeaderboardEntry {
206 steam_id: raw_entry.m_steamIDUser.into(),
207 global_rank: raw_entry.m_nGlobalRank,
208 score: raw_entry.m_nScore,
209 details,
210 ugc: UgcHandle::from_inner(raw_entry.m_hUGC),
211 });
212 }
213
214 entries
215 }
216 }
217}
218
219#[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)]
220pub struct LeaderboardEntry {
221 pub steam_id: SteamId,
222 pub global_rank: i32,
223 pub score: i32,
224 pub details: Vec<i32>,
225 pub ugc: Option<UgcHandle>,
226}
227
228#[derive(Debug, Copy, Clone, Default, Hash, Eq, PartialEq, Ord, PartialOrd)]
229pub struct LeaderboardScoreUploaded {
230 pub score_changed: bool,
231 pub global_rank_new: i32,
232 pub global_rank_previous: i32,
233}
234
235#[derive(Debug, Clone, Eq, PartialEq, snafu::Snafu)]
236pub enum FindLeaderboardError {
237 #[snafu(display("The leaderboard name contains nul byte(s): {}", source))]
239 Nul { source: std::ffi::NulError },
240
241 #[snafu(display(
243 "The leaderboard name has a length of {} bytes, which is over the {} byte limit",
244 length,
245 steamworks_sys::k_cchLeaderboardNameMax
246 ))]
247 TooLong { length: usize },
248
249 #[snafu(display("The leaderboard {:?} was not found", leaderboard_name))]
251 NotFound { leaderboard_name: CString },
252}
253
254#[derive(Debug, Copy, Clone, Default, Hash, Eq, PartialEq, Ord, PartialOrd)]
255pub struct UploadLeaderboardScoreError;
256
257impl Display for UploadLeaderboardScoreError {
258 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
259 write!(
260 f,
261 "A call to the Steamworks function 'UploadLeaderboardScore()' failed"
262 )
263 }
264}
265
266impl Error for UploadLeaderboardScoreError {}
267
268pub(crate) fn find_leaderboard(
269 client: &Client,
270 leaderboard_name: Vec<u8>,
271) -> impl Future<Output = Result<LeaderboardHandle, FindLeaderboardError>> + Send + '_ {
272 static SEMAPHORE: Lazy<Semaphore> = Lazy::new(|| Semaphore::new(false, 256));
276
277 let leaderboard_name = CString::new(leaderboard_name);
278 async move {
279 let leaderboard_name = leaderboard_name.context(NulSnafu)?;
280 let leaderboard_name_bytes = leaderboard_name.as_bytes_with_nul();
281 ensure!(
282 leaderboard_name_bytes.len() - 1 <= sys::k_cchLeaderboardNameMax as usize,
283 TooLongSnafu {
284 length: leaderboard_name_bytes.len() - 1
285 }
286 );
287
288 let _releaser = SEMAPHORE.acquire(1).await;
289 let response: sys::LeaderboardFindResult_t = unsafe {
290 let handle = sys::SteamAPI_ISteamUserStats_FindLeaderboard(
291 *client.0.user_stats,
292 leaderboard_name_bytes.as_ptr() as *const i8,
293 );
294
295 client.register_for_call_result(handle).await
296 };
297
298 ensure!(
299 response.m_bLeaderboardFound != 0,
300 NotFoundSnafu { leaderboard_name }
301 );
302
303 Ok(LeaderboardHandle {
304 client: client.clone(),
305 handle: response.m_hSteamLeaderboard,
306 })
307 }
308}