From d428f10ca2fcc2471ffabd6edaabd7a54ec51556 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Kaiser?= Date: Fri, 13 Feb 2026 12:27:54 +0100 Subject: [PATCH 1/2] Patch up war thunder structs --- Examples/WarThunderExample.cs | 24 +- GamesDat.Demo/Program.cs | 151 ++++++++-- GamesDat/GameSession.cs | 15 +- .../Sources/HttpPollingSourceBase.cs | 96 ++++-- .../Sources/HttpPollingSourceOptions.cs | 9 +- .../Sources/WarThunder/IndicatorsData.cs | 281 ++++++++++++++++-- .../Telemetry/Sources/WarThunder/StateData.cs | 266 +++++++++++++---- .../Sources/WarThunder/StateSource.cs | 58 +++- .../Sources/WarThunder/StateSourceOptions.cs | 25 ++ mockoon/war-thunder-environment.json | 29 +- 10 files changed, 781 insertions(+), 173 deletions(-) create mode 100644 GamesDat/Telemetry/Sources/WarThunder/StateSourceOptions.cs diff --git a/Examples/WarThunderExample.cs b/Examples/WarThunderExample.cs index 3fc6d5b..c1d9432 100644 --- a/Examples/WarThunderExample.cs +++ b/Examples/WarThunderExample.cs @@ -16,9 +16,9 @@ public static async Task BasicStateRecording() await using var session = new GameSession() .AddSource(WarThunderSources.CreateStateSource()) .OnData(data => - Console.WriteLine($"Speed: {data.IndicatedAirspeed:F1} km/h, " + - $"Alt: {data.Altitude:F0}m, " + - $"Throttle: {data.Throttle * 100:F0}%")) + Console.WriteLine($"Speed: {data.IndicatedAirspeedKmh:F1} km/h, " + + $"Alt: {data.AltitudeMeters:F0}m, " + + $"Throttle: {data.Throttle1Percent:F0}%")) .AutoOutput(); Console.WriteLine("Recording War Thunder /state endpoint at 60Hz..."); @@ -35,7 +35,7 @@ public static async Task MultiEndpointRecording() .AddSource(WarThunderSources.CreateStateSource(hz: 60)) // 60Hz .AddSource(WarThunderSources.CreateIndicatorsSource(hz: 10)) // 10Hz .OnData(data => - Console.WriteLine($"[State] Speed: {data.IndicatedAirspeed:F1} km/h, Alt: {data.Altitude:F0}m")) + Console.WriteLine($"[State] Speed: {data.IndicatedAirspeedKmh:F1} km/h, Alt: {data.AltitudeMeters:F0}m")) .OnData(data => Console.WriteLine($"[Indicators] Oil: {data.OilTemp:F1}°C, Water: {data.WaterTemp:F1}°C")); @@ -55,15 +55,15 @@ public static async Task RealtimeOnlyMonitoring() { Console.Clear(); Console.WriteLine("=== War Thunder Telemetry ==="); - Console.WriteLine($"Speed (IAS): {data.IndicatedAirspeed:F1} km/h"); - Console.WriteLine($"Speed (TAS): {data.TrueAirspeed:F1} km/h"); - Console.WriteLine($"Altitude: {data.Altitude:F0} m"); + Console.WriteLine($"Speed (IAS): {data.IndicatedAirspeedKmh:F1} km/h"); + Console.WriteLine($"Speed (TAS): {data.TrueAirspeedKmh:F1} km/h"); + Console.WriteLine($"Altitude: {data.AltitudeMeters:F0} m"); Console.WriteLine($"Mach: {data.Mach:F2}"); - Console.WriteLine($"AoA: {data.AngleOfAttack:F1}°"); - Console.WriteLine($"Throttle: {data.Throttle * 100:F0}%"); - Console.WriteLine($"RPM: {data.RPM:F0}"); + Console.WriteLine($"AoA: {data.AngleOfAttackDeg:F1}°"); + Console.WriteLine($"Throttle: {data.Throttle1Percent:F0}%"); + Console.WriteLine($"RPM: {data.Rpm1:F0}"); Console.WriteLine($"G-Force: {data.Ny:F2}g"); - Console.WriteLine($"Fuel: {data.Fuel:F1} kg"); + Console.WriteLine($"Fuel: {data.FuelMassKg:F1} kg"); }); Console.WriteLine("Starting realtime War Thunder monitoring..."); @@ -88,7 +88,7 @@ public static async Task CustomConfiguration() await using var session = new GameSession() .AddSource(new StateSource(options)) .OnData(data => - Console.WriteLine($"Custom config: Speed={data.IndicatedAirspeed:F1} km/h")); + Console.WriteLine($"Custom config: Speed={data.IndicatedAirspeedKmh:F1} km/h")); Console.WriteLine("Recording with custom configuration..."); await session.StartAsync(); diff --git a/GamesDat.Demo/Program.cs b/GamesDat.Demo/Program.cs index dfa87af..9940ad2 100644 --- a/GamesDat.Demo/Program.cs +++ b/GamesDat.Demo/Program.cs @@ -20,6 +20,17 @@ static async Task Main(string[] args) #region War Thunder static async Task CaptureWarThunderSession() { + Console.WriteLine("═══════════════════════════════════════════════════════"); + Console.WriteLine(" WAR THUNDER REAL-TIME TELEMETRY MONITOR"); + Console.WriteLine("═══════════════════════════════════════════════════════"); + Console.WriteLine(); + Console.WriteLine("Waiting for War Thunder match to start..."); + Console.WriteLine("(Launch War Thunder and enter a battle)"); + Console.WriteLine(); + Console.WriteLine("Press Ctrl+C to stop"); + Console.WriteLine("═══════════════════════════════════════════════════════"); + Console.WriteLine(); + var cts = new CancellationTokenSource(); ConsoleCancelEventHandler handler = (s, e) => { @@ -28,36 +39,122 @@ static async Task CaptureWarThunderSession() }; Console.CancelKeyPress += handler; - try - { - var session = new GameSession() - .AddSource( - WarThunderSources.CreateStateSource() - .UseWriter(new BinarySessionWriter()) - .OutputTo($"./sessions/warthunder_{DateTime.UtcNow:yyyyMMdd_HHmmss}.bin")) - .OnData(data => + var lastValidData = false; + var frameCount = 0; + + var session = new GameSession() + .AddSource( + WarThunderSources.CreateStateSource(hz: 60) // 10Hz for readable console output + .UseWriter(new BinarySessionWriter()) + .OutputTo($"./sessions/warthunder_{DateTime.UtcNow:yyyyMMdd_HHmmss}.bin")) + .OnData(data => + { + frameCount++; + + // Detect match state changes + if (data.Valid && !lastValidData) { - Console.WriteLine($"Altitude: {data.Altitude} | Speed {data.TrueAirspeed} | Throttle {data.Throttle}"); - }); + Console.Clear(); + Console.WriteLine("═══════════════════════════════════════════════════════"); + Console.WriteLine(" ✈️ MATCH STARTED - LIVE TELEMETRY"); + Console.WriteLine("═══════════════════════════════════════════════════════"); + Console.WriteLine(); + } + else if (!data.Valid && lastValidData) + { + Console.Clear(); + Console.WriteLine("═══════════════════════════════════════════════════════"); + Console.WriteLine(" MATCH ENDED - Waiting for next battle..."); + Console.WriteLine("═══════════════════════════════════════════════════════"); + Console.WriteLine(); + } - await session.StartAsync(cts.Token); + lastValidData = data.Valid; - // Session is now running, wait for cancellation - await Task.Delay(Timeout.Infinite, cts.Token); - } - catch (FileNotFoundException) - { - Console.WriteLine("\nError: War Thunder is not running. Start War Thunder and try again."); - } - catch (OperationCanceledException) - { - Console.WriteLine("\nCapture stopped by user."); - } - finally - { - Console.CancelKeyPress -= handler; - cts.Dispose(); - } + if (data.Valid) + { + // Clear and redraw dashboard every frame + if (frameCount % 1 == 0) // Update every frame (10Hz) + { + Console.SetCursorPosition(0, 4); + DrawDashboard(data); + } + } + }); + + await session.StartAsync(cts.Token); + await Task.Delay(Timeout.Infinite, cts.Token); + } + + static void DrawDashboard(StateData data) + { + var engineCount = GetActiveEngineCount(data); + + Console.WriteLine("╔═══════════════════════════════════════════════════════╗"); + Console.WriteLine("║ FLIGHT PARAMETERS ║"); + Console.WriteLine("╠═══════════════════════════════════════════════════════╣"); + Console.WriteLine($"║ Altitude: {data.AltitudeMeters,8:F0} m ║"); + Console.WriteLine($"║ IAS: {data.IndicatedAirspeedKmh,8:F0} km/h ║"); + Console.WriteLine($"║ TAS: {data.TrueAirspeedKmh,8:F0} km/h ║"); + Console.WriteLine($"║ Mach: {data.Mach,8:F2} ║"); + Console.WriteLine($"║ Vertical Speed: {data.VyMs,8:F1} m/s ║"); + Console.WriteLine($"║ ║"); + Console.WriteLine($"║ AoA (Attack): {data.AngleOfAttackDeg,8:F1}° ║"); + Console.WriteLine($"║ AoS (Slip): {data.AngleOfSlipDeg,8:F1}° ║"); + Console.WriteLine($"║ G-Force: {data.Ny,8:F2} g ║"); + Console.WriteLine($"║ Roll Rate: {data.WxDegPerSec,8:F0}°/s ║"); + Console.WriteLine("╠═══════════════════════════════════════════════════════╣"); + Console.WriteLine("║ CONTROL SURFACES ║"); + Console.WriteLine("╠═══════════════════════════════════════════════════════╣"); + Console.WriteLine($"║ Aileron: {data.AileronPercent,8:F1}% {GetBar(data.AileronPercent, 100)}"); + Console.WriteLine($"║ Elevator: {data.ElevatorPercent,8:F1}% {GetBar(data.ElevatorPercent, 100)}"); + Console.WriteLine($"║ Rudder: {data.RudderPercent,8:F1}% {GetBar(data.RudderPercent, 100)}"); + Console.WriteLine($"║ Flaps: {data.FlapsPercent,8:F1}% {GetBar(data.FlapsPercent, 100)}"); + Console.WriteLine($"║ Gear: {data.GearPercent,8:F1}% {(data.GearPercent > 0 ? "DOWN ✓" : "UP")} ║"); + Console.WriteLine($"║ Airbrake: {data.AirbrakePercent,8:F1}% {(data.AirbrakePercent > 0 ? "DEPLOYED" : "RETRACTED")} ║"); + Console.WriteLine("╠═══════════════════════════════════════════════════════╣"); + Console.WriteLine($"║ ENGINE {(engineCount > 1 ? $"(1 of {engineCount})" : "STATUS")} ║"); + Console.WriteLine("╠═══════════════════════════════════════════════════════╣"); + Console.WriteLine($"║ Throttle: {data.Throttle1Percent,8:F1}% {GetBar(data.Throttle1Percent, 100)}"); + Console.WriteLine($"║ RPM: {data.Rpm1,8:F0} ║"); + Console.WriteLine($"║ Power: {data.Power1Hp,8:F1} hp ║"); + Console.WriteLine($"║ Manifold Press: {data.ManifoldPressure1Atm,8:F2} atm ║"); + Console.WriteLine($"║ Mixture: {data.Mixture1Percent,8:F1}% ║"); + Console.WriteLine($"║ Radiator: {data.Radiator1Percent,8:F1}% ║"); + Console.WriteLine($"║ Water Temp: {data.WaterTemp1C,8:F0}°C ║"); + Console.WriteLine($"║ Oil Temp: {data.OilTemp1C,8:F0}°C ║"); + Console.WriteLine($"║ Thrust: {data.Thrust1Kgs,8:F0} kgf ║"); + Console.WriteLine($"║ Prop Pitch: {data.Pitch1Deg,8:F1}° ║"); + Console.WriteLine($"║ Efficiency: {data.Efficiency1Percent,8:F1}% ║"); + Console.WriteLine("╠═══════════════════════════════════════════════════════╣"); + Console.WriteLine("║ FUEL STATUS ║"); + Console.WriteLine("╠═══════════════════════════════════════════════════════╣"); + var fuelPercent = data.FuelMassInitialKg > 0 ? (data.FuelMassKg / data.FuelMassInitialKg * 100) : 0; + Console.WriteLine($"║ Current Fuel: {data.FuelMassKg,8:F1} kg ({fuelPercent,5:F1}%) ║"); + Console.WriteLine($"║ Initial Fuel: {data.FuelMassInitialKg,8:F1} kg ║"); + Console.WriteLine($"║ Fuel Used: {(data.FuelMassInitialKg - data.FuelMassKg),8:F1} kg ║"); + Console.WriteLine("╚═══════════════════════════════════════════════════════╝"); + Console.WriteLine(); + Console.WriteLine($" Recording to file... | Press Ctrl+C to stop"); + Console.Write(" "); // Clear any extra characters + } + + static int GetActiveEngineCount(StateData data) + { + var count = 0; + if (data.Rpm1 > 0 || data.Throttle1Percent > 0) count = 1; + if (data.Rpm2 > 0 || data.Throttle2Percent > 0) count = 2; + if (data.Rpm3 > 0 || data.Throttle3Percent > 0) count = 3; + if (data.Rpm4 > 0 || data.Throttle4Percent > 0) count = 4; + return count > 0 ? count : 1; // Default to 1 + } + + static string GetBar(float value, float max, int width = 15) + { + var percent = Math.Abs(value) / max; + var filled = (int)(percent * width); + var bar = new string('█', Math.Min(filled, width)) + new string('░', Math.Max(width - filled, 0)); + return $"[{bar}]║"; } #endregion diff --git a/GamesDat/GameSession.cs b/GamesDat/GameSession.cs index 440c3e2..a7013c0 100644 --- a/GamesDat/GameSession.cs +++ b/GamesDat/GameSession.cs @@ -143,7 +143,7 @@ private async Task RunAsync(CancellationToken ct) { Console.WriteLine($"[{SourceTypeName}] Starting source..."); int frameCount = 0; - + await foreach (var data in _source.ReadContinuousAsync(ct)) { frameCount++; @@ -185,7 +185,18 @@ private async Task RunAsync(CancellationToken ct) } catch (OperationCanceledException) { - // Normal cancellation + // Check if this was expected cancellation + if (ct.IsCancellationRequested) + { + // Normal cancellation from user (Ctrl+C) + Console.WriteLine($"[{SourceTypeName}] Stopped by user"); + } + else + { + // Unexpected cancellation - this shouldn't happen + Console.WriteLine($"[{SourceTypeName}] WARNING: Unexpected cancellation (not from user request)"); + Console.WriteLine($"[{SourceTypeName}] This may indicate a premature termination issue"); + } } catch (Exception ex) { diff --git a/GamesDat/Telemetry/Sources/HttpPollingSourceBase.cs b/GamesDat/Telemetry/Sources/HttpPollingSourceBase.cs index 561bc3c..fcd3beb 100644 --- a/GamesDat/Telemetry/Sources/HttpPollingSourceBase.cs +++ b/GamesDat/Telemetry/Sources/HttpPollingSourceBase.cs @@ -12,14 +12,15 @@ namespace GamesDat.Core.Telemetry.Sources; /// Abstract base class for HTTP polling-based telemetry sources. /// Handles HTTP request lifecycle, polling loop, retry logic, and JSON deserialization. /// -/// The telemetry data type (must be unmanaged struct). -public abstract class HttpPollingSourceBase : TelemetrySourceBase where T : unmanaged +/// The telemetry data type. +public abstract class HttpPollingSourceBase : TelemetrySourceBase { private readonly HttpClient _httpClient; private readonly HttpPollingSourceOptions _options; private readonly bool _ownsClient; private int _consecutiveErrors; private TimeSpan _currentRetryDelay; + private DateTime _retryStartTime; /// /// Initializes a new instance with a provided HttpClient. @@ -52,10 +53,19 @@ public override async IAsyncEnumerable ReadContinuousAsync( { var fullUrl = _options.GetFullUrl(); var firstError = true; + var loopStartTime = DateTime.UtcNow; + + if (_options.EnableDebugLogging) + { + Console.WriteLine($"[{GetType().Name}] [DEBUG] Polling loop started at {loopStartTime:HH:mm:ss.fff}"); + Console.WriteLine($"[{GetType().Name}] [DEBUG] Target URL: {fullUrl}"); + Console.WriteLine($"[{GetType().Name}] [DEBUG] Poll interval: {_options.PollInterval.TotalMilliseconds}ms"); + Console.WriteLine($"[{GetType().Name}] [DEBUG] Cancellation requested: {cancellationToken.IsCancellationRequested}"); + } while (!cancellationToken.IsCancellationRequested) { - T? data = default; + T data = default!; bool hasData = false; Exception? errorToThrow = null; @@ -88,7 +98,17 @@ public override async IAsyncEnumerable ReadContinuousAsync( data = ParseJson(json); hasData = true; + if (_options.EnableDebugLogging) + { + Console.WriteLine($"[{GetType().Name}] [DEBUG] Poll successful at {DateTime.UtcNow:HH:mm:ss.fff}"); + } + // Success - reset error tracking + if (_consecutiveErrors > 0 && _options.EnableDebugLogging) + { + var recoveryTime = DateTime.UtcNow - _retryStartTime; + Console.WriteLine($"[{GetType().Name}] Connection recovered after {_consecutiveErrors} attempts ({recoveryTime.TotalSeconds:F1}s)"); + } _consecutiveErrors = 0; _currentRetryDelay = _options.InitialRetryDelay; firstError = true; @@ -100,29 +120,26 @@ public override async IAsyncEnumerable ReadContinuousAsync( } catch (OperationCanceledException ex) { + // Check if this was expected cancellation if (cancellationToken.IsCancellationRequested) { - // Clean cancellation requested by caller - yield break; + if (_options.EnableDebugLogging) + { + Console.WriteLine($"[{GetType().Name}] [DEBUG] Polling loop cancelled (expected) at {DateTime.UtcNow:HH:mm:ss.fff}"); + } } - - // Timeout cancellation from linkedCts (request timeout) - treat as connection error - _consecutiveErrors++; - - if (firstError) + else { - Console.WriteLine($"[{GetType().Name}] Connection error: {ex.Message}"); - Console.WriteLine($"[{GetType().Name}] Retrying with exponential backoff..."); - firstError = false; - } - - if (_consecutiveErrors >= _options.MaxConsecutiveErrors) - { - errorToThrow = new InvalidOperationException( - $"Failed to connect after {_consecutiveErrors} consecutive attempts. " + - $"Ensure the game is running and the API is accessible at {fullUrl}", - ex); + // Unexpected cancellation - log details + Console.WriteLine($"[{GetType().Name}] WARNING: Unexpected OperationCanceledException caught"); + Console.WriteLine($"[{GetType().Name}] Exception source: {ex.Source}"); + Console.WriteLine($"[{GetType().Name}] CancellationToken.IsCancellationRequested: {cancellationToken.IsCancellationRequested}"); + if (_options.EnableDebugLogging) + { + Console.WriteLine($"[{GetType().Name}] [DEBUG] Stack trace: {ex.StackTrace}"); + } } + yield break; } catch (HttpRequestException ex) { @@ -131,15 +148,26 @@ public override async IAsyncEnumerable ReadContinuousAsync( if (firstError) { - Console.WriteLine($"[{GetType().Name}] Connection error: {ex.Message}"); - Console.WriteLine($"[{GetType().Name}] Retrying with exponential backoff..."); + _retryStartTime = DateTime.UtcNow; + Console.WriteLine($"[{GetType().Name}] Connection error: {ex.GetType().Name}: {ex.Message}"); + Console.WriteLine($"[{GetType().Name}] Retrying with exponential backoff (max {_options.MaxConsecutiveErrors} attempts)..."); firstError = false; } + // Show retry progress every 5 attempts or when debug logging is enabled + if (_consecutiveErrors % 5 == 0 || _options.EnableDebugLogging) + { + var elapsedTime = DateTime.UtcNow - _retryStartTime; + var nextDelay = _currentRetryDelay; + Console.WriteLine($"[{GetType().Name}] Retry {_consecutiveErrors}/{_options.MaxConsecutiveErrors} " + + $"(elapsed: {elapsedTime.TotalSeconds:F0}s, next delay: {nextDelay.TotalSeconds:F0}s)"); + } + if (_consecutiveErrors >= _options.MaxConsecutiveErrors) { + var totalRetryTime = DateTime.UtcNow - _retryStartTime; errorToThrow = new InvalidOperationException( - $"Failed to connect after {_consecutiveErrors} consecutive attempts. " + + $"Failed to connect after {_consecutiveErrors} consecutive attempts over {totalRetryTime.TotalSeconds:F0}s. " + $"Ensure the game is running and the API is accessible at {fullUrl}", ex); } @@ -156,9 +184,9 @@ public override async IAsyncEnumerable ReadContinuousAsync( } // Yield data outside try-catch - if (hasData && data.HasValue) + if (hasData) { - yield return data.Value; + yield return data; await Task.Delay(_options.PollInterval, cancellationToken); } else if (!hasData) @@ -175,6 +203,22 @@ public override async IAsyncEnumerable ReadContinuousAsync( } } } + + // Loop exited - log diagnostics + if (_options.EnableDebugLogging) + { + var loopDuration = DateTime.UtcNow - loopStartTime; + Console.WriteLine($"[{GetType().Name}] [DEBUG] Polling loop exited at {DateTime.UtcNow:HH:mm:ss.fff}"); + Console.WriteLine($"[{GetType().Name}] [DEBUG] Loop duration: {loopDuration.TotalSeconds:F1}s"); + Console.WriteLine($"[{GetType().Name}] [DEBUG] Cancellation requested: {cancellationToken.IsCancellationRequested}"); + } + + // Check for unexpected exit + if (!cancellationToken.IsCancellationRequested) + { + Console.WriteLine($"[{GetType().Name}] WARNING: Polling loop exited without cancellation request"); + Console.WriteLine($"[{GetType().Name}] This may indicate an unexpected termination condition"); + } } /// diff --git a/GamesDat/Telemetry/Sources/HttpPollingSourceOptions.cs b/GamesDat/Telemetry/Sources/HttpPollingSourceOptions.cs index 92422d3..b726d8b 100644 --- a/GamesDat/Telemetry/Sources/HttpPollingSourceOptions.cs +++ b/GamesDat/Telemetry/Sources/HttpPollingSourceOptions.cs @@ -30,9 +30,9 @@ public class HttpPollingSourceOptions public TimeSpan RequestTimeout { get; init; } = TimeSpan.FromSeconds(5); /// - /// Maximum number of consecutive errors before giving up. Default is 10. + /// Maximum number of consecutive errors before giving up. Default is 30. /// - public int MaxConsecutiveErrors { get; init; } = 10; + public int MaxConsecutiveErrors { get; init; } = 30; /// /// Initial delay before first retry after an error. Default is 1 second. @@ -44,6 +44,11 @@ public class HttpPollingSourceOptions /// public TimeSpan MaxRetryDelay { get; init; } = TimeSpan.FromSeconds(30); + /// + /// Enable verbose diagnostic logging for troubleshooting. Default is false. + /// + public bool EnableDebugLogging { get; init; } = false; + /// /// Optional custom HTTP headers to include with requests. /// diff --git a/GamesDat/Telemetry/Sources/WarThunder/IndicatorsData.cs b/GamesDat/Telemetry/Sources/WarThunder/IndicatorsData.cs index 22200ab..3a121e4 100644 --- a/GamesDat/Telemetry/Sources/WarThunder/IndicatorsData.cs +++ b/GamesDat/Telemetry/Sources/WarThunder/IndicatorsData.cs @@ -6,53 +6,80 @@ namespace GamesDat.Core.Telemetry.Sources.WarThunder; /// /// Telemetry data from War Thunder's /indicators endpoint. -/// Contains cockpit instrumentation data. +/// Contains cockpit/instrument panel data for all vehicle types (air, ground, naval). +/// The available fields depend on the vehicle type indicated by the 'army' field. /// Recommended polling rate: 10Hz. +/// Note: This type contains reference fields (strings) and cannot be used with BinarySessionWriter. +/// Use realtime-only mode or a JSON-based writer for this data type. /// -[StructLayout(LayoutKind.Sequential, Pack = 1)] [GameId("WarThunder")] -[DataVersion(1, 0, 0)] +[DataVersion(2, 0, 0)] public struct IndicatorsData { - // Validity + /// Default constructor to initialize string fields + public IndicatorsData() + { + Army = string.Empty; + Type = string.Empty; + } + + // ===== Common Fields ===== + + /// Data validity flag [JsonPropertyName("valid")] - public int Valid { get; set; } + public bool Valid { get; set; } - // Type indicator + /// Vehicle category: "air", "tank", or "naval" + [JsonPropertyName("army")] + public string Army { get; set; } = string.Empty; + + /// Vehicle type identifier (e.g., "he51b1", "tankModels/germ_pzkpfw_35t") [JsonPropertyName("type")] - public int Type { get; set; } + public string Type { get; set; } = string.Empty; - // Speed + /// Vehicle speed [JsonPropertyName("speed")] public float Speed { get; set; } + // ===== Aircraft Flight Controls ===== + + [JsonPropertyName("pedals")] + public float Pedals { get; set; } + + [JsonPropertyName("pedals1")] + public float Pedals1 { get; set; } + + [JsonPropertyName("pedals2")] + public float Pedals2 { get; set; } + + [JsonPropertyName("pedals3")] + public float Pedals3 { get; set; } + + [JsonPropertyName("pedals4")] + public float Pedals4 { get; set; } + [JsonPropertyName("pedal_position")] public float PedalPosition { get; set; } - // Engine instruments - [JsonPropertyName("rpm_hour")] - public float RpmHour { get; set; } + [JsonPropertyName("stick_elevator")] + public float StickElevator { get; set; } - [JsonPropertyName("rpm_min")] - public float RpmMin { get; set; } + [JsonPropertyName("stick_elevator1")] + public float StickElevator1 { get; set; } - [JsonPropertyName("manifold_pressure")] - public float ManifoldPressure { get; set; } + [JsonPropertyName("stick_ailerons")] + public float StickAilerons { get; set; } - [JsonPropertyName("oil_temp")] - public float OilTemp { get; set; } + // ===== Aircraft Instruments ===== - [JsonPropertyName("water_temp")] - public float WaterTemp { get; set; } + /// Vertical speed indicator + [JsonPropertyName("vario")] + public float Vario { get; set; } - // Attitude indicator - [JsonPropertyName("aviahorizon_roll")] - public float AviaHorizonRoll { get; set; } - - [JsonPropertyName("aviahorizon_pitch")] - public float AviaHorizonPitch { get; set; } + [JsonPropertyName("vertical_speed")] + public float VerticalSpeed { get; set; } - // Altimeter + // Altimeter (three-pointer display) [JsonPropertyName("altitude_hour")] public float AltitudeHour { get; set; } @@ -62,16 +89,29 @@ public struct IndicatorsData [JsonPropertyName("altitude_10k")] public float Altitude10k { get; set; } - // Other instruments - [JsonPropertyName("vertical_speed")] - public float VerticalSpeed { get; set; } + // Artificial horizon + [JsonPropertyName("aviahorizon_roll")] + public float AviaHorizonRoll { get; set; } + + [JsonPropertyName("aviahorizon_pitch")] + public float AviaHorizonPitch { get; set; } + + [JsonPropertyName("bank")] + public float Bank { get; set; } + [JsonPropertyName("turn")] + public float Turn { get; set; } + + // Compass [JsonPropertyName("compass")] public float Compass { get; set; } [JsonPropertyName("compass1")] public float Compass1 { get; set; } + [JsonPropertyName("compass2")] + public float Compass2 { get; set; } + // Clock [JsonPropertyName("clock_hour")] public float ClockHour { get; set; } @@ -81,4 +121,187 @@ public struct IndicatorsData [JsonPropertyName("clock_sec")] public float ClockSec { get; set; } + + // G-meter + [JsonPropertyName("g_meter")] + public float GMeter { get; set; } + + [JsonPropertyName("g_meter_min")] + public float GMeterMin { get; set; } + + [JsonPropertyName("g_meter_max")] + public float GMeterMax { get; set; } + + [JsonPropertyName("aoa")] + public float AngleOfAttack { get; set; } + + [JsonPropertyName("vne")] + public float Vne { get; set; } + + [JsonPropertyName("mach")] + public float Mach { get; set; } + + // ===== Aircraft Engine Instruments ===== + + [JsonPropertyName("rpm")] + public float Rpm { get; set; } + + [JsonPropertyName("rpm_hour")] + public float RpmHour { get; set; } + + [JsonPropertyName("rpm_min")] + public float RpmMin { get; set; } + + [JsonPropertyName("rpm1_min")] + public float Rpm1Min { get; set; } + + [JsonPropertyName("manifold_pressure")] + public float ManifoldPressure { get; set; } + + [JsonPropertyName("oil_pressure")] + public float OilPressure { get; set; } + + [JsonPropertyName("oil_temp")] + public float OilTemp { get; set; } + + [JsonPropertyName("oil_temperature")] + public float OilTemperature { get; set; } + + [JsonPropertyName("water_temp")] + public float WaterTemp { get; set; } + + [JsonPropertyName("water_temperature")] + public float WaterTemperature { get; set; } + + [JsonPropertyName("head_temperature")] + public float HeadTemperature { get; set; } + + [JsonPropertyName("carb_temperature")] + public float CarbTemperature { get; set; } + + [JsonPropertyName("supercharger")] + public float SuperCharger { get; set; } + + [JsonPropertyName("prop_pitch")] + public float PropellerPitch { get; set; } + + // ===== Aircraft Controls & Systems ===== + + [JsonPropertyName("throttle")] + public float Throttle { get; set; } + + [JsonPropertyName("throttle_1")] + public float Throttle1 { get; set; } + + [JsonPropertyName("mixture")] + public float Mixture { get; set; } + + [JsonPropertyName("mixture_1")] + public float Mixture1 { get; set; } + + [JsonPropertyName("radiator_lever1_1")] + public float RadiatorLever1_1 { get; set; } + + [JsonPropertyName("fuel")] + public float Fuel { get; set; } + + [JsonPropertyName("fuel_pressure")] + public float FuelPressure { get; set; } + + [JsonPropertyName("flaps")] + public float Flaps { get; set; } + + [JsonPropertyName("trimmer")] + public float Trimmer { get; set; } + + [JsonPropertyName("airbrake_lever")] + public float AirbrakeLever { get; set; } + + [JsonPropertyName("airbrake_indicator")] + public float AirbrakeIndicator { get; set; } + + // Landing gear + [JsonPropertyName("gears")] + public float Gears { get; set; } + + [JsonPropertyName("gear")] + public float Gear { get; set; } + + [JsonPropertyName("gears_lamp")] + public float GearsLamp { get; set; } + + [JsonPropertyName("gear_lamp_down")] + public float GearLampDown { get; set; } + + [JsonPropertyName("gear_lamp_up")] + public float GearLampUp { get; set; } + + [JsonPropertyName("gear_lamp_off")] + public float GearLampOff { get; set; } + + // Weapons + [JsonPropertyName("weapon1")] + public float Weapon1 { get; set; } + + [JsonPropertyName("weapon2")] + public float Weapon2 { get; set; } + + [JsonPropertyName("weapon3")] + public float Weapon3 { get; set; } + + [JsonPropertyName("weapon4")] + public float Weapon4 { get; set; } + + // ===== Tank-Specific Fields ===== + + [JsonPropertyName("engine_broken")] + public float EngineBroken { get; set; } + + [JsonPropertyName("stabilizer")] + public float Stabilizer { get; set; } + + [JsonPropertyName("gear_neutral")] + public float GearNeutral { get; set; } + + [JsonPropertyName("has_speed_warning")] + public float HasSpeedWarning { get; set; } + + [JsonPropertyName("driving_direction_mode")] + public float DrivingDirectionMode { get; set; } + + [JsonPropertyName("cruise_control")] + public float CruiseControl { get; set; } + + /// Laser Warning System + [JsonPropertyName("lws")] + public float Lws { get; set; } + + /// Infrared Countermeasures + [JsonPropertyName("ircm")] + public float Ircm { get; set; } + + [JsonPropertyName("roll_indicators_is_available")] + public float RollIndicatorsIsAvailable { get; set; } + + [JsonPropertyName("first_stage_ammo")] + public float FirstStageAmmo { get; set; } + + // Crew status + [JsonPropertyName("crew_total")] + public float CrewTotal { get; set; } + + [JsonPropertyName("crew_current")] + public float CrewCurrent { get; set; } + + [JsonPropertyName("crew_time_to_heal")] + public float CrewTimeToHeal { get; set; } + + [JsonPropertyName("crew_distance")] + public float CrewDistance { get; set; } + + [JsonPropertyName("gunner_state")] + public float GunnerState { get; set; } + + [JsonPropertyName("driver_state")] + public float DriverState { get; set; } } diff --git a/GamesDat/Telemetry/Sources/WarThunder/StateData.cs b/GamesDat/Telemetry/Sources/WarThunder/StateData.cs index 22395b4..2ee827b 100644 --- a/GamesDat/Telemetry/Sources/WarThunder/StateData.cs +++ b/GamesDat/Telemetry/Sources/WarThunder/StateData.cs @@ -16,93 +16,233 @@ public struct StateData { // Validity [JsonPropertyName("valid")] - public int Valid { get; set; } + public bool Valid { get; set; } - // Position (meters) - [JsonPropertyName("X")] - public float X { get; set; } + // Control surfaces (percent) + [JsonPropertyName("aileron, %")] + public float AileronPercent { get; set; } - [JsonPropertyName("Y")] - public float Y { get; set; } + [JsonPropertyName("elevator, %")] + public float ElevatorPercent { get; set; } - [JsonPropertyName("Z")] - public float Z { get; set; } + [JsonPropertyName("rudder, %")] + public float RudderPercent { get; set; } - // Velocity (m/s) - [JsonPropertyName("Vx")] - public float Vx { get; set; } + [JsonPropertyName("flaps, %")] + public float FlapsPercent { get; set; } - [JsonPropertyName("Vy")] - public float Vy { get; set; } + // Altitude and speeds + [JsonPropertyName("H, m")] + public float AltitudeMeters { get; set; } - [JsonPropertyName("Vz")] - public float Vz { get; set; } + [JsonPropertyName("TAS, km/h")] + public float TrueAirspeedKmh { get; set; } - // Angular velocity (rad/s) - [JsonPropertyName("Wx")] - public float Wx { get; set; } + [JsonPropertyName("IAS, km/h")] + public float IndicatedAirspeedKmh { get; set; } - [JsonPropertyName("Wy")] - public float Wy { get; set; } + [JsonPropertyName("M")] + public float Mach { get; set; } - [JsonPropertyName("Wz")] - public float Wz { get; set; } + // Flight angles and forces + [JsonPropertyName("AoA, deg")] + public float AngleOfAttackDeg { get; set; } - // Flight data - [JsonPropertyName("AoA")] - public float AngleOfAttack { get; set; } + [JsonPropertyName("AoS, deg")] + public float AngleOfSlipDeg { get; set; } - [JsonPropertyName("AoS")] - public float AngleOfSlip { get; set; } + [JsonPropertyName("Ny")] + public float Ny { get; set; } - [JsonPropertyName("IAS")] - public float IndicatedAirspeed { get; set; } + [JsonPropertyName("Vy, m/s")] + public float VyMs { get; set; } - [JsonPropertyName("TAS")] - public float TrueAirspeed { get; set; } + [JsonPropertyName("Wx, deg/s")] + public float WxDegPerSec { get; set; } - [JsonPropertyName("M")] - public float Mach { get; set; } + // Fuel + [JsonPropertyName("Mfuel, kg")] + public float FuelMassKg { get; set; } - [JsonPropertyName("H")] - public float Altitude { get; set; } + [JsonPropertyName("Mfuel0, kg")] + public float FuelMassInitialKg { get; set; } - // G-force - [JsonPropertyName("Ny")] - public float Ny { get; set; } + // Gear and Airbrake + [JsonPropertyName("gear, %")] + public int GearPercent { get; set; } - // Engine - [JsonPropertyName("throttle")] - public float Throttle { get; set; } + [JsonPropertyName("airbrake, %")] + public int AirbrakePercent { get; set; } - [JsonPropertyName("RPM")] - public float RPM { get; set; } + // Engine 1 parameters + [JsonPropertyName("throttle 1, %")] + public float Throttle1Percent { get; set; } - [JsonPropertyName("manifold_pressure")] - public float ManifoldPressure { get; set; } + [JsonPropertyName("RPM throttle 1, %")] + public float RpmThrottle1Percent { get; set; } - [JsonPropertyName("power")] - public float Power { get; set; } + [JsonPropertyName("mixture 1, %")] + public float Mixture1Percent { get; set; } - // Controls - [JsonPropertyName("flaps")] - public float Flaps { get; set; } + [JsonPropertyName("radiator 1, %")] + public float Radiator1Percent { get; set; } - [JsonPropertyName("gear")] - public float Gear { get; set; } + [JsonPropertyName("magneto 1")] + public int Magneto1 { get; set; } - [JsonPropertyName("airbrake")] - public float Airbrake { get; set; } + [JsonPropertyName("power 1, hp")] + public float Power1Hp { get; set; } - // Navigation - [JsonPropertyName("compass")] - public float Compass { get; set; } + [JsonPropertyName("RPM 1")] + public float Rpm1 { get; set; } - // Fuel - [JsonPropertyName("fuel")] - public float Fuel { get; set; } + [JsonPropertyName("manifold pressure 1, atm")] + public float ManifoldPressure1Atm { get; set; } + + [JsonPropertyName("water temp 1, C")] + public float WaterTemp1C { get; set; } + + [JsonPropertyName("oil temp 1, C")] + public float OilTemp1C { get; set; } + + [JsonPropertyName("pitch 1, deg")] + public float Pitch1Deg { get; set; } + + [JsonPropertyName("thrust 1, kgs")] + public float Thrust1Kgs { get; set; } + + [JsonPropertyName("efficiency 1, %")] + public float Efficiency1Percent { get; set; } + + [JsonPropertyName("compressor stage 1")] + public float CompressorStage1 { get; set; } + + // Engine 2 parameters + [JsonPropertyName("throttle 2, %")] + public float Throttle2Percent { get; set; } + + [JsonPropertyName("RPM throttle 2, %")] + public float RpmThrottle2Percent { get; set; } + + [JsonPropertyName("mixture 2, %")] + public float Mixture2Percent { get; set; } + + [JsonPropertyName("radiator 2, %")] + public float Radiator2Percent { get; set; } + + [JsonPropertyName("magneto 2")] + public int Magneto2 { get; set; } + + [JsonPropertyName("power 2, hp")] + public float Power2Hp { get; set; } + + [JsonPropertyName("RPM 2")] + public float Rpm2 { get; set; } + + [JsonPropertyName("manifold pressure 2, atm")] + public float ManifoldPressure2Atm { get; set; } + + [JsonPropertyName("water temp 2, C")] + public float WaterTemp2C { get; set; } + + [JsonPropertyName("oil temp 2, C")] + public float OilTemp2C { get; set; } + + [JsonPropertyName("pitch 2, deg")] + public float Pitch2Deg { get; set; } + + [JsonPropertyName("thrust 2, kgs")] + public float Thrust2Kgs { get; set; } + + [JsonPropertyName("efficiency 2, %")] + public float Efficiency2Percent { get; set; } + + [JsonPropertyName("compressor stage 2")] + public float CompressorStage2 { get; set; } + + // Engine 3 parameters + [JsonPropertyName("throttle 3, %")] + public float Throttle3Percent { get; set; } + + [JsonPropertyName("RPM throttle 3, %")] + public float RpmThrottle3Percent { get; set; } + + [JsonPropertyName("mixture 3, %")] + public float Mixture3Percent { get; set; } + + [JsonPropertyName("radiator 3, %")] + public float Radiator3Percent { get; set; } + + [JsonPropertyName("magneto 3")] + public int Magneto3 { get; set; } + + [JsonPropertyName("power 3, hp")] + public float Power3Hp { get; set; } + + [JsonPropertyName("RPM 3")] + public float Rpm3 { get; set; } + + [JsonPropertyName("manifold pressure 3, atm")] + public float ManifoldPressure3Atm { get; set; } + + [JsonPropertyName("water temp 3, C")] + public float WaterTemp3C { get; set; } + + [JsonPropertyName("oil temp 3, C")] + public float OilTemp3C { get; set; } + + [JsonPropertyName("pitch 3, deg")] + public float Pitch3Deg { get; set; } + + [JsonPropertyName("thrust 3, kgs")] + public float Thrust3Kgs { get; set; } + + [JsonPropertyName("efficiency 3, %")] + public float Efficiency3Percent { get; set; } + + [JsonPropertyName("compressor stage 3")] + public float CompressorStage3 { get; set; } + + // Engine 4 parameters + [JsonPropertyName("throttle 4, %")] + public float Throttle4Percent { get; set; } + + [JsonPropertyName("RPM throttle 4, %")] + public float RpmThrottle4Percent { get; set; } + + [JsonPropertyName("mixture 4, %")] + public float Mixture4Percent { get; set; } + + [JsonPropertyName("radiator 4, %")] + public float Radiator4Percent { get; set; } + + [JsonPropertyName("magneto 4")] + public int Magneto4 { get; set; } + + [JsonPropertyName("power 4, hp")] + public float Power4Hp { get; set; } + + [JsonPropertyName("RPM 4")] + public float Rpm4 { get; set; } + + [JsonPropertyName("manifold pressure 4, atm")] + public float ManifoldPressure4Atm { get; set; } + + [JsonPropertyName("water temp 4, C")] + public float WaterTemp4C { get; set; } + + [JsonPropertyName("oil temp 4, C")] + public float OilTemp4C { get; set; } + + [JsonPropertyName("pitch 4, deg")] + public float Pitch4Deg { get; set; } + + [JsonPropertyName("thrust 4, kgs")] + public float Thrust4Kgs { get; set; } + + [JsonPropertyName("efficiency 4, %")] + public float Efficiency4Percent { get; set; } - // Timestamp - [JsonPropertyName("time")] - public long TimeMs { get; set; } + [JsonPropertyName("compressor stage 4")] + public float CompressorStage4 { get; set; } } diff --git a/GamesDat/Telemetry/Sources/WarThunder/StateSource.cs b/GamesDat/Telemetry/Sources/WarThunder/StateSource.cs index a8ecf34..e7bd36b 100644 --- a/GamesDat/Telemetry/Sources/WarThunder/StateSource.cs +++ b/GamesDat/Telemetry/Sources/WarThunder/StateSource.cs @@ -1,4 +1,7 @@ using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; namespace GamesDat.Core.Telemetry.Sources.WarThunder; @@ -8,27 +11,68 @@ namespace GamesDat.Core.Telemetry.Sources.WarThunder; /// public class StateSource : HttpPollingSourceBase { + private readonly StateSourceOptions _stateOptions; + private DateTime _lastInvalidFrameLog = DateTime.MinValue; + + /// + /// Initializes a new instance with StateSourceOptions. + /// + /// Configuration options. + public StateSource(StateSourceOptions options) + : base(WarThunderHttpClient.Instance, options.HttpOptions, ownsClient: false) + { + _stateOptions = options ?? throw new ArgumentNullException(nameof(options)); + } + /// - /// Initializes a new instance with custom options. + /// Initializes a new instance with HttpPollingSourceOptions (legacy constructor). /// /// Configuration options. public StateSource(HttpPollingSourceOptions options) - : base(WarThunderHttpClient.Instance, options, ownsClient: false) + : this(new StateSourceOptions + { + HttpOptions = options, + SkipInvalidFrames = true + }) { } /// - /// Initializes a new instance with simplified parameters. + /// Initializes a new instance with simplified parameters (legacy constructor). /// /// Base URL of the War Thunder API. /// Time between polls. public StateSource(string baseUrl, TimeSpan pollInterval) - : this(new HttpPollingSourceOptions + : this(new StateSourceOptions { - BaseUrl = baseUrl, - EndpointPath = "/state", - PollInterval = pollInterval + HttpOptions = new HttpPollingSourceOptions + { + BaseUrl = baseUrl, + EndpointPath = "/state", + PollInterval = pollInterval + }, + SkipInvalidFrames = true }) { } + + /// + /// Continuously polls the HTTP endpoint and yields valid telemetry data. + /// Filters out invalid frames if SkipInvalidFrames is enabled. + /// + public override async IAsyncEnumerable ReadContinuousAsync( + [EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (var data in base.ReadContinuousAsync(cancellationToken)) + { + // If not filtering invalid frames, yield everything + if (!_stateOptions.SkipInvalidFrames) + { + yield return data; + continue; + } + + yield return data; + } + } } diff --git a/GamesDat/Telemetry/Sources/WarThunder/StateSourceOptions.cs b/GamesDat/Telemetry/Sources/WarThunder/StateSourceOptions.cs new file mode 100644 index 0000000..023f27d --- /dev/null +++ b/GamesDat/Telemetry/Sources/WarThunder/StateSourceOptions.cs @@ -0,0 +1,25 @@ +namespace GamesDat.Core.Telemetry.Sources.WarThunder; + +/// +/// Configuration options for War Thunder StateSource. +/// +public class StateSourceOptions +{ + /// + /// HTTP polling options (URL, intervals, retry settings). + /// + public required HttpPollingSourceOptions HttpOptions { get; init; } + + /// + /// If true, frames where Valid=false will be skipped (not yielded). + /// When enabled, the source will continue polling but only yield valid frames. + /// Default is true (skip invalid frames). + /// + public bool SkipInvalidFrames { get; init; } = true; + + /// + /// Interval for logging "waiting for valid data" messages when skipping invalid frames. + /// Set to TimeSpan.Zero to disable these messages. Default is 10 seconds. + /// + public TimeSpan InvalidFrameLogInterval { get; init; } = TimeSpan.FromSeconds(10); +} diff --git a/mockoon/war-thunder-environment.json b/mockoon/war-thunder-environment.json index 58f1cdd..4c3ae58 100644 --- a/mockoon/war-thunder-environment.json +++ b/mockoon/war-thunder-environment.json @@ -17,7 +17,7 @@ "responses": [ { "uuid": "42fee875-9751-4ee4-b411-04d05d3b993b", - "body": "{\n \"valid\": 1,\n \"X\": {{faker 'number.float' min=-10000 max=10000 precision=0.01}},\n \"Y\": {{faker 'number.float' min=100 max=5000 precision=0.01}},\n \"Z\": {{faker 'number.float' min=-10000 max=10000 precision=0.01}},\n \"Vx\": {{faker 'number.float' min=-50 max=50 precision=0.01}},\n \"Vy\": {{faker 'number.float' min=-20 max=20 precision=0.01}},\n \"Vz\": {{faker 'number.float' min=-50 max=50 precision=0.01}},\n \"Wx\": {{faker 'number.float' min=-0.5 max=0.5 precision=0.001}},\n \"Wy\": {{faker 'number.float' min=-0.5 max=0.5 precision=0.001}},\n \"Wz\": {{faker 'number.float' min=-0.5 max=0.5 precision=0.001}},\n \"AoA\": {{faker 'number.float' min=-5 max=15 precision=0.1}},\n \"AoS\": {{faker 'number.float' min=-3 max=3 precision=0.1}},\n \"IAS\": {{faker 'number.float' min=250 max=600 precision=0.1}},\n \"TAS\": {{faker 'number.float' min=280 max=650 precision=0.1}},\n \"M\": {{faker 'number.float' min=0.4 max=0.9 precision=0.01}},\n \"H\": {{faker 'number.float' min=500 max=4000 precision=0.1}},\n \"Ny\": {{faker 'number.float' min=0.8 max=2.5 precision=0.01}},\n \"throttle\": {{faker 'number.float' min=0.7 max=1.0 precision=0.01}},\n \"RPM\": {{faker 'number.float' min=2200 max=2800 precision=1}},\n \"manifold_pressure\": {{faker 'number.float' min=0.8 max=1.2 precision=0.01}},\n \"power\": {{faker 'number.float' min=0.7 max=1.0 precision=0.01}},\n \"flaps\": {{faker 'number.float' min=0 max=1 precision=0.1}},\n \"gear\": {{faker 'number.float' min=0 max=1 precision=0.1}},\n \"airbrake\": {{faker 'number.float' min=0 max=1 precision=0.1}},\n \"compass\": {{faker 'number.float' min=0 max=360 precision=0.1}},\n \"fuel\": {{faker 'number.float' min=200 max=800 precision=1}},\n \"time\": 1770825735\n}", + "body": "{\"valid\": true,\r\n\"aileron, %\": 0,\r\n\"elevator, %\": 0,\r\n\"rudder, %\": 0,\r\n\"flaps, %\": 0,\r\n\"H, m\": 416,\r\n\"TAS, km/h\": 306,\r\n\"IAS, km/h\": 300,\r\n\"M\": 0.25,\r\n\"AoA, deg\": -0.0,\r\n\"AoS, deg\": 0.0,\r\n\"Ny\": 1.00,\r\n\"Vy, m/s\": 0.0,\r\n\"Wx, deg/s\": 0,\r\n\"Mfuel, kg\": 91,\r\n\"Mfuel0, kg\": 303,\r\n\"gear, %\": 0,\r\n\"airbrake, %\": 0,\r\n\"throttle 1, %\": 0,\r\n\"RPM throttle 1, %\": 0,\r\n\"mixture 1, %\": 100,\r\n\"radiator 1, %\": 0,\r\n\"magneto 1\": 3,\r\n\"power 1, hp\": 0.0,\r\n\"RPM 1\": 600,\r\n\"manifold pressure 1, atm\": 1.00,\r\n\"water temp 1, C\": 55,\r\n\"oil temp 1, C\": 40,\r\n\"pitch 1, deg\": 28.2,\r\n\"thrust 1, kgs\": 0,\r\n\"efficiency 1, %\": 0,\r\n\"compressor stage 1\": 1}", "latency": 0, "statusCode": 200, "label": "Flying - Dynamic Values", @@ -101,10 +101,10 @@ "responses": [ { "uuid": "32b93b52-21c5-49cf-9da8-6bec6ac6cde2", - "body": "{\n \"valid\": 1,\n \"type\": 0,\n \"speed\": {{faker 'number.float' min=250 max=600 precision=1}},\n \"pedal_position\": {{faker 'number.float' min=0.5 max=1.0 precision=0.01}},\n \"rpm_hour\": {{faker 'number.float' min=0 max=10 precision=0.1}},\n \"rpm_min\": {{faker 'number.float' min=0 max=60 precision=0.1}},\n \"manifold_pressure\": {{faker 'number.float' min=0.8 max=1.2 precision=0.01}},\n \"oil_temp\": {{faker 'number.float' min=80 max=120 precision=0.1}},\n \"water_temp\": {{faker 'number.float' min=70 max=110 precision=0.1}},\n \"aviahorizon_roll\": {{faker 'number.float' min=-30 max=30 precision=0.1}},\n \"aviahorizon_pitch\": {{faker 'number.float' min=-15 max=15 precision=0.1}},\n \"altitude_hour\": {{faker 'number.float' min=0 max=10 precision=0.1}},\n \"altitude_min\": {{faker 'number.float' min=0 max=60 precision=0.1}},\n \"altitude_10k\": {{faker 'number.float' min=0 max=10 precision=0.1}},\n \"vertical_speed\": {{faker 'number.float' min=-10 max=10 precision=0.1}},\n \"compass\": {{faker 'number.float' min=0 max=360 precision=0.1}},\n \"compass1\": {{faker 'number.float' min=0 max=360 precision=0.1}},\n \"clock_hour\": {{faker 'number.float' min=0 max=12 precision=0.1}},\n \"clock_min\": {{faker 'number.float' min=0 max=60 precision=0.1}},\n \"clock_sec\": {{faker 'number.float' min=0 max=60 precision=0.1}}\n}", + "body": "{\r\n \"valid\": true,\r\n \"army\": \"tank\",\r\n \"type\": \"tankModels/germ_pzkpfw_35t\",\r\n \"engine_broken\": 1.000000,\r\n \"stabilizer\": 1.000000,\r\n \"gear\": 6.000000,\r\n \"gear_neutral\": 6.000000,\r\n \"speed\": 0.000000,\r\n \"has_speed_warning\": 0.000000,\r\n \"rpm\": 700.000000,\r\n \"driving_direction_mode\": 0.000000,\r\n \"cruise_control\": 0.000000,\r\n \"lws\": -1.000000,\r\n \"ircm\": -1.000000,\r\n \"roll_indicators_is_available\": 0.000000,\r\n \"first_stage_ammo\": -1.000000,\r\n \"crew_total\": 4.000000,\r\n \"crew_current\": 0.000000,\r\n \"crew_time_to_heal\": 20.000000,\r\n \"crew_distance\": 1.000000,\r\n \"gunner_state\": 1.000000,\r\n \"driver_state\": 1.000000\r\n}", "latency": 0, "statusCode": 200, - "label": "Flying - Dynamic Values", + "label": "Tank", "headers": [ { "key": "Content-Type", @@ -125,7 +125,7 @@ }, { "uuid": "3b6b1a99-c098-4e92-8f2d-15e060b72a9e", - "body": "{\n \"valid\": 1,\n \"type\": 0,\n \"speed\": 0.0,\n \"pedal_position\": 0.0,\n \"rpm_hour\": 0.0,\n \"rpm_min\": 10.0,\n \"manifold_pressure\": 0.3,\n \"oil_temp\": 60.0,\n \"water_temp\": 50.0,\n \"aviahorizon_roll\": 0.0,\n \"aviahorizon_pitch\": 0.0,\n \"altitude_hour\": 0.0,\n \"altitude_min\": 0.0,\n \"altitude_10k\": 0.0,\n \"vertical_speed\": 0.0,\n \"compass\": 0.0,\n \"compass1\": 0.0,\n \"clock_hour\": 10.0,\n \"clock_min\": 30.0,\n \"clock_sec\": 45.0\n}", + "body": "{\n \"valid\": true,\n \"type\": 0,\n \"speed\": 0.0,\n \"pedal_position\": 0.0,\n \"rpm_hour\": 0.0,\n \"rpm_min\": 10.0,\n \"manifold_pressure\": 0.3,\n \"oil_temp\": 60.0,\n \"water_temp\": 50.0,\n \"aviahorizon_roll\": 0.0,\n \"aviahorizon_pitch\": 0.0,\n \"altitude_hour\": 0.0,\n \"altitude_min\": 0.0,\n \"altitude_10k\": 0.0,\n \"vertical_speed\": 0.0,\n \"compass\": 0.0,\n \"compass1\": 0.0,\n \"clock_hour\": 10.0,\n \"clock_min\": 30.0,\n \"clock_sec\": 45.0\n}", "latency": 0, "statusCode": 200, "label": "On Ground - Idle", @@ -149,7 +149,7 @@ }, { "uuid": "12eb8f32-baf3-4c73-9821-7692f7f2fc77", - "body": "{\n \"valid\": 0,\n \"type\": 0,\n \"speed\": 0.0,\n \"pedal_position\": 0.0,\n \"rpm_hour\": 0.0,\n \"rpm_min\": 0.0,\n \"manifold_pressure\": 0.0,\n \"oil_temp\": 0.0,\n \"water_temp\": 0.0,\n \"aviahorizon_roll\": 0.0,\n \"aviahorizon_pitch\": 0.0,\n \"altitude_hour\": 0.0,\n \"altitude_min\": 0.0,\n \"altitude_10k\": 0.0,\n \"vertical_speed\": 0.0,\n \"compass\": 0.0,\n \"compass1\": 0.0,\n \"clock_hour\": 0.0,\n \"clock_min\": 0.0,\n \"clock_sec\": 0.0\n}", + "body": "{\n \"valid\": false\n}", "latency": 0, "statusCode": 200, "label": "Not in Match", @@ -170,6 +170,25 @@ "default": false, "crudKey": "id", "callbacks": [] + }, + { + "uuid": "63371bd4-cea8-4f18-aa41-2d650014ef2d", + "body": "{\r\n \"valid\": true,\r\n \"army\": \"air\",\r\n \"type\": \"he51b1\",\r\n \"speed\": -0.000805,\r\n \"pedals\": 0,\r\n \"stick_elevator\": 0,\r\n \"stick_elevator1\": 0,\r\n \"stick_ailerons\": 0,\r\n \"altitude_hour\": 31.400118,\r\n \"altitude_min\": 400.0,\r\n \"altitude_10k\": 0.0,\r\n \"aviahorizon_roll\": 0.278647,\r\n \"aviahorizon_pitch\": 2.5,\r\n \"bank\": -0.278646,\r\n \"compass\": 99.933029,\r\n \"clock_hour\": 10.716667,\r\n \"clock_min\": 43,\r\n \"clock_sec\": 57,\r\n \"rpm\": 0,\r\n \"rpm_min\": 0.0,\r\n \"oil_pressure\": 0,\r\n \"oil_temperature\": 8.370117,\r\n \"water_temperature\": 8.407104,\r\n \"mixture\": 0.833333,\r\n \"mixture_1\": 0.833333,\r\n \"fuel\": 0,\r\n \"fuel_pressure\": 0,\r\n \"flaps\": 0,\r\n \"gears\": 1,\r\n \"trimmer\": 0,\r\n \"throttle\": 0,\r\n \"throttle_1\": 0,\r\n \"weapon1\": 0,\r\n \"weapon2\": 0,\r\n \"radiator_lever1_1\": 0,\r\n \"g_meter\": 1.0,\r\n \"mach\": 0.0\r\n}", + "latency": 0, + "statusCode": 200, + "label": "Plane", + "headers": [], + "bodyType": "INLINE", + "filePath": "", + "databucketID": "", + "sendFileAsBody": false, + "rules": [], + "rulesOperator": "OR", + "disableTemplating": false, + "fallbackTo404": false, + "default": false, + "crudKey": "id", + "callbacks": [] } ], "responseMode": null, From c70c1a6c900c1093e38fbc8fd8d6571b49b422df Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 13:56:48 +0000 Subject: [PATCH 2/2] Apply PR feedback: remove SkipInvalidFrames, fix condition, update DataVersion, combine if statements Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com> --- GamesDat.Demo/Program.cs | 9 +++------ .../Sources/HttpPollingSourceBase.cs | 2 +- .../Telemetry/Sources/WarThunder/StateData.cs | 2 +- .../Sources/WarThunder/StateSource.cs | 19 ++++--------------- .../Sources/WarThunder/StateSourceOptions.cs | 13 ------------- 5 files changed, 9 insertions(+), 36 deletions(-) diff --git a/GamesDat.Demo/Program.cs b/GamesDat.Demo/Program.cs index 9940ad2..c79aa91 100644 --- a/GamesDat.Demo/Program.cs +++ b/GamesDat.Demo/Program.cs @@ -71,14 +71,11 @@ static async Task CaptureWarThunderSession() lastValidData = data.Valid; - if (data.Valid) + if (data.Valid && frameCount % 1 == 0) // Update every frame (10Hz) { // Clear and redraw dashboard every frame - if (frameCount % 1 == 0) // Update every frame (10Hz) - { - Console.SetCursorPosition(0, 4); - DrawDashboard(data); - } + Console.SetCursorPosition(0, 4); + DrawDashboard(data); } }); diff --git a/GamesDat/Telemetry/Sources/HttpPollingSourceBase.cs b/GamesDat/Telemetry/Sources/HttpPollingSourceBase.cs index fcd3beb..f7ebdb5 100644 --- a/GamesDat/Telemetry/Sources/HttpPollingSourceBase.cs +++ b/GamesDat/Telemetry/Sources/HttpPollingSourceBase.cs @@ -189,7 +189,7 @@ public override async IAsyncEnumerable ReadContinuousAsync( yield return data; await Task.Delay(_options.PollInterval, cancellationToken); } - else if (!hasData) + else { // Wait before retry (for connection errors or JSON errors) var delayTime = _consecutiveErrors > 0 ? _currentRetryDelay : _options.PollInterval; diff --git a/GamesDat/Telemetry/Sources/WarThunder/StateData.cs b/GamesDat/Telemetry/Sources/WarThunder/StateData.cs index 2ee827b..39fe42d 100644 --- a/GamesDat/Telemetry/Sources/WarThunder/StateData.cs +++ b/GamesDat/Telemetry/Sources/WarThunder/StateData.cs @@ -11,7 +11,7 @@ namespace GamesDat.Core.Telemetry.Sources.WarThunder; /// [StructLayout(LayoutKind.Sequential, Pack = 1)] [GameId("WarThunder")] -[DataVersion(1, 0, 0)] +[DataVersion(2, 0, 0)] public struct StateData { // Validity diff --git a/GamesDat/Telemetry/Sources/WarThunder/StateSource.cs b/GamesDat/Telemetry/Sources/WarThunder/StateSource.cs index e7bd36b..ee3acb3 100644 --- a/GamesDat/Telemetry/Sources/WarThunder/StateSource.cs +++ b/GamesDat/Telemetry/Sources/WarThunder/StateSource.cs @@ -12,7 +12,6 @@ namespace GamesDat.Core.Telemetry.Sources.WarThunder; public class StateSource : HttpPollingSourceBase { private readonly StateSourceOptions _stateOptions; - private DateTime _lastInvalidFrameLog = DateTime.MinValue; /// /// Initializes a new instance with StateSourceOptions. @@ -31,8 +30,7 @@ public StateSource(StateSourceOptions options) public StateSource(HttpPollingSourceOptions options) : this(new StateSourceOptions { - HttpOptions = options, - SkipInvalidFrames = true + HttpOptions = options }) { } @@ -50,29 +48,20 @@ public StateSource(string baseUrl, TimeSpan pollInterval) BaseUrl = baseUrl, EndpointPath = "/state", PollInterval = pollInterval - }, - SkipInvalidFrames = true + } }) { } /// - /// Continuously polls the HTTP endpoint and yields valid telemetry data. - /// Filters out invalid frames if SkipInvalidFrames is enabled. + /// Continuously polls the HTTP endpoint and yields telemetry data. /// public override async IAsyncEnumerable ReadContinuousAsync( [EnumeratorCancellation] CancellationToken cancellationToken) { await foreach (var data in base.ReadContinuousAsync(cancellationToken)) { - // If not filtering invalid frames, yield everything - if (!_stateOptions.SkipInvalidFrames) - { - yield return data; - continue; - } - - yield return data; + yield return data; } } } diff --git a/GamesDat/Telemetry/Sources/WarThunder/StateSourceOptions.cs b/GamesDat/Telemetry/Sources/WarThunder/StateSourceOptions.cs index 023f27d..e0355c9 100644 --- a/GamesDat/Telemetry/Sources/WarThunder/StateSourceOptions.cs +++ b/GamesDat/Telemetry/Sources/WarThunder/StateSourceOptions.cs @@ -9,17 +9,4 @@ public class StateSourceOptions /// HTTP polling options (URL, intervals, retry settings). /// public required HttpPollingSourceOptions HttpOptions { get; init; } - - /// - /// If true, frames where Valid=false will be skipped (not yielded). - /// When enabled, the source will continue polling but only yield valid frames. - /// Default is true (skip invalid frames). - /// - public bool SkipInvalidFrames { get; init; } = true; - - /// - /// Interval for logging "waiting for valid data" messages when skipping invalid frames. - /// Set to TimeSpan.Zero to disable these messages. Default is 10 seconds. - /// - public TimeSpan InvalidFrameLogInterval { get; init; } = TimeSpan.FromSeconds(10); }