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

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Diagnostics;
using System.Text.Json;
using XFEEDToolBox.Core.Models;

namespace XFEEDToolBox.Core.Session;

public interface INativeSessionEntryStatus
{
    Task<NativeSessionFrame> ReadEntryStatusAsync(CancellationToken token);
}
public sealed class NativeSessionConnectionFailedException(NativeSessionFrame frame,GameSessionTarget? client=null)
    :IOException("原版客户端已持续确认连接失败,尚未进入可驾驶会话。")
{
    public NativeSessionFrame Frame{get;}=frame;
    public GameSessionTarget? Client{get;}=client;
}
public sealed class StartupConnectionRecoveryExhaustedException(NativeSessionConnectionFailedException failure,int recoveries)
    :IOException($"原版连接失败已完成 {recoveries} 次有界恢复,保留客户端等待下一轮连接。",failure)
{
    public NativeSessionConnectionFailedException Failure{get;}=failure;
}
/// <summary>Repeated publications cannot turn a loading phase into a connection failure.</summary>
public sealed class NativeConnectionFailureObserver
{
    public TimeSpan ConfirmationPeriod{get;init;}=TimeSpan.FromSeconds(2);
    private NativeSessionFrame? first,last;
    public bool Observe(NativeSessionFrame frame,DateTimeOffset now)
    {
        if(!frame.Available||!NativeMenuExitSafety.Fresh(frame,now)||!frame.Stable||frame.State!="FailedToConnect")
        {first=last=null;return false;}
        if(last is {} previous)
        {
            if(frame.Sequence==previous.Sequence&&frame.ObservedAt==previous.ObservedAt)return false;
            if(frame.Sequence<=previous.Sequence||frame.ObservedAt<=previous.ObservedAt||frame.ObservedAt-previous.ObservedAt>TimeSpan.FromMilliseconds(500))first=null;
        }
        first??=frame;last=frame;
        return frame.ObservedAt-first.ObservedAt>=ConfirmationPeriod;
    }
}
public static class NativeSessionStartupReadiness
{
    public static bool JourneyEntryRequired(PlayerState current,NativeSessionFrame menu,JsonElement navigation,int processId,string hash,DateTimeOffset started,DateTimeOffset now)
    {
        bool Flag(string name,bool expected)=>navigation.TryGetProperty(name,out var value)&&value.ValueKind==(expected?JsonValueKind.True:JsonValueKind.False);
        if(navigation.GetProperty("schema").GetInt32()!=1||navigation.GetProperty("name").GetString()!="XFEEDToolBox.Navigation"
            ||navigation.GetProperty("processId").GetInt32()!=processId||navigation.GetProperty("gameSha256").GetString()!=hash||!Flag("verified",true))
            throw new InvalidDataException("完整航程的原生实例或构建身份不匹配。");
        if(Ready(current,menu,now,started)&&Flag("inGame",true))return false;
        if(!menu.Available||!NativeMenuExitSafety.Fresh(menu,now)||!menu.Stable
            ||!(NativeMenuExitSafety.MenuOrLoading(menu.State)||menu.State=="FailedToConnect")
            ||!(NativeMenuExitSafety.MenuOrLoading(menu.NextState)||menu.NextState=="FailedToConnect")
            ||!Flag("inGame",false)||!Flag("continuousPose",false)||!Flag("continuousHeat",false))
            throw new InvalidDataException("尚未确认原版主菜单或载入状态,已保留当前实例;未自动重启或发送驾驶输入。");
        if(current.LoadGameAt>=started&&Navigation.NavigationSafety.HasExternalThreat(current))throw new Navigation.FlightThreatPauseException();
        return true;
    }
    public static bool Ready(PlayerState current,NativeSessionFrame frame,DateTimeOffset now,DateTimeOffset? processStarted=null)
        =>frame.Available&&NativeMenuExitSafety.Fresh(frame,now)&&frame.Stable&&frame.State=="InGame"&&current.InGame
            &&current.LoadGameAt is {} loaded&&(processStarted is null||loaded>=processStarted)
            &&current.LocationAt>=loaded&&current.CargoReliable&&current.CargoAt>=loaded
            &&current.Navigation.CurrentSystemAddress is >0&&current.SystemName.Length>0;
    public static bool FailedWithoutLiveFlight(JsonElement menu,JsonElement navigation,int processId,string hash,DateTimeOffset now)
    {
        static bool Flag(JsonElement row,string key,bool expected)=>row.TryGetProperty(key,out var value)
            &&value.ValueKind==(expected?JsonValueKind.True:JsonValueKind.False);
        try
        {
            return processId>0&&menu.GetProperty("schema").GetInt32()==1&&menu.GetProperty("name").GetString()=="XFEEDToolBox.Session"
                &&menu.GetProperty("processId").GetInt32()==processId&&menu.GetProperty("gameSha256").GetString()==hash&&Flag(menu,"available",true)
                &&menu.GetProperty("sequence").GetInt64()>0&&menu.GetProperty("state").GetString()=="FailedToConnect"&&menu.GetProperty("nextState").GetString()=="FailedToConnect"
                &&now.ToUnixTimeMilliseconds()-menu.GetProperty("observedAtUnixMilliseconds").GetInt64() is >=-100 and <=350
                &&navigation.GetProperty("schema").GetInt32()==1&&navigation.GetProperty("name").GetString()=="XFEEDToolBox.Navigation"
                &&navigation.GetProperty("processId").GetInt32()==processId&&navigation.GetProperty("gameSha256").GetString()==hash&&Flag(navigation,"verified",true)
                // ready describes the initialized service, not a live cockpit.
                &&Flag(navigation,"inGame",false)&&Flag(navigation,"continuousPose",false)&&Flag(navigation,"continuousHeat",false);
        }
        catch(Exception error)when(error is KeyNotFoundException or InvalidOperationException or FormatException or OverflowException){return false;}
    }
}
/// <summary>Only a typed original-menu failure can retry. It never replays a trade.</summary>
public static class StartupConnectionRecovery
{
    /// <summary>Disk/report work must finish before this final live witness; never kill from its earlier snapshot.</summary>
    public static async Task TerminateWithFreshEvidenceAsync(Func<CancellationToken,Task<bool>> verify,
        Func<CancellationToken,Task> terminate,CancellationToken token)
    {
        token.ThrowIfCancellationRequested();
        var confirmed=await verify(token).ConfigureAwait(false);
        token.ThrowIfCancellationRequested();
        if(!confirmed)throw new InvalidDataException("结束客户端前原版连接失败或无驾驶场景证据已改变,当前实例已保留。");
        await terminate(token).ConfigureAwait(false);
    }
    public static async Task RunAsync(Func<Action<GameSessionTarget>,CancellationToken,Task> attempt,
        Func<GameSessionTarget,NativeSessionConnectionFailedException,bool,int,CancellationToken,Task> recover,CancellationToken token,
        bool allowVerifiedExisting=false,int maximumRecoveries=2,TimeSpan? totalTimeout=null,Func<int,TimeSpan>? retryDelay=null)
    {
        if(maximumRecoveries is <0 or >3)throw new ArgumentOutOfRangeException(nameof(maximumRecoveries));
        var limit=totalTimeout??TimeSpan.FromMinutes(20);if(limit<=TimeSpan.Zero)throw new ArgumentOutOfRangeException(nameof(totalTimeout));
        using var budget=CancellationTokenSource.CreateLinkedTokenSource(token);budget.CancelAfter(limit);
        try
        {
            for(var count=0;;count++)
            {
                GameSessionTarget? owned=null;
                try{budget.Token.ThrowIfCancellationRequested();await attempt(client=>owned=client,budget.Token).ConfigureAwait(false);return;}
                catch(NativeSessionConnectionFailedException failure)when(!budget.IsCancellationRequested&&count<maximumRecoveries&&failure.Client is not null
                    &&(allowVerifiedExisting||owned==failure.Client))
                {
                    var client=failure.Client!;var isOwned=owned==client;
                    await Task.Delay(retryDelay?.Invoke(count+1)??TimeSpan.FromSeconds(count==0?5:15),budget.Token).ConfigureAwait(false);
                    await recover(client,failure,isOwned,count+1,budget.Token).ConfigureAwait(false);
                }
                catch(NativeSessionConnectionFailedException failure)when(!budget.IsCancellationRequested&&count>=maximumRecoveries&&failure.Client is not null
                    &&(allowVerifiedExisting||owned==failure.Client))
                {throw new StartupConnectionRecoveryExhaustedException(failure,count);}
            }
        }
        catch(OperationCanceledException)when(!token.IsCancellationRequested&&budget.IsCancellationRequested)
        {throw new TimeoutException("原版连接失败恢复已达到总期限,客户端和原订单已保留。");}
    }
    public static async Task SuperviseAsync(Func<CancellationToken,Task> enter,
        Func<StartupConnectionRecoveryExhaustedException,int,CancellationToken,Task> wait,CancellationToken token)
    {
        var rounds=0;
        while(true)
        {
            token.ThrowIfCancellationRequested();
            // The waiting callback can update the invoking UI's run status.
            // Keep that context; the startup operation itself owns its worker.
            try{await enter(token);return;}
            catch(StartupConnectionRecoveryExhaustedException failure)when(!token.IsCancellationRequested)
            {await wait(failure,++rounds,token);}
        }
    }
}