XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Diagnostics;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
using XFEEDToolBox.Core.Data;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Native;
using XFEEDToolBox.Core.Navigation;
using XFEEDToolBox.Core.Session;
using XFEEDToolBox.NativeSupport;

/// <summary>Prepares a real, clear SC leg for the separate SCO proof. Never configures or enables SCO.</summary>
internal static class PrepareScoLeg
{
    private const double LightSecond = 299792458;
    private static readonly JsonSerializerOptions EvidenceJson = new(ToolboxSettings.JsonOptions)
        { NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals };
    private static bool True(JsonElement value, string key) => value.TryGetProperty(key, out var item) && item.ValueKind == JsonValueKind.True;
    private static ulong Address(JsonElement value, string key) => ulong.Parse(value.GetProperty(key).GetString()!, CultureInfo.InvariantCulture);
    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 }), EvidenceJson);

    internal static async Task RunAsync(int pid, string directory, bool longContinuityLeg = false)
    {
        directory = Path.GetFullPath(directory);
        if (Directory.Exists(directory)) throw new IOException("Use a fresh SC preparation 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);
        // Leave a cleanup margin inside the requested three-minute bound.
        using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(170));
        using var sentinel = new Timer(_ => { if (File.Exists(Path.Combine(directory, "stop"))) timeout.Cancel(); }, null, 100, 100);
        var token = timeout.Token; var started = DateTimeOffset.UtcNow;
        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 pipe = new NavigationPipeClient(pid);
        using var source = new NativeCurrentSystemSource(pid, NativeSystemLocationReader.GameSha256, () => journal.State);
        FlightTestProtectionScope? protection = null; NativeSystemFlightControlDriver? driver = null;
        var previousGuardian = PersistentProtectionProcess.Current;
        SystemLocation? priorSelection = null, chosen = null, finalSelection = null;
        SystemNavigationTarget? target = null; SystemFlightFrame? initialFrame = null, latest = null;
        ulong protectedEntity = 0, preparedEntity = 0; double previousHull = double.NaN, previousShield = double.NaN, reserve = double.NaN;
        string phase = "preflight"; string? failure = null, releaseFailure = null;
        var prepared = false; var passed = false; var zeroThrottleObserved = false; var released = false; var targetPreserved = false;
        var entryRequests = 0; var flightCommands = 0; var protectionRebinds = 0; var newEntityObserved = false;
        await using var log = new StreamWriter(new FileStream(Path.Combine(directory, "frames.jsonl"), FileMode.CreateNew, FileAccess.Write, FileShare.Read));
        async Task Record(object value)
        { await log.WriteLineAsync(JsonSerializer.Serialize(value, EvidenceJson)); await log.FlushAsync(); }
        void Session()
        {
            process.Refresh(); var state = journal.State;
            if (process.HasExited || process.StartTime.ToUniversalTime().Ticks != processStarted || !state.InGame || state.Docked
                || state.CommanderId != initial.CommanderId || state.LoadGameAt != initial.LoadGameAt || state.CurrentShipId != initial.CurrentShipId
                || state.Navigation.CurrentSystemAddress != initial.Navigation.CurrentSystemAddress || state.CargoCount != initial.CargoCount
                || state.CargoCapacity != initial.CargoCapacity || !state.CargoReliable || Cargo(state) != initialCargo
                || NavigationSafety.HasExternalThreat(state) || NavigationSafety.UnhealthyHull(state))
                throw new InvalidOperationException($"Preparation session changed: inGame={state.InGame}, docked={state.Docked}, ship={state.CurrentShipId}, cargo={state.CargoCount}, reliable={state.CargoReliable}, externalThreat={NavigationSafety.HasExternalThreat(state)}, flags={state.Status?.Flags}.");
            var ship = state.Ships.SingleOrDefault(item => item.ShipId == state.CurrentShipId);
            var fuel = state.Navigation.FuelAt > state.StatusAt ? state.Navigation.MainFuel : state.Status?.FuelMain ?? state.Navigation.MainFuel;
            if (!TravelPreparation.StatusConfirmed(state, DateTimeOffset.UtcNow) || ship?.FuelMain is not { } capacity || !double.IsFinite(capacity)
                || fuel is not { } main || !double.IsFinite(main) || main > capacity + .001 || main <= reserve + Math.Max(1, capacity * .02))
                throw new InvalidOperationException("Fresh actual fuel must remain above the main-tank reserve and preparation margin.");
        }
        async Task Protection(CancellationToken cancellation)
        {
            cancellation.ThrowIfCancellationRequested(); Session();
            var raw = await pipe.CallAsync(new { op = "health.read" }, cancellation); var health = raw.GetProperty("shipHealth");
            var at = health.GetProperty("observedAtUnixMilliseconds").GetInt64(); var hull = health.GetProperty("hullFraction").GetDouble();
            var shield = health.TryGetProperty("shieldFraction", out var value) && value.ValueKind == JsonValueKind.Number ? value.GetDouble() : double.NaN;
            var entity = Address(raw, "entityId");
            if (!True(raw, "localPlayerShipVerified") || !True(health, "verified") || Address(health, "entityId") != entity || entity == 0
                || DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - at is < 0 or > 500 || True(health, "emergency") || True(health, "damageObserved")
                || !double.IsFinite(hull) || hull < .5 || double.IsFinite(previousHull) && hull < previousHull - 1e-6
                || double.IsFinite(previousShield) && double.IsFinite(shield) && shield < previousShield - 1e-6)
                throw new InvalidOperationException("The original independent main-ship health observation stopped preparation.");
            previousHull = hull; previousShield = shield;
            if (protectedEntity != 0 && entity != protectedEntity)
            {
                var entered = journal.State.Navigation.SupercruiseEnteredAt;
                if (entryRequests == 0 || entered is null || entered < started || entered > DateTimeOffset.UtcNow
                    || DateTimeOffset.UtcNow - entered > TimeSpan.FromSeconds(30))
                    throw new InvalidOperationException("Only the requested, Journal-confirmed SC scene transition may rebind flight protection.");
                if (protection is not null) await protection.DisposeAsync();
                protection = await FlightTestProtectionScope.ArmAsync(pid, exitForFailedHandoff: false, token: cancellation);
                protectionRebinds++; protectedEntity = entity;
                await Record(new { at = DateTimeOffset.UtcNow, phase = "protection-rebound-after-original-sc-entry", entity, entered });
            }
            if (protection?.Stopped == true) throw new InvalidOperationException(protection.LastIssue ?? "Flight protection stopped.");
            NativeFlightTestProtection.CheckBeforeInput(pid);
        }
        async Task<SystemFlightFrame> Read(CancellationToken cancellation)
        {
            Session(); var frame = await driver!.ReadSystemFlightAsync(cancellation); await Protection(cancellation);
            if (frame.PlayerOverride || frame.ExternalThreat || frame.TargetObscured || frame.CollisionCourse || !frame.GeometryVerified
                || frame.HeatPercent is not { } heat || !double.IsFinite(heat) || heat >= 75 || frame.TargetDistance < 50 * LightSecond)
                throw new InvalidOperationException("A fresh, cool and collision-free distant local flight leg is required.");
            if (driver.LastTelemetry is not { } local || True(local.GetProperty("supercruiseAssist"), "enabled")
                || True(local.GetProperty("supercruiseAssist"), "engaged"))
                throw new InvalidOperationException("Game supercruise assist must remain disabled for preparation.");
            ClearRay(frame.Position, frame.TargetPosition, frame.ReferenceWorld, await pipe.CallAsync(new { op = "read" }, cancellation));
            latest = frame; return frame;
        }
        async Task Apply(SystemFlightFrame frame, SystemFlightDecision decision, CancellationToken cancellation)
        {
            const SystemNavigationAction allowed = SystemNavigationAction.EnterSupercruise | SystemNavigationAction.CancelOwnedAssists | SystemNavigationAction.ReleaseFlightAxes;
            if ((decision.Actions & ~allowed) != 0 || decision.Complete || decision.Phase == SystemNavigationPhase.Stopped)
                throw new InvalidOperationException("Preparation cannot launch, dock, change systems or perform destination arrival actions.");
            if (DateTimeOffset.UtcNow - frame.ObservedAt > TimeSpan.FromMilliseconds(150))
                throw new FlightTelemetryUnavailableException("Preparation source frame is older than 150ms.");
            await driver!.ApplySystemFlightAsync(decision, frame, target!, frame.ObservedAt.AddMilliseconds(250), cancellation);
            flightCommands++; if (decision.Actions.HasFlag(SystemNavigationAction.EnterSupercruise)) entryRequests++;
        }
        try
        {
            if (!initial.InGame || initial.Docked || initial.GameMode != "Solo" || initial.LoadGameAt is null || initial.CurrentShipId is null || !initial.CargoReliable
                || initial.Navigation.CurrentSystemAddress is not > 0 || NavigationSafety.HasExternalThreat(initial) || NavigationSafety.UnhealthyHull(initial))
                throw new InvalidOperationException("Preparation requires an undocked healthy Solo ship with its actual known cargo and current session.");
            var ship = initial.Ships.Single(item => item.ShipId == initial.CurrentShipId);
            if (ship.FuelMain is not { } capacity || !double.IsFinite(capacity) || capacity <= 8)
                throw new InvalidOperationException("Current actual main-tank capacity must be known and above the SCO proof reserve.");
            reserve = Math.Max(8, capacity * .25); Session();
            var hello = await pipe.CallAsync(new { op = "describe" }, token);
            if (hello.GetProperty("gameSha256").GetString() != NativeSystemLocationReader.GameSha256)
                throw new InvalidDataException("Preparation does not match the verified game build.");
            var catalogue = await source.ReadLocationsAsync(token);
            await File.WriteAllTextAsync(Path.Combine(directory, "catalogue-before.json"), JsonSerializer.Serialize(catalogue, EvidenceJson), token);
            priorSelection = catalogue.Locations.SingleOrDefault(item => item.Selected);
            var raw = await pipe.CallAsync(new { op = "read" }, token); var geometry = raw.GetProperty("geometry");
            if (Address(raw, "entityId") != catalogue.EntityId || catalogue.SystemAddress != initial.Navigation.CurrentSystemAddress)
                throw new InvalidDataException("The catalogue and original main-ship reference changed before target selection.");
            var origin = Vector(geometry, "position"); var world = Address(raw, "referenceWorldAddress");
            var rejected = new List<object>();
            bool Eligible(SystemLocation row)
            {
                if (row.WorldAddress == 0 || row.Identity.Length != 64 || row.NativeKind <= 0 || row.Key < 0
                    || row.DistanceMetres is not { } distance || !double.IsFinite(distance) || distance < 50 * LightSecond
                    || row.Position is not { Finite: true } point || row.Category is not (SystemLocationCategory.Station or SystemLocationCategory.Carrier
                        or SystemLocationCategory.Star or SystemLocationCategory.Planet or SystemLocationCategory.LandablePlanet)
                    || row.Category is SystemLocationCategory.Station or SystemLocationCategory.Carrier && row.MarketId is not > 0) return false;
                try { ClearRay(origin, point, world, raw); return true; }
                catch (Exception error) when (error is InvalidOperationException or InvalidDataException)
                { rejected.Add(new { row.Name, row.Identity, reason = error.Message }); return false; }
            }
            var minimumContinuityDistance = 50_000 * LightSecond;
            // A 1,000ls destination naturally reaches the production D/v
            // approach boundary after a short acceleration. Select a real
            // longer clear leg for this research proof; keep that boundary.
            chosen = longContinuityLeg
                ? (priorSelection is { } distantPrior && distantPrior.DistanceMetres >= minimumContinuityDistance && Eligible(distantPrior) ? distantPrior
                    : catalogue.Locations.Where(item => item.DistanceMetres >= minimumContinuityDistance
                            && item.Category is SystemLocationCategory.Planet or SystemLocationCategory.LandablePlanet or SystemLocationCategory.Star)
                        .OrderByDescending(item => item.DistanceMetres).ThenBy(item => item.Key).FirstOrDefault(Eligible)
                    ?? throw new InvalidOperationException("No clear actual catalogue body at least 50,000ls away is available for a sustained SCO proof."))
                : (priorSelection is { } prior && Eligible(prior) ? prior
                    : catalogue.Locations.FirstOrDefault(item => item.Name.Equals("Homer Platform", StringComparison.OrdinalIgnoreCase) && Eligible(item))
                        ?? catalogue.Locations.Where(item => item.Category is SystemLocationCategory.Planet or SystemLocationCategory.LandablePlanet or SystemLocationCategory.Star)
                            .OrderByDescending(item => item.DistanceMetres).ThenBy(item => item.Key).FirstOrDefault(Eligible)
                        ?? throw new InvalidOperationException("No clear, actual catalogue destination at least 50ls away is available."));
            await Record(new { at = DateTimeOffset.UtcNow, phase = "target-choice", priorSelection, chosen, rejected, longContinuityLeg });
            target = new(catalogue.SystemAddress, chosen.WorldAddress, chosen.Name,
                chosen.Category is SystemLocationCategory.Station or SystemLocationCategory.Carrier ? SystemTargetKind.Station : SystemTargetKind.Orbit, chosen.MarketId)
                { NativeKey = chosen.Key, NativeKind = chosen.NativeKind, NativeIdentity = chosen.Identity };
            PersistentProtectionProcess.Current = null;
            protection = await FlightTestProtectionScope.ArmAsync(pid, exitForFailedHandoff: false, token: token); protectedEntity = catalogue.EntityId;
            driver = new(pid, NativeSystemLocationReader.GameSha256, () => journal.State,
                new NativeGameInputChannel(pid, NativeSystemLocationReader.GameSha256, NativeSupportPackage.Directory), Protection, useNativeSupercruiseAssist: false);
            await driver.SelectAsync(target, token);
            // Preflight reads the real local mode. The general raw read has no inSupercruise property.
            initialFrame = await driver.ReadSystemFlightAsync(token);
            if (initialFrame.ShipEntity != catalogue.EntityId || initialFrame.Mode != LocalFlightMode.Normal || initialFrame.Speed > 1 || Math.Abs(initialFrame.Throttle) > .001)
                throw new InvalidOperationException("This preparation must begin stationary at zero throttle in actual normal space.");
            // Preserve raw producer evidence before the first possible flight command,
            // including a failed preflight. This operation configures observation only.
            var physicalInputs = await pipe.CallAsync(new { op = "input.physical.read", prepare = true }, token);
            await File.WriteAllTextAsync(Path.Combine(directory, "physical-input-preflight.json"),
                JsonSerializer.Serialize(physicalInputs, EvidenceJson), token);
            await driver.PrepareAsync(token);
            var navigator = new SystemFlightNavigator(); phase = "align-and-enter-original-supercruise";
            while (true)
            {
                token.ThrowIfCancellationRequested();
                try
                {
                    var frame = await Read(token);
                    if (frame.Mode == LocalFlightMode.Supercruise)
                    {
                        if (entryRequests == 0 || frame.ShipEntity == initialFrame.ShipEntity || frame.ReplacesShipEntity != initialFrame.ShipEntity
                            || frame.SceneTransitionAt is not { } transition || transition < started || journal.State.Navigation.SupercruiseEnteredAt != transition)
                            throw new InvalidDataException("SC mode requires a new original entity and the matching requested Journal transition.");
                        newEntityObserved = true; preparedEntity = frame.ShipEntity; break;
                    }
                    var decision = navigator.Decide(frame, target, DateTimeOffset.UtcNow);
                    await Apply(frame, decision, token); await Record(new { at = DateTimeOffset.UtcNow, phase, frame, decision, driver.LastTelemetry });
                }
                catch (Exception error) when (NavigationRecovery.CanRetry(error, token))
                { await Record(new { at = DateTimeOffset.UtcNow, phase, recovering = error.Message }); await driver.SuspendSystemFlightAsync(); }
                await Task.Delay(50, token);
            }
            phase = "confirm-zero-throttle-in-original-sc"; var zeroSamples = 0; long lastSequence = -1;
            while (zeroSamples < 3)
            {
                var frame = await Read(token);
                if (frame.Mode != LocalFlightMode.Supercruise || frame.ShipEntity != preparedEntity)
                    throw new InvalidDataException("Prepared SC entity changed before the zero-throttle handoff.");
                await Apply(frame, new(SystemNavigationPhase.Waiting, phase, null, 0), token);
                if (Math.Abs(frame.Throttle) <= .001 && frame.Sequence != lastSequence) zeroSamples++; else if (Math.Abs(frame.Throttle) > .001) zeroSamples = 0;
                lastSequence = frame.Sequence; await Record(new { at = DateTimeOffset.UtcNow, phase, frame, zeroSamples }); await Task.Delay(50, token);
            }
            await driver.ReleaseSystemFlightAsync(); released = true;
            var finalFrame = await Read(token);
            zeroThrottleObserved = finalFrame.Mode == LocalFlightMode.Supercruise && Math.Abs(finalFrame.Throttle) <= .001;
            var final = await source.ReadLocationsAsync(token); finalSelection = final.Locations.SingleOrDefault(item => item.Selected);
            targetPreserved = finalSelection is not null && finalSelection.Name == target.Name && finalSelection.NativeKind == target.NativeKind
                && finalSelection.MarketId == target.MarketId && finalSelection.DistanceMetres is >= 50 * LightSecond && final.EntityId == finalFrame.ShipEntity;
            prepared = newEntityObserved && zeroThrottleObserved && targetPreserved; phase = "prepared-for-separate-sco-proof";
            if (!prepared) throw new InvalidDataException("Actual SC/zero-throttle/selected-target handoff was not confirmed.");
        }
        catch (Exception error) { failure = error.ToString(); }
        finally
        {
            if (driver is not null && !released)
            {
                try
                {
                    using var cleanup = new CancellationTokenSource(TimeSpan.FromSeconds(3));
                    try
                    {
                        var frame = await driver.ReadSystemFlightAsync(cleanup.Token);
                        if (!frame.PlayerOverride && !frame.ExternalThreat && frame.InGame && target is not null)
                            await driver.ApplySystemFlightAsync(new(SystemNavigationPhase.Waiting, "Preparation cleanup: zero throttle", null, 0), frame, target,
                                frame.ObservedAt.AddMilliseconds(250), cleanup.Token);
                    }
                    finally { await driver.ReleaseSystemFlightAsync().WaitAsync(cleanup.Token); released = true; }
                }
                catch (Exception error) { releaseFailure = error.ToString(); }
            }
            try { if (protection is not null) await protection.DisposeAsync(); }
            catch (Exception error) { releaseFailure ??= error.ToString(); }
            PersistentProtectionProcess.Current = previousGuardian;
            var after = journal.State;
            var cargoPreserved = after.CargoCount == initial.CargoCount && after.CargoCapacity == initial.CargoCapacity && Cargo(after) == initialCargo;
            passed = prepared && released && cargoPreserved && failure is null && releaseFailure is null;
            await File.WriteAllTextAsync(Path.Combine(directory, "result.json"), JsonSerializer.Serialize(new
            {
                passed, pid, processStarted, started, finished = DateTimeOffset.UtcNow, phase, failure, releaseFailure,
                initial.CurrentShipId, initial.Ship, initial.CargoCount, initial.CargoCapacity, initialCargo, finalCargo = Cargo(after), cargoPreserved,
                priorSelection, chosen, finalSelection, targetPreserved, initialFrame, latest, newEntityObserved, protectionRebinds,
                reserveFuelTonnes = reserve, zeroThrottleObserved, released, entryRequests, flightCommands,
                overchargeControllerConfigured = false, overchargeCommandsSent = 0, gameSupercruiseAssistUsed = false,
                launchCommandsSent = 0, dockingRequestsSent = 0, tradeOrdersSent = 0, serviceOrdersSent = 0, crossSystemFlight = false
            }, EvidenceJson));
            Console.WriteLine(JsonSerializer.Serialize(new { passed, phase, newEntityObserved, zeroThrottleObserved, cargoPreserved, targetPreserved, released, directory }));
        }
        if (!passed)
            throw new InvalidOperationException("SC preparation stopped; inspect the preserved evidence before running SCO continuity verification.");
    }

    private static void ClearRay(Vector3d origin, Vector3d destination, ulong world, JsonElement raw)
    {
        var geometry = raw.GetProperty("geometry"); var at = raw.GetProperty("observedAtUnixMilliseconds").GetInt64();
        if (DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - at is < 0 or > 250 || !True(geometry, "fresh")
            || !True(geometry, "referenceFrameVerified") || Address(geometry, "worldAddress") != world || !origin.Finite || !destination.Finite)
            throw new InvalidDataException("A fresh, matching original reference frame is required for the preparation ray.");
        var offset = origin - Vector(geometry, "position"); var aim = destination - origin; var distance = aim.Length;
        if (!double.IsFinite(distance) || distance < 50 * LightSecond) throw new InvalidDataException("The actual target ray must remain at least 50ls long.");
        var direction = aim.Unit(); var horizon = distance - 10 * LightSecond; var bodies = geometry.GetProperty("nearbyBodies");
        if (bodies.ValueKind != JsonValueKind.Array || bodies.GetArrayLength() == 0) throw new InvalidDataException("Original body exclusion geometry is unavailable.");
        foreach (var body in bodies.EnumerateArray())
        {
            var centre = Vector(body, "centre") + offset; var radius = body.GetProperty("exclusionRadius").GetDouble();
            if (!centre.Finite || !double.IsFinite(radius) || radius <= 0) throw new InvalidDataException("Original body exclusion geometry is invalid.");
            var relative = centre - origin; var projection = Math.Clamp(Vector3d.Dot(relative, direction), 0, horizon);
            if ((relative - direction * projection).Length <= radius * 1.2)
                throw new InvalidOperationException("The actual target ray crosses an original exclusion envelope.");
        }
    }
}