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;
}
}
}
}
}
}