Skip to content

Commit ea9fa2b

Browse files
committed
Merge ceifa/steamworks.js PR ceifa#198: Leaderboards
2 parents 47c890e + 1eb3fc5 commit ea9fa2b

File tree

3 files changed

+358
-0
lines changed

3 files changed

+358
-0
lines changed

client.d.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,47 @@ export declare namespace input {
113113
getHandle(): bigint
114114
}
115115
}
116+
export declare namespace leaderboards {
117+
export interface LeaderboardEntry {
118+
globalRank: number
119+
score: number
120+
steamId: bigint
121+
details: Array<number>
122+
}
123+
124+
export const enum SortMethod {
125+
Ascending = 0,
126+
Descending = 1
127+
}
128+
129+
export const enum DisplayType {
130+
Numeric = 0,
131+
TimeSeconds = 1,
132+
TimeMilliSeconds = 2
133+
}
134+
135+
export const enum DataRequest {
136+
Global = 0,
137+
GlobalAroundUser = 1,
138+
Friends = 2
139+
}
140+
141+
export const enum UploadScoreMethod {
142+
KeepBest = 0,
143+
ForceUpdate = 1
144+
}
145+
146+
export function findLeaderboard(name: string): Promise<string | null>
147+
export function findOrCreateLeaderboard(name: string, sortMethod: SortMethod, displayType: DisplayType): Promise<string | null>
148+
export function uploadScore(leaderboardName: string, score: number, uploadMethod: UploadScoreMethod, details?: Array<number>): Promise<LeaderboardEntry | null>
149+
export function downloadScores(leaderboardName: string, dataRequest: DataRequest, rangeStart: number, rangeEnd: number): Promise<Array<LeaderboardEntry>>
150+
export function getLeaderboardName(leaderboardName: string): string | null
151+
export function getLeaderboardEntryCount(leaderboardName: string): number | null
152+
export function getLeaderboardSortMethod(leaderboardName: string): SortMethod | null
153+
export function getLeaderboardDisplayType(leaderboardName: string): DisplayType | null
154+
export function clearLeaderboardHandle(leaderboardName: string): boolean
155+
export function getCachedLeaderboardNames(): Array<string>
156+
}
116157
export declare namespace localplayer {
117158
export function getSteamId(): PlayerSteamId
118159
export function getName(): string

src/api/leaderboards.rs

Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
use napi_derive::napi;
2+
3+
#[napi]
4+
pub mod leaderboards {
5+
use napi::bindgen_prelude::BigInt;
6+
use std::collections::HashMap;
7+
use std::sync::{Arc, Mutex};
8+
use steamworks::{
9+
Leaderboard, LeaderboardDataRequest, LeaderboardDisplayType,
10+
LeaderboardEntry as SteamLeaderboardEntry, LeaderboardSortMethod, SteamId,
11+
UploadScoreMethod as SteamUploadScoreMethod,
12+
};
13+
use tokio::sync::oneshot;
14+
15+
#[napi(object)]
16+
pub struct LeaderboardEntry {
17+
pub global_rank: i32,
18+
pub score: i32,
19+
pub steam_id: BigInt,
20+
pub details: Vec<i32>,
21+
}
22+
23+
#[napi]
24+
pub enum SortMethod {
25+
Ascending,
26+
Descending,
27+
}
28+
29+
#[napi]
30+
pub enum DisplayType {
31+
Numeric,
32+
TimeSeconds,
33+
TimeMilliSeconds,
34+
}
35+
36+
#[napi]
37+
pub enum DataRequest {
38+
Global,
39+
GlobalAroundUser,
40+
Friends,
41+
}
42+
43+
#[napi]
44+
pub enum UploadScoreMethod {
45+
KeepBest,
46+
ForceUpdate,
47+
}
48+
49+
// Static storage for leaderboard handles
50+
lazy_static::lazy_static! {
51+
static ref LEADERBOARD_HANDLES: Arc<Mutex<HashMap<String, Leaderboard>>> =
52+
Arc::new(Mutex::new(HashMap::new()));
53+
}
54+
55+
impl From<SortMethod> for LeaderboardSortMethod {
56+
fn from(method: SortMethod) -> Self {
57+
match method {
58+
SortMethod::Ascending => LeaderboardSortMethod::Ascending,
59+
SortMethod::Descending => LeaderboardSortMethod::Descending,
60+
}
61+
}
62+
}
63+
64+
impl From<DisplayType> for LeaderboardDisplayType {
65+
fn from(display_type: DisplayType) -> Self {
66+
match display_type {
67+
DisplayType::Numeric => LeaderboardDisplayType::Numeric,
68+
DisplayType::TimeSeconds => LeaderboardDisplayType::TimeSeconds,
69+
DisplayType::TimeMilliSeconds => LeaderboardDisplayType::TimeMilliSeconds,
70+
}
71+
}
72+
}
73+
74+
impl From<UploadScoreMethod> for SteamUploadScoreMethod {
75+
fn from(method: UploadScoreMethod) -> Self {
76+
match method {
77+
UploadScoreMethod::KeepBest => SteamUploadScoreMethod::KeepBest,
78+
UploadScoreMethod::ForceUpdate => SteamUploadScoreMethod::ForceUpdate,
79+
}
80+
}
81+
}
82+
83+
impl From<SteamLeaderboardEntry> for LeaderboardEntry {
84+
fn from(entry: SteamLeaderboardEntry) -> Self {
85+
LeaderboardEntry {
86+
global_rank: entry.global_rank,
87+
score: entry.score,
88+
steam_id: BigInt::from(entry.user.raw()),
89+
details: entry.details,
90+
}
91+
}
92+
}
93+
94+
#[napi]
95+
pub async fn find_leaderboard(name: String) -> Option<String> {
96+
let client = crate::client::get_client();
97+
let (tx, rx) = oneshot::channel();
98+
let mut tx = Some(tx);
99+
100+
client.user_stats().find_leaderboard(&name, move |result| {
101+
if let Some(sender) = tx.take() {
102+
let _ = sender.send(result);
103+
}
104+
});
105+
106+
match rx.await {
107+
Ok(Ok(Some(leaderboard))) => {
108+
let mut handles = (*LEADERBOARD_HANDLES).lock().unwrap();
109+
handles.insert(name.clone(), leaderboard);
110+
Some(name)
111+
}
112+
_ => None,
113+
}
114+
}
115+
116+
#[napi]
117+
pub async fn find_or_create_leaderboard(
118+
name: String,
119+
sort_method: SortMethod,
120+
display_type: DisplayType,
121+
) -> Option<String> {
122+
let client = crate::client::get_client();
123+
let (tx, rx) = oneshot::channel();
124+
let mut tx = Some(tx);
125+
126+
client.user_stats().find_or_create_leaderboard(
127+
&name,
128+
sort_method.into(),
129+
display_type.into(),
130+
move |result| {
131+
if let Some(sender) = tx.take() {
132+
let _ = sender.send(result);
133+
}
134+
},
135+
);
136+
137+
match rx.await {
138+
Ok(Ok(Some(leaderboard))) => {
139+
let mut handles = (*LEADERBOARD_HANDLES).lock().unwrap();
140+
handles.insert(name.clone(), leaderboard);
141+
Some(name)
142+
}
143+
_ => None,
144+
}
145+
}
146+
147+
#[napi]
148+
pub async fn upload_score(
149+
leaderboard_name: String,
150+
score: i32,
151+
upload_method: UploadScoreMethod,
152+
details: Option<Vec<i32>>,
153+
) -> Option<LeaderboardEntry> {
154+
let client = crate::client::get_client();
155+
156+
// Get the leaderboard handle without holding the lock across await
157+
let leaderboard = {
158+
let handles = (*LEADERBOARD_HANDLES).lock().unwrap();
159+
handles.get(&leaderboard_name).cloned()
160+
};
161+
162+
if let Some(leaderboard) = leaderboard {
163+
let score_details = details.unwrap_or_default();
164+
let (tx, rx) = oneshot::channel();
165+
let mut tx = Some(tx);
166+
167+
client.user_stats().upload_leaderboard_score(
168+
&leaderboard,
169+
upload_method.into(),
170+
score,
171+
&score_details,
172+
move |result| {
173+
if let Some(sender) = tx.take() {
174+
let _ = sender.send(result);
175+
}
176+
},
177+
);
178+
179+
match rx.await {
180+
Ok(Ok(Some(result))) => {
181+
// Create a LeaderboardEntry from the result
182+
Some(LeaderboardEntry {
183+
global_rank: result.global_rank_new,
184+
score: result.score,
185+
steam_id: BigInt::from(client.user().steam_id().raw()),
186+
details: score_details,
187+
})
188+
}
189+
_ => None,
190+
}
191+
} else {
192+
None
193+
}
194+
}
195+
196+
#[napi]
197+
pub async fn download_scores(
198+
leaderboard_name: String,
199+
data_request: DataRequest,
200+
range_start: i32,
201+
range_end: i32,
202+
) -> Vec<LeaderboardEntry> {
203+
let client = crate::client::get_client();
204+
205+
// Get the leaderboard handle without holding the lock across await
206+
let leaderboard = {
207+
let handles = (*LEADERBOARD_HANDLES).lock().unwrap();
208+
handles.get(&leaderboard_name).cloned()
209+
};
210+
211+
if let Some(leaderboard) = leaderboard {
212+
let request_type = match data_request {
213+
DataRequest::Global => LeaderboardDataRequest::Global,
214+
DataRequest::GlobalAroundUser => LeaderboardDataRequest::GlobalAroundUser,
215+
DataRequest::Friends => LeaderboardDataRequest::Friends,
216+
};
217+
218+
let (tx, rx) = oneshot::channel();
219+
let mut tx = Some(tx);
220+
221+
client.user_stats().download_leaderboard_entries(
222+
&leaderboard,
223+
request_type,
224+
range_start as usize,
225+
range_end as usize,
226+
0, // max_detail_data_size - 0 means no details
227+
move |result| {
228+
if let Some(sender) = tx.take() {
229+
let _ = sender.send(result);
230+
}
231+
},
232+
);
233+
234+
match rx.await {
235+
Ok(Ok(entries)) => entries.into_iter().map(LeaderboardEntry::from).collect(),
236+
_ => Vec::new(),
237+
}
238+
} else {
239+
Vec::new()
240+
}
241+
}
242+
243+
#[napi]
244+
pub fn get_leaderboard_name(leaderboard_name: String) -> Option<String> {
245+
let client = crate::client::get_client();
246+
let handles = (*LEADERBOARD_HANDLES).lock().unwrap();
247+
248+
if let Some(leaderboard) = handles.get(&leaderboard_name) {
249+
Some(client.user_stats().get_leaderboard_name(leaderboard))
250+
} else {
251+
None
252+
}
253+
}
254+
255+
#[napi]
256+
pub fn get_leaderboard_entry_count(leaderboard_name: String) -> Option<i32> {
257+
let client = crate::client::get_client();
258+
let handles = (*LEADERBOARD_HANDLES).lock().unwrap();
259+
260+
if let Some(leaderboard) = handles.get(&leaderboard_name) {
261+
Some(client.user_stats().get_leaderboard_entry_count(leaderboard))
262+
} else {
263+
None
264+
}
265+
}
266+
267+
#[napi]
268+
pub fn get_leaderboard_sort_method(leaderboard_name: String) -> Option<SortMethod> {
269+
let client = crate::client::get_client();
270+
let handles = (*LEADERBOARD_HANDLES).lock().unwrap();
271+
272+
if let Some(leaderboard) = handles.get(&leaderboard_name) {
273+
match client.user_stats().get_leaderboard_sort_method(leaderboard) {
274+
Some(LeaderboardSortMethod::Ascending) => Some(SortMethod::Ascending),
275+
Some(LeaderboardSortMethod::Descending) => Some(SortMethod::Descending),
276+
None => None,
277+
}
278+
} else {
279+
None
280+
}
281+
}
282+
283+
#[napi]
284+
pub fn get_leaderboard_display_type(leaderboard_name: String) -> Option<DisplayType> {
285+
let client = crate::client::get_client();
286+
let handles = (*LEADERBOARD_HANDLES).lock().unwrap();
287+
288+
if let Some(leaderboard) = handles.get(&leaderboard_name) {
289+
match client
290+
.user_stats()
291+
.get_leaderboard_display_type(leaderboard)
292+
{
293+
Some(LeaderboardDisplayType::Numeric) => Some(DisplayType::Numeric),
294+
Some(LeaderboardDisplayType::TimeSeconds) => Some(DisplayType::TimeSeconds),
295+
Some(LeaderboardDisplayType::TimeMilliSeconds) => {
296+
Some(DisplayType::TimeMilliSeconds)
297+
}
298+
None => None,
299+
}
300+
} else {
301+
None
302+
}
303+
}
304+
305+
#[napi]
306+
pub fn clear_leaderboard_handle(leaderboard_name: String) -> bool {
307+
let mut handles = (*LEADERBOARD_HANDLES).lock().unwrap();
308+
handles.remove(&leaderboard_name).is_some()
309+
}
310+
311+
#[napi]
312+
pub fn get_cached_leaderboard_names() -> Vec<String> {
313+
let handles = (*LEADERBOARD_HANDLES).lock().unwrap();
314+
handles.keys().cloned().collect()
315+
}
316+
}

src/api/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ pub mod auth;
44
pub mod callback;
55
pub mod cloud;
66
pub mod input;
7+
pub mod leaderboards;
78
pub mod localplayer;
89
pub mod matchmaking;
910
pub mod networking;

0 commit comments

Comments
 (0)