using System; using System.Globalization; using System.Threading; using VRage.Game.ModAPI.Ingame.Utilities; using VRageMath; using Xunit; using P = AutoMiningScript.Program; namespace AutoMiningScript.Tests { public class ProtocolTests { static P.Packet Message() { var body = new MyIni(); body.Set("job", "Id", "base-hole-1"); body.Set("job", "Entry", "0,0,0"); body.Set("job", "Note", "第一行\n[not an outer section]\nkey=value; retained"); return new P.Packet { Kind = "JOB", From = "base", Target = "miner-01", Id = "session-5", Session = "session", Seq = 5, Reliable = true, Source = 123, Body = body }; } [Fact] public void WaitingSlotRoundTripsAndLegacyTelemetryRequestsAnUnassignedSlot() { var body=new P.Telemetry {WaitingSlot=23}.ToIni();Assert.Equal(23,P.Telemetry.FromIni(body).WaitingSlot); body.Delete("telemetry","WaitingSlot");Assert.Equal(-1,P.Telemetry.FromIni(body).WaitingSlot); body.Set("telemetry","WaitingSlot",1000001);Assert.Throws(()=>P.Telemetry.FromIni(body)); } [Fact] public void CancellationWatermarkRoundTripsWithoutAcknowledgingLegacyTelemetry() { var telemetry=new P.Telemetry {CancelledThrough=100};var body=telemetry.ToIni(); Assert.Equal(100,P.Telemetry.FromIni(body).CancelledThrough); body.Delete("telemetry","CancelledThrough");Assert.Equal(-1,P.Telemetry.FromIni(body).CancelledThrough); body.Set("telemetry","CancelledThrough",-2);Assert.Throws(()=>P.Telemetry.FromIni(body)); } [Fact] public void PacketRoundTripKeepsAtomicMultilinePayloadAndRouting() { var input = Message(); P.Packet output; Assert.True(P.Packet.TryDecode(input.Encode("mine-a"), "mine-a", out output)); Assert.Equal(input.Kind, output.Kind); Assert.Equal(input.From, output.From); Assert.Equal(input.Target, output.Target); Assert.Equal(input.Id, output.Id); Assert.Equal(input.Session, output.Session); Assert.Equal(5, output.Seq); Assert.True(output.Reliable); Assert.Equal(input.Body.Get("job", "Note").ToString(), output.Body.Get("job", "Note").ToString()); Assert.Equal("0,0,0", output.Body.Get("job", "Entry").ToString()); // Transport identity comes from IGC, never from a sender-supplied payload. Assert.Equal(0, output.Source); } [Fact] public void PacketRejectsOtherFleetAndOtherProtocolVersion() { P.Packet parsed; string encoded = Message().Encode("mine-a"); Assert.False(P.Packet.TryDecode(encoded, "mine-b", out parsed)); var ini = new MyIni(); Assert.True(ini.TryParse(encoded)); ini.Set("wire", "Version", 1); Assert.False(P.Packet.TryDecode(ini.ToString(), "mine-a", out parsed)); } [Theory] [InlineData("From", "")] [InlineData("Kind", "")] [InlineData("Session", "")] [InlineData("Seq", "0")] [InlineData("Seq", "-1")] [InlineData("Seq", "NaN")] [InlineData("Version", "Infinity")] [InlineData("Payload", "[broken")] public void PacketRejectsInvalidEnvelope(string field, string value) { var ini = new MyIni(); Assert.True(ini.TryParse(Message().Encode("mine-a"))); ini.Set("wire", field, value); P.Packet parsed; Assert.False(P.Packet.TryDecode(ini.ToString(), "mine-a", out parsed)); Assert.Null(parsed); } [Fact] public void PacketRejectsOversizeMalformedAndOverlongIdentities() { P.Packet parsed; Assert.False(P.Packet.TryDecode(null, "mine-a", out parsed)); Assert.False(P.Packet.TryDecode(new string('x', 24001), "mine-a", out parsed)); Assert.False(P.Packet.TryDecode("[broken", "mine-a", out parsed)); var input = Message(); input.From = new string('a', 65); Assert.False(P.Packet.TryDecode(input.Encode("mine-a"), "mine-a", out parsed)); input = Message(); input.Kind = new string('x', 33); Assert.False(P.Packet.TryDecode(input.Encode("mine-a"), "mine-a", out parsed)); input = Message(); input.Id = new string('x', 161); Assert.False(P.Packet.TryDecode(input.Encode("mine-a"), "mine-a", out parsed)); } [Theory] [InlineData("NaN")] [InlineData("Infinity")] [InlineData("-Infinity")] [InlineData("1e309")] public void SettingsAndVectorsRejectNonFiniteNumbers(string value) { var config = new P.Settings("[Flight]\nCruiseSpeed=" + value, "miner", "miner-1"); var exception = Assert.Throws(() => config.Number("Flight", "CruiseSpeed", 15, .5, 95)); Assert.Contains(P.Settings.SectionName("Flight")+"."+P.Settings.ParameterName("CruiseSpeed"), exception.Message); Vector3D vector; Assert.False(P.Data.TryVector("1," + value + ",3", out vector)); } [Fact] public void ZeroWorldCoordinatesAndGpsAreValid() { Vector3D vector; Assert.True(P.Data.TryVector("0,0,0", out vector)); Assert.Equal(Vector3D.Zero, vector); Assert.True(P.Data.TryGps("GPS:origin:0:0:0:", out vector)); Assert.Equal(Vector3D.Zero, vector); Assert.False(P.Data.TryGps("GPS:bad:0:NaN:0:", out vector)); Assert.False(P.Data.TryVector("0,0", out vector)); } [Fact] public void SerializationIsInvariantUnderCommaDecimalCulture() { var previous = Thread.CurrentThread.CurrentCulture; try { Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo("fr-FR"); Assert.Equal("1.25,-2.5,0", P.Data.Vector(new Vector3D(1.25, -2.5, 0))); Vector3D parsed; Assert.True(P.Data.TryVector("1.25,-2.5,0", out parsed)); Assert.Equal(1.25, parsed.X, 12); Assert.Equal(-2.5, parsed.Y, 12); var job = new P.Job { Id = "job-1", Entry = parsed, Direction = Vector3D.Forward, Up = Vector3D.Up, Depth = 12.75, Progress = 2.5 }; var restored = P.Job.FromIni(job.ToIni()); Assert.Equal(job.Entry, restored.Entry); Assert.Equal(12.75, restored.Depth, 12); var config = new P.Settings("[Flight]\nCruiseSpeed=1.25", "miner", "miner-1"); Assert.Equal(1.25, config.Number("Flight", "CruiseSpeed", 15, .5, 95), 12); } finally { Thread.CurrentThread.CurrentCulture = previous; } } [Fact] public void SettingsReportTheExactInvalidFieldAndPreserveInput() { string source = "[Flight]\nCruiseSpeed=96\n[Watchdog]\nEnabled=yes\n[Fleet]\nMaxMiners=17"; var config = new P.Settings(source, "miner", "miner-1"); Assert.Contains(P.Settings.SectionName("Flight")+"."+P.Settings.ParameterName("CruiseSpeed"), Assert.Throws(() => config.Number("Flight", "CruiseSpeed", 15, .5, 95)).Message); Assert.Contains(P.Settings.SectionName("Watchdog")+"."+P.Settings.ParameterName("Enabled"), Assert.Throws(() => config.Flag("Watchdog", "Enabled", false)).Message); Assert.Contains(P.Settings.SectionName("Fleet")+"."+P.Settings.ParameterName("MaxMiners"), Assert.Throws(() => config.Integer("Fleet", "MaxMiners", 16, 1, 16)).Message); Assert.Equal("96", config.Text("Flight", "CruiseSpeed", "fallback")); Assert.Equal(15, config.Number("Missing", "Speed", 15, 1, 95)); Assert.False(config.Flag("Missing", "Enabled", false)); } [Fact] public void SettingsIgnoreLegacyRoleButRejectMalformedIniAndInvalidIdentity() { Assert.Equal("miner", new P.Settings("[System]\nRole=fleet", "miner", "miner-1").Role); Assert.Throws(() => new P.Settings("[broken", "miner", "miner-1")); Assert.Throws(() => new P.Settings("[System]\nId=", "miner", "miner-1")); Assert.Throws(() => new P.Settings("[System]\nFleetId=" + new string('a', 65), "miner", "miner-1")); } [Fact] public void DockPointVelocityIncludesRotationAroundCenterOfMass() { var dock = new P.DockFrame { Center = new Vector3D(10, 20, 30), Position = new Vector3D(12, 20, 30), Velocity = new Vector3D(1, 2, 3), AngularVelocity = new Vector3D(0, 0, .5) }; Assert.Equal(new Vector3D(1, 3, 3), dock.PointVelocity(dock.Position)); Assert.Equal(dock.Velocity, dock.PointVelocity(dock.Center)); } [Fact] public void DockWindowRejectsStaleFastRotatingAndAcceleratingBases() { var config = new P.Settings("", "miner", "miner-1"); var dock = new P.DockFrame { ReceivedAt = 10, Forward = Vector3D.Forward, Up = Vector3D.Up }; Assert.True(dock.IsWindow(config, 10.5)); Assert.False(dock.IsWindow(config, 10.5001)); dock.Velocity = new Vector3D(5.01, 0, 0); Assert.False(dock.IsWindow(config, 10)); dock.Velocity = Vector3D.Zero; dock.AngularVelocity = new Vector3D(0, .21 * Math.PI / 180, 0); Assert.False(dock.IsWindow(config, 10)); dock.AngularVelocity = Vector3D.Zero; dock.Acceleration = new Vector3D(.1001, 0, 0); Assert.False(dock.IsWindow(config, 10)); } [Fact] public void DockWindowUsesConnectorSpeedInsteadOfCenterSpeed() { var config = new P.Settings("", "miner", "miner-1"); var dock = new P.DockFrame { ReceivedAt = 10, Position = new Vector3D(2000, 0, 0), AngularVelocity = new Vector3D(0, 0, .003) }; Assert.True(dock.AngularVelocity.Length() < .2 * Math.PI / 180); Assert.Equal(6, dock.PointVelocity(dock.Position).Length(), 10); Assert.False(dock.IsWindow(config, 10)); } [Fact] public void DockPredictionClampsHorizonAndRotatesTheConnectorOffset() { var dock = new P.DockFrame { Center = new Vector3D(100, 200, 300), Position = new Vector3D(110, 200, 300), Velocity = new Vector3D(2, 0, 0), AngularVelocity = new Vector3D(0, 0, 1), ReceivedAt = 10 }; Assert.Equal(dock.Position, dock.Predicted(9).Translation); var expected = dock.Center + new Vector3D(1, 0, 0) + new Vector3D(10 * Math.Cos(.5), 10 * Math.Sin(.5), 0); Assert.True(Vector3D.Distance(expected, dock.Predicted(1000).Translation) < 1e-9); Assert.Equal(dock.Predicted(10.5).Translation, dock.Predicted(1000).Translation); var frame = dock.Predicted(10.25); Assert.Equal(1, frame.Forward.Length(), 10); Assert.Equal(1, frame.Up.Length(), 10); Assert.True(Math.Abs(Vector3D.Dot(frame.Forward, frame.Up)) < 1e-10); Assert.True(Vector3D.Distance(frame.Translation + frame.Forward * 30, dock.GetPoint(30, 10.25)) < 1e-9); } [Fact] public void DockRoundTripPreservesGeometryButDoesNotTrustArrivalTimeOrSource() { var original = new P.DockFrame { BaseId = "base", Token = "lease-1", Owner = "miner-1", ConnectorId = 42, GridId = 7, Position = new Vector3D(1, 2, 3), Center = Vector3D.Zero, Velocity = new Vector3D(3, 2, 1), AngularVelocity = new Vector3D(0, .1, 0), ReceivedAt = 1234, Source = 9876, Connected = true }; var restored = P.DockFrame.FromIni(original.ToIni()); Assert.Equal(original.Position, restored.Position); Assert.Equal(original.Velocity, restored.Velocity); Assert.Equal(original.GridId, restored.GridId); Assert.Equal(original.ConnectorId, restored.ConnectorId); Assert.Equal(original.Owner, restored.Owner); Assert.Equal(original.Token, restored.Token); Assert.True(restored.Connected); Assert.Equal(0, restored.ReceivedAt); Assert.Equal(0, restored.Source); } } }