using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Threading.Tasks; using Avalonia.Threading; using DesertPaintCodex.Models; using DesertPaintCodex.Services; using ReactiveUI; namespace DesertPaintCodex.ViewModels { public class RecipeGeneratorViewModel : ViewModelBase { private const long UpdateInterval = 200; // ms private const long SaveInterval = 30000; // ms private const string PaintStateFile = "dp_generator_state"; private const string RibbonStateFile = "dp_generator_ribbon_state"; private int _selectedView = 0; public int SelectedView { get => _selectedView; private set => this.RaiseAndSetIfChanged(ref _selectedView, value); } private float _progress = 0; public float Progress { get => _progress; private set => this.RaiseAndSetIfChanged(ref _progress, value); } private string _permutationCount = "0"; public string PermutationCount { get => _permutationCount; private set => this.RaiseAndSetIfChanged(ref _permutationCount, value); } private DateTime _mostRecentTime; public DateTime MostRecentTime { get => _mostRecentTime; private set => this.RaiseAndSetIfChanged(ref _mostRecentTime, value); } private GeneratorRecipe? _mostRecentRecipe; public GeneratorRecipe? MostRecentRecipe { get => _mostRecentRecipe; private set => this.RaiseAndSetIfChanged(ref _mostRecentRecipe, value); } private int _productIndex = 0; public int ProductIndex { get => _productIndex; private set => this.RaiseAndSetIfChanged(ref _productIndex, value); } private int _maxReagents; public int MaxReagents { get => _maxReagents; private set => this.RaiseAndSetIfChanged(ref _maxReagents, value); } private int _maxConcentration; public int MaxConcentration { get => _maxConcentration; private set => this.RaiseAndSetIfChanged(ref _maxConcentration, value); } private int _fullQuantity; public int FullQuantity { get => _fullQuantity; private set => this.RaiseAndSetIfChanged(ref _fullQuantity, value); } private int _fullQuantityDepth; public int FullQuantityDepth { get => _fullQuantityDepth; private set => this.RaiseAndSetIfChanged(ref _fullQuantityDepth, value); } private int _maxConcentrationMax; public int MaxConcentrationMax { get => _maxConcentrationMax; private set => this.RaiseAndSetIfChanged(ref _maxConcentrationMax, value); } private int _fullQuantitynMax; public int FullQuantityMax { get => _fullQuantitynMax; private set => this.RaiseAndSetIfChanged(ref _fullQuantitynMax, value); } #region Flags private bool _isPaused = false; public bool IsPaused { get => _isPaused; private set => this.RaiseAndSetIfChanged(ref _isPaused, value); } private bool _isInProgress = false; public bool IsInProgress { get => _isInProgress; private set => this.RaiseAndSetIfChanged(ref _isInProgress, value); } private bool _isRunning = false; public bool IsRunning { get => _isRunning; private set => this.RaiseAndSetIfChanged(ref _isRunning, value); } private bool _canStart = false; public bool CanStart { get => _canStart; private set => this.RaiseAndSetIfChanged(ref _canStart, value); } private bool _canClear = false; public bool CanClear { get => _canClear; private set => this.RaiseAndSetIfChanged(ref _canClear, value); } #endregion // Flags #region Collections public ObservableCollection Reagents { get; } = new(); public ObservableCollection AllRecipes { get; } = new(); #endregion // Collections private readonly RecipeGenerator _generator; private DateTime _lastSave = DateTime.Now; private readonly PlayerProfile _profile; private uint _minConcentration; private readonly ConcurrentQueue _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 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"); } 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.BeginRecipeGeneration( _minConcentration, (uint)MaxConcentration, 1, (uint)MaxReagents, (uint)FullQuantityDepth, (uint)FullQuantity); } public void End() { IsPaused = false; IsRunning = false; IsInProgress = false; _updateTimer.Stop(); _profile.SaveRecipes(); 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.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 recipes, string stateFileName) { _minConcentration = minConcentration; _generator.InitRecipes(recipes); string stateFile = System.IO.Path.Combine(_profile.Directory, stateFileName); if (System.IO.File.Exists(stateFile)) { _generator.LoadState(stateFile); // Always set these, or the values will be invalid MaxReagents = (int)_generator.MaxReagents; MaxConcentration = (int)_generator.MaxConcentration; FullQuantity = (int)_generator.FullQuantity; FullQuantityDepth = (int)_generator.FullQuantityDepth; if (_generator.CanResume) { IsInProgress = true; IsPaused = true; 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) { SaveState(); if (_saving) { _generator.ResumeRecipeGeneration(); _lastSave = DateTime.Now; _saving = false; return; } if (IsPaused) return; End(); } 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; } } // Save if it is time. if (!((now - _lastSave).TotalMilliseconds > SaveInterval)) return; if (_unsavedRecipes) { _profile.SaveRecipes(); _unsavedRecipes = false; } _saving = true; _generator.Stop(); } #endregion private void SavePaintSettings() { SaveSettings("Paint"); } private void LoadPaintSettings() { LoadSettings("Paint", 5, 20, 20, 4); } private void SaveRibbonSettings() { SaveSettings("Ribbon"); } private void LoadRibbonSettings() { LoadSettings("Ribbon", 5, 75, 75, 4); } private void SaveSettings(string mode) { SettingsService.Set($"Generator.{mode}.MaxReagents", MaxReagents); SettingsService.Set($"Generator.{mode}.MaxConcentration", MaxConcentration); SettingsService.Set($"Generator.{mode}.FullQuantity", FullQuantity); SettingsService.Set($"Generator.{mode}.FulLQuantityDepth", FullQuantityDepth); } private void LoadSettings(string mode, int defaultMaxReagents, int defaultMaxConcentration, int defaultFullQuantity, int defaultFullQuantityDepth) { SettingsService.Get($"Generator.{mode}.MaxReagents", out int maxReagents, defaultMaxReagents); SettingsService.Get($"Generator.{mode}.MaxConcentration", out int maxConcentration, defaultMaxConcentration); SettingsService.Get($"Generator.{mode}.FullQuantity", out int fullQuantity, defaultFullQuantity); SettingsService.Get($"Generator.{mode}.FullQuantityDepth", out int fullQuantityDepth, defaultFullQuantityDepth); MaxReagents = maxReagents; MaxConcentration = maxConcentration; FullQuantity = fullQuantity; FullQuantityDepth = fullQuantityDepth; } private void SaveState() { _generator.SaveState(System.IO.Path.Combine(_profile.Directory, _ribbonMode ? RibbonStateFile : PaintStateFile)); } private void ChangeMode() { if (ProductIndex == 0) { if (!_ribbonMode) return; _ribbonMode = false; InitStateForPaint(); } else { if (_ribbonMode) return; _ribbonMode = true; InitStateForRibbons(); } } } }