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...");