Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<TStruct>` 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
Expand Down
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<Version>0.1.16-beta</Version>
<Version>0.1.17</Version>
<Authors>BARS</Authors>
<Company>BARS</Company>
<Product>SimConnect.NET</Product>
Expand Down
19 changes: 8 additions & 11 deletions src/SimConnect.NET/AI/SimObjectType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// Copyright (c) BARS. All rights reserved.
// </copyright>

using System.Linq;

namespace SimConnect.NET.AI
{
/// <summary>
Expand All @@ -25,17 +27,12 @@ public static IEnumerable<string> 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<string>();
}

/// <summary>
Expand Down
22 changes: 12 additions & 10 deletions src/SimConnect.NET/InputEvents/InputEventManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using System.Threading;
using System.Threading.Tasks;
using SimConnect.NET.Events;
using SimConnect.NET.Internal;

namespace SimConnect.NET.InputEvents
{
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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
Expand All @@ -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);

Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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;
}

Expand All @@ -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);
}
Expand Down Expand Up @@ -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);
}
Expand Down
26 changes: 15 additions & 11 deletions src/SimConnect.NET/InputEvents/InputEventValue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// </copyright>

using System;
using System.Diagnostics;
using System.Globalization;

namespace SimConnect.NET.InputEvents
Expand Down Expand Up @@ -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;
}

/// <summary>
Expand All @@ -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;
}

/// <summary>
Expand Down
32 changes: 32 additions & 0 deletions src/SimConnect.NET/Internal/ExceptionHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// <copyright file="ExceptionHelper.cs" company="BARS">
// Copyright (c) BARS. All rights reserved.
// </copyright>

using System;

namespace SimConnect.NET.Internal
{
/// <summary>
/// Provides helpers for determining whether an exception is critical enough to rethrow.
/// </summary>
internal static class ExceptionHelper
{
/// <summary>
/// Determines whether the supplied exception is considered critical and should not be swallowed.
/// </summary>
/// <param name="exception">The exception to inspect.</param>
/// <returns><c>true</c> if the exception is critical; otherwise, <c>false</c>.</returns>
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;
}
}
}
32 changes: 21 additions & 11 deletions src/SimConnect.NET/SimConnectClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -293,6 +294,10 @@ public async Task DisconnectAsync()
}
catch (OperationCanceledException)
{
if (SimConnectLogger.IsLevelEnabled(SimConnectLogger.LogLevel.Debug))
{
SimConnectLogger.Debug("Message processing task canceled during disconnect.");
}
}
}

Expand All @@ -309,6 +314,10 @@ public async Task DisconnectAsync()
}
catch (OperationCanceledException)
{
if (SimConnectLogger.IsLevelEnabled(SimConnectLogger.LogLevel.Debug))
{
SimConnectLogger.Debug("Reconnect task canceled during disconnect.");
}
}
}

Expand Down Expand Up @@ -360,12 +369,9 @@ public async Task<bool> 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;
Expand All @@ -385,7 +391,7 @@ public async Task<bool> 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}");
}
Expand Down Expand Up @@ -445,7 +451,7 @@ public async Task<bool> TestConnectionAsync(CancellationToken cancellationToken
await this.SimVars.GetAsync<double>("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;
Expand Down Expand Up @@ -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);
}
Expand All @@ -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");
Expand All @@ -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);
}
Expand Down Expand Up @@ -598,6 +604,10 @@ private async Task StartMessageProcessingLoopAsync(CancellationToken cancellatio
}
catch (OperationCanceledException)
{
if (SimConnectLogger.IsLevelEnabled(SimConnectLogger.LogLevel.Debug))
{
SimConnectLogger.Debug("Message processing loop canceled.");
}
}
}

Expand Down Expand Up @@ -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}");
Expand Down
Loading
Loading