Steam Stats \u2014 Agent Reference (Unity)

Plain-text agent reference for the Steam Stats 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/stats 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-stats
description: Steam Stats feature reference for Heathen's Steamworks Foundation + Toolkit (Unity, Godot, O3DE; Unreal deferred — Blueprint-only, not re-verified this pass). Use when a developer is implementing per-user stat read/write, AVGRATE (moving-average) stats, or game-server-authoritative stats.
source: https://heathen.group/kb/stats/
generated: 2026-07-29
---

# Steam Stats — 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/

**Tier: [Foundation] (FOSS, free) — with one important exception.** In Unity and Godot, Stats
read/write/store/AVGRATE work entirely on Foundation alone, no Toolkit purchase needed. **O3DE is
different**: Foundation only exposes the raw EBus calls; the ergonomic `StatData` wrapper (and its
Script Canvas exposure) is Toolkit-only. See the per-engine notes below — don't assume the tier
line is identical across engines for this feature.

**This pass covers Unity, Godot, and O3DE — Unreal is deliberately deferred** (Blueprint is
image-based, too expensive to verify this way for now). The live KB article's Unreal-side content
is screenshots only; no Unreal code exists in this article at all.

## Core concept (engine-agnostic)

A Steam Stat is a numeric value Valve stores per-user against your app — set by the game client,
or optionally by an authoritative game server. Four types exist: **Int**, **Float**, and
**AVGRATE** (a Steam-side moving average over a sliding time window you define, e.g. "points per
hour over the last 20 hours" — better than manually dividing two lifetime totals, which gets
sluggish as playtime accumulates). Each stat is configured in the Steamworks Developer Portal with
an auto-generated ID, a type, an "API Name" (the string your code uses to reference it), a
"Set By" permission (Client vs. Game Server), and optional constraints (increment-only, max
change per call, min/max value, default value, "Aggregated" for a Steam-maintained global total).

**Set vs. Store**: setting a stat's value is cheap and local (an in-memory/local cache write) —
call it as often as you like (e.g. every enemy kill). **Storing** pushes all pending stat *and*
achievement changes to Steam's servers in one call, is rate-limited by Valve, and should only be
called at natural checkpoints (level end, player death, session end) — not after every `Set`. You
don't need to store defensively either: if the game closes with pending changes, Steam stores them
for you.

## Read a stat

**Unity C#**
```csharp
// Identify which stat
StatData statData = "API_NameHere"; // Use the API name you created in the Steamworks Developer Portal

// Get the value as a float
float FloatValue = statData.FloatValue();

// Get the value as an int
int IntValue = statData.IntValue();
```
`StatData` is a struct with an implicit `string`→`StatData` conversion (so assigning a plain
string API name to a `StatData` variable just works), and `FloatValue()`/`IntValue()` are instance
methods that read the *current user's* value.

For Steam Game Servers (a server must first request a user's stats — only possible after that
user has authenticated with the server — before it can read them):
```csharp
// Steam Game Servers can read stats for users
CSteamID userId = new(); // This is just a stand-in for the ID of the user you want to read for

// First, the server must request the user's stats, which can only be done after authentication
StatsAndAchievements.Server.RequestUserStats(userId, HandleStatsReceived);

// When the HandleStatsReceived has completed, you can then read the stat values
// Get the value as a float
StatsAndAchievements.Server.GetUserStat(userId, "API_NameHere", out float floatValue);

// Get the value as an int
StatsAndAchievements.Server.GetUserStat(userId, "API_NameHere", out int intValue);
```
The handler:
```csharp
private void HandleStatsReceived(GSStatsReceived_t results, bool arg2)
{
    // results.m_eResult: The EResult of the request
    // results.m_steamIDUser: Which user you got the results for
}
```
(Assumes a `using Heathen.SteamworksIntegration.API;` — the fully-qualified form is
`API.StatsAndAchievements.Server...`, same class the client-side calls above use.)

**Godot GDScript**
```gdscript
var s := StatData.Get("API_NameHere")
var f := s.GetFloatValue()
var i := s.GetIntValue()
```
`StatData` is a GDExtension `RefCounted` class with a static `Get(String)` factory and instance
methods `GetFloatValue()`/`GetIntValue()` — verified against
`Godot-Foundation-for-Steamworks/src/public/StatData.h:32` and
`src/private/StatData.cpp:9-10`. All of it lives in **Foundation**, not Toolkit.

**Godot C#**
```csharp
float f = API.StatsAndAchievements.GetStatFloat("API_NameHere");
int i = API.StatsAndAchievements.GetStatInt("API_NameHere");
```
Verified against `CSharp/API/API.StatsAndAchievements.cs:51,53` (forwards to
`SteamTools.GetStatInt/GetStatFloat` in `CSharp/SteamTools.cs:281,283`).

**O3DE (C++)**
Foundation wraps `ISteamUserStats` behind an EBus request/notification pair
(`Heathen::SteamUserStatsRequestBus` / `SteamUserStatsNotificationBus`), not a class instance you
new up. **Before any Get call returns real data, the local user's stats must be requested once**
with `RequestCurrentStats()` — the result arrives asynchronously via
`SteamUserStatsNotificationBus::Handler::OnUserStatsReceived`, the same requirement the raw
Steamworks SDK has. Verified against
`O3DE-Foundation-for-Steamworks/Code/Include/FoundationSteamworks/SteamUserStatsRequestBus.h:41,48,51`.
```cpp
// Foundation: raw EBus (SteamUserStatsRequestBus is HandlerPolicy::Single,
// so BroadcastResult returns the single implementation's result)
AZ::s32 intValue = 0;
float floatValue = 0.0f;
bool ok = false;

Heathen::SteamUserStatsRequestBus::BroadcastResult(ok, &Heathen::SteamUserStatsRequests::GetStatInt, "API_NameHere", intValue);
Heathen::SteamUserStatsRequestBus::BroadcastResult(ok, &Heathen::SteamUserStatsRequests::GetStatFloat, "API_NameHere", floatValue);
```
Toolkit adds an ergonomic `StatData` wrapper (also reflected to Script Canvas as a "Stat Data"
type under category `Steam/Data`, with methods `Get Int`/`Get Float`/`Set Int`/`Set Float`/
`Update Average Rate`/`Store Stats`) — unlike Unity and Godot, where the ergonomic stat wrapper
ships in Foundation itself, **O3DE's convenience layer is Toolkit-only**; Foundation alone only
gives you the raw EBus calls above. Verified against
`O3DE-Toolkit-for-Steamworks/Code/Include/ToolkitSteamworks/Data/StatData.h` and
`Source/Clients/Data/StatData.cpp:53-73`.
```cpp
// Toolkit: Heathen::SteamTools::StatData wraps the bus call, same shape as
// Unity/Godot's StatData (construct with the stat's configured type)
Heathen::SteamTools::StatData stat("API_NameHere", Heathen::SteamTools::StatType::Int);
AZ::s32 intValue = stat.GetInt();

Heathen::SteamTools::StatData floatStat("API_NameHere", Heathen::SteamTools::StatType::Float);
float floatValue = floatStat.GetFloat();
```

## Set a stat (Int & Float)

**Unity C#**
```csharp
// Identify which stat
StatData statData = "API_NameHere"; // Use the API name you created in the Steamworks Developer Portal

// Set has an int and a float overload
statData.Set(42);
// or
statData.Set(42.0f);
```
For Steam Game Servers (same authenticate-then-request pattern as reading):
```csharp
// Servers that have authenticated a user can request
// that user's stats, and when they have those stats, they can update them
CSteamID userId = new(); // This is just a stand-in for the ID of the user you want to read for
StatsAndAchievements.Server.RequestUserStats(userId, HandleStatsReceived);

StatsAndAchievements.Server.SetUserStat(userId, "API_NameHere", 42);
// or
StatsAndAchievements.Server.SetUserStat(userId, "API_NameHere", 42.0f);
```
The handler is the same `HandleStatsReceived(GSStatsReceived_t, bool)` shown above.

**Godot GDScript**
Godot has separate typed setters, not one overloaded `Set`.
```gdscript
s.SetIntValue(42)
# or
s.SetFloatValue(42.0)
```
Verified: `SetIntValue(int)`/`SetFloatValue(float)` are real instance methods on `StatData`
(`Godot-Foundation-for-Steamworks/src/private/StatData.cpp:11-12` bind, `:44,50` impl — both call
`SteamUserStats()->SetStat`).

**Godot C#**
C# gets the single-name-two-overloads shape.
```csharp
API.StatsAndAchievements.SetStat("API_NameHere", 42);
// or
API.StatsAndAchievements.SetStat("API_NameHere", 42.0f);
```
Verified against `CSharp/API/API.StatsAndAchievements.cs:47,49` — both overloads exist exactly as
written.

**O3DE (C++)**
Same split as reading: Foundation exposes separate typed setters on the request bus, Toolkit's
`StatData` wraps them as instance methods.
```cpp
// Foundation: raw EBus, one method per type (no single overloaded "Set")
Heathen::SteamUserStatsRequestBus::Broadcast(&Heathen::SteamUserStatsRequests::SetStatInt, "API_NameHere", 42);
Heathen::SteamUserStatsRequestBus::Broadcast(&Heathen::SteamUserStatsRequests::SetStatFloat, "API_NameHere", 42.0f);
```
```cpp
// Toolkit: StatData instance methods
stat.SetInt(42);
// or
floatStat.SetFloat(42.0f);
```

## Store stats (push pending changes to Steam)

Setting a stat only updates a local cache — nothing reaches Steam until you call Store. Store
flushes **all** pending stat and achievement changes at once; you never need to store per-field.

**Unity C#**
```csharp
// can be called anywhere, on any existing StatData / AchievementData instance
statData.Store();
// or
achievementData.Store();
// or, store everything (all pending stats + achievements) at once:
API.StatsAndAchievements.Client.StoreStats();
```
> **Corrected from the live KB article.** The published version currently reads
> `StatData.Store();` / `AchevementData.Store();` / `API.StatsAndAcheivements.Client.StoreStats();`
> — two problems: `Store()` on `StatData`/`AchievementData` is an **instance** method (verified in
> `StatData.cs` line 141 and `AchievementData.cs` line 181), not something you call on the bare
> type name, and `AchevementData` / `StatsAndAcheivements` are simply misspelled class names
> (`AchievementData`, `API.StatsAndAchievements`). See the discrepancy report for the exact
> before/after and source line references.

**Godot GDScript**
```gdscript
s.Store()
# or, for everything at once:
SteamApi.StoreStats()
```
Verified: `StatData.Store()` instance method exists (`StatData.cpp:15,74`, calls
`SteamApi::StoreStats()`). The singleton really is named `SteamApi` — native GDExtension class
with static `StoreStats()` (`src/public/SteamApi.h:201`, bound `SteamApi.cpp:2385`); Foundation's
own README uses this identical snippet (`README.md:193`).

**Godot C#**
```csharp
API.StatsAndAchievements.StoreStats();
```
Verified: `CSharp/API/API.StatsAndAchievements.cs:69` → forwards to `SteamTools.StoreStats()`
(`CSharp/SteamTools.cs:208`) → native `SteamApi::StoreStats()`.

**O3DE (C++)**
```cpp
// Foundation: flushes all pending stats + achievements at once
Heathen::SteamUserStatsRequestBus::Broadcast(&Heathen::SteamUserStatsRequests::StoreStats);
// result arrives via SteamUserStatsNotificationBus::Handler::OnUserStatsStored
```
```cpp
// Toolkit: same call, via the StatData wrapper
stat.StoreStats();
```

## Update an AVGRATE stat

AVGRATE stats use a different `Set` overload that takes both the accumulated value *and* the
session length it was accumulated over, rather than an absolute value.

**Unity C#**
```csharp
// Identify which stat
StatData statData = "API_NameHere"; // Use the API name you created in the Steamworks Developer Portal

// The Set overload that takes a value and a length is how you update average stats
float value = 42f;
double length = 14;
statData.Set(value, length);
```
Keep the time unit consistent between whatever `Window` you configured for this stat in the
Steamworks Developer Portal and the `length` you pass here (seconds, minutes, hours — your
choice, just be consistent).

**Godot GDScript**
No GDScript convenience wrapper confirmed, but callable directly on the singleton:
```gdscript
SteamApi.UpdateAvgRateStat("API_NameHere", 42.0, 14.0)
```
Verified: `SteamApi.UpdateAvgRateStat(String, float, double)` is a real static method
(`src/public/SteamApi.h:220`, impl `src/private/SteamApi.cpp:699`, bound at `SteamApi.cpp:2402`).

**Godot C#**
```csharp
API.StatsAndAchievements.UpdateAvgRateStat("API_NameHere", 42.0f, 14.0);
// or: StatDataExtensions.UpdateAvgRate(stat, 42.0f, 14.0)
```
Both verified real: `API.StatsAndAchievements.UpdateAvgRateStat` lives in **Foundation**
(`CSharp/API/API.StatsAndAchievements.cs:63-64`); `StatDataExtensions.UpdateAvgRate` is a thin
**Toolkit** extension method wrapping the same call
(`Godot-Toolkit-for-Steamworks/CSharp/Extensions/StatDataExtensions.cs:8-9`) — cosmetic sugar, not
a separate implementation. Toolkit also adds global-stat aggregation helpers Foundation doesn't
have at all: `GlobalInt()`, `GlobalFloat()`, `GlobalIntHistory(int)`, `GlobalFloatHistory(int)`
(same file, lines 11-25).

**O3DE (C++)**
```cpp
// Foundation: raw EBus
Heathen::SteamUserStatsRequestBus::Broadcast(&Heathen::SteamUserStatsRequests::UpdateAvgRateStat, "API_NameHere", 42.0f, 14.0);
```
```cpp
// Toolkit: via StatData (construct with StatType::AverageRate)
Heathen::SteamTools::StatData rateStat("API_NameHere", Heathen::SteamTools::StatType::AverageRate);
rateStat.UpdateAverageRate(42.0f, 14.0);
```

## Engine tier summary for this feature

- **Unity**: Stats fully in Foundation (`StatData`, `API.StatsAndAchievements`). Toolkit adds
  nothing stats-specific.
- **Godot**: Stats fully in Foundation (`StatData`, `SteamApi`, `API.StatsAndAchievements`).
  Toolkit adds only a cosmetic `UpdateAvgRate` extension method plus global-stat aggregation
  helpers (`GlobalInt`/`GlobalFloat`/history) not present in Foundation at all.
- **O3DE**: Raw stat read/write/store/AVGRATE calls are in Foundation (`SteamUserStatsRequestBus`),
  but the ergonomic `StatData` wrapper — and its Script Canvas exposure — is **Toolkit-only**. This
  is the one engine where you need Toolkit for the convenience layer, not just extra sugar.

## If you're stuck

- Full human-readable article (all prose, screenshots, configuration-field docs this file
  compresses out): https://heathen.group/kb/stats/
- 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 — though note the Store Stats
  correction, and all of the Godot/O3DE content, were verified directly against Foundation/Toolkit
  source, not just the article.

---
Back to the full Steam agent index: https://heathen.group/agent-ref-steam-index/