using System; using System.Collections.Generic; using System.Text; using Sandbox.ModAPI.Ingame; using VRage.Game.ModAPI.Ingame.Utilities; namespace AutoMiningScript { public partial class Program : MyGridProgram { public class Settings { readonly MyIni ini; public readonly string Id, FleetId, BaseId, Role; public string BlockName => Role=="fleet"?L.CommonFleetBlockName:L.F(L.CommonNodeBlockName,Id); public Settings(string source, string defaultRole, string defaultId) { ini = new MyIni(); MyIniParseResult result; if (!string.IsNullOrWhiteSpace(source) && !ini.TryParse(source, out result)) throw Data.Invalid(L.F(L.CommonCustomDataInvalid, result.ToString())); NormalizeIni(ini); Role = defaultRole; Id = Text("System", "Id", defaultId); FleetId = Text("System", "FleetId", "mining"); BaseId = Text("System", "BaseId", "base"); if (Id.Length == 0 || FleetId.Length == 0 || BaseId.Length == 0 || Id.Length > 64 || FleetId.Length > 64) throw Data.Invalid(L.CommonIdLength); if (Id.IndexOf('\n') >= 0 || FleetId.IndexOf('\n') >= 0) throw Data.Invalid(L.CommonIdNewline); } public string Text(string section, string key, string fallback) => ini.ContainsKey(section, key) ? Data.Text(ini,section, key).Trim() : fallback; public double Number(string section, string key, double fallback, double min, double max) => ReadNumber(ini,section,key,fallback,min,max); public static double ReadNumber(MyIni ini,string section,string key,double fallback,double min,double max) { var value = fallback; if (ini.ContainsKey(section, key) && !Data.TryNumber(Data.Text(ini,section, key), out value)) throw Data.Invalid(L.F(L.CommonNumberInvalid, SectionName(section), ParameterName(key))); CheckRange(section, key, value, min, max); return value; } public int Integer(string section, string key, int fallback, int min, int max) { long value = fallback; if (ini.ContainsKey(section, key) && !Data.TryLong(Data.Text(ini,section, key), out value)) throw Data.Invalid(L.F(L.CommonIntegerInvalid, SectionName(section), ParameterName(key))); CheckRange(section, key, value, min, max); return (int)value; } static void CheckRange(string section,string key,double value,double min,double max) { if (value < min || value > max) throw Data.Invalid(L.F(L.CommonConfigRange, SectionName(section), ParameterName(key), min, max)); } public bool Flag(string section, string key, bool fallback) => ReadFlag(ini,section,key,fallback); public static bool ReadFlag(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, SectionName(section), ParameterName(key))); return value; } // Only the user-facing boundary is translated. Protocol and save identifiers // never pass through these maps; arbitrary IDs and block names stay verbatim. sealed class Aliases { public readonly Dictionary Read = new Dictionary(StringComparer.OrdinalIgnoreCase); readonly Dictionary write = new Dictionary(StringComparer.OrdinalIgnoreCase); public Aliases(string source) { foreach (var row in source.Split('|')) { var pair = row.Split('='); string name = pair[0], local = pair.Length > 1 ? pair[1] : name; Read[name] = name; Read[local] = name; if (!write.ContainsKey(name)) write[name] = local; } } public string Code(string value) { string result; return Read.TryGetValue(value, out result) ? result : value; } public string Local(string value) { string result; return write.TryGetValue(value, out result) ? result : value; } } static readonly Aliases sections = new Aliases(L.ParamSections), keys = new Aliases(L.ParamKeys), pages = new Aliases(L.ParamPages), fonts = new Aliases(L.ParamFonts), ores = new Aliases(L.ParamOres), flags = new Aliases(L.ParamFlags), commands = new Aliases(L.ParamCommands), pageCommands = new Aliases(L.ParamPageCommands), names = new Aliases(L.ParamDefaultNames); public static string DefaultFleetId => names.Local("mining"); public static string DefaultBaseId => names.Local("base"); public static string DefaultId(string role, long entityId) => role == "fleet" ? DefaultBaseId : names.Local(role) + "-" + entityId; public static string DefaultName(string value) => names.Local(value); public static string ParameterName(string key) => keys.Local(key); public static string SectionName(string section) => Section(section, true); static string Section(string section, bool local) { var value = local ? sections.Local(section) : sections.Code(section); if (value != section) return value; int dot = section.LastIndexOf('.'), index; if (dot > 0 && int.TryParse(section.Substring(dot + 1), out index) && index >= 0) { var prefix = section.Substring(0, dot); if (sections.Code(prefix) == "AMS.Screen") return (local ? sections.Local("AMS.Screen") : "AMS.Screen") + section.Substring(dot); } return value; } static Aliases Values(string section, string key) { if ((section == "Display" || section == "AMS.Screen" || section.StartsWith("AMS.Screen.", Data.Ordinal)) && (key == "Page" || key == "BuiltInPage")) return pages; if (key == "Font") return fonts; if (key == "Enabled" || key == "BuiltIn" || key == "AutoPage" || key == "UsesHydrogen" || key == "AutoCalibrate") return flags; if ((key == "TargetOres" || key == "PriorityOres") && section == "Mining") return ores; return null; } static string Value(string section, string key, string value, bool local) { var map = Values(section, key); if (map == null) return value; if (map != ores) return local ? map.Local(value) : map.Code(value); var items = value.Split(','); for (int n = 0; n < items.Length; n++) items[n] = local ? map.Local(items[n].Trim()) : map.Code(items[n].Trim()); return string.Join(",", items); } public static string ValueName(string section, string key, string value) => Value(section, key, value, true); public static void NormalizeIni(MyIni source) { var original = Data.CreateList(); source.GetKeys(original); var normalized = Data.CreateDictionary(); var origins = Data.CreateDictionary(); foreach (var field in original) { var section = Section(field.Section, false); if (!sections.Read.ContainsKey(section) && !section.StartsWith("AMS.Screen.", Data.Ordinal)) continue; // Old role entries cannot override the compiled script or conflict with aliases. if (section == "System" && (field.Name.Equals("Role", Data.IgnoreCase) || field.Name == L.ParamLegacyRoleKey)) continue; string key = keys.Code(field.Name), value = Value(section, key, source.Get(field).ToString().Trim(), false); var target = new MyIniKey(section, key); string previous; if (normalized.TryGetValue(target, out previous) && previous != value) throw Data.Invalid(L.F(L.ParamConflict, origins[target].Section, origins[target].Name, field.Section, field.Name)); normalized[target] = value; origins[target] = field; } // Check the whole input before modifying anything, including screen INI. foreach (var field in original) { var section = Section(field.Section, false); if (sections.Read.ContainsKey(section) || section.StartsWith("AMS.Screen.", Data.Ordinal)) source.Delete(field); } foreach (var pair in normalized) source.Set(pair.Key, pair.Value); } public static string NormalizeCommand(string command) { if (string.IsNullOrWhiteSpace(command)) return ""; command = command.Trim(); int space = 0; while (space < command.Length && !char.IsWhiteSpace(command[space])) space++; var verb = commands.Code(command.Substring(0, space)).ToLowerInvariant(); var tail = command.Substring(space).Trim(); if (verb == "page") tail = pageCommands.Code(pages.Code(tail)); else if (tail == L.ParamAll) tail = "all"; return verb + (tail.Length > 0 ? " " + tail : ""); } static string LocalTemplate(string source) { var result = new StringBuilder(); var section = ""; foreach (var line in source.Split('\n')) { if (line.StartsWith("[", Data.Ordinal) && line.EndsWith("]", Data.Ordinal)) { section = line.Substring(1, line.Length - 2); result.Append("[").Append(SectionName(section)).Append("]"); } else { int equal = line.IndexOf('='); if (equal > 0 && !line.StartsWith(";", Data.Ordinal)) { var key = line.Substring(0, equal); result.Append(ParameterName(key)).Append('=').Append(ValueName(section, key, line.Substring(equal + 1))); } else result.Append(line); } result.Append('\n'); } return result.ToString(); } public static string Template(string role, string id) => LocalTemplate("; " + L.CommonTemplateIntro + "\n[System]\nId=" + id + "\nFleetId=" + DefaultFleetId + "\nBaseId=" + DefaultBaseId + "\n\n[Hardware]\nAutoCalibrate=true\n\n[Flight]\n; " + L.CommonTemplateMass + "\nDepartureMass=0\n\n[Mining]\n; " + L.CommonTemplateOres + "\nTargetOres=\n; " + L.CommonTemplatePriority + "\nPriorityOres=Iron,Cobalt,Nickel,Silicon\nFootprintWidth=6\nFootprintHeight=6\nOverlap=0.15\nHoleRadius=3\nDrillSpeed=0.5\nSortieDepthBudget=15\nMaxEntryRelief=5\nApproachDistance=20\n\n[Survey]\nBaseClearance=80\nSpacing=60\nExpansionWidth=60\nExpansionHeight=60\nMaxDepth=100\nMaxJobs=2048\n\n[Energy]\nReturnBattery=0.25\nChargeTarget=0.90\n\n[Cargo]\nReturnFill=0.95\nDepartureFill=0.05\n\n[Dock]\nMaxBaseSpeed=5\nMaxBaseAngularDeg=0.2\nMaxBaseAcceleration=0.1\nMaxTelemetryAge=0.5\nApproachDistance=30\nFinalSpeed=0.5\n\n[Fleet]\nDockGroup=" + DefaultName("AMS Docks") + "\nCargoGroup=" + DefaultName("AMS Cargo") + "\nMaxMiners=16\n\n[Display]\nTag=" + DefaultName("[AMS LCD]") + "\nPage=fleet\nBuiltIn=true\nBuiltInPage=config\nFont=Debug\nOfflineSeconds=5\nAutoPage=true\nPageInterval=8\nManualHoldSeconds=20\n\n[Watchdog]\nEnabled=false\nTimeout=3\n"); } } }