using System;
using System.Collections.Generic;
using System.Linq;
using VRage.Game.ModAPI.Ingame.Utilities;
using VRageMath;
using Xunit;
using P = AutoMiningScript.Program;
namespace AutoMiningScript.Tests
{
public class PlannerTests
{
[Fact]
public void ClearTasksDropsGenerationAndAllCheckpointWorkWithoutReusingIds()
{
var p=Planner("[Survey]\nMaxJobs=4096\n");p.Manual(Vector3D.Zero,Vector3D.Forward,Vector3D.Up,1000,100,20);
Assert.True(p.Generating);Assert.True(p.PendingGeneration>0);int serial=p.TaskSerial;
var partial=new MyIni();Assert.False(p.Checkpoint(partial));List<string> records;Assert.False(p.CheckpointRecords(out records));
p.ClearTasks();Assert.Empty(p.Jobs);Assert.Empty(p.Regions);Assert.Empty(p.OwnedJobs);Assert.Equal(0,p.PendingCount);Assert.Equal(0,p.PendingGeneration);Assert.False(p.Generating);
Assert.Equal(serial,p.TaskSerial);for(int n=0;n<100;n++)p.Advance();Assert.Empty(p.Jobs);
Assert.True(p.Checkpoint(partial));Assert.False(partial.ContainsKey("job0","Id"));Assert.True(p.CheckpointRecords(out records));Assert.Single(records);
var persisted=new MyIni();p.Save(persisted);var restored=Planner();restored.Load(persisted);Assert.Empty(restored.Jobs);Assert.Equal(serial,restored.TaskSerial);
restored.Manual(Vector3D.Zero,Vector3D.Forward,Vector3D.Up,6,6,10);Assert.True(restored.TaskSerial>serial);
}
static P.TaskPlanner Planner(string config="") {return new P.TaskPlanner(TestRig.Program(config));}
static void Drain(P.TaskPlanner planner) {for(int n=0;n<1024;n++)planner.Advance();}
static P.Job SeedProbe(P.TaskPlanner planner)
{
planner.Survey(Vector3D.Zero,Vector3D.Down*9.81,Vector3D.Forward,Vector3D.Up,20);
P.Job survey=planner.Jobs[0];
planner.Report(survey,new P.JobReport {JobId=survey.Id,Outcome="SurveyHit",Position=new Vector3D(0,-10,0),Direction=Vector3D.Down,Up=Vector3D.Forward,EntityId=42});
return planner.Jobs.Single(j=>j.Kind==P.JobKind.Probe);
}
[Fact]
public void ManualGridCoversRequestedBoundsAndUsesSerpentineOrder()
{
var p=Planner();p.Manual(Vector3D.Zero,Vector3D.Forward,Vector3D.Up,12,12,20);Drain(p);
Assert.Equal(9,p.Jobs.Count);
Assert.Equal(-6,p.Jobs.Min(j=>j.Entry.X)-3,8);Assert.Equal(6,p.Jobs.Max(j=>j.Entry.X)+3,8);
Assert.Equal(-6,p.Jobs.Min(j=>j.Entry.Y)-3,8);Assert.Equal(6,p.Jobs.Max(j=>j.Entry.Y)+3,8);
Assert.True(p.Jobs[0].Entry.X<p.Jobs[1].Entry.X);Assert.True(p.Jobs[3].Entry.X>p.Jobs[4].Entry.X);
Assert.Equal(p.Jobs[2].Entry.X,p.Jobs[3].Entry.X,8);
for(int n=1;n<p.Jobs.Count;n++) Assert.InRange(Vector3D.Distance(p.Jobs[n-1].Entry,p.Jobs[n].Entry),0,5.1+1e-8);
Assert.All(p.Jobs,j=>Assert.Equal(20,j.Depth));
}
[Fact]
public void WorldOriginIsAValidEntranceAndGps()
{
Vector3D position;Assert.True(P.Data.TryGps("GPS:Origin:0:0:0:",out position));Assert.Equal(Vector3D.Zero,position);
var p=Planner();p.Manual(position,Vector3D.Forward,Vector3D.Up,6,6,10);Assert.Single(p.Jobs);Assert.Equal(Vector3D.Zero,p.Jobs[0].Entry);
}
[Theory]
[InlineData(0,6,10)] [InlineData(6,-1,10)] [InlineData(6,6,1001)] [InlineData(double.NaN,6,10)] [InlineData(6,double.PositiveInfinity,10)]
public void ManualRejectsInvalidDimensions(double width,double height,double depth)
{
Assert.Throws<ArgumentException>(()=>Planner().Manual(Vector3D.Zero,Vector3D.Forward,Vector3D.Up,width,height,depth));
}
[Fact]
public void ManualRejectsZeroDirectionAndOversizedJobBudgetWithoutPartialJobs()
{
var p=Planner("[Survey]\nMaxJobs=32\n");
Assert.Throws<ArgumentException>(()=>p.Manual(Vector3D.Zero,Vector3D.Zero,Vector3D.Up,6,6,10));
Assert.Throws<ArgumentException>(()=>p.Manual(Vector3D.Zero,Vector3D.Forward,Vector3D.Up,1000,1000,10));
Assert.Empty(p.Jobs);Assert.Empty(p.Regions);
}
[Fact]
public void PlanetSurveyIsFiniteOnFrozenHorizontalPlane()
{
var p=Planner();Vector3D origin=new Vector3D(123,456,789);p.Survey(origin,Vector3D.Down*9.81,Vector3D.Forward,Vector3D.Up,120);Drain(p);
Assert.Equal(13,p.Jobs.Count);
Assert.All(p.Jobs,j=>{Assert.Equal(origin.Y+80,j.Entry.Y,8);Assert.InRange(new Vector3D(j.Entry.X-origin.X,0,j.Entry.Z-origin.Z).Length(),0,120.00001);Assert.True(Vector3D.Dot(j.Direction,Vector3D.Down)>0.999);});
Vector3D saved=p.Jobs[0].Entry;p.Survey(origin+new Vector3D(1000,0,0),Vector3D.Down*9.81,Vector3D.Forward,Vector3D.Up,20);
Assert.Equal(saved,p.Jobs[0].Entry);Assert.Equal(origin,p.Regions[0].Center);
}
[Fact]
public void SpaceSurveyHasFiniteObservationPointsAndRaysEndingAtSphere()
{
var p=Planner();Vector3D origin=new Vector3D(10,20,30);p.Survey(origin,Vector3D.Zero,Vector3D.Forward,Vector3D.Up,2000);Drain(p);
Assert.Equal(294,p.Jobs.Count);
Assert.All(p.Jobs,j=>{Assert.InRange(Vector3D.Distance(origin,j.Entry),0,700.0001);Assert.Equal(2000,Vector3D.Distance(origin,j.Entry+j.Direction*j.Depth),6);});
Assert.Equal(7,p.Jobs.Select(j=>j.Entry).Distinct().Count());
}
[Fact]
public void LargeSearchKeepsEachMessageWithinJobScanBounds()
{
var p=Planner();p.Survey(Vector3D.Zero,Vector3D.Zero,Vector3D.Forward,Vector3D.Up,20000);Drain(p);
Assert.All(p.Jobs,j=>{Assert.InRange(j.Depth,1,10000);Assert.NotNull(P.Job.FromIni(j.ToIni()));});
}
[Fact]
public void RepeatedSurfaceHitsDeduplicateEntityAndPosition()
{
var p=Planner();P.Job probe=SeedProbe(p);
p.Survey(Vector3D.Zero,Vector3D.Down*9.81,Vector3D.Forward,Vector3D.Up,20);
P.Job survey=p.Jobs.Last();
p.Report(survey,new P.JobReport {Outcome="SurveyHit",Position=probe.Entry+new Vector3D(0.1,0,0),Direction=Vector3D.Down,Up=Vector3D.Forward,EntityId=42});
Assert.Single(p.Jobs,j=>j.Kind==P.JobKind.Probe);
p.Survey(Vector3D.Zero,Vector3D.Down*9.81,Vector3D.Forward,Vector3D.Up,20);survey=p.Jobs.Last();
p.Report(survey,new P.JobReport {Outcome="SurveyHit",Position=probe.Entry,Direction=Vector3D.Down,Up=Vector3D.Forward,EntityId=43});
Assert.Equal(2,p.Jobs.Count(j=>j.Kind==P.JobKind.Probe));
}
[Fact]
public void SurfaceOutsideFrozenSearchDoesNotGenerateProbe()
{
var p=Planner();p.Survey(Vector3D.Zero,Vector3D.Zero,Vector3D.Forward,Vector3D.Up,100);
p.Report(p.Jobs[0],new P.JobReport {Outcome="SurveyHit",Position=new Vector3D(101,0,0),Direction=Vector3D.Left,EntityId=42});
Assert.DoesNotContain(p.Jobs,j=>j.Kind==P.JobKind.Probe);
}
[Fact]
public void DiscoveryWaitsForRetreatCompletionBeforeSchedulingNeighborRing()
{
var p=Planner();P.Job probe=SeedProbe(p);probe.Owner="miner-1";
p.Report(probe,new P.JobReport {Outcome="Discovery",Ore="Cobalt,Iron",Progress=5});Drain(p);
Assert.Single(p.Jobs,j=>j.Kind==P.JobKind.Probe);Assert.Equal("miner-1",probe.Owner);
Assert.Equal("Cobalt,Iron",p.Regions.Single(r=>r.Patch).Ore);
Assert.Equal("Iron",p.PreferredOre(p.Regions.Single(r=>r.Patch).Ore));
p.Report(probe,new P.JobReport {Outcome="Complete",Ore="Iron",Progress=probe.Depth});Drain(p);
Assert.Equal(9,p.Jobs.Count(j=>j.Kind==P.JobKind.Probe));Assert.Equal("",probe.Owner);
Assert.All(p.Jobs.Where(j=>j.Kind==P.JobKind.Probe),j=>Assert.Equal(42,j.EntityId));
}
[Fact]
public void TerminalOreReportRecoversMissingOrReorderedDiscoveryOnce()
{
var p=Planner();P.Job probe=SeedProbe(p);
p.Report(probe,new P.JobReport {Outcome="Complete",Ore="Iron",Progress=probe.Depth});Drain(p);
Assert.Single(p.Regions,r=>r.Patch);Assert.Equal(9,p.Jobs.Count(j=>j.Kind==P.JobKind.Probe));
p.Report(probe,new P.JobReport {Outcome="Discovery",Ore="Iron",Progress=5});
p.Report(probe,new P.JobReport {Outcome="Complete",Ore="Iron",Progress=probe.Depth});Drain(p);
Assert.Single(p.Regions,r=>r.Patch);Assert.Equal(9,p.Jobs.Count(j=>j.Kind==P.JobKind.Probe));
}
[Fact]
public void TwoCompletedEmptyRingsStopExpansion()
{
var p=Planner("[Survey]\nExpansionWidth=60\nExpansionHeight=60\n");P.Job probe=SeedProbe(p);
// Widen the frozen search so both complete expansion rings fit.
p.Regions[0].Radius=100;
p.Report(probe,new P.JobReport {Outcome="Complete",Ore="Iron",Progress=probe.Depth});Drain(p);
Assert.Equal(8,p.Jobs.Count(j=>j.Kind==P.JobKind.Probe&&!P.TaskPlanner.Finished(j)));
foreach(P.Job j in p.Jobs.Where(j=>j.Kind==P.JobKind.Probe&&!P.TaskPlanner.Finished(j)).ToList()) p.Report(j,new P.JobReport {Outcome="Complete",Progress=j.Depth});Drain(p);
Assert.Equal(16,p.Jobs.Count(j=>j.Kind==P.JobKind.Probe&&!P.TaskPlanner.Finished(j)));
foreach(P.Job j in p.Jobs.Where(j=>j.Kind==P.JobKind.Probe&&!P.TaskPlanner.Finished(j)).ToList()) p.Report(j,new P.JobReport {Outcome="Complete",Progress=j.Depth});Drain(p);
Assert.Equal(25,p.Jobs.Count(j=>j.Kind==P.JobKind.Probe));Assert.DoesNotContain(p.Jobs,j=>!P.TaskPlanner.Finished(j));
Assert.Contains("two empty rings",p.Regions.Single(r=>r.Patch).Status);
}
[Fact]
public void InvalidSamplesNeverCountAsConfirmedEmptyRings()
{
var p=Planner();P.Job probe=SeedProbe(p);p.Report(probe,new P.JobReport {Outcome="Complete",Ore="Iron",Progress=probe.Depth});Drain(p);
foreach(P.Job j in p.Jobs.Where(j=>j.Kind==P.JobKind.Probe&&!P.TaskPlanner.Finished(j)).ToList()) p.Report(j,new P.JobReport {Outcome="InvalidSample",Progress=j.Depth});Drain(p);
var patch=p.Regions.Single(r=>r.Patch);Assert.Equal(0,patch.EmptyRings);Assert.Contains("incomplete",patch.Status);
}
[Fact]
public void PausedReportPreservesOwnershipAndMonotonicProgress()
{
var p=Planner();p.Manual(Vector3D.Zero,Vector3D.Forward,Vector3D.Up,6,6,30);P.Job j=p.Jobs[0];j.Owner="miner-1";j.Progress=12;
p.Report(j,new P.JobReport {Outcome="Paused",Progress=8});Drain(p);
Assert.False(P.TaskPlanner.Finished(j));Assert.Equal("miner-1",j.Owner);Assert.Equal(12,j.Progress);Assert.Equal("Pending",p.Regions[0].Status);
}
[Fact]
public void PlannerStoragePreservesFrozenGeometryOccupancyAndProgress()
{
var p=Planner();p.Manual(new Vector3D(1,2,3),Vector3D.Forward,Vector3D.Up,12,12,30);p.Jobs[0].Owner="miner-1";p.Jobs[0].Progress=12;
var saved=new MyIni();p.Save(saved);var restored=Planner();restored.Load(saved);
Assert.Equal(p.Jobs.Count,restored.Jobs.Count);Assert.Equal(p.Regions[0].Center,restored.Regions[0].Center);
Assert.Equal("miner-1",restored.Jobs[0].Owner);Assert.Equal(12,restored.Jobs[0].Progress);
Assert.Equal(p.Jobs.Select(j=>j.Entry),restored.Jobs.Select(j=>j.Entry));
}
[Fact]
public void StorageRejectsInvalidBoundsDuplicateIdentifiersAndSerial()
{
var p=Planner();p.Manual(Vector3D.Zero,Vector3D.Forward,Vector3D.Up,12,6,30);var saved=new MyIni();p.Save(saved);
saved.Set("job0","Depth",-1);Assert.Throws<ArgumentException>(()=>Planner().Load(saved));
saved.Clear();p.Save(saved);saved.Set("job1","Id",p.Jobs[0].Id);Assert.Throws<ArgumentException>(()=>Planner().Load(saved));
saved.Clear();p.Save(saved);saved.Set("planner","Serial",-1);Assert.Throws<ArgumentException>(()=>Planner().Load(saved));
}
[Fact]
public void IncrementalCheckpointPatchesEarlierCompletionsAndAppendedTasks()
{
var p=Planner();p.Manual(Vector3D.Zero,Vector3D.Forward,Vector3D.Up,300,6,20);Drain(p);p.Jobs[0].Owner="miner-1";
var saved=new MyIni();Assert.False(p.Checkpoint(saved));
p.Report(p.Jobs[0],new P.JobReport {Outcome="Complete",Progress=20});p.Jobs[30].Owner="miner-2";
p.Manual(new Vector3D(1000,0,0),Vector3D.Forward,Vector3D.Up,6,6,20);
int frames=0;while(!p.Checkpoint(saved)&&frames++<100){}Assert.True(frames<100);
var restored=Planner();restored.Load(saved);Assert.Equal(p.Jobs.Count,restored.Jobs.Count);
Assert.Equal("Complete",restored.Jobs[0].Outcome);Assert.Equal("",restored.Jobs[0].Owner);Assert.Equal("miner-2",restored.Jobs[30].Owner);
}
[Fact]
public void SurveyCommandQueuesBoundedWorkAndPreservesAllRays()
{
var p=Planner();p.Survey(Vector3D.Zero,Vector3D.Zero,Vector3D.Forward,Vector3D.Up,2000);
Assert.InRange(p.Jobs.Count,1,8);Assert.True(p.Generating);
int count=p.Jobs.Count;p.Advance();Assert.InRange(p.Jobs.Count-count,0,8);
Drain(p);Assert.False(p.Generating);Assert.Equal(294,p.Jobs.Count);Assert.Equal(294,p.Jobs.Select(j=>j.Id).Distinct().Count());
}
[Fact]
public void PendingGenerationReservesCapacityAcrossCommands()
{
var p=Planner("[Survey]\nMaxJobs=300\n");p.Survey(Vector3D.Zero,Vector3D.Zero,Vector3D.Forward,Vector3D.Up,2000);
Assert.Throws<ArgumentException>(()=>p.Manual(Vector3D.Zero,Vector3D.Forward,Vector3D.Up,12,12,20));
Drain(p);Assert.Equal(294,p.Jobs.Count);
}
[Fact]
public void GenerationYieldsWhenInstructionBudgetIsSpent()
{
var program=TestRig.Program();var runtime=new Stub<Sandbox.ModAPI.Ingame.IMyGridProgramRuntimeInfo>().Set("MaxInstructionCount",50000).Set("CurrentInstructionCount",30000);TestRig.SetBase(program,"Runtime",runtime.Value);
var p=new P.TaskPlanner(program);p.Survey(Vector3D.Zero,Vector3D.Zero,Vector3D.Forward,Vector3D.Up,2000);p.Advance();Assert.Empty(p.Jobs);Assert.True(p.Generating);
runtime.Set("CurrentInstructionCount",0);Drain(p);Assert.Equal(294,p.Jobs.Count);
}
[Fact]
public void RestartResumesPartialGenerationWithoutMissingOrRepeatingHoles()
{
var p=Planner();p.Survey(Vector3D.Zero,Vector3D.Zero,Vector3D.Forward,Vector3D.Up,2000);var save=new MyIni();p.Save(save);
var restored=Planner();restored.Load(save);Assert.True(restored.Generating);Drain(p);Drain(restored);
Assert.Equal(p.Jobs.Select(j=>j.Id),restored.Jobs.Select(j=>j.Id));Assert.Equal(p.Jobs.Select(j=>j.Entry),restored.Jobs.Select(j=>j.Entry));
}
[Fact]
public void RecordWriterRejectsAnyRecordTheReaderCannotRestore()
{
var ini=new MyIni();ini.Set("payload","Value",new string('x',32768));Assert.Throws<ArgumentException>(()=>P.TaskPlanner.StorageRecord(ini,"Fleet"));
}
[Fact]
public void RegionSummaryRestartsWhenJobsAreAppendedBetweenFrames()
{
var p=Planner();p.Survey(Vector3D.Zero,Vector3D.Down,Vector3D.Forward,Vector3D.Up,20);
p.Report(p.Jobs[0],new P.JobReport {Outcome="SurveyEmpty"});
for(int n=0;n<100;n++)p.Manual(new Vector3D(1000+n*20,0,0),Vector3D.Forward,Vector3D.Up,6,6,10);
p.Advance();p.Advance();
// Append a new pending hole in the region after its earlier jobs were scanned.
p.Manual(new Vector3D(5000,0,0),Vector3D.Forward,Vector3D.Up,6,6,10);p.Jobs.Last().RegionId=p.Regions[0].Id;
Drain(p);Assert.Equal("Pending",p.Regions[0].Status);
}
[Fact]
public void UnknownOreDoesNotCreateTargetDeposit()
{
var p=Planner("[Mining]\nTargetOres=Iron,Nickel,Cobalt\n");P.Job probe=SeedProbe(p);p.Report(probe,new P.JobReport {Outcome="Complete",Ore="Stone,Ice",Progress=probe.Depth});Drain(p);
Assert.DoesNotContain(p.Regions,r=>r.Patch);Assert.Equal(0,p.OrePriority("Stone"));Assert.True(p.OrePriority("Iron")>p.OrePriority("Cobalt"));
Assert.Equal("Stone,Ice",probe.ConfirmedOres);
}
[Theory]
[InlineData("")]
[InlineData("[Mining]\nTargetOres=\n")]
[InlineData("[Mining]\nTargetOres= , , \n")]
public void EmptyTargetsMineStoneAndAnyOtherOre(string config)
{
var p=Planner(config);var probe=SeedProbe(p);
p.Report(probe,new P.JobReport {Outcome="Complete",Ore="Stone,Ice,ModOre",Progress=probe.Depth});Drain(p);
var patch=Assert.Single(p.Regions,r=>r.Patch);Assert.Equal("Stone,Ice,ModOre",patch.Ore);
Assert.Equal(9,p.Jobs.Count(j=>j.Kind==P.JobKind.Probe));Assert.Equal(0,p.OrePriority("Stone"));
}
[Fact]
public void TargetOrderDoesNotSetPriorityAndPriorityCannotBypassTargets()
{
var p=Planner("[Mining]\nTargetOres=Stone,Gold,Iron,Cobalt\nPriorityOres=Cobalt,Nickel,Iron\n");
Assert.Equal("Cobalt",p.PreferredOre("Stone,Iron,Gold,Cobalt,Nickel"));
Assert.Equal("",p.PreferredOre("Nickel"));Assert.Equal(0,p.OrePriority("Nickel"));
Assert.Equal(0,p.OrePriority("Gold"));Assert.Equal(0,p.OrePriority("Stone"));
var unranked=Planner("[Mining]\nPriorityOres=\n");Assert.Equal("Stone",unranked.PreferredOre("Stone,Iron"));Assert.Equal(0,unranked.OrePriority("Iron"));
}
[Fact]
public void DefaultPriorityOrderIsIronCobaltNickelSilicon()
{
var p=Planner();Assert.True(p.OrePriority("Iron")>p.OrePriority("Cobalt"));Assert.True(p.OrePriority("Cobalt")>p.OrePriority("Nickel"));
Assert.True(p.OrePriority("Nickel")>p.OrePriority("Silicon"));Assert.True(p.OrePriority("Silicon")>p.OrePriority("Gold"));
Assert.Equal("Iron",p.PreferredOre("Stone,Silicon,Nickel,Cobalt,Iron"));
}
[Fact]
public void CompleteReportAddsLaterOresAndRestoredJobsUseCurrentPreference()
{
var p=Planner();var probe=SeedProbe(p);
p.Report(probe,new P.JobReport {Outcome="Discovery",Ore="Stone",Progress=5});
p.Report(probe,new P.JobReport {Outcome="Complete",Ore="Stone,Iron,Cobalt",Progress=probe.Depth});Drain(p);
Assert.Equal("Stone,Iron,Cobalt",probe.ConfirmedOres);Assert.Equal("Stone,Iron,Cobalt",p.Regions.Single(r=>r.Patch).Ore);
var saved=new MyIni();p.Save(saved);
var restored=Planner("[Mining]\nTargetOres=Cobalt\nPriorityOres=Cobalt\n");restored.Load(saved);
var pending=restored.Jobs.First(j=>j.Kind==P.JobKind.Probe&&!P.TaskPlanner.Finished(j));
Assert.Equal(11,restored.JobPriority(pending));
var excluded=Planner("[Mining]\nTargetOres=Gold\n");excluded.Load(saved);
Assert.Equal(-1,excluded.JobPriority(excluded.Find(pending.Id)));
}
[Fact]
public void LateTerminalOreReportUpdatesPatchWithoutReopeningFinishedHole()
{
var p=Planner();var probe=SeedProbe(p);
p.Report(probe,new P.JobReport {Outcome="Complete",Ore="Stone",Progress=probe.Depth});Drain(p);
int count=p.Jobs.Count;var pending=p.Jobs.First(j=>j.Kind==P.JobKind.Probe&&!P.TaskPlanner.Finished(j));Assert.Equal(10,p.JobPriority(pending));
p.Report(probe,new P.JobReport {Outcome="Complete",Ore="Iron,Stone",Progress=probe.Depth});Drain(p);
Assert.Equal(count,p.Jobs.Count);Assert.True(P.TaskPlanner.Finished(probe));Assert.Equal("Stone,Iron",probe.ConfirmedOres);
Assert.Equal("Stone,Iron",p.Regions.Single(r=>r.Patch).Ore);Assert.Equal(14,p.JobPriority(pending));
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using VRage.Game.ModAPI.Ingame.Utilities;
using VRageMath;
using Xunit;
using P = AutoMiningScript.Program;
namespace AutoMiningScript.Tests
{
public class PlannerTests
{
[Fact]
public void ClearTasksDropsGenerationAndAllCheckpointWorkWithoutReusingIds()
{
var p=Planner("[Survey]\nMaxJobs=4096\n");p.Manual(Vector3D.Zero,Vector3D.Forward,Vector3D.Up,1000,100,20);
Assert.True(p.Generating);Assert.True(p.PendingGeneration>0);int serial=p.TaskSerial;
var partial=new MyIni();Assert.False(p.Checkpoint(partial));List<string> records;Assert.False(p.CheckpointRecords(out records));
p.ClearTasks();Assert.Empty(p.Jobs);Assert.Empty(p.Regions);Assert.Empty(p.OwnedJobs);Assert.Equal(0,p.PendingCount);Assert.Equal(0,p.PendingGeneration);Assert.False(p.Generating);
Assert.Equal(serial,p.TaskSerial);for(int n=0;n<100;n++)p.Advance();Assert.Empty(p.Jobs);
Assert.True(p.Checkpoint(partial));Assert.False(partial.ContainsKey("job0","Id"));Assert.True(p.CheckpointRecords(out records));Assert.Single(records);
var persisted=new MyIni();p.Save(persisted);var restored=Planner();restored.Load(persisted);Assert.Empty(restored.Jobs);Assert.Equal(serial,restored.TaskSerial);
restored.Manual(Vector3D.Zero,Vector3D.Forward,Vector3D.Up,6,6,10);Assert.True(restored.TaskSerial>serial);
}
static P.TaskPlanner Planner(string config="") {return new P.TaskPlanner(TestRig.Program(config));}
static void Drain(P.TaskPlanner planner) {for(int n=0;n<1024;n++)planner.Advance();}
static P.Job SeedProbe(P.TaskPlanner planner)
{
planner.Survey(Vector3D.Zero,Vector3D.Down*9.81,Vector3D.Forward,Vector3D.Up,20);
P.Job survey=planner.Jobs[0];
planner.Report(survey,new P.JobReport {JobId=survey.Id,Outcome="SurveyHit",Position=new Vector3D(0,-10,0),Direction=Vector3D.Down,Up=Vector3D.Forward,EntityId=42});
return planner.Jobs.Single(j=>j.Kind==P.JobKind.Probe);
}
[Fact]
public void ManualGridCoversRequestedBoundsAndUsesSerpentineOrder()
{
var p=Planner();p.Manual(Vector3D.Zero,Vector3D.Forward,Vector3D.Up,12,12,20);Drain(p);
Assert.Equal(9,p.Jobs.Count);
Assert.Equal(-6,p.Jobs.Min(j=>j.Entry.X)-3,8);Assert.Equal(6,p.Jobs.Max(j=>j.Entry.X)+3,8);
Assert.Equal(-6,p.Jobs.Min(j=>j.Entry.Y)-3,8);Assert.Equal(6,p.Jobs.Max(j=>j.Entry.Y)+3,8);
Assert.True(p.Jobs[0].Entry.X<p.Jobs[1].Entry.X);Assert.True(p.Jobs[3].Entry.X>p.Jobs[4].Entry.X);
Assert.Equal(p.Jobs[2].Entry.X,p.Jobs[3].Entry.X,8);
for(int n=1;n<p.Jobs.Count;n++) Assert.InRange(Vector3D.Distance(p.Jobs[n-1].Entry,p.Jobs[n].Entry),0,5.1+1e-8);
Assert.All(p.Jobs,j=>Assert.Equal(20,j.Depth));
}
[Fact]
public void WorldOriginIsAValidEntranceAndGps()
{
Vector3D position;Assert.True(P.Data.TryGps("GPS:Origin:0:0:0:",out position));Assert.Equal(Vector3D.Zero,position);
var p=Planner();p.Manual(position,Vector3D.Forward,Vector3D.Up,6,6,10);Assert.Single(p.Jobs);Assert.Equal(Vector3D.Zero,p.Jobs[0].Entry);
}
[Theory]
[InlineData(0,6,10)] [InlineData(6,-1,10)] [InlineData(6,6,1001)] [InlineData(double.NaN,6,10)] [InlineData(6,double.PositiveInfinity,10)]
public void ManualRejectsInvalidDimensions(double width,double height,double depth)
{
Assert.Throws<ArgumentException>(()=>Planner().Manual(Vector3D.Zero,Vector3D.Forward,Vector3D.Up,width,height,depth));
}
[Fact]
public void ManualRejectsZeroDirectionAndOversizedJobBudgetWithoutPartialJobs()
{
var p=Planner("[Survey]\nMaxJobs=32\n");
Assert.Throws<ArgumentException>(()=>p.Manual(Vector3D.Zero,Vector3D.Zero,Vector3D.Up,6,6,10));
Assert.Throws<ArgumentException>(()=>p.Manual(Vector3D.Zero,Vector3D.Forward,Vector3D.Up,1000,1000,10));
Assert.Empty(p.Jobs);Assert.Empty(p.Regions);
}
[Fact]
public void PlanetSurveyIsFiniteOnFrozenHorizontalPlane()
{
var p=Planner();Vector3D origin=new Vector3D(123,456,789);p.Survey(origin,Vector3D.Down*9.81,Vector3D.Forward,Vector3D.Up,120);Drain(p);
Assert.Equal(13,p.Jobs.Count);
Assert.All(p.Jobs,j=>{Assert.Equal(origin.Y+80,j.Entry.Y,8);Assert.InRange(new Vector3D(j.Entry.X-origin.X,0,j.Entry.Z-origin.Z).Length(),0,120.00001);Assert.True(Vector3D.Dot(j.Direction,Vector3D.Down)>0.999);});
Vector3D saved=p.Jobs[0].Entry;p.Survey(origin+new Vector3D(1000,0,0),Vector3D.Down*9.81,Vector3D.Forward,Vector3D.Up,20);
Assert.Equal(saved,p.Jobs[0].Entry);Assert.Equal(origin,p.Regions[0].Center);
}
[Fact]
public void SpaceSurveyHasFiniteObservationPointsAndRaysEndingAtSphere()
{
var p=Planner();Vector3D origin=new Vector3D(10,20,30);p.Survey(origin,Vector3D.Zero,Vector3D.Forward,Vector3D.Up,2000);Drain(p);
Assert.Equal(294,p.Jobs.Count);
Assert.All(p.Jobs,j=>{Assert.InRange(Vector3D.Distance(origin,j.Entry),0,700.0001);Assert.Equal(2000,Vector3D.Distance(origin,j.Entry+j.Direction*j.Depth),6);});
Assert.Equal(7,p.Jobs.Select(j=>j.Entry).Distinct().Count());
}
[Fact]
public void LargeSearchKeepsEachMessageWithinJobScanBounds()
{
var p=Planner();p.Survey(Vector3D.Zero,Vector3D.Zero,Vector3D.Forward,Vector3D.Up,20000);Drain(p);
Assert.All(p.Jobs,j=>{Assert.InRange(j.Depth,1,10000);Assert.NotNull(P.Job.FromIni(j.ToIni()));});
}
[Fact]
public void RepeatedSurfaceHitsDeduplicateEntityAndPosition()
{
var p=Planner();P.Job probe=SeedProbe(p);
p.Survey(Vector3D.Zero,Vector3D.Down*9.81,Vector3D.Forward,Vector3D.Up,20);
P.Job survey=p.Jobs.Last();
p.Report(survey,new P.JobReport {Outcome="SurveyHit",Position=probe.Entry+new Vector3D(0.1,0,0),Direction=Vector3D.Down,Up=Vector3D.Forward,EntityId=42});
Assert.Single(p.Jobs,j=>j.Kind==P.JobKind.Probe);
p.Survey(Vector3D.Zero,Vector3D.Down*9.81,Vector3D.Forward,Vector3D.Up,20);survey=p.Jobs.Last();
p.Report(survey,new P.JobReport {Outcome="SurveyHit",Position=probe.Entry,Direction=Vector3D.Down,Up=Vector3D.Forward,EntityId=43});
Assert.Equal(2,p.Jobs.Count(j=>j.Kind==P.JobKind.Probe));
}
[Fact]
public void SurfaceOutsideFrozenSearchDoesNotGenerateProbe()
{
var p=Planner();p.Survey(Vector3D.Zero,Vector3D.Zero,Vector3D.Forward,Vector3D.Up,100);
p.Report(p.Jobs[0],new P.JobReport {Outcome="SurveyHit",Position=new Vector3D(101,0,0),Direction=Vector3D.Left,EntityId=42});
Assert.DoesNotContain(p.Jobs,j=>j.Kind==P.JobKind.Probe);
}
[Fact]
public void DiscoveryWaitsForRetreatCompletionBeforeSchedulingNeighborRing()
{
var p=Planner();P.Job probe=SeedProbe(p);probe.Owner="miner-1";
p.Report(probe,new P.JobReport {Outcome="Discovery",Ore="Cobalt,Iron",Progress=5});Drain(p);
Assert.Single(p.Jobs,j=>j.Kind==P.JobKind.Probe);Assert.Equal("miner-1",probe.Owner);
Assert.Equal("Cobalt,Iron",p.Regions.Single(r=>r.Patch).Ore);
Assert.Equal("Iron",p.PreferredOre(p.Regions.Single(r=>r.Patch).Ore));
p.Report(probe,new P.JobReport {Outcome="Complete",Ore="Iron",Progress=probe.Depth});Drain(p);
Assert.Equal(9,p.Jobs.Count(j=>j.Kind==P.JobKind.Probe));Assert.Equal("",probe.Owner);
Assert.All(p.Jobs.Where(j=>j.Kind==P.JobKind.Probe),j=>Assert.Equal(42,j.EntityId));
}
[Fact]
public void TerminalOreReportRecoversMissingOrReorderedDiscoveryOnce()
{
var p=Planner();P.Job probe=SeedProbe(p);
p.Report(probe,new P.JobReport {Outcome="Complete",Ore="Iron",Progress=probe.Depth});Drain(p);
Assert.Single(p.Regions,r=>r.Patch);Assert.Equal(9,p.Jobs.Count(j=>j.Kind==P.JobKind.Probe));
p.Report(probe,new P.JobReport {Outcome="Discovery",Ore="Iron",Progress=5});
p.Report(probe,new P.JobReport {Outcome="Complete",Ore="Iron",Progress=probe.Depth});Drain(p);
Assert.Single(p.Regions,r=>r.Patch);Assert.Equal(9,p.Jobs.Count(j=>j.Kind==P.JobKind.Probe));
}
[Fact]
public void TwoCompletedEmptyRingsStopExpansion()
{
var p=Planner("[Survey]\nExpansionWidth=60\nExpansionHeight=60\n");P.Job probe=SeedProbe(p);
// Widen the frozen search so both complete expansion rings fit.
p.Regions[0].Radius=100;
p.Report(probe,new P.JobReport {Outcome="Complete",Ore="Iron",Progress=probe.Depth});Drain(p);
Assert.Equal(8,p.Jobs.Count(j=>j.Kind==P.JobKind.Probe&&!P.TaskPlanner.Finished(j)));
foreach(P.Job j in p.Jobs.Where(j=>j.Kind==P.JobKind.Probe&&!P.TaskPlanner.Finished(j)).ToList()) p.Report(j,new P.JobReport {Outcome="Complete",Progress=j.Depth});Drain(p);
Assert.Equal(16,p.Jobs.Count(j=>j.Kind==P.JobKind.Probe&&!P.TaskPlanner.Finished(j)));
foreach(P.Job j in p.Jobs.Where(j=>j.Kind==P.JobKind.Probe&&!P.TaskPlanner.Finished(j)).ToList()) p.Report(j,new P.JobReport {Outcome="Complete",Progress=j.Depth});Drain(p);
Assert.Equal(25,p.Jobs.Count(j=>j.Kind==P.JobKind.Probe));Assert.DoesNotContain(p.Jobs,j=>!P.TaskPlanner.Finished(j));
Assert.Contains("two empty rings",p.Regions.Single(r=>r.Patch).Status);
}
[Fact]
public void InvalidSamplesNeverCountAsConfirmedEmptyRings()
{
var p=Planner();P.Job probe=SeedProbe(p);p.Report(probe,new P.JobReport {Outcome="Complete",Ore="Iron",Progress=probe.Depth});Drain(p);
foreach(P.Job j in p.Jobs.Where(j=>j.Kind==P.JobKind.Probe&&!P.TaskPlanner.Finished(j)).ToList()) p.Report(j,new P.JobReport {Outcome="InvalidSample",Progress=j.Depth});Drain(p);
var patch=p.Regions.Single(r=>r.Patch);Assert.Equal(0,patch.EmptyRings);Assert.Contains("incomplete",patch.Status);
}
[Fact]
public void PausedReportPreservesOwnershipAndMonotonicProgress()
{
var p=Planner();p.Manual(Vector3D.Zero,Vector3D.Forward,Vector3D.Up,6,6,30);P.Job j=p.Jobs[0];j.Owner="miner-1";j.Progress=12;
p.Report(j,new P.JobReport {Outcome="Paused",Progress=8});Drain(p);
Assert.False(P.TaskPlanner.Finished(j));Assert.Equal("miner-1",j.Owner);Assert.Equal(12,j.Progress);Assert.Equal("Pending",p.Regions[0].Status);
}
[Fact]
public void PlannerStoragePreservesFrozenGeometryOccupancyAndProgress()
{
var p=Planner();p.Manual(new Vector3D(1,2,3),Vector3D.Forward,Vector3D.Up,12,12,30);p.Jobs[0].Owner="miner-1";p.Jobs[0].Progress=12;
var saved=new MyIni();p.Save(saved);var restored=Planner();restored.Load(saved);
Assert.Equal(p.Jobs.Count,restored.Jobs.Count);Assert.Equal(p.Regions[0].Center,restored.Regions[0].Center);
Assert.Equal("miner-1",restored.Jobs[0].Owner);Assert.Equal(12,restored.Jobs[0].Progress);
Assert.Equal(p.Jobs.Select(j=>j.Entry),restored.Jobs.Select(j=>j.Entry));
}
[Fact]
public void StorageRejectsInvalidBoundsDuplicateIdentifiersAndSerial()
{
var p=Planner();p.Manual(Vector3D.Zero,Vector3D.Forward,Vector3D.Up,12,6,30);var saved=new MyIni();p.Save(saved);
saved.Set("job0","Depth",-1);Assert.Throws<ArgumentException>(()=>Planner().Load(saved));
saved.Clear();p.Save(saved);saved.Set("job1","Id",p.Jobs[0].Id);Assert.Throws<ArgumentException>(()=>Planner().Load(saved));
saved.Clear();p.Save(saved);saved.Set("planner","Serial",-1);Assert.Throws<ArgumentException>(()=>Planner().Load(saved));
}
[Fact]
public void IncrementalCheckpointPatchesEarlierCompletionsAndAppendedTasks()
{
var p=Planner();p.Manual(Vector3D.Zero,Vector3D.Forward,Vector3D.Up,300,6,20);Drain(p);p.Jobs[0].Owner="miner-1";
var saved=new MyIni();Assert.False(p.Checkpoint(saved));
p.Report(p.Jobs[0],new P.JobReport {Outcome="Complete",Progress=20});p.Jobs[30].Owner="miner-2";
p.Manual(new Vector3D(1000,0,0),Vector3D.Forward,Vector3D.Up,6,6,20);
int frames=0;while(!p.Checkpoint(saved)&&frames++<100){}Assert.True(frames<100);
var restored=Planner();restored.Load(saved);Assert.Equal(p.Jobs.Count,restored.Jobs.Count);
Assert.Equal("Complete",restored.Jobs[0].Outcome);Assert.Equal("",restored.Jobs[0].Owner);Assert.Equal("miner-2",restored.Jobs[30].Owner);
}
[Fact]
public void SurveyCommandQueuesBoundedWorkAndPreservesAllRays()
{
var p=Planner();p.Survey(Vector3D.Zero,Vector3D.Zero,Vector3D.Forward,Vector3D.Up,2000);
Assert.InRange(p.Jobs.Count,1,8);Assert.True(p.Generating);
int count=p.Jobs.Count;p.Advance();Assert.InRange(p.Jobs.Count-count,0,8);
Drain(p);Assert.False(p.Generating);Assert.Equal(294,p.Jobs.Count);Assert.Equal(294,p.Jobs.Select(j=>j.Id).Distinct().Count());
}
[Fact]
public void PendingGenerationReservesCapacityAcrossCommands()
{
var p=Planner("[Survey]\nMaxJobs=300\n");p.Survey(Vector3D.Zero,Vector3D.Zero,Vector3D.Forward,Vector3D.Up,2000);
Assert.Throws<ArgumentException>(()=>p.Manual(Vector3D.Zero,Vector3D.Forward,Vector3D.Up,12,12,20));
Drain(p);Assert.Equal(294,p.Jobs.Count);
}
[Fact]
public void GenerationYieldsWhenInstructionBudgetIsSpent()
{
var program=TestRig.Program();var runtime=new Stub<Sandbox.ModAPI.Ingame.IMyGridProgramRuntimeInfo>().Set("MaxInstructionCount",50000).Set("CurrentInstructionCount",30000);TestRig.SetBase(program,"Runtime",runtime.Value);
var p=new P.TaskPlanner(program);p.Survey(Vector3D.Zero,Vector3D.Zero,Vector3D.Forward,Vector3D.Up,2000);p.Advance();Assert.Empty(p.Jobs);Assert.True(p.Generating);
runtime.Set("CurrentInstructionCount",0);Drain(p);Assert.Equal(294,p.Jobs.Count);
}
[Fact]
public void RestartResumesPartialGenerationWithoutMissingOrRepeatingHoles()
{
var p=Planner();p.Survey(Vector3D.Zero,Vector3D.Zero,Vector3D.Forward,Vector3D.Up,2000);var save=new MyIni();p.Save(save);
var restored=Planner();restored.Load(save);Assert.True(restored.Generating);Drain(p);Drain(restored);
Assert.Equal(p.Jobs.Select(j=>j.Id),restored.Jobs.Select(j=>j.Id));Assert.Equal(p.Jobs.Select(j=>j.Entry),restored.Jobs.Select(j=>j.Entry));
}
[Fact]
public void RecordWriterRejectsAnyRecordTheReaderCannotRestore()
{
var ini=new MyIni();ini.Set("payload","Value",new string('x',32768));Assert.Throws<ArgumentException>(()=>P.TaskPlanner.StorageRecord(ini,"Fleet"));
}
[Fact]
public void RegionSummaryRestartsWhenJobsAreAppendedBetweenFrames()
{
var p=Planner();p.Survey(Vector3D.Zero,Vector3D.Down,Vector3D.Forward,Vector3D.Up,20);
p.Report(p.Jobs[0],new P.JobReport {Outcome="SurveyEmpty"});
for(int n=0;n<100;n++)p.Manual(new Vector3D(1000+n*20,0,0),Vector3D.Forward,Vector3D.Up,6,6,10);
p.Advance();p.Advance();
// Append a new pending hole in the region after its earlier jobs were scanned.
p.Manual(new Vector3D(5000,0,0),Vector3D.Forward,Vector3D.Up,6,6,10);p.Jobs.Last().RegionId=p.Regions[0].Id;
Drain(p);Assert.Equal("Pending",p.Regions[0].Status);
}
[Fact]
public void UnknownOreDoesNotCreateTargetDeposit()
{
var p=Planner("[Mining]\nTargetOres=Iron,Nickel,Cobalt\n");P.Job probe=SeedProbe(p);p.Report(probe,new P.JobReport {Outcome="Complete",Ore="Stone,Ice",Progress=probe.Depth});Drain(p);
Assert.DoesNotContain(p.Regions,r=>r.Patch);Assert.Equal(0,p.OrePriority("Stone"));Assert.True(p.OrePriority("Iron")>p.OrePriority("Cobalt"));
Assert.Equal("Stone,Ice",probe.ConfirmedOres);
}
[Theory]
[InlineData("")]
[InlineData("[Mining]\nTargetOres=\n")]
[InlineData("[Mining]\nTargetOres= , , \n")]
public void EmptyTargetsMineStoneAndAnyOtherOre(string config)
{
var p=Planner(config);var probe=SeedProbe(p);
p.Report(probe,new P.JobReport {Outcome="Complete",Ore="Stone,Ice,ModOre",Progress=probe.Depth});Drain(p);
var patch=Assert.Single(p.Regions,r=>r.Patch);Assert.Equal("Stone,Ice,ModOre",patch.Ore);
Assert.Equal(9,p.Jobs.Count(j=>j.Kind==P.JobKind.Probe));Assert.Equal(0,p.OrePriority("Stone"));
}
[Fact]
public void TargetOrderDoesNotSetPriorityAndPriorityCannotBypassTargets()
{
var p=Planner("[Mining]\nTargetOres=Stone,Gold,Iron,Cobalt\nPriorityOres=Cobalt,Nickel,Iron\n");
Assert.Equal("Cobalt",p.PreferredOre("Stone,Iron,Gold,Cobalt,Nickel"));
Assert.Equal("",p.PreferredOre("Nickel"));Assert.Equal(0,p.OrePriority("Nickel"));
Assert.Equal(0,p.OrePriority("Gold"));Assert.Equal(0,p.OrePriority("Stone"));
var unranked=Planner("[Mining]\nPriorityOres=\n");Assert.Equal("Stone",unranked.PreferredOre("Stone,Iron"));Assert.Equal(0,unranked.OrePriority("Iron"));
}
[Fact]
public void DefaultPriorityOrderIsIronCobaltNickelSilicon()
{
var p=Planner();Assert.True(p.OrePriority("Iron")>p.OrePriority("Cobalt"));Assert.True(p.OrePriority("Cobalt")>p.OrePriority("Nickel"));
Assert.True(p.OrePriority("Nickel")>p.OrePriority("Silicon"));Assert.True(p.OrePriority("Silicon")>p.OrePriority("Gold"));
Assert.Equal("Iron",p.PreferredOre("Stone,Silicon,Nickel,Cobalt,Iron"));
}
[Fact]
public void CompleteReportAddsLaterOresAndRestoredJobsUseCurrentPreference()
{
var p=Planner();var probe=SeedProbe(p);
p.Report(probe,new P.JobReport {Outcome="Discovery",Ore="Stone",Progress=5});
p.Report(probe,new P.JobReport {Outcome="Complete",Ore="Stone,Iron,Cobalt",Progress=probe.Depth});Drain(p);
Assert.Equal("Stone,Iron,Cobalt",probe.ConfirmedOres);Assert.Equal("Stone,Iron,Cobalt",p.Regions.Single(r=>r.Patch).Ore);
var saved=new MyIni();p.Save(saved);
var restored=Planner("[Mining]\nTargetOres=Cobalt\nPriorityOres=Cobalt\n");restored.Load(saved);
var pending=restored.Jobs.First(j=>j.Kind==P.JobKind.Probe&&!P.TaskPlanner.Finished(j));
Assert.Equal(11,restored.JobPriority(pending));
var excluded=Planner("[Mining]\nTargetOres=Gold\n");excluded.Load(saved);
Assert.Equal(-1,excluded.JobPriority(excluded.Find(pending.Id)));
}
[Fact]
public void LateTerminalOreReportUpdatesPatchWithoutReopeningFinishedHole()
{
var p=Planner();var probe=SeedProbe(p);
p.Report(probe,new P.JobReport {Outcome="Complete",Ore="Stone",Progress=probe.Depth});Drain(p);
int count=p.Jobs.Count;var pending=p.Jobs.First(j=>j.Kind==P.JobKind.Probe&&!P.TaskPlanner.Finished(j));Assert.Equal(10,p.JobPriority(pending));
p.Report(probe,new P.JobReport {Outcome="Complete",Ore="Iron,Stone",Progress=probe.Depth});Drain(p);
Assert.Equal(count,p.Jobs.Count);Assert.True(P.TaskPlanner.Finished(probe));Assert.Equal("Stone,Iron",probe.ConfirmedOres);
Assert.Equal("Stone,Iron",p.Regions.Single(r=>r.Patch).Ore);Assert.Equal(14,p.JobPriority(pending));
}
}
}