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);
}
}
}
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);
}
}
}