Changeset - d63ade4d8489
[Not reviewed]
Jason Maltzen - 3 years ago 2021-08-16 07:58:58
jason@hiddenachievement.com
Recipe generation no longer incorrectly skips some recipes when a catalyst is included. This should result in some less expensive receips, and more total recipes tested.
5 files changed with 232 insertions and 21 deletions:
0 comments (0 inline, 0 general)
Models/PaintRecipe.cs
Show inline comments
...
 
@@ -227,96 +227,117 @@ namespace DesertPaintCodex.Models
 
            baseRGB.R = 0;
 
            baseRGB.G = 0;
 
            baseRGB.B = 0;
 
            uint pigmentCount = 0;
 
            foreach (ReagentQuantity ingredient in _recipe)
 
            {
 
                string reagentName = ingredient.Name;
 
                if (string.IsNullOrEmpty(reagentName))
 
                {
 
                    continue;
 
                }
 

	
 
                Reagent reagent = ReagentService.GetReagent(reagentName);
 
                
 
                if (reagent.IsCatalyst) continue;
 
                
 
                Debug.Assert(reagent.Color != null);
 
                baseRGB.R    += (reagent.Color.Red   * (int)ingredient.Quantity);
 
                baseRGB.G    += (reagent.Color.Green * (int)ingredient.Quantity);
 
                baseRGB.B    += (reagent.Color.Blue  * (int)ingredient.Quantity);
 
                pigmentCount += ingredient.Quantity;
 
            }
 
            _baseColor.Red   = CalculateColor(baseRGB.R, pigmentCount, 0);
 
            _baseColor.Green = CalculateColor(baseRGB.G, pigmentCount, 0);
 
            _baseColor.Blue  = CalculateColor(baseRGB.B, pigmentCount, 0);
 
        }
 

	
 
        // Compute the base color without any reactions
 
        public uint Cost
 
        {
 
            get
 
            {
 
                uint cost = 0;
 
                foreach (ReagentQuantity ingredient in _recipe)
 
                {
 
                    string reagentName = ingredient.Name;
 
                    if (string.IsNullOrEmpty(reagentName))
 
                    {
 
                        continue;
 
                    }
 

	
 
                    Reagent reagent = ReagentService.GetReagent(reagentName);
 
                    cost += (reagent.Cost * ingredient.Quantity);
 
                }
 
                return cost;
 
            }
 
        }
 

	
 
        public uint Concentration
 
        {
 
            get
 
            {
 
                uint concentration = 0;
 
                foreach (ReagentQuantity ingredient in _recipe)
 
                {
 
                    string reagentName = ingredient.Name;
 
                    if (string.IsNullOrEmpty(reagentName))
 
                    {
 
                        continue;
 
                    }
 

	
 
                    Reagent reagent = ReagentService.GetReagent(reagentName);
 
                    if (reagent.IsCatalyst) continue;
 
                    concentration += ingredient.Quantity;
 
                }
 
                return concentration;
 
            }
 
        }
 

	
 
        public uint GetBulkCost(uint quantity)
 
        {
 
            uint cost = 0;
 
            foreach (ReagentQuantity ingredient in _recipe)
 
            {
 
                string reagentName = ingredient.Name;
 
                if (string.IsNullOrEmpty(reagentName))
 
                {
 
                    continue;
 
                }
 

	
 
                Reagent reagent = ReagentService.GetReagent(reagentName);
 
                cost += (reagent.Cost * ingredient.Quantity);
 
            }
 
            uint batchCount = (uint)Math.Ceiling((double)quantity / cost);  // number of batches require to make quantity
 
            return batchCount * cost;
 
        }
 

	
 
        public bool IsValidForConcentration(uint concentration)
 
        {
 
            uint weight = 0;
 
            foreach (ReagentQuantity ingredient in _recipe)
 
            {
 
                string reagentName = ingredient.Name;
 
                if (string.IsNullOrEmpty(reagentName))
 
                {
 
                    continue;
 
                }
 

	
 
                Reagent reagent = ReagentService.GetReagent(reagentName);
 
                if (!reagent.IsCatalyst)
 
                {
 
                    weight += ingredient.Quantity;
 
                }
 
            }
 
            return (weight >= concentration);
 
        }
 
        public bool HasMissingReactions()
 
        {
 
            ReactionSet? reactions = ProfileManager.CurrentProfile?.Reactions;
 
            Debug.Assert(reactions != null);
 

	
 
            HashSet<string> reagentSet = new();
 
            List<Reagent> prevReagents = new();
 
            
 
            foreach (ReagentQuantity ingredient in _recipe)
 
            {
 
                if (reagentSet.Count > 4) return false;
Models/RecipeSearchNode.cs
Show inline comments
 
using System;
 
using System.Collections.Generic;
 
using System.IO;
 
using System.Text.RegularExpressions;
 

	
 
namespace DesertPaintCodex.Models
 
{
 
    public class RecipeSearchNode
 
    {
 
        private readonly uint[] _reagents;
 
        private readonly uint _invalidReagent;
 

	
 
        private int _nextReagentPos;
 
        public int ReagentCount => _nextReagentPos;
 

	
 
        public uint MinConcentration { get; set; } = 10;
 

	
 
        private readonly bool[] _reagentInUse;
 
        private readonly List<Reagent> _costSortedReagents;
 
        public PaintRecipe? TestRecipe { get; set; }
 

	
 
        public uint CurrentTargetQuantity { get; set; }
 
        public uint MaxConcentration { get; set; }
 
        public uint UsedQuantity { get; private set; }
 
        public uint CatalystCount { get; set; }
 
        public uint FullQuantityDepth { get; set; }
 
        public uint FullQuantity { get; set; }
 
        public uint MinReagents { get; set; }
 

	
 
        public bool ShouldLog { get; private set; }
 

	
 
        public Action<string>? WriteLog = null;
 

	
 
        private uint _maxReagents;
 
        public uint MaxReagents
 
        {
 
            get => _maxReagents;
 
            set
 
            {
 
                _maxReagents = value;
 
                CurrentWeights = new uint[_maxReagents];
 
            }
 
        }
 

	
 
        public uint[] CurrentWeights { get; private set; }
 
        
 
        public uint LastReagent => _reagents[_nextReagentPos - 1];
 

	
 
        public RecipeSearchNode(RecipeSearchNode other)
 
        {
 
            _costSortedReagents = new List<Reagent>(other._costSortedReagents);
 
            _reagents = new uint[_costSortedReagents.Count];
 
            _invalidReagent = (uint)_costSortedReagents.Count;
 
            for (int i = 0; i < _costSortedReagents.Count; ++i)
 
            {
 
                this._reagents[i] = other._reagents[i];
 
            }
 
            _reagentInUse = new bool[_costSortedReagents.Count];
 
            for (uint i = 0; i < _costSortedReagents.Count; ++i)
 
            {
 
                _reagentInUse[i] = other._reagentInUse[i];
 
            }
 
            _nextReagentPos = other.ReagentCount;
 

	
 
            CurrentTargetQuantity = other.CurrentTargetQuantity;
 
            MaxConcentration = other.MaxConcentration;
 
            UsedQuantity = other.UsedQuantity;
 
            CatalystCount = other.CatalystCount;
 
            FullQuantityDepth = other.FullQuantityDepth;
 
            FullQuantity = other.FullQuantity;
 
            MinReagents = other.MinReagents;
 
            MaxReagents = other.MaxReagents;
 
            CurrentWeights = new uint[MaxReagents];
 
            for (int i = 0; i < MaxReagents; ++i)
 
            {
 
                CurrentWeights[i] = other.CurrentWeights[i];
 
            }
 

	
 
            RefreshShouldLog();
 
        }
 

	
 
        public RecipeSearchNode(List<Reagent> costSortedReagents, uint[] reagents)
 
        {
 
            _costSortedReagents = new List<Reagent>(costSortedReagents);
 
            _reagents = new uint[costSortedReagents.Count];
 
            _invalidReagent = (uint)costSortedReagents.Count;
 
            _nextReagentPos = reagents.Length;
 
            for (int i = _reagents.Length - 1; i >= _reagents.Length; --i)
 
            {
 
                reagents[i] = _invalidReagent;
 
            }
 
            for (int i = reagents.Length - 1; i >= 0; --i)
 
            {
 
                _reagents[i] = reagents[i];
 
                if (reagents[i] == _invalidReagent)
 
                {
 
                    _nextReagentPos = i;
 
                }
 
            }
 
            _reagentInUse = new bool[costSortedReagents.Count];
 
            for (uint reagentIdx = 0; reagentIdx < costSortedReagents.Count; ++reagentIdx)
 
            {
 
                _reagentInUse[reagentIdx] = false;
 
            }
 
            foreach (uint reagentIdx in this._reagents)
 
            {
 
                if (reagentIdx != _invalidReagent)
 
                {
 
                    _reagentInUse[reagentIdx] = true;
 
                }
 
            }
 

	
 
            MinReagents = (uint) _nextReagentPos;
 
            MaxReagents = (uint) _nextReagentPos;
 
            CurrentWeights = new uint[MaxReagents];
 
            UsedQuantity = 0;
 

	
 
            RefreshShouldLog();
 
        }
 

	
 
        // top-level search
 
        public RecipeSearchNode(List<Reagent> costSortedReagents, uint startReagent)
 
        {
 
            _costSortedReagents = new List<Reagent>(costSortedReagents);
 
            _reagents = new uint[costSortedReagents.Count];
 
            _invalidReagent = (uint)costSortedReagents.Count;
 
            _nextReagentPos = 0;
 
            for (int i = 0; i < _reagents.Length; ++i)
 
            {
 
                _reagents[i] = _invalidReagent;
 
            }
 
            _reagentInUse = new bool[costSortedReagents.Count];
 
            for (uint reagentIdx = 0; reagentIdx < costSortedReagents.Count; ++reagentIdx)
 
            {
 
                _reagentInUse[reagentIdx] = false;
 
            }
 
            _reagents[_nextReagentPos++] = NextFreeReagent(startReagent);
 
            //Console.WriteLine("Added reagent {0} at pos {1}", this.reagents[nextReagentPos-1], nextReagentPos-1);
 
            MinReagents = 1; // don't iterate up beyond the start reagent
 
            MaxReagents = 1;
 
            CurrentWeights = new uint[MaxReagents];
 
            UsedQuantity = 0;
 

	
 
            RefreshShouldLog();
 
        }
 

	
 
        public RecipeSearchNode(List<Reagent> costSortedReagents)
 
        {
 
            _costSortedReagents = costSortedReagents;
 
            _reagents = new uint[costSortedReagents.Count];
 
            _invalidReagent = (uint)costSortedReagents.Count;
 
            _nextReagentPos = 0;
 
            for (int i = 0; i < _reagents.Length; ++i)
 
            {
 
                _reagents[i] = _invalidReagent;
 
            }
 
            _reagentInUse = new bool[costSortedReagents.Count];
 
            for (uint reagentIdx = 0; reagentIdx < costSortedReagents.Count; ++reagentIdx)
 
            {
 
                _reagentInUse[reagentIdx] = false;
 
            }
 
            _reagents[_nextReagentPos++] = NextFreeReagent(0);
 
            MinReagents = 0;
 
            MaxReagents = 1;
 
            CurrentWeights = new uint[MaxReagents];
 
            UsedQuantity = 0;
 

	
 
            RefreshShouldLog();
 
        }
 

	
 
        private void RefreshShouldLog()
 
        {
 
            ShouldLog = false;
 
            // (ReagentCount == 5) && (GetReagent(0).Name == "Iron") && (GetReagent(1).Name == "Red Sand") && (GetReagent(2).Name == "Sulfur") && (GetReagent(3).Name == "Carrot") && (GetReagent(4).Name == "Copper");
 
        }
 

	
 
        public Reagent GetReagent(int idx)
 
        {
 
            return _costSortedReagents[(int)_reagents[idx]];
 
        }
 

	
 
        public void RemoveLastReagent()
 
        {
 
            uint reagentIdx = _reagents[_nextReagentPos - 1];
 
            ReleaseReagent(reagentIdx);
 
            if (_costSortedReagents[(int)reagentIdx].IsCatalyst)
 
            {
 
                --CatalystCount;
 
            }
 
            _reagents[_nextReagentPos - 1] = _invalidReagent;
 
            --_nextReagentPos;
 
            RefreshShouldLog();
 
        }
 

	
 
        public void ReplaceLastReagent(uint reagentIdx)
 
        {
 
            uint oldReagentIdx = _reagents[_nextReagentPos - 1];
 
            ReleaseReagent(oldReagentIdx);
 
            _reagents[_nextReagentPos - 1] = reagentIdx;
 
            if (_costSortedReagents[(int)oldReagentIdx].IsCatalyst)
 
            {
 
                --CatalystCount;
 
            }
 
            if (_costSortedReagents[(int)reagentIdx].IsCatalyst)
 
            {
 
                ++CatalystCount;
 
            }
 
            RefreshShouldLog();
 
        }
 

	
 
        public uint NextFreeReagent(uint startIdx)
 
        {
 
            uint idx = startIdx;
 
            for (; idx < _costSortedReagents.Count; ++idx)
 
            {
 
                bool inUse = _reagentInUse[idx];
 
                if ((inUse == false) && (_costSortedReagents[(int)idx].Enabled))
 
                {
 
                    //Console.WriteLine("Found free reagent idx {0}", idx);
 
                    _reagentInUse[idx] = true;
 
                    return idx;
 
                }
 
            }
 
            //Console.WriteLine("Failed to find free reagent.");
 
            return (uint)_costSortedReagents.Count;
 
        }
 

	
 
        private void ReleaseReagent(uint reagentIdx)
 
        {
 
            _reagentInUse[reagentIdx] = false;
 
        }
 

	
 
        public bool AddNextReagent()
 
        {
 
            if (ReagentCount >= MaxReagents) return false;
 

	
 
            uint nextReagent = NextFreeReagent(0);
 
            _reagents[_nextReagentPos++] = nextReagent;
 
            if (_costSortedReagents[(int)nextReagent].IsCatalyst)
 
            {
 
                ++CatalystCount;
 
            }
 
            InitForQuantity(CurrentTargetQuantity);
 

	
 
            return true;
 
        }
 

	
 
        public void InitForQuantity(uint quantity)
 
        {
 
            //System.Console.WriteLine("Init for quantity: {0}, reagent count: {1} ({2} catalysts)", quantity, ReagentCount, CatalystCount);
 
            CurrentTargetQuantity = quantity;
 
            if (CurrentTargetQuantity < (MinConcentration + CatalystCount))
 
            {
 
                // invalid quantity
 
                return;
 
            }
 
            UsedQuantity = 0;
 
            uint remainingReagents = ((uint)_nextReagentPos - CatalystCount);
 
            if (ShouldLog)
 
            {
 
                WriteLog?.Invoke($" == initializing {this} @ quantity {CurrentTargetQuantity} with {CatalystCount} catalysts ==");
 
            }
 
            uint remainingReagents = ((uint)_nextReagentPos); // Remaining reagents, including catalysts
 
            uint remainingWeight = CurrentTargetQuantity - CatalystCount;
 
            for (int i = 0; i < _nextReagentPos; ++i)
 
            {
 
                Reagent reagent = GetReagent(i);
 

	
 
                if (reagent.IsCatalyst)
 
                {
 
                    //Console.WriteLine("Init catalyst {0} weight 1", reagent.Name);
 
                    CurrentWeights[i] = 1;
 
                    ++UsedQuantity;
 
                    // This takes quantity but not weight (concentration)
 
                    if (ShouldLog)
 
                    {
 
                        WriteLog?.Invoke($"    + 1 {reagent.Name} (catalyst)");
 
                    }
 
                }
 
                else
 
                {
 
                    uint reagentMaxWeight = reagent.RecipeMax;
 
                    if (ReagentCount <= FullQuantityDepth)
 
                    {
 
                        reagentMaxWeight = Math.Max(FullQuantity, reagentMaxWeight);
 
                    }
 
                    uint weight = Math.Min(remainingWeight - (remainingReagents-1), reagentMaxWeight);
 
                    //Console.WriteLine("Init reagent {0} weight {1}", reagent.Name, weight);
 
                    if (ShouldLog)
 
                    {
 
                        WriteLog?.Invoke($"    + {weight} {reagent.Name} (remain: weight={remainingWeight} reagents={remainingReagents})");
 
                    }
 
                    remainingWeight -= weight;
 
                    CurrentWeights[i] = weight;
 
                    UsedQuantity += weight;
 
                }
 
                --remainingReagents;
 
            }
 
            RefreshShouldLog();
 
        }
 

	
 
        public void SetWeight(int idx, uint quantity)
 
        {
 
            UsedQuantity -= CurrentWeights[idx];
 
            CurrentWeights[idx] = quantity;
 
            UsedQuantity += quantity;
 
        }
 

	
 
        public void SaveState(StreamWriter writer)
 
        {
 
            writer.WriteLine("---SearchNode---");
 
            writer.WriteLine("MinReagents: {0}", MinReagents);
 
            writer.WriteLine("MaxReagents: {0}", MaxReagents);
 
            writer.WriteLine("Reagents: {0}", _nextReagentPos);
 
            for (int i = 0; i < _nextReagentPos; ++i)
 
            {
 
                uint idx = _reagents[i];
 
                uint weight = CurrentWeights[i];
 
                writer.WriteLine("Reagent: {0},{1},{2}", idx, _reagentInUse[idx] ? 1 : 0, weight);
 
            }
 
            // pulled from parent: List<Reagent> costSortedReagents;
 
            // new on construct: PaintRecipe testRecipe = null;
 
            writer.WriteLine("CurrentTargetQuantity: {0}", CurrentTargetQuantity);
 
            writer.WriteLine("MinConcentration: {0}", MinConcentration);
 
            writer.WriteLine("MaxConcentration: {0}", MaxConcentration);
 
            writer.WriteLine("UsedQuantity: {0}", UsedQuantity);
 
            writer.WriteLine("CatalystCount: {0}", CatalystCount);
 
            writer.WriteLine("FullQuantity: {0}", FullQuantity);
 
            writer.WriteLine("FullQuantityDepth: {0}", FullQuantityDepth);
 
            writer.WriteLine("---EndNode---");
 
            
 
        }
 

	
 
        static readonly Regex keyValueRegex = new Regex(@"(\w+)\:\s*(.*)\s*$");
 
        static readonly Regex reagentPartsRegex = new Regex(@"(?<id>\d+),(?<inUse>\d+),(?<weight>\d+)");
 
        public bool LoadState(StreamReader reader)
 
        {
 
            string? line = reader.ReadLine();
 
            
 
            if ((line == null) || !line.Equals("---SearchNode---"))
 
            {
 
                return false;
 
            }
 

	
 
            bool success = true;
 
            while ((line = reader.ReadLine()) != null)
 
            {
...
 
@@ -387,52 +421,69 @@ namespace DesertPaintCodex.Models
 
                            break;
 
                        case "MaxReagents":
 
                            {
 
                                uint value = uint.Parse(match.Groups[2].Value);
 
                                MaxReagents = value;
 
                            }
 
                            break;
 
                        case "UsedQuantity":
 
                            {
 
                                uint value = uint.Parse(match.Groups[2].Value);
 
                                UsedQuantity = value;
 
                            }
 
                            break;
 
                        case "CatalystCount":
 
                            {
 
                                uint value = uint.Parse(match.Groups[2].Value);
 
                                CatalystCount = value;
 
                            }
 
                            break;
 
                        case "InitialCount":
 
                            {
 
                                uint value = uint.Parse(match.Groups[2].Value);
 
                                MinReagents = value;
 
                            }
 
                            break;
 
                        case "FullQuantity":
 
                            {
 
                                uint value = uint.Parse(match.Groups[2].Value);
 
                                FullQuantity = value;
 
                            }
 
                            break;
 
                        case "FullQuantityDepth":
 
                            {
 
                                uint value = uint.Parse(match.Groups[2].Value);
 
                                FullQuantityDepth = value;
 
                            }
 
                            break;
 
                        default:
 
                            success = false;
 
                            break;
 
                    }
 
                }
 
                else
 
                {
 
                    success = false;
 
                    break;
 
                }
 
            }
 
            RefreshShouldLog();
 
            return success;
 
        }        
 

	
 
        public override string ToString()
 
        {
 
            if (_nextReagentPos == 0)
 
            {
 
                return "No ingredients";
 
            }
 
            System.Text.StringBuilder sb = new System.Text.StringBuilder();
 
            sb.Append(_costSortedReagents[(int)_reagents[0]].Name);
 
            for (int idx = 1; idx < _nextReagentPos; ++idx)
 
            {
 
                Reagent reagent = _costSortedReagents[(int)_reagents[idx]];
 
                sb.Append($", {reagent.Name}");
 
            }
 
            return sb.ToString();
 
        }
 
    }
 
}
...
 
\ No newline at end of file
Models/Settings.cs
Show inline comments
 
using System.Collections.Generic;
 
using System.IO;
 
using System.Text.RegularExpressions;
 

	
 
namespace DesertPaintCodex.Models
 
{
 
    internal class Settings
 
    {
 
        private readonly Dictionary<string, string> _settings = new();
 

	
 
        public bool TryGet(string key, out int value)
 
        {
 
            value = 0;
 
            return _settings.TryGetValue(key.ToLower(), out string? valStr)
 
                && int.TryParse(valStr, out value);
 
        }
 
        public bool TryGet(string key, out bool value)
 
        {
 
            value = false;
 
            return _settings.TryGetValue(key.ToLower(), out string? valStr)
 
                && bool.TryParse(valStr, out value);
 
            bool found = _settings.TryGetValue(key.ToLower(), out string? valStr);
 
            found = found && bool.TryParse(valStr, out value);
 
            return found;
 

	
 
        }
 
        public void Set(string key, int value)
 
        {
 
            _settings[key.ToLower()] = value.ToString();
 
        }
 
        public void Set(string key, bool value)
 
        {
 
            _settings[key.ToLower()] = value.ToString();
 
        }
 

	
 
        public void Reset()
 
        {
 
            _settings.Clear();
 
        }
 

	
 
        public void Save(string settingsPath)
 
        {
 
            using StreamWriter writer = new(settingsPath);
 
            foreach (KeyValuePair<string, string> pair in _settings)
 
            {
 
                writer.WriteLine("{0}={1}", pair.Key, pair.Value);
 
            }
 
        }
 

	
 
        private static readonly Regex OptionEntry = new(@"(?<opt>[^#=][^=]*)=(?<optval>.*)$");
 
        
 
        public bool Load(string settingsPath)
 
        {
 
            if (!File.Exists(settingsPath)) return false;
 
            using StreamReader reader = new(settingsPath);
 
            string? line;
 
            while ((line = reader.ReadLine()) != null)
 
            {
 
                Match match = OptionEntry.Match(line);
 
                
 
                if (!match.Success) continue;
 
                
 
                string optName = match.Groups["opt"].Value.ToLower();
 
                string optVal  = match.Groups["optval"].Value.Trim();
 
                if (optName.Equals("debug"))
 
                {
 
                    // convert
 
                    optName = "enabledebugmenu";
 
                }
 
                _settings[optName.ToLower()] = optVal;
 
            }
 

	
Services/RecipeGenerator.cs
Show inline comments
...
 
@@ -48,245 +48,279 @@ namespace DesertPaintCodex.Services
 
        private readonly Dictionary<string, uint> _recipeCosts = new ();
 
        private readonly Dictionary<string, PaintRecipe> _recipes = new();
 

	
 
        private uint _totalReagents;
 

	
 
        private readonly List<Reagent> _costSortedReagents = new();
 

	
 
        private readonly ConcurrentQueue<RecipeSearchNode> _searchQueue = new();
 

	
 
        private ulong _recipeCount = 0;
 

	
 
        private readonly List<Thread> _generatorThreads = new();
 
        private readonly object _workerLock = new();
 

	
 
        private bool _requestCancel = false;
 

	
 
        private StreamWriter? _log;
 

	
 
        // events
 
        public event EventHandler? Finished;
 
        public event EventHandler? Progress;
 
        public event EventHandler<NewRecipeEventArgs>? NewRecipe;
 

	
 
        public RecipeGenerator()
 
        {
 
            Mode = SearchType.BreadthFirst;
 
            foreach (PaintColor color in PaletteService.Colors)
 
            {
 
                _recipes.Add(color.Name, new PaintRecipe());
 
                _recipeCosts.Add(color.Name, uint.MaxValue);
 
            }
 
            MinReagents = 1;
 
            MaxReagents = 5;
 
            MinConcentration = 10;
 
            MaxConcentration = 20;
 
        }
 

	
 
        public Dictionary<string, PaintRecipe> Recipes => _recipes;
 

	
 
        public ulong RecipeCount => _recipeCount;
 

	
 
        public bool CanResume => (!_running && (_searchQueue.Count > 0));
 

	
 
        public string? Log
 
        {
 
            set => _log = value != null ? new StreamWriter(value) : null;
 
        }
 

	
 
        public void FlushLog()
 
        {
 
            _log?.Flush();
 
        }
 

	
 
        public void CloseLog()
 
        {
 
            _log?.Close();
 
            _log = null;
 
        }
 

	
 
        private void WriteLog(string msg)
 
        {
 
            if (_log == null) return;
 
            lock (_workerLock)
 
            {
 
                _log.WriteLine(msg);
 
            }
 
        }
 

	
 
        private void WriteLog(string msg, params object[] args)
 
        {
 
            if (_log == null) return;
 
            lock(_workerLock)
 
            {
 
                _log.WriteLine(msg, args);
 
            }
 
        }
 

	
 
        private class ReagentCostSort : IComparer<Reagent>
 
        {
 
            public int Compare(Reagent? reagent1, Reagent? reagent2)
 
            {
 
                if (reagent1 == null)
 
                {
 
                    if (reagent2 == null) return 0;
 
                    return -1;
 
                }
 
                if (reagent2 == null) return 1;
 
                
 
                return (int)reagent1.Cost - (int)reagent2.Cost;
 
            }
 
        }
 

	
 
        public void InitRecipes(Dictionary<string, PaintRecipe> 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 ReagentService.Names)
 
            {
 
                Reagent reagent = ReagentService.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 concentration 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;
 
            }
 

	
 
            MinConcentration = minConcentration;
 
            MaxConcentration = maxConcentration;
 
            MinReagents = minReagents;
 
            MaxReagents = maxReagents;
 
            FullQuantity = fullQuantity;
 
            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
 
            WriteLog($"===================== BEGIN RECIPE GENERATION ========================== {DateTime.Now}");
 
            WriteLog("Pre-populating basic recipes.");
 
            int enabledReagentCount = 0;
 
            PaintRecipe recipe = new();
 
            foreach (var reagent in _costSortedReagents.Where(reagent => reagent.Enabled))
 
            {
 
                if (!reagent.IsCatalyst && ((reagent.RecipeMax >= minConcentration) || ((FullQuantityDepth > 0) && (FullQuantity >= minConcentration))))
 
                {
 
                    recipe.Clear();
 
                    recipe.AddReagent(reagent.Name, minConcentration);
 
                    AddCheapestRecipe(recipe);
 
                }
 
                ++enabledReagentCount;
 
            }
 
            MaxReagents = (uint)Math.Min(enabledReagentCount, MaxReagents);
 
            MinReagents = Math.Min(MinReagents, MaxReagents);
 
            
 
            _searchQueue.Clear();
 

	
 
            for (uint reagentIdx = 0; reagentIdx < _costSortedReagents.Count; ++reagentIdx)
 
            {
 
                if (!_costSortedReagents[(int) reagentIdx].Enabled) continue;
 
                
 
                // queue up all combinations of MinReagents
 
                RecipeSearchNode initialNode = new(_costSortedReagents, reagentIdx)
 
                {
 
                    FullQuantity      = FullQuantity,
 
                    FullQuantityDepth = FullQuantityDepth,
 
                    MinConcentration  = minConcentration,
 
                    MaxConcentration  = maxConcentration,
 
                    MinReagents       = minReagents,
 
                    MaxReagents       = maxReagents
 
                };
 
                initialNode.WriteLog = WriteLog;
 

	
 
                if (MinReagents > 1)
 
                {
 
                    while (NextReagentSetBreadthFirst(initialNode, 1, minReagents))
 
                    {
 
                        if (initialNode.ReagentCount != minReagents) continue;
 
                        
 
                        //Console.WriteLine("Initial node at size {0}/{1} with recipe: {2}", initialNode.ReagentCount, minReagents, initialNode.TestRecipe.ToString());
 
                        RecipeSearchNode searchNode = new RecipeSearchNode(initialNode);
 
                        searchNode.WriteLog = WriteLog;
 
                        _searchQueue.Enqueue(searchNode);
 
                    }
 
                }
 
                else
 
                {
 
                    _searchQueue.Enqueue(initialNode);
 
                }
 
            }
 

	
 
            _recipeCount = 0;
 

	
 
            _log?.WriteLine("Begin recipe generation: MaxConcentration={0} MinReagents={1} MaxReagents={2} FullQuantity={3} FullQuantityDepth={4}", MaxConcentration, MinReagents, MaxReagents, FullQuantity, FullQuantityDepth);
 
            WriteLog("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;
 

	
 
            _log?.WriteLine("Resuming recipe generation: pre-threads={0} reagent count={1} search queue={2}", _runningThreads, _costSortedReagents.Count, _searchQueue.Count);
 
            WriteLog("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)
 
            {
 
                Finished?.Invoke(this, EventArgs.Empty);
 
            }
 
            _generatorThreads.Clear();
 
            Console.WriteLine("Starting {0} generator threads.", threadCount);
 
            WriteLog($"===== Starting {threadCount} threads.");
 
            for (int i = 0; i < threadCount; ++i)
 
            {
 
                Thread thr = new(Generate) {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(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<string, PaintRecipe> pair in _recipes)
 
                {
 
                    PaintRecipe recipe    = pair.Value;
 
                    string      colorName = PaletteService.FindNearest(recipe.ReactedColor);
 
                    writer.WriteLine("BeginRecipe: {0}", colorName);
 
                    foreach (PaintRecipe.ReagentQuantity reagent in recipe.Reagents)
 
                    {
 
                        writer.WriteLine("Ingredient: {0}={1}", reagent.Name, reagent.Quantity);
 
                    }
 
                    writer.WriteLine("EndRecipe: {0}", colorName);
 
                }
 
                writer.WriteLine("SearchNodes: {0}", _searchQueue.Count);
 
                foreach (RecipeSearchNode node in _searchQueue)
 
                {
 
                    node.SaveState(writer);
 
                }
 
            }
 
            return true;
 
        }
...
 
@@ -336,502 +370,580 @@ namespace DesertPaintCodex.Services
 
                        case "RecipeCount":
 
                            if (!ulong.TryParse(value, out _recipeCount))
 
                            {
 
                                // must have rolled to negative - try as an int and convert
 
                                long recipeCountInt = int.Parse(value);
 
                                _recipeCount = (ulong)(recipeCountInt & 0x00000000ffffffffL);
 
                            }
 
                            break;
 
                        case "BeginRecipe":
 
                            currentRecipe = new PaintRecipe();
 
                            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 = _reagentRegex.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(_costSortedReagents)
 
                                {
 
                                    FullQuantity      = FullQuantity,
 
                                    FullQuantityDepth = FullQuantityDepth,
 
                                    MinReagents       = MinReagents,
 
                                    MaxReagents       = MaxReagents,
 
                                    MaxConcentration  = MaxConcentration
 
                                };
 
                                node.WriteLog = WriteLog;
 
                                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()
 
        {
 
            lock(_workerLock)
 
            {
 
                ++_runningThreads;
 
            }
 

	
 
            bool ok;
 
            do
 
            {
 
                RecipeSearchNode? node;
 
                lock (_workerLock)
 
                {
 
                    ok = _searchQueue.TryDequeue(out node);
 
                }
 
                
 
                if (!ok) continue;
 
                if (!ok) break;
 
                
 
                Debug.Assert(node != null);
 
                
 
                node.WriteLog = WriteLog;
 
                
 
                if (Mode == SearchType.DepthFirst)
 
                {
 
                    uint targetQuantity = (node.ReagentCount <= FullQuantityDepth)
 
                        ? ((uint)node.ReagentCount * FullQuantity) 
 
                        : node.MaxConcentration + 1;
 
                    do {
 
                        --targetQuantity;
 
                        if (node.ShouldLog)
 
                        {
 
                            WriteLog($" == Initializing {node} for quantity {targetQuantity}");
 
                        }
 
                        node.InitForQuantity(targetQuantity);
 
                    } while (targetQuantity > MinConcentration && (node.CurrentTargetQuantity != node.UsedQuantity));
 
    
 
                    while ((ok = IterateDepthFirst(node)) && !_requestCancel)
 
                    {
 
                        Progress?.Invoke(this, EventArgs.Empty);
 
                    }
 
                }
 
                else
 
                {
 
                    // breadth-first search
 
                    uint targetQuantity = MinConcentration - 1;
 
                    uint quantityLimit = (node.ReagentCount <= FullQuantityDepth) 
 
                        ? (FullQuantity * (uint)node.ReagentCount) 
 
                        : node.MaxConcentration;
 
                    do {
 
                        ++targetQuantity;
 
                        if (node.ShouldLog)
 
                        {
 
                            WriteLog($" == Initializing {node} for quantity {targetQuantity}");
 
                        }
 
                        node.InitForQuantity(targetQuantity);
 
                    } while ((targetQuantity < quantityLimit) && (node.CurrentTargetQuantity != node.UsedQuantity));
 
    
 
                    while ((ok = IterateBreadthFirst(node)) && !_requestCancel)
 
                    {
 
                        Progress?.Invoke(this, EventArgs.Empty);
 
                    }
 
                }
 
                if (ok)
 
                {
 
                    // stopped because cancel was requested - requeue the node in its current state for resume
 
                    _searchQueue.Enqueue(node);
 
                }
 
            } while (!_requestCancel && ok);
 
            } while (!_requestCancel);
 

	
 
            bool done;
 
            lock(_workerLock)
 
            {
 
                --_runningThreads;
 
                //generatorThreads.Remove(Thread.CurrentThread);
 

	
 
                done = (_runningThreads == 0);
 
            }
 

	
 
            if (!done) return;
 
            
 
            _running       = false;
 
            _requestCancel = false;
 
            Finished?.Invoke(this, EventArgs.Empty);
 
        }
 

	
 
        // 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)) return;
 
            
 
            string colorName = PaletteService.FindNearest(recipe.ReactedColor);
 
            lock (_workerLock)
 
            {
 
                uint newCost = recipe.Cost;
 
                if (_recipeCosts.TryGetValue(colorName, out var cost))
 
                {
 
                    if (cost <= recipe.Cost) return;
 
                    if (cost < newCost)
 
                    {
 
                        // WriteLog($"Skipping recipe (cost {newCost} > {cost}): {recipe}");
 
                        return;
 
                    }
 
                    PaintRecipe origRecipe = _recipes[colorName];
 
                    if (cost == newCost && recipe.Concentration >= origRecipe.Concentration)
 
                    {
 
                        // Same cost, greater concentration - use the lower concentration recipe
 
                        WriteLog($"Skipping recipe (cost {newCost}, {recipe.Concentration} >= {origRecipe.Concentration}): {recipe}");
 
                        return;
 
                    }
 
                        
 
                    _recipes[colorName].CopyFrom(recipe);
 
                    _recipeCosts[colorName] = recipe.Cost;
 
                    _log?.WriteLine("New recipe (cost {0}): {1}", recipe.Cost, recipe);
 
                    _recipes[colorName].CopyFrom(recipe); // Copy recipe because the one passed in should be const and will get overwritten
 
                    _recipeCosts[colorName] = newCost;
 
                    WriteLog($"Replacing recipe (cost {newCost} < {cost}): {recipe}");
 
                        
 
                    if (NewRecipe == null) return;
 
                        
 
                    NewRecipeEventArgs args = new(colorName, recipe);
 
                    NewRecipeEventArgs args = new(colorName, _recipes[colorName]);
 
                    NewRecipe(this, args);
 
                }
 
                else
 
                {
 
                    // This would be an error!
 
                    _recipeCosts.Add(colorName, recipe.Cost);
 
                    _recipes.Add(colorName, new PaintRecipe(recipe));
 
                    _recipeCosts.Add(colorName, newCost);
 
                    PaintRecipe newRecipe = new PaintRecipe(recipe); // Copy recipe because the one passed in should be const and will get overwritten
 
                    _recipes.Add(colorName, newRecipe);
 

	
 
                    WriteLog($"New recipe (cost {newCost}): {recipe}");
 
                        
 
                    if (NewRecipe == null) return;
 
                        
 
                    NewRecipeEventArgs args = new(colorName, recipe);
 
                    NewRecipeEventArgs args = new(colorName, newRecipe);
 
                    NewRecipe(this, args);
 
                }
 
            }
 
        }
 

	
 
        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()) continue;
 
                
 
                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;
 
            }
 

	
 
            string origNodeVal = node.ToString();
 

	
 
            // 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) continue;
 
                
 
                if (node.ShouldLog)
 
                {
 
                    WriteLog($" == Initializing {node} for quantity {newQuantity}");
 
                }
 
                node.InitForQuantity(newQuantity);
 
                if (node.CurrentTargetQuantity <= node.UsedQuantity)
 
                {
 
                    //if (log != null) { lock(log) { log.WriteLine("Update quantity to {0}", node.CurrentTargetQuantity); } }
 
                    if (node.ShouldLog)
 
                    {
 
                        WriteLog($"  == {node} quantity {newQuantity}");
 
                    }
 
                    return true;
 
                }
 
            } while (newQuantity < quantityLimit);
 

	
 
            bool ok = NextReagentSetBreadthFirst(node, node.MinReagents, node.MaxReagents);
 
            if (node.ShouldLog)
 
            {
 
                if (ok)
 
                {
 
                    WriteLog($"==== {origNodeVal} next reagent set: {node} @ {node.CurrentTargetQuantity}");
 
                }
 
                else
 
                {
 
                    WriteLog($"==== done with reagent set: {origNodeVal}");
 
                }
 
            }
 
            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);
 
            if (node.ShouldLog)
 
            {
 
                WriteLog($" == Initializing {node} for quantity {node.MinConcentration + node.CatalystCount}");
 
            }
 
            node.InitForQuantity(node.MinConcentration + node.CatalystCount); // reset quantity
 
            int currentDepth = node.ReagentCount;
 
            bool recipeFound;
 
            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) continue;
 
                    
 
                    while (node.ReagentCount > minReagents)
 
                    {
 
                        if (node.LastReagent < (_totalReagents - 1))
 
                        {
 
                            var 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) return false;
 
            if (!recipeFound)
 
            {
 
                if (node.ShouldLog)
 
                {
 
                    WriteLog($" == no more recipes for {node}");
 
                }
 
                return false;
 
            }
 
            
 
            node.InitForQuantity(node.MinConcentration+node.CatalystCount); // minimum quantity for this recipe
 
            
 
            node.TestRecipe ??= new PaintRecipe();
 
            node.TestRecipe.Clear();
 
            
 
            for (int i = 0; i < node.ReagentCount; ++i)
 
            {
 
                node.TestRecipe.AddReagent(node.GetReagent(i).Name, node.CurrentWeights[i]);
 
            }
 

	
 
            return recipeFound;
 
        }
 

	
 
        private void TestCurrentRecipe(RecipeSearchNode node)
 
        {
 
            node.TestRecipe ??= new PaintRecipe();
 
            node.TestRecipe.Clear();
 
            for (int i = 0; i < node.ReagentCount; ++i)
 
            {
 
                node.TestRecipe.AddReagent(node.GetReagent(i).Name, node.CurrentWeights[i]);
 
            }
 
            if (node.ShouldLog)
 
            {
 
                WriteLog($"   -> {node.TestRecipe}");
 
            }
 
            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");
 
                if (node.ShouldLog)
 
                {
 
                    WriteLog($" ***** Not enough remaining weight in recipe {node} at target quantity {node.CurrentTargetQuantity} catalysts {node.CatalystCount} min concentration {MinConcentration}");
 
                }
 
                return false;
 
            }
 
            //uint remainingReagents = (uint)node.Reagents.Count - node.CatalystCount;
 

	
 
            uint depth = (uint)node.ReagentCount;
 
            uint weightToConsume = 0;
 
            uint spaceBelow = 0;
 
            int reagentsBelow = 0;
 
            // Start from the end of the current reagent list and work to the front
 
            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.GetReagent(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.GetReagent(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 static bool NextRecipeSize(RecipeSearchNode node)
 
        {
 
            uint newQuantity = node.CurrentTargetQuantity - 1;
 
            if (newQuantity < (node.MinConcentration + node.CatalystCount))
 
            {
 
                return false;
 
            }
 

	
 
            node.InitForQuantity(newQuantity);
 
            return node.CurrentTargetQuantity <= node.UsedQuantity;
 
        }
 

	
 
        public void Wait()
 
        {
 
            if (_generatorThreads.Count <= 0) return;
 
            
 
            foreach (Thread thr in _generatorThreads)
 
            {
 
                thr.Join();
 
            }
 
            _generatorThreads.Clear();
 
        }
 

	
 
        public void Stop()
 
        {
 
            _requestCancel = true;
 
        }
 

	
 
        public void ResetQueue()
 
        {
 
            lock (_workerLock)
 
            {
 
                // Don't reset the queue ever while running
 
                if (_running) return;
 
                _searchQueue.Clear();
 
            }
 
        }
 

	
 
        public void Reset()
 
        {
 
            foreach (PaintRecipe recipe in _recipes.Values)
 
            {
 
                recipe.Clear();
 
            }
 
            foreach (string key in _recipeCosts.Keys)
 
            {
 
                _recipeCosts[key] = uint.MaxValue;
 
            }
 
        }        
 
    }
 
}
...
 
\ No newline at end of file
ViewModels/RecipeGeneratorViewModel.cs
Show inline comments
...
 
@@ -81,160 +81,178 @@ namespace DesertPaintCodex.ViewModels
 
        public ObservableCollection<Reagent> Reagents { get; } = new();
 
        public ObservableCollection<GeneratorRecipe> AllRecipes { get; } = new();
 
        
 
        #endregion // Collections
 
        
 
        private readonly RecipeGenerator _generator;
 

	
 
        private DateTime _lastSave = DateTime.Now;
 
        private readonly PlayerProfile _profile;
 
        private uint _minConcentration;
 

	
 
        private readonly ConcurrentQueue<PaintRecipe> _pendingNewRecipes = new();
 
        private volatile int _newRecipeCount;
 
        private volatile bool _updatesAvailable;
 
        private readonly DispatcherTimer _updateTimer;
 
        
 
        private bool _ribbonMode;
 
        private bool _unsavedRecipes;
 
        private bool _saving;
 

	
 
        
 
        public RecipeGeneratorViewModel()
 
        {
 
            List<string> reagentNames = ReagentService.Names;
 
            foreach (string name in reagentNames)
 
            {
 
                Reagents.Add(ReagentService.GetReagent(name));
 
            }
 
            
 
            SettingsService.Get("Generator.RibbonMode", out bool ribbonMode, false);
 
            ProductIndex = ribbonMode ? 1 : 0;
 
            _ribbonMode = ribbonMode;
 

	
 
            PlayerProfile? profile = ProfileManager.CurrentProfile;
 
            Debug.Assert(profile != null);
 
            _profile = profile;
 
            
 
            _profile.LoadRecipes();
 

	
 
            _generator = new RecipeGenerator();
 

	
 
            _generator.Progress  += OnProgress;
 
            _generator.NewRecipe += OnNewRecipe;
 
            _generator.Finished  += OnGeneratorStopped;
 

	
 
            SettingsService.Get("Generator.Logging", out bool logGenerator, false);
 
            if (logGenerator)
 
            {
 
                string logDir = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
 
                _generator.Log = System.IO.Path.Combine(logDir, "dpl_generator.txt");
 
                string logDir = DesertPaintCodex.Util.FileUtils.AppDataPath;
 
                _generator.Log = System.IO.Path.Combine(logDir, "recipe_generator_log.txt");
 
            }
 
            
 
            if (ribbonMode)
 
            {
 
                InitStateForRibbons();
 
            }
 
            else
 
            {
 
                InitStateForPaint();
 
            }
 

	
 
            _updateTimer = new DispatcherTimer {Interval = TimeSpan.FromMilliseconds(UpdateInterval)};
 
            _updateTimer.Tick += Update;
 

	
 
            this.WhenAnyValue(x => x.ProductIndex).Subscribe(_ => ChangeMode());
 
        }
 

	
 
        public void Start()
 
        {
 
            IsInProgress = true;
 
            IsRunning = true;
 
            ReagentService.SaveProfileReagents(_profile.ReagentFile);
 
            SaveSettings(_ribbonMode ? "Ribbon" : "Paint");
 
            _updateTimer.Start();
 
            SelectedView = 1;
 
            
 
            _generator.CloseLog();
 
            SettingsService.Get("Generator.Logging", out bool logGenerator, false);
 
            if (logGenerator)
 
            {
 
                string logDir = DesertPaintCodex.Util.FileUtils.AppDataPath;
 
                _generator.Log = System.IO.Path.Combine(logDir, "recipe_generator_log.txt");
 
            }
 

	
 
            _generator.BeginRecipeGeneration(
 
                _minConcentration, 
 
                (uint)MaxConcentration, 
 
                1, 
 
                (uint)MaxReagents, 
 
                (uint)FullQuantityDepth, 
 
                (uint)FullQuantity);
 
        }
 

	
 
        
 
        public void End()
 
        {
 
            IsPaused     = false;
 
            IsRunning    = false;
 
            IsInProgress = false;
 
            _updateTimer.Stop();
 
            _profile.SaveRecipes();
 
            _generator.ResetQueue();
 
            SaveState();
 
            
 
            CanClear = true;
 
        }
 

	
 
        public void Pause()
 
        {
 
            IsPaused  = true;
 
            IsRunning = false;
 
            _updateTimer.Stop();
 
            _generator.Stop();
 
        }
 

	
 
        public void Resume()
 
        {
 
            IsPaused  = false;
 
            IsRunning = true;
 
            _updateTimer.Start();
 
            SelectedView = 1;
 
            
 
            _generator.CloseLog();
 
            SettingsService.Get("Generator.Logging", out bool logGenerator, false);
 
            if (logGenerator)
 
            {
 
                string logDir = DesertPaintCodex.Util.FileUtils.AppDataPath;
 
                _generator.Log = System.IO.Path.Combine(logDir, "recipe_generator_log.txt");
 
            }
 

	
 
            _generator.ResumeRecipeGeneration();
 

	
 
        }
 

	
 
        public async Task Clear()
 
        {
 
            bool result = await ShowYesNoBox("Clear Recipes", "Are you sure you want to clear your recipes?");
 
            if (result)
 
            {
 
                CanClear = false;
 
                if (_ribbonMode)
 
                {
 
                    _profile.ClearRibbonRecipes();
 
                }
 
                else
 
                {
 
                    _profile.ClearPaintRecipes();
 
                }
 

	
 
                foreach (GeneratorRecipe recipe in AllRecipes)
 
                {
 
                    recipe.ClearRecipe();
 
                }
 
                
 
                _generator.Reset();
 
            }
 
        }
 

	
 
        private void InitStateForPaint()
 
        {
 
            MaxConcentrationMax = 20;
 
            FullQuantityMax = 20;
 
            InitState("Paint", PaintRecipe.PaintRecipeMinConcentration, _profile.Recipes, PaintStateFile);
 

	
 
        }
 

	
 
        private void InitStateForRibbons()
 
        {
 
            MaxConcentrationMax = 100;
 
            FullQuantityMax = 100;
 
            InitState("Ribbon", PaintRecipe.RibbonRecipeMinConcentration, _profile.RibbonRecipes, RibbonStateFile);
 
        }
 

	
 
        private void InitState(string mode, uint minConcentration, Dictionary<string, PaintRecipe> recipes, string stateFileName)
 
        {
 
            _minConcentration = minConcentration;
 
            _generator.InitRecipes(recipes);
 
            
...
 
@@ -257,109 +275,117 @@ namespace DesertPaintCodex.ViewModels
 
                    SaveSettings(mode);
 
                }
 
                else
 
                {
 
                    IsInProgress = false;
 
                    IsPaused = false;
 
                    CanClear = true;
 
                }
 
            }
 
            else if (mode == "Paint")
 
            {
 
                LoadPaintSettings();
 
            }
 
            else
 
            {
 
                LoadRibbonSettings();
 
            }
 

	
 
            AllRecipes.Clear();
 

	
 
            foreach (PaintColor color in PaletteService.Colors)
 
            {
 
                GeneratorRecipe genRecipe = new(color);
 
                if (recipes.TryGetValue(color.Name, out PaintRecipe? recipe)) //  && recipe.IsValidForConcentration(minConcentration)) // This check is redundant, since they're checked when loading.
 
                {
 
                    genRecipe.DraftRecipe(recipe);
 
                }
 
                AllRecipes.Add(genRecipe);
 
            }
 
        }
 

	
 
        #region Event Handlers
 
        private void OnProgress(object? sender, EventArgs args)
 
        {
 
            _newRecipeCount = (int)_generator.RecipeCount;
 
            _updatesAvailable = true;
 
        }
 

	
 
        private void OnNewRecipe(object? sender, NewRecipeEventArgs args)
 
        {
 
            PaintRecipe recipe = new(args.Recipe);
 
            _pendingNewRecipes.Enqueue(recipe);
 
            _updatesAvailable = true;
 
        }
 
        
 
        
 
        private void OnGeneratorStopped(object? sender, EventArgs args)
 
        {
 
            if (_saving)
 
            {
 
            SaveState();
 
            
 
            if (_saving)
 
            {
 
                _generator.ResumeRecipeGeneration();
 
                _lastSave = DateTime.Now;
 
                _saving = false;
 
                return;
 
            }
 

	
 
            if (IsPaused) return;
 
            _generator.FlushLog();
 

	
 
            if (IsPaused)
 
            {
 
                SaveState();
 
                _generator.CloseLog();
 
                return;
 
            }
 

	
 
            End();
 
            _generator.CloseLog();
 
        }
 

	
 
        private void Update(object? sender, EventArgs e)
 
        {
 
            if (!_updatesAvailable) return;
 
            if (_saving) return;
 

	
 
            _updatesAvailable = false;
 

	
 
            DateTime now = DateTime.Now;
 

	
 
            // Update test count.
 
            PermutationCount = $"{_newRecipeCount:n0}";
 
            
 
            // Pull in new recipes.
 
            if (!_pendingNewRecipes.IsEmpty)
 
            {
 
                GeneratorRecipe? lastRecipe = null;
 
                while (_pendingNewRecipes.TryDequeue(out PaintRecipe? newRecipe))
 
                {
 
                    string recipeColor = PaletteService.FindNearest(newRecipe.ReactedColor);
 
                    foreach (GeneratorRecipe recipe in AllRecipes)
 
                    {
 
                        if (recipe.Color.Name != recipeColor) continue;
 
                        recipe.DraftRecipe(newRecipe);
 
                        lastRecipe = recipe;
 
                        if (_ribbonMode)
 
                        {
 
                            _profile.SetRibbonRecipe(recipeColor, newRecipe);
 
                        }
 
                        else
 
                        {
 
                            _profile.SetRecipe(recipeColor, newRecipe);
 
                        }
 
                        break;
 
                    }
 
                }
 

	
 
                // If at least one recipe was processed, let's check to see if it's time to save, and
 
                // highlight that recipe.
 
                if (lastRecipe != null)
 
                {
 
                    _unsavedRecipes = true;
 
                    MostRecentRecipe = lastRecipe;
 
                    MostRecentTime = now;
 
                }
 
            }
 
            
0 comments (0 inline, 0 general)