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 new file mode 100644 index 0000000..ecac1f8 --- /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..cba6392 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,31 @@ 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. + /// + public event EventHandler? SystemEventReceived; + /// /// Gets a value indicating whether the client is connected to SimConnect. /// @@ -346,12 +370,15 @@ public async Task DisconnectAsync() } /// - /// Processes the next SimConnect message. + /// 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 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 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) { ObjectDisposedException.ThrowIf(this.disposed, nameof(SimConnectClient)); @@ -360,78 +387,185 @@ 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_SubscribeToSystemEvent( + this.simConnectHandle, + systemEventId, + systemEventName); 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 subscribe to event {systemEventName}: {(SimConnectError)result}", (SimConnectError)result); } + }, + cancellationToken).ConfigureAwait(false); + } - if (ppData != IntPtr.Zero && pcbData > 0) + /// + /// 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 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)); + + if (!this.isConnected) + { + throw new InvalidOperationException("Not connected to SimConnect."); + } + + await Task.Run( + () => + { + var result = SimConnectNative.SimConnect_SetSystemEventState( + this.simConnectHandle, + systemEventId, + (uint)state); + + if (result != (int)SimConnectError.None) { - var recv = Marshal.PtrToStructure(ppData); - var recvId = (SimConnectRecvId)recv.Id; + throw new SimConnectException($"Failed to set system event state for {systemEventId}: {(SimConnectError)result}", (SimConnectError)result); + } + }, + cancellationToken).ConfigureAwait(false); + } - 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; - 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. /// @@ -543,6 +677,129 @@ 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); + } + } + + /// + /// 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 be0d9cd..692013a 100644 Binary files a/src/SimConnect.NET/lib/SimConnect.dll and b/src/SimConnect.NET/lib/SimConnect.dll differ diff --git a/tests/SimConnect.NET.Tests.Net8/Tests/SystemEventStateTests.cs b/tests/SimConnect.NET.Tests.Net8/Tests/SystemEventStateTests.cs new file mode 100644 index 0000000..1d1c4b5 --- /dev/null +++ b/tests/SimConnect.NET.Tests.Net8/Tests/SystemEventStateTests.cs @@ -0,0 +1,144 @@ +// +// 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 new file mode 100644 index 0000000..d8cb083 --- /dev/null +++ b/tests/SimConnect.NET.Tests.Net8/Tests/SystemEventSubscriptionTests.cs @@ -0,0 +1,83 @@ +// +// Copyright (c) BARS. All rights reserved. +// + +using SimConnect.NET.Events; + +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; + EventHandler handler = (sender, e) => + { + switch (e.EventId) + { + case 100: + Console.WriteLine("4 seconds has passed!"); + testEventReceived = true; + break; + } + }; + + client.SystemEventReceived += handler; + + try + { + 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; + } + finally + { + client.SystemEventReceived -= handler; + } + } + 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..103ec06 100644 --- a/tests/SimConnect.NET.Tests.Net8/Tests/TestRunner.cs +++ b/tests/SimConnect.NET.Tests.Net8/Tests/TestRunner.cs @@ -31,6 +31,8 @@ public TestRunner() new InputEventTests(), new InputEventValueTests(), new PerformanceTests(), + new SystemEventSubscriptionTests(), + new SystemEventStateTests(), }; } @@ -143,6 +145,9 @@ private static TestOptions ParseArguments(string[] args) case "--verbose": options.Verbose = true; break; + case "--test-events": + options.Categories.Add("System Event"); + break; } }