using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Runtime.Remoting; using Sandbox.ModAPI.Ingame; using VRage.Game.ModAPI.Ingame.Utilities; using VRageMath; using Xunit; using P = AutoMiningScript.Program; namespace AutoMiningScript.Tests { public class WaitingBerthLayoutTests { const string WaitingOnly = "[Dock]\nStableSeconds=30\n"; static readonly Vector3D RowOrigin = new Vector3D(-18, 7, 21); static MatrixD SetPort(FleetStateTests.Rig rig, int index, Vector3D position, Vector3D forward, double roll) { forward = Vector3D.Normalize(forward); var up = Vector3D.TransformNormal(P.Data.Perpendicular(forward, Vector3D.Up), MatrixD.CreateFromAxisAngle(forward, roll * Math.PI / 180)); var pose = MatrixD.CreateWorld(position, forward, up); rig.SetPort(index, position, forward); var ports = (List>)typeof(FleetStateTests.Rig).GetField("portStubs", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(rig); ports[index].Set("WorldMatrix", pose); return pose; } static void SetHorizontalPorts(FleetStateTests.Rig rig, int count) { for (int i = 0; i < count; i++) SetPort(rig, i, RowOrigin + Vector3D.Right * (12 * i), Vector3D.Forward, i * 90); } static void RequestPoses(FleetStateTests.Rig rig, IEnumerable miners) { var list = miners.ToList(); rig.Sent.Clear(); var ports = (List>)typeof(FleetStateTests.Rig).GetField("portStubs", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(rig); for (int i = 0; i < ports.Count; i++) { var occupant = new Stub().Set("EntityId", 9000L + i); ports[i].Set("Status", MyShipConnectorStatus.Connected).Set("OtherConnector", occupant.Value); } foreach (var miner in list) { miner.State = P.FlightState.Holding; miner.Connected = false; rig.Receive("HELLO", miner); rig.Receive("DOCK_REQUEST", miner); } rig.Tick(20); // Occupied physical connectors keep these as waiting poses. Base // motion and stability no longer delay allocation of a free berth. Assert.DoesNotContain(rig.Sent, p => p.Kind == "DOCK_GRANT"); } static P.DockFrame Pose(FleetStateTests.Rig rig, string miner) { return P.DockFrame.FromIni(rig.Sent.Last(p => p.Kind == "DOCKPOSE" && p.Target == miner).Body); } static Vector3D WaitingPoint(FleetStateTests.Rig rig, P.DockFrame pose) { pose.ReceivedAt = rig.Program.Now; return Vector3D.Transform(pose.WaitingLocal, pose.Predicted(rig.Program.Now)); } static void Near(Vector3D expected, Vector3D actual) { Assert.True(Vector3D.Distance(expected, actual) < .000001, expected + " vs " + actual); } static P.Telemetry AddMiner(FleetStateTests.Rig rig, string id) { var miner = new P.Telemetry { Id = id, State = P.FlightState.Holding, Position = new Vector3D(1000, 1000, -1000), Radius = 3, Battery = 1, Hydrogen = 1 }; rig.Miners.Add(miner); return miner; } [Theory] [InlineData(0)] [InlineData(90)] [InlineData(180)] public void ConnectorRollDoesNotRotateAHorizontalWaitingRow(double roll) { var rig = new FleetStateTests.Rig(4, false, WaitingOnly); var ports = Enumerable.Range(0, 4).Select(i => SetPort(rig, i, RowOrigin + Vector3D.Right * (12 * i), Vector3D.Forward, roll)).ToArray(); RequestPoses(rig, rig.Miners); var points = new List(); for (int i = 0; i < 4; i++) { var pose = Pose(rig, rig.Miners[i].Id); var point = WaitingPoint(rig, pose); points.Add(point); Assert.Equal(100L + i, pose.ConnectorId); Near(ports[i].Translation + ports[i].Forward * 80, point); Near(ports[i].Up, pose.Up); // Mating attitude still follows the real connector roll. Near(Vector3D.Zero, Vector3D.Cross(point - ports[i].Translation, ports[i].Forward)); } for (int i = 1; i < points.Count; i++) Near(Vector3D.Right * 12, points[i] - points[i - 1]); } [Fact] public void MixedConnectorRollsKeepEachWaitingPointInFrontOfItsOwnHorizontalBerth() { var rig = new FleetStateTests.Rig(4, false, WaitingOnly); var rolls = new[] { 0d, 90d, 180d, -45d }; var ports = Enumerable.Range(0, 4).Select(i => SetPort(rig, i, RowOrigin + Vector3D.Right * (12 * i), Vector3D.Forward, rolls[i])).ToArray(); RequestPoses(rig, rig.Miners); for (int i = 0; i < 4; i++) { var pose = Pose(rig, rig.Miners[i].Id); Assert.Equal(100L + i, pose.ConnectorId); Near(RowOrigin + Vector3D.Right * (12 * i) + Vector3D.Forward * 80, WaitingPoint(rig, pose)); Near(ports[i].Up, pose.Up); } } [Fact] public void DifferentlyFacingBerthsPlaceWaitingPointsAlongTheirActualOpeningDirections() { var rig = new FleetStateTests.Rig(3, false, WaitingOnly); var forwards = new[] { Vector3D.Forward, Vector3D.Right, Vector3D.Normalize(new Vector3D(-1, .2, 1)) }; var ports = Enumerable.Range(0, 3).Select(i => SetPort(rig, i, RowOrigin + Vector3D.Right * (30 * i), forwards[i], i * 90)).ToArray(); RequestPoses(rig, rig.Miners); for (int i = 0; i < 3; i++) { var pose = Pose(rig, rig.Miners[i].Id); Assert.Equal(100L + i, pose.ConnectorId); Near(ports[i].Translation + ports[i].Forward * 80, WaitingPoint(rig, pose)); Near(ports[i].Forward, pose.Forward); Near(ports[i].Up, pose.Up); } } [Theory] [InlineData(40, 10)] [InlineData(180, 35)] [InlineData(500, 100)] public void AdditionalWaitingRowsUseTheSameBerthsAndConfiguredForwardSeparation(int distance, int spacing) { var rig = new FleetStateTests.Rig(2, false, WaitingOnly + "WaitingDistance=" + distance + "\nWaitingSpacing=" + spacing + "\n"); SetHorizontalPorts(rig, 2); for (int i = 2; i < 6; i++) AddMiner(rig, "miner-" + i); RequestPoses(rig, rig.Miners); var points = new List(); for (int i = 0; i < 6; i++) { var telemetry = rig.Fleet.GetTelemetry().Single(t => t.Id == rig.Miners[i].Id); Assert.Equal(i, telemetry.WaitingSlot); var pose = Pose(rig, telemetry.Id); var point = WaitingPoint(rig, pose); points.Add(point); Assert.Equal(100L + i % 2, pose.ConnectorId); var firstRowDistance=Math.Max(distance,pose.ApproachDistance+spacing); Near(RowOrigin + Vector3D.Right * (12 * (i % 2)) + Vector3D.Forward * (firstRowDistance + i / 2 * spacing), point); if (i >= 2) Near(Vector3D.Forward * spacing, points[i] - points[i - 2]); } } [Fact] public void ConnectorApproachDistancesAreUniqueAndStableAcrossRotationEnumerationAndReload() { var rig=new FleetStateTests.Rig(6,false,WaitingOnly);SetHorizontalPorts(rig,6); RequestPoses(rig,rig.Miners); var initial=rig.Miners.Select(t=>Pose(rig,t.Id)).ToDictionary(p=>p.ConnectorId,p=>p.ApproachDistance); Assert.Equal(new[]{30d,40d,50d,60d,70d,80d},initial.Values.OrderBy(d=>d)); var rotation=MatrixD.CreateFromAxisAngle(Vector3D.Normalize(new Vector3D(1,2,3)),.7); var actual=new Dictionary(); for(int n=0;n<6;n++) { var position=Vector3D.TransformNormal(RowOrigin+Vector3D.Right*(12*n),rotation)+new Vector3D(7,-11,17); actual[100+n]=SetPort(rig,n,position,Vector3D.TransformNormal(Vector3D.Forward,rotation),n*60+90); } RequestPoses(rig,rig.Miners); foreach(var miner in rig.Miners) { var pose=Pose(rig,miner.Id);var port=actual[pose.ConnectorId]; Assert.Equal(initial[pose.ConnectorId],pose.ApproachDistance); Near(port.Forward,pose.Forward);Near(port.Up,pose.Up); var entry=pose.Position+pose.Forward*pose.ApproachDistance; Near(port.Translation+port.Forward*initial[pose.ConnectorId],entry); Near(Vector3D.Zero,Vector3D.Cross(entry-port.Translation,port.Forward)); Assert.True(Vector3D.Dot(WaitingPoint(rig,pose)-port.Translation,port.Forward)>=pose.ApproachDistance+20-.000001); } // Rescan a genuinely reversed terminal enumeration, so cached offsets // must be rebuilt for the same IDs instead of merely staying cached. var group=rig.Program.GridTerminalSystem.GetBlockGroupWithName("AMS Docks"); var source=(Stub)RemotingServices.GetRealProxy(group); var fill=source.Methods["GetBlocksOfType"]; source.Method("GetBlocksOfType",args=>{var result=fill(args);((List)args[0]).Reverse();return result;}); typeof(P.FleetController).GetMethod("Scan",BindingFlags.NonPublic|BindingFlags.Instance).Invoke(rig.Fleet,new object[0]); RequestPoses(rig,rig.Miners); foreach(var miner in rig.Miners){var pose=Pose(rig,miner.Id);Assert.Equal(initial[pose.ConnectorId],pose.ApproachDistance);} var restored=new FleetStateTests.Rig(6,false,WaitingOnly);SetHorizontalPorts(restored,6);restored.Restore(rig.Checkpoint()); RequestPoses(restored,restored.Miners); foreach(var miner in restored.Miners){var pose=Pose(restored,miner.Id);Assert.Equal(initial[pose.ConnectorId],pose.ApproachDistance);} } [Fact] public void ConfiguredApproachBaseIsRespectedAndOldFramesRemainCompatible() { var rig=new FleetStateTests.Rig(3,false,WaitingOnly+"ApproachDistance=45\n");SetHorizontalPorts(rig,3); RequestPoses(rig,rig.Miners); Assert.Equal(new[]{45d,55d,65d},rig.Miners.Select(t=>Pose(rig,t.Id).ApproachDistance).OrderBy(d=>d)); Assert.Equal(0,P.DockFrame.FromIni(new MyIni()).ApproachDistance); var sent=new P.DockFrame {ApproachDistance=95};Assert.Equal(95,P.DockFrame.FromIni(sent.ToIni()).ApproachDistance); } [Fact] public void AnActiveAssignmentKeepsItsApproachDistanceWhenTheConfiguredBaseChanges() { var rig=new FleetStateTests.Rig(3,false); foreach(var miner in rig.Miners){miner.State=P.FlightState.Holding;miner.Position+=Vector3D.Forward*100;} rig.RegisterAll();rig.Tick(30);rig.Receive("DOCK_REQUEST",rig.Miners[0]);rig.Tick(12); var granted=P.DockFrame.FromIni(Assert.Single(rig.Sent,p=>p.Kind=="DOCK_GRANT").Body); rig.Program.Config=new P.Settings("[System]\nId=base\nBaseId=base\nFleetId=mining\n[Dock]\nApproachDistance=70\n","fleet","base"); rig.Sent.Clear();rig.Receive("TELEMETRY",rig.Miners[0]);rig.Tick(12); var updated=Pose(rig,rig.Miners[0].Id); Assert.Equal(granted.Token,updated.Token); Assert.Equal(granted.ApproachDistance,updated.ApproachDistance); Assert.Equal(granted.ApproachDistance,P.Data.ReadNumber(rig.Save(),"fleet","LaneApproachDistance",0)); rig.Sent.Clear();rig.Receive("DOCK_REQUEST",rig.Miners[1]);rig.Tick(12); var next=P.DockFrame.FromIni(Assert.Single(rig.Sent,p=>p.Kind=="DOCK_GRANT").Body); Assert.InRange(next.ApproachDistance,70,90); Assert.Equal(granted.ApproachDistance,Pose(rig,rig.Miners[0].Id).ApproachDistance); } [Fact] public void RemovingAnOfflineMinerAndReloadingDoNotMoveSurvivingWaitingAssignments() { var before = new FleetStateTests.Rig(3, false, WaitingOnly); SetHorizontalPorts(before, 3); RequestPoses(before, before.Miners); var original = before.Miners.Skip(1).ToDictionary(t => t.Id, t => WaitingPoint(before, Pose(before, t.Id))); before.Program.Now += 10; foreach (var miner in before.Miners.Skip(1)) before.Receive("TELEMETRY", miner); before.Fleet.Command("clear-offline"); Assert.DoesNotContain(before.Fleet.GetTelemetry(), t => t.Id == "miner-0"); var newcomer = AddMiner(before, "newcomer"); RequestPoses(before, before.Miners.Skip(1)); foreach (var pair in original) Near(pair.Value, WaitingPoint(before, Pose(before, pair.Key))); Assert.Equal(3, before.Fleet.GetTelemetry().Single(t => t.Id == newcomer.Id).WaitingSlot); var newcomerPoint = WaitingPoint(before, Pose(before, newcomer.Id)); Near(RowOrigin + Vector3D.Forward * 100, newcomerPoint); var after = new FleetStateTests.Rig(3, false, WaitingOnly); SetHorizontalPorts(after, 3); after.Restore(before.Checkpoint()); var restoredNewcomer = AddMiner(after, newcomer.Id); // Client values must not reallocate the server's persisted waiting slot. foreach (var miner in after.Miners.Skip(1)) miner.WaitingSlot = 999; RequestPoses(after, after.Miners.Skip(1)); foreach (var pair in original) Near(pair.Value, WaitingPoint(after, Pose(after, pair.Key))); Near(newcomerPoint, WaitingPoint(after, Pose(after, restoredNewcomer.Id))); Assert.Equal(new[] { 1, 2, 3 }, after.Fleet.GetTelemetry().Select(t => t.WaitingSlot).OrderBy(s => s).ToArray()); var next = AddMiner(after, "next"); after.Receive("HELLO", next); Assert.Equal(4, after.Fleet.GetTelemetry().Single(t => t.Id == next.Id).WaitingSlot); } } }