From f5446415a14f073f41fcf5479954bd5269d0809c Mon Sep 17 00:00:00 2001 From: bstudtma Date: Sun, 19 Oct 2025 19:57:12 -0500 Subject: [PATCH 1/2] Add struct-based SimVar set functionality Introduces IFieldWriter, SimVarFieldWriter, and SimVarFieldWriterFactory to enable writing SimVar-annotated structs into unmanaged buffers. Adds SimVarManager.SetAsync and SetStructAsync for setting multiple SimVars in one call using a struct. Updates tests to verify struct-based SimVar setting and restoration. --- .../SimVar/Internal/IFieldWriter.cs | 32 +++ .../SimVar/Internal/SimVarFieldWriter.cs | 125 +++++++++++ .../Internal/SimVarFieldWriterFactory.cs | 212 ++++++++++++++++++ src/SimConnect.NET/SimVar/SimVarManager.cs | 72 ++++++ .../Tests/SimVarTests.cs | 49 ++++ 5 files changed, 490 insertions(+) create mode 100644 src/SimConnect.NET/SimVar/Internal/IFieldWriter.cs create mode 100644 src/SimConnect.NET/SimVar/Internal/SimVarFieldWriter.cs create mode 100644 src/SimConnect.NET/SimVar/Internal/SimVarFieldWriterFactory.cs diff --git a/src/SimConnect.NET/SimVar/Internal/IFieldWriter.cs b/src/SimConnect.NET/SimVar/Internal/IFieldWriter.cs new file mode 100644 index 0000000..a84c412 --- /dev/null +++ b/src/SimConnect.NET/SimVar/Internal/IFieldWriter.cs @@ -0,0 +1,32 @@ +// +// Copyright (c) BARS. All rights reserved. +// + +using System; + +namespace SimConnect.NET.SimVar.Internal +{ + /// + /// Writes a single annotated field from a struct into a contiguous unmanaged buffer. + /// + /// Struct type containing SimVar-annotated fields. + internal interface IFieldWriter + where T : struct + { + /// Gets or sets the byte offset of this field's payload within the packed buffer. + int OffsetBytes { get; set; } + + /// Gets or sets the size in bytes of this field's payload in the packed buffer. + int Size { get; set; } + + /// Gets or sets the effective SimConnect data type used for marshaling this field. + SimConnectDataType DataType { get; set; } + + /// + /// Writes the field value from the given struct into the buffer at OffsetBytes. + /// + /// Struct source value. + /// Base pointer to the packed buffer. + void WriteFrom(in T source, IntPtr basePtr); + } +} diff --git a/src/SimConnect.NET/SimVar/Internal/SimVarFieldWriter.cs b/src/SimConnect.NET/SimVar/Internal/SimVarFieldWriter.cs new file mode 100644 index 0000000..edc9a23 --- /dev/null +++ b/src/SimConnect.NET/SimVar/Internal/SimVarFieldWriter.cs @@ -0,0 +1,125 @@ +// +// Copyright (c) BARS. All rights reserved. +// + +using System; +using System.Runtime.InteropServices; + +namespace SimConnect.NET.SimVar.Internal +{ + internal sealed class SimVarFieldWriter : IFieldWriter + where T : struct + { + public int OffsetBytes { get; set; } + + public int Size { get; set; } + + public SimConnectDataType DataType { get; set; } + + // Holds a typed extractor matching the field type, e.g. Func + public Delegate Extractor { get; set; } = default!; + + public void WriteFrom(in T source, IntPtr basePtr) + { + var addr = IntPtr.Add(basePtr, this.OffsetBytes); + switch (this.DataType) + { + case SimConnectDataType.FloatDouble: + { + var getter = (Func)this.Extractor; + double v = getter(source); + var bytes = BitConverter.GetBytes(v); + Marshal.Copy(bytes, 0, addr, 8); + break; + } + + case SimConnectDataType.FloatSingle: + { + var getter = (Func)this.Extractor; + float v = getter(source); + var bytes = BitConverter.GetBytes(v); + Marshal.Copy(bytes, 0, addr, 4); + break; + } + + case SimConnectDataType.Integer64: + { + var getter = (Func)this.Extractor; + long v = getter(source); + Marshal.WriteInt64(addr, v); + break; + } + + case SimConnectDataType.Integer32: + { + var getter = (Func)this.Extractor; + int v = getter(source); + Marshal.WriteInt32(addr, v); + break; + } + + case SimConnectDataType.String8: + case SimConnectDataType.String32: + case SimConnectDataType.String64: + case SimConnectDataType.String128: + case SimConnectDataType.String256: + case SimConnectDataType.String260: + { + 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 + 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); + break; + } + + case SimConnectDataType.InitPosition: + { + var getter = (Func)this.Extractor; + var v = getter(source); + Marshal.StructureToPtr(v, addr, fDeleteOld: false); + break; + } + + case SimConnectDataType.MarkerState: + { + var getter = (Func)this.Extractor; + var v = getter(source); + Marshal.StructureToPtr(v, addr, fDeleteOld: false); + break; + } + + case SimConnectDataType.Waypoint: + { + var getter = (Func)this.Extractor; + var v = getter(source); + Marshal.StructureToPtr(v, addr, fDeleteOld: false); + break; + } + + case SimConnectDataType.LatLonAlt: + { + var getter = (Func)this.Extractor; + var v = getter(source); + Marshal.StructureToPtr(v, addr, fDeleteOld: false); + break; + } + + case SimConnectDataType.Xyz: + { + var getter = (Func)this.Extractor; + var v = getter(source); + Marshal.StructureToPtr(v, addr, fDeleteOld: false); + break; + } + + default: + throw new NotSupportedException($"Unsupported SimConnectDataType {this.DataType}"); + } + } + } +} diff --git a/src/SimConnect.NET/SimVar/Internal/SimVarFieldWriterFactory.cs b/src/SimConnect.NET/SimVar/Internal/SimVarFieldWriterFactory.cs new file mode 100644 index 0000000..90e74c0 --- /dev/null +++ b/src/SimConnect.NET/SimVar/Internal/SimVarFieldWriterFactory.cs @@ -0,0 +1,212 @@ +// +// Copyright (c) BARS. All rights reserved. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.InteropServices; + +namespace SimConnect.NET.SimVar.Internal +{ + internal static class SimVarFieldWriterFactory + { + /// + /// Builds field writers for a struct T and optionally adds each field to a SimConnect definition. + /// Mirrors the reader factory logic to ensure identical packing order and sizes. + /// + public static (List> Writers, int TotalSize) Build( + Action? addToDefinition = null) + where T : struct + { + var t = typeof(T); + var fields = GetOrderedSimVarFields(t); + if (fields.Count == 0) + { + throw new InvalidOperationException($"Type {t.FullName} has no fields with [SimVar]."); + } + + var writers = new List>(fields.Count); + int offset = 0; + + foreach (var (field, simVar) in fields) + { + if (field == null) + { + throw new InvalidOperationException("FieldInfo is null in SimVarFieldWriterFactory.Build."); + } + + if (simVar == null) + { + throw new InvalidOperationException($"SimConnectAttribute is null for field '{field.Name}' in SimVarFieldWriterFactory.Build."); + } + + // Determine effective data type matching the reader factory rules + SimConnectDataType effectiveDataType; + if (simVar.DataType.HasValue) + { + effectiveDataType = simVar.DataType.Value; + } + else + { + var ft = field.FieldType; + var nullableUnderlying = Nullable.GetUnderlyingType(ft); + if (nullableUnderlying != null) + { + ft = nullableUnderlying; + } + + if (ft.IsEnum) + { + ft = Enum.GetUnderlyingType(ft); + } + + effectiveDataType = ft switch + { + _ when ft == typeof(double) => SimConnectDataType.FloatDouble, + _ when ft == typeof(float) => SimConnectDataType.FloatSingle, + _ when ft == typeof(long) || ft == typeof(ulong) => SimConnectDataType.Integer64, + _ when ft == typeof(int) || ft == typeof(uint) || + ft == typeof(short) || ft == typeof(ushort) || + ft == typeof(byte) || ft == typeof(sbyte) || + ft == typeof(bool) => SimConnectDataType.Integer32, + _ when ft == typeof(SimConnectDataInitPosition) => SimConnectDataType.InitPosition, + _ when ft == typeof(SimConnectDataMarkerState) => SimConnectDataType.MarkerState, + _ when ft == typeof(SimConnectDataWaypoint) => SimConnectDataType.Waypoint, + _ when ft == typeof(SimConnectDataLatLonAlt) => SimConnectDataType.LatLonAlt, + _ when ft == typeof(SimConnectDataXyz) => SimConnectDataType.Xyz, + _ when ft == typeof(string) => SimConnectDataType.String256, + _ => throw new NotSupportedException($"Cannot infer SimConnectDataType for field '{field.Name}' of type {ft.FullName}."), + }; + } + + addToDefinition?.Invoke(simVar.Name, simVar.Unit, effectiveDataType); + + var (dataType, rawType, size) = Classify(field, effectiveDataType); + + var writerType = typeof(SimVarFieldWriter<,>).MakeGenericType(t, field.FieldType); + var writer = Activator.CreateInstance(writerType)!; + + dynamic d = writer; + d.OffsetBytes = offset; + d.DataType = dataType; + d.Size = size; + d.Extractor = BuildExtractor(t, field, rawType); + + writers.Add((IFieldWriter)writer); + offset += size; + } + + return (writers, offset); + } + + private static List<(FieldInfo Field, SimConnectAttribute? Attr)> GetOrderedSimVarFields(Type t) + { + var fields = t.GetFields(BindingFlags.Instance | BindingFlags.Public) + .Select(f => (Field: f, Attr: f.GetCustomAttribute())) + .Where(x => x.Attr != null) + .OrderBy(x => x!.Attr!.Order) + .ThenBy(x => x.Field.MetadataToken) + .ToList(); + + return fields; + } + + private static (SimConnectDataType DataType, Type RawType, int SizeBytes) Classify(FieldInfo field, SimConnectDataType dt) + { + switch (dt) + { + case SimConnectDataType.FloatDouble: + return (SimConnectDataType.FloatDouble, typeof(double), 8); + case SimConnectDataType.FloatSingle: + return (SimConnectDataType.FloatSingle, typeof(float), 4); + case SimConnectDataType.Integer32: + return (SimConnectDataType.Integer32, typeof(int), 4); + case SimConnectDataType.Integer64: + return (SimConnectDataType.Integer64, typeof(long), 8); + case SimConnectDataType.String8: + return (SimConnectDataType.String8, typeof(string), 8); + case SimConnectDataType.String32: + return (SimConnectDataType.String32, typeof(string), 32); + case SimConnectDataType.String64: + return (SimConnectDataType.String64, typeof(string), 64); + case SimConnectDataType.String128: + return (SimConnectDataType.String128, typeof(string), 128); + case SimConnectDataType.String256: + return (SimConnectDataType.String256, typeof(string), 256); + case SimConnectDataType.String260: + return (SimConnectDataType.String260, typeof(string), 260); + case SimConnectDataType.InitPosition: + return (SimConnectDataType.InitPosition, typeof(SimConnectDataInitPosition), Marshal.SizeOf()); + case SimConnectDataType.MarkerState: + return (SimConnectDataType.MarkerState, typeof(SimConnectDataMarkerState), Marshal.SizeOf()); + case SimConnectDataType.Waypoint: + return (SimConnectDataType.Waypoint, typeof(SimConnectDataWaypoint), Marshal.SizeOf()); + case SimConnectDataType.LatLonAlt: + return (SimConnectDataType.LatLonAlt, typeof(SimConnectDataLatLonAlt), Marshal.SizeOf()); + case SimConnectDataType.Xyz: + return (SimConnectDataType.Xyz, typeof(SimConnectDataXyz), Marshal.SizeOf()); + default: + throw new NotSupportedException($"{field.DeclaringType!.FullName}.{field.Name}: unsupported SimConnectDataType {dt}"); + } + } + + /// + /// Builds an extractor that returns the field value converted to the requested raw type. + /// + private static Delegate BuildExtractor(Type structType, FieldInfo fi, Type rawType) + { + // param: T s + var s = Expression.Parameter(structType, "s"); + + // access field: s.Field + var fieldExpr = Expression.Field(s, fi); + + // If the field type equals rawType -> identity + if (fi.FieldType == rawType) + { + var lambdaTypeId = typeof(Func<,>).MakeGenericType(structType, rawType); + return Expression.Lambda(lambdaTypeId, fieldExpr, s).Compile(); + } + + // If field is Nullable, unwrap .Value or default + Type destFieldType = fi.FieldType; + var nullableUnderlying = Nullable.GetUnderlyingType(destFieldType); + Expression valueExpr = fieldExpr; + if (nullableUnderlying != null) + { + // coalesce: field.HasValue ? field.Value : default(U) + var valueProp = Expression.Property(fieldExpr, "Value"); + var defaultValue = Expression.Default(nullableUnderlying); + valueExpr = Expression.Condition( + Expression.Property(fieldExpr, "HasValue"), + valueProp, + defaultValue); + destFieldType = nullableUnderlying; + } + + // Enums -> convert to underlying integral type first + if (destFieldType.IsEnum) + { + var underlying = Enum.GetUnderlyingType(destFieldType); + if (valueExpr.Type != underlying) + { + valueExpr = Expression.Convert(valueExpr, underlying); + } + + destFieldType = underlying; + } + + // Finally convert to rawType + if (valueExpr.Type != rawType) + { + valueExpr = Expression.Convert(valueExpr, rawType); + } + + var lambdaType = typeof(Func<,>).MakeGenericType(structType, rawType); + return Expression.Lambda(lambdaType, valueExpr, s).Compile(); + } + } +} diff --git a/src/SimConnect.NET/SimVar/SimVarManager.cs b/src/SimConnect.NET/SimVar/SimVarManager.cs index cc9b010..c86812f 100644 --- a/src/SimConnect.NET/SimVar/SimVarManager.cs +++ b/src/SimConnect.NET/SimVar/SimVarManager.cs @@ -128,6 +128,33 @@ public async Task SetAsync(string simVarName, string unit, T value, uint obje await this.SetWithDefinitionAsync(dynamicDefinition, value, objectId, cancellationToken).ConfigureAwait(false); } + /// + /// Sets multiple SimVars in one call by passing a struct annotated with fields. + /// This mirrors the struct-based GetAsync and uses the same definition/layout. + /// + /// The struct type to write. Must have public fields annotated with . + /// The struct instance whose fields should be written. + /// The SimConnect object ID (defaults to user aircraft). + /// Cancellation token. + /// A task that represents the asynchronous set operation. + public async Task SetAsync( + T value, + uint objectId = SimConnectObjectIdUser, + CancellationToken cancellationToken = default) + where T : struct + { + ObjectDisposedException.ThrowIf(this.disposed, nameof(SimVarManager)); + cancellationToken.ThrowIfCancellationRequested(); + + if (this.simConnectHandle == IntPtr.Zero) + { + throw new InvalidOperationException("SimConnect handle is not initialized."); + } + + var defId = this.EnsureTypeDefinition(cancellationToken); + await this.SetStructAsync(defId, value, objectId, cancellationToken).ConfigureAwait(false); + } + /// /// Gets a full struct from SimConnect as a strongly-typed object using a dynamically built data definition. /// @@ -881,5 +908,50 @@ private uint EnsureScalarDefinition(string name, string? unit = null, SimConnect this.dataDefinitions[key] = definitionId; return definitionId; } + + /// + /// 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) + where T : struct + { + cancellationToken.ThrowIfCancellationRequested(); + + // Build writers without re-adding to definition; EnsureTypeDefinition already registered it. + var (writers, totalSize) = SimVarFieldWriterFactory.Build(addToDefinition: null); + + await Task.Run( + () => + { + var dataPtr = Marshal.AllocHGlobal(totalSize); + try + { + // Fill the buffer in the same order/sizes as the definition + foreach (var w in writers) + { + w.WriteFrom(in value, dataPtr); + } + + var hr = SimConnectNative.SimConnect_SetDataOnSimObject( + this.simConnectHandle, + definitionId, + objectId, + 0, + 1, + (uint)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); + } } } diff --git a/tests/SimConnect.NET.Tests.Net8/Tests/SimVarTests.cs b/tests/SimConnect.NET.Tests.Net8/Tests/SimVarTests.cs index a79c520..e58c48c 100644 --- a/tests/SimConnect.NET.Tests.Net8/Tests/SimVarTests.cs +++ b/tests/SimConnect.NET.Tests.Net8/Tests/SimVarTests.cs @@ -47,6 +47,12 @@ public async Task RunAsync(SimConnectClient client, CancellationToken canc return false; } + // Test setting position via struct + if (!await TestPositionStructSetAsync(client, cts.Token)) + { + return false; + } + // Test rapid consecutive requests if (!await TestRapidRequests(client, cts.Token)) { @@ -166,6 +172,49 @@ private static async Task TestSimVarSetting(SimConnectClient client, Cance return true; } + private static async Task TestPositionStructSetAsync(SimConnectClient client, CancellationToken cancellationToken) + { + Console.WriteLine(" 🔧 Testing SetAsync with Position struct (altitude +15 ft, then restore)..."); + + // Get current position (includes altitude) + var original = await client.SimVars.GetAsync(cancellationToken: cancellationToken); + Console.WriteLine($" Original Altitude: {original.Altitude:F2} ft"); + + var target = original; + target.Altitude = original.Altitude + 15.0; + + // Attempt to set new altitude via struct + await client.SimVars.SetAsync(target, cancellationToken: cancellationToken); + + var afterSet = await client.SimVars.GetAsync(cancellationToken: cancellationToken); + Console.WriteLine($" After Set Altitude: {afterSet.Altitude:F2} ft (target {target.Altitude:F2} ft)"); + + // Tolerance for sim update/drift + const double toleranceFt = 10.0; + var deltaAfterSet = Math.Abs(afterSet.Altitude - target.Altitude); + if (deltaAfterSet > toleranceFt) + { + Console.WriteLine($" ❌ Altitude not within tolerance after set. Δ={deltaAfterSet:F2} ft"); + return false; + } + + // Restore original altitude + await client.SimVars.SetAsync(original, cancellationToken: cancellationToken); + + var restored = await client.SimVars.GetAsync(cancellationToken: cancellationToken); + Console.WriteLine($" Restored Altitude: {restored.Altitude:F2} ft (expected {original.Altitude:F2} ft)"); + + var deltaRestore = Math.Abs(restored.Altitude - original.Altitude); + if (deltaRestore > toleranceFt) + { + Console.WriteLine($" ❌ Altitude not restored within tolerance. Δ={deltaRestore:F2} ft"); + return false; + } + + Console.WriteLine(" ✅ Position struct SetAsync OK"); + return true; + } + private static async Task TestRapidRequests(SimConnectClient client, CancellationToken cancellationToken) { Console.WriteLine(" 🔍 Testing rapid consecutive requests..."); From 919da710571b60a11eb3bfc8ea186769505e5412 Mon Sep 17 00:00:00 2001 From: bstudtma Date: Mon, 20 Oct 2025 20:58:40 -0500 Subject: [PATCH 2/2] Improve SimConnect struct writing and error handling Refactored string field writing to ensure explicit null-termination and use Latin1 encoding for SimConnect compatibility. Added caching of struct writers and total size in SimVarManager for efficient SetDataOnSimObject calls. Improved error handling and updated attribute references from [SimVar] to [SimConnect]. --- .../Internal/SimVarFieldReaderFactory.cs | 4 +- .../SimVar/Internal/SimVarFieldWriter.cs | 23 +++++++-- .../Internal/SimVarFieldWriterFactory.cs | 2 +- src/SimConnect.NET/SimVar/SimVarManager.cs | 47 +++++++++++++------ 4 files changed, 54 insertions(+), 22 deletions(-) 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..169eb9e 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); diff --git a/src/SimConnect.NET/SimVar/SimVarManager.cs b/src/SimConnect.NET/SimVar/SimVarManager.cs index c86812f..1c72716 100644 --- a/src/SimConnect.NET/SimVar/SimVarManager.cs +++ b/src/SimConnect.NET/SimVar/SimVarManager.cs @@ -25,6 +25,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 +101,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); @@ -427,6 +428,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; } } @@ -474,6 +487,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}"), @@ -593,15 +607,15 @@ private static string ParseString(IntPtr dataPtr, SimConnectDataType dataType) return SimVarMemoryReader.ReadFixedString(dataPtr, maxLength); } - /// - /// Wrapper to call SimConnect_RequestDataOnSimObject with consistent error handling. - /// A local context string is generated from the parameters for logging and exception messages. - /// + /// + /// Wrapper to call SimConnect_RequestDataOnSimObject with consistent error handling. + /// A local context string is generated from the parameters for logging and exception messages. + /// /// The SimConnect request identifier. /// The data definition identifier. /// The target object identifier. - /// The request period. - /// Throws a SimConnectException on error (except when period == Never which is used internally for cancellation). + /// The request period. + /// Throws a SimConnectException on error (except when period == Never which is used internally for cancellation). private void RequestDataOnSimObject( uint requestId, uint definitionId, @@ -917,28 +931,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)