From 1eb3fc52bc79feffb944011eff21554330a3b885 Mon Sep 17 00:00:00 2001 From: fixfell7 <30707948+fixfell7@users.noreply.github.com> Date: Tue, 2 Sep 2025 10:33:34 -0400 Subject: [PATCH] Leaderboards API integration --- client.d.ts | 41 ++++++ src/api/leaderboards.rs | 316 ++++++++++++++++++++++++++++++++++++++++ src/api/mod.rs | 1 + 3 files changed, 358 insertions(+) create mode 100644 src/api/leaderboards.rs diff --git a/client.d.ts b/client.d.ts index a2501ab..ba9c5c8 100644 --- a/client.d.ts +++ b/client.d.ts @@ -113,6 +113,47 @@ export declare namespace input { getHandle(): bigint } } +export declare namespace leaderboards { + export interface LeaderboardEntry { + globalRank: number + score: number + steamId: bigint + details: Array + } + + export const enum SortMethod { + Ascending = 0, + Descending = 1 + } + + export const enum DisplayType { + Numeric = 0, + TimeSeconds = 1, + TimeMilliSeconds = 2 + } + + export const enum DataRequest { + Global = 0, + GlobalAroundUser = 1, + Friends = 2 + } + + export const enum UploadScoreMethod { + KeepBest = 0, + ForceUpdate = 1 + } + + export function findLeaderboard(name: string): Promise + export function findOrCreateLeaderboard(name: string, sortMethod: SortMethod, displayType: DisplayType): Promise + export function uploadScore(leaderboardName: string, score: number, uploadMethod: UploadScoreMethod, details?: Array): Promise + export function downloadScores(leaderboardName: string, dataRequest: DataRequest, rangeStart: number, rangeEnd: number): Promise> + export function getLeaderboardName(leaderboardName: string): string | null + export function getLeaderboardEntryCount(leaderboardName: string): number | null + export function getLeaderboardSortMethod(leaderboardName: string): SortMethod | null + export function getLeaderboardDisplayType(leaderboardName: string): DisplayType | null + export function clearLeaderboardHandle(leaderboardName: string): boolean + export function getCachedLeaderboardNames(): Array +} export declare namespace localplayer { export function getSteamId(): PlayerSteamId export function getName(): string diff --git a/src/api/leaderboards.rs b/src/api/leaderboards.rs new file mode 100644 index 0000000..fe0c99e --- /dev/null +++ b/src/api/leaderboards.rs @@ -0,0 +1,316 @@ +use napi_derive::napi; + +#[napi] +pub mod leaderboards { + use napi::bindgen_prelude::BigInt; + use std::collections::HashMap; + use std::sync::{Arc, Mutex}; + use steamworks::{ + Leaderboard, LeaderboardDataRequest, LeaderboardDisplayType, + LeaderboardEntry as SteamLeaderboardEntry, LeaderboardSortMethod, SteamId, + UploadScoreMethod as SteamUploadScoreMethod, + }; + use tokio::sync::oneshot; + + #[napi(object)] + pub struct LeaderboardEntry { + pub global_rank: i32, + pub score: i32, + pub steam_id: BigInt, + pub details: Vec, + } + + #[napi] + pub enum SortMethod { + Ascending, + Descending, + } + + #[napi] + pub enum DisplayType { + Numeric, + TimeSeconds, + TimeMilliSeconds, + } + + #[napi] + pub enum DataRequest { + Global, + GlobalAroundUser, + Friends, + } + + #[napi] + pub enum UploadScoreMethod { + KeepBest, + ForceUpdate, + } + + // Static storage for leaderboard handles + lazy_static::lazy_static! { + static ref LEADERBOARD_HANDLES: Arc>> = + Arc::new(Mutex::new(HashMap::new())); + } + + impl From for LeaderboardSortMethod { + fn from(method: SortMethod) -> Self { + match method { + SortMethod::Ascending => LeaderboardSortMethod::Ascending, + SortMethod::Descending => LeaderboardSortMethod::Descending, + } + } + } + + impl From for LeaderboardDisplayType { + fn from(display_type: DisplayType) -> Self { + match display_type { + DisplayType::Numeric => LeaderboardDisplayType::Numeric, + DisplayType::TimeSeconds => LeaderboardDisplayType::TimeSeconds, + DisplayType::TimeMilliSeconds => LeaderboardDisplayType::TimeMilliSeconds, + } + } + } + + impl From for SteamUploadScoreMethod { + fn from(method: UploadScoreMethod) -> Self { + match method { + UploadScoreMethod::KeepBest => SteamUploadScoreMethod::KeepBest, + UploadScoreMethod::ForceUpdate => SteamUploadScoreMethod::ForceUpdate, + } + } + } + + impl From for LeaderboardEntry { + fn from(entry: SteamLeaderboardEntry) -> Self { + LeaderboardEntry { + global_rank: entry.global_rank, + score: entry.score, + steam_id: BigInt::from(entry.user.raw()), + details: entry.details, + } + } + } + + #[napi] + pub async fn find_leaderboard(name: String) -> Option { + let client = crate::client::get_client(); + let (tx, rx) = oneshot::channel(); + let mut tx = Some(tx); + + client.user_stats().find_leaderboard(&name, move |result| { + if let Some(sender) = tx.take() { + let _ = sender.send(result); + } + }); + + match rx.await { + Ok(Ok(Some(leaderboard))) => { + let mut handles = (*LEADERBOARD_HANDLES).lock().unwrap(); + handles.insert(name.clone(), leaderboard); + Some(name) + } + _ => None, + } + } + + #[napi] + pub async fn find_or_create_leaderboard( + name: String, + sort_method: SortMethod, + display_type: DisplayType, + ) -> Option { + let client = crate::client::get_client(); + let (tx, rx) = oneshot::channel(); + let mut tx = Some(tx); + + client.user_stats().find_or_create_leaderboard( + &name, + sort_method.into(), + display_type.into(), + move |result| { + if let Some(sender) = tx.take() { + let _ = sender.send(result); + } + }, + ); + + match rx.await { + Ok(Ok(Some(leaderboard))) => { + let mut handles = (*LEADERBOARD_HANDLES).lock().unwrap(); + handles.insert(name.clone(), leaderboard); + Some(name) + } + _ => None, + } + } + + #[napi] + pub async fn upload_score( + leaderboard_name: String, + score: i32, + upload_method: UploadScoreMethod, + details: Option>, + ) -> Option { + let client = crate::client::get_client(); + + // Get the leaderboard handle without holding the lock across await + let leaderboard = { + let handles = (*LEADERBOARD_HANDLES).lock().unwrap(); + handles.get(&leaderboard_name).cloned() + }; + + if let Some(leaderboard) = leaderboard { + let score_details = details.unwrap_or_default(); + let (tx, rx) = oneshot::channel(); + let mut tx = Some(tx); + + client.user_stats().upload_leaderboard_score( + &leaderboard, + upload_method.into(), + score, + &score_details, + move |result| { + if let Some(sender) = tx.take() { + let _ = sender.send(result); + } + }, + ); + + match rx.await { + Ok(Ok(Some(result))) => { + // Create a LeaderboardEntry from the result + Some(LeaderboardEntry { + global_rank: result.global_rank_new, + score: result.score, + steam_id: BigInt::from(client.user().steam_id().raw()), + details: score_details, + }) + } + _ => None, + } + } else { + None + } + } + + #[napi] + pub async fn download_scores( + leaderboard_name: String, + data_request: DataRequest, + range_start: i32, + range_end: i32, + ) -> Vec { + let client = crate::client::get_client(); + + // Get the leaderboard handle without holding the lock across await + let leaderboard = { + let handles = (*LEADERBOARD_HANDLES).lock().unwrap(); + handles.get(&leaderboard_name).cloned() + }; + + if let Some(leaderboard) = leaderboard { + let request_type = match data_request { + DataRequest::Global => LeaderboardDataRequest::Global, + DataRequest::GlobalAroundUser => LeaderboardDataRequest::GlobalAroundUser, + DataRequest::Friends => LeaderboardDataRequest::Friends, + }; + + let (tx, rx) = oneshot::channel(); + let mut tx = Some(tx); + + client.user_stats().download_leaderboard_entries( + &leaderboard, + request_type, + range_start as usize, + range_end as usize, + 0, // max_detail_data_size - 0 means no details + move |result| { + if let Some(sender) = tx.take() { + let _ = sender.send(result); + } + }, + ); + + match rx.await { + Ok(Ok(entries)) => entries.into_iter().map(LeaderboardEntry::from).collect(), + _ => Vec::new(), + } + } else { + Vec::new() + } + } + + #[napi] + pub fn get_leaderboard_name(leaderboard_name: String) -> Option { + let client = crate::client::get_client(); + let handles = (*LEADERBOARD_HANDLES).lock().unwrap(); + + if let Some(leaderboard) = handles.get(&leaderboard_name) { + Some(client.user_stats().get_leaderboard_name(leaderboard)) + } else { + None + } + } + + #[napi] + pub fn get_leaderboard_entry_count(leaderboard_name: String) -> Option { + let client = crate::client::get_client(); + let handles = (*LEADERBOARD_HANDLES).lock().unwrap(); + + if let Some(leaderboard) = handles.get(&leaderboard_name) { + Some(client.user_stats().get_leaderboard_entry_count(leaderboard)) + } else { + None + } + } + + #[napi] + pub fn get_leaderboard_sort_method(leaderboard_name: String) -> Option { + let client = crate::client::get_client(); + let handles = (*LEADERBOARD_HANDLES).lock().unwrap(); + + if let Some(leaderboard) = handles.get(&leaderboard_name) { + match client.user_stats().get_leaderboard_sort_method(leaderboard) { + Some(LeaderboardSortMethod::Ascending) => Some(SortMethod::Ascending), + Some(LeaderboardSortMethod::Descending) => Some(SortMethod::Descending), + None => None, + } + } else { + None + } + } + + #[napi] + pub fn get_leaderboard_display_type(leaderboard_name: String) -> Option { + let client = crate::client::get_client(); + let handles = (*LEADERBOARD_HANDLES).lock().unwrap(); + + if let Some(leaderboard) = handles.get(&leaderboard_name) { + match client + .user_stats() + .get_leaderboard_display_type(leaderboard) + { + Some(LeaderboardDisplayType::Numeric) => Some(DisplayType::Numeric), + Some(LeaderboardDisplayType::TimeSeconds) => Some(DisplayType::TimeSeconds), + Some(LeaderboardDisplayType::TimeMilliSeconds) => { + Some(DisplayType::TimeMilliSeconds) + } + None => None, + } + } else { + None + } + } + + #[napi] + pub fn clear_leaderboard_handle(leaderboard_name: String) -> bool { + let mut handles = (*LEADERBOARD_HANDLES).lock().unwrap(); + handles.remove(&leaderboard_name).is_some() + } + + #[napi] + pub fn get_cached_leaderboard_names() -> Vec { + let handles = (*LEADERBOARD_HANDLES).lock().unwrap(); + handles.keys().cloned().collect() + } +} diff --git a/src/api/mod.rs b/src/api/mod.rs index 22161ba..4b97b1c 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -4,6 +4,7 @@ pub mod auth; pub mod callback; pub mod cloud; pub mod input; +pub mod leaderboards; pub mod localplayer; pub mod matchmaking; pub mod networking;