Last active
April 21, 2018 02:16
-
-
Save LexManos/7c4746b17a39f38d9ab8a682bb031e9e to your computer and use it in GitHub Desktop.
SpaceEngineersSorting
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//Basic Readme: | |
//Name your inventories with <ItemName!Priority#Count,ItemName2!Priority2#Count2> | |
//Name is the name of the item you want, leave it empty for all items, categories also work: ingot, ore, component, or whatever other categories Space Engineers has. | |
//Priority is optional, defaults to 0, higher priorities will get fed first. | |
//Count is optional, this will be the number of this item in the inventory for it to be 'satisfied'. And able to move the extra to lower priority inventories. | |
//I've included defaults for a few things, like refinearies, assemblers, o2 gens, gatling guns, etc.. | |
//But you can always override them by manually specfying in the name. | |
//This is a major work in progress and i'll be tweaking it as I play. But it works for me... so have fun, no support/warranty/etc.. | |
// Set this to true, and the debug log from each tick will be set to the programming block's Custom Data field. | |
private static readonly bool DEBUG = false; | |
// Frequency in 100 tick increments when to rescan the structure for inventories to manipulate. | |
private int RebuildFrequency { get; } = 100; | |
// Max number of intentories to process per update. To prevent 'to complex' issues. | |
private static readonly int MaxUpdates = 10; | |
//========================================================================================================== | |
private int Rebuild { get; set; } | |
private static Program Instance { get; set; } | |
private static Dictionary<string, string> Categories { get; } = new Dictionary<string, string>(); | |
private Dictionary<string, List<InventoryWrapper>> InventoryPriorities { get; } = | |
new Dictionary<string, List<InventoryWrapper>>(); | |
private List<InventoryWrapper> Inventories { get; } = new List<InventoryWrapper>(); | |
private StringBuilder _debugBuffer = new StringBuilder(); | |
private int _lastIndex = 0; | |
public Program() | |
{ | |
Runtime.UpdateFrequency = UpdateFrequency.Update10; | |
Categories.Clear(); | |
//AddCategory("Tools", | |
// "WelderItem", "HandDrillItem", "AngleGrinderItem", | |
// "AutomaticRifleItem"); | |
//AddCategory("Ammo", | |
// "Missile200mm", "NATO_25x184mm", "NATO_5p56x45mm", "Explosives"); | |
Instance = this; | |
} | |
private void AddCategory(String cat, params string[] names) | |
{ | |
foreach (var name in names) | |
{ | |
Categories.Add(name.ToLower(), cat.ToLower()); | |
} | |
} | |
public void Main(string argument, UpdateType updateSource) | |
{ | |
if (DEBUG) | |
{ | |
_debugBuffer = new StringBuilder(); | |
Echo("-------------------------------------------------------------------------------------------------------"); | |
} | |
if (Rebuild-- == 0) | |
{ | |
BuildInventories(); | |
Rebuild = RebuildFrequency; | |
} | |
else | |
{ | |
foreach (var inv in Inventories) | |
{ | |
inv.ClearCache(); //Reset counts incase other systems moved stuff between ticks. | |
} | |
if (_lastIndex >= Inventories.Count) | |
{ | |
_lastIndex = 0; | |
} | |
int processed = 0; | |
for (; _lastIndex < Inventories.Count; _lastIndex++) | |
{ | |
if (processed++ == MaxUpdates) | |
{ | |
break; | |
} | |
SortInventory(Inventories[_lastIndex]); | |
} | |
} | |
if (DEBUG) | |
{ | |
Me.CustomData = _debugBuffer.ToString(); | |
} | |
} | |
public void Echo(string line) | |
{ | |
_debugBuffer?.Append(line).Append('\n'); | |
} | |
private void BuildInventories() | |
{ | |
var blocks = new List<IMyTerminalBlock>(); | |
GridTerminalSystem.GetBlocksOfType<IMyEntity>(blocks); | |
Inventories.Clear(); | |
_lastIndex = 0; | |
InventoryPriorities.Clear(); | |
foreach (var block in blocks) | |
{ | |
if (!block.HasInventory || block.InventoryCount <= 0) | |
{ | |
continue; | |
} | |
var _default = | |
block is IMyRefinery ? DefaultRefinery : | |
block is IMyReactor ? DefaultReactor : | |
block is IMyAssembler ? DefaultAssembler : | |
block is IMyCargoContainer ? DefaultCargo : | |
block is IMyGasGenerator ? DefaultO2Gen : | |
block is IMyLargeGatlingTurret ? DefaultGatlingTurret : | |
DefaultSortArr; | |
var inv = new InventoryWrapper(block.GetInventory(0), block.CustomName, false, _default); | |
AddInventory(inv); | |
if (block is IMyProductionBlock) | |
{ | |
AddInventory(new InventoryWrapper(((IMyProductionBlock) block).OutputInventory, inv.BlockName + " Output", true, DefaultSortArr)); | |
} | |
} | |
foreach (var ent in InventoryPriorities) | |
{ | |
//Sort it based on priority, higher numbers at top, and anything with a count before non-counted | |
ent.Value.Sort(Comparer<InventoryWrapper>.Create((o1, o2) => | |
{ | |
var p1 = o1.Predicates.GetValueOrDefault(ent.Key, DefaultSort); | |
var p2 = o2.Predicates.GetValueOrDefault(ent.Key, DefaultSort); | |
var ret = p2.Priority - p1.Priority; | |
if (ret == 0) | |
{ | |
ret = p1.Count == p2.Count ? 0 : p1.Count == 0 ? 1 : p2.Count == 0 ? -1 : 0; | |
} | |
if (ret == 0) | |
{ | |
ret = String.CompareOrdinal(o1.BlockName, o2.BlockName); | |
} | |
return ret; | |
})); | |
if (DEBUG) | |
{ | |
Echo(ent.Key + ":"); | |
foreach (var w in ent.Value) | |
{ | |
Echo(" " + w.BlockName + " " + w.Predicates.GetValueOrDefault(ent.Key)); | |
} | |
} | |
} | |
} | |
private void AddInventory(InventoryWrapper wrapper) | |
{ | |
Inventories.Add(wrapper); | |
if (wrapper.ExtractOnly) | |
{ | |
return; | |
} | |
foreach (var pred in wrapper.Predicates) | |
{ | |
var lst = InventoryPriorities.GetValueOrDefault(pred.Key); | |
if (lst == null) | |
{ | |
lst = new List<InventoryWrapper>(); | |
InventoryPriorities[pred.Key] = lst; | |
} | |
lst.Add(wrapper); | |
} | |
} | |
//TODO: Sort from lower priority slots, and less strict slots to the more strict ones. | |
// Iron Ingot x100 in <Ingot> with another Box <Iron Ingot> with space. | |
// Iron Ingot x100 in <Ingot> with another Box <Ingot!1> with space. | |
private void SortInventory(InventoryWrapper inv) | |
{ | |
var items = inv.Inventory.GetItems(); | |
for (var x = items.Count - 1; x >= 0; x--) | |
{ | |
var item = items[x]; | |
var extra = inv.GetExtra(item); | |
if (extra <= 0) | |
{ | |
continue; | |
} | |
if (DEBUG) | |
{ | |
Echo(inv.BlockName + ": Item: " + GetName(item) + " Extra: " + extra); | |
} | |
var foundSelf = false; | |
for (var i = 0; i < 3; i++) | |
{ | |
if (extra <= 0 || foundSelf) | |
{ | |
break; | |
} | |
var key = i == 0 ? GetName(item) : | |
i == 1 ? GetCategory(item) : | |
DefaultSort.Desc; | |
var lst = InventoryPriorities.GetValueOrDefault(key); | |
if (lst == null) | |
{ | |
continue; | |
} | |
if (DEBUG) | |
{ | |
Echo(" " + key + ":"); | |
} | |
var minPriority = int.MinValue; | |
foreach (var other in lst) | |
{ | |
if (DEBUG) | |
{ | |
Echo(" " + other.BlockName + ": " + other.Predicates[key].Priority); | |
} | |
if (other == inv) | |
{ | |
minPriority = inv.Predicates[key].Priority; //If we were found in this list, then go no further then our sibling priorities. | |
foundSelf = true; | |
continue; | |
} | |
if (minPriority > other.Predicates[key].Priority) | |
{ | |
break; | |
} | |
if (!inv.Inventory.IsConnectedTo(other.Inventory)) | |
{ | |
continue; | |
} | |
var needed = other.GetNeeded(key); | |
if (needed > 0) | |
{ | |
if (DEBUG) | |
{ | |
Echo(" Moving: " + key + " " + inv.BlockName + " -> " + other.BlockName + " " + needed); | |
} | |
if (inv.Inventory.TransferItemTo(other.Inventory, x, null, true, | |
extra > needed ? needed : extra)) | |
{ | |
inv.ClearCache(item); | |
other.ClearCache(item); | |
var oldLen = items.Count; | |
items = inv.Inventory.GetItems(); | |
if (oldLen != items.Count) //We removed the stack so kick out. | |
{ | |
extra = 0; | |
break; | |
} | |
item = items[x]; | |
extra = inv.GetExtra(item); | |
if (DEBUG) | |
{ | |
Echo(" Extra: " + extra); | |
} | |
} | |
} | |
if (extra <= 0) | |
{ | |
break; | |
} | |
} | |
} | |
} | |
} | |
private static readonly Dictionary<string, string> NameCache = new Dictionary<string, string>(); | |
private static string GetName(IMyInventoryItem item) | |
{ | |
var content = item.Content; | |
var key = content.TypeId + " " + content.SubtypeName; | |
var ret = NameCache.GetValueOrDefault(key); | |
if (ret == null) | |
{ | |
var category = GetCategory(item); | |
ret = item.Content.SubtypeName; | |
if (category == "Ore" || category == "Ingot") | |
{ | |
ret += " " + category; | |
} | |
//These Items don't quite match the in-game names so lets fix them. | |
if (ret == "Stone Ingot") | |
{ | |
ret = "Gravel"; | |
} | |
else if (ret == "Silicon Ingot") | |
{ | |
ret = "Silicon Wafer"; | |
} | |
else if (ret == "Magnesium Ingot") | |
{ | |
ret = "Magnesium Powder"; | |
} | |
else if (ret == "Scrap Ore") | |
{ | |
ret = "Scrap Metal"; | |
} | |
else if (ret == "Ice Ore") | |
{ | |
ret = "Ice"; | |
} | |
NameCache[key] = ret.ToLower(); | |
} | |
return ret; | |
} | |
private static readonly string BuilderString = "MyObjectBuilder_"; | |
private static readonly Dictionary<string, string> CatCache = new Dictionary<string, string>(); | |
private static string GetCategory(IMyInventoryItem item) | |
{ | |
var content = item.Content; | |
var key = content.TypeId + " " + content.SubtypeName; | |
var ret = CatCache.GetValueOrDefault(key); | |
if (ret == null) | |
{ | |
var type = content.TypeId.ToString(); | |
if (type.StartsWith(BuilderString)) | |
{ | |
ret = type.Substring(BuilderString.Length); | |
} | |
else if (Categories.ContainsKey(content.SubtypeName)) | |
{ | |
ret = Categories[item.Content.SubtypeName]; | |
} | |
else | |
{ | |
if (DEBUG) | |
{ | |
Instance.Echo("Unknown Category: " + item.Content.SubtypeName + " " + content.TypeId); | |
} | |
ret = type; | |
} | |
CatCache[key] = ret.ToLower(); | |
} | |
return ret; | |
} | |
private class InventoryWrapper | |
{ | |
public readonly Dictionary<string, ItemPredicate> Predicates = new Dictionary<string, ItemPredicate>(); | |
public readonly IMyInventory Inventory; | |
public readonly bool ExtractOnly; | |
public readonly string BlockName; | |
private readonly Dictionary<string, VRage.MyFixedPoint> _counts = new Dictionary<string, VRage.MyFixedPoint>(); | |
public InventoryWrapper(IMyInventory inv, String name, bool extractOnly = false, ItemPredicate[] defaults = null) | |
{ | |
BlockName = name; | |
if (!extractOnly) | |
{ | |
var start = name.IndexOf('<'); | |
var end = start == -1 ? -1 : name.IndexOf('>', start + 1); | |
if (start != -1 && end != -1 && start < end) | |
{ | |
foreach (var part in name.Substring(start + 1, end - start - 1).Split(',')) | |
{ | |
var pred = new ItemPredicate(part.Trim()); | |
Predicates[pred.Desc] = pred; | |
} | |
} | |
} | |
if (Predicates.Count == 0) | |
{ | |
if (defaults != null) | |
{ | |
foreach (var def in defaults) | |
{ | |
if (def != null) | |
{ | |
Predicates[def.Desc] = def; | |
} | |
} | |
} | |
} | |
Inventory = inv; | |
ExtractOnly = extractOnly; | |
} | |
public VRage.MyFixedPoint GetExtra(IMyInventoryItem item) | |
{ | |
if (Predicates.Count == 0) | |
{ | |
return item.Amount; | |
} | |
var pred = GetPredicate(item); | |
if (pred == null) | |
{ | |
return item.Amount; | |
} | |
return pred.Count == 0 ? item.Amount : GetCount(pred.Desc) - pred.Count; | |
} | |
private ItemPredicate GetPredicate(IMyInventoryItem item) | |
{ | |
var pred = Predicates.GetValueOrDefault(GetName(item)); | |
return pred == null ? Predicates.GetValueOrDefault(GetCategory(item)) : pred; | |
} | |
public void ClearCache(IMyInventoryItem item = null) | |
{ | |
if (item == null) | |
{ | |
_counts.Clear(); | |
} | |
else | |
{ | |
_counts.Remove(GetName(item)); | |
_counts.Remove(GetCategory(item)); | |
} | |
} | |
private VRage.MyFixedPoint GetCount(string key) | |
{ | |
if (_counts.ContainsKey(key)) | |
{ | |
return _counts[key]; | |
} | |
VRage.MyFixedPoint count = 0; | |
foreach (var item in Inventory.GetItems()) | |
{ | |
var pred = GetPredicate(item); | |
if (pred != null && pred.Desc == key) | |
{ | |
count += item.Amount; | |
} | |
} | |
_counts[key] = count; | |
return count; | |
} | |
public VRage.MyFixedPoint GetNeeded(string key) | |
{ | |
var pred = Predicates.GetValueOrDefault(key); | |
return pred == null ? 0 : pred.Count == 0 ? VRage.MyFixedPoint.MaxValue : pred.Count - GetCount(key); | |
} | |
} | |
private static readonly ItemPredicate DefaultSort = new ItemPredicate("Default!-1"); | |
private static readonly ItemPredicate[] DefaultSortArr = {DefaultSort}; | |
private static readonly ItemPredicate[] DefaultCargo = {new ItemPredicate("!0")}; | |
private static readonly ItemPredicate[] DefaultAssembler = //These are all the max values for vanilla recipes. | |
{ | |
new ItemPredicate("Silicon Wafer!1#12"), | |
new ItemPredicate("Iron Ingot!1#200"), | |
new ItemPredicate("Nickel Ingot!1#24"), | |
new ItemPredicate("Magnesium Powder!1#1"), | |
new ItemPredicate("Gold Ingot!1#4"), | |
new ItemPredicate("Cobalt Ingot!1#74"), | |
new ItemPredicate("Silver Ingot!1#7"), | |
new ItemPredicate("Uranium Ingot!1#0.05"), | |
new ItemPredicate("Platinum Ingot!1#1"), | |
new ItemPredicate("Gravel!1#7") | |
}; | |
private static readonly ItemPredicate[] DefaultRefinery = {new ItemPredicate("Ore#100")}; | |
private static readonly ItemPredicate[] DefaultReactor = {new ItemPredicate("Uranium Ingot!1#1")}; | |
private static readonly ItemPredicate[] DefaultO2Gen = {new ItemPredicate("Ice!1#100")}; | |
private static readonly ItemPredicate[] DefaultGatlingTurret = {new ItemPredicate("NATO_25x184mm!1#25")}; | |
private class ItemPredicate | |
{ | |
public readonly int Priority; | |
public readonly VRage.MyFixedPoint Count; | |
public readonly string Desc; | |
public ItemPredicate(string desc) | |
{ | |
int pi = desc.IndexOf('!'); | |
int ci = desc.IndexOf('#'); | |
if (pi != -1) | |
{ | |
if (ci != -1 && ci > pi) | |
{ | |
Count = (VRage.MyFixedPoint)double.Parse(desc.Substring(ci + 1)); | |
desc = desc.Substring(0, ci); | |
ci = -1; | |
} | |
Priority = int.Parse(desc.Substring(pi + 1)); | |
desc = desc.Substring(0, pi); | |
} | |
if (ci != -1) | |
{ | |
Count = (VRage.MyFixedPoint)double.Parse(desc.Substring(ci + 1)); | |
desc = desc.Substring(0, ci); | |
} | |
desc = desc.Trim(); | |
Desc = (desc.Length == 0 && DefaultSort != null ? DefaultSort.Desc : desc).ToLower(); | |
} | |
public override string ToString() | |
{ | |
return Desc + "!" + Priority + '#' + Count; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment