Files
2026-10-24 11:38:40 +01:00

8.7 KiB

OmegaLeo's Helper Library - Steamworks

Lightweight Steamworks helper library for C# game projects.

This package provides a simple Steam API wrapper for common tasks like initialization, callback processing, player name lookup, and achievements.

Features

  • Simple Steam initialization via SteamManager.Instance (uses SteamAPI_InitFlat on SDK 1.58+ and reports the error message in InitErrorMessage)
  • Configurable native library location with SteamManager.ConfigureLibraryPath() (Unity Plugins/x86_64, custom folders)
  • SteamManager.RestartAppIfNecessary() for launching through Steam
  • Per-frame manual callback dispatch with SteamManager.Update(): callback subscriptions, OverlayActivated event and async call results (SteamManager.Instance.Callbacks)
  • Ownership, DLC and language queries via SteamManager.Instance.Apps (ISteamApps)
  • Safe shutdown with SteamManager.Shutdown()
  • Player persona name access with SteamManager.GetSteamName()
  • Achievement helpers via AchievementManager
  • Steam Workshop (UGC) helpers via SteamManager.Instance.Ugc
  • Steam Remote Play helpers via SteamManager.Instance.RemotePlay
  • Additional interface managers: Controller, Friends, GameServer, Input, Inventory, Matchmaking, Parties, RemoteStorage, Screenshots, UserStats, Utils, Video, and more.
  • Includes native Steam runtime files in NuGet package runtimes/

Installation

dotnet add package OmegaLeo.HelperLib.Steamworks

Quick Start

using OmegaLeo.HelperLib.Steamworks;

// Optional but recommended for local/editor runs.
SteamManager.ConfigureAppId(480); // Replace with your real AppId.

var steam = SteamManager.Instance;
if (steam.IsSteamworksInitialized)
{
    Console.WriteLine($"Welcome {steam.GetSteamName()}");
}

// Call this in your game loop (Unity Update, Godot _Process, etc.)
steam.Update();

var achievements = new AchievementManager();
achievements.UnlockAchievement("ACH_FIRST_WIN");

// Steam Workshop (UGC) example
var subscribedIds = steam.Ugc.GetSubscribedItems();

// Steam Remote Play example
var remotePlaySessionCount = steam.RemotePlay.GetSessionCount();

steam.Shutdown();

More API Examples

var steam = SteamManager.Instance;

// Friends / rich presence
steam.Friends.SetRichPresence("status", "In Match");
steam.Friends.SetRichPresence("map", "Arena_01");
var friendName = steam.Friends.GetFriendPersonaName(76561198000000000UL);
var friendStatus = steam.Friends.GetFriendRichPresence(76561198000000000UL, "status");

// Remote storage
var saveBytes = System.Text.Encoding.UTF8.GetBytes("save-data-v1");
steam.RemoteStorage.FileWrite("save_slot_1.dat", saveBytes);
if (steam.RemoteStorage.FileRead("save_slot_1.dat", out var loadedBytes))
{
    var loadedText = System.Text.Encoding.UTF8.GetString(loadedBytes);
}

// User stats
steam.UserStats.SetStatInt32("KILLS", 25);
steam.UserStats.StoreStats();
steam.UserStats.RequestGlobalStats(7);

// UGC update convenience APIs (existing item)
var options = new SteamUgcItemUpdateOptions
{
    Title = "My Workshop Item",
    Description = "Updated description.",
    Tags = new[] { "map", "coop" },
    ContentFolder = "/absolute/path/to/content",
    PreviewFile = "/absolute/path/to/preview.png"
};
var submitCallHandle = steam.Ugc.UpdateItem(480, 123456789012345678UL, options, "Updated content", out var failedStep);

// Fluent session alternative
var session = steam.Ugc.BeginUpdateSession(480, 123456789012345678UL)
    .WithTitle("My Workshop Item")
    .WithDescription("Updated description")
    .WithTags("map", "coop")
    .WithContentFolder("/absolute/path/to/content")
    .WithPreviewFile("/absolute/path/to/preview.png");
var submitCallHandle2 = session.Submit("Updated content", out var failedStep2);

// Ownership / language (ISteamApps)
bool ownsDemo = steam.Apps.BIsSubscribedApp(480);
bool hasDlc = steam.Apps.BIsDlcInstalled(123456);
string language = steam.Apps.GetCurrentGameLanguage(); // "english", "portuguese", ...

Callbacks and async call results

SteamManager.Update() runs Steam's manual dispatch loop (SteamAPI_ManualDispatch_*). Callbacks and async call results are delivered from Update(), on the thread that calls it. If the loaded steam_api has no manual dispatch exports, or you call SteamManager.ConfigureManualDispatch(false) before Instance, Update() falls back to SteamAPI_RunCallbacks and no events or results are delivered. The library never runs both.

var steam = SteamManager.Instance;

// Typed overlay event (GameOverlayActivated_t)
steam.OverlayActivated += overlay => Console.WriteLine($"Overlay open: {overlay.Active}");

// Any callback by id, with the raw payload
IDisposable sub = steam.Callbacks.Subscribe(SteamCallbackIds.GameOverlayActivated, data =>
{
    bool active = data.ReadBoolean(0);
});
sub.Dispose(); // unsubscribe

// Workshop results: callback style...
steam.Ugc.SubmitItemUpdate(updateHandle, "Patch notes", result =>
{
    if (result.IsSuccess) Console.WriteLine($"Uploaded {result.PublishedFileId}");
    if (result.UserNeedsToAcceptWorkshopLegalAgreement) { /* show the agreement link */ }
});
steam.Ugc.CreateItem(480, 0 /* k_EWorkshopFileTypeCommunity */, created => { /* created.PublishedFileId */ });
steam.Ugc.SendQueryUgcRequest(queryHandle, query => { /* query.NumResultsReturned */ });

// ...or awaitable (keep calling Update() while waiting)
var query = await steam.Ugc.SendQueryUgcRequestAsync(queryHandle);

// Any other SteamAPICall_t handle
var raw = await steam.Callbacks.WaitForCallResultAsync(apiCallHandle);
if (!raw.IoFailure) { int eResult = raw.ReadInt32(0); }

Callback structs use 8-byte packing on Windows and 4-byte packing on Linux, Steam Deck and macOS, which moves 64-bit fields in some structs. The typed results handle this. When you parse a raw payload yourself, check SteamCallbackDispatcher.UsesLargeCallbackPack.

Using in Unity

The library has no Unity references; add the DLLs and the Steam native libraries (steam_api64.dll for Windows, libsteam_api.so for Linux) to Assets/Plugins/x86_64/ and set their platforms in the import settings. Keep all Steam code behind your Steam-off define (for example DISABLESTEAMWORKS) so itch / Microsoft Store builds don't touch it.

#if !DISABLESTEAMWORKS
using System.IO;
using OmegaLeo.HelperLib.Steamworks;
using UnityEngine;

public class SteamBootstrap : MonoBehaviour
{
    private const uint AppId = 480; // your AppId (full game and demo have different ids)

    private void Awake()
    {
        DontDestroyOnLoad(gameObject);

        // Player: <Game>_Data/Plugins/x86_64 (some Linux layouts use <Game>_Data/Plugins).
        // Editor: Application.dataPath is Assets/, so this finds Assets/Plugins/x86_64.
        SteamManager.ConfigureLibraryPath(Path.Combine(Application.dataPath, "Plugins", "x86_64"));
        SteamManager.ConfigureLibraryPath(Path.Combine(Application.dataPath, "Plugins"));

#if !UNITY_EDITOR
        // Relaunch through Steam when started from the exe. Call before ConfigureAppId.
        if (SteamManager.RestartAppIfNecessary(AppId))
        {
            Application.Quit();
            return;
        }
#endif

#if UNITY_EDITOR || DEVELOPMENT_BUILD
        // Editor/dev only: writes steam_appid.txt. Shipped builds get the AppId from Steam.
        SteamManager.ConfigureAppId(AppId, writeSteamAppIdFile: true);
#endif

        var steam = SteamManager.Instance;
        if (!steam.IsSteamworksInitialized)
        {
            Debug.LogWarning($"Steam not available: {steam.InitErrorMessage}");
            return;
        }

        steam.OverlayActivated += OnOverlayActivated;
    }

    private void Update()
    {
        SteamManager.Instance.Update(); // every frame, main thread
    }

    private void OnOverlayActivated(SteamOverlayActivated overlay)
    {
        Time.timeScale = overlay.Active ? 0f : 1f; // or your own pause logic
    }

    private void OnApplicationQuit()
    {
        SteamManager.Instance.OverlayActivated -= OnOverlayActivated;
        SteamManager.Instance.Shutdown();
    }
}
#endif

Pass an ILogger to SteamManager.ConfigureLogger (a small adapter around Debug.Log) to see which library path loaded and why init failed.

Notes

  • Steam must be running and your app must be launched in a valid Steam context.
  • Configure your AppId, library path and dispatch mode before first access to SteamManager.Instance.
  • ConfigureAppId(appId) only sets the SteamAppId / SteamGameId environment variables. Use ConfigureAppId(appId, writeSteamAppIdFile: true) to also write steam_appid.txt (development only; a shipped build with that file skips RestartAppIfNecessary).
  • SteamManager.Update() should be called continuously during runtime.
  • If Steam is unavailable, GetSteamName() falls back to "Player".

License

Open source under the AGPL v3 License. See the repository LICENSE for details.