using System; using System.Collections.Generic; using System.Runtime.InteropServices; using System.Threading.Tasks; using Facepunch.Steamworks.Data; using System.Linq; namespace Facepunch.Steamworks { /// /// Responsible for all callback/callresult handling /// /// This manually pumps Steam's message queue and dispatches those /// events to any waiting callbacks/callresults. /// public static class Dispatch { /// /// If set then we'll call this function every time a callback is generated. /// /// This is SLOW!! - it's for debugging - don't keep it on all the time. If you want to access a specific /// callback then please create an issue on github and I'll add it! /// /// Params are : [Callback Type] [Callback Contents] [server] /// /// public static Action OnDebugCallback; /// /// Called if an exception happens during a callback/callresult. /// This is needed because the exception isn't always accessible when running /// async.. and can fail silently. With this hooked you won't be stuck wondering /// what happened. /// public static Action OnException; #region interop [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ManualDispatch_Init", CallingConvention = CallingConvention.Cdecl )] internal static extern void SteamAPI_ManualDispatch_Init(); [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ManualDispatch_RunFrame", CallingConvention = CallingConvention.Cdecl )] internal static extern void SteamAPI_ManualDispatch_RunFrame( HSteamPipe pipe ); [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ManualDispatch_GetNextCallback", CallingConvention = CallingConvention.Cdecl )] [return: MarshalAs( UnmanagedType.I1 )] internal static extern bool SteamAPI_ManualDispatch_GetNextCallback( HSteamPipe pipe, [In, Out] ref CallbackMsg_t msg ); [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ManualDispatch_FreeLastCallback", CallingConvention = CallingConvention.Cdecl )] [return: MarshalAs( UnmanagedType.I1 )] internal static extern bool SteamAPI_ManualDispatch_FreeLastCallback( HSteamPipe pipe ); [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] internal struct CallbackMsg_t { public HSteamUser m_hSteamUser; // Specific user to whom this callback applies. public CallbackType Type; // Callback identifier. (Corresponds to the k_iCallback enum in the callback structure.) public IntPtr Data; // Points to the callback structure public int DataSize; // Size of the data pointed to by m_pubParam }; #endregion internal static HSteamPipe ClientPipe { get; set; } internal static HSteamPipe ServerPipe { get; set; } /// /// This gets called from Client/Server Init /// It's important to switch to the manual dispatcher /// internal static void Init() { SteamAPI_ManualDispatch_Init(); } /// /// Make sure we don't call Frame in a callback - because that'll cause some issues for everyone. /// static bool runningFrame = false; /// /// Calls RunFrame and processes events from this Steam Pipe /// internal static void Frame( HSteamPipe pipe ) { if ( runningFrame ) return; try { runningFrame = true; SteamAPI_ManualDispatch_RunFrame( pipe ); SteamNetworkingUtils.OutputDebugMessages(); CallbackMsg_t msg = default; while ( SteamAPI_ManualDispatch_GetNextCallback( pipe, ref msg ) ) { try { ProcessCallback( msg, pipe == ServerPipe ); } finally { SteamAPI_ManualDispatch_FreeLastCallback( pipe ); } } } catch ( System.Exception e ) { OnException?.Invoke( e ); } finally { runningFrame = false; } } /// /// To be safe we don't call the continuation functions while iterating /// the Callback list. This is maybe overly safe because the only way this /// could be an issue is if the callback list is modified in the continuation /// which would only happen if starting or shutting down in the callback. /// static List> actionsToCall = new List>(); /// /// A callback is a general global message /// private static void ProcessCallback( CallbackMsg_t msg, bool isServer ) { OnDebugCallback?.Invoke( msg.Type, CallbackToString( msg.Type, msg.Data, msg.DataSize ), isServer ); // Is this a special callback telling us that the call result is ready? if ( msg.Type == CallbackType.SteamAPICallCompleted ) { ProcessResult( msg ); return; } if ( Callbacks.TryGetValue( msg.Type, out var list ) ) { actionsToCall.Clear(); foreach ( var item in list ) { if ( item.server != isServer ) continue; actionsToCall.Add( item.action ); } foreach ( var action in actionsToCall ) { action( msg.Data ); } actionsToCall.Clear(); } } /// /// Given a callback, try to turn it into a string /// internal static string CallbackToString( CallbackType type, IntPtr data, int expectedsize ) { if ( !CallbackTypeFactory.All.TryGetValue( type, out var t ) ) return $"[{type} not in sdk]"; var strct = data.ToType( t ); if ( strct == null ) return "[null]"; var str = ""; var fields = t.GetFields( System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic ); if ( fields.Length == 0 ) return "[no fields]"; var columnSize = fields.Max( x => x.Name.Length ) + 1; if ( columnSize < 10 ) columnSize = 10; foreach ( var field in fields ) { var spaces = (columnSize - field.Name.Length); if ( spaces < 0 ) spaces = 0; str += $"{new String( ' ', spaces )}{field.Name}: {field.GetValue( strct )}\n"; } return str.Trim( '\n' ); } /// /// A result is a reply to a specific command /// private static void ProcessResult( CallbackMsg_t msg ) { var result = msg.Data.ToType(); // // Do we have an entry added via OnCallComplete // if ( !ResultCallbacks.TryGetValue( result.AsyncCall, out var callbackInfo ) ) { // // This can happen if the callback result was immediately available // so we just returned that without actually going through the callback // dance. It's okay for this to fail. // // // But still let everyone know that this happened.. // OnDebugCallback?.Invoke( (CallbackType)result.Callback, $"[no callback waiting/required]", false ); return; } // Remove it before we do anything, incase the continuation throws exceptions ResultCallbacks.Remove( result.AsyncCall ); // At this point whatever async routine called this // continues running. callbackInfo.continuation(); } /// /// Pumps the queue in an async loop so we don't /// have to think about it. This has the advantage that /// you can call .Wait() on async shit and it still works. /// internal static async void LoopClientAsync() { while ( ClientPipe != 0 ) { Frame( ClientPipe ); await Task.Delay( 16 ); } } /// /// Pumps the queue in an async loop so we don't /// have to think about it. This has the advantage that /// you can call .Wait() on async shit and it still works. /// internal static async void LoopServerAsync() { while ( ServerPipe != 0 ) { Frame( ServerPipe ); await Task.Delay( 32 ); } } struct ResultCallback { public Action continuation; public bool server; } static Dictionary ResultCallbacks = new Dictionary(); /// /// Watch for a steam api call /// internal static void OnCallComplete( SteamAPICall_t call, Action continuation, bool server ) where T : struct, ICallbackData { ResultCallbacks[call.Value] = new ResultCallback { continuation = continuation, server = server }; } struct Callback { public Action action; public bool server; } static Dictionary> Callbacks = new Dictionary>(); /// /// Install a global callback. The passed function will get called if it's all good. /// internal static void Install( Action p, bool server = false ) where T : ICallbackData { var t = default( T ); var type = t.CallbackType; if ( !Callbacks.TryGetValue( type, out var list ) ) { list = new List(); Callbacks[type] = list; } list.Add( new Callback { action = x => p( x.ToType() ), server = server } ); } internal static void ShutdownServer() { ServerPipe = 0; foreach ( var callback in Callbacks ) { Callbacks[callback.Key].RemoveAll( x => x.server ); } ResultCallbacks = ResultCallbacks.Where( x => !x.Value.server ) .ToDictionary( x => x.Key, x => x.Value ); } internal static void ShutdownClient() { ClientPipe = 0; foreach ( var callback in Callbacks ) { Callbacks[callback.Key].RemoveAll( x => !x.server ); } ResultCallbacks = ResultCallbacks.Where( x => x.Value.server ) .ToDictionary( x => x.Key, x => x.Value ); } } }