diff --git a/Plugins/Steamworks.NET/autogen/NativeMethods.cs b/Plugins/Steamworks.NET/autogen/NativeMethods.cs index 6151a6fd..d0b1fc25 100644 --- a/Plugins/Steamworks.NET/autogen/NativeMethods.cs +++ b/Plugins/Steamworks.NET/autogen/NativeMethods.cs @@ -38,7 +38,7 @@ namespace Steamworks { [System.Security.SuppressUnmanagedCodeSecurity()] - internal static class NativeMethods { + internal static partial class NativeMethods { #if STEAMWORKS_WIN && STEAMWORKS_X64 internal const string NativeLibraryName = "steam_api64"; internal const string NativeLibrary_SDKEncryptedAppTicket = "sdkencryptedappticket64"; diff --git a/Plugins/Steamworks.NET/types/SteamNetworkingTypes/SteamNetworkingConfigValue_t.cs b/Plugins/Steamworks.NET/types/SteamNetworkingTypes/SteamNetworkingConfigValue_t.cs index 4b6753f2..98474845 100644 --- a/Plugins/Steamworks.NET/types/SteamNetworkingTypes/SteamNetworkingConfigValue_t.cs +++ b/Plugins/Steamworks.NET/types/SteamNetworkingTypes/SteamNetworkingConfigValue_t.cs @@ -41,6 +41,7 @@ public struct SteamNetworkingConfigValue_t public OptionValue m_val; [StructLayout(LayoutKind.Explicit)] + [System.Serializable] public struct OptionValue { [FieldOffset(0)] diff --git a/Standalone/StandaloneOnlyCode/CallbackDispatcher3_1.cs b/Standalone/StandaloneOnlyCode/CallbackDispatcher3_1.cs new file mode 100644 index 00000000..e09555bb --- /dev/null +++ b/Standalone/StandaloneOnlyCode/CallbackDispatcher3_1.cs @@ -0,0 +1,398 @@ +// This file is provided under The MIT License as part of Steamworks.NET. +// Copyright (c) 2013-2019 Riley Labrecque +// Please see the included LICENSE.txt for additional information. + +// This file is automatically generated. +// Changes to this file will be reverted when you update Steamworks.NET + +//------ This Event dispatch system is only suitable for .NET Core 3.1+(including .NET 5.0+) ------- +#if !(NETCOREAPP3_1 || NET5_0) +#error This Event dispatch system is only suitable for .NET Core 3.1+(including .NET 5.0+) +#endif + +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace Steamworks { + + internal static partial class NativeMethods + { + [DllImport(NativeLibraryName, EntryPoint = "SteamAPI_ManualDispatch_GetNextCallback", CallingConvention = CallingConvention.Cdecl)] + [return: MarshalAs(UnmanagedType.I1)] + public static extern unsafe bool SteamAPI_ManualDispatch_GetNextCallback(HSteamPipe hSteamPipe, CallbackMsg_t* pCallbackMsg); + + [DllImport(NativeLibraryName, EntryPoint = "SteamAPI_ManualDispatch_GetAPICallResult", CallingConvention = CallingConvention.Cdecl)] + [return: MarshalAs(UnmanagedType.I1)] + public static extern unsafe bool SteamAPI_ManualDispatch_GetAPICallResult(HSteamPipe hSteamPipe, SteamAPICall_t hSteamAPICall, void* pCallback, int cubCallback, int iCallbackExpected, out bool pbFailed); + + } + + + public static class CallbackDispatcher { + // We catch exceptions inside callbacks and reroute them here. + // For some reason throwing an exception causes RunCallbacks() to break otherwise. + // If you have a custom ExceptionHandler in your engine you can register it here manually until we get something more elegant hooked up. + public static void ExceptionHandler(Exception e) { + Console.WriteLine(e.Message); + System.Diagnostics.Debug.WriteLine(e); + } + + private static Dictionary> m_registeredCallbacks = new Dictionary>(); + private static Dictionary> m_registeredGameServerCallbacks = new Dictionary>(); + private static Dictionary> m_registeredCallResults = new Dictionary>(); + private static readonly object m_sync = new object(); + private static uint m_paramBufferSize = 4096U * 4U;// allocate 4 pages first, may extend later + private static Memory m_paramBuffer; + private static int m_initCount; + + public static bool IsInitialized { + get { return m_initCount > 0; } + } + + internal static void Initialize() { + lock (m_sync) { + if (m_initCount == 0) { + NativeMethods.SteamAPI_ManualDispatch_Init(); + m_paramBuffer = new Memory(new byte[m_paramBufferSize]); + } + ++m_initCount; + } + } + + internal static void Shutdown() { + lock (m_sync) { + --m_initCount; + if (m_initCount == 0) { + UnregisterAll(); + } + } + } + + internal static void Register(Callback cb) { + int iCallback = CallbackIdentities.GetCallbackIdentity(cb.GetCallbackType()); + var callbacksRegistry = cb.IsGameServer ? m_registeredGameServerCallbacks : m_registeredCallbacks; + lock (m_sync) { + List callbacksList; + if (!callbacksRegistry.TryGetValue(iCallback, out callbacksList)) { + callbacksList = new List(); + callbacksRegistry.Add(iCallback, callbacksList); + } + + callbacksList.Add(cb); + } + } + + internal static void Register(SteamAPICall_t asyncCall, CallResult cr) { + lock (m_sync) { + List callResultsList; + if (!m_registeredCallResults.TryGetValue((ulong)asyncCall, out callResultsList)) { + callResultsList = new List(); + m_registeredCallResults.Add((ulong)asyncCall, callResultsList); + } + + callResultsList.Add(cr); + } + } + + internal static void Unregister(Callback cb) { + int iCallback = CallbackIdentities.GetCallbackIdentity(cb.GetCallbackType()); + var callbacksRegistry = cb.IsGameServer ? m_registeredGameServerCallbacks : m_registeredCallbacks; + lock (m_sync) { + List callbacksList; + if (callbacksRegistry.TryGetValue(iCallback, out callbacksList)) { + callbacksList.Remove(cb); + if (callbacksList.Count == 0) + callbacksRegistry.Remove(iCallback); + } + } + } + + internal static void Unregister(SteamAPICall_t asyncCall, CallResult cr) { + lock (m_sync) { + List callResultsList; + if (m_registeredCallResults.TryGetValue((ulong)asyncCall, out callResultsList)) { + callResultsList.Remove(cr); + if (callResultsList.Count == 0) + m_registeredCallResults.Remove((ulong)asyncCall); + } + } + } + + private static void UnregisterAll() { + List callbacks = new List(); + List callResults = new List(); + lock (m_sync) { + foreach (var pair in m_registeredCallbacks) { + callbacks.AddRange(pair.Value); + } + m_registeredCallbacks.Clear(); + + foreach (var pair in m_registeredGameServerCallbacks) { + callbacks.AddRange(pair.Value); + } + m_registeredGameServerCallbacks.Clear(); + + foreach (var pair in m_registeredCallResults) { + callResults.AddRange(pair.Value); + } + m_registeredCallResults.Clear(); + + foreach (var callback in callbacks) { + callback.SetUnregistered(); + } + + foreach (var callResult in callResults) { + callResult.SetUnregistered(); + } + } + } + + // Allowed unsafe code to use pointer abilities + internal unsafe static void RunFrame(bool isGameServer) { + if (!IsInitialized) throw new InvalidOperationException("Callback dispatcher is not initialized."); + + // Allocate structures on stack + // so we don't need to pin it + CallbackMsg_t callbackMsg; + + HSteamPipe hSteamPipe = (HSteamPipe)(isGameServer ? NativeMethods.SteamGameServer_GetHSteamPipe() : NativeMethods.SteamAPI_GetHSteamPipe()); + NativeMethods.SteamAPI_ManualDispatch_RunFrame(hSteamPipe); + var callbacksRegistry = isGameServer ? m_registeredGameServerCallbacks : m_registeredCallbacks; + while (NativeMethods.SteamAPI_ManualDispatch_GetNextCallback(hSteamPipe, &callbackMsg)) { + try { + // Check for dispatching API call results + if (callbackMsg.m_iCallback == SteamAPICallCompleted_t.k_iCallback) { + SteamAPICallCompleted_t* apicallInfo = (SteamAPICallCompleted_t*)callbackMsg.m_pubParam; + + while (apicallInfo->m_cubParam >= 4096U*4U) { + uint newSize = checked(m_paramBufferSize += 4096U); + m_paramBuffer = new Memory(new byte[newSize]); + } + + using var pinned = m_paramBuffer.Pin(); + if (NativeMethods.SteamAPI_ManualDispatch_GetAPICallResult(hSteamPipe, apicallInfo->m_hAsyncCall, pinned.Pointer, checked((int)apicallInfo->m_cubParam), apicallInfo->m_iCallback, out bool bFailed)) { + lock (m_sync) { + if (m_registeredCallResults.TryGetValue(unchecked((ulong)apicallInfo->m_hAsyncCall), out List callResults)) { + m_registeredCallResults.Remove(unchecked((ulong)apicallInfo->m_hAsyncCall)); + foreach (var cr in callResults) { + cr.OnRunCallResult(pinned.Pointer, bFailed, unchecked((ulong)apicallInfo->m_hAsyncCall)); + cr.SetUnregistered(); + } + } + } + } + } else { + if (callbacksRegistry.TryGetValue(callbackMsg.m_iCallback, out List callbacks)) { + List callbacksCopy; + lock (m_sync) { + callbacksCopy = new List(callbacks); + } + foreach (var callback in callbacksCopy) { + callback.OnRunCallback((void*)callbackMsg.m_pubParam); + } + } + } + } catch (Exception e) { + ExceptionHandler(e); + } finally { + NativeMethods.SteamAPI_ManualDispatch_FreeLastCallback(hSteamPipe); + } + } + } + } + + public abstract class Callback { + public abstract bool IsGameServer { get; } + internal abstract Type GetCallbackType(); + internal abstract unsafe void OnRunCallback(void* pvParam); + internal abstract void SetUnregistered(); + } + + public sealed class Callback : Callback, IDisposable + where T : unmanaged + { + public delegate void DispatchDelegate(ref T param); + private event DispatchDelegate m_Func; + + private bool m_bGameServer; + private bool m_bIsRegistered; + + private bool m_bDisposed = false; + + /// + /// Creates a new Callback. You must be calling SteamAPI.RunCallbacks() to retrieve the callbacks. + /// Returns a handle to the Callback. + /// This MUST be assigned to a member variable to prevent the GC from cleaning it up. + /// + public static Callback Create(DispatchDelegate func) { + return new Callback(func, bGameServer: false); + } + + /// + /// Creates a new GameServer Callback. You must be calling GameServer.RunCallbacks() to retrieve the callbacks. + /// Returns a handle to the Callback. + /// This MUST be assigned to a member variable to prevent the GC from cleaning it up. + /// + public static Callback CreateGameServer(DispatchDelegate func) { + return new Callback(func, bGameServer: true); + } + + public Callback(DispatchDelegate func, bool bGameServer = false) { + m_bGameServer = bGameServer; + Register(func); + } + + ~Callback() { + Dispose(); + } + + public void Dispose() { + if (m_bDisposed) { + return; + } + + GC.SuppressFinalize(this); + + if (m_bIsRegistered) + Unregister(); + + m_bDisposed = true; + } + + // Manual registration of the callback + public void Register(DispatchDelegate func) { + if (m_bIsRegistered) { + Unregister(); + } + + m_Func = func ?? throw new ArgumentException("Callback function must not be null.", nameof(func)); + + CallbackDispatcher.Register(this); + m_bIsRegistered = true; + } + + public void Unregister() { + CallbackDispatcher.Unregister(this); + m_bIsRegistered = false; + } + + public override bool IsGameServer { + get { return m_bGameServer; } + } + + internal override Type GetCallbackType() { + return typeof(T); + } + + internal override unsafe void OnRunCallback(void* pvParam) { + try { + m_Func(ref *(T*)pvParam); + } + catch (Exception e) { + CallbackDispatcher.ExceptionHandler(e); + } + } + + internal override void SetUnregistered() { + m_bIsRegistered = false; + } + } + + public abstract class CallResult { + internal abstract Type GetCallbackType(); + internal abstract unsafe void OnRunCallResult(void* pvParam, bool bFailed, ulong hSteamAPICall); + internal abstract void SetUnregistered(); + } + + public sealed class CallResult : CallResult, IDisposable + where T: unmanaged { + public delegate void APIDispatchDelegate(ref T param, bool bIOFailure); + private event APIDispatchDelegate m_Func; + + private SteamAPICall_t m_hAPICall = SteamAPICall_t.Invalid; + public SteamAPICall_t Handle { get { return m_hAPICall; } } + + private bool m_bDisposed = false; + + /// + /// Creates a new async CallResult. You must be calling SteamAPI.RunCallbacks() to retrieve the callback. + /// Returns a handle to the CallResult. + /// This MUST be assigned to a member variable to prevent the GC from cleaning it up. + /// + public static CallResult Create(APIDispatchDelegate func = null) { + return new CallResult(func); + } + + public CallResult(APIDispatchDelegate func = null) { + m_Func = func; + } + + ~CallResult() { + Dispose(); + } + + public void Dispose() { + if (m_bDisposed) { + return; + } + + GC.SuppressFinalize(this); + + Cancel(); + + m_bDisposed = true; + } + + public void Set(SteamAPICall_t hAPICall, APIDispatchDelegate func = null) { + // Unlike the official SDK we let the user assign a single function during creation, + // and allow them to skip having to do so every time that they call .Set() + if (func != null) { + m_Func = func; + } + + if (m_Func == null) { + throw new ArgumentException("CallResult function was null, you must either set it in the CallResult Constructor or via Set()", nameof(func)); + } + + if (m_hAPICall != SteamAPICall_t.Invalid) { + CallbackDispatcher.Unregister(m_hAPICall, this); + } + + m_hAPICall = hAPICall; + + if (hAPICall != SteamAPICall_t.Invalid) { + CallbackDispatcher.Register(hAPICall, this); + } + } + + public bool IsActive() { + return (m_hAPICall != SteamAPICall_t.Invalid); + } + + public void Cancel() { + if (IsActive()) + CallbackDispatcher.Unregister(m_hAPICall, this); + } + + internal override Type GetCallbackType() { + return typeof(T); + } + + internal override unsafe void OnRunCallResult(void* pvParam, bool bFailed, ulong hSteamAPICall_) { + SteamAPICall_t hSteamAPICall = (SteamAPICall_t)hSteamAPICall_; + if (hSteamAPICall == m_hAPICall) { + try { + m_Func(ref *(T*)pvParam, bFailed); + } + catch (Exception e) { + CallbackDispatcher.ExceptionHandler(e); + } + } + } + + internal override void SetUnregistered() { + m_hAPICall = SteamAPICall_t.Invalid; + } + } +} diff --git a/Standalone/Steamworks.NET.Standard.csproj b/Standalone/Steamworks.NET.Standard.csproj index fd87f475..8264d6d6 100644 --- a/Standalone/Steamworks.NET.Standard.csproj +++ b/Standalone/Steamworks.NET.Standard.csproj @@ -1,7 +1,7 @@  - netstandard2.1 + net5.0 Steamworks Steamworks.NET x64;x86 @@ -52,6 +52,7 @@ x86 prompt MinimumRecommendedRules.ruleset + true @@ -63,8 +64,8 @@ x86 prompt MinimumRecommendedRules.ruleset - - + true + bin\x64\Windows\ TRACE;STEAMWORKS_WIN;STEAMWORKS_X64 @@ -74,6 +75,7 @@ x64 prompt MinimumRecommendedRules.ruleset + true @@ -85,6 +87,7 @@ x64 prompt MinimumRecommendedRules.ruleset + true @@ -126,7 +129,6 @@ - @@ -195,7 +197,7 @@ - +