We use cookies only to manage your session — no third-party tracking or advertising. Learn more in our Privacy Policy.

RoLearn

Unlock Pro features

DISCOVER
Back to Guidance

Steamworks + RoLearn SDK: Cross-Platform Analytics for Native PC Games

Guide
May 24, 2026
9 min
RoLearn SDK Team
SDK
Steamworks
C++
Cross-Platform

The RoLearn C++ SDK ships a single-header library suitable for any native PC game on Steam — engine-agnostic, no external dependencies beyond a JSON header. This guide walks through the Steamworks-specific integration: header-only setup, the C-ABI wrapper for engines that cannot consume C++ headers, identity mapping to Steam IDs, and the cross-platform analytics roll-up pattern when the same title ships on both Steam and another platform tracked by RoLearn.

What ships

Three pieces in the Steamworks bundle (downloads page → Steamworks tab):

  • rolearn_sdk.h — the public C++ header. Single-file, header-only, all SDK behavior lives here.
  • rolearn_sdk_c.h + rolearn_sdk_c.cpp — a C-ABI wrapper for engines that cannot consume the C++ header directly (Game Maker, RPG Maker MV/MZ with C plugins, some proprietary in-house engines).
  • json.hpp — bundled nlohmann/json. Only dependency beyond the C++ standard library. Skip if your project already uses nlohmann/json from elsewhere.

Targets C++17 or newer. Compiles cleanly under MSVC 19.30+, Clang 13+, and GCC 11+. Tested in production with Unreal Engine 5 (via the C ABI from blueprint plugins), Godot 4 (via GDExtension), and bare-metal SDL2 games.

Header-only install (C++ projects)

Drop rolearn_sdk.h and json.hpp into your project's third-party include path. No CMake target, no link step, no vcpkg dance unless you want it. If you do use vcpkg, a port is available — see the downloads page.

Initialize in your game's bootstrap (where you call SteamAPI_Init is the right neighborhood):

#include "rolearn_sdk.h"

void bootstrap_analytics() {
    rolearn::Config cfg;
    cfg.api_key     = "rk_live_XXXX";
    cfg.endpoint    = "https://rolearn.dev/api/sdk/events";
    cfg.game_id     = "440";              // your Steam app ID, as string
    cfg.platform    = "steam";
    cfg.persist_on_disk = true;           // survives game crash / kill
    cfg.flush_every_seconds = 10;

    rolearn::SDK::instance().init(cfg);
}

On clean shutdown, call shutdown() — it flushes the outstanding batch and joins the worker thread:

void on_game_exit() {
    rolearn::SDK::instance().shutdown();
}

If your game is force-killed (crash, Steam kill switch, user ends process), the on-disk persistence layer holds the outstanding batch; it re-emits on the next launch.

Sending events from C++

The header-only API uses nlohmann/json's brace-init style. The canonical example:

rolearn::SDK::instance().track("level_complete",
    {
        {"level", 5},
        {"score", 1840},
        {"duration", 92},
        {"result", "win"}
    },
    player_steam_id_str);    // optional 3rd arg

For purchases (Steam micro-transactions, in-game store consumables, gift redemptions):

rolearn::SDK::instance().purchase({
    .sku      = "keys_5",
    .price    = 4.99,
    .currency = "USD",
    .quantity = 1,
    .source   = "steam_microtxn"
});

For Steam-native ad-format integrations (rare but happens — Steam-Workshop sponsored content placements) or in-game ad networks:

rolearn::SDK::instance().track("ad_impression", {
    {"format",      "rewarded_video"},
    {"network",     "your_network"},
    {"revenue_usd", 0.012},
    {"placement",   "post_match"}
});

The C-ABI wrapper (non-C++ engines)

For engines that consume C ABIs only — Game Maker plugin extensions, RPG Maker plugins, custom engines without a C++17-capable toolchain — link against the C wrapper:

#include "rolearn_sdk_c.h"

void bootstrap(void) {
    rolearn_init(
        /* api_key  */ "rk_live_XXXX",
        /* endpoint */ "https://rolearn.dev/api/sdk/events",
        /* game_id  */ "440",
        /* platform */ "steam"
    );
}

void on_level_complete(int level, int score) {
    /* payload is a JSON string */
    rolearn_track("level_complete",
        "{\"level\":5,\"score\":1840,\"result\":\"win\"}",
        NULL /* player_id */);
}

void on_shutdown(void) {
    rolearn_flush();
    rolearn_shutdown();
}

The C wrapper exposes the same surface as the C++ one — track, purchase, brand-zone events, equip, share, flush, shutdown. The payload argument is a serialized JSON string rather than a builder object; build the JSON yourself with your engine's preferred library, or with sprintf for simple cases.

Identity: mapping Steam IDs to player_id

The SDK's player_id field is intentionally platform-agnostic. For Steam, the convention is to pass the player's SteamID64 as a string:

std::string player_id = std::to_string(SteamUser()->GetSteamID().ConvertToUint64());
rolearn::SDK::instance().track("queue_entered", {{"mode", "ranked"}}, player_id);

Three things to know about identity:

  • SteamID64 is fine to send to the SDK. It is not PII under GDPR — it is a pseudonymous identifier the player chose to associate with your game by purchasing it through Steam. Your privacy policy must still disclose that you collect it.
  • Country auto-fills from ISteamUtils::GetIPCountry() when Steam is initialized. If you call track() before SteamAPI_Init completes, the country falls back to "XX" — initialize Steam first, then RoLearn.
  • Don't send Steam personas (display names).Personas can change at any time, and the SDK's aggregations are keyed on stable IDs. Send the SteamID64 as identity, store personas in your own backend if you need them for customer support.

Cross-platform roll-up: same game, multiple platforms

Many studios ship the same title on Steam, mobile, and (less often) Roblox UGC ports. The RoLearn workspace handles cross-platform analytics by keying on (team_id, game_id, platform). The conventions that produce a clean cross-platform dashboard:

  1. Pick a single game_id per title and use it across every platform's SDK init. Steam app ID, internal product code, or a UUID — pick one and use it consistently. The SDK does NOT require the ID to match a Steam app ID; it is opaque to the server.
  2. Pass a different platform value per build: "steam", "unity", "roblox". The dashboard pivots on this column; the analytics views let you filter to one or sum across all.
  3. Use the same event vocabulary across platforms. Calling track("level_complete", …) on Steam and track("levelComplete", …) on Unity will produce two unrelated event types in the dashboard. Pick one name, enforce it via a shared header / code-gen / wiki page.
  4. Identity is harder. The same human player on Steam and on mobile has two different IDs (SteamID64 vs Apple/Google ID), and there is no way for RoLearn to know they are the same person without your backend providing a cross-platform "account ID" you control. If you have one, pass it as player_id on both platforms. If you don't, retention cohorts are calculated per platform — which is usually the right read anyway.

Persistence and crash resilience

Native PC games crash. The SDK's on-disk persistence is on by default (persist_on_disk = true) and writes buffered batches to the OS temp directory, keyed by a worker UUID. On the next launch, the worker picks up orphaned batch files and re-emits them. End-result: a game that crashes between flushes loses zero events.

For games that handle their own crash dumps (Crashpad, Breakpad, custom), call flush_sync() from your crash handler to drain the buffer before the dump completes:

void on_crash_signal(int signum) {
    rolearn::SDK::instance().flush_sync();  // blocks up to 2s
    // ... your existing crash-dump logic ...
    std::abort();
}

flush_sync() is safe to call from a signal handler in the SDK's worker design (no allocations or locks on the hot path), but most teams call it earlier — from a last-chance handler before the crash dump fires.

Verifying the Steam integration

Same verification path as the other platforms: build, launch, check SDK → Events in the RoLearn web app. Steam-specific diagnostics:

  1. Are events landing under the right platform="steam" column in the dashboard filter? If not, check your Config initialization — platform defaults to "steam" but is overridable.
  2. Is country resolving? If every event shows country = "XX", the SDK initialized before SteamAPI did. Moveinit() after SteamAPI_Init() in your bootstrap.
  3. Persistence dir writable? If you see[RoLearn] failed to write WAL in your game's console, the SDK can't write to its temp directory. Setpersist_path in Config to a guaranteed-writable directory inside your game's save path.

Where to go next