From 2549bb47ae79f3f042bc6ac74662ff402b5a53f6 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Wed, 15 Nov 2023 01:09:03 -0800 Subject: [PATCH] Move simulation and macro state to separate class --- Craftimizer/Utils/SimulatedMacro.cs | 234 ++++++++++++++++++++++++++++ Craftimizer/Windows/MacroEditor.cs | 213 ++----------------------- 2 files changed, 251 insertions(+), 196 deletions(-) create mode 100644 Craftimizer/Utils/SimulatedMacro.cs diff --git a/Craftimizer/Utils/SimulatedMacro.cs b/Craftimizer/Utils/SimulatedMacro.cs new file mode 100644 index 0000000..7e3f223 --- /dev/null +++ b/Craftimizer/Utils/SimulatedMacro.cs @@ -0,0 +1,234 @@ +using Craftimizer.Plugin; +using Craftimizer.Simulator; +using Craftimizer.Simulator.Actions; +using System; +using System.Collections.Generic; +using System.Linq; +using Sim = Craftimizer.Simulator.Simulator; +using SimNoRandom = Craftimizer.Simulator.SimulatorNoRandom; + +namespace Craftimizer.Utils; + +internal sealed class SimulatedMacro +{ + public readonly record struct Reliablity + { + public sealed class Param + { + private List DataList { get; } + private ImGuiUtils.ViolinData? ViolinData { get; set; } + + public int Max { get; private set; } + public int Min { get; private set; } + public float Median { get; private set; } + public float Average { get; private set; } + + public Param() + { + DataList = new(); + } + + public void Add(int value) + { + DataList.Add(value); + } + + public void FinalizeData() + { + if (DataList.Count == 0) + { + Average = Median = Max = Min = 0; + return; + } + + Max = DataList.Max(); + Min = DataList.Min(); + if (DataList.Count % 2 == 0) + Median = (float)DataList.Order().Skip(DataList.Count / 2 - 1).Take(2).Average(); + else + Median = DataList.Order().ElementAt(DataList.Count / 2); + Average = (float)DataList.Average(); + } + + public ImGuiUtils.ViolinData? GetViolinData(float barMax, int resolution, double bandwidth) => + ViolinData ??= + Min != Max ? + new(DataList, 0, barMax, resolution, bandwidth) : + null; + } + + public readonly Param Progress = new(); + public readonly Param Quality = new(); + + // Param is either collectability, quality, or hq%, depending on the recipe + public readonly Param ParamScore = new(); + + public Reliablity(in SimulationState startState, IEnumerable actions, int iterCount, RecipeData recipeData) + { + Func getParam; + if (recipeData.Recipe.ItemResult.Value!.IsCollectable) + getParam = s => s.Collectability; + else if (recipeData.Recipe.RequiredQuality > 0) + { + var reqQual = recipeData.Recipe.RequiredQuality; + getParam = s => (int)((float)s.Quality / reqQual * 100); + } + else if (recipeData.RecipeInfo.MaxQuality > 0) + getParam = s => s.HQPercent; + else + getParam = s => 0; + + for (var i = 0; i < iterCount; ++i) + { + var sim = new Sim(); + var (_, state, _) = sim.ExecuteMultiple(startState, actions); + Progress.Add(state.Progress); + Quality.Add(state.Quality); + ParamScore.Add(getParam(state)); + } + Progress.FinalizeData(); + Quality.FinalizeData(); + ParamScore.FinalizeData(); + } + } + + private sealed record Step + { + public ActionType Action { get; } + // State *after* executing the action + public ActionResponse Response { get; private set; } + public SimulationState State { get; private set; } + private Reliablity? Reliability { get; set; } + + public Step(ActionType action, Sim sim, in SimulationState lastState, out SimulationState newState) + { + Action = action; + newState = Recalculate(sim, lastState); + } + + public SimulationState Recalculate(Sim sim, in SimulationState lastState) + { + (Response, State) = sim.Execute(lastState, Action); + Reliability = null; + return State; + } + + public Reliablity GetReliability(in SimulationState initialState, IEnumerable actionSet, RecipeData recipeData) => + Reliability ??= + new(initialState, actionSet, Service.Configuration.ReliabilitySimulationCount, recipeData); + }; + + private List Macro { get; set; } = new(); + private SimulationState initialState; + public SimulationState InitialState + { + get => initialState; + set + { + if (initialState != value) + { + initialState = value; + RecalculateState(); + } + } + } + private object QueueLock { get; } = new(); + private List QueuedSteps { get; set; } = new(); + + public SimulationState State => Macro.Count > 0 ? Macro[^1].State : InitialState; + + public IEnumerable Actions => Macro.Select(m => m.Action); + public int Count => Macro.Count; + + public (ActionType Action, ActionResponse Response, SimulationState State) this[int i] + { + get + { + var step = Macro[i]; + return (step.Action, step.Response, step.State); + } + } + + public Reliablity GetReliability(RecipeData recipeData) => + Macro.Count > 0 ? + Macro[^1].GetReliability(InitialState, Macro.Select(m => m.Action), recipeData) : + new(InitialState, Array.Empty(), 0, recipeData); + + public void RemoveRange(int index, int count) => + Macro.RemoveRange(index, count); + + public void Clear() => + Macro.Clear(); + + public void Add(ActionType action) + { + var sim = CreateSim(); + Macro.Add(new(action, sim, State, out _)); + } + + public void Insert(int index, ActionType action) + { + if (index < 0 || index >= Macro.Count) + throw new ArgumentOutOfRangeException(nameof(index)); + + var state = index == 0 ? InitialState : Macro[index - 1].State; + var sim = CreateSim(); + Macro.Insert(index, new(action, sim, state, out state)); + + for (var i = index + 1; i < Macro.Count; i++) + state = Macro[i].Recalculate(sim, state); + } + + public void RemoveAt(int index) + { + if (index < 0 || index >= Macro.Count) + throw new ArgumentOutOfRangeException(nameof(index)); + + Macro.RemoveAt(index); + + var state = index == 0 ? InitialState : Macro[index - 1].State; + var sim = CreateSim(); + for (var i = index; i < Macro.Count; i++) + state = Macro[i].Recalculate(sim, state); + } + + public void Enqueue(ActionType action) + { + lock (QueueLock) + { + var lastState = QueuedSteps.Count > 0 ? QueuedSteps[^1].State : State; + QueuedSteps.Add(new(action, CreateSim(), lastState, out _)); + } + } + + public void ClearQueue() + { + lock (QueueLock) + { + QueuedSteps.Clear(); + } + } + + public void FlushQueue() + { + lock (QueueLock) + { + if (QueuedSteps.Count > 0) + { + Macro.AddRange(QueuedSteps); + QueuedSteps.Clear(); + } + } + } + + public void RecalculateState() + { + var sim = CreateSim(); + var lastState = InitialState; + for (var i = 0; i < Macro.Count; i++) + lastState = Macro[i].Recalculate(sim, lastState); + } + + private static Sim CreateSim() => + Service.Configuration.ConditionRandomness ? new Sim() : new SimNoRandom(); +} diff --git a/Craftimizer/Windows/MacroEditor.cs b/Craftimizer/Windows/MacroEditor.cs index 2aae8f5..c23bdc1 100644 --- a/Craftimizer/Windows/MacroEditor.cs +++ b/Craftimizer/Windows/MacroEditor.cs @@ -77,125 +77,16 @@ public sealed class MacroEditor : Window, IDisposable private List HQIngredientCounts { get; set; } private int StartingQuality => RecipeData.CalculateStartingQuality(HQIngredientCounts); - private readonly record struct SimulationReliablity - { - public sealed class ParamReliability - { - private List DataList { get; } - private ImGuiUtils.ViolinData? ViolinData { get; set; } + private SimulatedMacro Macro { get; set; } = new(); + private SimulationState State => Macro.State; + private SimulatedMacro.Reliablity Reliability => Macro.GetReliability(RecipeData); - public int Max { get; private set; } - public int Min { get; private set; } - public float Median { get; private set; } - public float Average { get; private set; } - - public ParamReliability() - { - DataList = new(); - } - - public void Add(int value) - { - DataList.Add(value); - } - - public void FinalizeData() - { - if (DataList.Count == 0) - { - Average = Median = Max = Min = 0; - return; - } - - Max = DataList.Max(); - Min = DataList.Min(); - if (DataList.Count % 2 == 0) - Median = (float)DataList.Order().Skip(DataList.Count / 2 - 1).Take(2).Average(); - else - Median = DataList.Order().ElementAt(DataList.Count / 2); - Average = (float)DataList.Average(); - } - - public ImGuiUtils.ViolinData? GetViolinData(float barMax, int resolution, double bandwidth) => - ViolinData ??= - Min != Max ? - new(DataList, 0, barMax, resolution, bandwidth) : - null; - } - - public readonly ParamReliability Progress = new(); - public readonly ParamReliability Quality = new(); - - // Param is either collectability, quality, or hq%, depending on the recipe - public readonly ParamReliability Param = new(); - - public SimulationReliablity(in SimulationState startState, IEnumerable actions, int iterCount, RecipeData recipeData) - { - Func getParam; - if (recipeData.Recipe.ItemResult.Value!.IsCollectable) - getParam = s => s.Collectability; - else if (recipeData.Recipe.RequiredQuality > 0) - { - var reqQual = recipeData.Recipe.RequiredQuality; - getParam = s => (int)((float)s.Quality / reqQual * 100); - } - else if (recipeData.RecipeInfo.MaxQuality > 0) - getParam = s => s.HQPercent; - else - getParam = s => 0; - - for (var i = 0; i < iterCount; ++i) - { - var sim = new Sim(); - var (_, state, _) = sim.ExecuteMultiple(startState, actions); - Progress.Add(state.Progress); - Quality.Add(state.Quality); - Param.Add(getParam(state)); - } - Progress.FinalizeData(); - Quality.FinalizeData(); - Param.FinalizeData(); - } - } - - private sealed record SimulatedActionStep - { - public ActionType Action { get; } - // State *after* executing the action - public ActionResponse Response { get; private set; } - public SimulationState State { get; private set; } - private SimulationReliablity? Reliability { get; set; } - - public SimulatedActionStep(ActionType action, Sim sim, in SimulationState lastState, out SimulationState newState) - { - Action = action; - newState = Recalculate(sim, lastState); - } - - public SimulationState Recalculate(Sim sim, in SimulationState lastState) - { - (Response, State) = sim.Execute(lastState, Action); - Reliability = null; - return State; - } - - public SimulationReliablity GetReliability(in SimulationState initialState, IEnumerable actionSet, RecipeData recipeData) => - Reliability ??= - new(initialState, actionSet, Service.Configuration.ReliabilitySimulationCount, recipeData); - }; - - private List Macro { get; set; } = new(); - private SimulationState InitialState { get; set; } - private SimulationState State => Macro.Count > 0 ? Macro[^1].State : InitialState; - private SimulationReliablity Reliability => Macro.Count > 0 ? Macro[^1].GetReliability(InitialState, Macro.Select(m => m.Action), RecipeData) : new(InitialState, Array.Empty(), 0, RecipeData); private ActionType[] DefaultActions { get; } private Action>? MacroSetter { get; set; } private CancellationTokenSource? SolverTokenSource { get; set; } private Exception? SolverException { get; set; } private int? SolverStartStepCount { get; set; } - private object? SolverQueueLock { get; set; } - private List? SolverQueuedSteps { get; set; } private Solver.Solver? SolverObject { get; set; } private bool SolverRunning => SolverTokenSource != null; @@ -263,7 +154,7 @@ public sealed class MacroEditor : Window, IDisposable public override void Update() { - TryFlushSolvedSteps(); + Macro.FlushQueue(); } public override void Draw() @@ -1100,14 +991,14 @@ public sealed class MacroEditor : Window, IDisposable new("Condition", default, null, 0, 0, null, State.Condition) }; if (RecipeData.Recipe.ItemResult.Value!.IsCollectable) - datas.Add(new("Collectability", Colors.HQ, Reliability.Param, State.Collectability, State.MaxCollectability, $"{State.Collectability}", null)); + datas.Add(new("Collectability", Colors.HQ, Reliability.ParamScore, State.Collectability, State.MaxCollectability, $"{State.Collectability}", null)); else if (RecipeData.Recipe.RequiredQuality > 0) { var qualityPercent = (float)State.Quality / RecipeData.Recipe.RequiredQuality * 100; - datas.Add(new("Quality %%", Colors.HQ, Reliability.Param, qualityPercent, 100, $"{qualityPercent:0}%", null)); + datas.Add(new("Quality %%", Colors.HQ, Reliability.ParamScore, qualityPercent, 100, $"{qualityPercent:0}%", null)); } else if (RecipeData.RecipeInfo.MaxQuality > 0) - datas.Add(new("HQ %%", Colors.HQ, Reliability.Param, State.HQPercent, 100, $"{State.HQPercent}%", null)); + datas.Add(new("HQ %%", Colors.HQ, Reliability.ParamScore, State.HQPercent, 100, $"{State.HQPercent}%", null)); DrawBars(datas); ImGui.TableNextColumn(); @@ -1162,7 +1053,7 @@ public sealed class MacroEditor : Window, IDisposable } } - private readonly record struct BarData(string Name, Vector4 Color, SimulationReliablity.ParamReliability? Reliability, float Value, float Max, string? Caption, Condition? Condition); + private readonly record struct BarData(string Name, Vector4 Color, SimulatedMacro.Reliablity.Param? Reliability, float Value, float Max, string? Caption, Condition? Condition); private void DrawBars(IEnumerable bars) { var spacing = ImGui.GetStyle().ItemSpacing.X; @@ -1264,7 +1155,7 @@ public sealed class MacroEditor : Window, IDisposable { var spacing = ImGui.GetStyle().ItemSpacing.X; var imageSize = ImGui.GetFrameHeight() * 2; - var lastState = InitialState; + var lastState = Macro.InitialState; using var panel = ImRaii2.GroupPanel("Macro", -1, out var availSpace); ImGui.Dummy(new(0, imageSize)); @@ -1378,7 +1269,7 @@ public sealed class MacroEditor : Window, IDisposable } ImGui.SameLine(); if (ImGuiUtils.IconButtonSquare(FontAwesomeIcon.Paste)) - Service.Plugin.CopyMacro(Macro.Select(s => s.Action).ToArray()); + Service.Plugin.CopyMacro(Macro.Actions.ToArray()); if (ImGui.IsItemHovered()) ImGui.SetTooltip("Copy to Clipboard"); ImGui.SameLine(); @@ -1438,7 +1329,7 @@ public sealed class MacroEditor : Window, IDisposable { if (!string.IsNullOrWhiteSpace(popupSaveAsMacroName)) { - var newMacro = new Macro() { Name = popupSaveAsMacroName, Actions = Macro.Select(s => s.Action).ToArray() }; + var newMacro = new Macro() { Name = popupSaveAsMacroName, Actions = Macro.Actions.ToArray() }; Service.Configuration.AddMacro(newMacro); MacroSetter = actions => { @@ -1606,16 +1497,7 @@ public sealed class MacroEditor : Window, IDisposable SolverTokenSource?.Cancel(); SolverTokenSource = new(); SolverException = null; - if (SolverQueueLock is { }) - { - lock (SolverQueueLock) - { - SolverQueuedSteps!.Clear(); - SolverQueueLock = null; - } - } - SolverQueueLock = new(); - SolverQueuedSteps ??= new(); + Macro.ClearQueue(); RevertPreviousMacro(); @@ -1665,7 +1547,7 @@ public sealed class MacroEditor : Window, IDisposable using (SolverObject = new Solver.Solver(config, state) { Token = token }) { SolverObject.OnLog += Log.Debug; - SolverObject.OnNewAction += QueueSolverStep; + SolverObject.OnNewAction += Macro.Enqueue; SolverObject.Start(); _ = SolverObject.GetTask().GetAwaiter().GetResult(); } @@ -1681,95 +1563,34 @@ public sealed class MacroEditor : Window, IDisposable private void SaveMacro() { - MacroSetter?.Invoke(Macro.Select(s => s.Action)); + MacroSetter?.Invoke(Macro.Actions); } private void RecalculateState() { - InitialState = new SimulationState(new(CharacterStats, RecipeData.RecipeInfo, StartingQuality)); - var sim = CreateSim(); - var lastState = InitialState; - for (var i = 0; i < Macro.Count; i++) - lastState = Macro[i].Recalculate(sim, lastState); + Macro.InitialState = new SimulationState(new(CharacterStats, RecipeData.RecipeInfo, StartingQuality)); } - private static Sim CreateSim() => - Service.Configuration.ConditionRandomness ? new Sim() : new SimNoRandom(); - private static Sim CreateSim(in SimulationState state) => Service.Configuration.ConditionRandomness ? new Sim() { State = state } : new SimNoRandom() { State = state }; - private void AddStep(ActionType action, int index = -1) + private void AddStep(ActionType action) { - if (index < -1 || index >= Macro.Count) - throw new ArgumentOutOfRangeException(nameof(index)); if (SolverRunning) throw new InvalidOperationException("Cannot add steps while solver is running"); if (!SolverRunning) SolverStartStepCount = null; - if (index == -1) - { - var sim = CreateSim(); - Macro.Add(new(action, sim, State, out _)); - } - else - { - var state = index == 0 ? InitialState : Macro[index - 1].State; - var sim = CreateSim(); - Macro.Insert(index, new(action, sim, state, out state)); - - for (var i = index + 1; i < Macro.Count; i++) - state = Macro[i].Recalculate(sim, state); - } - } - - private void QueueSolverStep(ActionType action) - { - if (!SolverRunning) - throw new InvalidOperationException("Cannot queue steps while solver isn't running"); - lock (SolverQueueLock!) - { - var lastState = SolverQueuedSteps!.Count > 0 ? SolverQueuedSteps[^1].State : State; - SolverQueuedSteps.Add(new(action, CreateSim(), lastState, out _)); - } - } - - private void TryFlushSolvedSteps() - { - if (SolverQueueLock == null) - return; - - lock (SolverQueueLock!) - { - if (SolverQueuedSteps!.Count > 0) - { - Macro.AddRange(SolverQueuedSteps); - SolverQueuedSteps.Clear(); - } - - if (!SolverRunning) - { - SolverQueuedSteps.Clear(); - SolverQueueLock = null; - } - } + Macro.Add(action); } private void RemoveStep(int index) { - if (index < 0 || index >= Macro.Count) - throw new ArgumentOutOfRangeException(nameof(index)); if (SolverRunning) throw new InvalidOperationException("Cannot remove steps while solver is running"); SolverStartStepCount = null; Macro.RemoveAt(index); - - var state = index == 0 ? InitialState : Macro[index - 1].State; - var sim = CreateSim(); - for (var i = index; i < Macro.Count; i++) - state = Macro[i].Recalculate(sim, state); } public void Dispose()