In my first post about SimConnect, I showed how to create a simple C# console app that connected to the SimConnect server running within Microsoft Flight Simulator 2020, using the managed SimConnect library. Now let's talk about how to actually interact with the simulator in a (somewhat) useful fashion.
I have a GitHub repository with all the code from this series in it, tagged at each stage. If you want to start out with the code as it was at the end of the first post, use the tag "03-add-a-pause".
SimConnect client/server communication is primarily through a set of defined data structures called messages. Clients receive these messages periodically from the server and either ignore the message, or take action, based on the kind of message received. In this model, the simulation engine itself is a client of the SimConnect server, so in order to control the simulation, we send messages from our client to the server, which the simulation engine then receives as a client. To start with, let's look at how we can receive messages from SimConnect.
Managed library message events
In the native (C++) environment, all messages initially look the same to a SimConnect client. Based on an ID value in the message, the message can be cast to a more specialized message structure containing data specific to that type of message, if necessary, and then processed by the client code in some way.
The managed SimConnect library layers a .NET-style event-based model over the native message flow. Each message type has a corresponding SimConnect event, and managed clients attach event handlers to the events in which they are interested.
You can see the various events available via IntelliSense in Visual Studio. They all start with OnRecv.
I think that the SimConnect.OnRecvOpen event is a good place to start. This event is triggered when the SIMCONNECT_RECV_OPEN message is received, which in turn happens when a call to the SimConnect_Open() function succeeds in the native library. That function is:
HANDLE* phSimConnect,
LPCSTR szName,
HWND hWnd,
DWORD UserEventWin32,
HANDLE hEventHandle,
DWORD ConfigIndex
);
If those parameters look familiar, recall the SimConnect object constructor from the last post:
That's right: as I mentioned briefly in the previous post, when you instantiate a SimConnect object, the required constructor arguments are used (along with a handle argument) to call SimConnect_Open() behind the scenes. That's why the constructor returns an error if you instantiate the object while the sim is not running. If you compare the documented return values from SimConnect_Open() with the error message we received in that scenario, you can see the "E_FAIL" HRESULT reported in the COMException message. The SimConnect object also automatically calls the SimConnect_Close() function when the object is destroyed.
In order to capture the SIMCONNECT_RECV_OPEN message, we need to create a delegate to handle the message. Visual Studio can create this for you on the fly, but let's just go ahead and create it ourselves.
private static void OnSimConnectOpen(SimConnect sender, SIMCONNECT_RECV_OPEN data)
{
Console.WriteLine($"SimConnect connection to application '{data.szApplicationName}' opened.");
}
The SIMCONNECT_RECV_OPEN message contains information on the version of the sim, as well as the version of SimConnect, but we can ignore that for now, and just use the name of the flight sim application.
To get the SimConnect server to send any messages that it has our way, we need to call a method on the SimConnect object called ReceiveMessage(). It does not actually give us the message (hence the void return value), it just tells the object to get any messages and turn them into the appropriate events behind the scenes. Let's add the code for attaching the event handler to the event and calling ReceiveMessage() between where we create the object and where we prompt the user to quit the program.
catch (Exception ex)
{
Console.WriteLine($"An unexpected exception occurred. Exception: {ex.Message}");
}
if (simConnect != null)
{
simConnect.OnRecvOpen += OnSimConnectOpen;
}
simConnect?.ReceiveMessage();
Console.WriteLine("Press any key to exit the program.");
Start Flight Simulator, and then run the program.
Huh. No new message. Why?
Well, this is what happens when you use synchronous code on an asynchronous process. Even though the SimConnect object was created (or else we would have gotten an error message), we then
- add the event handler
- call ReceiveMessage()
- move on to the "press any key" message
all before the message is available. You can see this by adding another pause before ReceiveMessage():
Run the program, wait a couple of seconds before pressing a key, and now we get the expected result:
As a lifelong North Carolinian, I'm glad to get a reminder of my state's contribution to the history of aviation every time I see the name of the MSFS application!
Of course, we don't want to ask the to user press a key every time they want to update our application on what the sim is doing. The best approach for a real application is probably to use asynchronous code, to match the asynchronous nature of the problem, but that's more complication than we need in this relatively simple demonstration app. Instead, let's add a boolean flag variable isSimConnectOpen, and set it to true in the OnSimConnectOpen() event handler.
Now we can just call ReceiveMessage() until the event handler sets the flag, then move on. At this point, we should enclose all of the logic that depends on successful creation of the SimConnect object inside the existing null check, so the program doesn't hang after an error waiting for a flag value to change that never will. That means we can remove the "?" from our call to simConnect.ReceiveMessage().
The OnRecvOpen event has a matching event that is called when you quit MSFS, called (of course) OnRecvQuit. The SIMCONNECT_RECV_QUIT message doesn't hold extra information like a SIMCONNECT_RECV_OPEN message does, and in fact the managed library just treats it as a generic SIMCONNECT_RECV message; it just tells you that the user closed the program. But it's still useful information. We can use this event to take the appropriate action in our client applications, such as exiting, or maybe entering some sort of wait state to see if the user launches MSFS again. In our case, let's just add a handler that lets the user know that the quit message was received, and sets the isSimConnectOpen variable back to false. In the Main() method, we'll wait for the user to press a key to exit as before. We'll need to add a second loop to call ReceiveMessage(), this time looping until the flag is false, rather than true.
(Yes, the two ReceiveMessage() loops look weird one after the other, but that's the nature of these toy demo programs.)
Click the quit button in the sim, and then we should see this:
Success!
Well, this is a little bit better than where we ended up last time, but it still isn't much. I have added another post after this one that illustrates using system events to get more information from the simulator. I'm hoping to add on to this series even further, looking at how to use client events and simulation variables to illustrate interacting with the simulator in a more useful way. So keep an eye out!