using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Navigation;
using XFEExtension.NetCore.XUnit.Attributes;
namespace XFEEDToolBox.Test;
[TestFixture]
public class GameRoutePlannerTests
{
private const long Destination=9007199254740995;
private static readonly NavWaypoint Target=new(Destination,"Destination",new(30,0,0),"K");
private static PlayerState Initial()=>new(){InGame=true,SystemName="Origin",Position=new(0,0,0),Navigation=new(){CurrentSystemAddress=1}};
private sealed class Driver:IGameRoutePlanningDriver
{
public bool SupportsGameRoutePlanning { get; set; }=true;
public int Requests,Reads;
public NavWaypoint? Submitted;
public Func<int,GameRouteStatus>? Read;
public Task<long> RequestGameRouteAsync(NavWaypoint target,CancellationToken token)
{Requests++;Submitted=target;return Task.FromResult(7L);}
public Task<GameRouteStatus> ReadGameRouteStatusAsync(long generation,long destinationAddress,CancellationToken token)
{token.ThrowIfCancellationRequested();return Task.FromResult(Read?.Invoke(++Reads)??Status());}
}
private static GameRouteStatus Status(long selected=Destination,bool fresh=true,IReadOnlyList<NavWaypoint>? route=null)=>new(7,true,false,false,Destination,selected,fresh,true,(route??DefaultRoute()).Count,GameRouteFingerprint.Compute(route??DefaultRoute()));
private static IReadOnlyList<NavWaypoint> DefaultRoute()=>[new(1,"Origin",new(0,0,0),"F"),Target];
private static async Task Rejects<T>(Func<Task> operation) where T:Exception
{try{await operation();}catch(T){return;}throw new InvalidOperationException("Expected "+typeof(T).Name);}
[Test] public async Task CachedMatchingRouteCannotConfirmNewRequestAndNextTargetMustMatch()
{
var old=new[]{new NavWaypoint(1,"Origin",new(0,0,0),"F"),Target};
var state=Initial() with{Navigation=Initial().Navigation with{Route=old}};
var driver=new Driver();
driver.Read=count=>
{
if(count==2)state=state with{Navigation=state.Navigation with{Route=old.ToArray()}};
return Status(count<3?42:Destination) with{TopologyFresh=count>1};
};
var result=await new GameRoutePlanner(()=>state,TimeSpan.FromSeconds(3)).PlanAsync(Target,driver);
Check.Equal(1,driver.Requests);Check.Equal(3,driver.Reads);Check.Equal(Destination,driver.Submitted!.SystemAddress);
Check.That(!ReferenceEquals(old,result.Route));Check.Equal(Destination,result.Next.SystemAddress);
}
[Test] public async Task SeveralThousandGameWaypointsArePreservedWithoutLocalRouteSelection()
{
var state=Initial();var gameRoute=Enumerable.Range(0,5000).Select(i=>new NavWaypoint(i+1,"Game "+i,new(i,i%7,-i%11),i%2==0?"K":"L")).Append(Target).ToArray();
var driver=new Driver();driver.Read=count=>{state=state with{Navigation=state.Navigation with{Route=gameRoute}};return Status(gameRoute[1].SystemAddress,route:gameRoute);};
var result=await new GameRoutePlanner(()=>state).PlanAsync(Target,driver);
Check.That(ReferenceEquals(gameRoute,result.Route));Check.Equal(5001,result.Route.Count);Check.Equal(2L,result.Next.SystemAddress);
Check.Equal("Game 3789",result.Route[3789].Name);Check.Equal(1,driver.Requests);
}
[Test] public async Task NoGameRouteCannotBecomeDirectJumpEvenWhenNativeDestinationMatches()
{
var state=Initial() with{MaxJumpRange=1000};var driver=new Driver();
await Rejects<TimeoutException>(async()=>{await new GameRoutePlanner(()=>state,TimeSpan.FromMilliseconds(250)).PlanAsync(Target,driver);});
Check.Equal(0,state.Navigation.Route.Count);Check.Equal(1,driver.Requests);
}
[Test] public async Task UnchangedJournalCacheRequiresTheEntireLiveGamePathToMatch()
{
var route=DefaultRoute();var state=Initial() with{Navigation=Initial().Navigation with{Route=route}};
var driver=new Driver{Read=count=>count==1?Status() with{RouteFingerprint=123}:Status()};
var result=await new GameRoutePlanner(()=>state).PlanAsync(Target,driver);
Check.Equal(2,driver.Reads);Check.That(ReferenceEquals(route,result.Route));Check.Equal(GameRouteFingerprint.Compute(route),result.NativeRouteFingerprint);
}
[Test] public async Task ChangedDestinationOrOriginCannotConfirmSuccessfulPlan()
{
var state=Initial();var driver=new Driver{Read=_=>Status() with{DestinationAddress=42}};
await Rejects<InvalidOperationException>(async()=>{await new GameRoutePlanner(()=>state).PlanAsync(Target,driver);});
driver=new Driver{Read=_=>{state=state with{Navigation=state.Navigation with{CurrentSystemAddress=42}};return Status();}};
await Rejects<InvalidOperationException>(async()=>{await new GameRoutePlanner(()=>state).PlanAsync(Target,driver);});
}
[Test] public async Task MatchingAddressWithAnUnrelatedEndpointNameIsRejected()
{
var target=Target with{Name="Adnyayar"};
var route=new[]{DefaultRoute()[0],target with{Name="Col 285 Sector MC-I b11-2"}};
var state=Initial() with{Navigation=Initial().Navigation with{Route=route}};
var driver=new Driver{Read=_=>Status(route:route)};
await Rejects<InvalidDataException>(()=>new GameRoutePlanner(()=>state).PlanAsync(target,driver));
Check.Equal("Adnyayar",driver.Submitted!.Name);Check.Equal(1,driver.Reads);
}
[Test] public async Task UnsupportedPluginAndRejectedGameRequestCannotReportReady()
{
var driver=new Driver{SupportsGameRoutePlanning=false};
await Rejects<InvalidOperationException>(async()=>{await new GameRoutePlanner(Initial).PlanAsync(Target,driver);});
Check.Equal(0,driver.Requests);
driver=new Driver{Read=_=>new(7,false,false,true,0,0,false)};
await Rejects<InvalidOperationException>(async()=>{await new GameRoutePlanner(Initial).PlanAsync(Target,driver);});
}
[Test] public async Task TemporaryRouteStatusDisconnectKeepsTheSameGameRequestUntilConfirmation()
{
var state=Initial() with{Navigation=Initial().Navigation with{Route=DefaultRoute()}};
var driver=new Driver{Read=i=>i==1?throw new IOException("Brief pipe disconnect"):Status()};var recovering=false;
var result=await new GameRoutePlanner(()=>state,TimeSpan.FromSeconds(3)).PlanAsync(Target,driver,s=>recovering|=s.Contains("重新读取"));
Check.That(recovering);Check.Equal(1,driver.Requests);Check.Equal(2,driver.Reads);Check.Equal(Destination,result.Next.SystemAddress);
}
}
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Navigation;
using XFEExtension.NetCore.XUnit.Attributes;
namespace XFEEDToolBox.Test;
[TestFixture]
public class GameRoutePlannerTests
{
private const long Destination=9007199254740995;
private static readonly NavWaypoint Target=new(Destination,"Destination",new(30,0,0),"K");
private static PlayerState Initial()=>new(){InGame=true,SystemName="Origin",Position=new(0,0,0),Navigation=new(){CurrentSystemAddress=1}};
private sealed class Driver:IGameRoutePlanningDriver
{
public bool SupportsGameRoutePlanning { get; set; }=true;
public int Requests,Reads;
public NavWaypoint? Submitted;
public Func<int,GameRouteStatus>? Read;
public Task<long> RequestGameRouteAsync(NavWaypoint target,CancellationToken token)
{Requests++;Submitted=target;return Task.FromResult(7L);}
public Task<GameRouteStatus> ReadGameRouteStatusAsync(long generation,long destinationAddress,CancellationToken token)
{token.ThrowIfCancellationRequested();return Task.FromResult(Read?.Invoke(++Reads)??Status());}
}
private static GameRouteStatus Status(long selected=Destination,bool fresh=true,IReadOnlyList<NavWaypoint>? route=null)=>new(7,true,false,false,Destination,selected,fresh,true,(route??DefaultRoute()).Count,GameRouteFingerprint.Compute(route??DefaultRoute()));
private static IReadOnlyList<NavWaypoint> DefaultRoute()=>[new(1,"Origin",new(0,0,0),"F"),Target];
private static async Task Rejects<T>(Func<Task> operation) where T:Exception
{try{await operation();}catch(T){return;}throw new InvalidOperationException("Expected "+typeof(T).Name);}
[Test] public async Task CachedMatchingRouteCannotConfirmNewRequestAndNextTargetMustMatch()
{
var old=new[]{new NavWaypoint(1,"Origin",new(0,0,0),"F"),Target};
var state=Initial() with{Navigation=Initial().Navigation with{Route=old}};
var driver=new Driver();
driver.Read=count=>
{
if(count==2)state=state with{Navigation=state.Navigation with{Route=old.ToArray()}};
return Status(count<3?42:Destination) with{TopologyFresh=count>1};
};
var result=await new GameRoutePlanner(()=>state,TimeSpan.FromSeconds(3)).PlanAsync(Target,driver);
Check.Equal(1,driver.Requests);Check.Equal(3,driver.Reads);Check.Equal(Destination,driver.Submitted!.SystemAddress);
Check.That(!ReferenceEquals(old,result.Route));Check.Equal(Destination,result.Next.SystemAddress);
}
[Test] public async Task SeveralThousandGameWaypointsArePreservedWithoutLocalRouteSelection()
{
var state=Initial();var gameRoute=Enumerable.Range(0,5000).Select(i=>new NavWaypoint(i+1,"Game "+i,new(i,i%7,-i%11),i%2==0?"K":"L")).Append(Target).ToArray();
var driver=new Driver();driver.Read=count=>{state=state with{Navigation=state.Navigation with{Route=gameRoute}};return Status(gameRoute[1].SystemAddress,route:gameRoute);};
var result=await new GameRoutePlanner(()=>state).PlanAsync(Target,driver);
Check.That(ReferenceEquals(gameRoute,result.Route));Check.Equal(5001,result.Route.Count);Check.Equal(2L,result.Next.SystemAddress);
Check.Equal("Game 3789",result.Route[3789].Name);Check.Equal(1,driver.Requests);
}
[Test] public async Task NoGameRouteCannotBecomeDirectJumpEvenWhenNativeDestinationMatches()
{
var state=Initial() with{MaxJumpRange=1000};var driver=new Driver();
await Rejects<TimeoutException>(async()=>{await new GameRoutePlanner(()=>state,TimeSpan.FromMilliseconds(250)).PlanAsync(Target,driver);});
Check.Equal(0,state.Navigation.Route.Count);Check.Equal(1,driver.Requests);
}
[Test] public async Task UnchangedJournalCacheRequiresTheEntireLiveGamePathToMatch()
{
var route=DefaultRoute();var state=Initial() with{Navigation=Initial().Navigation with{Route=route}};
var driver=new Driver{Read=count=>count==1?Status() with{RouteFingerprint=123}:Status()};
var result=await new GameRoutePlanner(()=>state).PlanAsync(Target,driver);
Check.Equal(2,driver.Reads);Check.That(ReferenceEquals(route,result.Route));Check.Equal(GameRouteFingerprint.Compute(route),result.NativeRouteFingerprint);
}
[Test] public async Task ChangedDestinationOrOriginCannotConfirmSuccessfulPlan()
{
var state=Initial();var driver=new Driver{Read=_=>Status() with{DestinationAddress=42}};
await Rejects<InvalidOperationException>(async()=>{await new GameRoutePlanner(()=>state).PlanAsync(Target,driver);});
driver=new Driver{Read=_=>{state=state with{Navigation=state.Navigation with{CurrentSystemAddress=42}};return Status();}};
await Rejects<InvalidOperationException>(async()=>{await new GameRoutePlanner(()=>state).PlanAsync(Target,driver);});
}
[Test] public async Task MatchingAddressWithAnUnrelatedEndpointNameIsRejected()
{
var target=Target with{Name="Adnyayar"};
var route=new[]{DefaultRoute()[0],target with{Name="Col 285 Sector MC-I b11-2"}};
var state=Initial() with{Navigation=Initial().Navigation with{Route=route}};
var driver=new Driver{Read=_=>Status(route:route)};
await Rejects<InvalidDataException>(()=>new GameRoutePlanner(()=>state).PlanAsync(target,driver));
Check.Equal("Adnyayar",driver.Submitted!.Name);Check.Equal(1,driver.Reads);
}
[Test] public async Task UnsupportedPluginAndRejectedGameRequestCannotReportReady()
{
var driver=new Driver{SupportsGameRoutePlanning=false};
await Rejects<InvalidOperationException>(async()=>{await new GameRoutePlanner(Initial).PlanAsync(Target,driver);});
Check.Equal(0,driver.Requests);
driver=new Driver{Read=_=>new(7,false,false,true,0,0,false)};
await Rejects<InvalidOperationException>(async()=>{await new GameRoutePlanner(Initial).PlanAsync(Target,driver);});
}
[Test] public async Task TemporaryRouteStatusDisconnectKeepsTheSameGameRequestUntilConfirmation()
{
var state=Initial() with{Navigation=Initial().Navigation with{Route=DefaultRoute()}};
var driver=new Driver{Read=i=>i==1?throw new IOException("Brief pipe disconnect"):Status()};var recovering=false;
var result=await new GameRoutePlanner(()=>state,TimeSpan.FromSeconds(3)).PlanAsync(Target,driver,s=>recovering|=s.Contains("重新读取"));
Check.That(recovering);Check.Equal(1,driver.Requests);Check.Equal(2,driver.Reads);Check.Equal(Destination,result.Next.SystemAddress);
}
}