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

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Nodes;
using XFEEDToolBox.Core.Plugins;

namespace XFEEDToolBox.Core.Navigation;

public sealed record NativeCruiseControlCapabilities(bool Available,bool Qualified,string Service,int OwningThread);

/// <summary>Program-owned SCO policy and bounded original-command receipts. Never steers or selects a target.</summary>
public sealed class NativeCruiseControlChannel(int processId,string gameSha256)
{
    public const string Service=CruiseControlProtocol.Service;
    private readonly NavigationPipeClient pipe=new(processId,Service);
    private readonly CruiseOverchargeSampleHistory history=new(gameSha256);
    private readonly CruiseOverchargePolicy policy=new();
    private ulong entity,world;
    private ulong boundInterface,originalStartSelection;
    private long originalStartAt;
    private bool leaseRequested,originalStartWitness;
    private string owner="";
    private string boundLoadoutFingerprint="";
    private string? pendingStopId,lastStopIdentity,originalStartRequestId;
    private DateTimeOffset nativeStartRefusedUntil;
    private SystemFlightFrame? applicationFrame;
    private SystemFlightDecision? applicationDecision;
    private DateTimeOffset applicationDeadline;
    private string applicationStage="",applicationDetail="";
    public JsonElement? LastStatus{get;private set;}
    public CruiseOverchargeObservation? LastObservation{get;private set;}
    public CruiseOverchargeDecision? LastDecision{get;private set;}
    public event Action<JsonElement>? Observed;
    public event Action<JsonElement>? OriginalReceipt;
    public event Action<JsonElement>? OriginalStopReceipt;
    /// <summary>Only arms a bounded strategy trial; does not enable SCO, steer, select or start navigation.</summary>
    public Task<JsonElement> ConfigureExitEstimateTrialAsync(ulong shipEntity,ulong referenceWorld,DateTimeOffset expires,int maximumBursts,CancellationToken token=default)
        =>pipe.CallAsync(new{op="sco.exit.configure",mode="trial",entityId=shipEntity,referenceWorldAddress=referenceWorld,
            expiresAtUnixMilliseconds=expires.ToUnixTimeMilliseconds(),maximumBursts},token);
    public Task<JsonElement> UseShadowExitEstimateAsync(CancellationToken token=default)=>pipe.CallAsync(new{op="sco.exit.configure",mode="shadow"},token);
    public Task<JsonElement> PromoteExitEstimateProductionAsync(ulong shipEntity,ulong referenceWorld,DateTimeOffset expires,CancellationToken token=default)
        =>pipe.CallAsync(new{op="sco.exit.configure",mode="production",entityId=shipEntity,referenceWorldAddress=referenceWorld,
            expiresAtUnixMilliseconds=expires.ToUnixTimeMilliseconds()},token);
    private static bool Flag(JsonElement value,string key)=>value.TryGetProperty(key,out var found)&&found.ValueKind==JsonValueKind.True;
    private static ulong Identity(JsonElement value,string key)=>ulong.Parse(value.GetProperty(key).GetString()!,CultureInfo.InvariantCulture);
    private static bool Key(JsonElement value,string key)=>value.TryGetProperty(key,out var item)&&item.ValueKind==JsonValueKind.String
        &&item.GetString() is {Length:32} text&&text.All(c=>c is >= '0' and <= '9' or >= 'a' and <= 'f');
    private void CheckIdentity(JsonElement result)
    {
        if(result.GetProperty("schema").GetInt32()!=1||result.GetProperty("processId").GetInt32()!=processId
            ||result.GetProperty("gameSha256").GetString()!=gameSha256||result.GetProperty("service").GetString()!=Service
            ||!Flag(result,"overchargeControl")||!Flag(result,"enabled")||!Flag(result,"originalFuelRangeObservations")
            ||!Flag(result,"nativeStopReceipts")||!Flag(result,"componentThermalModel")||!Flag(result,"expiringOriginalControl")||result.GetProperty("controlTelemetrySchema").GetInt32()!=1
            ||result.GetProperty("owningThread").GetInt32()<=0)throw new InvalidDataException("Original SCO controller does not match this client.");
        if(Flag(result,"overchargeControlQualified"))
        {
            if(!result.TryGetProperty("qualifiedCycle",out var cycle)||cycle.ValueKind!=JsonValueKind.Object
                ||!Key(cycle,"owner")||!Key(cycle,"startRequestId")||!Key(cycle,"releaseRequestId")
                ||cycle.GetProperty("selectionGeneration").GetUInt64()==0||Identity(cycle,"entityId")==0||Identity(cycle,"referenceWorldAddress")==0
                ||cycle.GetProperty("activeSamples").GetInt32()<5)throw new InvalidDataException("Original SCO qualification has no matching live cycle.");
            var enabledAt=cycle.GetProperty("enableAtUnixMilliseconds").GetUInt64();var activeAt=cycle.GetProperty("lastActiveAtUnixMilliseconds").GetUInt64();
            var stopAt=cycle.GetProperty("originalStopAtUnixMilliseconds").GetUInt64();var offAt=cycle.GetProperty("offAtUnixMilliseconds").GetUInt64();
            if(enabledAt==0||activeAt<enabledAt||stopAt<activeAt||offAt<=stopAt||offAt>(ulong)DateTimeOffset.UtcNow.AddMilliseconds(100).ToUnixTimeMilliseconds())
                throw new InvalidDataException("Original SCO qualification timestamps do not establish enable, Active, stop and subsequent Off.");
        }
    }
    private void Capture(JsonElement result)
    {
        CheckIdentity(result);LastStatus=result.Clone();
        if(result.TryGetProperty("lastOriginalStop",out var stop)&&stop.ValueKind==JsonValueKind.Object&&stop.GetProperty("owner").GetString()==owner)
        {
            var id=stop.GetRawText();if(id!=lastStopIdentity){lastStopIdentity=id;OriginalStopReceipt?.Invoke(stop.Clone());}
        }
        // A real Off sample in this response follows its original stop receipt.
        var traced=JsonNode.Parse(result.GetRawText())!.AsObject();
        traced["applicationControl"]=JsonSerializer.SerializeToNode(new{stage=applicationStage,detail=applicationDetail,
            sequence=applicationFrame?.Sequence,flightObservedAt=applicationFrame?.ObservedAt,deadline=applicationDeadline,
            phase=applicationDecision?.Phase.ToString(),actions=applicationDecision?.Actions.ToString(),
            useOvercharge=LastDecision?.UseOvercharge,policyReason=LastDecision?.Reason.ToString()});
        Observed?.Invoke(JsonSerializer.SerializeToElement(traced));
    }
    public async Task<NativeCruiseControlCapabilities> InspectAsync(CancellationToken token)
    {
        var result=await pipe.CallAsync(new{op="describe"},token);CheckIdentity(result);
        return new(true,Flag(result,"overchargeControlQualified"),Service,result.GetProperty("owningThread").GetInt32());
    }
    /// <summary>Read-only proof for the local navigator; does not consume or advance command sample history.</summary>
    public async Task<DateTimeOffset?> ReadThermalControlExpiryAsync(SystemFlightFrame flight,CancellationToken token)
    {
        if(flight.Mode!=LocalFlightMode.Supercruise||flight.Assists.Supercruise
            ||flight.SupercruiseAssist is NativeAssistState.Requested or NativeAssistState.Aligning or NativeAssistState.Engaged
            ||!flight.InGame||flight.PlayerOverride||flight.ExternalThreat||flight.ShipEntity==0||flight.ReferenceWorld==0
            ||flight.GameSha256!=gameSha256||flight.HeatPercent is not >=0 or >=100)return null;
        try
        {
            var status=await pipe.CallAsync(new{op="read"},token);CheckIdentity(status);
            if(!Flag(status,"sampleReady")||!status.TryGetProperty("sample",out var sample)||sample.ValueKind!=JsonValueKind.Object
                ||!Flag(sample,"verified")||!Flag(sample,"inSupercruise")||!Flag(sample,"stableFsd")
                ||Identity(sample,"entityId")!=flight.ShipEntity||Identity(sample,"referenceWorldAddress")!=flight.ReferenceWorld
                ||sample.GetProperty("rawFsdState").GetInt32()!=2||sample.GetProperty("state").GetInt32() is <0 or >3)return null;
            var now=DateTimeOffset.UtcNow;var at=DateTimeOffset.FromUnixTimeMilliseconds(sample.GetProperty("observedAtUnixMilliseconds").GetInt64());
            if(at>now||now-at>TimeSpan.FromMilliseconds(200)||flight.ObservedAt>now||now-flight.ObservedAt>TimeSpan.FromMilliseconds(200))return null;
            if(ScoThermalJson.Read(sample,flight.ShipEntity,flight.ReferenceWorld,at) is not {} model)return null;
            var age=(now-at).TotalSeconds;
            var forecast=ScoThermalPredictor.Predict(model with{HeatPercent=Math.Max(model.HeatPercent,flight.HeatPercent!.Value),
                SourceAgeSeconds=model.SourceAgeSeconds+age,HeatAgeSeconds=model.HeatAgeSeconds+age,LeaseRemainingSeconds=.25});
            if(!forecast.Known||forecast.ScoOffNetRisePercentPerSecond>=0)return null;
            var oldest=at.AddSeconds(-Math.Max(model.SourceAgeSeconds,model.HeatAgeSeconds));
            if(flight.ObservedAt<oldest)oldest=flight.ObservedAt;
            return oldest.AddMilliseconds(200);
        }
        catch(Exception error)when(!token.IsCancellationRequested&&error is IOException or OperationCanceledException or InvalidOperationException
            or InvalidDataException or KeyNotFoundException or FormatException or OverflowException or ArgumentOutOfRangeException)
        {return null;}
    }
    public async Task<NativeCruiseControlCapabilities> PrepareAsync(string packageDirectory,CancellationToken token)
    {
        try{return await InspectAsync(token).ConfigureAwait(false);}
        catch(Exception error)when(!token.IsCancellationRequested&&error is IOException or OperationCanceledException){}
        var hello=await new NavigationPipeClient(processId).CallAsync(new{op="describe"},token).ConfigureAwait(false);
        if(hello.GetProperty("processId").GetInt32()!=processId||hello.GetProperty("gameSha256").GetString()!=gameSha256)
            throw new InvalidDataException("SCO扩展与当前游戏实例不匹配。");
        await RetireIdleLegacyAsync(token).ConfigureAwait(false);
        await RuntimeInteractionModule.LoadAsync(processId,gameSha256,hello.GetProperty("owningThread").GetInt32(),packageDirectory,token).ConfigureAwait(false);
        return await InspectAsync(token).ConfigureAwait(false);
    }
    private async Task RetireIdleLegacyAsync(CancellationToken token)
    {
        var legacy=new NavigationPipeClient(processId,CruiseControlProtocol.LegacyService);JsonElement status;
        try{status=await legacy.CallAsync(new{op="read"},token).ConfigureAwait(false);}
        catch(Exception error)when(!token.IsCancellationRequested&&error is IOException or OperationCanceledException){return;}
        if(!Flag(status,"enabled"))return;
        var now=DateTimeOffset.UtcNow;
        if(Flag(status,"owned")||status.GetProperty("leaseExpiresAtUnixMilliseconds").GetInt64()>now.ToUnixTimeMilliseconds())
            throw new FlightTelemetryUnavailableException("等待上一段 SCO 归还控制后连接新版扩展。");
        status=await legacy.CallAsync(new{op="watch"},token).ConfigureAwait(false);
        var until=DateTimeOffset.UtcNow.AddSeconds(2);
        while(!CruiseControllerReplacement.CanRetire(status,processId,gameSha256,DateTimeOffset.UtcNow))
        {
            if(DateTimeOffset.UtcNow>=until)throw new FlightTelemetryUnavailableException("等待原版 SCO 停止,当前飞行保留。");
            await Task.Delay(25,token);status=await legacy.CallAsync(new{op="read"},token).ConfigureAwait(false);
        }
        var retired=await legacy.CallAsync(new{op="deactivate"},token).ConfigureAwait(false);
        if(Flag(retired,"enabled")||Flag(retired,"owned"))throw new InvalidDataException("上一版 SCO 扩展尚未确认释放。");
    }
    public async Task UpdateAsync(string controllerOwner,ulong selectionGeneration,SystemFlightFrame flight,SystemFlightDecision decision,
        SystemNavigationTarget target,double reserve,DateTimeOffset deadline,CancellationToken token)
    {
        applicationFrame=flight;applicationDecision=decision;applicationDeadline=deadline;applicationStage="flight-update";applicationDetail="";
        if(owner.Length!=0&&owner!=controllerOwner)throw new InvalidOperationException("SCO channel is already bound to another controller.");
        owner=controllerOwner;
        var mayBoost=flight.Mode==LocalFlightMode.Supercruise&&(decision.Actions&~SystemNavigationAction.CancelOwnedAssists)==SystemNavigationAction.None
            &&decision.Phase is SystemNavigationPhase.Supercruise or SystemNavigationPhase.Aligning;
        if(!mayBoost){await ReleaseAsync(token,"ineligible-flight-phase");return;}
        if(DateTimeOffset.UtcNow<nativeStartRefusedUntil&&!leaseRequested)return;
        if(flight.GameSha256!=gameSha256||flight.ShipEntity==0||flight.ReferenceWorld==0||selectionGeneration==0)
            throw new InvalidDataException("SCO requires the current original flight target and controller.");
        if(entity!=flight.ShipEntity||world!=flight.ReferenceWorld||boundLoadoutFingerprint!=flight.LoadoutFingerprint)
        {
            await ReleaseAsync(token);var bound=await pipe.CallAsync(new{op="watch",loadoutFingerprint=flight.LoadoutFingerprint},token);Capture(bound);
            entity=flight.ShipEntity;world=flight.ReferenceWorld;boundLoadoutFingerprint=flight.LoadoutFingerprint;
            boundInterface=bound.TryGetProperty("watchInterface",out var component)&&component.ValueKind==JsonValueKind.String
                &&ulong.TryParse(component.GetString(),NumberStyles.HexNumber,CultureInfo.InvariantCulture,out var address)?address:0;
            history.Reset();originalStartWitness=false;
        }
        var status=await pipe.CallAsync(new{op="read"},token);Capture(status);
        if(leaseRequested&&!Flag(status,"owned"))
        {
            var safelyStopped=originalStartWitness&&ScoStoppedBurst.CanContinue(status,owner,originalStartRequestId,selectionGeneration,flight,DateTimeOffset.UtcNow);
            RememberApproachStop(status,flight,selectionGeneration);
            // The native guard may stop our burst between updates. Observe
            // its real Off state before requesting another original command.
            await ReleaseAsync(token);
            if(safelyStopped)
            {
                policy.ObserveNativeStartRefusal(DateTimeOffset.UtcNow);nativeStartRefusedUntil=DateTimeOffset.UtcNow.AddSeconds(1);
                LastDecision=new(false,CruiseOverchargeReason.Unavailable);return;
            }
            throw new FlightTelemetryUnavailableException("Original SCO burst ended; refresh flight telemetry before continuing.");
        }
        var observed=history.Read(status,flight,DateTimeOffset.UtcNow,allowRepeated:leaseRequested&&Flag(status,"owned"));LastObservation=observed;
        if(!Flag(status,"owned")&&status.TryGetProperty("sample",out var sample)&&sample.ValueKind==JsonValueKind.Object&&!Flag(sample,"active"))originalStartWitness=false;
        var selected=policy.Decide(flight,observed,reserve,DateTimeOffset.UtcNow,deadline);LastDecision=selected;
        applicationStage="policy";Capture(status);
        if(!selected.UseOvercharge){await ReleaseAsync(token,"policy:"+selected.Reason);return;}
        var requestId=Guid.NewGuid().ToString("N");
        // Mark before IPC: a lost reply may still have queued an original call.
        leaseRequested=true;
        JsonElement result;
        try
        {
            result=await pipe.CallAsync(new{op="cruise.set",owner,requestId,selectionGeneration,sequence=flight.Sequence,
                expiresAtUnixMilliseconds=deadline.ToUnixTimeMilliseconds(),requiredFuelReserveTonnes=reserve,
                target=new{key=target.NativeKey,bodyAddress=target.BodyAddress.ToString(CultureInfo.InvariantCulture),kind=target.NativeKind,marketId=target.MarketId??uint.MaxValue,name=target.Name}},token);
        }
        catch(InvalidOperationException error)when(error.Message=="SCO was already active outside this controller."&&originalStartWitness)
        {
            var stopped=await pipe.CallAsync(new{op="read"},token);Capture(stopped);
            if(Flag(stopped,"owned")||!stopped.TryGetProperty("lastOriginalStop",out var stop)||stop.ValueKind!=JsonValueKind.Object
                ||stop.GetProperty("owner").GetString()!=owner||stop.GetProperty("startRequestId").GetString()!=originalStartRequestId
                ||stop.GetProperty("selectionGeneration").GetUInt64()!=selectionGeneration||Identity(stop,"entityId")!=entity||Identity(stop,"referenceWorldAddress")!=world
                ||!Flag(stop,"nativeMethodCalled")||Flag(stop,"argument")||stop.GetProperty("nativeMethodRva").GetString()!="2eaacd0")throw;
            await ReleaseAsync(token);
            throw new FlightTelemetryUnavailableException("Original SCO stop overlapped its renewal; refresh telemetry before continuing.",error);
        }
        var command=result.GetProperty("generation").GetUInt64();
        while(true)
        {
            Capture(result);
            if(result.TryGetProperty("receipt",out var receipt)&&receipt.ValueKind==JsonValueKind.Object&&receipt.GetProperty("requestId").GetString()==requestId)
            {
                if(Flag(receipt,"failed"))
                {
                    // A rejected optional start is not loss of ordinary flight
                    // telemetry. Only a matching, never-called, healthy/off
                    // receipt can leave the primary control lease running.
                    if(!originalStartWitness&&ScoStartRefusal.CanContinueOrdinaryFlight(result,receipt,owner,requestId,command,selectionGeneration,flight,DateTimeOffset.UtcNow))
                    {
                        leaseRequested=false;pendingStopId=null;nativeStartRefusedUntil=DateTimeOffset.UtcNow.AddSeconds(1);
                        policy.ObserveNativeStartRefusal(DateTimeOffset.UtcNow);
                        LastDecision=new(false,CruiseOverchargeReason.Unavailable);return;
                    }
                    throw new FlightTelemetryUnavailableException("Original SCO interlock refused: "+(receipt.TryGetProperty("reason",out var why)?why.GetString():"unknown"));
                }
                if(Flag(receipt,"applied"))
                {
                    if(receipt.GetProperty("owner").GetString()!=owner||receipt.GetProperty("generation").GetUInt64()!=command
                        ||receipt.GetProperty("selectionGeneration").GetUInt64()!=selectionGeneration||Identity(receipt,"entityId")!=entity||Identity(receipt,"referenceWorldAddress")!=world)
                        throw new InvalidDataException("Original SCO receipt identity differs.");
                    var called=Flag(receipt,"nativeMethodCalled");
                    if(called)
                    {
                        if(receipt.GetProperty("nativeMethodRva").GetString()!="2eaacd0"||!Flag(receipt,"argument")||receipt.GetProperty("actualStateAfterCall").GetInt32() is not(1 or 2))
                            throw new InvalidDataException("Original SCO enable was not confirmed.");
                        originalStartWitness=true;
                        originalStartRequestId=requestId;
                        originalStartSelection=selectionGeneration;
                        originalStartAt=receipt.TryGetProperty("observedAtUnixMilliseconds",out var started)&&started.TryGetInt64(out var startAt)?startAt:0;
                    }
                    else if(!originalStartWitness)throw new InvalidDataException("SCO renewal has no original enable witness.");
                    OriginalReceipt?.Invoke(receipt.Clone());return;
                }
            }
            if(DateTimeOffset.UtcNow>=deadline)throw new FlightTelemetryUnavailableException("Original SCO command expired without a matching receipt.");
            await Task.Delay(10,token);result=await pipe.CallAsync(new{op="read"},token);
        }
    }
    private void RememberApproachStop(JsonElement status,SystemFlightFrame flight,ulong selectionGeneration)
    {
        if(!originalStartWitness||originalStartRequestId is null||flight.SelectedTarget is null
            ||!status.TryGetProperty("lastOriginalStop",out var stop)||stop.ValueKind!=JsonValueKind.Object
            ||stop.GetProperty("owner").GetString()!=owner||stop.GetProperty("startRequestId").GetString()!=originalStartRequestId
            ||stop.GetProperty("selectionGeneration").GetUInt64()!=selectionGeneration||Identity(stop,"entityId")!=entity||Identity(stop,"referenceWorldAddress")!=world
            ||!Flag(stop,"nativeMethodCalled")||Flag(stop,"argument")||stop.GetProperty("nativeMethodRva").GetString()!="2eaacd0"
            ||stop.GetProperty("reason").GetString()!="approach-or-alignment"||!stop.TryGetProperty("gate",out var gate)||gate.ValueKind!=JsonValueKind.Object
            ||!Flag(gate,"identityVerified")||!Flag(gate,"targetVerified")||!Flag(gate,"geometryVerified"))return;
        static double Number(JsonElement row,string key)=>row.TryGetProperty(key,out var v)&&v.ValueKind==JsonValueKind.Number?v.GetDouble():double.NaN;
        var distance=Number(gate,"distanceMetres");var speed=Number(gate,"speedMetresPerSecond");
        var approach=ScoExitApproachDecision.Read(gate);
        var stoppedAt=stop.GetProperty("observedAtUnixMilliseconds").GetUInt64();
        if(double.IsFinite(distance)&&distance>0&&double.IsFinite(speed)&&speed>=0
            &&(approach?.RequiresStop(distance,speed,true,Flag(gate,"cruiseParametersVerified"),Number(gate,"cruiseApproachSeconds"),
                entity,world,flight.SelectedTarget.NativeKey,stoppedAt)
                ??ScoApproachEnvelope.RequiresStop(distance,speed,true,Flag(gate,"cruiseParametersVerified"),Number(gate,"cruiseApproachSeconds"))))
            policy.ObserveNativeApproachStop(flight);
    }
    public async Task ReleaseAsync(CancellationToken token=default,string cause="driver-release")
    {
        if(!leaseRequested)return;
        applicationStage="release";applicationDetail=cause;
        pendingStopId??=Guid.NewGuid().ToString("N");
        var result=await pipe.CallAsync(new{op="cruise.release",owner,requestId=pendingStopId},token);
        var until=DateTimeOffset.UtcNow.AddSeconds(2);
        while(true)
        {
            var status=await pipe.CallAsync(new{op="read"},token);Capture(status);
            if(!Flag(status,"owned")&&Flag(status,"sampleReady")&&status.TryGetProperty("sample",out var sample)&&sample.ValueKind==JsonValueKind.Object
                &&Identity(sample,"entityId")==entity&&Identity(sample,"referenceWorldAddress")==world&&sample.GetProperty("state").GetInt32() is 0 or 3)
            {leaseRequested=false;originalStartWitness=false;pendingStopId=null;return;}
            // A natural SOI transition changes the observed world before the
            // old watch binding can be replaced. Its old world will never be
            // sampled again. Only our actual stop and the same live component
            // reporting Off in its new world can finish this old ownership.
            if(originalStartWitness&&ScoReferenceWorldRelease.CanConfirm(status,owner,originalStartRequestId,
                originalStartSelection,originalStartAt,entity,world,boundInterface,DateTimeOffset.UtcNow))
            {leaseRequested=false;originalStartWitness=false;pendingStopId=null;return;}
            if(!Flag(status,"owned")&&result.TryGetProperty("released",out var released)&&released.ValueKind==JsonValueKind.True&&!originalStartWitness)
            {leaseRequested=false;pendingStopId=null;return;}
            if(DateTimeOffset.UtcNow>=until)throw new FlightTelemetryUnavailableException("Waiting for the owned SCO to report its real stopped state.");
            await Task.Delay(15,token);
        }
    }
}