diff --git a/CHANGELOG.md b/CHANGELOG.md index dd7aa26..3e78ed7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.1.17] - 2025-11-14 + +### Added + +- Introduced `ExceptionHelper` and wired it into the client, SimVar manager, logger, and input-event flows so background loops now swallow only non-critical exceptions while allowing fatal runtime issues to surface. +- Automatic SimVar type inference now understands `SimConnectDataInitPosition`, letting scalar and struct-based calls pick the correct `SimConnectDataType.InitPosition` without manual annotations. + +### Changed + +- Struct-based `SimVars.SetAsync` reuses a cached writer delegate and layout per definition ID (`defToWriter`) and fails fast with a clear error when a writer is missing, keeping writes aligned with the cached reader pipeline. +- String fields written through the struct pipeline now emit fixed-size, null-terminated ANSI buffers without intermediate allocations, matching SimConnect’s expectations and avoiding buffer overruns. +- Validation and messaging now consistently reference `[SimConnect]` attributes, and `SimVars.SetAsync` switches to `ArgumentNullException` for the `unit` parameter so SimVars that rely on default units (e.g., “INITIAL POSITION”) can be written. +- SimConnect client message loops, the SimVar manager, and `SimConnectLogger` gate debug logging with `SimConnectLogger.IsLevelEnabled`, use the shared `ExceptionHelper`, and improve diagnostics (e.g., disconnect/reconnect cancellation messages, suppressed callback logging) for quieter yet more informative logs. +- Input event enumeration and value parsing became more defensive: node names default to empty strings, doubles are only decoded when the payload actually contains eight bytes, and `InputEventValue` try-get helpers catch `InvalidCastException`, `FormatException`, and `OverflowException` to prevent spurious crashes. +- Integration tests received maintenance—`TestRunner` now reuses a single `SimConnectClient`, AI object tests use double arithmetic for position offsets, and connection tests drop unused locals—leading to steadier end-to-end coverage. + +### Fixed + +- Struct writers now enforce cached layouts plus fixed-size ANSI encoding when sending data back to SimConnect, eliminating garbled string fields and layout drift when multiple structs are written in succession. +- `SimVarManager` logs and safely ignores null pending-request slots, reports unknown definition IDs cleanly, and surfaces missing struct writers instead of breaking the message loop. +- Subscription callbacks dispatched through `SimVarRequest` now log suppressed user exceptions (without crashing the SimConnect thread), improving observability when user code faults. + ## [0.1.16-beta] - 2025-10-20 ### Added diff --git a/Directory.Build.props b/Directory.Build.props index 0bb0ac2..2b04d21 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 0.1.16-beta + 0.1.17 BARS BARS SimConnect.NET diff --git a/src/SimConnect.NET/AI/SimObjectType.cs b/src/SimConnect.NET/AI/SimObjectType.cs index 96a805b..08e20a4 100644 --- a/src/SimConnect.NET/AI/SimObjectType.cs +++ b/src/SimConnect.NET/AI/SimObjectType.cs @@ -2,6 +2,8 @@ // Copyright (c) BARS. All rights reserved. // +using System.Linq; + namespace SimConnect.NET.AI { /// @@ -25,17 +27,12 @@ public static IEnumerable GetAllContainerTitles() typeof(Special), }; - foreach (var type in types) - { - var fields = type.GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static); - foreach (var field in fields) - { - if (field.FieldType == typeof(string) && field.GetValue(null) is string value) - { - yield return value; - } - } - } + const System.Reflection.BindingFlags bindingFlags = System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static; + + return types + .SelectMany(type => type.GetFields(bindingFlags)) + .Select(field => field.GetValue(null)) + .OfType(); } /// diff --git a/src/SimConnect.NET/InputEvents/InputEventManager.cs b/src/SimConnect.NET/InputEvents/InputEventManager.cs index 4b37767..8cc25a2 100644 --- a/src/SimConnect.NET/InputEvents/InputEventManager.cs +++ b/src/SimConnect.NET/InputEvents/InputEventManager.cs @@ -12,6 +12,7 @@ using System.Threading; using System.Threading.Tasks; using SimConnect.NET.Events; +using SimConnect.NET.Internal; namespace SimConnect.NET.InputEvents { @@ -517,7 +518,7 @@ internal void ProcessReceivedData(IntPtr ppData, uint cbData) break; } } - catch (Exception ex) + catch (Exception ex) when (!ExceptionHelper.IsCritical(ex)) { SimConnectLogger.Error("Error processing input event data", ex); } @@ -608,17 +609,17 @@ private void ProcessEnumerateInputEvents(IntPtr ppData) // For 76-byte structure, we might have additional padding or node names // Let's skip any remaining bytes to get to the next item - var nodeNames = string.Empty; // For now, assume no node names in this structure currentPtr = IntPtr.Add(dataPtr, (int)(i + 1) * (int)actualItemSize); // Create descriptor only if we have valid data if (!string.IsNullOrEmpty(name)) { - var descriptor = new InputEventDescriptor(name, hash, type, nodeNames); + // Node name parsing not yet understood, so default to empty for now + var descriptor = new InputEventDescriptor(name, hash, type, string.Empty); descriptors.Add(descriptor); } } - catch (Exception itemEx) + catch (Exception itemEx) when (!ExceptionHelper.IsCritical(itemEx)) { SimConnectLogger.Warning($"Error parsing input event item {i}: {itemEx.Message}"); break; // Stop processing if we hit an error with an individual item @@ -635,7 +636,7 @@ private void ProcessEnumerateInputEvents(IntPtr ppData) return; } } - catch (Exception ex) + catch (Exception ex) when (!ExceptionHelper.IsCritical(ex)) { SimConnectLogger.Error("Error processing enumerate input events", ex); @@ -663,7 +664,7 @@ private void ProcessEnumerateInputEventParams(IntPtr ppData) tcs.TrySetResult(recvParams.Value ?? string.Empty); } } - catch (Exception ex) + catch (Exception ex) when (!ExceptionHelper.IsCritical(ex)) { SimConnectLogger.Error("Error processing enumerate input event params", ex); } @@ -697,8 +698,9 @@ private void ProcessGetInputEvent(IntPtr ppData) default: byte[] defBuf = new byte[payloadSize]; Marshal.Copy(pValue, defBuf, 0, defBuf.Length); - value = defBuf; - value = BitConverter.ToDouble(defBuf, 0); + value = payloadSize >= sizeof(double) + ? BitConverter.ToDouble(defBuf, 0) + : defBuf; break; } @@ -713,7 +715,7 @@ private void ProcessGetInputEvent(IntPtr ppData) tcs.TrySetResult(inputEventValue); } } - catch (Exception ex) + catch (Exception ex) when (!ExceptionHelper.IsCritical(ex)) { SimConnectLogger.Error("Error processing get input event", ex); } @@ -776,7 +778,7 @@ private void ProcessSubscribeInputEvent(IntPtr ppData) // Also raise the general event this.InputEventChanged?.Invoke(this, new InputEventChangedEventArgs(inputEventValue)); } - catch (Exception ex) + catch (Exception ex) when (!ExceptionHelper.IsCritical(ex)) { SimConnectLogger.Error("Error processing subscribe input event", ex); } diff --git a/src/SimConnect.NET/InputEvents/InputEventValue.cs b/src/SimConnect.NET/InputEvents/InputEventValue.cs index 94b74c1..dc6ce45 100644 --- a/src/SimConnect.NET/InputEvents/InputEventValue.cs +++ b/src/SimConnect.NET/InputEvents/InputEventValue.cs @@ -3,6 +3,7 @@ // using System; +using System.Diagnostics; using System.Globalization; namespace SimConnect.NET.InputEvents @@ -84,10 +85,20 @@ public bool TryGetDoubleValue(out double value) value = this.GetDoubleValue(); return true; } - catch + catch (InvalidCastException ex) { - return false; + Debug.WriteLine($"[InputEventValue.TryGetDoubleValue] Invalid type conversion: {ex.Message}"); + } + catch (FormatException ex) + { + Debug.WriteLine($"[InputEventValue.TryGetDoubleValue] Invalid format: {ex.Message}"); } + catch (OverflowException ex) + { + Debug.WriteLine($"[InputEventValue.TryGetDoubleValue] Value overflow: {ex.Message}"); + } + + return false; } /// @@ -103,15 +114,8 @@ public bool TryGetStringValue(out string value) return false; } - try - { - value = this.GetStringValue(); - return true; - } - catch - { - return false; - } + value = this.GetStringValue(); + return true; } /// diff --git a/src/SimConnect.NET/Internal/ExceptionHelper.cs b/src/SimConnect.NET/Internal/ExceptionHelper.cs new file mode 100644 index 0000000..4485280 --- /dev/null +++ b/src/SimConnect.NET/Internal/ExceptionHelper.cs @@ -0,0 +1,32 @@ +// +// Copyright (c) BARS. All rights reserved. +// + +using System; + +namespace SimConnect.NET.Internal +{ + /// + /// Provides helpers for determining whether an exception is critical enough to rethrow. + /// + internal static class ExceptionHelper + { + /// + /// Determines whether the supplied exception is considered critical and should not be swallowed. + /// + /// The exception to inspect. + /// true if the exception is critical; otherwise, false. + public static bool IsCritical(Exception exception) + { + ArgumentNullException.ThrowIfNull(exception); + + return exception is OutOfMemoryException + or StackOverflowException + or ThreadAbortException + or AccessViolationException + or AppDomainUnloadedException + or CannotUnloadAppDomainException + or BadImageFormatException; + } + } +} diff --git a/src/SimConnect.NET/SimConnectClient.cs b/src/SimConnect.NET/SimConnectClient.cs index ac99f0b..c5b6d3f 100644 --- a/src/SimConnect.NET/SimConnectClient.cs +++ b/src/SimConnect.NET/SimConnectClient.cs @@ -10,6 +10,7 @@ using SimConnect.NET.Aircraft; using SimConnect.NET.Events; using SimConnect.NET.InputEvents; +using SimConnect.NET.Internal; using SimConnect.NET.SimVar; namespace SimConnect.NET @@ -293,6 +294,10 @@ public async Task DisconnectAsync() } catch (OperationCanceledException) { + if (SimConnectLogger.IsLevelEnabled(SimConnectLogger.LogLevel.Debug)) + { + SimConnectLogger.Debug("Message processing task canceled during disconnect."); + } } } @@ -309,6 +314,10 @@ public async Task DisconnectAsync() } catch (OperationCanceledException) { + if (SimConnectLogger.IsLevelEnabled(SimConnectLogger.LogLevel.Debug)) + { + SimConnectLogger.Debug("Reconnect task canceled during disconnect."); + } } } @@ -360,12 +369,9 @@ public async Task ProcessNextMessageAsync(CancellationToken cancellationTo if (result != (int)SimConnectError.None) { // Filter out the common "no messages available" error to reduce log spam - if (result != -2147467259) + if (result != -2147467259 && SimConnectLogger.IsLevelEnabled(SimConnectLogger.LogLevel.Debug)) { - if (SimConnectLogger.IsLevelEnabled(SimConnectLogger.LogLevel.Debug)) - { - SimConnectLogger.Debug($"SimConnect_GetNextDispatch returned: {(SimConnectError)result}"); - } + SimConnectLogger.Debug($"SimConnect_GetNextDispatch returned: {(SimConnectError)result}"); } return false; @@ -385,7 +391,7 @@ public async Task ProcessNextMessageAsync(CancellationToken cancellationTo { this.RawMessageReceived?.Invoke(this, new RawSimConnectMessageEventArgs(ppData, pcbData, recvId)); } - catch (Exception hookEx) + catch (Exception hookEx) when (!ExceptionHelper.IsCritical(hookEx)) { SimConnectLogger.Warning($"RawMessageReceived hook threw: {hookEx.Message}"); } @@ -445,7 +451,7 @@ public async Task TestConnectionAsync(CancellationToken cancellationToken await this.SimVars.GetAsync("SIMULATION RATE", "number", cancellationToken: cancellationToken).ConfigureAwait(false); return true; } - catch (Exception ex) + catch (Exception ex) when (!ExceptionHelper.IsCritical(ex)) { this.OnErrorOccurred(SimConnectError.Error, ex, "Connection health check failed"); return false; @@ -484,7 +490,7 @@ private void ProcessAssignedObjectId(IntPtr ppData) SimConnectLogger.Info($"Processed assigned object ID: RequestId={recvAssignedObjectId.RequestId}, ObjectId={recvAssignedObjectId.ObjectId}"); } - catch (Exception ex) + catch (Exception ex) when (!ExceptionHelper.IsCritical(ex)) { SimConnectLogger.Error("Error processing assigned object ID", ex); } @@ -510,7 +516,7 @@ private void ProcessError(IntPtr ppData) this.simObjectManager.ProcessObjectCreationFailed(recvError.SendId, error); } } - catch (Exception ex) + catch (Exception ex) when (!ExceptionHelper.IsCritical(ex)) { SimConnectLogger.Error("Error processing SimConnect error message", ex); this.OnErrorOccurred(SimConnectError.Error, ex, "Error processing SimConnect error message"); @@ -531,7 +537,7 @@ private void ProcessOpen(IntPtr ppData) this.isMSFS2024 = recvOpen.ApplicationVersionMajor == 12; SimConnectLogger.Info($"SimConnect OPEN received: AppVersion={recvOpen.ApplicationVersionMajor}.{recvOpen.ApplicationVersionMinor} Build={recvOpen.ApplicationBuildMajor}.{recvOpen.ApplicationBuildMinor} (IsMSFS2024={this.isMSFS2024})"); } - catch (Exception ex) + catch (Exception ex) when (!ExceptionHelper.IsCritical(ex)) { SimConnectLogger.Error("Error processing SimConnect OPEN message", ex); } @@ -598,6 +604,10 @@ private async Task StartMessageProcessingLoopAsync(CancellationToken cancellatio } catch (OperationCanceledException) { + if (SimConnectLogger.IsLevelEnabled(SimConnectLogger.LogLevel.Debug)) + { + SimConnectLogger.Debug("Message processing loop canceled."); + } } } @@ -673,7 +683,7 @@ private async Task PerformAutoReconnectAsync(CancellationToken cancellationToken break; } } - catch (Exception ex) + catch (Exception ex) when (!ExceptionHelper.IsCritical(ex)) { SimConnectLogger.Warning($"Auto-reconnection attempt {this.reconnectAttempts} failed: {ex.Message}"); this.OnErrorOccurred(SimConnectError.Error, ex, $"Auto-reconnection attempt {this.reconnectAttempts}"); diff --git a/src/SimConnect.NET/SimConnectLogger.cs b/src/SimConnect.NET/SimConnectLogger.cs index 967fbd7..1e4e48f 100644 --- a/src/SimConnect.NET/SimConnectLogger.cs +++ b/src/SimConnect.NET/SimConnectLogger.cs @@ -5,9 +5,11 @@ using System; using System.Collections.Concurrent; using System.IO; +using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; +using SimConnect.NET.Internal; namespace SimConnect.NET { @@ -33,12 +35,12 @@ private SimConnectLogger() try { var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); - var folder = Path.Combine(localAppData, DefaultFolderName); + var folder = Path.Join(localAppData, DefaultFolderName); Directory.CreateDirectory(folder); - var filePath = Path.Combine(folder, DefaultFileName); + var filePath = Path.Join(folder, DefaultFileName); this.sink = new FileLogSink(filePath); } - catch (Exception ex) + catch (Exception ex) when (!ExceptionHelper.IsCritical(ex)) { // Fall back to debug output only if file sink fails. System.Diagnostics.Debug.WriteLine($"SimConnectLogger: Failed to initialize file sink: {ex.Message}"); @@ -122,7 +124,7 @@ public static void Configure(LogLevel minimumLevel = LogLevel.Debug, string? log var oldSink = logger.ExchangeSink(newSink); oldSink?.Dispose(); } - catch (Exception ex) + catch (Exception ex) when (!ExceptionHelper.IsCritical(ex)) { System.Diagnostics.Debug.WriteLine($"SimConnectLogger.Configure: Failed to set file sink: {ex.Message}"); } @@ -187,27 +189,27 @@ public void Dispose() this.queue.CompleteAdding(); this.cts.Cancel(); } - catch + catch (Exception ex) when (!ExceptionHelper.IsCritical(ex)) { - // ignored + System.Diagnostics.Debug.WriteLine($"SimConnectLogger.Dispose: queue shutdown failed: {ex.Message}"); } try { this.worker.Wait(2000); } - catch + catch (Exception ex) when (!ExceptionHelper.IsCritical(ex)) { - // ignored + System.Diagnostics.Debug.WriteLine($"SimConnectLogger.Dispose: worker wait failed: {ex.Message}"); } try { this.sink?.Dispose(); } - catch + catch (Exception ex) when (!ExceptionHelper.IsCritical(ex)) { - // ignored + System.Diagnostics.Debug.WriteLine($"SimConnectLogger.Dispose: sink dispose failed: {ex.Message}"); } } @@ -237,9 +239,9 @@ private void Enqueue(LogLevel level, string message) { this.queue.Add((DateTime.UtcNow, level, message)); } - catch + catch (Exception ex) when (!ExceptionHelper.IsCritical(ex)) { - // ignored + System.Diagnostics.Debug.WriteLine($"SimConnectLogger.Enqueue failed: {ex.Message}"); } } @@ -249,15 +251,14 @@ private void ProcessQueue() { try { - foreach (var item in this.queue.GetConsumingEnumerable(this.cts.Token)) + foreach (var line in this.queue.GetConsumingEnumerable(this.cts.Token) + .Select(item => Format(item.TimestampUtc, item.Level, item.Message))) { - var line = Format(item.TimestampUtc, item.Level, item.Message); - try { this.sink?.WriteLine(line); } - catch (Exception ex) + catch (Exception ex) when (!ExceptionHelper.IsCritical(ex)) { // If sink fails at runtime, try to swap to debug sink. System.Diagnostics.Debug.WriteLine($"SimConnectLogger: Write failed: {ex.Message}"); @@ -274,7 +275,7 @@ private void ProcessQueue() { // Expected during shutdown } - catch (Exception ex) + catch (Exception ex) when (!ExceptionHelper.IsCritical(ex)) { System.Diagnostics.Debug.WriteLine($"SimConnectLogger worker crashed: {ex.Message}"); } diff --git a/src/SimConnect.NET/SimVar/Internal/SimVarFieldReaderFactory.cs b/src/SimConnect.NET/SimVar/Internal/SimVarFieldReaderFactory.cs index 748d583..1ba9891 100644 --- a/src/SimConnect.NET/SimVar/Internal/SimVarFieldReaderFactory.cs +++ b/src/SimConnect.NET/SimVar/Internal/SimVarFieldReaderFactory.cs @@ -23,12 +23,12 @@ public static List> Build( { var t = typeof(T); - // Collect and order fields with [SimVar] + // Collect and order fields with [SimConnect] var fields = GetOrderedSimVarFields(t); if (fields.Count == 0) { - throw new InvalidOperationException($"Type {t.FullName} has no fields with [SimVar]."); + throw new InvalidOperationException($"Type {t.FullName} has no fields with [SimConnect]."); } var readers = new List>(fields.Count); diff --git a/src/SimConnect.NET/SimVar/Internal/SimVarFieldWriter.cs b/src/SimConnect.NET/SimVar/Internal/SimVarFieldWriter.cs index edc9a23..941efec 100644 --- a/src/SimConnect.NET/SimVar/Internal/SimVarFieldWriter.cs +++ b/src/SimConnect.NET/SimVar/Internal/SimVarFieldWriter.cs @@ -67,13 +67,26 @@ public void WriteFrom(in T source, IntPtr basePtr) { var getter = (Func)this.Extractor; string s = getter(source) ?? string.Empty; - var bytes = System.Text.Encoding.ASCII.GetBytes(s); - // zero-initialize then copy up to Size + // zero-initialize then encode into span up to Size-1, ensure explicit null-termination + // SimConnect expects fixed-size, null-terminated ANSI strings. + // We reserve the last byte for '\0' when Size > 0 to avoid losing the terminator. Span tmp = stackalloc byte[this.Size]; - var copyLen = Math.Min(bytes.Length, this.Size); - bytes.AsSpan(0, copyLen).CopyTo(tmp); - Marshal.Copy(tmp.ToArray(), 0, addr, this.Size); + if (this.Size > 0) + { + var dest = tmp[..(this.Size - 1)]; + _ = System.Text.Encoding.Latin1.GetBytes(s.AsSpan(), dest); + + // Explicitly set terminator even though tmp is zeroed by default + tmp[this.Size - 1] = 0; + } + + // Copy without allocating an intermediate array + for (int i = 0; i < this.Size; i++) + { + Marshal.WriteByte(addr, i, tmp[i]); + } + break; } diff --git a/src/SimConnect.NET/SimVar/Internal/SimVarFieldWriterFactory.cs b/src/SimConnect.NET/SimVar/Internal/SimVarFieldWriterFactory.cs index 90e74c0..deb39ec 100644 --- a/src/SimConnect.NET/SimVar/Internal/SimVarFieldWriterFactory.cs +++ b/src/SimConnect.NET/SimVar/Internal/SimVarFieldWriterFactory.cs @@ -25,7 +25,7 @@ public static (List> Writers, int TotalSize) Build( var fields = GetOrderedSimVarFields(t); if (fields.Count == 0) { - throw new InvalidOperationException($"Type {t.FullName} has no fields with [SimVar]."); + throw new InvalidOperationException($"Type {t.FullName} has no fields with [SimConnect]."); } var writers = new List>(fields.Count); @@ -195,8 +195,6 @@ private static Delegate BuildExtractor(Type structType, FieldInfo fi, Type rawTy { valueExpr = Expression.Convert(valueExpr, underlying); } - - destFieldType = underlying; } // Finally convert to rawType diff --git a/src/SimConnect.NET/SimVar/Internal/SimVarRequest.cs b/src/SimConnect.NET/SimVar/Internal/SimVarRequest.cs index 4f80b03..b4c327d 100644 --- a/src/SimConnect.NET/SimVar/Internal/SimVarRequest.cs +++ b/src/SimConnect.NET/SimVar/Internal/SimVarRequest.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using SimConnect.NET; +using SimConnect.NET.Internal; namespace SimConnect.NET.SimVar.Internal { @@ -96,10 +97,12 @@ public void SetResult(T result) // Invoke the user-provided callback with the received value. state.Callback(state.Value); } - catch + catch (Exception callbackEx) when (!ExceptionHelper.IsCritical(callbackEx)) { - // Swallow exceptions from user callbacks to avoid breaking the message loop. - // User code errors should not affect SimConnect processing. + if (SimConnectLogger.IsLevelEnabled(SimConnectLogger.LogLevel.Debug)) + { + SimConnectLogger.Debug($"SimVar callback suppressed exception: {callbackEx.Message}"); + } } }, (cb, result), @@ -137,7 +140,7 @@ public void SetResultBoxed(object? value) this.SetResult(converted!); } - catch (Exception ex) + catch (Exception ex) when (!ExceptionHelper.IsCritical(ex)) { this.SetException(ex); } diff --git a/src/SimConnect.NET/SimVar/SimVarManager.cs b/src/SimConnect.NET/SimVar/SimVarManager.cs index c590489..f4fbff0 100644 --- a/src/SimConnect.NET/SimVar/SimVarManager.cs +++ b/src/SimConnect.NET/SimVar/SimVarManager.cs @@ -7,6 +7,7 @@ using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; +using SimConnect.NET.Internal; using SimConnect.NET.SimVar.Internal; namespace SimConnect.NET.SimVar @@ -25,6 +26,7 @@ public sealed class SimVarManager : IDisposable private readonly ConcurrentDictionary<(string Name, string Unit, SimConnectDataType DataType), uint> dataDefinitions = new(); private readonly ConcurrentDictionary typeToDefIndex = new(); private readonly ConcurrentDictionary> defToParser = new(); + private readonly ConcurrentDictionary defToWriter = new(); private readonly ConcurrentDictionary subscriptions = new(); private readonly object typeDefinitionSync = new(); @@ -100,7 +102,7 @@ public async Task SetAsync(string simVarName, string unit, T value, uint obje { ObjectDisposedException.ThrowIf(this.disposed, nameof(SimVarManager)); ArgumentException.ThrowIfNullOrEmpty(simVarName); - ArgumentException.ThrowIfNullOrEmpty(unit); + ArgumentNullException.ThrowIfNull(unit); // Try to get definition from registry first var definition = SimVarRegistry.Get(simVarName); @@ -219,6 +221,12 @@ public void ProcessReceivedData(IntPtr data, uint dataSize) return; } + if (request is null) + { + SimConnectLogger.Warning($"Pending request entry for RequestId={requestId} was null"); + return; + } + var definitionId = objectData.DefineId; if (!this.defToParser.TryGetValue(definitionId, out var parse)) { @@ -239,7 +247,7 @@ public void ProcessReceivedData(IntPtr data, uint dataSize) { parse(dataPtr, request); } - catch (Exception ex) + catch (Exception ex) when (!ExceptionHelper.IsCritical(ex)) { SimConnectLogger.Error($"Error completing SimVar request (RequestId={requestId}, DefinitionId={definitionId})", ex); if (request is ISimVarRequest req) @@ -265,7 +273,7 @@ public void ProcessReceivedData(IntPtr data, uint dataSize) break; } } - catch (Exception ex) + catch (Exception ex) when (!ExceptionHelper.IsCritical(ex)) { // Log error but don't throw - this shouldn't break the message processing loop SimConnectLogger.Error("Error processing SimVar data", ex); @@ -353,7 +361,7 @@ public void Dispose() { kvp.Value.Dispose(); } - catch (Exception ex) + catch (Exception ex) when (!ExceptionHelper.IsCritical(ex)) { if (SimConnectLogger.IsLevelEnabled(SimConnectLogger.LogLevel.Debug)) { @@ -430,6 +438,18 @@ internal uint EnsureTypeDefinition(CancellationToken cancellationToken) } }; this.typeToDefIndex[typeof(T)] = definitionId; + + var writerBuild = SimVarFieldWriterFactory.Build(addToDefinition: null); + Action write = (basePtr, v) => + { + foreach (var w in writerBuild.Writers) + { + w.WriteFrom(in v, basePtr); + } + }; + + this.defToWriter[definitionId] = (write, writerBuild.TotalSize); + return definitionId; } } @@ -461,7 +481,7 @@ internal void CancelRequest(ISimVarRequest request) this.RequestDataOnSimObject(request.RequestId, request.DefinitionId, request.ObjectId, SimConnectPeriod.Never); } - catch (Exception ex) + catch (Exception ex) when (!ExceptionHelper.IsCritical(ex)) { if (SimConnectLogger.IsLevelEnabled(SimConnectLogger.LogLevel.Debug)) { @@ -476,6 +496,7 @@ internal void CancelRequest(ISimVarRequest request) private static SimConnectDataType InferDataType() { var type = typeof(T); + ArgumentNullException.ThrowIfNull(type); return type switch { @@ -484,6 +505,7 @@ private static SimConnectDataType InferDataType() Type t when t == typeof(float) => SimConnectDataType.FloatSingle, Type t when t == typeof(double) => SimConnectDataType.FloatDouble, Type t when t == typeof(string) => SimConnectDataType.String256, // Default string size + Type t when t == typeof(SimConnectDataInitPosition) => SimConnectDataType.InitPosition, Type t when t == typeof(SimConnectDataLatLonAlt) => SimConnectDataType.LatLonAlt, Type t when t == typeof(SimConnectDataXyz) => SimConnectDataType.Xyz, _ => throw new ArgumentException($"Unsupported type for SimVar: {type.Name}"), @@ -662,7 +684,7 @@ private SimVarRequest StartRequest( this.RequestDataOnSimObject(requestId, definitionId, objectId, period); return request; } - catch + catch (Exception ex) when (!ExceptionHelper.IsCritical(ex)) { this.pendingRequests.TryRemove(requestId, out _); throw; @@ -938,28 +960,33 @@ private async Task SetStructAsync(uint definitionId, T value, uint objectId, { cancellationToken.ThrowIfCancellationRequested(); - // Build writers without re-adding to definition; EnsureTypeDefinition already registered it. - var (writers, totalSize) = SimVarFieldWriterFactory.Build(addToDefinition: null); + // Use cached write delegate/layout keyed by definitionId (must exist if EnsureTypeDefinition was used) + if (!this.defToWriter.TryGetValue(definitionId, out var cache)) + { + throw new InvalidOperationException($"No struct writer found for DefinitionId={definitionId}. EnsureTypeDefinition must be called first."); + } await Task.Run( () => { - var dataPtr = Marshal.AllocHGlobal(totalSize); + var dataPtr = Marshal.AllocHGlobal(cache.TotalSize); try { - // Fill the buffer in the same order/sizes as the definition - foreach (var w in writers) + // Fill the buffer using the cached writer delegate for this definition + if (cache.Write is not Action write) { - w.WriteFrom(in value, dataPtr); + throw new InvalidOperationException($"Cached writer has unexpected type for DefinitionId={definitionId} and T={typeof(T).Name}."); } + write(dataPtr, value); + var hr = SimConnectNative.SimConnect_SetDataOnSimObject( this.simConnectHandle, definitionId, objectId, 0, 1, - (uint)totalSize, + (uint)cache.TotalSize, dataPtr); if (hr != (int)SimConnectError.None) diff --git a/tests/SimConnect.NET.Tests.Net8/Tests/AIObjectTests.cs b/tests/SimConnect.NET.Tests.Net8/Tests/AIObjectTests.cs index 51bcecd..254f595 100644 --- a/tests/SimConnect.NET.Tests.Net8/Tests/AIObjectTests.cs +++ b/tests/SimConnect.NET.Tests.Net8/Tests/AIObjectTests.cs @@ -124,10 +124,10 @@ private static async Task TestMultipleObjects(SimConnectClient client, Can { var position = new SimConnectDataInitPosition { - Latitude = currentLat + (0.001 * (i + 1)), - Longitude = currentLon + (0.001 * (i + 1)), + Latitude = currentLat + (0.001 * (double)(i + 1)), + Longitude = currentLon + (0.001 * (double)(i + 1)), Altitude = 100, - Heading = 90 + (i * 30), + Heading = 90.0 + ((double)i * 30.0), OnGround = 1, Airspeed = 0, }; diff --git a/tests/SimConnect.NET.Tests.Net8/Tests/ConnectionTests.cs b/tests/SimConnect.NET.Tests.Net8/Tests/ConnectionTests.cs index d3e54ec..6496ada 100644 --- a/tests/SimConnect.NET.Tests.Net8/Tests/ConnectionTests.cs +++ b/tests/SimConnect.NET.Tests.Net8/Tests/ConnectionTests.cs @@ -36,8 +36,8 @@ public async Task RunAsync(SimConnectClient client, CancellationToken canc // Test that we can access managers var simVars = client.SimVars; - var aircraft = client.Aircraft; - var aiObjects = client.AIObjects; + _ = client.Aircraft; + _ = client.AIObjects; Console.WriteLine(" ✅ All managers accessible"); diff --git a/tests/SimConnect.NET.Tests.Net8/Tests/TestRunner.cs b/tests/SimConnect.NET.Tests.Net8/Tests/TestRunner.cs index 2276b69..7f2c83d 100644 --- a/tests/SimConnect.NET.Tests.Net8/Tests/TestRunner.cs +++ b/tests/SimConnect.NET.Tests.Net8/Tests/TestRunner.cs @@ -3,6 +3,7 @@ // using System.Diagnostics; +using System.Linq; using System.Runtime.InteropServices; using SimConnect.NET; @@ -53,32 +54,19 @@ public static async Task Main(string[] args) var runner = new TestRunner(); - try - { - await runner.RunAllTestsAsync(args); - } - catch (Exception ex) + using (var client = new SimConnectClient("SimConnect.NET Enhanced Test Suite")) { - Console.WriteLine($"💥 Fatal error: {ex.Message}"); - Environment.Exit(1); - } - finally - { - // Ensure cleanup - if (runner.client?.IsConnected == true) + runner.client = client; + + try { - try - { - await runner.client.DisconnectAsync(); - Console.WriteLine("🔌 Disconnected from SimConnect"); - } - catch (Exception ex) - { - Console.WriteLine($"⚠️ Error during disconnect: {ex.Message}"); - } + await runner.RunAllTestsAsync(args); + } + catch (Exception ex) + { + Console.WriteLine($"💥 Fatal error: {ex.Message}"); + Environment.Exit(1); } - - runner.client?.Dispose(); } Console.WriteLine(); @@ -116,9 +104,9 @@ public async Task RunAllTestsAsync(string[] args) var results = new List(); // Run tests - foreach (var test in testsToRun) + foreach (var resultTask in testsToRun.Select(test => this.RunSingleTestAsync(test))) { - var result = await this.RunSingleTestAsync(test); + var result = await resultTask; results.Add(result); if (!result.Passed && options.StopOnFirstFailure) @@ -219,8 +207,7 @@ private async Task ConnectToSimConnectAsync() try { Console.WriteLine("🔌 Connecting to SimConnect..."); - this.client = new SimConnectClient("SimConnect.NET Enhanced Test Suite"); - await this.client.ConnectAsync(); + await this.client!.ConnectAsync(); if (!this.client.IsConnected) {