/* * Copyright (c) 2015, Jason Maltzen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ using System; using System.IO; using System.Collections.Generic; using System.Collections.Concurrent; using System.Text.RegularExpressions; using System.Threading; namespace DesertPaintLab { public class NewRecipeEventArgs : EventArgs { string color; PaintRecipe recipe; public NewRecipeEventArgs(string color, PaintRecipe recipe) { this.color = color; this.recipe = recipe; } public string Color { get { return color; } } public PaintRecipe Recipe { get { return recipe; } } } public class RecipeGenerator { public enum SearchType { DEPTH_FIRST, BREADTH_FIRST }; public SearchType Mode { get; set; } public uint MinConcentration { get; private set; } // minimum paint concentration public uint MaxConcentration { get; private set; } // maximum paint concentration public uint MaxReagents { get; private set; } // maximum number of reagents to use in the recipe public uint MinReagents { get; private set; } // minimum number of reagents to use in the recipe public uint FullQuantityDepth { get; private set; } // at or equal this number of reagents, ignore ingredient settings for max quantity public uint FullQuantity { get; private set; } // The max number of a reagent to use at full quantity public uint MaxThreads { get; set; } ReactionSet reactions; bool running = false; int runningThreads = 0; SortedDictionary recipeCosts = new SortedDictionary(); SortedDictionary recipes = new SortedDictionary(); uint totalReagents; List costSortedReagents = new List(); ConcurrentQueue searchQueue = new ConcurrentQueue(); ulong recipeCount = 0; List generatorThreads = new List(); Object workerLock = new Object(); bool requestCancel = false; StreamWriter log; // events public event EventHandler Finished; public event EventHandler Progress; public event EventHandler NewRecipe; public RecipeGenerator(ReactionSet reactions) { Mode = SearchType.BREADTH_FIRST; this.reactions = reactions; foreach (PaintColor color in Palette.Colors) { recipes.Add(color.Name, new PaintRecipe()); recipeCosts.Add(color.Name, uint.MaxValue); } MinReagents = 1; MaxReagents = 5; MinConcentration = 10; MaxConcentration = 20; } public SortedDictionary Recipes { get { return recipes; } } public ulong RecipeCount { get { return recipeCount; } } public bool CanResume { get { return (!running && (searchQueue.Count > 0)); } } public string Log { set { if (value != null) { log = new StreamWriter(value); } else { log = null; } } } private class ReagentCostSort : IComparer { public int Compare(Reagent reagent1, Reagent reagent2) { return (int)reagent1.Cost - (int)reagent2.Cost; } } public void InitRecipes(SortedDictionary initialRecipes) { if (running) { return; } recipeCosts.Clear(); recipes.Clear(); foreach (PaintRecipe recipe in initialRecipes.Values) { //PaintRecipe recipeCopy = new PaintRecipe(recipe); AddCheapestRecipe(recipe); } } private void InitSortedReagents() { costSortedReagents.Clear(); foreach (string name in ReagentManager.Names) { Reagent reagent = ReagentManager.GetReagent(name); costSortedReagents.Add(reagent); } costSortedReagents.Sort(new ReagentCostSort()); } // Generate paint recipes. // minConcentration - the minimum permitted concentration in a recipe (10 for paint, 50 for ribbons) // maxConcentration - the maximum concentrataion for a recipe // minReagents - the minimum number of ingredients in a recipe // maxReagents - the maximum number of ingredients allowed in a recipe // fullQuantityDepth - at this depth of ingredient or below, allow up to fullQuantity of any reagent // fullQuantity - the maximum amount of any reagent to permit up to the full quantity depth. After that, reagents are limited by the // per-reagent value public void BeginRecipeGeneration(uint minConcentration, uint maxConcentration, uint minReagents, uint maxReagents, uint fullQuantityDepth, uint fullQuantity) { if (running) { // Already running - don't start again return; } this.MinConcentration = minConcentration; this.MaxConcentration = maxConcentration; this.MinReagents = minReagents; this.MaxReagents = maxReagents; this.FullQuantity = fullQuantity; this.FullQuantityDepth = fullQuantityDepth; // first, sort reagents by cost. InitSortedReagents(); totalReagents = (uint)costSortedReagents.Count; // Pre-populate recipes list with: // 1) 1-ingredient recipes @ min concentration for all enabled ingredients with a count >= min concentration // 2) any previously-generated recipes int enabledReagentCount = 0; PaintRecipe recipe = new PaintRecipe(); recipe.Reactions = reactions; foreach (Reagent reagent in costSortedReagents) { if (reagent.Enabled) { if (!reagent.IsCatalyst && ((reagent.RecipeMax >= minConcentration) || ((FullQuantityDepth > 0) && (FullQuantity >= minConcentration)))) { recipe.Clear(); recipe.AddReagent(reagent.Name, minConcentration); AddCheapestRecipe(recipe); } ++enabledReagentCount; } } this.MaxReagents = (uint)Math.Min(enabledReagentCount, this.MaxReagents); this.MinReagents = (uint)Math.Min(this.MinReagents, this.MaxReagents); while (!searchQueue.IsEmpty) { RecipeSearchNode node; searchQueue.TryDequeue(out node); } for (uint reagentIdx = 0; reagentIdx < costSortedReagents.Count; ++reagentIdx) { if (costSortedReagents[(int)reagentIdx].Enabled) { // queue up all combinations of MinReagents RecipeSearchNode initialNode = new RecipeSearchNode(costSortedReagents, reagentIdx); initialNode.FullQuantity = FullQuantity; initialNode.FullQuantityDepth = FullQuantityDepth; initialNode.MinConcentration = minConcentration; initialNode.MaxConcentration = maxConcentration; initialNode.MinReagents = minReagents; initialNode.MaxReagents = maxReagents; if (MinReagents > 1) { while (NextReagentSetBreadthFirst(initialNode, 1, minReagents) == true) { if (initialNode.ReagentCount == minReagents) { //Console.WriteLine("Initial node at size {0}/{1} with recipe: {2}", initialNode.ReagentCount, minReagents, initialNode.TestRecipe.ToString()); RecipeSearchNode searchNode = new RecipeSearchNode(initialNode); searchQueue.Enqueue(searchNode); } } } else { searchQueue.Enqueue(initialNode); } } } recipeCount = 0; if (log != null) { log.WriteLine("Begin recipe generation: MaxConcentration={0} MinReagents={1} MaxReagents={2} FullQuantity={3} FullQuantityDepth={4}", MaxConcentration, MinReagents, MaxReagents, FullQuantity, FullQuantityDepth); } // start worker threads to do the actual work ResumeRecipeGeneration(); } public void ResumeRecipeGeneration() { if (running) { // Already running - don't start again return; } running = true; requestCancel = false; if (log != null) { log.WriteLine("Resuming recipe generation: pre-threads={0} reagent count={1} search queue={2}", runningThreads, costSortedReagents.Count, searchQueue.Count); } runningThreads = 0; // presumably! int threadCount = Math.Min(Math.Min(costSortedReagents.Count, searchQueue.Count), (int)MaxThreads); if (threadCount == 0) { if (Finished != null) { Finished(this, null); } } generatorThreads.Clear(); System.Console.WriteLine("Starting {0} generator threads.", threadCount); for (int i = 0; i < threadCount; ++i) { Thread thr = new Thread(new ThreadStart(this.Generate)); thr.Priority = ThreadPriority.BelowNormal; generatorThreads.Add(thr); } foreach (Thread thr in generatorThreads) { thr.Start(); } } public bool SaveState(string file) { if (running) { // can't save state while running return false; } lock(workerLock) { using (StreamWriter writer = new StreamWriter(file, false)) { writer.WriteLine("MinReagents: {0}", MinReagents); writer.WriteLine("MaxReagents: {0}", MaxReagents); writer.WriteLine("FullQuantityDepth: {0}", FullQuantityDepth); writer.WriteLine("FullQuantity: {0}", FullQuantity); writer.WriteLine("TotalReagents: {0}", totalReagents); writer.WriteLine("RecipeCount: {0}", recipeCount); writer.WriteLine("SearchType: {0}", Mode.ToString()); foreach (KeyValuePair pair in recipes) { PaintRecipe recipe = pair.Value; string colorName = Palette.FindNearest(recipe.ReactedColor); writer.WriteLine("BeginRecipe: {0}", colorName); foreach (PaintRecipe.RecipeIngredient ingredient in recipe.Ingredients) { writer.WriteLine("Ingredient: {0}={1}", ingredient.name, ingredient.quantity); } writer.WriteLine("EndRecipe: {0}", colorName); } writer.WriteLine("SearchNodes: {0}", searchQueue.Count); foreach (RecipeSearchNode node in searchQueue) { node.SaveState(writer); } } } return true; } static Regex keyValueRegex = new Regex(@"(?\w+)\:\s*(?.+)\s*"); static Regex ingredientRegex = new Regex(@"(?(\w+\s)*\w+)\s*=\s*(?\d)\s*"); public bool LoadState(string file) { // cannot be running, and reactions must be set if (running) { return false; } InitSortedReagents(); bool success = true; PaintRecipe currentRecipe = null; Match match; string line; using (StreamReader reader = new StreamReader(file, false)) { while (success && ((line = reader.ReadLine()) != null)) { match = keyValueRegex.Match(line); if (match.Success) { string value = match.Groups["value"].Value; switch(match.Groups["key"].Value) { case "MinReagents": MinReagents = uint.Parse(value); MaxReagents = (uint)Math.Max(this.MinReagents, this.MaxReagents); break; case "MaxReagents": MaxReagents = uint.Parse(value); MinReagents = (uint)Math.Min(this.MinReagents, this.MaxReagents); break; case "FullQuantityDepth": FullQuantityDepth = uint.Parse(value); break; case "FullQuantity": FullQuantity = uint.Parse(value); break; case "TotalReagents": totalReagents = uint.Parse(value); break; case "RecipeCount": if (!ulong.TryParse(value, out recipeCount)) { // must have rolled to negative - try as an int and convert int recipeCountInt = int.Parse(value); recipeCount = (ulong)((long)recipeCountInt & 0x00000000ffffffffL); } break; case "BeginRecipe": currentRecipe = new PaintRecipe(); currentRecipe.Reactions = reactions; break; case "EndRecipe": if (currentRecipe != null) { PaintColor color = currentRecipe.ReactedColor; uint cost = currentRecipe.Cost; recipes[color.Name] = currentRecipe; // replace recipeCosts[color.Name] = cost; currentRecipe = null; } break; case "Ingredient": if (currentRecipe != null) { Match ingredientMatch = ingredientRegex.Match(match.Groups["value"].Value); if (ingredientMatch.Success) { uint quantity = uint.Parse(ingredientMatch.Groups["quantity"].Value); currentRecipe.AddReagent(ingredientMatch.Groups["ingredient"].Value, quantity); } else { success = false; } } break; case "SearchNodes": int nodeCount = int.Parse(match.Groups["value"].Value); for (int i = 0; i < nodeCount; ++i) { RecipeSearchNode node = new RecipeSearchNode(costSortedReagents); node.FullQuantity = FullQuantity; node.FullQuantityDepth = FullQuantityDepth; node.MinReagents = MinReagents; node.MaxReagents = MaxReagents; node.MaxConcentration = MaxConcentration; success = success && node.LoadState(reader); if (success) { searchQueue.Enqueue(node); } } break; case "SearchType": Mode = (SearchType)Enum.Parse(typeof(SearchType), match.Groups["value"].Value); break; default: success = false; break; } } else { success = false; break; } } return success; } } private void Generate() { RecipeSearchNode node; lock(workerLock) { ++runningThreads; } bool ok = true; do { lock (workerLock) { ok = searchQueue.TryDequeue(out node); } if (ok) { if (Mode == SearchType.DEPTH_FIRST) { uint targetQuantity = (node.ReagentCount <= FullQuantityDepth) ? ((uint)node.ReagentCount * FullQuantity) : node.MaxConcentration + 1; do { --targetQuantity; node.InitForQuantity(targetQuantity); } while (targetQuantity > MinConcentration && (node.CurrentTargetQuantity != node.UsedQuantity)); while ((ok = IterateDepthFirst(node)) && !requestCancel) { if (Progress != null) { Progress(this, null); } } } else { // breadth-first search uint targetQuantity = MinConcentration - 1; uint quantityLimit = (node.ReagentCount <= FullQuantityDepth) ? (FullQuantity * (uint)node.ReagentCount) : node.MaxConcentration; do { ++targetQuantity; node.InitForQuantity(targetQuantity); } while ((targetQuantity < quantityLimit) && (node.CurrentTargetQuantity != node.UsedQuantity)); while ((ok = IterateBreadthFirst(node)) && !requestCancel) { Progress?.Invoke(this, null); } } if (ok) { // stopped because cancel was requested - requeue the node in its current state for resume searchQueue.Enqueue(node); } } } while (!requestCancel && ok); bool done = false; lock(workerLock) { --runningThreads; //generatorThreads.Remove(Thread.CurrentThread); done = (runningThreads == 0); } if (done) { running = false; requestCancel = false; if (Finished != null) { Finished(this, null); } } } // Add the cheapest recipe to the recipe list // returns the discarded recipe from the pair (or null if no original recipe to replace) private void AddCheapestRecipe(PaintRecipe recipe) { if (recipe.IsValidForConcentration(MinConcentration)) { recipe.Reactions = reactions; string colorName = Palette.FindNearest(recipe.ReactedColor); //System.Console.WriteLine("Recipe: {0} {1}:", colorName, recipe.Cost); //foreach (PaintRecipe.RecipeIngredient ingr in recipe.Ingredients) //{ // System.Console.WriteLine(" -> {0} {1}", ingr.quantity, ingr.name); //} uint cost; lock (workerLock) { if (recipeCosts.TryGetValue(colorName, out cost)) { if (cost > recipe.Cost) { recipes[colorName].CopyFrom(recipe); recipeCosts[colorName] = recipe.Cost; log?.WriteLine("New recipe (cost {0}): {1}", recipe.Cost, recipe); if (NewRecipe != null) { NewRecipeEventArgs args = new NewRecipeEventArgs(colorName, recipe); NewRecipe(this, args); } } //else //{ // Console.WriteLine("Ignoring recipe - existing cost is cheaper."); //} } else { // This would be an error! recipeCosts.Add(colorName, recipe.Cost); recipes.Add(colorName, new PaintRecipe(recipe)); if (NewRecipe != null) { NewRecipeEventArgs args = new NewRecipeEventArgs(colorName, recipe); NewRecipe(this, args); } } } } else { //string msg = String.Format("Recipe is invalid ({0} ingredients)\n", recipe.Ingredients.Count); //foreach (PaintRecipe.RecipeIngredient ingr in recipe.Ingredients) //{ // msg += String.Format(" -> {0} {1}", ingr.quantity, ingr.name); //} //lock (workerLock) { // Console.WriteLine(msg); //} } } private bool IterateDepthFirst(RecipeSearchNode node) { TestCurrentRecipe(node); // pick recipe quantities at current recipe ingredients/size if (NextRecipe(node)) { lock(workerLock) { ++recipeCount; } //System.Console.WriteLine("Found next recipe at size {0} qty {1}", node.Reagents.Count, node.CurrentTargetQuantity); return true; } if (NextRecipeSize(node)) { //System.Console.WriteLine("Found next recipe size {0}", node.CurrentTargetQuantity); return true; } // Search for next ingredient combo - all quantity combos for previous were searched //System.Console.WriteLine("Finding next ingredient combo"); do { if (!node.AddNextReagent()) { while ((node.ReagentCount > node.MinReagents) && (node.LastReagent == (totalReagents-1))) { node.RemoveLastReagent(); } if (node.ReagentCount == node.MinReagents) { // done return false; } uint nextReagent = node.NextFreeReagent(node.LastReagent); while ((node.ReagentCount > node.MinReagents) && (nextReagent >= totalReagents)) { // No more reagents to try at this level node.RemoveLastReagent(); if (node.ReagentCount > node.MinReagents) { nextReagent = node.NextFreeReagent(node.LastReagent); } } if (node.ReagentCount == node.MinReagents) { // done return false; } node.ReplaceLastReagent(nextReagent); } } while (node.MaxConcentration < (node.MinConcentration + node.CatalystCount)); node.InitForQuantity(node.MaxConcentration); //string outStr = "{0} : {1} : "; //for (int i = 0; i < currentReagents.Count; ++i) //{ // Reagent reagent = costSortedReagents[(int)currentReagents[i]]; // if (i > 0) // { // outStr += ", "; // } // outStr += reagent.Name + " (" + reagent.Cost + ")"; //} //Console.WriteLine(outStr, currentReagents.Count, recipeCount); return true; } private bool IterateBreadthFirst(RecipeSearchNode node) { // pick recipe quantities at current recipe ingredients/size TestCurrentRecipe(node); lock(workerLock) { ++recipeCount; } // search all quantities of current recipe if (NextRecipe(node)) { //System.Console.WriteLine("Found next recipe at size {0} qty {1}", node.ReagentCount, node.CurrentTargetQuantity); return true; } // Try next quantity uint newQuantity; uint quantityLimit = ((uint)node.ReagentCount <= FullQuantityDepth) ? ((uint)node.ReagentCount * FullQuantity) : node.MaxConcentration; do { newQuantity = node.CurrentTargetQuantity + 1; //Console.WriteLine("Try quantity {0}", newQuantity); if (newQuantity <= quantityLimit) { node.InitForQuantity(newQuantity); if (node.CurrentTargetQuantity <= node.UsedQuantity) { //if (log != null) { lock(log) { log.WriteLine("Update quantity to {0}", node.CurrentTargetQuantity); } } return true; } } } while (newQuantity < quantityLimit); bool ok = NextReagentSetBreadthFirst(node, node.MinReagents, node.MaxReagents); return ok; } private bool NextReagentSetBreadthFirst(RecipeSearchNode node, uint minReagents, uint maxReagents) { // search all variants at this depth of recipe // increase recipe depth // next reagent in last position // if at end, pop reagent //Console.WriteLine("Finding new recipe after quantity {0}/{1} used {2}", newQuantity, node.MaxConcentration, node.UsedQuantity); node.InitForQuantity(node.MinConcentration+node.CatalystCount); // reset quantity int currentDepth = node.ReagentCount; bool recipeFound = false; uint nextReagent; do { //Console.WriteLine("Current depth: {0}/{1}", currentDepth, node.MaxReagents); do { recipeFound = false; // back out until we find a node that can be incremented if (currentDepth > minReagents) { while (node.ReagentCount > minReagents) { if (node.LastReagent < (totalReagents - 1)) { nextReagent = node.NextFreeReagent(node.LastReagent); if (nextReagent < totalReagents) { //Console.WriteLine("Replace last reagent with {0}", nextReagent); node.ReplaceLastReagent(nextReagent); break; } else { // shouldn't happen //Console.WriteLine("No available reagents at depth {0}!", node.ReagentCount); node.RemoveLastReagent(); if (node.ReagentCount == minReagents) { // just popped the last reagent at the top level ++currentDepth; //if (log != null) { lock(log) { log.WriteLine("Increased depth to {0}/{1}", currentDepth, node.MaxReagents); } } } } } else { //Console.WriteLine("Pop last reagent"); node.RemoveLastReagent(); if (node.ReagentCount == minReagents) { // just popped the last reagent at the top level ++currentDepth; //if (log != null) { lock(log) { log.WriteLine("Increased depth to {0}/{1} [pop last reagent at top level]", currentDepth, node.MaxReagents); } } } } } // fill in the nodes up to the current depth if (node.ReagentCount >= minReagents && currentDepth <= maxReagents) { recipeFound = true; while (node.ReagentCount < currentDepth) { if (! node.AddNextReagent()) { //if (log != null) { lock(log) { log.WriteLine("Failed to add reagent {0}/{1}", node.ReagentCount+1, currentDepth); } } recipeFound = false; } } } } //Console.WriteLine("Catalysts: {0} Reagents: {1} Min: {2}", node.CatalystCount, node.ReagentCount, node.MinReagents); } while ((node.CatalystCount >= node.ReagentCount) && (node.ReagentCount >= minReagents)); // make sure to skip all-catalyst combinations if (recipeFound) { break; } else { ++currentDepth; //if (log != null) { lock(log) { log.WriteLine("Increased depth to {0}/{1} [no recipe]", currentDepth, node.MaxReagents); } } } } while (currentDepth <= maxReagents); if (recipeFound) { node.InitForQuantity(node.MinConcentration+node.CatalystCount); // minimum quantity for this recipe if (node.TestRecipe == null) { node.TestRecipe = new PaintRecipe(); node.TestRecipe.Reactions = reactions; } node.TestRecipe.Clear(); for (int i = 0; i < node.ReagentCount; ++i) { node.TestRecipe.AddReagent(node.Reagent(i).Name, node.CurrentWeights[i]); } if (log != null) { //string combo = ""; //foreach (PaintRecipe.RecipeIngredient ingr in node.TestRecipe.Ingredients) //{ // combo += " " + ingr.name; //} //lock(log) { log.WriteLine("New ingredients: " + combo); } } } return recipeFound; } private void TestCurrentRecipe(RecipeSearchNode node) { if (node.TestRecipe == null) { node.TestRecipe = new PaintRecipe(); node.TestRecipe.Reactions = reactions; } node.TestRecipe.Clear(); for (int i = 0; i < node.ReagentCount; ++i) { node.TestRecipe.AddReagent(node.Reagent(i).Name, node.CurrentWeights[i]); } AddCheapestRecipe(node.TestRecipe); //if (log != null) { lock(log) { log.WriteLine("Tested recipe: {0}", node.TestRecipe); } } } private bool NextRecipe(RecipeSearchNode node) { // check for the next recipe uint remainingWeight = node.CurrentTargetQuantity - node.CatalystCount; if (remainingWeight < MinConcentration) { // not possible to make a valid recipe //Console.WriteLine("Insufficient remaining weight"); return false; } //uint remainingReagents = (uint)node.Reagents.Count - node.CatalystCount; uint depth = (uint)node.ReagentCount; uint weightToConsume = 0; uint spaceBelow = 0; int reagentsBelow = 0; for (int i = (int)depth-1 ; i >= 0; --i) { uint currentWeight = node.CurrentWeights[i]; if ((spaceBelow >= (weightToConsume+1)) && (currentWeight > 1)) { // reduce this node by 1, allocate remaining weight to reagents below it node.SetWeight(i, currentWeight-1); weightToConsume += 1; for (int j = i+1; j < depth; ++j) { --reagentsBelow; Reagent reagent = node.Reagent(j); uint allocated = (uint)Math.Min(reagent.IsCatalyst ? 1 : (depth <= FullQuantityDepth ? FullQuantity : reagent.RecipeMax), weightToConsume - reagentsBelow); if (allocated > 100) { Console.WriteLine("ACK: allocated = {0}", allocated); } node.SetWeight(j, allocated); weightToConsume -= allocated; } break; } else { Reagent reagent = node.Reagent(i); spaceBelow += (reagent.IsCatalyst ? 1 : (depth <= FullQuantityDepth ? FullQuantity : reagent.RecipeMax)); weightToConsume += currentWeight; ++reagentsBelow; } } //int recipeWeight = 0; //foreach (int weight in node.CurrentWeights) //{ // recipeWeight += weight; //} //if ((weightToConsume != 0) || (recipeWeight != node.CurrentTargetQuantity)) //{ // Console.WriteLine("Failed recipe with leftover weight {0} ({1}/{2}):", weightToConsume, recipeWeight, node.CurrentTargetQuantity); // for (int i = 0; i < node.Reagents.Count; ++i) // { // Console.WriteLine(" > {0} {1}", node.Reagent(i).Name, node.CurrentWeights[i]); // } //} return (weightToConsume == 0); } private bool NextRecipeSize(RecipeSearchNode node) { uint newQuantity = node.CurrentTargetQuantity - 1; if (newQuantity < (node.MinConcentration + node.CatalystCount)) { return false; } node.InitForQuantity(newQuantity); if (node.CurrentTargetQuantity > node.UsedQuantity) { return false; } return true; } public void Wait() { if (generatorThreads.Count > 0) { foreach (Thread thr in generatorThreads) { thr.Join(); } generatorThreads.Clear(); } } public void Stop() { this.requestCancel = true; } public void Reset() { foreach (PaintRecipe recipe in recipes.Values) { recipe.Clear(); } foreach (string key in recipeCosts.Keys) { recipeCosts[key] = uint.MaxValue; } } } }