214 lines
8.7 KiB
Markdown
214 lines
8.7 KiB
Markdown
# 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: <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.
|
|
|