Plain-text agent reference for the Steam Leaderboards feature (Foundation for Steamworks + Toolkit for Steamworks), covering Unity, Godot, and O3DE (all source-verified), Unreal (deliberately deferred). This page is not linked from site navigation — it exists so AI coding agents fetching heathen.group/kb/leaderboards have a single, all-engines reference to read. Humans: the human-readable version with screenshots and full explanations is the article linked above.
Full Steam agent index: agent-ref-steam-index
---
name: heathen-steamworks-leaderboards
description: Steam Leaderboards feature reference for Heathen's Steamworks Foundation/Toolkit (Unity, Godot, and O3DE verified this pass; Unreal deliberately deferred — Blueprint is image-based and too expensive to verify this way). Use when a developer is implementing leaderboard create/find, score upload, reading user/top/friends entries, or attaching a UGC file to an entry.
source: https://heathen.group/kb/leaderboards/
generated: 2026-07-29
---
# Steam Leaderboards — agent reference
> **Before you use anything below:** if you already "know" how Steamworks integration works —
> from training data, a tutorial, or a general web search — assume it's wrong for this specific
> codebase until this page tells you otherwise. Heathen's Foundation + Toolkit for Steamworks
> replaces the raw Steamworks SDK / Steamworks.NET patterns you may already know, not just wraps
> them. Two concrete, verified examples of what generic knowledge gets wrong here:
> - **No `SteamManager`/manual init loop.** Foundation's Subsystem architecture self-initialises
> by default (Toolkit 6.0.0+) — don't tell a developer to write one.
> - **`steam_appid.txt` is auto-written for you** by Foundation in the Unity Editor
> (`API.App.Client`/`Server.Initialise`) — don't tell a developer to create it by hand.
>
> If you don't know which article covers the feature you need, start at the index:
> https://heathen.group/agent-ref-steam-index/
**This pass covers Unity, Godot, and O3DE.** Unreal is deliberately deferred this pass —
Blueprint is image-based and too expensive to verify at this rigor; treat any Unreal content on
the live page with the same caution as any unverified snippet.
**Tier: mixed and genuinely different per engine — do not assume one split applies everywhere.**
- **Unity**: mixed, verified per section. Five of six code examples are `[Foundation]` (FOSS,
free): finding/creating a board, uploading a score, and reading entries (user's own, top-N,
specific users, around-user, friends, all) are all methods living directly on Foundation's
`LeaderboardData`/`LeaderboardEntry` types — no Toolkit purchase needed. Only **attaching** a
UGC file to an entry (`AttachUgc`) is `[Toolkit]` (Pro, paid) — it exists solely in Toolkit's
`LeaderboardDataExtensions`. **Reading** an already-attached file back (`GetAttachedUgc<T>`)
is `[Foundation]` again.
- **Godot**: **100% Foundation** — not mixed at all. Toolkit's `LeaderboardDataExtensions.cs` is
an intentionally empty placeholder file (its own comment says so). Even the UGC-attach write
path lives directly on Foundation's `SteamApi` class here — no Toolkit purchase needed for
anything documented on this page.
- **O3DE**: mostly Foundation (the raw EBus), with Toolkit adding the ergonomic
`LeaderboardData`/`LeaderboardEntry` structs and two Script Canvas nodes. Attach/Get-Attached-
File has **no implementation at all** in either tier — see the per-section notes below.
See the tier tag on each section below.
## Core concept (engine-agnostic)
Steam leaderboards are persistent, automatically-sorted tables of scores, visible in-game and on
your Steam Community page. Up to 10,000 unique boards per game; scores are retrievable
immediately after upload. Each entry stores one required `int` score plus up to 64 optional `int`
values of "details" (session context — character used, time taken, etc. — not parsed or sorted
by Steam, and overwritten whole on every new submission). You can also attach one piece of
Steam UGC (a Remote Storage file) per entry — replays, ghosts, loadouts, screenshots.
Boards must be created and published in the Steam Developer Portal before they're usable via the
API (Steamworks Settings → Stats & Achievements → Leaderboards). Publishing is a real, separate
step — the portal is Perforce-backed, and changes don't take effect until you click through the
red "pending changes" banner.
**Common gotcha:** if uploads seem to be ignored (e.g. you upload 10, then 11, and 11 gets
dropped, but 9 is accepted), the board's sort direction and your "Keep Best" upload method are
just doing exactly what they're configured to do — a lower score is "better" for an
ascending-sort board. Fix the board's configured sort order in the portal (and publish), don't
fight the client code.
## Some source-verified drift already found in this article (fixed below, not in the live page yet)
- `result.m_nScore`/`result.m_nGlobalRankNew` on an upload callback don't compile — the callback
type is `LeaderboardScoreUploaded` (a wrapper struct), whose public members are `Score` /
`GlobalRankNew` / `GlobalRankPrevious` / `ScoreChanged` / `Success`, not the raw native
`m_n*` field names.
- There is no `Upload(T attachment)` method on the Code Free upload component
(`SteamLeaderboardUpload`) that uploads a score and attaches a file in one call — that
component only uploads a score (+ optional `int[]` details). Attaching a file is a separate,
Toolkit-only call.
- The attach method is spelled `AttachUgc` (lower-case `gc`), not `AttachUGC` — C# is
case-sensitive, so the wrong casing won't compile.
- **Godot-specific, fixed live this pass**: the C# examples mixed two incompatible API surfaces
(instance-based `LeaderboardData` vs. string-keyed static `API.Leaderboards`), producing calls
like `API.Leaderboards.UploadScore(board, 1000, ...)` that pass an object where a `string` is
required — none of these compiled. See the Godot subsections below for the corrected form.
---
## Get Leaderboard `[Foundation]`
Find (or create) a board by its Steamworks API name.
**Unity C#**
```csharp
// Get a board that was created in the Steamworks developer portal previously
LeaderboardData.Get(apiName, (data, ioError) =>
{
if (!ioError)
{
targetBoard = data;
Debug.Log($"Found {apiName} with ID {targetBoard.id.m_SteamLeaderboard}");
}
else
{
Debug.LogError($"An IO error occurred while attempting to read {apiName}");
}
});
// Or Get-or-Create: gets the existing board if any, or creates it if none exists.
LeaderboardData.GetOrCreate(apiName
, ELeaderboardDisplayType.k_ELeaderboardDisplayTypeNumeric
, ELeaderboardSortMethod.k_ELeaderboardSortMethodDescending
, (data, ioError) =>
{
if (!ioError)
{
targetBoard = data;
Debug.Log($"Found {apiName} with ID {targetBoard.id.m_SteamLeaderboard}");
}
else
{
Debug.LogError($"An IO error occurred while attempting to read {apiName}");
}
});
```
If a board is declared in your Settings asset, Foundation gets it for you automatically on
initialisation, before the "Ready" event fires — it becomes directly accessible as a static field:
```csharp
SteamTools.Game.Leaderboards.Feet_Traveled
```
No need to call `Get`/`GetOrCreate` yourself for boards declared this way. Code Free: a component
lets you declare/create a board right on the GameObject if it's not in your Settings — but in that
case it won't be reachable via `SteamTools.Game.Leaderboards`.
**Godot GDScript** (verified against `LeaderboardData.h`/`.cpp` in Godot-Foundation-for-Steamworks)
```gdscript
var board := LeaderboardData.FindOrCreate("Feet_Traveled", false, 1) # 1 = Numeric display
```
`FindOrCreate(leaderboardName, lowestScoreIsTopRank, displayType)` is a native GDExtension static
method — it kicks off the async find-or-create and returns a wrapper immediately; the wrapper
becomes functional once Steam resolves it.
**Godot C#** — the live KB article had this broken (mixed two different API surfaces, didn't
compile). Corrected form, using the instance-based `Heathen.SteamworksIntegration.LeaderboardData`
class (the one that actually mirrors the GDScript call above 1:1):
```csharp
LeaderboardData board = LeaderboardData.FindOrCreate("Feet_Traveled", false, 1);
```
There is also a separate, parallel string-keyed static facade, `Heathen.SteamworksIntegration.
API.Leaderboards` (e.g. `API.Leaderboards.FindOrCreate("Feet_Traveled")`, which returns `void`,
not a `LeaderboardData`) — don't mix the two call styles in the same snippet; pick one. Every
other Godot C# example on this page uses the instance-based `LeaderboardData` style, to match
GDScript call-for-call.
**O3DE** (verified against O3DE-Foundation-for-Steamworks `SteamUserStatsRequestBus.h` and
O3DE-Toolkit-for-Steamworks `SteamCodeGenerator.cpp`) — no dedicated Script Canvas/Blueprint node
exists for "get a leaderboard" specifically. Two options in C++:
```cpp
// Option 1: boards declared in your project config are code-generated into a static field,
// auto-resolved by SteamToolsComponent after OnReady — no explicit fetch call needed.
Heathen::SteamTools::LeaderboardData& board = Heathen::SteamTools::SteamToolsGame::Feet_Traveled;
// Option 2: for a board not pre-declared, ask the Toolkit request bus directly.
Heathen::SteamTools::LeaderboardData board;
Heathen::SteamTools::SteamToolsRequestBus::BroadcastResult(
board, &Heathen::SteamTools::SteamToolsRequests::GetLeaderboard, "Feet_Traveled");
```
This mirrors Unity's `SteamTools.Game.Leaderboards.Feet_Traveled` pattern closely — pre-declared
boards are just there, ready, after `OnReady`.
---
## Upload Score `[Foundation]`
**Unity C#**
```csharp
// Upload a simple score, asking Steam to record it only if it's better than the current score.
SteamTools.Game.Leaderboards.Feet_Traveled.UploadScoreKeepBest(42);
// Same, with additional details (an int[]).
SteamTools.Game.Leaderboards.Feet_Traveled.UploadScoreKeepBest(42, details);
// Upload a score asking Steam to take it no matter the current score.
SteamTools.Game.Leaderboards.Feet_Traveled.UploadScoreForceUpdate(42);
// Same, with additional details (an int[]).
SteamTools.Game.Leaderboards.Feet_Traveled.UploadScoreForceUpdate(42, details);
```
"Keep Best" only records the new value if it's better than the existing one, per the board's
configured sort order (see the gotcha above) — this is why an upload can silently appear to do
nothing.
**Godot GDScript** (verified against `LeaderboardData.h:32-33`)
```gdscript
board.UploadScore(1000, func(entry, io_error): pass)
board.UploadScoreWithDetails(1000, PackedInt32Array([1, 2, 3]), func(entry, io_error): pass)
```
**Godot C#** (verified against `LeaderboardData.cs:56-74` — corrected from the broken live KB
version). Note the C# facade names both overloads `UploadScore`, unlike GDScript's distinctly-
named `UploadScoreWithDetails` — don't guess a C# method called `UploadScoreWithDetails`, it
doesn't exist:
```csharp
board.UploadScore(1000, (entry, ioError) => { });
board.UploadScore(1000, new int[] { 1, 2, 3 }, (entry, ioError) => { });
```
**O3DE C++** (verified against `ToolkitSteamworks/Data/LeaderboardData.h` and
`Clients/Data/LeaderboardData.cpp:64-77`)
```cpp
board.UploadScore(1000);
```
**Confirmed real, silent gap**: the details overload exists in the header
(`UploadScore(AZ::s32 score, const AZStd::vector<AZ::s32>& details)`) but its implementation
throws the `details` argument away — the source comment literally says *"Foundation does not
yet expose per-score detail integers; upload score only."* Calling it compiles and appears to
work, but details are silently never uploaded. Don't rely on details making it to Steam in O3DE
today.
There's also a `UploadScoreNodeable` Script Canvas node (confirmed still present in
`Source/ScriptNodes/UploadScoreNodeable.h/.cpp`) wrapping the same call for visual scripting.
---
## Get User's Entry `[Foundation]`
**Unity C#**
```csharp
SteamTools.Game.Leaderboards.Feet_Traveled.GetUserEntry(detailEntriesCount, (foundEntry, ioError) =>
{
// Handle the found entry if ioError is false
});
```
**Godot GDScript** (verified against `LeaderboardData.h:28`) — there's no dedicated
"get just my entry" call; it's `DownloadAroundUserEntries` with a zero-width window (start=0,
end=0 around the local user resolves to just their own entry):
```gdscript
board.DownloadAroundUserEntries(0, 0, 0, func(entries, io_error): pass)
```
**Godot C#** (corrected from the broken live KB version, verified against `LeaderboardData.cs:36-39`):
```csharp
board.DownloadAroundUserEntries(0, 0, 0, (entries, ioError) => { });
```
**O3DE C++** (verified against `SteamToolsRequestBus.h:110` and
`SteamToolsComponent.cpp:504-522`) — same pattern as Godot: no dedicated call, use
`DownloadAroundUser` with `range = 0`:
```cpp
board.DownloadAroundUser(0); // range=0 -> start=-0, end=0 -> just the local user's own entry
```
---
## Get Entries `[Foundation]`
Several quality-of-life shortcuts for reading records; in every case the final parameter is a
delegate called with the results once the read completes.
**Unity C#**
```csharp
// Top X number of records
int howMany = 42;
int detailsCount = 0;
SteamTools.Game.Leaderboards.Feet_Traveled.GetTopEntries(howMany,
detailsCount,
(entriesFound, ioError) =>
{
// Handle the entries found if ioError is false
});
// Get entries for specific users
UserData[] users; // set this to who you want to read for
int detailsCount = 0;
SteamTools.Game.Leaderboards.Feet_Traveled.GetEntries(users,
detailsCount,
(entriesFound, ioError) =>
{
// Handle the entries found if ioError is false
});
// Get entries around the user's entry (includes the user's own entry)
int beforeUser = -5;
int afterUser = 5;
int detailsCount = 0;
SteamTools.Game.Leaderboards.Feet_Traveled.GetEntries(ELeaderboardDataRequest.k_ELeaderboardDataRequestGlobalAroundUser,
beforeUser,
afterUser,
detailsCount,
(entriesFound, ioError) =>
{
// Handle the entries found if ioError is false
});
// Get all your friends' entries
int detailsCount = 0;
SteamTools.Game.Leaderboards.Feet_Traveled.GetEntries(ELeaderboardDataRequest.k_ELeaderboardDataRequestFriends,
0,
0,
detailsCount,
(entriesFound, ioError) =>
{
// Handle the entries found if ioError is false
});
// Get all entries ... really not recommended but it exists
int detailsCount = 0;
SteamTools.Game.Leaderboards.Feet_Traveled.GetAllEntries(detailsCount,
(entriesFound, ioError) =>
{
// Handle the entries found if ioError is false
});
```
**Godot GDScript** (verified against `LeaderboardData.h:27-30` — all four methods exist natively)
```gdscript
board.DownloadGlobalEntries(0, 10, 0, func(entries, io_error): pass)
board.DownloadFriendsEntries(0, 10, 0, func(entries, io_error): pass)
board.DownloadEntriesForUsers(some_users, 0, func(entries, io_error): pass)
```
**Godot C#** — the live KB article's C# example was incomplete (missing the specific-users
variant entirely) as well as broken (wrong receiver type). Corrected and completed, verified
against `LeaderboardData.cs:31-52`. **Note the method name genuinely differs from GDScript
here**: the C# facade calls it `DownloadForUsers`, not `DownloadEntriesForUsers` — this is the
one place on this page where the two languages don't share a method name:
```csharp
board.DownloadGlobalEntries(0, 10, 0, (entries, ioError) => { });
board.DownloadFriendsEntries(0, 10, 0, (entries, ioError) => { });
board.DownloadForUsers(someUsers, 0, (entries, ioError) => { }); // note: "DownloadForUsers", not "DownloadEntriesForUsers"
```
There's no "get literally all entries" convenience in Godot Foundation — only Global/Friends/
AroundUser/ForUsers.
**O3DE C++** (verified against `SteamToolsRequestBus.h:105-115` and
`SteamToolsComponent.cpp:484-542`)
```cpp
board.DownloadTopEntries(10); // -> DownloadLeaderboardEntries(handle, Global, 1, 10)
board.DownloadFriends(); // -> DownloadLeaderboardEntries(handle, Friends, 0, 0)
// DownloadAroundUser(range) covered under "Get User's Entry" above.
```
**Gap**: O3DE Toolkit has no "specific users" or "literally all entries" variant at all — unlike
Unity and Godot, which both have one. Foundation's underlying EBus call
(`DownloadLeaderboardEntries`) could technically support a hand-rolled version with a raw
`SteamLeaderboardHandle` and explicit range, but there's no ergonomic Toolkit wrapper for it
today.
There's also a `DownloadLeaderboardNodeable` Script Canvas node (confirmed still present,
`Source/ScriptNodes/DownloadLeaderboardNodeable.h/.cpp`) covering top-entries/around-user/
friends via an integer "mode" parameter, for visual scripting.
---
## Attach File `[Toolkit in Unity only — Foundation in Godot — not implemented in O3DE]`
Attaches a piece of UGC (e.g. a replay, ghost, or loadout file) to the **current user's own**
entry on a board.
**Unity C#** — this is a Toolkit-only extension method — Foundation has no write-direction
attach call at all. There is **no** combined "upload score and attach a file in one call" helper.
The Code Free `SteamLeaderboardUpload` component only uploads a score (+ optional `int[]`
details) — it has no file/UGC awareness. To attach a file, call the leaderboard's
`AttachUgc(...)` extension directly:
```csharp
// Note fileData can be an object of any type as long as it is [Serializable].
// "tempFile" is just the temporary Remote Storage file name used for the upload — safe to
// reuse the same name every time and let it overwrite, rather than cleaning it up.
SteamTools.Game.Leaderboards.Feet_Traveled.AttachUgc("tempFile", fileData, (ugcResult, ugcIoError) =>
{
if (!ugcIoError)
{
if (ugcResult.Result == Steamworks.EResult.k_EResultOK)
Debug.Log($"Attached file data to user entry on board {apiName}");
else
Debug.LogError($"Failed to attach file data to user entry on board {apiName}, Response Result = {ugcResult.Result}");
}
else
{
Debug.LogError($"Failed to attach file data to user entry on board {apiName}, IO Error!");
}
});
```
Note the exact casing — `AttachUgc`, not `AttachUGC`. Under the hood this writes the data to
Remote Storage, shares the file, then attaches the resulting UGC handle to your entry — all in one
call, but it does require the Toolkit package (`LeaderboardDataExtensions`) to be installed.
**Godot GDScript** — unlike Unity, this is **Foundation, not Toolkit**, in Godot. Verified
against `SteamApi.cpp:547-565` (native) — Steam has no single "attach a raw file" call; it's a
two-step process under the hood (RemoteStorage `FileShare` then `AttachLeaderboardUGC`), but
Foundation wraps both steps into one call for you:
```gdscript
SteamApi.AttachLeaderboardFile("save.dat", "Feet_Traveled", func(result, io_error): pass)
```
`"save.dat"` must already exist as a file you've written via Godot's Remote Storage API — this
call shares and attaches it, it doesn't create it from raw bytes the way Unity's `AttachUgc`
does with a serializable object.
**Godot C#** — verified correct (`SteamApi` really is registered as a Godot engine singleton at
runtime, `SteamApi.cpp:1763`), but **there is no ergonomic C# facade wrapper for this at all** —
neither `API.Leaderboards` nor `LeaderboardData.cs` expose an Attach method, Foundation or
Toolkit. The raw singleton call below is the *only* way to reach this from C#, not a stylistic
choice:
```csharp
Engine.GetSingleton("SteamApi").Call("AttachLeaderboardFile", "save.dat", "Feet_Traveled", callback);
```
**O3DE** — **not implemented, Foundation or Toolkit.** Interesting detail from source: the
Foundation notification bus does declare an `OnLeaderboardUGCSet` event (comment: "Response to
AttachLeaderboardUGC"), and it's wired into the reflection/behavior-context plumbing — but no
matching `AttachLeaderboardUGC` *request* method exists anywhere in either the Foundation or
Toolkit source trees to actually trigger it. This reads as scaffolding for a planned-but-never-
built feature, not something this KB pass broke. There is currently no way to attach a UGC file
to a leaderboard entry in O3DE.
---
## Get Attached File `[Foundation in Unity — genuine gap in Godot and O3DE]`
**Unity C#** — reading an attachment back is Foundation-native — no Toolkit required, unlike
attaching one:
```csharp
// Assuming you already have a LeaderboardEntry (e.g. from Get Entries above) and the data was
// serialised from a type named "ExampleSerializableDataForAttachment":
LeaderboardEntry entry;
entry.GetAttachedUgc<ExampleSerializableDataForAttachment>((dataFound, ioError) =>
{
if (!ioError)
{
// dataFound is your attachment, if any. It's up to you to check whether this is a
// default value or genuinely-set data — for a struct, implement IEquatable/IComparable
// and == / != overloads so you can tell the difference.
}
});
```
This downloads and deserialises the UGC content directly (JSON via `JsonUtility`) — it's a full
working read path, not just a raw handle getter.
**Godot** — **genuine gap, verified via source.** `LeaderboardEntryData` (both the native
`.h`/`.cpp` and the C# facade `LeaderboardEntryData.cs:24`) exposes only a raw UGC handle, no
download/deserialize helper anywhere in Foundation or Toolkit:
```gdscript
var handle := entry.GetUgcHandle() # raw uint64 handle — nothing decodes it for you
```
```csharp
ulong handle = entry.UgcHandle; // same — raw handle only
```
There is no Godot equivalent of Unity's `GetAttachedUgc<T>()`. A developer would need to
hand-roll the Remote Storage UGC download and their own deserialization on top of this raw
handle — treat this as WIP/absent, not a documented API surface.
**O3DE** — **not implemented at all**, same situation as "Attach File" above (no
`AttachLeaderboardUGC` request path exists, so there's nothing to read back either).
---
## If you're stuck
- Full human-readable article (all prose, screenshots, Code-Free inspector field docs this file
compresses out): https://heathen.group/kb/leaderboards/
- This file is generated from that article's raw content; if something here looks wrong, the
article is the fallback source of truth, not this file's memory.
---
Back to the full Steam agent index: https://heathen.group/agent-ref-steam-index/