# 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 ```bash dotnet add package OmegaLeo.HelperLib.Steamworks ``` ## Quick Start ```cs 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 ```cs 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. ```cs 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. ```cs #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: _Data/Plugins/x86_64 (some Linux layouts use _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.