From a83f8eb65314ce04cff8cb2bd1bdbc1fb4e7fac4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eren=20=C3=96zdil?= Date: Mon, 29 Dec 2025 04:54:35 +0100 Subject: [PATCH 1/2] Add SimConnect_SubscribeToSystemEvent handling (#15) --- .../Events/SimSystemEventReceivedEventArgs.cs | 28 ++++++++ src/SimConnect.NET/SimConnectClient.cs | 66 ++++++++++++++++- .../Tests/SystemEventSubscriptionTests.cs | 70 +++++++++++++++++++ .../Tests/TestRunner.cs | 4 ++ 4 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 src/SimConnect.NET/Events/SimSystemEventReceivedEventArgs.cs create mode 100644 tests/SimConnect.NET.Tests.Net8/Tests/SystemEventSubscriptionTests.cs diff --git a/src/SimConnect.NET/Events/SimSystemEventReceivedEventArgs.cs b/src/SimConnect.NET/Events/SimSystemEventReceivedEventArgs.cs new file mode 100644 index 0000000..1b92f3d --- /dev/null +++ b/src/SimConnect.NET/Events/SimSystemEventReceivedEventArgs.cs @@ -0,0 +1,28 @@ +// +// Copyright (c) BARS. All rights reserved. +// + +namespace SimConnect.NET.Events +{ + /// + /// Provides data for an event that is raised when a Simconnect system event is raised. + /// + /// + /// Initializes a new instance of the class with the specified event identifier and. + /// associated data. + /// + /// The unique identifier for the system event. + /// The data associated with the system event. + public class SimSystemEventReceivedEventArgs(uint eventId, uint data) : EventArgs + { + /// + /// Gets the unique identifier for the event. + /// + public uint EventId { get; } = eventId; + + /// + /// Gets the data associated with the event. + /// + public uint Data { get; } = data; + } +} diff --git a/src/SimConnect.NET/SimConnectClient.cs b/src/SimConnect.NET/SimConnectClient.cs index c5b6d3f..2516dc6 100644 --- a/src/SimConnect.NET/SimConnectClient.cs +++ b/src/SimConnect.NET/SimConnectClient.cs @@ -2,7 +2,6 @@ // Copyright (c) BARS. All rights reserved. // -using System; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; @@ -70,6 +69,11 @@ public SimConnectClient(string applicationName = "SimConnect.NET Client") /// public event EventHandler? RawMessageReceived; + /// + /// Occurs when a subscribed event is fired. + /// + public event EventHandler? SystemEventReceived; + /// /// Gets a value indicating whether the client is connected to SimConnect. /// @@ -345,6 +349,40 @@ public async Task DisconnectAsync() } } + /// + /// Subscribes to a specific simulator system event. + /// + /// The name of the system event (e.g., "SimStart", "4Sec", "Crashed"). + /// A user-defined ID to identify this subscription. + /// Cancellation token for the operation. + /// A task representing the event. + /// Thrown when a sim connection wasn't found. + /// Thrown when the event wasn't subscribed. + public async Task SubscribeToEventAsync(string systemEventName, uint systemEventId, CancellationToken cancellationToken = default) + { + ObjectDisposedException.ThrowIf(this.disposed, nameof(SimConnectClient)); + + if (!this.isConnected) + { + throw new InvalidOperationException("Not connected to SimConnect."); + } + + await Task.Run( + () => + { + var result = SimConnectNative.SimConnect_SubscribeToSystemEvent( + this.simConnectHandle, + systemEventId, + systemEventName); + + if (result != (int)SimConnectError.None) + { + throw new SimConnectException($"Failed to subscribe to event {systemEventName}: {(SimConnectError)result}", (SimConnectError)result); + } + }, + cancellationToken).ConfigureAwait(false); + } + /// /// Processes the next SimConnect message. /// @@ -419,6 +457,9 @@ public async Task ProcessNextMessageAsync(CancellationToken cancellationTo case SimConnectRecvId.VorList: case SimConnectRecvId.NdbList: break; + case SimConnectRecvId.Event: + this.ProcessSystemEvent(ppData); + break; default: this.simVarManager?.ProcessReceivedData(ppData, pcbData); break; @@ -543,6 +584,29 @@ private void ProcessOpen(IntPtr ppData) } } + /// + /// Processes a system event message from SimConnect. + /// + /// Pointer to the received Event data. + private void ProcessSystemEvent(IntPtr ppData) + { + try + { + var recvEvent = Marshal.PtrToStructure(ppData); + + this.SystemEventReceived?.Invoke(this, new SimSystemEventReceivedEventArgs(recvEvent.EventId, recvEvent.Data)); + + if (SimConnectLogger.IsLevelEnabled(SimConnectLogger.LogLevel.Debug)) + { + SimConnectLogger.Debug($"System Event Received: ID={recvEvent.EventId} Data={recvEvent.Data}"); + } + } + catch (Exception ex) when (!ExceptionHelper.IsCritical(ex)) + { + SimConnectLogger.Error("Error processing system event", ex); + } + } + /// /// Starts the background message processing loop. /// diff --git a/tests/SimConnect.NET.Tests.Net8/Tests/SystemEventSubscriptionTests.cs b/tests/SimConnect.NET.Tests.Net8/Tests/SystemEventSubscriptionTests.cs new file mode 100644 index 0000000..f124d37 --- /dev/null +++ b/tests/SimConnect.NET.Tests.Net8/Tests/SystemEventSubscriptionTests.cs @@ -0,0 +1,70 @@ +// +// Copyright (c) BARS. All rights reserved. +// + +namespace SimConnect.NET.Tests.Net8.Tests +{ + internal class SystemEventSubscriptionTests : ISimConnectTest + { + public string Name => "SystemEventSubscription"; + + public string Description => "Tests system event subscription"; + + public string Category => "System Event"; + + public async Task RunAsync(SimConnectClient client, CancellationToken cancellationToken = default) + { + try + { + if (!client.IsConnected) + { + Console.WriteLine(" ❌ Client should already be connected"); + return false; + } + + Console.WriteLine(" ✅ Connection status verified"); + + using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + cts.CancelAfter(TimeSpan.FromSeconds(15)); + + bool testEventReceived = false; + client.SystemEventReceived += (sender, e) => + { + switch (e.EventId) + { + case 100: + Console.WriteLine("4 seconds has passed!"); + testEventReceived = true; + break; + } + }; + + await client.SubscribeToEventAsync("4sec", 100, cts.Token); + + Console.WriteLine("Listening for events..."); + + while (!testEventReceived && !cts.Token.IsCancellationRequested) + { + await Task.Delay(500, cts.Token); + } + if (!testEventReceived) + { + Console.WriteLine(" ❌ Did not receive expected system event"); + return false; + } + Console.WriteLine(" ✅ Received expected system event"); + return true; + } + catch (OperationCanceledException) + { + Console.WriteLine(" ❌ Connection test timed out"); + return false; + } + catch (Exception ex) + { + Console.WriteLine($" ❌ Connection test failed: {ex.Message}"); + return false; + } + } + } +} diff --git a/tests/SimConnect.NET.Tests.Net8/Tests/TestRunner.cs b/tests/SimConnect.NET.Tests.Net8/Tests/TestRunner.cs index 7f2c83d..512f3aa 100644 --- a/tests/SimConnect.NET.Tests.Net8/Tests/TestRunner.cs +++ b/tests/SimConnect.NET.Tests.Net8/Tests/TestRunner.cs @@ -31,6 +31,7 @@ public TestRunner() new InputEventTests(), new InputEventValueTests(), new PerformanceTests(), + new SystemEventSubscriptionTests(), }; } @@ -143,6 +144,9 @@ private static TestOptions ParseArguments(string[] args) case "--verbose": options.Verbose = true; break; + case "--test-events": + options.Categories.Add("System Event"); + break; } } From fe8df16e02b3d8f299c98723a36913d99669aeae Mon Sep 17 00:00:00 2001 From: AussieScorcher Date: Mon, 29 Dec 2025 15:12:34 +0800 Subject: [PATCH 2/2] feat: add system event subscription and management features in version 0.1.18 - Implemented subscription to simulator system events via `SimConnectClient.SubscribeToEventAsync`. - Added typed event handlers for frame, filename, object add/remove, and extended EX1 system events. - Introduced helper methods for managing event states: `SetSystemEventStateAsync` and `UnsubscribeFromEventAsync`. - Enhanced performance by optimizing message processing and memory allocation in SimVar setters. - Updated bundled `SimConnect.dll` to the latest SDK version. - Added tests for system event subscription and state management. --- CHANGELOG.md | 15 + Directory.Build.props | 2 +- .../SimSystemEventEx1ReceivedEventArgs.cs | 50 +++ ...SimSystemEventFilenameReceivedEventArgs.cs | 26 ++ .../Events/SimSystemEventFrameEventArgs.cs | 26 ++ .../SimSystemEventObjectAddRemoveEventArgs.cs | 21 ++ .../Events/SimSystemEventReceivedEventArgs.cs | 2 +- src/SimConnect.NET/SimConnectClient.cs | 323 ++++++++++++++---- src/SimConnect.NET/SimVar/SimVarManager.cs | 172 ++++++---- src/SimConnect.NET/lib/SimConnect.dll | Bin 78848 -> 79360 bytes .../Tests/SystemEventStateTests.cs | 144 ++++++++ .../Tests/SystemEventSubscriptionTests.cs | 35 +- .../Tests/TestRunner.cs | 1 + 13 files changed, 669 insertions(+), 148 deletions(-) create mode 100644 src/SimConnect.NET/Events/SimSystemEventEx1ReceivedEventArgs.cs create mode 100644 src/SimConnect.NET/Events/SimSystemEventFilenameReceivedEventArgs.cs create mode 100644 src/SimConnect.NET/Events/SimSystemEventFrameEventArgs.cs create mode 100644 src/SimConnect.NET/Events/SimSystemEventObjectAddRemoveEventArgs.cs create mode 100644 tests/SimConnect.NET.Tests.Net8/Tests/SystemEventStateTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e78ed7..472c105 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.1.18] - 2025-12-29 + +### Added + +- Subscribe to simulator system events via `SimConnectClient.SubscribeToEventAsync` and handle notifications through `SystemEventReceived`, enabling callbacks for SimConnect system events like `4sec` and pause state changes. +- Typed system event dispatch and events: `FrameEventReceived`, `FilenameEventReceived`, `ObjectAddRemoveEventReceived`, and `SystemEventEx1Received` now surface frame-rate, filename, object add/remove, and EX1 payloads instead of dropping them. +- Helpers to control and clean up subscriptions: `SetSystemEventStateAsync` and `UnsubscribeFromEventAsync` wrap the native APIs for toggling and stopping system event notifications. +- Test runner now includes a system-event subscription test and a state toggle test to exercise the full subscribe/on/off/unsubscribe flow. +- Bundled `SimConnect.dll` updated to the latest SDK build to align with current simulator versions. + +### Performance + +- Message processing loop no longer spins up a worker task per dispatch; `SimConnect_GetNextDispatch` is polled synchronously to reduce context switches and lower idle CPU. +- SimVar setters use pooled pinned buffers instead of per-call unmanaged allocations and extra `Task.Run` hops, and request timeouts now rely on linked cancellation sources instead of `Task.Delay`, cutting allocations on hot paths. + ## [0.1.17] - 2025-11-14 ### Added diff --git a/Directory.Build.props b/Directory.Build.props index 2b04d21..5193bb4 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 0.1.17 + 0.1.18 BARS BARS SimConnect.NET diff --git a/src/SimConnect.NET/Events/SimSystemEventEx1ReceivedEventArgs.cs b/src/SimConnect.NET/Events/SimSystemEventEx1ReceivedEventArgs.cs new file mode 100644 index 0000000..40a490f --- /dev/null +++ b/src/SimConnect.NET/Events/SimSystemEventEx1ReceivedEventArgs.cs @@ -0,0 +1,50 @@ +// +// Copyright (c) BARS. All rights reserved. +// + +using System; + +namespace SimConnect.NET.Events +{ + /// + /// Provides data for extended EX1 system events that carry multiple data parameters. + /// + /// The identifier of the event. + /// First data parameter. + /// Second data parameter. + /// Third data parameter. + /// Fourth data parameter. + /// Fifth data parameter. + public class SimSystemEventEx1ReceivedEventArgs(uint eventId, uint data0, uint data1, uint data2, uint data3, uint data4) : EventArgs + { + /// + /// Gets the event identifier. + /// + public uint EventId { get; } = eventId; + + /// + /// Gets the first data parameter. + /// + public uint Data0 { get; } = data0; + + /// + /// Gets the second data parameter. + /// + public uint Data1 { get; } = data1; + + /// + /// Gets the third data parameter. + /// + public uint Data2 { get; } = data2; + + /// + /// Gets the fourth data parameter. + /// + public uint Data3 { get; } = data3; + + /// + /// Gets the fifth data parameter. + /// + public uint Data4 { get; } = data4; + } +} diff --git a/src/SimConnect.NET/Events/SimSystemEventFilenameReceivedEventArgs.cs b/src/SimConnect.NET/Events/SimSystemEventFilenameReceivedEventArgs.cs new file mode 100644 index 0000000..4510646 --- /dev/null +++ b/src/SimConnect.NET/Events/SimSystemEventFilenameReceivedEventArgs.cs @@ -0,0 +1,26 @@ +// +// Copyright (c) BARS. All rights reserved. +// + +using System; + +namespace SimConnect.NET.Events +{ + /// + /// Provides data for filename-based system events such as flight load/save notifications. + /// + /// The filename reported by the simulator. + /// Optional flags returned by the simulator. + public class SimSystemEventFilenameReceivedEventArgs(string fileName, uint flags) : EventArgs + { + /// + /// Gets the filename reported by the simulator. + /// + public string FileName { get; } = fileName; + + /// + /// Gets the flags returned alongside the filename. + /// + public uint Flags { get; } = flags; + } +} diff --git a/src/SimConnect.NET/Events/SimSystemEventFrameEventArgs.cs b/src/SimConnect.NET/Events/SimSystemEventFrameEventArgs.cs new file mode 100644 index 0000000..9377db8 --- /dev/null +++ b/src/SimConnect.NET/Events/SimSystemEventFrameEventArgs.cs @@ -0,0 +1,26 @@ +// +// Copyright (c) BARS. All rights reserved. +// + +using System; + +namespace SimConnect.NET.Events +{ + /// + /// Provides data for a frame-based system event that includes frame rate and simulation speed. + /// + /// The reported frame rate in frames per second. + /// The reported simulation speed multiplier. + public class SimSystemEventFrameEventArgs(float frameRate, float simulationSpeed) : EventArgs + { + /// + /// Gets the reported frame rate in frames per second. + /// + public float FrameRate { get; } = frameRate; + + /// + /// Gets the reported simulation speed multiplier. + /// + public float SimulationSpeed { get; } = simulationSpeed; + } +} diff --git a/src/SimConnect.NET/Events/SimSystemEventObjectAddRemoveEventArgs.cs b/src/SimConnect.NET/Events/SimSystemEventObjectAddRemoveEventArgs.cs new file mode 100644 index 0000000..59cf3cb --- /dev/null +++ b/src/SimConnect.NET/Events/SimSystemEventObjectAddRemoveEventArgs.cs @@ -0,0 +1,21 @@ +// +// Copyright (c) BARS. All rights reserved. +// + +using System; +using SimConnect.NET; + +namespace SimConnect.NET.Events +{ + /// + /// Provides data for system events that report AI object creation or removal. + /// + /// The type of the object that was added or removed. + public class SimSystemEventObjectAddRemoveEventArgs(SimConnectSimObjectType objectType) : EventArgs + { + /// + /// Gets the type of the object that was added or removed. + /// + public SimConnectSimObjectType ObjectType { get; } = objectType; + } +} diff --git a/src/SimConnect.NET/Events/SimSystemEventReceivedEventArgs.cs b/src/SimConnect.NET/Events/SimSystemEventReceivedEventArgs.cs index 1b92f3d..ecac1f8 100644 --- a/src/SimConnect.NET/Events/SimSystemEventReceivedEventArgs.cs +++ b/src/SimConnect.NET/Events/SimSystemEventReceivedEventArgs.cs @@ -8,7 +8,7 @@ namespace SimConnect.NET.Events /// Provides data for an event that is raised when a Simconnect system event is raised. /// /// - /// Initializes a new instance of the class with the specified event identifier and. + /// Initializes a new instance of the class with the specified event identifier and /// associated data. /// /// The unique identifier for the system event. diff --git a/src/SimConnect.NET/SimConnectClient.cs b/src/SimConnect.NET/SimConnectClient.cs index 2516dc6..cba6392 100644 --- a/src/SimConnect.NET/SimConnectClient.cs +++ b/src/SimConnect.NET/SimConnectClient.cs @@ -69,6 +69,26 @@ public SimConnectClient(string applicationName = "SimConnect.NET Client") /// public event EventHandler? RawMessageReceived; + /// + /// Occurs when a typed frame system event is received (frame rate and sim speed). + /// + public event EventHandler? FrameEventReceived; + + /// + /// Occurs when a typed filename-based system event is received (for example FlightLoaded or FlightSaved). + /// + public event EventHandler? FilenameEventReceived; + + /// + /// Occurs when an object add/remove system event is received. + /// + public event EventHandler? ObjectAddRemoveEventReceived; + + /// + /// Occurs when an extended EX1 system event is received with additional data payload. + /// + public event EventHandler? SystemEventEx1Received; + /// /// Occurs when a subscribed event is fired. /// @@ -355,7 +375,7 @@ public async Task DisconnectAsync() /// The name of the system event (e.g., "SimStart", "4Sec", "Crashed"). /// A user-defined ID to identify this subscription. /// Cancellation token for the operation. - /// A task representing the event. + /// A task representing the subscription operation. /// Thrown when a sim connection wasn't found. /// Thrown when the event wasn't subscribed. public async Task SubscribeToEventAsync(string systemEventName, uint systemEventId, CancellationToken cancellationToken = default) @@ -384,12 +404,15 @@ await Task.Run( } /// - /// Processes the next SimConnect message. + /// Sets the reporting state for a previously subscribed system event. /// + /// The user-defined ID of the system event. + /// The desired reporting state. /// Cancellation token for the operation. - /// A task that represents the asynchronous message processing operation, returning true if a message was processed. - /// Thrown when message processing fails. - public async Task ProcessNextMessageAsync(CancellationToken cancellationToken = default) + /// A task representing the state change operation. + /// Thrown when a sim connection was not found. + /// Thrown when the state change fails. + public async Task SetSystemEventStateAsync(uint systemEventId, SimConnectState state, CancellationToken cancellationToken = default) { ObjectDisposedException.ThrowIf(this.disposed, nameof(SimConnectClient)); @@ -398,81 +421,151 @@ public async Task ProcessNextMessageAsync(CancellationToken cancellationTo throw new InvalidOperationException("Not connected to SimConnect."); } - return await Task.Run( + await Task.Run( () => { - cancellationToken.ThrowIfCancellationRequested(); - var result = SimConnectNative.SimConnect_GetNextDispatch(this.simConnectHandle, out var ppData, out var pcbData); + var result = SimConnectNative.SimConnect_SetSystemEventState( + this.simConnectHandle, + systemEventId, + (uint)state); if (result != (int)SimConnectError.None) { - // Filter out the common "no messages available" error to reduce log spam - if (result != -2147467259 && SimConnectLogger.IsLevelEnabled(SimConnectLogger.LogLevel.Debug)) - { - SimConnectLogger.Debug($"SimConnect_GetNextDispatch returned: {(SimConnectError)result}"); - } - - return false; + throw new SimConnectException($"Failed to set system event state for {systemEventId}: {(SimConnectError)result}", (SimConnectError)result); } + }, + cancellationToken).ConfigureAwait(false); + } - if (ppData != IntPtr.Zero && pcbData > 0) - { - var recv = Marshal.PtrToStructure(ppData); - var recvId = (SimConnectRecvId)recv.Id; - - if (SimConnectLogger.IsLevelEnabled(SimConnectLogger.LogLevel.Debug)) - { - SimConnectLogger.Debug($"Received SimConnect message: Id={recv.Id}, Size={recv.Size}"); - } + /// + /// Unsubscribes from a previously subscribed system event. + /// + /// The user-defined ID of the system event. + /// Cancellation token for the operation. + /// A task representing the unsubscribe operation. + /// Thrown when a sim connection was not found. + /// Thrown when the unsubscribe fails. + public async Task UnsubscribeFromEventAsync(uint systemEventId, CancellationToken cancellationToken = default) + { + ObjectDisposedException.ThrowIf(this.disposed, nameof(SimConnectClient)); - try - { - this.RawMessageReceived?.Invoke(this, new RawSimConnectMessageEventArgs(ppData, pcbData, recvId)); - } - catch (Exception hookEx) when (!ExceptionHelper.IsCritical(hookEx)) - { - SimConnectLogger.Warning($"RawMessageReceived hook threw: {hookEx.Message}"); - } + if (!this.isConnected) + { + throw new InvalidOperationException("Not connected to SimConnect."); + } - switch (recvId) - { - case SimConnectRecvId.AssignedObjectId: - this.ProcessAssignedObjectId(ppData); - break; - case SimConnectRecvId.Exception: - this.ProcessError(ppData); - break; - case SimConnectRecvId.Open: - this.ProcessOpen(ppData); - break; - case SimConnectRecvId.ControllersList: - case SimConnectRecvId.ActionCallback: - case SimConnectRecvId.EnumerateInputEvents: - case SimConnectRecvId.EnumerateInputEventParams: - case SimConnectRecvId.GetInputEvent: - case SimConnectRecvId.SubscribeInputEvent: - this.inputEventManager?.ProcessReceivedData(ppData, pcbData); - break; - case SimConnectRecvId.AirportList: - case SimConnectRecvId.VorList: - case SimConnectRecvId.NdbList: - break; - case SimConnectRecvId.Event: - this.ProcessSystemEvent(ppData); - break; - default: - this.simVarManager?.ProcessReceivedData(ppData, pcbData); - break; - } + await Task.Run( + () => + { + var result = SimConnectNative.SimConnect_UnsubscribeFromSystemEvent( + this.simConnectHandle, + systemEventId); - return true; + if (result != (int)SimConnectError.None) + { + throw new SimConnectException($"Failed to unsubscribe from system event {systemEventId}: {(SimConnectError)result}", (SimConnectError)result); } - - return false; }, cancellationToken).ConfigureAwait(false); } + /// + /// Processes the next SimConnect message. + /// + /// Cancellation token for the operation. + /// A task that represents the asynchronous message processing operation, returning true if a message was processed. + /// Thrown when message processing fails. + public Task ProcessNextMessageAsync(CancellationToken cancellationToken = default) + { + ObjectDisposedException.ThrowIf(this.disposed, nameof(SimConnectClient)); + + if (!this.isConnected) + { + throw new InvalidOperationException("Not connected to SimConnect."); + } + + cancellationToken.ThrowIfCancellationRequested(); + var result = SimConnectNative.SimConnect_GetNextDispatch(this.simConnectHandle, out var ppData, out var pcbData); + + if (result != (int)SimConnectError.None) + { + // Filter out the common "no messages available" error to reduce log spam + if (result != -2147467259 && SimConnectLogger.IsLevelEnabled(SimConnectLogger.LogLevel.Debug)) + { + SimConnectLogger.Debug($"SimConnect_GetNextDispatch returned: {(SimConnectError)result}"); + } + + return Task.FromResult(false); + } + + if (ppData != IntPtr.Zero && pcbData > 0) + { + var recv = Marshal.PtrToStructure(ppData); + var recvId = (SimConnectRecvId)recv.Id; + + if (SimConnectLogger.IsLevelEnabled(SimConnectLogger.LogLevel.Debug)) + { + SimConnectLogger.Debug($"Received SimConnect message: Id={recv.Id}, Size={recv.Size}"); + } + + try + { + this.RawMessageReceived?.Invoke(this, new RawSimConnectMessageEventArgs(ppData, pcbData, recvId)); + } + catch (Exception hookEx) when (!ExceptionHelper.IsCritical(hookEx)) + { + SimConnectLogger.Warning($"RawMessageReceived hook threw: {hookEx.Message}"); + } + + switch (recvId) + { + case SimConnectRecvId.AssignedObjectId: + this.ProcessAssignedObjectId(ppData); + break; + case SimConnectRecvId.Exception: + this.ProcessError(ppData); + break; + case SimConnectRecvId.Open: + this.ProcessOpen(ppData); + break; + case SimConnectRecvId.ControllersList: + case SimConnectRecvId.ActionCallback: + case SimConnectRecvId.EnumerateInputEvents: + case SimConnectRecvId.EnumerateInputEventParams: + case SimConnectRecvId.GetInputEvent: + case SimConnectRecvId.SubscribeInputEvent: + this.inputEventManager?.ProcessReceivedData(ppData, pcbData); + break; + case SimConnectRecvId.AirportList: + case SimConnectRecvId.VorList: + case SimConnectRecvId.NdbList: + break; + case SimConnectRecvId.Event: + this.ProcessSystemEvent(ppData); + break; + case SimConnectRecvId.EventFrame: + this.ProcessSystemEventFrame(ppData); + break; + case SimConnectRecvId.EventFilename: + this.ProcessSystemEventFilename(ppData); + break; + case SimConnectRecvId.EventObjectAddRemove: + this.ProcessSystemEventObjectAddRemove(ppData); + break; + case SimConnectRecvId.EventEx1: + this.ProcessSystemEventEx1(ppData); + break; + default: + this.simVarManager?.ProcessReceivedData(ppData, pcbData); + break; + } + + return Task.FromResult(true); + } + + return Task.FromResult(false); + } + /// /// Tests the connection to SimConnect by performing a simple operation. /// @@ -607,6 +700,106 @@ private void ProcessSystemEvent(IntPtr ppData) } } + /// + /// Processes a system frame event message from SimConnect. + /// + /// Pointer to the received EventFrame data. + private void ProcessSystemEventFrame(IntPtr ppData) + { + try + { + var recvEventFrame = Marshal.PtrToStructure(ppData); + + this.FrameEventReceived?.Invoke(this, new SimSystemEventFrameEventArgs(recvEventFrame.FrameRate, recvEventFrame.SimSpeed)); + + if (SimConnectLogger.IsLevelEnabled(SimConnectLogger.LogLevel.Debug)) + { + SimConnectLogger.Debug($"Frame Event Received: FrameRate={recvEventFrame.FrameRate} SimSpeed={recvEventFrame.SimSpeed}"); + } + } + catch (Exception ex) when (!ExceptionHelper.IsCritical(ex)) + { + SimConnectLogger.Error("Error processing frame system event", ex); + } + } + + /// + /// Processes a system filename event message from SimConnect. + /// + /// Pointer to the received EventFilename data. + private void ProcessSystemEventFilename(IntPtr ppData) + { + try + { + var recvEventFilename = Marshal.PtrToStructure(ppData); + + this.FilenameEventReceived?.Invoke(this, new SimSystemEventFilenameReceivedEventArgs(recvEventFilename.FileName, recvEventFilename.Flags)); + + if (SimConnectLogger.IsLevelEnabled(SimConnectLogger.LogLevel.Debug)) + { + SimConnectLogger.Debug($"Filename Event Received: FileName={recvEventFilename.FileName} Flags={recvEventFilename.Flags}"); + } + } + catch (Exception ex) when (!ExceptionHelper.IsCritical(ex)) + { + SimConnectLogger.Error("Error processing filename system event", ex); + } + } + + /// + /// Processes an object add/remove system event message from SimConnect. + /// + /// Pointer to the received EventObjectAddRemove data. + private void ProcessSystemEventObjectAddRemove(IntPtr ppData) + { + try + { + var recvEventObject = Marshal.PtrToStructure(ppData); + + this.ObjectAddRemoveEventReceived?.Invoke(this, new SimSystemEventObjectAddRemoveEventArgs(recvEventObject.EObjType)); + + if (SimConnectLogger.IsLevelEnabled(SimConnectLogger.LogLevel.Debug)) + { + SimConnectLogger.Debug($"Object Add/Remove Event Received: Type={recvEventObject.EObjType}"); + } + } + catch (Exception ex) when (!ExceptionHelper.IsCritical(ex)) + { + SimConnectLogger.Error("Error processing object add/remove system event", ex); + } + } + + /// + /// Processes an extended EX1 system event message from SimConnect. + /// + /// Pointer to the received EventEx1 data. + private void ProcessSystemEventEx1(IntPtr ppData) + { + try + { + var recvEventEx1 = Marshal.PtrToStructure(ppData); + + this.SystemEventEx1Received?.Invoke( + this, + new SimSystemEventEx1ReceivedEventArgs( + recvEventEx1.EventId, + recvEventEx1.Data0, + recvEventEx1.Data1, + recvEventEx1.Data2, + recvEventEx1.Data3, + recvEventEx1.Data4)); + + if (SimConnectLogger.IsLevelEnabled(SimConnectLogger.LogLevel.Debug)) + { + SimConnectLogger.Debug($"EX1 System Event Received: EventId={recvEventEx1.EventId} Data=[{recvEventEx1.Data0},{recvEventEx1.Data1},{recvEventEx1.Data2},{recvEventEx1.Data3},{recvEventEx1.Data4}]"); + } + } + catch (Exception ex) when (!ExceptionHelper.IsCritical(ex)) + { + SimConnectLogger.Error("Error processing EX1 system event", ex); + } + } + /// /// Starts the background message processing loop. /// diff --git a/src/SimConnect.NET/SimVar/SimVarManager.cs b/src/SimConnect.NET/SimVar/SimVarManager.cs index f4fbff0..fd78f77 100644 --- a/src/SimConnect.NET/SimVar/SimVarManager.cs +++ b/src/SimConnect.NET/SimVar/SimVarManager.cs @@ -3,6 +3,7 @@ // using System; +using System.Buffers; using System.Collections.Concurrent; using System.Runtime.InteropServices; using System.Threading; @@ -703,64 +704,84 @@ private async Task GetAsyncCore(uint definitionId, uint objectId, Cancella { var request = this.StartRequest(definitionId, objectId, SimConnectPeriod.Once, onValue: null); - using (cancellationToken.Register(() => this.CancelRequest(request))) + CancellationTokenSource? timeoutCts = null; + CancellationTokenSource? linkedCts = null; + try { - Task awaited = request.Task; if (this.requestTimeout != Timeout.InfiniteTimeSpan) { - var timeoutTask = Task.Delay(this.requestTimeout, CancellationToken.None); - var completed = await Task.WhenAny(awaited, timeoutTask).ConfigureAwait(false); - if (completed == timeoutTask) + timeoutCts = new CancellationTokenSource(this.requestTimeout); + linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token); + } + + var combinedToken = linkedCts?.Token ?? cancellationToken; + + using (combinedToken.Register(() => this.CancelRequest(request))) + { + try + { + return await request.Task.ConfigureAwait(false); + } + catch (OperationCanceledException) when (timeoutCts?.IsCancellationRequested == true && !cancellationToken.IsCancellationRequested) { this.pendingRequests.TryRemove(request.RequestId, out _); throw new TimeoutException($"Request '{typeof(T).Name}' timed out after {this.requestTimeout} (RequestId={request.RequestId})"); } } - - var value = await awaited.ConfigureAwait(false); - return value; + } + finally + { + linkedCts?.Dispose(); + timeoutCts?.Dispose(); } } - private async Task SetWithDefinitionAsync(SimVarDefinition definition, T value, uint objectId, CancellationToken cancellationToken) + private Task SetWithDefinitionAsync(SimVarDefinition definition, T value, uint objectId, CancellationToken cancellationToken) { var definitionId = this.EnsureDataDefinition(definition, cancellationToken); cancellationToken.ThrowIfCancellationRequested(); - // Offload blocking marshaling + native call to thread-pool so the async method can await - await Task.Run( - () => - { - // Allocate memory for the value - var dataSize = GetDataSize(); - var dataPtr = Marshal.AllocHGlobal(dataSize); + var dataSize = GetDataSize(); + byte[]? rented = null; + GCHandle handle = default; - try - { - // Marshal the value to unmanaged memory - MarshalValue(value, dataPtr); + try + { + rented = ArrayPool.Shared.Rent(dataSize); + handle = GCHandle.Alloc(rented, GCHandleType.Pinned); + var dataPtr = handle.AddrOfPinnedObject(); + + MarshalValue(value, dataPtr); + + var result = SimConnectNative.SimConnect_SetDataOnSimObject( + this.simConnectHandle, + definitionId, + objectId, + 0, + 1, + (uint)dataSize, + dataPtr); + + if (result != (int)SimConnectError.None) + { + throw new SimConnectException($"Failed to set SimVar {definition.Name}: {(SimConnectError)result}", (SimConnectError)result); + } + } + finally + { + if (handle.IsAllocated) + { + handle.Free(); + } - var result = SimConnectNative.SimConnect_SetDataOnSimObject( - this.simConnectHandle, - definitionId, - objectId, - 0, // flags - 1, // arrayCount - (uint)dataSize, - dataPtr); + if (rented != null) + { + ArrayPool.Shared.Return(rented); + } + } - if (result != (int)SimConnectError.None) - { - throw new SimConnectException($"Failed to set SimVar {definition.Name}: {(SimConnectError)result}", (SimConnectError)result); - } - } - finally - { - Marshal.FreeHGlobal(dataPtr); - } - }, - cancellationToken).ConfigureAwait(false); + return Task.CompletedTask; } private uint EnsureDataDefinition(SimVarDefinition definition, CancellationToken cancellationToken) @@ -955,7 +976,7 @@ private uint EnsureScalarDefinition(string name, string? unit = null, SimConnect /// /// Core handler that writes a struct T using the same field layout as EnsureTypeDefinition created. /// - private async Task SetStructAsync(uint definitionId, T value, uint objectId, CancellationToken cancellationToken) + private Task SetStructAsync(uint definitionId, T value, uint objectId, CancellationToken cancellationToken) where T : struct { cancellationToken.ThrowIfCancellationRequested(); @@ -966,40 +987,51 @@ private async Task SetStructAsync(uint definitionId, T value, uint objectId, throw new InvalidOperationException($"No struct writer found for DefinitionId={definitionId}. EnsureTypeDefinition must be called first."); } - await Task.Run( - () => + byte[]? rented = null; + GCHandle handle = default; + + try + { + rented = ArrayPool.Shared.Rent(cache.TotalSize); + handle = GCHandle.Alloc(rented, GCHandleType.Pinned); + var dataPtr = handle.AddrOfPinnedObject(); + + // Fill the buffer using the cached writer delegate for this definition + if (cache.Write is not Action write) { - var dataPtr = Marshal.AllocHGlobal(cache.TotalSize); - try - { - // Fill the buffer using the cached writer delegate for this definition - if (cache.Write is not Action write) - { - throw new InvalidOperationException($"Cached writer has unexpected type for DefinitionId={definitionId} and T={typeof(T).Name}."); - } + throw new InvalidOperationException($"Cached writer has unexpected type for DefinitionId={definitionId} and T={typeof(T).Name}."); + } - write(dataPtr, value); + write(dataPtr, value); - var hr = SimConnectNative.SimConnect_SetDataOnSimObject( - this.simConnectHandle, - definitionId, - objectId, - 0, - 1, - (uint)cache.TotalSize, - dataPtr); + var hr = SimConnectNative.SimConnect_SetDataOnSimObject( + this.simConnectHandle, + definitionId, + objectId, + 0, + 1, + (uint)cache.TotalSize, + dataPtr); - if (hr != (int)SimConnectError.None) - { - throw new SimConnectException($"Failed to set struct '{typeof(T).Name}': {(SimConnectError)hr}", (SimConnectError)hr); - } - } - finally - { - Marshal.FreeHGlobal(dataPtr); - } - }, - cancellationToken).ConfigureAwait(false); + if (hr != (int)SimConnectError.None) + { + throw new SimConnectException($"Failed to set struct '{typeof(T).Name}': {(SimConnectError)hr}", (SimConnectError)hr); + } + } + finally + { + if (handle.IsAllocated) + { + handle.Free(); + } + + if (rented != null) + { + ArrayPool.Shared.Return(rented); + } + } + + return Task.CompletedTask; } } } diff --git a/src/SimConnect.NET/lib/SimConnect.dll b/src/SimConnect.NET/lib/SimConnect.dll index be0d9cd1c8577f1f834be488ed33946bbb89e511..692013a1e8922d1b3a8ef36b4b997b63f465a448 100644 GIT binary patch delta 22325 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 j2s4!gMgYtqPA{h# delta 21995 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 +// Copyright (c) BARS. All rights reserved. +// + +using System; +using System.Threading; +using System.Threading.Tasks; +using SimConnect.NET.Events; + +namespace SimConnect.NET.Tests.Net8.Tests +{ + internal class SystemEventStateTests : ISimConnectTest + { + public string Name => "SystemEventState"; + + public string Description => "Verifies system event subscribe, SetSystemEventState, and Unsubscribe behavior"; + + public string Category => "System Event"; + + public async Task RunAsync(SimConnectClient client, CancellationToken cancellationToken = default) + { + if (!client.IsConnected) + { + Console.WriteLine(" ❌ Client should already be connected"); + return false; + } + + const uint systemEventId = 101; + const string systemEventName = "1sec"; + + using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + cts.CancelAfter(TimeSpan.FromSeconds(40)); + + int totalEvents = 0; + + EventHandler handler = (sender, e) => + { + if (e.EventId != systemEventId) + { + return; + } + + Interlocked.Increment(ref totalEvents); + }; + + client.SystemEventReceived += handler; + + try + { + await client.SubscribeToEventAsync(systemEventName, systemEventId, cts.Token).ConfigureAwait(false); + await client.SetSystemEventStateAsync(systemEventId, SimConnectState.On, cts.Token).ConfigureAwait(false); + Console.WriteLine(" ⏳ Waiting for initial system event..."); + + if (!await WaitForConditionAsync(() => Volatile.Read(ref totalEvents) > 0, TimeSpan.FromSeconds(15), cts.Token).ConfigureAwait(false)) + { + Console.WriteLine(" ❌ Did not receive initial system event"); + return false; + } + + Console.WriteLine(" ✅ Initial system event received"); + await client.SetSystemEventStateAsync(systemEventId, SimConnectState.Off, cts.Token).ConfigureAwait(false); + int afterOffBaseline = Volatile.Read(ref totalEvents); + + Console.WriteLine(" ⏳ Waiting to confirm events are suppressed after SetSystemEventState(Off)..."); + await Task.Delay(TimeSpan.FromSeconds(6), cts.Token).ConfigureAwait(false); + + int afterOffCount = Volatile.Read(ref totalEvents) - afterOffBaseline; + if (afterOffCount > 0) + { + Console.WriteLine(" ❌ Received system events while state was Off"); + return false; + } + + Console.WriteLine(" ✅ No events received while state was Off"); + await client.SetSystemEventStateAsync(systemEventId, SimConnectState.On, cts.Token).ConfigureAwait(false); + int afterOnBaseline = Volatile.Read(ref totalEvents); + + Console.WriteLine(" ⏳ Waiting for events after SetSystemEventState(On)..."); + if (!await WaitForConditionAsync(() => Volatile.Read(ref totalEvents) > afterOnBaseline, TimeSpan.FromSeconds(15), cts.Token).ConfigureAwait(false)) + { + Console.WriteLine(" ❌ Did not receive event after turning state On"); + return false; + } + + Console.WriteLine(" ✅ Events resumed after turning state On"); + + await client.UnsubscribeFromEventAsync(systemEventId, cts.Token).ConfigureAwait(false); + int afterUnsubscribeBaseline = Volatile.Read(ref totalEvents); + + Console.WriteLine(" ⏳ Waiting to ensure no events after unsubscribe..."); + await Task.Delay(TimeSpan.FromSeconds(6), cts.Token).ConfigureAwait(false); + + int afterUnsubscribeCount = Volatile.Read(ref totalEvents) - afterUnsubscribeBaseline; + if (afterUnsubscribeCount > 0) + { + Console.WriteLine(" ❌ Received system events after unsubscribe"); + return false; + } + + Console.WriteLine(" ✅ No events after unsubscribe"); + return true; + } + catch (OperationCanceledException) + { + Console.WriteLine(" ❌ System event state test timed out"); + return false; + } + catch (Exception ex) + { + Console.WriteLine($" ❌ System event state test failed: {ex.Message}"); + return false; + } + finally + { + client.SystemEventReceived -= handler; + + try + { + await client.UnsubscribeFromEventAsync(systemEventId, CancellationToken.None).ConfigureAwait(false); + } + catch + { + // Best-effort cleanup; ignore errors + } + } + } + + private static async Task WaitForConditionAsync(Func predicate, TimeSpan timeout, CancellationToken token) + { + var deadline = DateTime.UtcNow + timeout; + while (DateTime.UtcNow < deadline) + { + if (predicate()) + { + return true; + } + + await Task.Delay(200, token).ConfigureAwait(false); + } + + return predicate(); + } + } +} diff --git a/tests/SimConnect.NET.Tests.Net8/Tests/SystemEventSubscriptionTests.cs b/tests/SimConnect.NET.Tests.Net8/Tests/SystemEventSubscriptionTests.cs index f124d37..d8cb083 100644 --- a/tests/SimConnect.NET.Tests.Net8/Tests/SystemEventSubscriptionTests.cs +++ b/tests/SimConnect.NET.Tests.Net8/Tests/SystemEventSubscriptionTests.cs @@ -2,6 +2,8 @@ // Copyright (c) BARS. All rights reserved. // +using SimConnect.NET.Events; + namespace SimConnect.NET.Tests.Net8.Tests { internal class SystemEventSubscriptionTests : ISimConnectTest @@ -28,7 +30,7 @@ public async Task RunAsync(SimConnectClient client, CancellationToken canc cts.CancelAfter(TimeSpan.FromSeconds(15)); bool testEventReceived = false; - client.SystemEventReceived += (sender, e) => + EventHandler handler = (sender, e) => { switch (e.EventId) { @@ -39,21 +41,32 @@ public async Task RunAsync(SimConnectClient client, CancellationToken canc } }; - await client.SubscribeToEventAsync("4sec", 100, cts.Token); - - Console.WriteLine("Listening for events..."); + client.SystemEventReceived += handler; - while (!testEventReceived && !cts.Token.IsCancellationRequested) + try { - await Task.Delay(500, cts.Token); + await client.SubscribeToEventAsync("4sec", 100, cts.Token); + + Console.WriteLine("Listening for events..."); + + while (!testEventReceived && !cts.Token.IsCancellationRequested) + { + await Task.Delay(500, cts.Token); + } + + if (!testEventReceived) + { + Console.WriteLine(" ❌ Did not receive expected system event"); + return false; + } + + Console.WriteLine(" ✅ Received expected system event"); + return true; } - if (!testEventReceived) + finally { - Console.WriteLine(" ❌ Did not receive expected system event"); - return false; + client.SystemEventReceived -= handler; } - Console.WriteLine(" ✅ Received expected system event"); - return true; } catch (OperationCanceledException) { diff --git a/tests/SimConnect.NET.Tests.Net8/Tests/TestRunner.cs b/tests/SimConnect.NET.Tests.Net8/Tests/TestRunner.cs index 512f3aa..103ec06 100644 --- a/tests/SimConnect.NET.Tests.Net8/Tests/TestRunner.cs +++ b/tests/SimConnect.NET.Tests.Net8/Tests/TestRunner.cs @@ -32,6 +32,7 @@ public TestRunner() new InputEventValueTests(), new PerformanceTests(), new SystemEventSubscriptionTests(), + new SystemEventStateTests(), }; }