From f5446415a14f073f41fcf5479954bd5269d0809c Mon Sep 17 00:00:00 2001 From: bstudtma Date: Sun, 19 Oct 2025 19:57:12 -0500 Subject: [PATCH] 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...");