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 class Dashboard
{
readonly Program p;
readonly List<Screen> screens = new List<Screen>();
int nextScreen, pageOverride = -1, autoOverride = -1;
List<KeyValuePair<string, string>> configurationRows;
sealed class RegionView
{
public Job First;
public Vector3D Up, Right;
public double Extent = 1;
public int Total, Complete, Sampled, Empty;
public readonly List<Job> Dots = new List<Job>();
}
Dictionary<string, RegionView> mapViews = new Dictionary<string, RegionView>(), buildingViews;
List<string> mapRegions = new List<string>(), buildingRegions;
List<Job> buildingSource;
int jobCursor, buildingDone, completedJobs;
double nextJobRefresh;
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 { get { return screens.Count; } }
public void ClearTransientCache() { configurationRows = null; }
public void Update(RoleLogic role)
{
if (p.HasBudget(0.45)) RefreshJobs(role.GetJobs());
}
void RefreshJobs(List<Job> jobs)
{
if (buildingViews == null && p.Now < nextJobRefresh && jobs == buildingSource) return;
if (buildingViews == null || jobs != buildingSource || jobCursor > jobs.Count)
{
buildingSource = jobs; buildingViews = new Dictionary<string, RegionView>(); buildingRegions = new List<string>(); 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 = Vector3D.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(Vector3D.Dot(d, view.Right)), Math.Abs(Vector3D.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;
}
public void Command(string command)
{
string[] words = command.Trim().ToLowerInvariant().Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
if (words.Length == 0 || words[0] != "page") return;
string 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;
}
bool 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) { return 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) { return Data.Clamp((p.Now - (screen.AdvanceAt - screen.Interval)) / screen.Interval, 0, 1); }
static int Wrap(int value, int count) { return ((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<string, Screen>();
foreach (var screen in screens) previous[screen.Key] = screen;
screens.Clear();
bool defaultAuto = p.Config.Flag("Display", "AutoPage", true);
double defaultInterval = p.Config.Number("Display", "PageInterval", 8, 2, 120);
double defaultHold = p.Config.Number("Display", "ManualHoldSeconds", 20, 0, 300);
var blocks = new List<IMyTerminalBlock>();
string groupName = p.Config.Text("Display", "Group", "AMS Screens");
string tag = p.Config.Text("Display", "Tag", "[AMS LCD]");
var group = p.GridTerminalSystem.GetBlockGroupWithName(groupName);
if (group != null) group.GetBlocks(blocks);
else p.GridTerminalSystem.GetBlocksOfType(blocks, b => b.CubeGrid == p.Me.CubeGrid && b.CustomName.Contains(tag));
// 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;
bool 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; }
int count = 1;
var provider = b as IMyTextSurfaceProvider;
if (provider != null) count = provider.SurfaceCount;
for (int index = 0; index < count && screens.Count < 16; index++)
{
string section = "AMS.Screen" + (index == 0 ? "" : "." + index);
if (index > 0 && !ini.ContainsSection(section)) continue;
if (!ini.Get(section, "Enabled").ToBoolean(true)) 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);
string font = ini.Get(section, "Font").ToString(p.Config.Text("Display", "Font", "Debug"));
var screen = new Screen { Surface = surface, Page = ParsePage(ini.Get(section, "Page").ToString(), defaultPage), Miner = ini.Get(section, "Miner").ToString(), 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(ini.Get(section, key).ToString(), out value)) throw new ArgumentException(L.F(L.CommonConfigBool, section, key));
return value;
}
static double ScreenNumber(MyIni ini, string section, string key, double fallback, double min, double max)
{
double value = Data.ReadNumber(ini, section, key, fallback);
if (value < min || value > max) throw new ArgumentException(L.F(L.CommonConfigRange, section, 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(jobs);
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);
int count = 1;
if (page == 0) count = Math.Max(1, (fleet.Count + 6) / 7);
else if (page == 1 && screen.Miner.Length == 0)
{
bool 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, 512, 512, 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, 468, 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, 474, 468, 1, Panel);
canvas.Text(new[] { L.DisplayFleet, L.DisplayMiner, L.DisplayMap, L.DisplayDiagnostics, L.DisplayConfiguration }[page], 22, 486, 0.45f, Blue);
canvas.Text(L.F(L.DisplayFooter, F(p.Now, "0")), 490, 486, 0.42f, Muted, TextAlignment.RIGHT);
Carousel(canvas, screen);
}
}
}
void Fleet(Canvas c, List<Telemetry> fleet, List<Job> 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;
bool fresh = Fresh(t); Color 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<Telemetry> 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);
}
void Map(Canvas c, List<Telemetry> 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)Vector3D.Dot(d, right) * scale, y = 277 - (float)Vector3D.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)Vector3D.Dot(d, right) * scale, y = 277 - (float)Vector3D.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);
}
void Diagnostics(Canvas c, RoleLogic role)
{
c.Text(L.DisplaySystemDiagnostics, 22, 93, 0.64f, White);
double 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);
string[] 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<Telemetry> 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;
bool 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(L.Role(p.Config.Role) + " / " + L.Language, 22, 59, width, 0.68f, ConsoleLabel);
c.Box(22, 90, width, 2, Blue);
string status = (role.Diagnostics ?? "").Replace("\r", "").Split('\n')[0];
if (status.Length == 0) status = L.DisplayUnknown;
Color 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;
float rowHeight = (c.Height - 192) / perPage;
for (int i = start; i < Math.Min(rows.Count, start + perPage); i++)
{
string 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;
float 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.
float labelY = y + Math.Max(3, (rowHeight - 4 - c.TextHeight(0.80f)) / 2);
float 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<KeyValuePair<string, string>> ConfigurationRows()
{
if (configurationRows != null) return configurationRows;
var rows = new List<KeyValuePair<string, string>>();
rows.Add(new KeyValuePair<string, string>(L.DisplayConfigId, p.Config.Id));
rows.Add(new KeyValuePair<string, string>(L.DisplayConfigFleet, p.Config.FleetId));
rows.Add(new KeyValuePair<string, string>(L.DisplayConfigBase, p.Config.BaseId));
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");
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");
}
if (p.Config.Role == "fleet" || p.Config.Role == "marker" || p.Config.Role == "miner")
{
rows.Add(new KeyValuePair<string, string>(L.DisplayConfigTargetOres, L.OreList(p.Config.Text("Mining", "TargetOres", "Iron,Nickel,Cobalt"))));
ConfigRow(rows, L.DisplayConfigDepthLimit, "Survey", "MaxDepth", "100", " m");
}
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");
}
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");
}
ConfigRow(rows, L.DisplayConfigWatchdogTimeout, "Watchdog", "Timeout", "3", " s");
ConfigRow(rows, L.DisplayConfigFont, "Display", "Font", "Debug");
configurationRows = rows; return rows;
}
void ConfigRow(List<KeyValuePair<string, string>> rows, string label, string section, string key, string fallback, string unit = "")
{
rows.Add(new KeyValuePair<string, string>(label, p.Config.Text(section, key, fallback) + unit));
}
void Carousel(Canvas c, Screen screen)
{
if (screen.Count <= 1) return;
bool playing = AutoPaging(screen);
if (playing) screen.Progress = Progress(screen);
Color color = playing && p.Now >= screen.HoldUntil ? Blue : Muted;
float 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;
Color 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) { return j.Outcome == "Complete" || j.Outcome == "Blocked" || j.Outcome == "SurveyHit" || j.Outcome == "SurveyEmpty" || j.Outcome == "InvalidSample"; }
bool Fresh(Telemetry t) { return p.Now - t.ReceivedAt <= p.Config.Number("Display", "OfflineSeconds", 5, 1, 60); }
string FirstAlert(List<Telemetry> 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) { return Data.Finite(n) ? n.ToString(format, Data.Culture) : "--"; }
static string Percent(double n) { return F(Data.Clamp(n, 0, 1) * 100, "0") + "%"; }
static string Duration(double seconds) { return !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 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) { return 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);
}
}
}
}
}
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 class Dashboard
{
readonly Program p;
readonly List<Screen> screens = new List<Screen>();
int nextScreen, pageOverride = -1, autoOverride = -1;
List<KeyValuePair<string, string>> configurationRows;
sealed class RegionView
{
public Job First;
public Vector3D Up, Right;
public double Extent = 1;
public int Total, Complete, Sampled, Empty;
public readonly List<Job> Dots = new List<Job>();
}
Dictionary<string, RegionView> mapViews = new Dictionary<string, RegionView>(), buildingViews;
List<string> mapRegions = new List<string>(), buildingRegions;
List<Job> buildingSource;
int jobCursor, buildingDone, completedJobs;
double nextJobRefresh;
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 { get { return screens.Count; } }
public void ClearTransientCache() { configurationRows = null; }
public void Update(RoleLogic role)
{
if (p.HasBudget(0.45)) RefreshJobs(role.GetJobs());
}
void RefreshJobs(List<Job> jobs)
{
if (buildingViews == null && p.Now < nextJobRefresh && jobs == buildingSource) return;
if (buildingViews == null || jobs != buildingSource || jobCursor > jobs.Count)
{
buildingSource = jobs; buildingViews = new Dictionary<string, RegionView>(); buildingRegions = new List<string>(); 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 = Vector3D.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(Vector3D.Dot(d, view.Right)), Math.Abs(Vector3D.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;
}
public void Command(string command)
{
string[] words = command.Trim().ToLowerInvariant().Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
if (words.Length == 0 || words[0] != "page") return;
string 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;
}
bool 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) { return 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) { return Data.Clamp((p.Now - (screen.AdvanceAt - screen.Interval)) / screen.Interval, 0, 1); }
static int Wrap(int value, int count) { return ((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<string, Screen>();
foreach (var screen in screens) previous[screen.Key] = screen;
screens.Clear();
bool defaultAuto = p.Config.Flag("Display", "AutoPage", true);
double defaultInterval = p.Config.Number("Display", "PageInterval", 8, 2, 120);
double defaultHold = p.Config.Number("Display", "ManualHoldSeconds", 20, 0, 300);
var blocks = new List<IMyTerminalBlock>();
string groupName = p.Config.Text("Display", "Group", "AMS Screens");
string tag = p.Config.Text("Display", "Tag", "[AMS LCD]");
var group = p.GridTerminalSystem.GetBlockGroupWithName(groupName);
if (group != null) group.GetBlocks(blocks);
else p.GridTerminalSystem.GetBlocksOfType(blocks, b => b.CubeGrid == p.Me.CubeGrid && b.CustomName.Contains(tag));
// 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;
bool 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; }
int count = 1;
var provider = b as IMyTextSurfaceProvider;
if (provider != null) count = provider.SurfaceCount;
for (int index = 0; index < count && screens.Count < 16; index++)
{
string section = "AMS.Screen" + (index == 0 ? "" : "." + index);
if (index > 0 && !ini.ContainsSection(section)) continue;
if (!ini.Get(section, "Enabled").ToBoolean(true)) 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);
string font = ini.Get(section, "Font").ToString(p.Config.Text("Display", "Font", "Debug"));
var screen = new Screen { Surface = surface, Page = ParsePage(ini.Get(section, "Page").ToString(), defaultPage), Miner = ini.Get(section, "Miner").ToString(), 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(ini.Get(section, key).ToString(), out value)) throw new ArgumentException(L.F(L.CommonConfigBool, section, key));
return value;
}
static double ScreenNumber(MyIni ini, string section, string key, double fallback, double min, double max)
{
double value = Data.ReadNumber(ini, section, key, fallback);
if (value < min || value > max) throw new ArgumentException(L.F(L.CommonConfigRange, section, 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(jobs);
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);
int count = 1;
if (page == 0) count = Math.Max(1, (fleet.Count + 6) / 7);
else if (page == 1 && screen.Miner.Length == 0)
{
bool 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, 512, 512, 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, 468, 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, 474, 468, 1, Panel);
canvas.Text(new[] { L.DisplayFleet, L.DisplayMiner, L.DisplayMap, L.DisplayDiagnostics, L.DisplayConfiguration }[page], 22, 486, 0.45f, Blue);
canvas.Text(L.F(L.DisplayFooter, F(p.Now, "0")), 490, 486, 0.42f, Muted, TextAlignment.RIGHT);
Carousel(canvas, screen);
}
}
}
void Fleet(Canvas c, List<Telemetry> fleet, List<Job> 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;
bool fresh = Fresh(t); Color 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<Telemetry> 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);
}
void Map(Canvas c, List<Telemetry> 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)Vector3D.Dot(d, right) * scale, y = 277 - (float)Vector3D.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)Vector3D.Dot(d, right) * scale, y = 277 - (float)Vector3D.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);
}
void Diagnostics(Canvas c, RoleLogic role)
{
c.Text(L.DisplaySystemDiagnostics, 22, 93, 0.64f, White);
double 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);
string[] 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<Telemetry> 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;
bool 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(L.Role(p.Config.Role) + " / " + L.Language, 22, 59, width, 0.68f, ConsoleLabel);
c.Box(22, 90, width, 2, Blue);
string status = (role.Diagnostics ?? "").Replace("\r", "").Split('\n')[0];
if (status.Length == 0) status = L.DisplayUnknown;
Color 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;
float rowHeight = (c.Height - 192) / perPage;
for (int i = start; i < Math.Min(rows.Count, start + perPage); i++)
{
string 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;
float 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.
float labelY = y + Math.Max(3, (rowHeight - 4 - c.TextHeight(0.80f)) / 2);
float 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<KeyValuePair<string, string>> ConfigurationRows()
{
if (configurationRows != null) return configurationRows;
var rows = new List<KeyValuePair<string, string>>();
rows.Add(new KeyValuePair<string, string>(L.DisplayConfigId, p.Config.Id));
rows.Add(new KeyValuePair<string, string>(L.DisplayConfigFleet, p.Config.FleetId));
rows.Add(new KeyValuePair<string, string>(L.DisplayConfigBase, p.Config.BaseId));
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");
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");
}
if (p.Config.Role == "fleet" || p.Config.Role == "marker" || p.Config.Role == "miner")
{
rows.Add(new KeyValuePair<string, string>(L.DisplayConfigTargetOres, L.OreList(p.Config.Text("Mining", "TargetOres", "Iron,Nickel,Cobalt"))));
ConfigRow(rows, L.DisplayConfigDepthLimit, "Survey", "MaxDepth", "100", " m");
}
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");
}
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");
}
ConfigRow(rows, L.DisplayConfigWatchdogTimeout, "Watchdog", "Timeout", "3", " s");
ConfigRow(rows, L.DisplayConfigFont, "Display", "Font", "Debug");
configurationRows = rows; return rows;
}
void ConfigRow(List<KeyValuePair<string, string>> rows, string label, string section, string key, string fallback, string unit = "")
{
rows.Add(new KeyValuePair<string, string>(label, p.Config.Text(section, key, fallback) + unit));
}
void Carousel(Canvas c, Screen screen)
{
if (screen.Count <= 1) return;
bool playing = AutoPaging(screen);
if (playing) screen.Progress = Progress(screen);
Color color = playing && p.Now >= screen.HoldUntil ? Blue : Muted;
float 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;
Color 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) { return j.Outcome == "Complete" || j.Outcome == "Blocked" || j.Outcome == "SurveyHit" || j.Outcome == "SurveyEmpty" || j.Outcome == "InvalidSample"; }
bool Fresh(Telemetry t) { return p.Now - t.ReceivedAt <= p.Config.Number("Display", "OfflineSeconds", 5, 1, 60); }
string FirstAlert(List<Telemetry> 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) { return Data.Finite(n) ? n.ToString(format, Data.Culture) : "--"; }
static string Percent(double n) { return F(Data.Clamp(n, 0, 1) * 100, "0") + "%"; }
static string Duration(double seconds) { return !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 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) { return 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);
}
}
}
}
}