using System; using System.Collections.Generic; using System.Text; using Sandbox.ModAPI.Ingame; using VRage.Game.GUI.TextPanel; using VRage.Game.ModAPI.Ingame.Utilities; using VRageMath; namespace AutoMiningScript { public partial class Program { // Screens belong to this rigid grid. A docked miner never draws on the mother ship. public partial class Dashboard { readonly Program p; readonly List screens = new List(); int nextScreen, pageOverride = -1, autoOverride = -1; List> configurationRows; #if !FLEET && !TESTS sealed class RegionView { public Job First; public Vector3D Up, Right; public double Extent = 1; public int Total, Complete, Sampled, Empty; public readonly List Dots = new List(); } Dictionary mapViews = new Dictionary(), buildingViews; List mapRegions = new List(), buildingRegions; List buildingSource; Job buildingFirst; int jobCursor, buildingDone, completedJobs; double nextJobRefresh; #endif double discoveredAt = -30; static readonly Color Background = new Color(9, 17, 27); static readonly Color Panel = new Color(23, 36, 51); static readonly Color White = new Color(225, 235, 246); static readonly Color Muted = new Color(138, 159, 181); static readonly Color Blue = new Color(57, 174, 235); static readonly Color Green = new Color(71, 218, 162); static readonly Color Amber = new Color(255, 188, 78); static readonly Color Red = new Color(255, 102, 117); static readonly Color ConsoleBackground = new Color(3, 7, 12); static readonly Color ConsolePanel = new Color(16, 27, 39); static readonly Color ConsoleLabel = new Color(211, 224, 237); static readonly Color ConsoleValue = new Color(250, 252, 255); class Screen { public IMyTextSurface Surface; public int Page; public string Miner = ""; public string Font = "Debug"; public string Key; public bool AutoPage, Started; public double Interval, ManualHold, HoldUntil, AdvanceAt, Progress; public int RenderedPage = -1, Slide, Count, PendingStep; } public Dashboard(Program program) { p = program; } public int ScreenCount => screens.Count; public void ClearTransientCache() { configurationRows = null; } public void Update(RoleLogic role) { if (p.HasBudget(0.45)) RefreshJobs(role); } #if !FLEET && !TESTS void RefreshJobs(RoleLogic role) { var jobs = role.GetJobs(); Job first = jobs.Count == 0 ? null : jobs[0]; bool replaced = jobs != buildingSource || first != buildingFirst; if (buildingViews == null && p.Now < nextJobRefresh && !replaced) return; if (buildingViews == null || replaced || jobCursor > jobs.Count) { buildingSource = jobs; buildingFirst = first; buildingViews = new Dictionary(); buildingRegions = new List(); jobCursor = buildingDone = 0; } int work = 0; while (jobCursor < jobs.Count && work++ < 64 && p.HasBudget(0.55)) { Job j = jobs[jobCursor++]; RegionView view; if (!buildingViews.TryGetValue(j.RegionId, out view)) { view = new RegionView { First = j, Up = Data.Perpendicular(j.Direction, j.Up) }; view.Right = Data.Cross(j.Direction, view.Up); buildingViews[j.RegionId] = view; buildingRegions.Add(j.RegionId); } Vector3D d = j.Entry - view.First.Entry; view.Extent = Math.Max(view.Extent, Math.Max(Math.Abs(Data.Dot(d, view.Right)), Math.Abs(Data.Dot(d, view.Up))) + j.Radius); view.Total++; if (Finished(j)) { view.Complete++; buildingDone++; } if (j.Kind == JobKind.Probe && j.Outcome == "Complete") view.Sampled++; if (j.Outcome == "SurveyEmpty") view.Empty++; if (view.Dots.Count < 400) view.Dots.Add(j); } if (jobCursor < jobs.Count) return; mapViews = buildingViews; mapRegions = buildingRegions; completedJobs = buildingDone; buildingViews = null; buildingRegions = null; nextJobRefresh = p.Now + 0.5; } #endif public void Command(string command) { command = Settings.NormalizeCommand(command); var words = command.Trim().ToLowerInvariant().Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); if (words.Length == 0 || words[0] != "page") return; var value = words.Length > 1 ? words[1] : "next"; if (discoveredAt < 0) Discover(); if (value == "stop") { foreach (var screen in screens) if (AutoPaging(screen)) screen.Progress = Progress(screen); autoOverride = 0; return; } if (value == "play") { autoOverride = 1; foreach (var screen in screens) { screen.HoldUntil = p.Now; RestartSlide(screen); } return; } if (value == "auto") { pageOverride = -1; autoOverride = -1; foreach (var screen in screens) { screen.RenderedPage = -1; screen.Slide = screen.PendingStep = 0; screen.HoldUntil = p.Now; RestartSlide(screen); } return; } var step = value == "+" || value == "-"; if (!step) { int current = pageOverride >= 0 ? pageOverride : screens.Count > 0 ? screens[0].Page : 0; int target = value == "next" ? (current + 1) % 5 : value == "prev" ? (current + 4) % 5 : ParsePage(value, -1); if (target < 0) return; pageOverride = target; } foreach (var screen in screens) { if (step) screen.PendingStep += value == "+" ? 1 : -1; else { screen.Slide = screen.PendingStep = 0; screen.RenderedPage = -1; } screen.HoldUntil = p.Now + screen.ManualHold; RestartSlide(screen); } } bool AutoPaging(Screen screen) => autoOverride >= 0 ? autoOverride == 1 : screen.AutoPage; void RestartSlide(Screen screen) { screen.Started = true; screen.AdvanceAt = Math.Max(p.Now, screen.HoldUntil) + screen.Interval; screen.Progress = 0; } double Progress(Screen screen) => Data.Clamp((p.Now - (screen.AdvanceAt - screen.Interval)) / screen.Interval, 0, 1); static int Wrap(int value, int count) => ((value % count) + count) % count; void UpdateSlide(Screen screen, int page, int count) { if (screen.RenderedPage != page) { screen.RenderedPage = page; screen.Slide = 0; RestartSlide(screen); } if (screen.Count != count) { screen.Slide = Math.Min(screen.Slide, count - 1); screen.Count = count; RestartSlide(screen); } if (screen.PendingStep != 0) { screen.Slide = Wrap(screen.Slide + screen.PendingStep, count); screen.PendingStep = 0; RestartSlide(screen); } if (!screen.Started) RestartSlide(screen); if (AutoPaging(screen) && count > 1 && p.Now >= screen.AdvanceAt) { screen.Slide = (screen.Slide + 1) % count; // A delayed draw advances once, then grants the new slide a full dwell. RestartSlide(screen); } } static int ParsePage(string value, int fallback) { switch (value.ToLowerInvariant()) { case "fleet": case "0": return 0; case "miner": case "1": return 1; case "map": case "2": return 2; case "diagnostics": case "3": return 3; case "config": case "4": return 4; default: return fallback; } } void Discover() { var previous = new Dictionary(); foreach (var screen in screens) previous[screen.Key] = screen; screens.Clear(); var defaultAuto = p.Config.Flag("Display", "AutoPage", true); var defaultInterval = p.Config.Number("Display", "PageInterval", 8, 2, 120); var defaultHold = p.Config.Number("Display", "ManualHoldSeconds", 20, 0, 300); var blocks = new List(); var groupName = p.Config.Text("Display", "Group", "AMS Screens"); var tag = p.Config.Text("Display", "Tag", "[AMS LCD]"); var group = p.GridTerminalSystem.GetBlockGroupWithName(groupName); if (group == null && (groupName == "AMS Screens" || groupName == Settings.DefaultName("AMS Screens"))) group = p.GridTerminalSystem.GetBlockGroupWithName(groupName == "AMS Screens" ? Settings.DefaultName("AMS Screens") : "AMS Screens"); if (group != null) group.GetBlocks(blocks); else p.GridTerminalSystem.GetBlocksOfType(blocks, b => b.CubeGrid == p.Me.CubeGrid && (b.CustomName.Contains(tag) || ((tag == "[AMS LCD]" || tag == Settings.DefaultName("[AMS LCD]")) && b.CustomName.Contains(tag == "[AMS LCD]" ? Settings.DefaultName("[AMS LCD]") : "[AMS LCD]")))); // A programming block's own main screen is available without naming or grouping. // Insert first so a full external-screen group cannot starve the local console. blocks.RemoveAll(b => b.EntityId == p.Me.EntityId); if (p.Config.Flag("Display", "BuiltIn", true)) blocks.Insert(0, p.Me); for (int n = 0; n < blocks.Count && screens.Count < 16; n++) { var b = blocks[n]; if (b.CubeGrid != p.Me.CubeGrid) continue; var builtIn = b.EntityId == p.Me.EntityId; var ini = new MyIni(); MyIniParseResult error; if (!ini.TryParse(b.CustomData, out error)) { p.Log(L.F(L.DisplayScreenIni, b.CustomName)); continue; } try { Settings.NormalizeIni(ini); } catch (ArgumentException e) { p.Log(L.F(L.DisplayScreenIni, b.CustomName) + " / " + e.Message); continue; } int count = 1; var provider = b as IMyTextSurfaceProvider; if (provider != null) count = provider.SurfaceCount; for (int index = 0; index < count && screens.Count < 16; index++) { var section = "AMS.Screen" + (index == 0 ? "" : "." + index); if (index > 0 && !ini.ContainsSection(section)) continue; try { if (!ScreenFlag(ini, section, "Enabled", true)) continue; } catch (ArgumentException e) { p.Log(L.F(L.DisplayScreenIni, b.CustomName) + " / " + e.Message); continue; } var surface = provider != null ? provider.GetSurface(index) : b as IMyTextSurface; if (surface == null) continue; int defaultPage = ParsePage(p.Config.Text("Display", "Page", p.Config.Role == "fleet" ? "fleet" : "miner"), p.Config.Role == "fleet" ? 0 : 1); if (builtIn) defaultPage = ParsePage(p.Config.Text("Display", "BuiltInPage", "config"), 4); var font = ini.Get(section, "Font").ToString(p.Config.Text("Display", "Font", "Debug")); var screen = new Screen { Surface = surface, Page = ParsePage(Data.Text(ini,section, "Page"), defaultPage), Miner = Data.Text(ini,section, "Miner"), Font = font, Key = b.EntityId + "/" + index }; try { screen.AutoPage = ScreenFlag(ini, section, "AutoPage", defaultAuto); screen.Interval = ScreenNumber(ini, section, "PageInterval", defaultInterval, 2, 120); screen.ManualHold = ScreenNumber(ini, section, "ManualHoldSeconds", defaultHold, 0, 300); } catch (ArgumentException e) { p.Log(L.F(L.DisplayScreenIni, b.CustomName) + " / " + e.Message); continue; } Screen old; if (previous.TryGetValue(screen.Key, out old) && old.Page == screen.Page && old.Miner == screen.Miner && old.AutoPage == screen.AutoPage && old.Interval == screen.Interval && old.ManualHold == screen.ManualHold) { old.Surface = surface; old.Font = font; screen = old; } screens.Add(screen); surface.ContentType = ContentType.SCRIPT; surface.Script = ""; surface.ScriptBackgroundColor = Background; } } discoveredAt = p.Now; } static bool ScreenFlag(MyIni ini, string section, string key, bool fallback) { if (!ini.ContainsKey(section, key)) return fallback; bool value; if (!bool.TryParse(Data.Text(ini,section, key), out value)) throw Data.Invalid(L.F(L.CommonConfigBool, Settings.SectionName(section), Settings.ParameterName(key))); return value; } static double ScreenNumber(MyIni ini, string section, string key, double fallback, double min, double max) { double value; try { value = Data.ReadNumber(ini, section, key, fallback); } catch (ArgumentException) { throw Data.Invalid(L.F(L.CommonNumberInvalid, Settings.SectionName(section), Settings.ParameterName(key))); } if (value < min || value > max) throw Data.Invalid(L.F(L.CommonConfigRange, Settings.SectionName(section), Settings.ParameterName(key), min, max)); return value; } public void Draw(RoleLogic role) { if (!p.HasBudget(0.55)) return; if (p.Now - discoveredAt >= 30) Discover(); if (screens.Count == 0) return; var fleet = role.GetTelemetry(); var jobs = role.GetJobs(); RefreshJobs(role); fleet.Sort((a, b) => string.CompareOrdinal(a.Id, b.Id)); int perTick = p.Config.Integer("Display", "ScreensPerTick", 2, 1, 16); for (int n = 0; n < Math.Min(perTick, screens.Count); n++) { if (!p.HasBudget(0.6)) return; if (nextScreen >= screens.Count) nextScreen = 0; var screen = screens[nextScreen++]; int page = pageOverride >= 0 ? pageOverride : screen.Page; using (var frame = screen.Surface.DrawFrame()) { var canvas = new Canvas(frame, screen.Surface, screen.Font, page == 4 || page == 2); int count = 1; if (page == 0) count = Math.Max(1, (fleet.Count + 6) / 7); else if (page == 1 && screen.Miner.Length == 0) { var own = false; foreach (var item in fleet) if (item.Id == p.Config.Id) { own = true; break; } if (!own) count = Math.Max(1, fleet.Count); } else if (page == 2) { count = Math.Max(1, mapRegions.Count); } else if (page == 4) { int perPage = canvas.Width >= 720 ? 5 : 4; count = Math.Max(1, (ConfigurationRows().Count + perPage - 1) / perPage); } UpdateSlide(screen, page, count); int slide = screen.Slide; if (page == 4) { Configuration(canvas, role, fleet, slide); Carousel(canvas, screen); continue; } canvas.Box(0, 0, canvas.Width, canvas.Height, Background); canvas.Text(L.Brand + " / " + Version, 22, 18, 0.8f, White); canvas.Text(p.Config.FleetId + " / " + p.Config.Id, 22, 48, 0.5f, Muted); canvas.Box(22, 76, canvas.Width-44, 2, Blue); if (page == 0) Fleet(canvas, fleet, jobs, slide); else if (page == 1) Miner(canvas, fleet, screen.Miner, slide); else if (page == 2) Map(canvas, fleet, slide); else Diagnostics(canvas, role); canvas.Box(22, canvas.Height-38, canvas.Width-44, 1, Panel); canvas.Text(new[] { L.DisplayFleet, L.DisplayMiner, L.DisplayMap, L.DisplayDiagnostics, L.DisplayConfiguration }[page], 22, canvas.Height-26, 0.45f, Blue); canvas.Text(L.F(L.DisplayFooter, F(p.Now, "0")), canvas.Width-22, canvas.Height-26, 0.42f, Muted, TextAlignment.RIGHT); Carousel(canvas, screen); } } } void Fleet(Canvas c, List fleet, List jobs, int listPage) { int online = 0, docked = 0, alerts = 0, waiting = 0; foreach (var t in fleet) { if (Fresh(t)) online++; if (t.Connected) docked++; if (!Fresh(t) || t.Reason.Length > 0 || t.State == FlightState.Fault) alerts++; if (t.State == FlightState.Holding) waiting++; } c.Text(L.F(L.DisplayOnlineCount, online, fleet.Count), 22, 92, 0.6f, Green); c.Text(L.F(L.DisplayDockQueue, docked, waiting), 245, 92, 0.55f, Blue); c.Text(L.F(L.DisplayJobAlerts, completedJobs, jobs.Count, alerts), 22, 119, 0.48f, alerts > 0 ? Amber : Muted); c.Text(L.DisplayMinerStatus, 22, 153, 0.42f, Muted); c.Text(L.DisplayBatteryShort, 297, 153, 0.42f, Muted); c.Text(L.DisplayHydrogenShort, 357, 153, 0.42f, Muted); c.Text(L.DisplayCargoShort, 422, 153, 0.42f, Muted); int pages = Math.Max(1, (fleet.Count + 6) / 7), start = (listPage % pages) * 7; for (int i = start; i < Math.Min(fleet.Count, start + 7); i++) { var t = fleet[i]; float y = 178 + (i - start) * 37; var fresh = Fresh(t); var color = !fresh || t.State == FlightState.Fault ? Red : t.Reason.Length > 0 ? Amber : White; c.Box(22, y - 5, 468, 33, Panel); c.Text(Short(Short(t.Id, 12) + " " + (fresh ? L.State(t.State) : L.F(L.DisplayOfflineAge, F(p.Now - t.ReceivedAt, "0"))), 35), 29, y, 0.42f, color); c.Text(Percent(t.Battery), 297, y, 0.42f, t.Battery < .25 ? Red : Green); c.Text(Percent(t.Hydrogen), 357, y, 0.42f, Blue); c.Text(Percent(t.Cargo), 422, y, 0.42f, t.Cargo > .9 ? Amber : White); } if (fleet.Count == 0) c.Text(L.DisplayWaitingMiners, 22, 194, 0.48f, Amber); c.Text(L.F(L.DisplayList, (listPage % pages) + 1, pages, FirstAlert(fleet)), 22, 448, 0.43f, alerts > 0 ? Amber : Muted); } void Miner(Canvas c, List fleet, string miner, int listPage) { Telemetry t = null; foreach (var item in fleet) if (item.Id == miner || (miner.Length == 0 && item.Id == p.Config.Id)) { t = item; break; } if (t == null && miner.Length == 0 && fleet.Count > 0) t = fleet[listPage % fleet.Count]; if (t == null) { c.Text(L.F(L.DisplayNoTelemetry, miner.Length > 0 ? miner : L.DisplayThisMiner), 22, 103, 0.6f, Amber); return; } c.Text(Short(t.Id, 22), 22, 94, 0.8f, White); c.Text(Fresh(t) ? L.State(t.State) : L.F(L.DisplayOfflineAge, F(p.Now - t.ReceivedAt, "0.0")), 22, 130, 0.56f, Fresh(t) ? Blue : Red); Bar(c, L.DisplayBattery, t.Battery, 174, t.Battery < .25 ? Red : Green); Bar(c, L.DisplayHydrogen, t.Hydrogen, 218, Blue); Bar(c, L.DisplayCargo, t.Cargo, 262, t.Cargo > .9 ? Amber : Blue); c.Text(L.F(L.DisplayPower, F(t.StoredMWh, "0.00"), F(t.CapacityMWh, "0.00"), F(t.NetMW, "+0.00;-0.00;0.00")), 22, 300, 0.43f, White); c.Text(L.F(L.DisplayMotion, Duration(t.EnduranceSeconds), F(t.Velocity.Length(), "0.0"), F(t.Gravity, "0.00")), 22, 325, 0.45f, White); c.Text(L.F(L.DisplayHomeBrake, F(t.DistanceHome, "0"), F(t.ThrustMargin, "0.0")), 22, 351, 0.45f, White); c.Text(L.F(L.DisplayDepth, F(t.Depth, "0.0"), F(t.TargetDepth, "0.0"), t.Connected ? L.DisplayConnected : L.DisplayFlying), 22, 377, 0.48f, White); c.Text(L.F(L.DisplayJob, Short(t.JobId, 36)), 22, 405, 0.43f, Muted); c.Text(Short(t.Reason.Length > 0 ? t.Reason : L.OreList(t.OreSummary), 55), 22, 435, 0.43f, t.Reason.Length > 0 ? Amber : Green); } #if !FLEET && !TESTS void Map(Canvas c, List fleet, int listPage) { c.Text(L.DisplayMiningPlane, 22, 93, 0.6f, White); if (mapRegions.Count == 0) { c.Text(buildingViews == null ? L.DisplayNoArea : L.CommonBudgetDeferred, 22, 140, 0.55f, Amber); return; } // Region paging keeps independently rotated mining areas legible. string region = mapRegions[listPage % mapRegions.Count]; RegionView view = mapViews[region]; Job first = view.First; Vector3D up = view.Up, right = view.Right; double extent = view.Extent; int total = view.Total, complete = view.Complete, sampled = view.Sampled, empty = view.Empty; c.Box(22, 129, 468, 295, Panel); c.Box(256, 139, 1, 275, Background); c.Box(32, 277, 448, 1, Background); float scale = (float)(132 / extent); foreach (var j in view.Dots) { if (!p.HasBudget(0.7)) break; var d = j.Entry - first.Entry; float x = 256 + (float)Data.Dot(d, right) * scale, y = 277 - (float)Data.Dot(d, up) * scale; Color color = Finished(j) ? (j.Outcome == "Blocked" || j.Outcome == "InvalidSample" ? Red : j.Kind == JobKind.Manual ? Green : Blue) : j.Owner.Length > 0 ? Amber : Muted; c.Dot(x, y, Math.Max(3, Math.Min(8, (float)j.Radius * scale)), color); } foreach (var t in fleet) { var d = t.Position - first.Entry; float x = 256 + (float)Data.Dot(d, right) * scale, y = 277 - (float)Data.Dot(d, up) * scale; if (x > 27 && x < 485 && y > 134 && y < 420) c.Text("+", x, y, 0.6f, Fresh(t) ? Blue : Red, TextAlignment.CENTER); } c.Text(Short(region, 20) + " " + complete + "/" + total + " +/- " + F(extent, "0") + "m", 22, 434, 0.42f, White); c.Text(L.F(L.DisplayMapLegend, sampled, empty), 22, 455, 0.36f, Muted); } #endif void Diagnostics(Canvas c, RoleLogic role) { c.Text(L.DisplaySystemDiagnostics, 22, 93, 0.64f, White); var load = (double)p.Runtime.CurrentInstructionCount / Math.Max(1, p.Runtime.MaxInstructionCount); Bar(c, L.DisplayInstructions, load, 144, load > .7 ? Amber : Blue); c.Text(L.F(L.DisplayCpuScreens, F(p.Runtime.LastRunTimeMs, "0.000"), screens.Count), 22, 179, 0.47f, Muted); var lines = (role.Diagnostics ?? "").Replace("\r", "").Split('\n'); float y = 210; foreach (var line in lines) { if (y > 310) break; c.Text(Short(line, 60), 22, y, 0.43f, White); y += 23; } c.Text(L.DisplayRecentEvents, 22, 329, 0.48f, Blue); var events = p.Events.ToArray(); for (int i = Math.Max(0, events.Length - 4); i < events.Length; i++) c.Text(Short(events[i], 61), 22, 358 + (i - Math.Max(0, events.Length - 4)) * 25, 0.4f, Muted); } void Configuration(Canvas c, RoleLogic role, List fleet, int listPage) { // The console is composed for the actual viewport, not a square inside it. // Preserve font aspect ratio and spend extra width on content rather than margins. float width = c.Width - 44, right = c.Width - 22; var wide = c.Width >= 720; c.Box(0, 0, c.Width, c.Height, ConsoleBackground); c.TextFit(L.Brand + " / " + Version, 22, 14, width * 0.59f, 0.95f, ConsoleValue); c.TextFit(L.DisplayConfiguration, right, 20, width * 0.38f, 0.78f, Blue, TextAlignment.RIGHT); c.TextFit(p.Config.Id + " / " + L.Language, 22, 59, width, 0.68f, ConsoleLabel); c.Box(22, 90, width, 2, Blue); var status = (role.Diagnostics ?? "").Replace("\r", "").Split('\n')[0]; if (status.Length == 0) status = L.DisplayUnknown; var statusColor = Amber; foreach (var t in fleet) if (t.Id == p.Config.Id) { status = Fresh(t) ? L.State(t.State) : L.DisplayOffline; if (t.Reason.Length > 0) status += " / " + t.Reason; statusColor = !Fresh(t) || t.State == FlightState.Fault ? Red : t.Reason.Length > 0 || t.State == FlightState.Paused ? Amber : Green; break; } c.TextFit(L.F(L.DisplayConfigStatus, status), 22, 102, width, 0.70f, statusColor); var rows = ConfigurationRows(); int perPage = wide ? 5 : 4; int pages = Math.Max(1, (rows.Count + perPage - 1) / perPage), start = (listPage % pages) * perPage; var rowHeight = (c.Height - 192) / perPage; for (int i = start; i < Math.Min(rows.Count, start + perPage); i++) { var value = rows[i].Value; if (rows[i].Key == L.DisplayConfigMass && p.Config.Number("Flight", "DepartureMass", 0, 0, 1e10) == 0) value = role.MeasuredDepartureMass > 0 ? L.F(L.DisplayAutoMass, F(role.MeasuredDepartureMass, "0")) : L.DisplayMeasuringMass; var y = 144 + (i - start) * rowHeight; c.Box(22, y, width, rowHeight - 4, ConsolePanel); if (wide) { // Independent columns keep long IDs and translated labels from overlapping. var labelY = y + Math.Max(3, (rowHeight - 4 - c.TextHeight(0.80f)) / 2); var valueY = y + Math.Max(3, (rowHeight - 4 - c.TextHeight(0.90f)) / 2); c.TextFit(rows[i].Key, 34, labelY, width * 0.46f - 24, 0.80f, ConsoleLabel); c.TextFit(value, right - 12, valueY, width * 0.54f - 24, 0.90f, ConsoleValue, TextAlignment.RIGHT); } else { // Stack on square/narrow screens instead of shrinking either text column. c.TextFit(rows[i].Key, 34, y + 3, width - 24, 0.80f, ConsoleLabel); c.TextFit(value, right - 12, y + rowHeight * 0.45f, width - 24, 0.90f, ConsoleValue, TextAlignment.RIGHT); } } c.TextFit(L.F(L.DisplayConfigPages, (listPage % pages) + 1, pages), 22, c.Height - 32, width, 0.65f, Blue); } List> ConfigurationRows() { if (configurationRows != null) return configurationRows; var rows = new List>(); rows.Add(new KeyValuePair(L.DisplayConfigId, p.Config.Id)); rows.Add(new KeyValuePair(L.DisplayConfigFleet, p.Config.FleetId)); rows.Add(new KeyValuePair(L.DisplayConfigBase, p.Config.BaseId)); #if MINER || TESTS if (p.Config.Role == "miner") { ConfigRow(rows, L.DisplayConfigMass, "Flight", "DepartureMass", "0", " kg"); ConfigRow(rows, L.DisplayConfigReturnBattery, "Energy", "ReturnBattery", "0.25"); ConfigRow(rows, L.DisplayConfigChargeTarget, "Energy", "ChargeTarget", "0.90"); ConfigRow(rows, L.DisplayConfigReturnCargo, "Cargo", "ReturnFill", "0.95"); rows.Add(new KeyValuePair(L.DisplayConfigDepartureCargo, Percent(p.Config.Number("Cargo", "DepartureFill", 0.05, 0, 0.99)))); ConfigRow(rows, L.DisplayConfigDrillSpeed, "Mining", "DrillSpeed", "0.5", " m/s"); ConfigRow(rows, L.DisplayConfigHoleRadius, "Mining", "HoleRadius", "3", " m"); ConfigRow(rows, L.DisplayConfigSortieDepth, "Mining", "SortieDepthBudget", "15", " m"); } #endif #if FLEET || MARKER || MINER || TESTS if (p.Config.Role == "fleet" || p.Config.Role == "marker" || p.Config.Role == "miner") { rows.Add(new KeyValuePair(L.DisplayConfigTargetOres, L.OreList(p.Config.Text("Mining", "TargetOres", "Iron,Nickel,Cobalt")))); ConfigRow(rows, L.DisplayConfigDepthLimit, "Survey", "MaxDepth", "100", " m"); } #endif #if FLEET || TESTS if (p.Config.Role == "fleet") { ConfigRow(rows, L.DisplayConfigMinerLimit, "Fleet", "MaxMiners", "16"); ConfigRow(rows, L.DisplayConfigDockGroup, "Fleet", "DockGroup", "AMS Docks"); ConfigRow(rows, L.DisplayConfigCargoGroup, "Fleet", "CargoGroup", "AMS Cargo"); } #endif #if FLEET || MINER || TESTS if (p.Config.Role == "fleet" || p.Config.Role == "miner") { ConfigRow(rows, L.DisplayConfigDockSpeed, "Dock", "MaxBaseSpeed", "5", " m/s"); ConfigRow(rows, L.DisplayConfigDockAge, "Dock", "MaxTelemetryAge", "0.5", " s"); } #endif ConfigRow(rows, L.DisplayConfigWatchdogTimeout, "Watchdog", "Timeout", "3", " s"); ConfigRow(rows, L.DisplayConfigFont, "Display", "Font", "Debug"); configurationRows = rows; return rows; } void ConfigRow(List> rows, string label, string section, string key, string fallback, string unit = "") { rows.Add(new KeyValuePair(label, Settings.ValueName(section, key, p.Config.Text(section, key, fallback)) + unit)); } void Carousel(Canvas c, Screen screen) { if (screen.Count <= 1) return; var playing = AutoPaging(screen); if (playing) screen.Progress = Progress(screen); var color = playing && p.Now >= screen.HoldUntil ? Blue : Muted; var width = c.Width - 44; c.Box(22, c.Height - 3, width, 2, Panel); c.Box(22, c.Height - 3, width * (float)screen.Progress, 2, color); // Bound sprite cost even for a fleet with many mining regions. int dots = Math.Min(10, screen.Count), active = screen.Slide * dots / screen.Count; var activeColor = playing && p.Now >= screen.HoldUntil ? Blue : ConsoleValue; for (int i = 0; i < dots; i++) c.Dot(c.Width / 2 + (i - (dots - 1) / 2f) * 10, c.Height - 7, 2, i == active ? activeColor : Muted); } static bool Finished(Job j) => j.Outcome == "Complete" || j.Outcome == "Blocked" || j.Outcome == "SurveyHit" || j.Outcome == "SurveyEmpty" || j.Outcome == "InvalidSample"; bool Fresh(Telemetry t) => p.Now - t.ReceivedAt <= p.Config.Number("Display", "OfflineSeconds", 5, 1, 60); string FirstAlert(List fleet) { foreach (var t in fleet) if (!Fresh(t)) return Short(t.Id + " " + L.DisplayOffline, 40); else if (t.Reason.Length > 0) return Short(t.Id + " " + t.Reason, 40); return L.DisplayNominal; } // CJK glyphs take approximately twice the width of Latin UI glyphs. static string Short(string s, int n) { s = (s ?? "").Replace('\n', ' ').Replace('\r', ' '); int width = 0; for (int i = 0; i < s.Length; i++) { width += s[i] >= 0x2e80 ? 2 : 1; if (width > n) return s.Substring(0, Math.Max(0, i - 1)) + "~"; } return s; } static string F(double n, string format) => Data.Finite(n) ? n.ToString(format, Data.Culture) : "--"; static string Percent(double n) => F(Data.Clamp(n, 0, 1) * 100, "0") + "%"; static string Duration(double seconds) => !Data.Finite(seconds) || seconds < 0 || seconds > 86400 ? "--" : L.F(L.DisplayMinutes, F(seconds / 60, "0")); static void Bar(Canvas c, string title, double value, float y, Color color) { c.Text(title, 22, y, 0.43f, Muted); c.Text(Percent(value), 490, y, 0.45f, color, TextAlignment.RIGHT); c.Box(22, y + 21, 468, 8, Panel); c.Box(22, y + 21, (float)Data.Clamp(value, 0, 1) * 468, 8, color); } class Canvas { readonly MySpriteDrawFrame frame; readonly Vector2 origin; readonly float scale; readonly string font; readonly IMyTextSurface surface; readonly StringBuilder measurement = new StringBuilder(); public readonly float Width, Height; public Canvas(MySpriteDrawFrame drawFrame, IMyTextSurface textSurface, string fontName, bool fullViewport) { font = fontName; surface = textSurface; frame = drawFrame; scale = Math.Max(1, Math.Min(surface.SurfaceSize.X, surface.SurfaceSize.Y)) / 512f; Width = fullViewport ? surface.SurfaceSize.X / scale : 512; Height = fullViewport ? surface.SurfaceSize.Y / scale : 512; origin = (surface.TextureSize - new Vector2(Width, Height) * scale) / 2; } public void Box(float x, float y, float width, float height, Color color) { if (width <= 0 || height <= 0) return; frame.Add(new MySprite(SpriteType.TEXTURE, "SquareSimple", origin + new Vector2(x + width / 2, y + height / 2) * scale, new Vector2(width, height) * scale, color)); } public void Dot(float x, float y, float radius, Color color) { frame.Add(new MySprite(SpriteType.TEXTURE, "Circle", origin + new Vector2(x, y) * scale, new Vector2(radius * 2) * scale, color)); } public void Line(Vector2 a, Vector2 b, Color color, float width = 1) { var d = b-a; frame.Add(new MySprite(SpriteType.TEXTURE,"SquareSimple",origin+(a+b)*.5f*scale,new Vector2(d.Length(),width)*scale,color,rotation:(float)Math.Atan2(d.Y,d.X))); } public void Text(string text, float x, float y, float size, Color color, TextAlignment alignment = TextAlignment.LEFT) { var sprite = MySprite.CreateText(text, font, color, size * scale, alignment); sprite.Position = origin + new Vector2(x, y) * scale; frame.Add(sprite); } Vector2 Measure(string text, float size) { measurement.Clear(); measurement.Append(text); return surface.MeasureStringInPixels(measurement, font, size * scale) / scale; } public float TextHeight(float size) => Measure("Ag", size).Y; public void TextFit(string text, float x, float y, float width, float size, Color color, TextAlignment alignment = TextAlignment.LEFT) { text = (text ?? "").Replace('\n', ' ').Replace('\r', ' '); if (width <= 0) return; if (Measure(text, size).X > width) { // Keep readable glyphs. Never shrink a long value into tiny text. const string ellipsis = "..."; if (Measure(ellipsis, size).X > width) return; int low = 0, high = text.Length; while (low < high) { int mid = (low + high + 1) / 2; if (Measure(text.Substring(0, mid) + ellipsis, size).X <= width) low = mid; else high = mid - 1; } if (low > 0 && char.IsHighSurrogate(text[low - 1])) low--; text = text.Substring(0, low) + ellipsis; } Text(text, x, y, size, color, alignment); } } } } }