using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; namespace SimConnect.Concrete { /// /// Default implementation of ISimConnectClient. Compatible with Flight Simulator 2020, X Stream Edition, X and Prepar3D. /// Requires the following DLL files to be present: FSX2020-SimConnect.dll, FSX-SE-SimConnect.dll, FSX-SE-SimConnect.dll, FSX-SimConnect.dll. /// These are renamed versions of the SimConnect.dll from the various flight simulator installations. /// public class DefaultSimConnectClient : ISimConnectClient { private readonly ISimConnectLibrary simConnectLibrary; private SimConnectWorker worker; private uint nextDefinitionID = 1; private uint nextEventID = 1; private readonly object observersLock = new object(); private readonly List observers = new List(); /// /// Creates an instance of DefaultSimConnectClient. /// /// The low-level SimConnect library interface to use public DefaultSimConnectClient(ISimConnectLibrary simConnectLibrary) { this.simConnectLibrary = simConnectLibrary; } /// public async ValueTask DisposeAsync() { worker?.Close(); if (simConnectLibrary != null) await simConnectLibrary.DisposeAsync(); } /// /// Attempts to open a connection to the SimConnect server /// /// The application name passed to the SimConnect server. /// public async Task TryOpen(string appName) { worker?.Close(); worker = new SimConnectWorker(simConnectLibrary, appName); return await worker.Open(); } /// public void AttachObserver(ISimConnectClientObserver observer) { Monitor.Enter(observersLock); try { observers.Add(observer); } finally { Monitor.Exit(observersLock); } } /// public IAsyncDisposable AddDefinition(SimConnectDataHandlerAction onData) where T : class { if (worker == null) throw new InvalidOperationException("TryOpen must be called first"); var definition = new SimConnectDefinition(typeof(T)); void HandleData(Stream stream) { var data = definition.ParseData(stream); onData((T)data); } var definitionRegistration = new SimConnectDefinitionRegistration(nextDefinitionID, definition, HandleData, worker); nextDefinitionID++; return definitionRegistration; } /// public IAsyncDisposable SubscribeToSystemEvent(SimConnectSystemEvent systemEvent, SimConnectSystemEventAction onEvent) { if (worker == null) throw new InvalidOperationException("TryOpen must be called first"); void HandleData(SimConnectRecvEvent recvEvent) { SimConnectSystemEventArgs args; switch (systemEvent) { case SimConnectSystemEvent.Pause: args = new SimConnectPauseSystemEventArgs { Paused = recvEvent.dwData == 1 }; break; case SimConnectSystemEvent.Sim: args = new SimConnectSimSystemEventArgs { SimRunning = recvEvent.dwData == 1 }; break; default: throw new ArgumentOutOfRangeException(nameof(systemEvent), systemEvent, null); } onEvent(args); } var eventRegistration = new SimConnectSystemEventRegistration(nextEventID, systemEvent, HandleData, worker); nextEventID++; return eventRegistration; } private class SimConnectDefinitionRegistration : IAsyncDisposable where T : class { private readonly uint definitionID; private readonly SimConnectWorker worker; public SimConnectDefinitionRegistration(uint definitionID, SimConnectDefinition definition, Action onData, SimConnectWorker worker) { this.definitionID = definitionID; this.worker = worker; worker.RegisterDefinition(definitionID, definition, onData); } public ValueTask DisposeAsync() { worker.UnregisterDefinition(definitionID); return default; } } private class SimConnectSystemEventRegistration : IAsyncDisposable { private readonly uint eventID; private readonly SimConnectWorker worker; public SimConnectSystemEventRegistration(uint eventID, SimConnectSystemEvent systemEvent, Action onData, SimConnectWorker worker) { this.eventID = eventID; this.worker = worker; worker.SubscribeToSystemEvent(eventID, systemEvent, onData); } public ValueTask DisposeAsync() { worker.UnsubscribeFromSystemEvent(eventID); return default; } } private class SimConnectWorker { private readonly ISimConnectLibrary simConnectLibrary; private readonly string appName; private Task workerTask; private readonly AutoResetEvent workerPulse = new AutoResetEvent(false); private readonly object workerLock = new object(); private volatile bool closed; private readonly Queue> workQueue = new Queue>(); private readonly TaskCompletionSource openResult = new TaskCompletionSource(); private readonly ConcurrentDictionary> definitionDataHandler = new ConcurrentDictionary>(); private readonly ConcurrentDictionary> eventDataHandler = new ConcurrentDictionary>(); public SimConnectWorker(ISimConnectLibrary simConnectLibrary, string appName) { this.simConnectLibrary = simConnectLibrary; this.appName = appName; } public async Task Open() { if (workerTask == null) workerTask = Task.Run(RunInBackground); return await openResult.Task; } public void Close() { closed = true; workerPulse.Set(); } public void RegisterDefinition(uint definitionID, SimConnectDefinition definition, Action onData) { Enqueue(hSimConnect => { foreach (var variable in definition.Variables) { simConnectLibrary.SimConnect_AddToDataDefinition(hSimConnect, definitionID, variable.VariableName, variable.UnitsName, variable.DataType, variable.Epsilon); } definitionDataHandler.AddOrUpdate(definitionID, onData, (key, value) => onData); simConnectLibrary.SimConnect_RequestDataOnSimObject(hSimConnect, definitionID, definitionID, 0, SimConnectPeriod.SimFrame, 1); }); } public void UnregisterDefinition(uint definitionID) { Enqueue(hSimConnect => { definitionDataHandler.TryRemove(definitionID, out var unused); simConnectLibrary.SimConnect_ClearDataDefinition(hSimConnect, definitionID); }); } public void SubscribeToSystemEvent(uint eventID, SimConnectSystemEvent systemEvent, Action onData) { Enqueue(hSimConnect => { eventDataHandler.AddOrUpdate(eventID, onData, (key, value) => onData); var result = simConnectLibrary.SimConnect_SubscribeToSystemEvent(hSimConnect, eventID, systemEvent.ToString()); if (result == 0) return; }); } public void UnsubscribeFromSystemEvent(uint eventID) { Enqueue(hSimConnect => { eventDataHandler.TryRemove(eventID, out var unused); simConnectLibrary.SimConnect_UnsubscribeFromSystemEvent(hSimConnect, eventID); }); } private void Enqueue(Action work) { lock(workerLock) { workQueue.Enqueue(work); } workerPulse.Set(); } private void RunInBackground() { var dataEvent = new EventWaitHandle(false, EventResetMode.AutoReset); var errorCode = simConnectLibrary.SimConnect_Open(out var simConnectHandle, appName, IntPtr.Zero, 0, dataEvent.SafeWaitHandle.DangerousGetHandle(), 0); openResult.TrySetResult(errorCode == 0); if (errorCode != 0) return; while(!closed) { switch (WaitHandle.WaitAny(new WaitHandle[] { workerPulse, dataEvent })) { case 0: HandleWorkQueue(simConnectHandle); break; case 1: HandleData(ref simConnectHandle); break; } } if (simConnectHandle != IntPtr.Zero) simConnectLibrary.SimConnect_Close(simConnectHandle); } private void HandleWorkQueue(IntPtr simConnectHandle) { while (!closed) { Action work; lock(workerLock) { work = workQueue.Count > 0 ? workQueue.Dequeue() : null; } if (work == null) break; work(simConnectHandle); } } private void HandleData(ref IntPtr simConnectHandle) { while (!closed && simConnectLibrary.SimConnect_GetNextDispatch(simConnectHandle, out var dataPtr, out var dataSize) == 0) { var recv = Marshal.PtrToStructure(dataPtr); // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault switch ((SimConnectRecvID) recv.dwID) { case SimConnectRecvID.Exception: var recvException = Marshal.PtrToStructure(dataPtr); // TODO provide a way to get insight into exceptions if (recvException.dwException == 0) break; break; case SimConnectRecvID.Event: var recvEvent = Marshal.PtrToStructure(dataPtr); if (!eventDataHandler.TryGetValue(recvEvent.uEventID, out var eventHandler)) break; eventHandler(recvEvent); break; case SimConnectRecvID.SimobjectData: case SimConnectRecvID.SimobjectDataByType: var recvSimobjectData = Marshal.PtrToStructure(dataPtr); if (!definitionDataHandler.TryGetValue(recvSimobjectData.dwDefineID, out var dataHandler)) break; unsafe { var streamOffset = Marshal.OffsetOf("dwData").ToInt32(); var stream = new UnmanagedMemoryStream((byte*)IntPtr.Add(dataPtr, streamOffset).ToPointer(), dataSize - streamOffset); dataHandler(stream); } break; case SimConnectRecvID.Quit: simConnectHandle = IntPtr.Zero; closed = true; break; } } } } } }