updated SteamWorks library

This commit is contained in:
2026-10-24 11:38:40 +01:00
parent 91cdba0fef
commit c745fb18bd
8 changed files with 1485 additions and 33 deletions
+114 -3
View File
@@ -4,8 +4,11 @@ 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`
- Per-frame callback runner with `SteamManager.Update()`
- 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`
@@ -89,11 +92,119 @@ var session = steam.Ugc.BeginUpdateSession(480, 123456789012345678UL)
.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 before first access to `SteamManager.Instance`.
- 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"`.
@@ -0,0 +1,480 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using OmegaLeo.HelperLib.Shared.Attributes;
namespace OmegaLeo.HelperLib.Steamworks
{
[Documentation(nameof(SteamCallbackIds), "Steamworks callback ids (the k_iCallback value of each callback struct) used by this library.", null, null)]
public static class SteamCallbackIds
{
[Documentation(nameof(GameOverlayActivated), "GameOverlayActivated_t (k_iSteamFriendsCallbacks + 31).", null, null)]
public const int GameOverlayActivated = 331;
[Documentation(nameof(SteamApiCallCompleted), "SteamAPICallCompleted_t (k_iSteamUtilsCallbacks + 3). Handled internally to deliver async call results.", null, null)]
public const int SteamApiCallCompleted = 703;
[Documentation(nameof(SteamUgcQueryCompleted), "SteamUGCQueryCompleted_t (k_iSteamUGCCallbacks + 1), the result of SendQueryUGCRequest.", null, null)]
public const int SteamUgcQueryCompleted = 3401;
[Documentation(nameof(CreateItemResult), "CreateItemResult_t (k_iSteamUGCCallbacks + 3), the result of CreateItem.", null, null)]
public const int CreateItemResult = 3403;
[Documentation(nameof(SubmitItemUpdateResult), "SubmitItemUpdateResult_t (k_iSteamUGCCallbacks + 4), the result of SubmitItemUpdate.", null, null)]
public const int SubmitItemUpdateResult = 3404;
}
[Documentation(nameof(SteamCallbackPayload), "Base class for a copied Steam callback payload with little-endian field readers. Offsets follow the native struct layout for the current platform (see SteamCallbackDispatcher.UsesLargeCallbackPack).", null, null)]
public abstract class SteamCallbackPayload
{
protected SteamCallbackPayload(byte[] payload)
{
Payload = payload ?? Array.Empty<byte>();
}
[Documentation(nameof(Payload), "Raw bytes of the native callback struct, copied before Steam frees it.", null, null)]
public byte[] Payload { get; }
[Documentation(nameof(ReadByte), "Reads a byte at the given offset, or 0 when out of range.", new[] { "offset: Byte offset in the payload." }, null)]
public byte ReadByte(int offset)
{
return offset >= 0 && offset < Payload.Length ? Payload[offset] : (byte)0;
}
[Documentation(nameof(ReadBoolean), "Reads a one-byte C++ bool at the given offset.", new[] { "offset: Byte offset in the payload." }, null)]
public bool ReadBoolean(int offset)
{
return ReadByte(offset) != 0;
}
[Documentation(nameof(ReadInt32), "Reads a 32-bit signed integer at the given offset, or 0 when out of range.", new[] { "offset: Byte offset in the payload." }, null)]
public int ReadInt32(int offset)
{
return HasBytes(offset, 4) ? BitConverter.ToInt32(Payload, offset) : 0;
}
[Documentation(nameof(ReadUInt32), "Reads a 32-bit unsigned integer at the given offset, or 0 when out of range.", new[] { "offset: Byte offset in the payload." }, null)]
public uint ReadUInt32(int offset)
{
return HasBytes(offset, 4) ? BitConverter.ToUInt32(Payload, offset) : 0;
}
[Documentation(nameof(ReadUInt64), "Reads a 64-bit unsigned integer at the given offset, or 0 when out of range.", new[] { "offset: Byte offset in the payload." }, null)]
public ulong ReadUInt64(int offset)
{
return HasBytes(offset, 8) ? BitConverter.ToUInt64(Payload, offset) : 0;
}
[Documentation(nameof(ReadString), "Reads a NUL-terminated UTF-8 char array field.", new[] { "offset: Byte offset of the char array.", "maxLength: Size of the char array in the native struct." }, null)]
public string ReadString(int offset, int maxLength)
{
if (offset < 0 || offset >= Payload.Length || maxLength <= 0)
{
return string.Empty;
}
var end = Math.Min(Payload.Length, offset + maxLength);
var length = 0;
while (offset + length < end && Payload[offset + length] != 0)
{
length++;
}
return Encoding.UTF8.GetString(Payload, offset, length);
}
private bool HasBytes(int offset, int size)
{
return offset >= 0 && offset + size <= Payload.Length;
}
}
[Documentation(nameof(SteamCallbackData), "A Steam callback received through manual dispatch.", null, null)]
public sealed class SteamCallbackData : SteamCallbackPayload
{
internal SteamCallbackData(int callbackId, int steamUser, byte[] payload) : base(payload)
{
CallbackId = callbackId;
SteamUser = steamUser;
}
[Documentation(nameof(CallbackId), "The callback id (k_iCallback) of the native struct.", null, null)]
public int CallbackId { get; }
[Documentation(nameof(SteamUser), "The HSteamUser the callback was posted for.", null, null)]
public int SteamUser { get; }
}
[Documentation(nameof(SteamApiCallResult), "The completed result of an asynchronous Steam API call (SteamAPICall_t).", null, null)]
public sealed class SteamApiCallResult : SteamCallbackPayload
{
internal SteamApiCallResult(ulong apiCall, int callbackId, bool ioFailure, byte[] payload) : base(payload)
{
ApiCall = apiCall;
CallbackId = callbackId;
IoFailure = ioFailure;
}
[Documentation(nameof(ApiCall), "The SteamAPICall_t handle this result belongs to.", null, null)]
public ulong ApiCall { get; }
[Documentation(nameof(CallbackId), "The callback id of the result struct (for example SteamCallbackIds.SubmitItemUpdateResult).", null, null)]
public int CallbackId { get; }
[Documentation(nameof(IoFailure), "True when Steam reported a failure fetching the result, or Steam was shut down before it completed. The payload is then not valid.", null, null)]
public bool IoFailure { get; }
internal static SteamApiCallResult Failed(ulong apiCall)
{
return new SteamApiCallResult(apiCall, 0, true, Array.Empty<byte>());
}
}
[Documentation(nameof(SteamOverlayActivated), "Data from GameOverlayActivated_t, raised when the Steam overlay opens or closes.", null, null)]
public readonly struct SteamOverlayActivated
{
internal SteamOverlayActivated(bool active, bool userInitiated, uint appId)
{
Active = active;
UserInitiated = userInitiated;
AppId = appId;
}
[Documentation(nameof(Active), "True when the overlay was opened, false when it was closed.", null, null)]
public bool Active { get; }
[Documentation(nameof(UserInitiated), "True when the user opened the overlay (as opposed to the game or Steam).", null, null)]
public bool UserInitiated { get; }
[Documentation(nameof(AppId), "The AppId the overlay was activated for.", null, null)]
public uint AppId { get; }
}
[Documentation(nameof(SteamCallbackDispatcher), "Runs the Steam manual callback dispatch loop and delivers callbacks and async call results. Access it through SteamManager.Instance.Callbacks; it is pumped by SteamManager.Update().", null,
@"```csharp
var steam = SteamManager.Instance;
steam.Callbacks.OverlayActivated += overlay => Pause(overlay.Active);
using var sub = steam.Callbacks.Subscribe(SteamCallbackIds.GameOverlayActivated, data => Log(data.ReadBoolean(0)));
var result = await steam.Callbacks.WaitForCallResultAsync(apiCallHandle);
```")]
public sealed class SteamCallbackDispatcher
{
private readonly object _sync = new object();
private readonly Dictionary<int, List<Action<SteamCallbackData>>> _handlers = new Dictionary<int, List<Action<SteamCallbackData>>>();
private readonly Dictionary<ulong, List<Action<SteamApiCallResult>>> _pendingCallResults = new Dictionary<ulong, List<Action<SteamApiCallResult>>>();
internal SteamCallbackDispatcher()
{
}
[Documentation(nameof(UsesLargeCallbackPack), "True when Steam callback structs use 8-byte packing (Windows). Linux, Steam Deck and macOS use 4-byte packing, which moves 64-bit fields in some structs.", null, null)]
public static bool UsesLargeCallbackPack => RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
[Documentation(nameof(IsManualDispatchActive), "True when manual dispatch is running, so callbacks and async call results are delivered.", null, null)]
public bool IsManualDispatchActive => SteamNativeApi.IsManualDispatchActive;
[Documentation(nameof(PendingCallResultCount), "Number of async call handles still waiting for a result.", null, null)]
public int PendingCallResultCount
{
get
{
lock (_sync)
{
return _pendingCallResults.Count;
}
}
}
[Documentation(nameof(OverlayActivated), "Raised from SteamManager.Update() when the Steam overlay opens or closes (GameOverlayActivated_t). Use it to pause the game.", null, null)]
public event Action<SteamOverlayActivated> OverlayActivated;
[Documentation(nameof(Subscribe), "Subscribes to every callback with the given id. The handler receives a copy of the raw payload. Dispose the returned object to unsubscribe.", new[]
{
"callbackId: Callback id (k_iCallback), see SteamCallbackIds.",
"handler: Called from SteamManager.Update() for each matching callback."
}, null)]
public IDisposable Subscribe(int callbackId, Action<SteamCallbackData> handler)
{
if (handler == null)
{
throw new ArgumentNullException(nameof(handler));
}
lock (_sync)
{
if (!_handlers.TryGetValue(callbackId, out var list))
{
list = new List<Action<SteamCallbackData>>();
_handlers[callbackId] = list;
}
list.Add(handler);
}
return new Subscription(() => Unsubscribe(callbackId, handler));
}
[Documentation(nameof(Unsubscribe), "Removes a handler added with Subscribe.", new[] { "callbackId: Callback id the handler was registered for.", "handler: The handler to remove." }, null)]
public bool Unsubscribe(int callbackId, Action<SteamCallbackData> handler)
{
lock (_sync)
{
if (!_handlers.TryGetValue(callbackId, out var list) || !list.Remove(handler))
{
return false;
}
if (list.Count == 0)
{
_handlers.Remove(callbackId);
}
return true;
}
}
[Documentation(nameof(RegisterCallResult), "Registers a handler for the result of an async Steam API call. The handler runs once, from SteamManager.Update(). Returns false when the handle is invalid or manual dispatch is not active.", new[]
{
"apiCall: SteamAPICall_t handle returned by an async Steam API method.",
"onCompleted: Called with the result (check IoFailure)."
}, null)]
public bool RegisterCallResult(ulong apiCall, Action<SteamApiCallResult> onCompleted)
{
if (onCompleted == null)
{
throw new ArgumentNullException(nameof(onCompleted));
}
if (apiCall == 0 || !IsManualDispatchActive)
{
return false;
}
lock (_sync)
{
if (!_pendingCallResults.TryGetValue(apiCall, out var list))
{
list = new List<Action<SteamApiCallResult>>();
_pendingCallResults[apiCall] = list;
}
list.Add(onCompleted);
}
return true;
}
[Documentation(nameof(WaitForCallResultAsync), "Returns a task that completes with the result of an async Steam API call. Keep calling SteamManager.Update() while waiting. Faults with InvalidOperationException when manual dispatch is not active.", new[]
{
"apiCall: SteamAPICall_t handle returned by an async Steam API method.",
"cancellationToken: Cancels the wait (the Steam call itself keeps running)."
}, null)]
public Task<SteamApiCallResult> WaitForCallResultAsync(ulong apiCall, CancellationToken cancellationToken = default)
{
if (apiCall == 0)
{
return Task.FromException<SteamApiCallResult>(new ArgumentException("The Steam API call handle is invalid.", nameof(apiCall)));
}
var completion = new TaskCompletionSource<SteamApiCallResult>(TaskCreationOptions.RunContinuationsAsynchronously);
Action<SteamApiCallResult> handler = result => completion.TrySetResult(result);
if (!RegisterCallResult(apiCall, handler))
{
return Task.FromException<SteamApiCallResult>(new InvalidOperationException("Steam manual callback dispatch is not active, so async call results cannot be delivered."));
}
if (cancellationToken.CanBeCanceled)
{
var registration = cancellationToken.Register(() =>
{
RemoveCallResult(apiCall, handler);
completion.TrySetCanceled(cancellationToken);
});
completion.Task.ContinueWith(_ => registration.Dispose(), TaskScheduler.Default);
}
return completion.Task;
}
internal void RunFrame()
{
if (!SteamNativeApi.IsManualDispatchActive)
{
return;
}
SteamNativeApi.ManualDispatchRunFrame();
while (SteamNativeApi.ManualDispatchGetNextCallback(out var message))
{
try
{
HandleCallback(message);
}
catch (Exception e)
{
SteamManager.Logger.LogError(e, "An exception occurred while handling Steam callback {CallbackId}.", message.CallbackId);
}
finally
{
SteamNativeApi.ManualDispatchFreeLastCallback();
}
}
}
internal void FailPendingCallResults()
{
List<KeyValuePair<ulong, List<Action<SteamApiCallResult>>>> pending;
lock (_sync)
{
pending = new List<KeyValuePair<ulong, List<Action<SteamApiCallResult>>>>(_pendingCallResults);
_pendingCallResults.Clear();
}
foreach (var entry in pending)
{
InvokeCallResultHandlers(entry.Value, SteamApiCallResult.Failed(entry.Key));
}
}
private void HandleCallback(SteamNativeCallbackMessage message)
{
var payload = CopyPayload(message.Param, message.ParamSize);
var data = new SteamCallbackData(message.CallbackId, message.SteamUser, payload);
if (message.CallbackId == SteamCallbackIds.SteamApiCallCompleted)
{
HandleApiCallCompleted(data);
}
else if (message.CallbackId == SteamCallbackIds.GameOverlayActivated)
{
// GameOverlayActivated_t { uint8 m_bActive; bool m_bUserInitiated; AppId_t m_nAppID; uint32 m_dwOverlayPID; }
try
{
OverlayActivated?.Invoke(new SteamOverlayActivated(data.ReadBoolean(0), data.ReadBoolean(1), data.ReadUInt32(4)));
}
catch (Exception e)
{
SteamManager.Logger.LogError(e, "An OverlayActivated handler threw.");
}
}
Action<SteamCallbackData>[] handlers = null;
lock (_sync)
{
if (_handlers.TryGetValue(message.CallbackId, out var list))
{
handlers = list.ToArray();
}
}
if (handlers == null)
{
return;
}
foreach (var handler in handlers)
{
try
{
handler(data);
}
catch (Exception e)
{
SteamManager.Logger.LogError(e, "A subscriber threw while handling Steam callback {CallbackId}.", message.CallbackId);
}
}
}
private void HandleApiCallCompleted(SteamCallbackData data)
{
// SteamAPICallCompleted_t { SteamAPICall_t m_hAsyncCall; int m_iCallback; uint32 m_cubParam; } - same layout under pack 4 and 8.
var apiCall = data.ReadUInt64(0);
var expectedCallback = data.ReadInt32(8);
var resultSize = (int)data.ReadUInt32(12);
List<Action<SteamApiCallResult>> handlers;
lock (_sync)
{
if (!_pendingCallResults.TryGetValue(apiCall, out handlers))
{
return;
}
_pendingCallResults.Remove(apiCall);
}
var buffer = Marshal.AllocHGlobal(Math.Max(resultSize, 1));
SteamApiCallResult result;
try
{
var fetched = SteamNativeApi.ManualDispatchGetApiCallResult(apiCall, buffer, resultSize, expectedCallback, out var failed);
result = fetched
? new SteamApiCallResult(apiCall, expectedCallback, failed, CopyPayload(buffer, resultSize))
: new SteamApiCallResult(apiCall, expectedCallback, true, Array.Empty<byte>());
}
finally
{
Marshal.FreeHGlobal(buffer);
}
InvokeCallResultHandlers(handlers, result);
}
private void RemoveCallResult(ulong apiCall, Action<SteamApiCallResult> handler)
{
lock (_sync)
{
if (_pendingCallResults.TryGetValue(apiCall, out var list) && list.Remove(handler) && list.Count == 0)
{
_pendingCallResults.Remove(apiCall);
}
}
}
private static void InvokeCallResultHandlers(List<Action<SteamApiCallResult>> handlers, SteamApiCallResult result)
{
foreach (var handler in handlers)
{
try
{
handler(result);
}
catch (Exception e)
{
SteamManager.Logger.LogError(e, "A handler threw while handling the result of Steam API call {ApiCall}.", result.ApiCall);
}
}
}
private static byte[] CopyPayload(IntPtr source, int size)
{
if (source == IntPtr.Zero || size <= 0)
{
return Array.Empty<byte>();
}
var payload = new byte[size];
Marshal.Copy(source, payload, 0, size);
return payload;
}
private sealed class Subscription : IDisposable
{
private Action _dispose;
public Subscription(Action dispose)
{
_dispose = dispose;
}
public void Dispose()
{
Interlocked.Exchange(ref _dispose, null)?.Invoke();
}
}
}
}
@@ -356,6 +356,59 @@ namespace OmegaLeo.HelperLib.Steamworks
}
}
[Documentation(nameof(SteamAppsManager), "Provides ownership, DLC and language queries from ISteamApps.", null,
@"```csharp
var apps = SteamManager.Instance.Apps;
if (apps.BIsSubscribedApp(5498630)) { /* user owns the demo */ }
var language = apps.GetCurrentGameLanguage(); // e.g. ""english""
```")]
public class SteamAppsManager
{
public bool IsAvailable => SteamNativeApi.IsSteamAppsAvailable();
[Documentation(nameof(BIsSubscribed), "Checks whether the current user owns (is subscribed to) the running app.", null, null)]
public bool BIsSubscribed()
{
return SteamNativeApi.AppsIsSubscribed();
}
[Documentation(nameof(BIsSubscribedApp), "Checks whether the current user owns the given app, for example a related demo or another of your games.",new[] { "appId: AppId to check." }, null)]
public bool BIsSubscribedApp(uint appId)
{
return SteamNativeApi.AppsIsSubscribedApp(appId);
}
[Documentation(nameof(BIsDlcInstalled), "Checks whether the user owns the given DLC and it is installed.", new[] { "appId: DLC AppId." }, null)]
public bool BIsDlcInstalled(uint appId)
{
return SteamNativeApi.AppsIsDlcInstalled(appId);
}
[Documentation(nameof(BIsAppInstalled), "Checks whether the given app is installed on this machine.", new[] { "appId: AppId to check." }, null)]
public bool BIsAppInstalled(uint appId)
{
return SteamNativeApi.AppsIsAppInstalled(appId);
}
[Documentation(nameof(GetCurrentGameLanguage), "Gets the language the user selected for this game in Steam (API language code such as \"english\"), or an empty string when unavailable.", null, null)]
public string GetCurrentGameLanguage()
{
return SteamNativeApi.AppsGetCurrentGameLanguage();
}
[Documentation(nameof(GetAvailableGameLanguages), "Gets a comma-separated list of the languages the game supports.", null, null)]
public string GetAvailableGameLanguages()
{
return SteamNativeApi.AppsGetAvailableGameLanguages();
}
[Documentation(nameof(GetAppBuildId), "Gets the Steam build id of the installed depot.", null, null)]
public int GetAppBuildId()
{
return SteamNativeApi.AppsGetAppBuildId();
}
}
[Documentation(nameof(SteamUtilsManager), "Provides access checks for ISteamUtils.", null, null)]
public class SteamUtilsManager
{
+102 -9
View File
@@ -5,11 +5,13 @@ using OmegaLeo.HelperLib.Shared.Attributes;
namespace OmegaLeo.HelperLib.Steamworks
{
[Documentation(nameof(SteamManager), "Manages Steamworks initialization, callback polling, and shutdown for applications using the Steam API.", null,
[Documentation(nameof(SteamManager), "Manages Steamworks initialization, callback dispatch, and shutdown for applications using the Steam API.", null,
@"```csharp
SteamManager.ConfigureLogger(logger);
SteamManager.ConfigureAppId(480);
SteamManager.ConfigureLibraryPath(pluginFolder);
if (SteamManager.RestartAppIfNecessary(480)) { Quit(); return; }
var steam = SteamManager.Instance;
steam.OverlayActivated += overlay => Pause(overlay.Active);
steam.Update();
```")]
public class SteamManager
@@ -17,6 +19,8 @@ steam.Update();
private static SteamManager _instance;
private static uint? _configuredAppId;
private static ILogger _logger = NullLogger.Instance;
private static readonly SteamCallbackDispatcher CallbackDispatcher = new SteamCallbackDispatcher();
private static readonly SteamAppsManager AppsManager = new SteamAppsManager();
private static readonly SteamUgcManager UgcManager = new SteamUgcManager();
private static readonly SteamRemotePlayManager RemotePlayManager = new SteamRemotePlayManager();
private static readonly SteamControllerManager ControllerManager = new SteamControllerManager();
@@ -41,14 +45,76 @@ steam.Update();
internal static ILogger Logger => _logger;
internal static SteamCallbackDispatcher Dispatcher => CallbackDispatcher;
[Documentation(nameof(ConfigureLogger), "Configures the logger used by Steamworks helper classes.", new[] { "logger: ILogger instance to receive Steamworks logs." }, null)]
public static void ConfigureLogger(ILogger logger)
{
_logger = logger ?? NullLogger.Instance;
}
[Documentation(nameof(ConfigureAppId), "Configures the Steam AppId used during initialization. Must be called before accessing the singleton instance.", new[] { "appId: Steam application identifier." }, null)]
[Documentation(nameof(ConfigureLibraryPath), "Adds a folder (or full file path) to search first for the Steam API native library (steam_api64.dll / libsteam_api.so / libsteam_api.dylib). Can be called several times; paths are tried in order, then the default locations. Must be called before Instance or RestartAppIfNecessary. Returns false when the library is already loaded.", new[] { "directoryOrFile: Folder containing the native library, or the full path to it." },
@"```csharp
SteamManager.ConfigureLibraryPath(Path.Combine(gameDataFolder, ""Plugins"", ""x86_64""));
```")]
public static bool ConfigureLibraryPath(string directoryOrFile)
{
if (!SteamNativeApi.AddLibrarySearchPath(directoryOrFile))
{
_logger.LogWarning("ConfigureLibraryPath ignored for '{SteamLibraryPath}': the path is empty or the Steam API library is already loaded.", directoryOrFile);
return false;
}
return true;
}
[Documentation(nameof(ConfigureManualDispatch), "Enables or disables manual callback dispatch (enabled by default). When disabled, Update() uses SteamAPI_RunCallbacks and no callback events or async call results are delivered. Must be called before Instance.", new[] { "enabled: True to use manual dispatch." }, null)]
public static bool ConfigureManualDispatch(bool enabled)
{
return _instance == null && SteamNativeApi.ConfigureManualDispatch(enabled);
}
[Documentation(nameof(RestartAppIfNecessary), "Calls SteamAPI_RestartAppIfNecessary. Returns true when the game was not launched through Steam and Steam is relaunching it: quit immediately in that case. Call it before Instance and before ConfigureAppId (steam_appid.txt, and possibly the SteamAppId environment variable, suppress the relaunch). Returns false when the native library cannot be loaded.", new[] { "appId: Your Steam AppId." },
@"```csharp
if (SteamManager.RestartAppIfNecessary(480))
{
Quit();
return;
}
```")]
public static bool RestartAppIfNecessary(uint appId)
{
if (appId == 0)
{
return false;
}
if (_configuredAppId.HasValue)
{
_logger.LogWarning("RestartAppIfNecessary was called after ConfigureAppId; the AppId environment variables or steam_appid.txt may stop Steam from relaunching the game.");
}
var restarting = SteamNativeApi.RestartAppIfNecessary(appId);
if (restarting)
{
_logger.LogInformation("Steam is relaunching the game through the Steam client; the current process should exit.");
}
return restarting;
}
[Documentation(nameof(ConfigureAppId), "Configures the Steam AppId used during initialization by setting the SteamAppId/SteamGameId environment variables. Does not write steam_appid.txt. Must be called before accessing the singleton instance.", new[] { "appId: Steam application identifier." }, null)]
public static bool ConfigureAppId(uint appId)
{
return ConfigureAppId(appId, false);
}
[Documentation(nameof(ConfigureAppId), "Configures the Steam AppId used during initialization. Optionally writes steam_appid.txt next to the executable and in the working directory (useful in editors and development builds; leave false for shipped builds). Must be called before accessing the singleton instance.", new[]
{
"appId: Steam application identifier.",
"writeSteamAppIdFile: True to also write steam_appid.txt."
}, null)]
public static bool ConfigureAppId(uint appId, bool writeSteamAppIdFile)
{
if (appId == 0)
{
@@ -62,13 +128,29 @@ steam.Update();
}
_configuredAppId = appId;
SteamNativeApi.ConfigureAppId(appId);
SteamNativeApi.ConfigureAppId(appId, writeSteamAppIdFile);
return true;
}
[Documentation(nameof(IsSteamworksInitialized), "Indicates whether Steamworks is initialized and ready for API calls.", null, null)]
public bool IsSteamworksInitialized { get; private set; }
[Documentation(nameof(InitErrorMessage), "The reason initialization failed, as reported by SteamAPI_InitFlat when available. Empty after a successful init.", null, null)]
public string InitErrorMessage { get; private set; } = string.Empty;
[Documentation(nameof(Callbacks), "Provides callback subscriptions and async call results through manual dispatch.", null, null)]
public SteamCallbackDispatcher Callbacks => CallbackDispatcher;
[Documentation(nameof(OverlayActivated), "Raised from Update() when the Steam overlay opens or closes. Same as Callbacks.OverlayActivated.", null, null)]
public event Action<SteamOverlayActivated> OverlayActivated
{
add => CallbackDispatcher.OverlayActivated += value;
remove => CallbackDispatcher.OverlayActivated -= value;
}
[Documentation(nameof(Apps), "Provides access to ISteamApps bindings (ownership, DLC, language).", null, null)]
public SteamAppsManager Apps => AppsManager;
[Documentation(nameof(Ugc), "Provides access to Steam Workshop (UGC) helper operations.", null, null)]
public SteamUgcManager Ugc => UgcManager;
@@ -134,11 +216,12 @@ steam.Update();
if (SteamNativeApi.Initialize(_configuredAppId))
{
IsSteamworksInitialized = true;
_logger.LogInformation("Steamworks initialized successfully.");
_logger.LogInformation("Steamworks initialized successfully (manual callback dispatch: {ManualDispatchActive}).", SteamNativeApi.IsManualDispatchActive);
}
else
{
_logger.LogWarning("Steamworks initialization failed. Ensure Steam is running and AppId context is valid.");
InitErrorMessage = SteamNativeApi.LastInitErrorMessage;
_logger.LogWarning("Steamworks initialization failed: {InitErrorMessage} Ensure Steam is running and AppId context is valid.", InitErrorMessage);
}
}
catch (Exception e)
@@ -148,20 +231,30 @@ steam.Update();
}
// This must be called from your game's main loop (e.g., Update in Unity/Godot)
[Documentation(nameof(Update), "Executes Steamworks callbacks. Call every frame from your game loop.", null, null)]
[Documentation(nameof(Update), "Executes Steamworks callbacks (manual dispatch, or SteamAPI_RunCallbacks when manual dispatch is unavailable) and delivers events and async call results. Call every frame from your game loop's main thread.", null, null)]
public void Update()
{
if (IsSteamworksInitialized)
if (!IsSteamworksInitialized)
{
return;
}
if (SteamNativeApi.IsManualDispatchActive)
{
CallbackDispatcher.RunFrame();
}
else
{
SteamNativeApi.RunCallbacks();
}
}
[Documentation(nameof(Shutdown), "Shuts down Steamworks and clears the initialized state.", null, null)]
[Documentation(nameof(Shutdown), "Shuts down Steamworks and clears the initialized state. Pending async call results complete with IoFailure = true.", null, null)]
public void Shutdown()
{
if (IsSteamworksInitialized)
{
CallbackDispatcher.FailPendingCallResults();
SteamNativeApi.Shutdown();
IsSteamworksInitialized = false;
}
+487 -19
View File
@@ -2,18 +2,70 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using Microsoft.Extensions.Logging;
namespace OmegaLeo.HelperLib.Steamworks
{
internal readonly struct SteamNativeCallbackMessage
{
public SteamNativeCallbackMessage(int steamUser, int callbackId, IntPtr param, int paramSize)
{
SteamUser = steamUser;
CallbackId = callbackId;
Param = param;
ParamSize = paramSize;
}
public int SteamUser { get; }
public int CallbackId { get; }
public IntPtr Param { get; }
public int ParamSize { get; }
}
internal static class SteamNativeApi
{
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I1)]
private delegate bool SteamApiInitDelegate();
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate int SteamApiInitFlatDelegate(IntPtr errorMessage);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I1)]
private delegate bool SteamApiRestartAppIfNecessaryDelegate(uint appId);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate void SteamApiVoidDelegate();
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate int SteamApiGetHSteamPipeDelegate();
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate void SteamApiManualDispatchPipeDelegate(int steamPipe);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I1)]
private delegate bool SteamApiManualDispatchGetNextCallbackDelegate(int steamPipe, IntPtr callbackMessage);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I1)]
private delegate bool SteamApiManualDispatchGetApiCallResultDelegate(int steamPipe, ulong apiCall, IntPtr callbackBuffer, int callbackSize, int callbackExpected, [MarshalAs(UnmanagedType.I1)] out bool failed);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I1)]
private delegate bool SteamAppsBoolDelegate(IntPtr steamApps);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I1)]
private delegate bool SteamAppsAppIdBoolDelegate(IntPtr steamApps, uint appId);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate IntPtr SteamAppsStringDelegate(IntPtr steamApps);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate int SteamAppsGetAppBuildIdDelegate(IntPtr steamApps);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate IntPtr SteamFriendsAccessorDelegate();
@@ -289,13 +341,41 @@ namespace OmegaLeo.HelperLib.Steamworks
private static readonly object SyncRoot = new object();
// SteamErrMsg is a char[1024] buffer in steam_api_common.h.
private const int SteamErrMsgSize = 1024;
// Large enough for CallbackMsg_t on 32-bit and 64-bit (24 bytes on x64).
private const int CallbackMessageBufferSize = 64;
private static IntPtr _libraryHandle;
private static bool _libraryLoaded;
private static bool _loadFailureLogged;
private static bool _initialized;
private static bool _manualDispatchEnabled = true;
private static bool _manualDispatchActive;
private static int _steamPipe;
private static IntPtr _callbackMessageBuffer;
private static readonly List<string> CustomLibraryPaths = new List<string>();
private static SteamApiInitFlatDelegate _steamApiInitFlat;
private static SteamApiInitDelegate _steamApiInit;
private static SteamApiRestartAppIfNecessaryDelegate _steamApiRestartAppIfNecessary;
private static SteamApiVoidDelegate _steamApiShutdown;
private static SteamApiVoidDelegate _steamApiRunCallbacks;
private static SteamApiGetHSteamPipeDelegate _steamApiGetHSteamPipe;
private static SteamApiVoidDelegate _steamApiManualDispatchInit;
private static SteamApiManualDispatchPipeDelegate _steamApiManualDispatchRunFrame;
private static SteamApiManualDispatchGetNextCallbackDelegate _steamApiManualDispatchGetNextCallback;
private static SteamApiManualDispatchPipeDelegate _steamApiManualDispatchFreeLastCallback;
private static SteamApiManualDispatchGetApiCallResultDelegate _steamApiManualDispatchGetApiCallResult;
private static SteamAppsBoolDelegate _steamAppsIsSubscribed;
private static SteamAppsAppIdBoolDelegate _steamAppsIsSubscribedApp;
private static SteamAppsAppIdBoolDelegate _steamAppsIsDlcInstalled;
private static SteamAppsAppIdBoolDelegate _steamAppsIsAppInstalled;
private static SteamAppsStringDelegate _steamAppsGetCurrentGameLanguage;
private static SteamAppsStringDelegate _steamAppsGetAvailableGameLanguages;
private static SteamAppsGetAppBuildIdDelegate _steamAppsGetAppBuildId;
private static SteamFriendsAccessorDelegate _steamFriendsAccessor;
private static SteamUserStatsAccessorDelegate _steamUserStatsAccessor;
private static SteamUgcAccessorDelegate _steamUgcAccessor;
@@ -385,6 +465,7 @@ namespace OmegaLeo.HelperLib.Steamworks
private static readonly Dictionary<string, SteamInterfaceAccessorDelegate> AdditionalInterfaceAccessors = new Dictionary<string, SteamInterfaceAccessorDelegate>();
private static readonly Dictionary<string, IntPtr> AdditionalInterfacePointers = new Dictionary<string, IntPtr>();
private const string InterfaceSteamApps = "SteamApps";
private const string InterfaceSteamController = "SteamController";
private const string InterfaceSteamGameCoordinator = "SteamGameCoordinator";
private const string InterfaceSteamGameServer = "SteamGameServer";
@@ -403,13 +484,53 @@ namespace OmegaLeo.HelperLib.Steamworks
public static bool IsAvailable => EnsureLoaded();
public static bool IsManualDispatchActive => _manualDispatchActive;
public static string LastInitErrorMessage { get; private set; } = string.Empty;
private static ILogger Logger => SteamManager.Logger;
public static bool AddLibrarySearchPath(string directoryOrFile)
{
if (string.IsNullOrWhiteSpace(directoryOrFile))
{
return false;
}
lock (SyncRoot)
{
if (_libraryLoaded)
{
return false;
}
CustomLibraryPaths.Add(directoryOrFile);
_loadFailureLogged = false;
return true;
}
}
public static bool ConfigureManualDispatch(bool enabled)
{
lock (SyncRoot)
{
if (_initialized)
{
return false;
}
_manualDispatchEnabled = enabled;
return true;
}
}
public static bool Initialize(uint? appId = null)
{
lock (SyncRoot)
{
if (appId.HasValue && appId.Value > 0)
{
ConfigureAppId(appId.Value);
ApplyAppIdEnvironment(appId.Value);
}
if (_initialized)
@@ -419,25 +540,39 @@ namespace OmegaLeo.HelperLib.Steamworks
if (!EnsureLoaded())
{
LastInitErrorMessage = "The Steam API native library could not be loaded.";
return false;
}
if (_steamApiInit == null)
{
return false;
}
_initialized = _steamApiInit();
_initialized = InvokeSteamApiInit();
if (_initialized)
{
CacheInterfaces();
TryEnableManualDispatch();
}
return _initialized;
}
}
public static void ConfigureAppId(uint appId)
public static bool RestartAppIfNecessary(uint appId)
{
if (!EnsureLoaded())
{
Logger.LogError("SteamAPI_RestartAppIfNecessary skipped: the Steam API native library could not be loaded.");
return false;
}
if (_steamApiRestartAppIfNecessary == null)
{
Logger.LogWarning("SteamAPI_RestartAppIfNecessary export not found in the loaded Steam API library.");
return false;
}
return _steamApiRestartAppIfNecessary(appId);
}
public static void ConfigureAppId(uint appId, bool writeSteamAppIdFile)
{
if (appId == 0)
{
@@ -447,7 +582,10 @@ namespace OmegaLeo.HelperLib.Steamworks
lock (SyncRoot)
{
ApplyAppIdEnvironment(appId);
TryWriteSteamAppIdFile(appId);
if (writeSteamAppIdFile)
{
TryWriteSteamAppIdFile(appId);
}
}
}
@@ -462,6 +600,14 @@ namespace OmegaLeo.HelperLib.Steamworks
_steamApiShutdown?.Invoke();
_initialized = false;
_manualDispatchActive = false;
_steamPipe = 0;
if (_callbackMessageBuffer != IntPtr.Zero)
{
Marshal.FreeHGlobal(_callbackMessageBuffer);
_callbackMessageBuffer = IntPtr.Zero;
}
_steamFriends = IntPtr.Zero;
_steamUserStats = IntPtr.Zero;
_steamUgc = IntPtr.Zero;
@@ -472,7 +618,8 @@ namespace OmegaLeo.HelperLib.Steamworks
public static void RunCallbacks()
{
if (!_initialized)
// SteamAPI_RunCallbacks must never run once manual dispatch is in use.
if (!_initialized || _manualDispatchActive)
{
return;
}
@@ -480,6 +627,152 @@ namespace OmegaLeo.HelperLib.Steamworks
_steamApiRunCallbacks?.Invoke();
}
public static void ManualDispatchRunFrame()
{
if (!_manualDispatchActive)
{
return;
}
_steamApiManualDispatchRunFrame(_steamPipe);
}
public static bool ManualDispatchGetNextCallback(out SteamNativeCallbackMessage message)
{
message = default;
if (!_manualDispatchActive || !_steamApiManualDispatchGetNextCallback(_steamPipe, _callbackMessageBuffer))
{
return false;
}
// CallbackMsg_t { HSteamUser m_hSteamUser; int m_iCallback; uint8 *m_pubParam; int m_cubParam; }
// The pointer lands on offset 8 under both pack 4 and pack 8, so the layout is the same on every platform.
message = new SteamNativeCallbackMessage(
Marshal.ReadInt32(_callbackMessageBuffer, 0),
Marshal.ReadInt32(_callbackMessageBuffer, 4),
Marshal.ReadIntPtr(_callbackMessageBuffer, 8),
Marshal.ReadInt32(_callbackMessageBuffer, 8 + IntPtr.Size));
return true;
}
public static void ManualDispatchFreeLastCallback()
{
if (!_manualDispatchActive)
{
return;
}
_steamApiManualDispatchFreeLastCallback(_steamPipe);
}
public static bool ManualDispatchGetApiCallResult(ulong apiCall, IntPtr callbackBuffer, int callbackSize, int callbackExpected, out bool failed)
{
failed = true;
if (!_manualDispatchActive)
{
return false;
}
return _steamApiManualDispatchGetApiCallResult(_steamPipe, apiCall, callbackBuffer, callbackSize, callbackExpected, out failed);
}
private static bool InvokeSteamApiInit()
{
LastInitErrorMessage = string.Empty;
if (_steamApiInitFlat != null)
{
var errorBuffer = Marshal.AllocHGlobal(SteamErrMsgSize);
try
{
Marshal.WriteByte(errorBuffer, 0, 0);
var result = _steamApiInitFlat(errorBuffer);
if (result == 0)
{
Logger.LogDebug("Steamworks initialized through SteamAPI_InitFlat.");
return true;
}
var message = Marshal.PtrToStringAnsi(errorBuffer) ?? string.Empty;
LastInitErrorMessage = string.IsNullOrWhiteSpace(message) ? DescribeInitResult(result) : message;
Logger.LogWarning("SteamAPI_InitFlat failed with {InitResult} ({InitResultName}): {InitErrorMessage}", result, DescribeInitResult(result), LastInitErrorMessage);
return false;
}
finally
{
Marshal.FreeHGlobal(errorBuffer);
}
}
if (_steamApiInit != null)
{
if (_steamApiInit())
{
Logger.LogDebug("Steamworks initialized through the legacy SteamAPI_Init export.");
return true;
}
LastInitErrorMessage = "SteamAPI_Init returned false (this SDK version does not report a reason).";
return false;
}
LastInitErrorMessage = "No SteamAPI_InitFlat, SteamAPI_Init or SteamAPI_InitSafe export was found.";
Logger.LogError(LastInitErrorMessage);
return false;
}
private static string DescribeInitResult(int result)
{
// ESteamAPIInitResult
switch (result)
{
case 0: return "OK";
case 1: return "FailedGeneric";
case 2: return "NoSteamClient";
case 3: return "VersionMismatch";
default: return "Unknown";
}
}
private static void TryEnableManualDispatch()
{
if (!_manualDispatchEnabled)
{
Logger.LogInformation("Manual callback dispatch disabled; using SteamAPI_RunCallbacks.");
return;
}
if (_steamApiGetHSteamPipe == null ||
_steamApiManualDispatchInit == null ||
_steamApiManualDispatchRunFrame == null ||
_steamApiManualDispatchGetNextCallback == null ||
_steamApiManualDispatchFreeLastCallback == null ||
_steamApiManualDispatchGetApiCallResult == null)
{
Logger.LogWarning("Steam manual dispatch exports not found; falling back to SteamAPI_RunCallbacks. Callback events and async call results will not be delivered.");
return;
}
var steamPipe = _steamApiGetHSteamPipe();
if (steamPipe == 0)
{
Logger.LogWarning("SteamAPI_GetHSteamPipe returned an invalid pipe; falling back to SteamAPI_RunCallbacks.");
return;
}
_steamApiManualDispatchInit();
_steamPipe = steamPipe;
if (_callbackMessageBuffer == IntPtr.Zero)
{
_callbackMessageBuffer = Marshal.AllocHGlobal(CallbackMessageBufferSize);
}
_manualDispatchActive = true;
Logger.LogDebug("Steam manual callback dispatch enabled.");
}
public static string GetPersonaName()
{
if (!EnsureReady())
@@ -784,6 +1077,8 @@ namespace OmegaLeo.HelperLib.Steamworks
return _steamRemotePlayGetSessionClientResolution?.Invoke(_steamRemotePlay, sessionId, out resolutionX, out resolutionY) ?? false;
}
public static bool IsSteamAppsAvailable() => EnsureAdditionalInterfaceReady(InterfaceSteamApps);
public static bool IsSteamControllerAvailable() => EnsureAdditionalInterfaceReady(InterfaceSteamController);
public static bool IsSteamFriendsAvailable() => EnsureFriendsReady();
@@ -928,6 +1223,85 @@ namespace OmegaLeo.HelperLib.Steamworks
return _steamUtilsIsOverlayEnabled?.Invoke(steamUtils) ?? false;
}
public static bool AppsIsSubscribed()
{
if (!EnsureAdditionalInterfaceReady(InterfaceSteamApps))
{
return false;
}
var steamApps = GetAdditionalInterfacePointer(InterfaceSteamApps);
return _steamAppsIsSubscribed?.Invoke(steamApps) ?? false;
}
public static bool AppsIsSubscribedApp(uint appId)
{
if (!EnsureAdditionalInterfaceReady(InterfaceSteamApps))
{
return false;
}
var steamApps = GetAdditionalInterfacePointer(InterfaceSteamApps);
return _steamAppsIsSubscribedApp?.Invoke(steamApps, appId) ?? false;
}
public static bool AppsIsDlcInstalled(uint appId)
{
if (!EnsureAdditionalInterfaceReady(InterfaceSteamApps))
{
return false;
}
var steamApps = GetAdditionalInterfacePointer(InterfaceSteamApps);
return _steamAppsIsDlcInstalled?.Invoke(steamApps, appId) ?? false;
}
public static bool AppsIsAppInstalled(uint appId)
{
if (!EnsureAdditionalInterfaceReady(InterfaceSteamApps))
{
return false;
}
var steamApps = GetAdditionalInterfacePointer(InterfaceSteamApps);
return _steamAppsIsAppInstalled?.Invoke(steamApps, appId) ?? false;
}
public static string AppsGetCurrentGameLanguage()
{
if (!EnsureAdditionalInterfaceReady(InterfaceSteamApps))
{
return string.Empty;
}
var steamApps = GetAdditionalInterfacePointer(InterfaceSteamApps);
var languagePtr = _steamAppsGetCurrentGameLanguage?.Invoke(steamApps) ?? IntPtr.Zero;
return languagePtr == IntPtr.Zero ? string.Empty : Marshal.PtrToStringAnsi(languagePtr) ?? string.Empty;
}
public static string AppsGetAvailableGameLanguages()
{
if (!EnsureAdditionalInterfaceReady(InterfaceSteamApps))
{
return string.Empty;
}
var steamApps = GetAdditionalInterfacePointer(InterfaceSteamApps);
var languagesPtr = _steamAppsGetAvailableGameLanguages?.Invoke(steamApps) ?? IntPtr.Zero;
return languagesPtr == IntPtr.Zero ? string.Empty : Marshal.PtrToStringAnsi(languagesPtr) ?? string.Empty;
}
public static int AppsGetAppBuildId()
{
if (!EnsureAdditionalInterfaceReady(InterfaceSteamApps))
{
return 0;
}
var steamApps = GetAdditionalInterfacePointer(InterfaceSteamApps);
return _steamAppsGetAppBuildId?.Invoke(steamApps) ?? 0;
}
public static bool RemoteStorageFileExists(string fileName)
{
if (!EnsureAdditionalInterfaceReady(InterfaceSteamRemoteStorage))
@@ -1486,35 +1860,66 @@ namespace OmegaLeo.HelperLib.Steamworks
return true;
}
var failures = new List<string>();
foreach (var candidate in GetLibraryCandidates())
{
try
{
_libraryHandle = LoadUnmanagedLibrary(candidate);
_libraryLoaded = _libraryHandle != IntPtr.Zero;
if (_libraryLoaded)
var handle = LoadUnmanagedLibrary(candidate);
if (handle != IntPtr.Zero)
{
_libraryHandle = handle;
_libraryLoaded = true;
BindDelegates();
Logger.LogInformation("Loaded Steam API native library from '{SteamLibraryPath}'.", candidate);
return true;
}
failures.Add($"{candidate} ({GetLastLoadError()})");
}
catch
catch (Exception e)
{
// Try the next candidate.
failures.Add($"{candidate} ({e.GetType().Name}: {e.Message})");
}
}
if (!_loadFailureLogged)
{
_loadFailureLogged = true;
Logger.LogError(
"Could not load the Steam API native library '{SteamLibraryName}'. Call SteamManager.ConfigureLibraryPath with the folder that contains it. Tried: {SteamLibraryCandidates}",
GetLibraryFileName(),
string.Join("; ", failures));
}
return false;
}
}
private static void BindDelegates()
{
// SDK 1.58+ exposes SteamAPI_InitFlat (with an error message); older SDKs only have SteamAPI_Init/InitSafe.
_steamApiInitFlat = Bind<SteamApiInitFlatDelegate>("SteamAPI_InitFlat");
_steamApiInit =
Bind<SteamApiInitDelegate>("SteamAPI_Init")
?? Bind<SteamApiInitDelegate>("SteamAPI_InitSafe");
_steamApiRestartAppIfNecessary = Bind<SteamApiRestartAppIfNecessaryDelegate>("SteamAPI_RestartAppIfNecessary");
_steamApiShutdown = Bind<SteamApiVoidDelegate>("SteamAPI_Shutdown");
_steamApiRunCallbacks = Bind<SteamApiVoidDelegate>("SteamAPI_RunCallbacks");
_steamApiGetHSteamPipe = Bind<SteamApiGetHSteamPipeDelegate>("SteamAPI_GetHSteamPipe");
_steamApiManualDispatchInit = Bind<SteamApiVoidDelegate>("SteamAPI_ManualDispatch_Init");
_steamApiManualDispatchRunFrame = Bind<SteamApiManualDispatchPipeDelegate>("SteamAPI_ManualDispatch_RunFrame");
_steamApiManualDispatchGetNextCallback = Bind<SteamApiManualDispatchGetNextCallbackDelegate>("SteamAPI_ManualDispatch_GetNextCallback");
_steamApiManualDispatchFreeLastCallback = Bind<SteamApiManualDispatchPipeDelegate>("SteamAPI_ManualDispatch_FreeLastCallback");
_steamApiManualDispatchGetApiCallResult = Bind<SteamApiManualDispatchGetApiCallResultDelegate>("SteamAPI_ManualDispatch_GetAPICallResult");
_steamAppsIsSubscribed = Bind<SteamAppsBoolDelegate>("SteamAPI_ISteamApps_BIsSubscribed");
_steamAppsIsSubscribedApp = Bind<SteamAppsAppIdBoolDelegate>("SteamAPI_ISteamApps_BIsSubscribedApp");
_steamAppsIsDlcInstalled = Bind<SteamAppsAppIdBoolDelegate>("SteamAPI_ISteamApps_BIsDlcInstalled");
_steamAppsIsAppInstalled = Bind<SteamAppsAppIdBoolDelegate>("SteamAPI_ISteamApps_BIsAppInstalled");
_steamAppsGetCurrentGameLanguage = Bind<SteamAppsStringDelegate>("SteamAPI_ISteamApps_GetCurrentGameLanguage");
_steamAppsGetAvailableGameLanguages = Bind<SteamAppsStringDelegate>("SteamAPI_ISteamApps_GetAvailableGameLanguages");
_steamAppsGetAppBuildId = Bind<SteamAppsGetAppBuildIdDelegate>("SteamAPI_ISteamApps_GetAppBuildId");
_steamFriendsAccessor = BindInterfaceAccessor<SteamFriendsAccessorDelegate>("SteamFriends", "ISteamFriends");
_steamUserStatsAccessor = BindInterfaceAccessor<SteamUserStatsAccessorDelegate>("SteamUserStats", "ISteamUserStats");
_steamUgcAccessor = BindInterfaceAccessor<SteamUgcAccessorDelegate>("SteamUGC", "ISteamUGC");
@@ -1597,6 +2002,7 @@ namespace OmegaLeo.HelperLib.Steamworks
_steamUserStatsSetStatInt32 = Bind<SteamUserStatsSetStatInt32Delegate>("SteamAPI_ISteamUserStats_SetStatInt32");
// Additional interface accessors requested for broad API coverage.
BindAdditionalInterfaceAccessor(InterfaceSteamApps, "SteamApps", "ISteamApps");
BindAdditionalInterfaceAccessor(InterfaceSteamController, "SteamController", "ISteamController");
BindAdditionalInterfaceAccessor(InterfaceSteamGameCoordinator, "SteamGameCoordinator", "ISteamGameCoordinator");
BindAdditionalInterfaceAccessor(InterfaceSteamGameServer, "SteamGameServer", "ISteamGameServer");
@@ -1735,13 +2141,38 @@ namespace OmegaLeo.HelperLib.Steamworks
{
var libraryName = GetLibraryFileName();
var runtimeIdentifier = GetRuntimeIdentifier();
var candidates = new List<string>();
return new[]
// Paths from SteamManager.ConfigureLibraryPath are tried first, in the order they were added.
foreach (var customPath in CustomLibraryPaths)
{
Path.Combine(AppContext.BaseDirectory, "runtimes", runtimeIdentifier, "native", libraryName),
Path.Combine(AppContext.BaseDirectory, libraryName),
libraryName
};
var isFile = File.Exists(customPath) ||
string.Equals(Path.GetFileName(customPath), libraryName, StringComparison.OrdinalIgnoreCase);
candidates.Add(isFile ? customPath : Path.Combine(customPath, libraryName));
}
candidates.Add(Path.Combine(AppContext.BaseDirectory, "runtimes", runtimeIdentifier, "native", libraryName));
candidates.Add(Path.Combine(AppContext.BaseDirectory, libraryName));
candidates.Add(libraryName);
return candidates.ToArray();
}
private static string GetLastLoadError()
{
try
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error()).Message;
}
var errorPtr = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? DlerrorMac() : DlerrorLinux();
return errorPtr == IntPtr.Zero ? "unknown error" : Marshal.PtrToStringAnsi(errorPtr) ?? "unknown error";
}
catch (Exception e)
{
return e.Message;
}
}
private static string GetRuntimeIdentifier()
@@ -1779,6 +2210,31 @@ namespace OmegaLeo.HelperLib.Steamworks
var appIdValue = appId.ToString();
Environment.SetEnvironmentVariable("SteamAppId", appIdValue);
Environment.SetEnvironmentVariable("SteamGameId", appIdValue);
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return;
}
// Some runtimes keep Environment.SetEnvironmentVariable managed-only on Unix,
// so mirror the values into the native environment that steam_api reads.
try
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
SetEnvMac("SteamAppId", appIdValue, 1);
SetEnvMac("SteamGameId", appIdValue, 1);
}
else
{
SetEnvLinux("SteamAppId", appIdValue, 1);
SetEnvLinux("SteamGameId", appIdValue, 1);
}
}
catch (Exception e)
{
Logger.LogDebug(e, "Could not set the native SteamAppId environment variables.");
}
}
private static void TryWriteSteamAppIdFile(uint appId)
@@ -1856,6 +2312,18 @@ namespace OmegaLeo.HelperLib.Steamworks
[DllImport("libSystem.B.dylib", EntryPoint = "dlsym")]
private static extern IntPtr DlsymMac(IntPtr handle, string symbol);
[DllImport("libdl.so.2", EntryPoint = "dlerror")]
private static extern IntPtr DlerrorLinux();
[DllImport("libSystem.B.dylib", EntryPoint = "dlerror")]
private static extern IntPtr DlerrorMac();
[DllImport("libc.so.6", EntryPoint = "setenv")]
private static extern int SetEnvLinux(string name, string value, int overwrite);
[DllImport("libSystem.B.dylib", EntryPoint = "setenv")]
private static extern int SetEnvMac(string name, string value, int overwrite);
private const int RtldNow = 2;
}
@@ -0,0 +1,121 @@
using OmegaLeo.HelperLib.Shared.Attributes;
namespace OmegaLeo.HelperLib.Steamworks
{
[Documentation(nameof(SteamUgcSubmitItemUpdateResult), "Result of ISteamUGC::SubmitItemUpdate (SubmitItemUpdateResult_t).", null, null)]
public sealed class SteamUgcSubmitItemUpdateResult
{
[Documentation(nameof(Result), "Numeric EResult value (1 = OK).", null, null)]
public int Result { get; private set; }
[Documentation(nameof(UserNeedsToAcceptWorkshopLegalAgreement), "True when the user must accept the Workshop legal agreement before the item becomes visible.", null, null)]
public bool UserNeedsToAcceptWorkshopLegalAgreement { get; private set; }
[Documentation(nameof(PublishedFileId), "The updated Workshop item ID.", null, null)]
public ulong PublishedFileId { get; private set; }
[Documentation(nameof(IoFailure), "True when the result could not be retrieved; the other fields are then not valid.", null, null)]
public bool IoFailure { get; private set; }
[Documentation(nameof(IsSuccess), "True when the result was retrieved and EResult is OK.", null, null)]
public bool IsSuccess => !IoFailure && Result == 1;
internal static SteamUgcSubmitItemUpdateResult FromCallResult(SteamApiCallResult callResult)
{
// SubmitItemUpdateResult_t { EResult m_eResult; bool m_bUserNeedsToAcceptWorkshopLegalAgreement; PublishedFileId_t m_nPublishedFileId; }
// Offsets 0/4/8 under both pack 4 and pack 8.
return new SteamUgcSubmitItemUpdateResult
{
IoFailure = IsFailure(callResult, SteamCallbackIds.SubmitItemUpdateResult, 16),
Result = callResult.ReadInt32(0),
UserNeedsToAcceptWorkshopLegalAgreement = callResult.ReadBoolean(4),
PublishedFileId = callResult.ReadUInt64(8)
};
}
internal static bool IsFailure(SteamApiCallResult callResult, int expectedCallbackId, int minimumSize)
{
return callResult.IoFailure || callResult.CallbackId != expectedCallbackId || callResult.Payload.Length < minimumSize;
}
}
[Documentation(nameof(SteamUgcCreateItemResult), "Result of ISteamUGC::CreateItem (CreateItemResult_t).", null, null)]
public sealed class SteamUgcCreateItemResult
{
[Documentation(nameof(Result), "Numeric EResult value (1 = OK).", null, null)]
public int Result { get; private set; }
[Documentation(nameof(PublishedFileId), "The new Workshop item ID. Pass it to StartItemUpdate to upload content.", null, null)]
public ulong PublishedFileId { get; private set; }
[Documentation(nameof(UserNeedsToAcceptWorkshopLegalAgreement), "True when the user must accept the Workshop legal agreement before the item becomes visible.", null, null)]
public bool UserNeedsToAcceptWorkshopLegalAgreement { get; private set; }
[Documentation(nameof(IoFailure), "True when the result could not be retrieved; the other fields are then not valid.", null, null)]
public bool IoFailure { get; private set; }
[Documentation(nameof(IsSuccess), "True when the result was retrieved and EResult is OK.", null, null)]
public bool IsSuccess => !IoFailure && Result == 1;
internal static SteamUgcCreateItemResult FromCallResult(SteamApiCallResult callResult)
{
// CreateItemResult_t { EResult m_eResult; PublishedFileId_t m_nPublishedFileId; bool m_bUserNeedsToAcceptWorkshopLegalAgreement; }
// Pack 8 (Windows): offsets 0/8/16, size 24. Pack 4 (Linux/macOS): offsets 0/4/12, size 16.
var large = SteamCallbackDispatcher.UsesLargeCallbackPack;
var idOffset = large ? 8 : 4;
var legalOffset = large ? 16 : 12;
return new SteamUgcCreateItemResult
{
IoFailure = SteamUgcSubmitItemUpdateResult.IsFailure(callResult, SteamCallbackIds.CreateItemResult, legalOffset + 1),
Result = callResult.ReadInt32(0),
PublishedFileId = callResult.ReadUInt64(idOffset),
UserNeedsToAcceptWorkshopLegalAgreement = callResult.ReadBoolean(legalOffset)
};
}
}
[Documentation(nameof(SteamUgcQueryCompletedResult), "Result of ISteamUGC::SendQueryUGCRequest (SteamUGCQueryCompleted_t). Release the query handle with ReleaseQueryUgcRequest when done.", null, null)]
public sealed class SteamUgcQueryCompletedResult
{
[Documentation(nameof(QueryHandle), "The UGC query handle the results belong to.", null, null)]
public ulong QueryHandle { get; private set; }
[Documentation(nameof(Result), "Numeric EResult value (1 = OK).", null, null)]
public int Result { get; private set; }
[Documentation(nameof(NumResultsReturned), "Number of items returned in this page.", null, null)]
public uint NumResultsReturned { get; private set; }
[Documentation(nameof(TotalMatchingResults), "Total number of items matching the query.", null, null)]
public uint TotalMatchingResults { get; private set; }
[Documentation(nameof(CachedData), "True when the results came from the local cache.", null, null)]
public bool CachedData { get; private set; }
[Documentation(nameof(NextCursor), "Cursor for the next page (cursor-based queries only).", null, null)]
public string NextCursor { get; private set; } = string.Empty;
[Documentation(nameof(IoFailure), "True when the result could not be retrieved; the other fields are then not valid.", null, null)]
public bool IoFailure { get; private set; }
[Documentation(nameof(IsSuccess), "True when the result was retrieved and EResult is OK.", null, null)]
public bool IsSuccess => !IoFailure && Result == 1;
internal static SteamUgcQueryCompletedResult FromCallResult(SteamApiCallResult callResult)
{
// SteamUGCQueryCompleted_t { UGCQueryHandle_t m_handle; EResult m_eResult; uint32 m_unNumResultsReturned;
// uint32 m_unTotalMatchingResults; bool m_bCachedData; char m_rgchNextCursor[256]; }
// Offsets 0/8/12/16/20/21 under both pack 4 and pack 8.
return new SteamUgcQueryCompletedResult
{
IoFailure = SteamUgcSubmitItemUpdateResult.IsFailure(callResult, SteamCallbackIds.SteamUgcQueryCompleted, 21),
QueryHandle = callResult.ReadUInt64(0),
Result = callResult.ReadInt32(8),
NumResultsReturned = callResult.ReadUInt32(12),
TotalMatchingResults = callResult.ReadUInt32(16),
CachedData = callResult.ReadBoolean(20),
NextCursor = callResult.ReadString(21, 256)
};
}
}
}
@@ -1,3 +1,4 @@
using System;
using OmegaLeo.HelperLib.Shared.Attributes;
namespace OmegaLeo.HelperLib.Steamworks
@@ -68,6 +69,17 @@ namespace OmegaLeo.HelperLib.Steamworks
{
return _ugcManager.UpdateItem(_consumerAppId, _publishedFileId, _options, changeNote, out failedStep);
}
[Documentation(nameof(Submit), "Submits the fluent update session and calls onCompleted with the upload result from SteamManager.Update().", new[]
{
"changeNote: Changelog note for this update.",
"onCompleted: Called once with the upload result.",
"failedStep: Name of the failed step if submission returns 0."
}, null)]
public ulong Submit(string changeNote, Action<SteamUgcSubmitItemUpdateResult> onCompleted, out string failedStep)
{
return _ugcManager.UpdateItem(_consumerAppId, _publishedFileId, _options, changeNote, onCompleted, out failedStep);
}
}
}
@@ -1,3 +1,7 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using OmegaLeo.HelperLib.Shared.Attributes;
namespace OmegaLeo.HelperLib.Steamworks
@@ -113,6 +117,22 @@ var items = ugc.GetSubscribedItems();
return submitHandle;
}
[Documentation(nameof(UpdateItem), "Starts an item update, applies option fields, submits it, and calls onCompleted with the upload result from SteamManager.Update().", new[]
{
"consumerAppId: Your game AppId.",
"publishedFileId: Existing workshop item ID.",
"options: Options object containing fields to update.",
"changeNote: Changelog note for submission.",
"onCompleted: Called once with the upload result.",
"failedStep: Name of the failed step when the call returns 0."
}, null)]
public ulong UpdateItem(uint consumerAppId, ulong publishedFileId, SteamUgcItemUpdateOptions options, string changeNote, Action<SteamUgcSubmitItemUpdateResult> onCompleted, out string failedStep)
{
var submitHandle = UpdateItem(consumerAppId, publishedFileId, options, changeNote, out failedStep);
RegisterResult(submitHandle, onCompleted, SteamUgcSubmitItemUpdateResult.FromCallResult, nameof(SubmitItemUpdate));
return submitHandle;
}
[Documentation(nameof(CreateItem), "Creates a new Workshop item and returns a Steam API call handle.", new[]
{
"consumerAppId: Your game AppId.",
@@ -123,6 +143,30 @@ var items = ugc.GetSubscribedItems();
return SteamNativeApi.CreateItem(consumerAppId, fileType);
}
[Documentation(nameof(CreateItem), "Creates a new Workshop item and calls onCompleted with the CreateItemResult_t from SteamManager.Update(). Returns the Steam API call handle, or 0 on failure.", new[]
{
"consumerAppId: Your game AppId.",
"fileType: Numeric EWorkshopFileType value.",
"onCompleted: Called once with the new item ID and result."
}, null)]
public ulong CreateItem(uint consumerAppId, uint fileType, Action<SteamUgcCreateItemResult> onCompleted)
{
var apiCall = CreateItem(consumerAppId, fileType);
RegisterResult(apiCall, onCompleted, SteamUgcCreateItemResult.FromCallResult, nameof(CreateItem));
return apiCall;
}
[Documentation(nameof(CreateItemAsync), "Creates a new Workshop item and returns a task with the CreateItemResult_t. Keep calling SteamManager.Update() while waiting.", new[]
{
"consumerAppId: Your game AppId.",
"fileType: Numeric EWorkshopFileType value.",
"cancellationToken: Cancels the wait (the Steam call keeps running)."
}, null)]
public Task<SteamUgcCreateItemResult> CreateItemAsync(uint consumerAppId, uint fileType, CancellationToken cancellationToken = default)
{
return AwaitResult(CreateItem(consumerAppId, fileType), SteamUgcCreateItemResult.FromCallResult, nameof(CreateItem), cancellationToken);
}
[Documentation(nameof(StartItemUpdate), "Starts an update transaction for an existing Workshop item.", new[]
{
"consumerAppId: Your game AppId.",
@@ -181,7 +225,31 @@ var items = ugc.GetSubscribedItems();
return SteamNativeApi.SubmitItemUpdate(updateHandle, changeNote);
}
[Documentation(nameof(GetItemUpdateProgress), "Gets progress state for a pending item update upload.", new[] { "updateHandle: Handle from StartItemUpdate.", "bytesProcessed: Uploaded bytes output.", "bytesTotal: Total bytes output." }, null)]
[Documentation(nameof(SubmitItemUpdate), "Submits a pending workshop item update and calls onCompleted with the SubmitItemUpdateResult_t from SteamManager.Update(). Returns the Steam API call handle, or 0 when submission failed.", new[]
{
"updateHandle: Handle from StartItemUpdate.",
"changeNote: Changelog note for the update.",
"onCompleted: Called once with the upload result."
}, null)]
public ulong SubmitItemUpdate(ulong updateHandle, string changeNote, Action<SteamUgcSubmitItemUpdateResult> onCompleted)
{
var apiCall = SubmitItemUpdate(updateHandle, changeNote);
RegisterResult(apiCall, onCompleted, SteamUgcSubmitItemUpdateResult.FromCallResult, nameof(SubmitItemUpdate));
return apiCall;
}
[Documentation(nameof(SubmitItemUpdateAsync), "Submits a pending workshop item update and returns a task with the SubmitItemUpdateResult_t. Keep calling SteamManager.Update() while waiting.", new[]
{
"updateHandle: Handle from StartItemUpdate.",
"changeNote: Changelog note for the update.",
"cancellationToken: Cancels the wait (the upload keeps running)."
}, null)]
public Task<SteamUgcSubmitItemUpdateResult> SubmitItemUpdateAsync(ulong updateHandle, string changeNote, CancellationToken cancellationToken = default)
{
return AwaitResult(SubmitItemUpdate(updateHandle, changeNote), SteamUgcSubmitItemUpdateResult.FromCallResult, nameof(SubmitItemUpdate), cancellationToken);
}
[Documentation(nameof(GetItemUpdateProgress),"Gets progress state for a pending item update upload.", new[] { "updateHandle: Handle from StartItemUpdate.", "bytesProcessed: Uploaded bytes output.", "bytesTotal: Total bytes output." }, null)]
public uint GetItemUpdateProgress(ulong updateHandle, out ulong bytesProcessed, out ulong bytesTotal)
{
return SteamNativeApi.GetItemUpdateProgress(updateHandle, out bytesProcessed, out bytesTotal);
@@ -206,7 +274,29 @@ var items = ugc.GetSubscribedItems();
return SteamNativeApi.SendQueryUgcRequest(queryHandle);
}
[Documentation(nameof(ReleaseQueryUgcRequest), "Releases a UGC query handle after you are done with query results.", new[] { "queryHandle: UGC query handle to release." }, null)]
[Documentation(nameof(SendQueryUgcRequest), "Submits a UGC query and calls onCompleted with the SteamUGCQueryCompleted_t from SteamManager.Update(). Returns the Steam API call handle, or 0 on failure.", new[]
{
"queryHandle: UGC query handle returned from CreateQueryAllUgcRequest.",
"onCompleted: Called once with the query result."
}, null)]
public ulong SendQueryUgcRequest(ulong queryHandle, Action<SteamUgcQueryCompletedResult> onCompleted)
{
var apiCall = SendQueryUgcRequest(queryHandle);
RegisterResult(apiCall, onCompleted, SteamUgcQueryCompletedResult.FromCallResult, nameof(SendQueryUgcRequest));
return apiCall;
}
[Documentation(nameof(SendQueryUgcRequestAsync), "Submits a UGC query and returns a task with the SteamUGCQueryCompleted_t. Keep calling SteamManager.Update() while waiting.", new[]
{
"queryHandle: UGC query handle returned from CreateQueryAllUgcRequest.",
"cancellationToken: Cancels the wait (the query keeps running)."
}, null)]
public Task<SteamUgcQueryCompletedResult> SendQueryUgcRequestAsync(ulong queryHandle, CancellationToken cancellationToken = default)
{
return AwaitResult(SendQueryUgcRequest(queryHandle), SteamUgcQueryCompletedResult.FromCallResult, nameof(SendQueryUgcRequest), cancellationToken);
}
[Documentation(nameof(ReleaseQueryUgcRequest),"Releases a UGC query handle after you are done with query results.", new[] { "queryHandle: UGC query handle to release." }, null)]
public bool ReleaseQueryUgcRequest(ulong queryHandle)
{
return SteamNativeApi.ReleaseQueryUgcRequest(queryHandle);
@@ -235,6 +325,30 @@ var items = ugc.GetSubscribedItems();
{
return SteamNativeApi.UnsubscribeItem(publishedFileId);
}
private static void RegisterResult<T>(ulong apiCall, Action<T> onCompleted, Func<SteamApiCallResult, T> convert, string operation)
{
if (apiCall == 0 || onCompleted == null)
{
return;
}
if (!SteamManager.Dispatcher.RegisterCallResult(apiCall, result => onCompleted(convert(result))))
{
SteamManager.Logger.LogWarning("{Operation} was submitted but its result will not be delivered because Steam manual callback dispatch is not active.", operation);
}
}
private static async Task<T> AwaitResult<T>(ulong apiCall, Func<SteamApiCallResult, T> convert, string operation, CancellationToken cancellationToken)
{
if (apiCall == 0)
{
throw new InvalidOperationException($"{operation} did not return a Steam API call handle. Ensure Steamworks is initialized.");
}
var result = await SteamManager.Dispatcher.WaitForCallResultAsync(apiCall, cancellationToken).ConfigureAwait(false);
return convert(result);
}
}
}