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? Read; public Task RequestGameRouteAsync(NavWaypoint target,CancellationToken token) {Requests++;Submitted=target;return Task.FromResult(7L);} public Task 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? route=null)=>new(7,true,false,false,Destination,selected,fresh,true,(route??DefaultRoute()).Count,GameRouteFingerprint.Compute(route??DefaultRoute())); private static IReadOnlyList DefaultRoute()=>[new(1,"Origin",new(0,0,0),"F"),Target]; private static async Task Rejects(Func 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(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(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(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(()=>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(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(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); } }