using System.Diagnostics;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Navigation;
namespace XFEEDToolBox.Core.Session;
/// <summary>Enters the selected original mode; never edits account or menu availability fields.</summary>
public sealed class NativeSessionEntryWorkflow(INativeRelogChannel channel,Func<PlayerState> player,string mode,string commander)
{
public TimeSpan Timeout{get;init;}=TimeSpan.FromMinutes(8);
public TimeSpan PollInterval{get;init;}=TimeSpan.FromMilliseconds(60);
public TimeSpan ConnectionFailureConfirmation{get;init;}=TimeSpan.FromSeconds(2);
public DateTimeOffset? ProcessStartedAt{get;init;}
public long? ExpectedShipId{get;init;}
public event Action<string>? Progress;
public async Task RunAsync(CancellationToken token)
{
if(!NativeRelogWorkflow.SupportsMode(mode))throw new NotSupportedException("自动恢复只支持已接入的单人或公开模式。");
var owner=Guid.NewGuid().ToString("N");var watch=Stopwatch.StartNew();string? request=null;var initial=player().LoadGameAt;
var attempted=false;string? lastState=null;
var failed=new NativeConnectionFailureObserver{ConfirmationPeriod=ConnectionFailureConfirmation};
void RequireIdentity()
{
token.ThrowIfCancellationRequested();var current=player();
if(commander.Length>0&¤t.CommanderId.Length>0&¤t.CommanderId!=commander
||ExpectedShipId is {} ship&¤t.CurrentShipId is {} actual&&actual!=ship
||current.GameMode.Length>0&¤t.GameMode!=mode&¤t.LoadGameAt is {} loaded
&&(ProcessStartedAt is {} started?loaded>=started:initial is null||loaded>initial))
throw new InvalidDataException("原始载入期间指挥官或飞船身份改变,未自动重启。");
}
try
{
attempted=true;var begun=false;
Progress?.Invoke("正在等待游戏原始主菜单就绪");
while(watch.Elapsed<Timeout)
{
token.ThrowIfCancellationRequested();
RequireIdentity();
if(channel is INativeSessionEntryStatus status)
{
NativeSessionFrame? observed=null;
try{observed=await status.ReadEntryStatusAsync(token);}
catch(Exception error)when(!token.IsCancellationRequested&&!NavigationRecovery.HasInvalidDataCause(error)&&error is IOException or OperationCanceledException){}
RequireIdentity();
if(observed is not null&&failed.Observe(observed,DateTimeOffset.UtcNow))throw new NativeSessionConnectionFailedException(observed);
RequireIdentity();var current=player();
if(observed is not null&&NativeSessionStartupReadiness.Ready(current,observed,DateTimeOffset.UtcNow,ProcessStartedAt)
&¤t.GameMode==mode&¤t.CommanderId.Length>0&&(ProcessStartedAt is not null||initial is null||current.LoadGameAt>initial))return;
}
try{await channel.BeginAsync(owner,token).ConfigureAwait(false);begun=true;break;}
catch(InvalidOperationException error)when(error.Message.StartsWith("Wait for an idle game and stop automatic flight before relogging",StringComparison.Ordinal)){}
catch(Exception error)when(!token.IsCancellationRequested&&!NavigationRecovery.HasInvalidDataCause(error)&&error is IOException or OperationCanceledException){}
await Task.Delay(PollInterval,token).ConfigureAwait(false);
}
if(!begun)throw new TimeoutException("游戏原始主菜单未就绪,当前客户端已保留。");
while(watch.Elapsed<Timeout)
{
token.ThrowIfCancellationRequested();NativeSessionFrame frame;
try{frame=await channel.ReadAsync(owner,token).ConfigureAwait(false);}
catch(Exception error)when(!token.IsCancellationRequested&&!NavigationRecovery.HasInvalidDataCause(error)&&error is IOException or OperationCanceledException){await Task.Delay(PollInterval,token);continue;}
RequireIdentity();var age=DateTimeOffset.UtcNow-frame.ObservedAt;
if(!frame.Available||frame.Sequence<=0||age>TimeSpan.FromMilliseconds(350)||age<TimeSpan.FromMilliseconds(-100)){await Task.Delay(PollInterval,token);continue;}
if(failed.Observe(frame,DateTimeOffset.UtcNow))throw new NativeSessionConnectionFailedException(frame);
if(frame.Stable&&frame.State=="FailedToConnect"){await Task.Delay(PollInterval,token);continue;}
if(frame.State!=lastState){lastState=frame.State;Progress?.Invoke("正在进入游戏:"+frame.State);}
var current=player();
if(NativeSessionStartupReadiness.Ready(current,frame,DateTimeOffset.UtcNow,ProcessStartedAt)&¤t.LoadGameAt is {} loaded&&(ProcessStartedAt is not null||initial is null||loaded>initial)
&¤t.GameMode==mode&¤t.CommanderId.Length>0)
{
if(commander.Length>0&¤t.CommanderId!=commander)throw new InvalidOperationException("重新进入的指挥官与原会话不同。");
return;
}
if(request is not null)
{
if(frame.LastRequestId==request&&(frame.ActionApplied||frame.ActionFailed))
{if(frame.ActionFailed)throw new IOException("原始游戏模式选择失败,未重放选择。");request=null;}
else{await Task.Delay(PollInterval,token);continue;}
}
var action=frame.Stable?frame.State switch{"MainMenu"=>"ContinueMenu","ContinueMenu"=>mode=="Solo"?"SetupPlaySolo":"SetupPlayWithEveryone",_=>null}:null;
if(action is not null&&frame.Actions.Count(a=>a.Enabled&&a.Name==action)==1)
{
request=Guid.NewGuid().ToString("N");
try{await channel.SubmitAsync(owner,request,frame,action,token).ConfigureAwait(false);}
catch(NativeSessionStateChangedException){request=null;}
catch(IOException error)when(!NavigationRecovery.HasInvalidDataCause(error)){}catch(OperationCanceledException)when(!token.IsCancellationRequested){}
}
await Task.Delay(PollInterval,token).ConfigureAwait(false);
}
throw new TimeoutException("游戏原始载入未完成,当前客户端与日志已保留。");
}
finally{if(attempted)await channel.EndAsync(owner).ConfigureAwait(false);}
}
}
using System.Diagnostics;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Navigation;
namespace XFEEDToolBox.Core.Session;
/// <summary>Enters the selected original mode; never edits account or menu availability fields.</summary>
public sealed class NativeSessionEntryWorkflow(INativeRelogChannel channel,Func<PlayerState> player,string mode,string commander)
{
public TimeSpan Timeout{get;init;}=TimeSpan.FromMinutes(8);
public TimeSpan PollInterval{get;init;}=TimeSpan.FromMilliseconds(60);
public TimeSpan ConnectionFailureConfirmation{get;init;}=TimeSpan.FromSeconds(2);
public DateTimeOffset? ProcessStartedAt{get;init;}
public long? ExpectedShipId{get;init;}
public event Action<string>? Progress;
public async Task RunAsync(CancellationToken token)
{
if(!NativeRelogWorkflow.SupportsMode(mode))throw new NotSupportedException("自动恢复只支持已接入的单人或公开模式。");
var owner=Guid.NewGuid().ToString("N");var watch=Stopwatch.StartNew();string? request=null;var initial=player().LoadGameAt;
var attempted=false;string? lastState=null;
var failed=new NativeConnectionFailureObserver{ConfirmationPeriod=ConnectionFailureConfirmation};
void RequireIdentity()
{
token.ThrowIfCancellationRequested();var current=player();
if(commander.Length>0&¤t.CommanderId.Length>0&¤t.CommanderId!=commander
||ExpectedShipId is {} ship&¤t.CurrentShipId is {} actual&&actual!=ship
||current.GameMode.Length>0&¤t.GameMode!=mode&¤t.LoadGameAt is {} loaded
&&(ProcessStartedAt is {} started?loaded>=started:initial is null||loaded>initial))
throw new InvalidDataException("原始载入期间指挥官或飞船身份改变,未自动重启。");
}
try
{
attempted=true;var begun=false;
Progress?.Invoke("正在等待游戏原始主菜单就绪");
while(watch.Elapsed<Timeout)
{
token.ThrowIfCancellationRequested();
RequireIdentity();
if(channel is INativeSessionEntryStatus status)
{
NativeSessionFrame? observed=null;
try{observed=await status.ReadEntryStatusAsync(token);}
catch(Exception error)when(!token.IsCancellationRequested&&!NavigationRecovery.HasInvalidDataCause(error)&&error is IOException or OperationCanceledException){}
RequireIdentity();
if(observed is not null&&failed.Observe(observed,DateTimeOffset.UtcNow))throw new NativeSessionConnectionFailedException(observed);
RequireIdentity();var current=player();
if(observed is not null&&NativeSessionStartupReadiness.Ready(current,observed,DateTimeOffset.UtcNow,ProcessStartedAt)
&¤t.GameMode==mode&¤t.CommanderId.Length>0&&(ProcessStartedAt is not null||initial is null||current.LoadGameAt>initial))return;
}
try{await channel.BeginAsync(owner,token).ConfigureAwait(false);begun=true;break;}
catch(InvalidOperationException error)when(error.Message.StartsWith("Wait for an idle game and stop automatic flight before relogging",StringComparison.Ordinal)){}
catch(Exception error)when(!token.IsCancellationRequested&&!NavigationRecovery.HasInvalidDataCause(error)&&error is IOException or OperationCanceledException){}
await Task.Delay(PollInterval,token).ConfigureAwait(false);
}
if(!begun)throw new TimeoutException("游戏原始主菜单未就绪,当前客户端已保留。");
while(watch.Elapsed<Timeout)
{
token.ThrowIfCancellationRequested();NativeSessionFrame frame;
try{frame=await channel.ReadAsync(owner,token).ConfigureAwait(false);}
catch(Exception error)when(!token.IsCancellationRequested&&!NavigationRecovery.HasInvalidDataCause(error)&&error is IOException or OperationCanceledException){await Task.Delay(PollInterval,token);continue;}
RequireIdentity();var age=DateTimeOffset.UtcNow-frame.ObservedAt;
if(!frame.Available||frame.Sequence<=0||age>TimeSpan.FromMilliseconds(350)||age<TimeSpan.FromMilliseconds(-100)){await Task.Delay(PollInterval,token);continue;}
if(failed.Observe(frame,DateTimeOffset.UtcNow))throw new NativeSessionConnectionFailedException(frame);
if(frame.Stable&&frame.State=="FailedToConnect"){await Task.Delay(PollInterval,token);continue;}
if(frame.State!=lastState){lastState=frame.State;Progress?.Invoke("正在进入游戏:"+frame.State);}
var current=player();
if(NativeSessionStartupReadiness.Ready(current,frame,DateTimeOffset.UtcNow,ProcessStartedAt)&¤t.LoadGameAt is {} loaded&&(ProcessStartedAt is not null||initial is null||loaded>initial)
&¤t.GameMode==mode&¤t.CommanderId.Length>0)
{
if(commander.Length>0&¤t.CommanderId!=commander)throw new InvalidOperationException("重新进入的指挥官与原会话不同。");
return;
}
if(request is not null)
{
if(frame.LastRequestId==request&&(frame.ActionApplied||frame.ActionFailed))
{if(frame.ActionFailed)throw new IOException("原始游戏模式选择失败,未重放选择。");request=null;}
else{await Task.Delay(PollInterval,token);continue;}
}
var action=frame.Stable?frame.State switch{"MainMenu"=>"ContinueMenu","ContinueMenu"=>mode=="Solo"?"SetupPlaySolo":"SetupPlayWithEveryone",_=>null}:null;
if(action is not null&&frame.Actions.Count(a=>a.Enabled&&a.Name==action)==1)
{
request=Guid.NewGuid().ToString("N");
try{await channel.SubmitAsync(owner,request,frame,action,token).ConfigureAwait(false);}
catch(NativeSessionStateChangedException){request=null;}
catch(IOException error)when(!NavigationRecovery.HasInvalidDataCause(error)){}catch(OperationCanceledException)when(!token.IsCancellationRequested){}
}
await Task.Delay(PollInterval,token).ConfigureAwait(false);
}
throw new TimeoutException("游戏原始载入未完成,当前客户端与日志已保留。");
}
finally{if(attempted)await channel.EndAsync(owner).ConfigureAwait(false);}
}
}