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"&¤t.InGame
&¤t.LoadGameAt is {} loaded&&(processStarted is null||loaded>=processStarted)
&¤t.LocationAt>=loaded&¤t.CargoReliable&¤t.CargoAt>=loaded
&¤t.Navigation.CurrentSystemAddress is >0&¤t.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);}
}
}
}
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"&¤t.InGame
&¤t.LoadGameAt is {} loaded&&(processStarted is null||loaded>=processStarted)
&¤t.LocationAt>=loaded&¤t.CargoReliable&¤t.CargoAt>=loaded
&¤t.Navigation.CurrentSystemAddress is >0&¤t.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);}
}
}
}