using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using Microsoft.Win32.SafeHandles; 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, IDisposable { private readonly ISimConnectLibrary simConnectLibrary; private SimConnectWorker worker; private uint nextDefinitionID = 1; /// /// Creates an instance of DefaultSimConnectClient. /// /// The low-level SimConnect library interface to use public DefaultSimConnectClient(ISimConnectLibrary simConnectLibrary) { this.simConnectLibrary = simConnectLibrary; } /// public void Dispose() { worker?.Close().Wait(); simConnectLibrary?.Dispose(); } /// /// Attempts to open a connection to the SimConnect server /// /// The application name passed to the SimConnect server. /// public async Task TryOpen(string appName) { if (worker != null) await worker.Close(); worker = new SimConnectWorker(simConnectLibrary, appName); return await worker.Open(); } /// public void AttachObserver(ISimConnectClientObserver observer) { throw new NotImplementedException(); } /// public IDisposable 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; } private class SimConnectDefinitionRegistration : IDisposable 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 void Dispose() { worker.UnregisterDefinition(definitionID); } } 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>(); 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 async Task Close() { closed = true; workerPulse.Set(); await workerTask; } 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); }); } 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); if (recvException.dwException == 0) break; break; case SimConnectRecvID.SimobjectData: case SimConnectRecvID.SimobjectDataByType: var recvSimobjectData = Marshal.PtrToStructure(dataPtr); if (!definitionDataHandler.TryGetValue((uint)recvSimobjectData.dwDefineID, out var dataHandler)) break; unsafe { var streamOffset = Marshal.OffsetOf("dwData").ToInt32(); var stream = new UnmanagedMemoryStream((byte*)IntPtr.Add(dataPtr, streamOffset).ToPointer(), (long)dataSize - streamOffset); dataHandler(stream); } break; case SimConnectRecvID.Quit: simConnectHandle = IntPtr.Zero; closed = true; break; } } } } } }