updated SteamWorks library
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user