-
Notifications
You must be signed in to change notification settings - Fork 0
Leaderboards
Since v0.3.0. steam.leaderboards is the curated layer over the leaderboard
half of ISteamUserStats: find or create a leaderboard, upload a score,
download entries, attach a replay.
Every method is async and awaits its call result through the dispatch pump,
see How It Works. Failures are thrown rather than reported as a
flag on a struct: a non-OK EResult becomes a SteamResultError, and Steam's
own boolean failure flags become plain Errors. See Errors.
Every method except find and findOrCreate takes the
handle (SteamLeaderboard_t), not the name, so keep the
LeaderboardInfo around for as long as you use the
leaderboard. Handles are 64-bit, so bigint, like every other Steam id here.
The achievements and per-user stats on the same interface are on Stats.
find(name: string): Promise<LeaderboardInfo | null>Looks a leaderboard up by name. The name is case sensitive and capped at 128 UTF-8 bytes.
Returns null when the app has no leaderboard with that name. That is the
difference to findOrCreate, which throws instead. A missing
leaderboard is a normal answer here, so treat the null as data, not as an
error.
const board = await steam.leaderboards.find('Fastest Lap');
if (board) console.log(board.handle, board.entryCount);Throws SteamApiCallError only if the call itself could not be completed.
findOrCreate(
name: string,
sortMethod: number,
displayType: number,
): Promise<LeaderboardInfo>The same lookup, but it creates the leaderboard when the app has none.
| Parameter | Type | Meaning |
|---|---|---|
name |
string |
Leaderboard name, max 128 UTF-8 bytes, case sensitive. |
sortMethod |
number |
ELeaderboardSortMethod: 1 ascending (lowest score is best), 2 descending. |
displayType |
number |
ELeaderboardDisplayType: 1 numeric, 2 seconds, 3 milliseconds. |
The last two arguments only apply to a leaderboard this call creates. An
existing leaderboard keeps whatever it was created with, and passing different
values here does not change it. Read the real settings back from the returned
sortMethod and displayType.
import { init, flat } from 'steamwand.js';
const steam = init({ appId: 480 });
const board = await steam.leaderboards.findOrCreate(
'Fastest Lap',
flat.ELeaderboardSortMethod.k_ELeaderboardSortMethodAscending,
flat.ELeaderboardDisplayType.k_ELeaderboardDisplayTypeTimeMilliSeconds,
);
steam.close();Throws
Error: steamwand: FindOrCreateLeaderboard could not find or create "<name>"
when Steam did neither.
uploadScore(
handle: bigint,
score: number,
opts?: { method?: number; details?: number[] },
): Promise<ScoreUploadResult>Uploads the current user's score.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
handle |
bigint |
none | From find or findOrCreate. |
score |
number |
none | Steam stores a 32-bit signed integer, so scale times and floats yourself. |
opts.method |
number |
k_ELeaderboardUploadScoreMethodKeepBest |
ELeaderboardUploadScoreMethod. |
opts.details |
number[] |
none | Game defined details stored with the entry, at most 64 int32 values. |
With the default keep-best method Steam drops the upload when the user already
has a better score and reports that as scoreChanged: false. That is not an
error, and newGlobalRank still holds the rank the old score has. Force-update
(k_ELeaderboardUploadScoreMethodForceUpdate) always overwrites, which is what
a "reset my time" button needs.
const { scoreChanged, newGlobalRank } = await steam.leaderboards.uploadScore(
board.handle,
91_240,
);
console.log(scoreChanged ? `now rank ${newGlobalRank}` : 'kept the old score');Throws
Error: steamwand: UploadLeaderboardScore failed (leaderboard handle or score rejected)
when Steam clears the success flag on the result.
downloadEntries(handle: bigint, opts?: DownloadOptions): Promise<LeaderboardEntry[]>Downloads a range of entries, best first.
How the range is read depends on dataRequest, and this is the part that
surprises people. Global takes absolute 1-based ranks, so 1 to 10 is the
top ten. Around-user takes offsets from the user's own rank, so -4 to 5 is a
ten entry window centred on them, and 1 to 10 would be the ten people
behind them. Friends ignores the range entirely and returns every friend with
an entry.
import { init, flat } from 'steamwand.js';
const steam = init({ appId: 480 });
const board = await steam.leaderboards.findOrCreate('Fastest Lap', 1, 3);
const top = await steam.leaderboards.downloadEntries(board.handle, {
rangeStart: 1,
rangeEnd: 10,
});
const nearMe = await steam.leaderboards.downloadEntries(board.handle, {
dataRequest: flat.ELeaderboardDataRequest.k_ELeaderboardDataRequestGlobalAroundUser,
rangeStart: -4,
rangeEnd: 5,
});
for (const e of top) console.log(e.globalRank, e.steamId, e.score);
steam.close();Steam can return fewer entries than the range asked for, near the ends of the board or when the user has no entry of their own.
downloadEntriesForUsers(
handle: bigint,
steamIds: bigint[],
maxDetails?: number,
): Promise<LeaderboardEntry[]>The entries of a named set of users, for example everyone in a
lobby. Steam caps the request at 100 users. Users without
an entry on this leaderboard are left out, so the result can be shorter than
steamIds, and the order is by rank, not by the order you passed the ids in.
const members = steam.lobbies.getMembers(lobbyId);
const scores = await steam.leaderboards.downloadEntriesForUsers(board.handle, members);Throws
Error: steamwand: downloadEntriesForUsers needs at least one Steam id for an
empty array, before any native call.
attachUgc(handle: bigint, ugcHandle: bigint): Promise<void>Attaches a piece of UGC, usually a replay file, to the current user's entry on this leaderboard. The user must already have an entry, and one entry holds one UGC handle, so a second call replaces the first.
ugcHandle is a UGCHandle_t, which you get by sharing a cloud file with
steam.remoteStorage.FileShare. That call is not part of the curated cloud
layer, see Cloud. Entries you download
carry the handle back as ugcHandle.
Throws SteamResultError with operation: 'AttachLeaderboardUGC', typically
k_EResultFail when the user has no entry on this leaderboard.
Returned by find and findOrCreate. The four properties beside the handle are local reads against the client's copy, so they cost no round trip.
| Field | Type | Meaning |
|---|---|---|
handle |
bigint |
SteamLeaderboard_t. Pass it to every other method. |
name |
string |
Leaderboard name as Steam has it. |
entryCount |
number |
Entries on the leaderboard right now. |
sortMethod |
number |
ELeaderboardSortMethod: 0 none, 1 ascending, 2 descending. |
displayType |
number |
ELeaderboardDisplayType: 0 none, 1 numeric, 2 seconds, 3 milliseconds. |
Returned by uploadScore.
| Field | Type | Meaning |
|---|---|---|
scoreChanged |
boolean |
True when the score replaced the previous one. False when keep-best kept the old score. |
newGlobalRank |
number |
Rank after the upload, 1-based. |
previousGlobalRank |
number |
Rank before it, 1-based, or 0 when the user had no entry yet. |
One row from downloadEntries or downloadEntriesForUsers.
| Field | Type | Meaning |
|---|---|---|
steamId |
bigint |
Steam id of the user who set the score. |
globalRank |
number |
Rank on the leaderboard, 1-based. |
score |
number |
The score. Always a 32-bit signed integer. |
details |
number[] |
Game defined details, at most maxDetails of them. Empty when none were requested or stored. |
ugcHandle |
bigint |
UGCHandle_t of the attached replay, or k_UGCHandleInvalid when the entry has none. |
The entry set handle Steam returns is only valid until the next download, so this layer decodes every row before it resolves. There is no cursor to hold on to and nothing to release.
Every field is optional.
| Field | Type | Default | Meaning |
|---|---|---|---|
dataRequest |
number |
k_ELeaderboardDataRequestGlobal |
ELeaderboardDataRequest: 0 global, 1 around user, 2 friends. |
rangeStart |
number |
1 |
First entry. Absolute rank for global, offset from the user's rank for around-user, ignored for friends. |
rangeEnd |
number |
10 |
Last entry, read the same way. |
maxDetails |
number |
0 |
Details to read per entry. Steam stores at most 64. At 0 no details buffer is allocated at all. |
| Shape | When |
|---|---|
SteamResultError |
AttachLeaderboardUGC completed with a non-OK EResult. It is the only method here that reports through an EResult. |
Error: steamwand: FindOrCreateLeaderboard could not find or create "<name>" |
Steam cleared the found flag on the result. |
Error: steamwand: UploadLeaderboardScore failed (...) |
Steam cleared the success flag on the upload result. |
Error: steamwand: downloadEntriesForUsers needs at least one Steam id |
An empty steamIds array. |
Error: steamwand: GetDownloadedLeaderboardEntry returned false (...) |
A row inside the count Steam reported could not be read. |
SteamApiCallError |
A call never produced a usable result, or completed carrying a different callback struct. |
Note the shape of the two "failed" errors: Steam reports find and upload
failures as a boolean on the result struct, not as an EResult, so there is no
result code to attach and no SteamResultError to catch.
Six methods cover the whole leaderboard surface of ISteamUserStats, so little
is left over. What this layer hides rather than drops:
- The raw entry-set handle from
LeaderboardScoresDownloaded_t, and theGetDownloadedLeaderboardEntryloop over it. Decoding happens before the promise resolves, because the handle dies at the next download. - The four separate property reads behind
LeaderboardInfo(GetLeaderboardName,GetLeaderboardEntryCount,GetLeaderboardSortMethod,GetLeaderboardDisplayType).
Both are on the raw generated steam.userStats if you want them. Await the call
handles through steam.dispatch, or use steam.async.userStats, which returns
the undecoded result structs as promises. Flat API explains the
calling convention, and achievements and stats are on Stats.