XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

AutoMiningScript

【SpaceEngineer】全自动挖矿脚本

公开
关注 0 Fork 0 Star 0
UTF-8
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox.ModAPI.Ingame;
using VRage.Game.GUI.TextPanel;
using VRageMath;
using Xunit;
using P = AutoMiningScript.Program;

namespace AutoMiningScript.Tests
{
    public class DisplayLocalizationTests
    {
        sealed class IdleRole : P.RoleLogic
        {
            readonly string diagnostics;
            readonly List<P.Telemetry> telemetry;
            public IdleRole(P program, string text = "", List<P.Telemetry> items = null) : base(program) { diagnostics = text; telemetry = items ?? new List<P.Telemetry>(); }
            public override string Diagnostics { get { return diagnostics; } }
            public override List<P.Telemetry> GetTelemetry() { return telemetry; }
        }

        sealed class ConsoleRig
        {
            public readonly P Program;
            public readonly P.Dashboard Dashboard;
            public readonly List<MySprite> Sprites = new List<MySprite>();
            public readonly Stub<IMyTextSurface> MainSurface;
            public readonly Stub<IMyTextSurface> AuxiliarySurface;
            public int FrameCount;

            public readonly Vector2 ViewportSize;
            public readonly Vector2 TextureSize;

            public ConsoleRig(string config = "", bool grouped = false, Vector2? viewport = null, Vector2? texture = null)
            {
                ViewportSize = viewport ?? new Vector2(512, 512);
                TextureSize = texture ?? ViewportSize;
                Program = TestRig.Program(config);
                MainSurface = Surface(); AuxiliarySurface = Surface();
                var me = new Stub<IMyProgrammableBlock>()
                    .Set("CubeGrid", Program.Me.CubeGrid).Set("EntityId", 20L)
                    .Set("CustomName", "PB without LCD tag").Set("CustomData", config).Set("SurfaceCount", 2)
                    .Method("GetSurface", a => (int)a[0] == 0 ? MainSurface.Value : AuxiliarySurface.Value);
                TestRig.SetBase(Program, "Me", me.Value);
                var terminal = new Stub<IMyGridTerminalSystem>();
                if (grouped)
                {
                    var group = new Stub<IMyBlockGroup>().Method("GetBlocks", a =>
                    {
                        ((List<IMyTerminalBlock>)a[0]).Add(me.Value); return null;
                    });
                    terminal.Method("GetBlockGroupWithName", a => group.Value);
                }
                TestRig.SetBase(Program, "GridTerminalSystem", terminal.Value);
                Dashboard = new P.Dashboard(Program);
            }

            Stub<IMyTextSurface> Surface()
            {
                return new Stub<IMyTextSurface>().Set("TextureSize", TextureSize)
                    .Set("SurfaceSize", ViewportSize)
                    .Method("MeasureStringInPixels", a => Measure(a[0].ToString(), (float)a[2]))
                    .Method("DrawFrame", a =>
                    new MySpriteDrawFrame(frame => { frame.AddToList(Sprites); FrameCount++; }));
            }

            public void Draw(P.RoleLogic role = null) { Dashboard.Draw(role ?? new IdleRole(Program)); }
            public IEnumerable<string> Text { get { return Sprites.Where(s => s.Type == SpriteType.TEXT).Select(s => s.Data); } }
        }

        // Deterministic game-font stand-in: CJK glyphs are wider than Latin glyphs.
        // Assertions use these same surface-provided metrics, not character-count clipping.
        static Vector2 Measure(string text, float scale)
        {
            float width = 0, longest = 0; int lines = 1;
            foreach (char ch in text)
            {
                if (ch == '\n') { longest = Math.Max(longest, width); width = 0; lines++; }
                else if (ch != '\r') width += ch >= 0x2e80 ? 32 : 16;
            }
            return new Vector2(Math.Max(longest, width), lines * 32) * scale;
        }

        struct Bounds
        {
            public float Left, Top, Right, Bottom;
            public float Area { get { return (Right - Left) * (Bottom - Top); } }
            public bool Contains(Bounds other)
            {
                const float tolerance = .1f;
                return other.Left >= Left - tolerance && other.Top >= Top - tolerance && other.Right <= Right + tolerance && other.Bottom <= Bottom + tolerance;
            }
            public bool Overlaps(Bounds other) { return Left < other.Right && Right > other.Left && Top < other.Bottom && Bottom > other.Top; }
        }

        static Bounds SpriteBounds(MySprite sprite)
        {
            Vector2 position = sprite.Position.Value;
            if (sprite.Type == SpriteType.TEXT)
            {
                Vector2 size = Measure(sprite.Data, sprite.RotationOrScale);
                if (sprite.Alignment == TextAlignment.RIGHT) position.X -= size.X;
                else if (sprite.Alignment == TextAlignment.CENTER) position.X -= size.X / 2;
                return new Bounds { Left = position.X, Top = position.Y, Right = position.X + size.X, Bottom = position.Y + size.Y };
            }
            Vector2 half = sprite.Size.Value / 2;
            return new Bounds { Left = position.X - half.X, Top = position.Y - half.Y, Right = position.X + half.X, Bottom = position.Y + half.Y };
        }

        static string[] ConfigLabels
        {
            get
            {
                return new[] { P.L.DisplayConfigId, P.L.DisplayConfigFleet, P.L.DisplayConfigBase, P.L.DisplayConfigMass,
                    P.L.DisplayConfigReturnBattery, P.L.DisplayConfigChargeTarget, P.L.DisplayConfigReturnCargo, P.L.DisplayConfigDrillSpeed,
                    P.L.DisplayConfigHoleRadius, P.L.DisplayConfigSortieDepth, P.L.DisplayConfigTargetOres, P.L.DisplayConfigDepthLimit,
                    P.L.DisplayConfigDockSpeed, P.L.DisplayConfigDockAge, P.L.DisplayConfigWatchdogTimeout, P.L.DisplayConfigFont };
            }
        }

        static Bounds CardContaining(ConsoleRig rig, MySprite label)
        {
            Bounds text = SpriteBounds(label);
            return rig.Sprites.Where(s => s.Type == SpriteType.TEXTURE && s.Data == "SquareSimple")
                .Select(SpriteBounds).Where(b => b.Contains(text)).OrderBy(b => b.Area).First();
        }

        [Fact]
        public void UntaggedProgrammingBlockDrawsConfigurationInSelectedLanguage()
        {
            var rig = new ConsoleRig("[Flight]\nDepartureMass=13579\n"); rig.Draw();
            Assert.Equal(1, rig.Dashboard.ScreenCount);
            Assert.Equal(1, rig.FrameCount);
            Assert.Contains(P.L.Brand + " / " + P.Version, rig.Text);
            Assert.Contains(P.L.DisplayConfiguration, rig.Text);
            Assert.Contains(P.L.Role("miner") + " / " + P.L.Language, rig.Text);
            Assert.Contains(P.L.DisplayConfigMass, rig.Text);
            Assert.Contains("13579 kg", rig.Text);
            Assert.Equal(ContentType.SCRIPT, rig.MainSurface.Values["ContentType"]);
        }

        [Theory]
        [InlineData("[Display]\nBuiltIn=false\n")]
        [InlineData("[AMS.Screen]\nEnabled=false\n")]
        public void ProgrammingBlockOptOutIsRespected(string config)
        {
            var rig = new ConsoleRig(config, true); rig.Draw();
            Assert.Equal(0, rig.Dashboard.ScreenCount); Assert.Equal(0, rig.FrameCount);
            Assert.False(rig.MainSurface.Values.ContainsKey("ContentType"));
        }

        [Fact]
        public void GroupedProgrammingBlockIsDiscoveredOnlyOnce()
        {
            var rig = new ConsoleRig("", true); rig.Draw();
            Assert.Equal(1, rig.Dashboard.ScreenCount); Assert.Equal(1, rig.FrameCount);
        }

        [Fact]
        public void PerSurfacePageAndFontOverrideBuiltInDefaults()
        {
            var rig = new ConsoleRig("[Display]\nBuiltInPage=miner\n[AMS.Screen]\nPage=config\nFont=White\n"); rig.Draw();
            Assert.Contains(P.L.DisplayConfigId, rig.Text);
            Assert.All(rig.Sprites.Where(s => s.Type == SpriteType.TEXT), s => Assert.Equal("White", s.FontId));
        }

        [Fact]
        public void AuxiliaryProgrammingBlockSurfaceRequiresExplicitOptIn()
        {
            var rig = new ConsoleRig("[AMS.Screen.1]\nEnabled=true\nPage=miner\n"); rig.Draw();
            Assert.Equal(2, rig.Dashboard.ScreenCount); Assert.Equal(2, rig.FrameCount);
            Assert.Contains(P.L.F(P.L.DisplayNoTelemetry, P.L.DisplayThisMiner), rig.Text);
        }

        [Fact]
        public void PageCommandSelectsConfigurationAndPaginatesParameters()
        {
            var rig = new ConsoleRig("[Display]\nBuiltInPage=miner\n");
            rig.Dashboard.Command("page config"); rig.Dashboard.Command("page +"); rig.Dashboard.Command("page +"); rig.Draw();
            Assert.Contains(P.L.DisplayConfigTargetOres, rig.Text);
            Assert.Contains(P.L.OreList("Iron,Nickel,Cobalt"), rig.Text);
            Assert.Contains(P.L.F(P.L.DisplayConfigPages, 3, 4), rig.Text);
        }

        [Theory]
        [InlineData(512, 512, 1024, 1024, 4)]
        [InlineData(1024, 512, 2048, 1024, 5)]
        [InlineData(512, 768, 1024, 1024, 4)]
        public void ConfigurationUsesVisibleViewportAndReadableRows(int width, int height, int textureWidth, int textureHeight, int rows)
        {
            var rig = new ConsoleRig(viewport: new Vector2(width, height), texture: new Vector2(textureWidth, textureHeight)); rig.Draw();
            var background = rig.Sprites.Where(s => s.Type == SpriteType.TEXTURE).OrderByDescending(s => SpriteBounds(s).Area).First();
            Assert.Equal(rig.ViewportSize, background.Size.Value);
            Assert.Equal(rig.TextureSize / 2, background.Position.Value);
            Bounds viewport = SpriteBounds(background);
            var labels = rig.Sprites.Where(s => s.Type == SpriteType.TEXT && ConfigLabels.Contains(s.Data)).ToArray();
            Assert.Equal(rows, labels.Length);
            float scale = Math.Min(width, height) / 512f;
            foreach (var label in labels)
            {
                Assert.True(label.RotationOrScale >= .8f * scale, "Parameter labels must remain readable.");
                Bounds card = CardContaining(rig, label);
                Assert.True(card.Right - card.Left >= width * .85f, "Parameter cards should use the visible screen width.");
                var value = Assert.Single(rig.Sprites, s => s.Type == SpriteType.TEXT && s.Data != label.Data && card.Contains(SpriteBounds(s)));
                Assert.True(value.RotationOrScale >= .85f * scale, "Values must not shrink to the old tiny font size.");
                Assert.False(SpriteBounds(label).Overlaps(SpriteBounds(value)), "Labels and values must have separate space.");
            }
            Assert.All(rig.Sprites.Where(s => s.Type == SpriteType.TEXT), s => Assert.True(viewport.Contains(SpriteBounds(s)), "Text outside visible viewport: " + s.Data));
        }

        [Theory]
        [InlineData(512, 512, 4)]
        [InlineData(1024, 512, 5)]
        public void ConfigurationPaginationKeepsEveryParameterReachable(int width, int height, int rowsPerPage)
        {
            var rig = new ConsoleRig(viewport: new Vector2(width, height));
            var observed = new List<string>(); int pageCount = (ConfigLabels.Length + rowsPerPage - 1) / rowsPerPage;
            for (int page = 0; page < pageCount; page++)
            {
                rig.Sprites.Clear(); rig.Draw();
                observed.AddRange(rig.Text.Where(t => ConfigLabels.Contains(t)));
                Assert.Contains(P.L.F(P.L.DisplayConfigPages, page + 1, pageCount), rig.Text);
                rig.Dashboard.Command("page +");
            }
            Assert.Equal(ConfigLabels, observed);
            rig.Sprites.Clear(); rig.Draw();
            Assert.Contains(P.L.F(P.L.DisplayConfigPages, 1, pageCount), rig.Text);
            Assert.Contains(P.L.DisplayConfigId, rig.Text);
        }

        [Theory]
        [InlineData(false, 512)]
        [InlineData(true, 512)]
        [InlineData(false, 1024)]
        [InlineData(true, 1024)]
        public void LongIdentifiersStayInsideTheirCardsWithoutShrinkingOrOverlapping(bool chinese, int width)
        {
            string id = new string(chinese ? '矿' : 'W', 64);
            var rig = new ConsoleRig("[System]\nId=" + id + "\nFleetId=" + id + "\nBaseId=" + id + "\n", viewport: new Vector2(width, 512)); rig.Draw();
            foreach (string labelText in ConfigLabels.Take(3))
            {
                var label = Assert.Single(rig.Sprites, s => s.Type == SpriteType.TEXT && s.Data == labelText);
                Bounds card = CardContaining(rig, label);
                var value = Assert.Single(rig.Sprites, s => s.Type == SpriteType.TEXT && s.Data != labelText && card.Contains(SpriteBounds(s)));
                Assert.NotEqual(id, value.Data);
                Assert.StartsWith(id.Substring(0, 2), value.Data);
                Assert.True(value.RotationOrScale >= .85f, "Long values must be clipped or wrapped, not made illegibly small.");
                Assert.False(SpriteBounds(label).Overlaps(SpriteBounds(value)));
            }
        }

        [Fact]
        public void SurfaceScalingPreservesTextAndSpriteAspectRatio()
        {
            var large = new ConsoleRig(viewport: new Vector2(1024, 512)); large.Draw();
            var small = new ConsoleRig(viewport: new Vector2(512, 256)); small.Draw();
            Assert.Equal(large.Sprites.Count, small.Sprites.Count);
            for (int index = 0; index < large.Sprites.Count; index++)
            {
                var a = large.Sprites[index]; var b = small.Sprites[index];
                Assert.Equal(a.Type, b.Type); Assert.Equal(a.Data, b.Data);
                Assert.Equal(a.Position.Value / 2, b.Position.Value);
                if (a.Type == SpriteType.TEXT)
                {
                    Assert.Equal(a.RotationOrScale / 2, b.RotationOrScale);
                    Assert.Null(a.Size); Assert.Null(b.Size);
                }
                else Assert.Equal(a.Size.Value / 2, b.Size.Value);
            }
        }

        [Fact]
        public void ExistingMinerPageRetainsItsCenteredSquareCanvas()
        {
            var rig = new ConsoleRig("[Display]\nBuiltInPage=miner\n", viewport: new Vector2(1024, 512), texture: new Vector2(2048, 1024)); rig.Draw();
            var background = rig.Sprites.Where(s => s.Type == SpriteType.TEXTURE).OrderByDescending(s => SpriteBounds(s).Area).First();
            Assert.Equal(new Vector2(512, 512), background.Size.Value);
            Assert.Equal(rig.TextureSize / 2, background.Position.Value);
            Assert.Contains(P.L.F(P.L.DisplayNoTelemetry, P.L.DisplayThisMiner), rig.Text);
        }

        [Fact]
        public void PresentationMappingsPreserveProtocolTokensAndUnknownModdedOre()
        {
            foreach (P.FlightState state in Enum.GetValues(typeof(P.FlightState)))
                Assert.NotEqual(P.L.DisplayUnknown, P.L.State(state));
            Assert.Equal(P.L.DisplayOreIron + ":12.3, Unobtainium:4", P.L.OreList("Iron:12.3, Unobtainium:4"));
            Assert.Equal(P.L.DisplayOutcomeInvalidSample, P.L.Outcome("InvalidSample"));
            Assert.Equal("CustomOutcome", P.L.Outcome("CustomOutcome"));
            Assert.Equal("SurveyEmpty", new P.Job { Outcome = "SurveyEmpty" }.Outcome);
        }

        [Fact]
        public void ConfigurationShowsRoleDiagnosticWhenNoSelfTelemetryExists()
        {
            var rig = new ConsoleRig();
            rig.Draw(new IdleRole(rig.Program, P.L.DisplayStatePaused + "\nDetailed diagnostics"));
            var sprite = Assert.Single(rig.Sprites, s => s.Data == P.L.F(P.L.DisplayConfigStatus, P.L.DisplayStatePaused));
            Assert.Equal(new Color(255, 188, 78), sprite.Color);
            Assert.DoesNotContain(P.L.F(P.L.DisplayConfigStatus, P.L.DisplayNominal), rig.Text);
        }

        [Theory]
        [InlineData(false)]
        [InlineData(true)]
        public void ConfigurationMarksFaultOrOfflineTelemetryRed(bool offline)
        {
            var rig = new ConsoleRig();
            rig.Program.Now = offline ? 10 : 0;
            var telemetry = new P.Telemetry { Id = "test", State = offline ? P.FlightState.Ready : P.FlightState.Fault, ReceivedAt = 0 };
            rig.Draw(new IdleRole(rig.Program, "", new List<P.Telemetry> { telemetry }));
            string status = offline ? P.L.DisplayOffline : P.L.DisplayStateFault;
            var sprite = Assert.Single(rig.Sprites, s => s.Data == P.L.F(P.L.DisplayConfigStatus, status));
            Assert.Equal(new Color(255, 102, 117), sprite.Color);
        }
    }
}