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<string,string> Read = new Dictionary<string,string>(StringComparer.OrdinalIgnoreCase);
readonly Dictionary<string,string> write = new Dictionary<string,string>(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<MyIniKey>(); source.GetKeys(original);
var normalized = Data.CreateDictionary<MyIniKey,string>(); var origins = Data.CreateDictionary<MyIniKey,MyIniKey>();
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");
}
}
}
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<string,string> Read = new Dictionary<string,string>(StringComparer.OrdinalIgnoreCase);
readonly Dictionary<string,string> write = new Dictionary<string,string>(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<MyIniKey>(); source.GetKeys(original);
var normalized = Data.CreateDictionary<MyIniKey,string>(); var origins = Data.CreateDictionary<MyIniKey,MyIniKey>();
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");
}
}
}