public bool CanUltimatelyTakeFrom(ProductionNode node) { Queue<ProductionNode> Q = new Queue<ProductionNode>(); HashSet<ProductionNode> V = new HashSet<ProductionNode>(); V.Add(this); Q.Enqueue(this); while (Q.Any()) { ProductionNode t = Q.Dequeue(); if (t == node) { return true; } foreach (NodeLink e in t.InputLinks) { ProductionNode u = e.Supplier; if (!V.Contains(u)) { V.Add(u); Q.Enqueue(u); } } } return false; }
public static NodeLink Create(ProductionNode supplier, ProductionNode consumer, Item item, float maxAmount = float.PositiveInfinity) { NodeLink link = new NodeLink(supplier, consumer, item, maxAmount); supplier.OutputLinks.Add(link); consumer.InputLinks.Add(link); supplier.Graph.InvalidateCaches(); return link; }
//Returns true if a new link was created public void CreateAllLinksForNode(ProductionNode node) { foreach (Item item in node.Inputs) { foreach (ProductionNode existingNode in Nodes.Where(n => n.Outputs.Contains(item))) { if (existingNode != node) { NodeLink.Create(existingNode, node, item); } } } }
public static NodeLink Create(ProductionNode supplier, ProductionNode consumer, Item item, float maxAmount = float.PositiveInfinity) { if (supplier.OutputLinks.Any(l => l.Item == item && l.Consumer == consumer)) { return(null); } NodeLink link = new NodeLink(supplier, consumer, item, maxAmount); supplier.OutputLinks.Add(link); consumer.InputLinks.Add(link); supplier.Graph.InvalidateCaches(); return(link); }
// Ensure that the solution has a rate matching desired for this node. Typically there will // one of these on the ultimate output node, though multiple are supported, on any node. If // there is a conflict, a 'best effort' solution will be returned, where some nodes actual // rates will not match the desired asked for here. public void AddTarget(ProductionNode node, float desiredRate) { var nodeVar = variableFor(node, RateType.ACTUAL); var errorVar = variableFor(node, RateType.ERROR); // The sum of the rate for this node, plus an error variable, must be equal to // desiredRate. In normal scenarios, the error variable will be zero. var constraint = MakeConstraint(desiredRate, desiredRate); constraint.SetCoefficient(nodeVar, 1); constraint.SetCoefficient(errorVar, 1); minimizeError(node, errorVar); }
// Ensure that the sum on the end of all the links is in relation to the rate of the recipe. // The given rate is always for a single execution of the recipe, so the ratio is always (X1 // + X2 + ... + XN)*Rate:1 // // For example, if a copper wire recipe (1 plate makes 2 wires) is connected to two different // consumers, then the sum of the wire rate flowing over those two links must be equal to 2 // time the rate of the recipe. private void addRatio(ProductionNode node, Item item, IEnumerable <NodeLink> links, double rate, EndpointType type) { // Ensure that the sum of all inputs for this type of item is in relation to the rate of the recipe // So for the steel input to a solar panel, the sum of every input variable to this node must equal 5 * rate. var constraint = MakeConstraint(0, 0); var rateVariable = variableFor(node); constraint.SetCoefficient(rateVariable, rate); foreach (var link in links) { var variable = variableFor(link, type); constraint.SetCoefficient(variable, -1); } }
public void LinkUpAllInputs() { HashSet <ProductionNode> nodesToVisit = new HashSet <ProductionNode>(Nodes); while (nodesToVisit.Any()) { ProductionNode currentNode = nodesToVisit.First(); nodesToVisit.Remove(nodesToVisit.First()); nodesToVisit.UnionWith(CreateOrLinkAllPossibleRecipeNodes(currentNode)); nodesToVisit.UnionWith(CreateOrLinkAllPossibleSupplyNodes(currentNode)); CreateAllPossibleInputLinks(); } }
public void AddNode(ProductionNode node) { var x = variableFor(node); this.nodes.Add(node); // The rate of all nodes should be minimized. // // TODO: There is a tension between minimizing supply and minimizing time to produce that // I suspect is not explicitly handled well here. That will likely result in "unexpected" // results depending on which the user is wanting. Need to figure out concrete examples // of where this would happen. With sufficiently large error co-efficients it's probably // fine for most real world recipes? objective.SetCoefficient(x, 1.0); }
public NodeElement(ProductionNode displayedNode, ProductionGraphViewModel parent) { Parent = parent; HorizontalAlignment = HorizontalAlignment.Center; VerticalAlignment = VerticalAlignment.Center; DisplayedNode = displayedNode; Initialize(displayedNode); BackgroundColor = DisplayedNode switch { ConsumerNode consumer => consumer.ConsumedItem.IsMissingItem ? MissingColor : SupplyColor, SupplyNode supplier => supplier.SuppliedItem.IsMissingItem ? MissingColor : ConsumerColor, RecipeNode recipe => recipe.BaseRecipe.IsMissingRecipe ? MissingColor : RecipeColor, PassthroughNode passthrough => passthrough.PassedItem.IsMissingItem ? MissingColor : PassthroughColor, _ => throw new ArgumentException("No branch for node: " + DisplayedNode) }; }
public NodeElement(ProductionNode node, ProductionGraphViewer parent) : base(parent) { Width = 100; Height = 90; DisplayedNode = node; if (DisplayedNode.GetType() == typeof(ConsumerNode)) { backgroundColour = supplyColour; } else if (DisplayedNode.GetType() == typeof(SupplyNode)) { backgroundColour = consumerColour; } else { backgroundColour = recipeColour; } backgroundBrush = new SolidBrush(backgroundColour); foreach (Item item in node.Inputs) { ItemTab newTab = new ItemTab(item, LinkType.Input, Parent); SubElements.Add(newTab); inputTabs.Add(newTab); } foreach (Item item in node.Outputs) { ItemTab newTab = new ItemTab(item, LinkType.Output, Parent); SubElements.Add(newTab); outputTabs.Add(newTab); } if (DisplayedNode is RecipeNode || DisplayedNode is SupplyNode) { assemblerBox = new AssemblerBox(Parent); SubElements.Add(assemblerBox); assemblerBox.Height = assemblerBox.Width = 50; } centreFormat.Alignment = centreFormat.LineAlignment = StringAlignment.Center; }
public List <ProductionNode> GetTopologicalSort() { int[,] matrix = AdjacencyMatrix; List <ProductionNode> L = new List <ProductionNode>(); //Final sorted list List <ProductionNode> S = GetInputlessNodes().ToList(); while (S.Any()) { ProductionNode node = S.First(); S.Remove(node); L.Add(node); int n = Nodes.IndexOf(node); for (int m = 0; m < Nodes.Count(); m++) { if (matrix[n, m] == 1) { matrix[n, m] = 0; int edgeCount = 0; for (int i = 0; i < matrix.GetLength(1); i++) { edgeCount += matrix[i, m]; } if (edgeCount == 0) { S.Insert(0, Nodes[m]); } } } } for (int i = 0; i < matrix.GetLength(0); i++) { for (int j = 0; j < matrix.GetLength(1); j++) { // Edges mean there's a cycle somewhere and the sort can't be completed } } return(L); }
public IEnumerable <IEnumerable <ProductionNode> > GetConnectedComponents() { HashSet <ProductionNode> unvisitedNodes = new HashSet <ProductionNode>(Nodes); List <HashSet <ProductionNode> > connectedComponents = new List <HashSet <ProductionNode> >(); while (unvisitedNodes.Any()) { connectedComponents.Add(new HashSet <ProductionNode>()); HashSet <ProductionNode> toVisitNext = new HashSet <ProductionNode>(); toVisitNext.Add(unvisitedNodes.First()); while (toVisitNext.Any()) { ProductionNode currentNode = toVisitNext.First(); foreach (NodeLink link in currentNode.InputLinks) { if (unvisitedNodes.Contains(link.Supplier)) { toVisitNext.Add(link.Supplier); } } foreach (NodeLink link in currentNode.OutputLinks) { if (unvisitedNodes.Contains(link.Consumer)) { toVisitNext.Add(link.Consumer); } } connectedComponents.Last().Add(currentNode); toVisitNext.Remove(currentNode); unvisitedNodes.Remove(currentNode); } } return(connectedComponents); }
// Constrain input to a node for a particular item so that the node does not consume more // than is being produced by the supplier. // // Consuming less than is being produced is fine. This represents a backup. public void AddInputLink(ProductionNode node, Item item, IEnumerable <NodeLink> links, double inputRate) { Debug.Assert(links.All(x => x.Consumer == node)); // Each item input/output to a recipe has one varible per link. These variables should be // related to one another using one of the other Ratio methods. foreach (var link in links) { var supplierVariable = variableFor(link, EndpointType.SUPPLY); var consumerVariable = variableFor(link, EndpointType.CONSUME); var errorVariable = variableFor(link, EndpointType.ERROR); { // The consuming end of the link must be no greater than the supplying end. var constraint = MakeConstraint(0, double.PositiveInfinity); constraint.SetCoefficient(supplierVariable, 1); constraint.SetCoefficient(consumerVariable, -1); } // Minimize over-supply. Necessary for unbalanced diamond recipe chains (such as // Yuoki smelting - this doesn't occur in Vanilla) where the deficit is made up by an // infinite supplier, in order to not just grab everything from that supplier and let // produced materials backup. Also, this is needed so that resources don't "pool" in // pass-through nodes. // // TODO: A more correct solution for pass-through would be to forbid over-supply on them. { var constraint = MakeConstraint(0, 0); constraint.SetCoefficient(errorVariable, 1); constraint.SetCoefficient(supplierVariable, -1); constraint.SetCoefficient(consumerVariable, 1); // The cost of over-supply needs to be greater than benefit of minimizing rate, // other-wise pure consumption nodes won't consume anything. objective.SetCoefficient(errorVariable, 100); } } }
public static NodeLink?Create(ProductionNode supplier, ProductionNode consumer, Item item, float maxAmount = float.PositiveInfinity) { if (!supplier.Supplies(item) || !consumer.Consumes(item)) { throw new InvalidOperationException($"Cannot connect {supplier} to {consumer} using item {item}"); } if (consumer.InputLinks.Any(l => l.Item == item && l.Supplier == supplier)) { return(null); } if (supplier.OutputLinks.Any(l => l.Item == item && l.Consumer == consumer)) { return(null); } var link = new NodeLink(supplier, consumer, item, maxAmount); supplier.OutputLinks.Add(link); consumer.InputLinks.Add(link); supplier.Graph.InvalidateCaches(); return(link); }
public void CreateRecipeNodeToSatisfyItemDemand(ProductionNode node, Item item, Recipe recipe) { RecipeNode newNode = RecipeNode.Create(recipe, this); NodeLink.Create(newNode, node, item, node.GetUnsatisfiedDemand(item)); }
public void CreateSupplyNodeToSatisfyItemDemand(ProductionNode node, Item item) { SupplyNode newNode = SupplyNode.Create(item, node.Graph); NodeLink.Create(newNode, node, item, node.GetUnsatisfiedDemand(item)); }
public void AutoSatisfyNodeDemand(ProductionNode node, Item item) { if (node.InputLinks.Any(l => l.Item == item)) //Increase throughput of existing node link { NodeLink link = node.InputLinks.First(l => l.Item == item); //link.Amount += node.GetExcessDemand(item); } else if (Nodes.Any(n => n.Outputs.Contains(item))) //Add link from existing node { ProductionNode existingNode = Nodes.Find(n => n.Outputs.Contains(item)); NodeLink.Create(existingNode, node, item); } else if (item.Recipes.Any(r => !CyclicRecipes.Contains(r))) //Create new recipe node and link from it { RecipeNode newNode = RecipeNode.Create(item.Recipes.First(r => !CyclicRecipes.Contains(r)), this); NodeLink.Create(newNode, node, item); } else //Create new supply node and link from it { SupplyNode newNode = SupplyNode.Create(item, this); NodeLink.Create(newNode, node, item, node.GetUnsatisfiedDemand(item)); } ReplaceCycles(); }
public bool TakesFrom(ProductionNode node) { return(node.OutputLinks.Any(l => l.Consumer == this)); }
public double ActualRate(ProductionNode node) { return(Nodes[node]); }
private Variable variableFor(ProductionNode node, RateType type = RateType.ACTUAL) { return(variableFor(Tuple.Create(node, type), makeName("node", type, node.DisplayName))); }
public HashSet <TarjanNode> Links = new HashSet <TarjanNode>(); //Links to other nodes public TarjanNode(ProductionNode sourceNode) { this.SourceNode = sourceNode; }
// Constrain a ratio on the input side of a node public void AddInputRatio(ProductionNode node, Item item, IEnumerable <NodeLink> links, double rate) { Debug.Assert(links.All(x => x.Consumer == node)); addRatio(node, item, links, rate, EndpointType.CONSUME); }
// Constrain a ratio on the output side of a node public void AddOutputRatio(ProductionNode node, Item item, IEnumerable <NodeLink> links, double rate) { Debug.Assert(links.All(x => x.Supplier == node)); addRatio(node, item, links, rate * node.ProductivityMultiplier(), EndpointType.SUPPLY); }
public NodeElement GetElementForNode(ProductionNode node) { return(Elements.OfType <NodeElement>().FirstOrDefault(e => e.DisplayedNode == node)); }
public void LoadFromJson(JObject json) { Graph.Nodes.Clear(); Elements.Clear(); //Has to go first, as all other data depends on which mods are loaded List <String> EnabledMods = json["EnabledMods"].Select(t => (String)t).ToList(); foreach (Mod mod in DataCache.Mods) { mod.Enabled = EnabledMods.Contains(mod.Name); } List <String> enabledMods = DataCache.Mods.Where(m => m.Enabled).Select(m => m.Name).ToList(); using (ProgressForm form = new ProgressForm(enabledMods)) { form.ShowDialog(); } Graph.SelectedAmountType = (AmountType)(int)json["AmountType"]; Graph.SelectedUnit = (RateUnit)(int)json["Unit"]; List <JToken> nodes = json["Nodes"].ToList <JToken>(); foreach (var node in nodes) { ProductionNode newNode = null; switch ((String)node["NodeType"]) { case "Consumer": { String itemName = (String)node["ItemName"]; if (DataCache.Items.ContainsKey(itemName)) { Item item = DataCache.Items[itemName]; newNode = ConsumerNode.Create(item, Graph); } else { Item missingItem = new Item(itemName); missingItem.IsMissingItem = true; newNode = ConsumerNode.Create(missingItem, Graph); } break; } case "Supply": { String itemName = (String)node["ItemName"]; if (DataCache.Items.ContainsKey(itemName)) { Item item = DataCache.Items[itemName]; newNode = SupplyNode.Create(item, Graph); } else { Item missingItem = new Item(itemName); missingItem.IsMissingItem = true; DataCache.Items.Add(itemName, missingItem); newNode = SupplyNode.Create(missingItem, Graph); } break; } case "PassThrough": { String itemName = (String)node["ItemName"]; if (DataCache.Items.ContainsKey(itemName)) { Item item = DataCache.Items[itemName]; newNode = PassthroughNode.Create(item, Graph); } else { Item missingItem = new Item(itemName); missingItem.IsMissingItem = true; DataCache.Items.Add(itemName, missingItem); newNode = PassthroughNode.Create(missingItem, Graph); } break; } case "Recipe": { String recipeName = (String)node["RecipeName"]; if (DataCache.Recipes.ContainsKey(recipeName)) { Recipe recipe = DataCache.Recipes[recipeName]; newNode = RecipeNode.Create(recipe, Graph); } else { Recipe missingRecipe = new Recipe(recipeName, 0f, new Dictionary <Item, float>(), new Dictionary <Item, float>()); missingRecipe.IsMissingRecipe = true; DataCache.Recipes.Add(recipeName, missingRecipe); newNode = RecipeNode.Create(missingRecipe, Graph); } if (node["Assembler"] != null) { var assemblerKey = (String)node["Assembler"]; if (DataCache.Assemblers.ContainsKey(assemblerKey)) { (newNode as RecipeNode).Assembler = DataCache.Assemblers[assemblerKey]; } } (newNode as RecipeNode).NodeModules = ModuleSelector.Load(node); break; } default: { Trace.Fail("Unknown node type: " + node["NodeType"]); break; } } if (newNode != null) { newNode.rateType = (RateType)(int)node["RateType"]; if (newNode.rateType == RateType.Manual) { if (node["DesiredRate"] != null) { newNode.desiredRate = (float)node["DesiredRate"]; } else { // Legacy data format stored desired rate in actual newNode.desiredRate = (float)node["ActualRate"]; } } if (node["SpeedBonus"] != null) { newNode.SpeedBonus = Math.Round((float)node["SpeedBonus"], 4); } if (node["ProductivityBonus"] != null) { newNode.ProductivityBonus = Math.Round((float)node["ProductivityBonus"], 4); } } } List <JToken> nodeLinks = json["NodeLinks"].ToList <JToken>(); foreach (var nodelink in nodeLinks) { ProductionNode supplier = Graph.Nodes[(int)nodelink["Supplier"]]; ProductionNode consumer = Graph.Nodes[(int)nodelink["Consumer"]]; String itemName = (String)nodelink["Item"]; if (!DataCache.Items.ContainsKey(itemName)) { Item missingItem = new Item(itemName); missingItem.IsMissingItem = true; DataCache.Items.Add(itemName, missingItem); } Item item = DataCache.Items[itemName]; NodeLink.Create(supplier, consumer, item); } IEnumerable <String> EnabledAssemblers = json["EnabledAssemblers"].Select(t => (String)t); foreach (Assembler assembler in DataCache.Assemblers.Values) { assembler.Enabled = EnabledAssemblers.Contains(assembler.Name); } IEnumerable <String> EnabledMiners = json["EnabledMiners"].Select(t => (String)t); foreach (Miner miner in DataCache.Miners.Values) { miner.Enabled = EnabledMiners.Contains(miner.Name); } IEnumerable <String> EnabledModules = json["EnabledModules"].Select(t => (String)t); foreach (Module module in DataCache.Modules.Values) { module.Enabled = EnabledModules.Contains(module.Name); } JToken enabledRecipesToken; if (json.TryGetValue("EnabledRecipes", out enabledRecipesToken)) { IEnumerable <String> EnabledRecipes = enabledRecipesToken.Select(t => (String)t); foreach (Recipe recipe in DataCache.Recipes.Values) { recipe.Enabled = EnabledRecipes.Contains(recipe.Name); } } Graph.UpdateNodeValues(); AddRemoveElements(); List <String> ElementLocations = json["ElementLocations"].Select(l => (String)l).ToList(); for (int i = 0; i < ElementLocations.Count; i++) { int[] splitPoint = ElementLocations[i].Split(',').Select(s => Convert.ToInt32(s)).ToArray(); GraphElement element = GetElementForNode(Graph.Nodes[i]); element.Location = new Point(splitPoint[0], splitPoint[1]); } LimitViewToBounds(); }
private Variable VariableFor(ProductionNode node, RateType type = RateType.ACTUAL) { return(VariableFor( Tuple.Create(node, type), key => MakeName(GetNodePrefix(key.Item1), type, key.Item1.DisplayName))); }
public bool TakesFrom(ProductionNode node) { return node.OutputLinks.Any(l => l.Consumer == this); }
public HashSet<TarjanNode> Links = new HashSet<TarjanNode>(); //Links to other nodes public TarjanNode(ProductionNode sourceNode) { this.SourceNode = sourceNode; }
public bool GivesTo(ProductionNode node) { return node.InputLinks.Any(l => l.Supplier == this); }
public static bool CanLink(ProductionNode supplier, ProductionNode consumer, Item item) { return(supplier.Supplies(item) && consumer.Consumes(item)); }
public NodeElement GetElementForNode(ProductionNode node) { return Elements.OfType<NodeElement>().FirstOrDefault(e => e.DisplayedNode == node); }
private NodeLink(ProductionNode supplier, ProductionNode consumer, Item item, float maxAmount = float.PositiveInfinity) { Supplier = supplier; Consumer = consumer; Item = item; }
public bool GivesTo(ProductionNode node) { return(node.InputLinks.Any(l => l.Supplier == this)); }
public NodeElement(ProductionNode node, ProductionGraphViewer parent) : base(parent) { Width = 100; Height = 90; DisplayedNode = node; Color backgroundColour = missingColour; Color textColour = darkTextColour; if (DisplayedNode is ConsumerNode) { backgroundColour = outputColour; if (((ConsumerNode)DisplayedNode).ConsumedItem.IsMissingItem) { backgroundColour = missingColour; } } else if (DisplayedNode is SupplyNode) { backgroundColour = supplyColour; if (((SupplyNode)DisplayedNode).SuppliedItem.IsMissingItem) { backgroundColour = missingColour; } } else if (DisplayedNode is RecipeNode) { backgroundColour = recipeColour; if (((RecipeNode)DisplayedNode).BaseRecipe.IsMissingRecipe) { backgroundColour = missingColour; } } else if (DisplayedNode is PassthroughNode) { backgroundColour = passthroughColour; if (((PassthroughNode)DisplayedNode).PassedItem.IsMissingItem) { backgroundColour = missingColour; } } else { Trace.Fail("No branch for node: " + DisplayedNode.ToString()); } backgroundBrush = new SolidBrush(backgroundColour); textBrush = new SolidBrush(textColour); foreach (Item item in node.Inputs) { ItemTab newTab = new ItemTab(item, LinkType.Input, Parent); SubElements.Add(newTab); inputTabs.Add(newTab); } foreach (Item item in node.Outputs) { ItemTab newTab = new ItemTab(item, LinkType.Output, Parent); SubElements.Add(newTab); outputTabs.Add(newTab); } if (DisplayedNode is RecipeNode || DisplayedNode is SupplyNode) { assemblerBox = new AssemblerBox(Parent); SubElements.Add(assemblerBox); assemblerBox.Height = assemblerBox.Width = 50; } centreFormat.Alignment = centreFormat.LineAlignment = StringAlignment.Center; }