using System.Diagnostics;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Channels;
using XFEEDToolBox.Core.Data;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Native;
using XFEEDToolBox.Core.Navigation;
using XFEEDToolBox.Core.Session;
using XFEEDToolBox.NativeSupport;
/// Production driving, thermal stops and re-engagement on one existing clear SC leg.
internal static class SCOThermalCycleAcceptance
{
private const double LightSecond = 299792458;
private static readonly JsonSerializerOptions EvidenceJson = new(ToolboxSettings.JsonOptions)
{ NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals };
private static readonly JsonSerializerOptions EventJson = new(EvidenceJson) { WriteIndented = false };
private static bool True(JsonElement value, string key) => value.TryGetProperty(key, out var item) && item.ValueKind == JsonValueKind.True;
private static long At(JsonElement value) => value.GetProperty("observedAtUnixMilliseconds").GetInt64();
private static string Text(JsonElement value, string key) => value.GetProperty(key).GetString()!;
private static Vector3d Vector(JsonElement value, string key)
{ var array = value.GetProperty(key); return new(array[0].GetDouble(), array[1].GetDouble(), array[2].GetDouble()); }
private static string Cargo(PlayerState state) => JsonSerializer.Serialize(state.Inventory
.OrderBy(item => item.Key, StringComparer.Ordinal).ThenBy(item => item.MissionId)
.Select(item => new { item.Key, item.Count, item.Stolen, item.MissionId, item.Group }), ToolboxSettings.JsonOptions);
private sealed class Cycle(JsonElement enable)
{
public JsonElement Enable { get; } = enable.Clone();
public Dictionary Renewals { get; } = new(StringComparer.Ordinal);
public SortedDictionary Active { get; } = new();
public SortedDictionary StoppedHeatSamples { get; } = new();
public long? ReenabledAt { get; set; }
public JsonElement? Stop { get; set; }
public JsonElement? Off { get; set; }
public CruiseOverchargeDecision? PolicyAtStop { get; set; }
public CruiseOverchargeObservation? BeforeEnableObservation { get; init; }
public CruiseOverchargeDecision? BeforeEnablePolicy { get; init; }
public bool CoolingObserved { get; set; }
public double ObservedCoolingPercent => StoppedHeatSamples.Count > 1
? StoppedHeatSamples.First().Value.GetProperty("heatPercent").GetDouble()
- StoppedHeatSamples.Values.Min(sample => sample.GetProperty("heatPercent").GetDouble()) : 0;
public bool ActualCoolingObserved => ObservedCoolingPercent >= .05;
public bool HeatLimited => Stop is { } stop && (Text(stop, "reason") == "heat" || PolicyAtStop?.Reason == CruiseOverchargeReason.Heat);
public double ActiveSeconds => Active.Count > 1 ? (Active.Keys.Last() - Active.Keys.First()) / 1000d : 0;
public bool Matches(JsonElement value) => Text(value, "owner") == Text(Enable, "owner")
&& value.GetProperty("selectionGeneration").GetUInt64() == Enable.GetProperty("selectionGeneration").GetUInt64()
&& Text(value, "entityId") == Text(Enable, "entityId") && Text(value, "referenceWorldAddress") == Text(Enable, "referenceWorldAddress");
public bool Complete => Stop is { } stop && Off is { } off && True(stop, "nativeMethodCalled") && !True(stop, "argument")
&& Text(stop, "nativeMethodRva") == "2eaacd0" && Text(stop, "startRequestId") == Text(Enable, "requestId")
&& Matches(stop) && stop.GetProperty("owningThread").GetInt32() == Enable.GetProperty("owningThread").GetInt32()
&& stop.GetProperty("stateAfter").GetInt32() is 0 or 3 && At(stop) >= At(Enable)
&& (Active.Count == 0 || At(stop) >= Active.Keys.Last()) && At(off) > At(stop)
&& off.GetProperty("state").GetInt32() is 0 or 3 && !True(off, "active");
public bool Qualified => Complete && Active.Count >= 5 && Renewals.Count >= 5;
public object Evidence => new { startRequestId = Text(Enable, "requestId"), enable = Enable,
renewals = Renewals.Values, renewalCount = Renewals.Count, activeSamples = Active.Values, activeSampleCount = Active.Count,
activeSeconds = ActiveSeconds, stop = Stop, policyAtStop = PolicyAtStop, subsequentOff = Off, coolingObserved = CoolingObserved,
heatLimited = HeatLimited, stoppedHeatSamples = StoppedHeatSamples.Values, observedCoolingPercent = ObservedCoolingPercent,
actualCoolingObserved = ActualCoolingObserved, reenabledAtUnixMilliseconds = ReenabledAt,
beforeEnableObservation = BeforeEnableObservation, beforeEnablePolicy = BeforeEnablePolicy, complete = Complete, qualified = Qualified };
}
internal static async Task RunAsync(int pid, string directory)
{
directory = Path.GetFullPath(directory);
if (Directory.Exists(directory)) throw new IOException("Use a fresh SCO thermal-cycle evidence directory.");
var parent = Path.GetDirectoryName(directory)!; Directory.CreateDirectory(parent);
using var exclusive = new FileStream(Path.Combine(parent, "docking-fixture-wait.lock"), FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);
Directory.CreateDirectory(directory);
var start = DateTimeOffset.UtcNow; var flightUntil = start.AddSeconds(60);
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(65));
using var sentinel = new Timer(_ => { if (File.Exists(Path.Combine(directory, "stop"))) timeout.Cancel(); }, null, 100, 100);
var token = timeout.Token;
using var process = Process.GetProcessById(pid); var processStarted = process.StartTime.ToUniversalTime().Ticks;
var settings = ToolboxSettings.Load(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "XFEstudio", "XFEEDToolBox", "settings.json"));
await using var journal = new JournalMonitor(settings.JournalDirectory); await journal.StartAsync(token);
var initial = journal.State; var initialCargo = Cargo(initial);
var rawPipe = new NavigationPipeClient(pid); var cruisePipe = new NavigationPipeClient(pid, NativeCruiseControlChannel.Service);
var previousGuardian = PersistentProtectionProcess.Current;
FlightTestProtectionScope? protection = null; NativeSystemFlightControlDriver? driver = null; SystemNavigationTarget? target = null;
SystemFlightFrame? firstFrame = null, lastFrame = null;
JsonElement? inventoryBefore = null, inventoryAfter = null, finalRaw = null, finalCruise = null;
string phase = "preflight", stopReason = "not-started";
string? failure = null, releaseFailure = null, writerFailure = null;
var cycles = new List(); var receipts = new List(); var stops = new List();
var recoveryCount = 0; var zeroThrottleObserved = false; var targetPreserved = false; var overallPassed = false; var inputSubmitted = false;
double reserve = double.NaN, maximumSpeed = 0, maximumHeat = 0, minimumDistance = double.PositiveInfinity;
var events = Channel.CreateUnbounded