From c471d3edf8f85d08db3be6a798197fb685006901 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Fri, 21 Jul 2023 20:44:15 +0400 Subject: [PATCH] Split synth helper, use ienumerator to manage state --- Craftimizer/Windows/Craft.cs | 282 +---------------------------- Craftimizer/Windows/CraftAddon.cs | 157 ++++++++++++++++ Craftimizer/Windows/CraftSolver.cs | 96 ++++++++++ Craftimizer/Windows/CraftState.cs | 81 +++++++++ 4 files changed, 344 insertions(+), 272 deletions(-) create mode 100644 Craftimizer/Windows/CraftAddon.cs create mode 100644 Craftimizer/Windows/CraftSolver.cs create mode 100644 Craftimizer/Windows/CraftState.cs diff --git a/Craftimizer/Windows/Craft.cs b/Craftimizer/Windows/Craft.cs index 74e1f2f..7c1cdfb 100644 --- a/Craftimizer/Windows/Craft.cs +++ b/Craftimizer/Windows/Craft.cs @@ -1,30 +1,14 @@ using Craftimizer.Plugin.Utils; -using Craftimizer.Simulator; -using Craftimizer.Simulator.Actions; using Craftimizer.Utils; -using Dalamud.Game; -using Dalamud.Game.Text.SeStringHandling; using Dalamud.Interface.Windowing; -using Dalamud.Logging; -using Dalamud.Memory; using FFXIVClientStructs.FFXIV.Client.Game; -using FFXIVClientStructs.FFXIV.Client.Game.Character; -using FFXIVClientStructs.FFXIV.Client.UI; -using FFXIVClientStructs.FFXIV.Component.GUI; using ImGuiNET; using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; using System.Numerics; -using System.Threading; -using System.Threading.Tasks; -using ActionType = Craftimizer.Simulator.Actions.ActionType; -using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType; namespace Craftimizer.Plugin.Windows; -public sealed unsafe class Craft : Window, IDisposable +public sealed unsafe partial class Craft : Window, IDisposable { private const ImGuiWindowFlags WindowFlags = ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.AlwaysAutoResize @@ -39,26 +23,6 @@ public sealed unsafe class Craft : Window, IDisposable private bool WasOpen { get; set; } - private CharacterStats CharacterStats { get; set; } = null!; - private SimulationInput Input { get; set; } = null!; - private int ActionCount { get; set; } - private ActionStates ActionStates { get; set; } - - // Set to true if we used an action, but it's not reflected in the addon yet - private bool IsIntermediate { get; set; } - private SimulationState IntermediateState { get; set; } - - private SimulationState? SolverState { get; set; } - private Task? SolverTask { get; set; } - private CancellationTokenSource? SolverTaskToken { get; set; } - private ConcurrentQueue SolverActionQueue { get; } = new(); - - // State is the state of the simulation *after* its corresponding action is executed. - private List<(ActionType Action, string Tooltip, ActionResponse Response, SimulationState State)> SolverActions { get; } = new(); - private SimulatorNoRandom SolverSim { get; set; } = null!; - - private SimulationState SolverLatestState => SolverActions.Count == 0 ? SolverState!.Value : SolverActions[^1].State; - public Craft() : base("Craftimizer SynthesisHelper", WindowFlags, true) { Service.WindowSystem.AddWindow(this); @@ -69,15 +33,16 @@ public sealed unsafe class Craft : Window, IDisposable public override void Draw() { - while (SolverActionQueue.TryDequeue(out var poppedAction)) - AppendGeneratedAction(poppedAction); + SolveTick(); + DequeueSolver(); DrawActions(); + ImGui.SameLine(0, 0); ImGui.Dummy(default); - ImGui.BeginDisabled(!(SolverTask?.IsCompleted ?? true) || IsIntermediate); + ImGui.BeginDisabled(!(SolverTask?.IsCompleted ?? true)); if (ImGui.Button("Retry")) - QueueSolve(CreateSimulationState()); + QueueSolve(GetNextState()!.Value); ImGui.EndDisabled(); } @@ -95,7 +60,7 @@ public sealed unsafe class Craft : Window, IDisposable ImGui.SameLine(0, 0); for (var i = 0; i < SolverActions.Count; ++i) { - var (action, tooltip, _, state) = SolverActions[i]; + var (action, tooltip, state) = SolverActions[i]; ImGui.PushID(i); if (ImGui.ImageButton(action.GetIcon(RecipeUtils.ClassJob).ImGuiHandle, actionSize, Vector2.Zero, Vector2.One, 0)) { @@ -139,18 +104,6 @@ public sealed unsafe class Craft : Window, IDisposable if (Input == null) return; - var addonState = CreateSimulationState(); - if (IsIntermediate) - { - if (StatesEqualExceptSome(addonState, IntermediateState)) - return; - - IsIntermediate = false; - } - - if (SolverState != addonState) - QueueSolve(addonState); - base.PreDraw(); } @@ -159,6 +112,9 @@ public sealed unsafe class Craft : Window, IDisposable if (!RecipeUtils.HasValidRecipe) return false; + if (!RecipeUtils.IsCrafting) + return false; + if (RecipeUtils.AddonSynthesis == null) return false; @@ -185,64 +141,6 @@ public sealed unsafe class Craft : Window, IDisposable return ret; } - private void StopSolve() - { - if (SolverTask == null || SolverTaskToken == null) - return; - - if (!SolverTask.IsCompleted) - SolverTaskToken.Cancel(); - else - { - SolverTaskToken.Dispose(); - SolverTask.Dispose(); - - SolverTask = null; - SolverTaskToken = null; - } - } - - private void QueueSolve(SimulationState state) - { - StopSolve(); - - SolverActionQueue.Clear(); - SolverActions.Clear(); - SolverState = state; - SolverSim = new(state); - - SolverTaskToken = new(); - SolverTask = Task.Run(() => Config.SolverAlgorithm.Invoke(Config.SolverConfig, state, SolverActionQueue.Enqueue, SolverTaskToken.Token)); - } - - private void AppendGeneratedAction(ActionType action) - { - var actionBase = action.Base(); - if (actionBase is BaseComboAction comboActionBase) - { - AppendGeneratedAction(comboActionBase.ActionTypeA); - AppendGeneratedAction(comboActionBase.ActionTypeB); - } - else - { - if (SolverActions.Count >= Config.SynthesisHelperStepCount) - { - StopSolve(); - return; - } - - var tooltip = actionBase.GetTooltip(SolverSim, false); - var (response, state) = SolverSim.Execute(SolverLatestState, action); - SolverActions.Add((action, tooltip, response, state)); - - if (SolverActions.Count >= Config.SynthesisHelperStepCount) - { - StopSolve(); - return; - } - } - } - private void ResetSimulation() { var container = InventoryManager.Instance()->GetInventoryContainer(InventoryType.EquippedItems); @@ -253,166 +151,6 @@ public sealed unsafe class Craft : Window, IDisposable Input = new(CharacterStats, RecipeUtils.Info, 0, Random); ActionCount = 0; ActionStates = new(); - IsIntermediate = false; - } - - private sealed class AddonValues - { - public AddonSynthesis* Addon { get; } - public AtkValue* Values => Addon->AtkUnitBase.AtkValues; - public ushort ValueCount => Addon->AtkUnitBase.AtkValuesCount; - - public AddonValues(AddonSynthesis* addon) - { - Addon = addon; - if (ValueCount != 26) - throw new ArgumentException("AddonSynthesis must have 26 AtkValues", nameof(addon)); - } - - public unsafe AtkValue* this[int i] => Values + i; - - // Always 0? - private uint Unk0 => GetUInt(0); - // Always true? - private bool Unk1 => GetBool(1); - - public SeString ItemName => GetString(2); - public uint ItemIconId => GetUInt(3); - public uint ItemCount => GetUInt(4); - public uint Progress => GetUInt(5); - public uint MaxProgress => GetUInt(6); - public uint Durability => GetUInt(7); - public uint MaxDurability => GetUInt(8); - public uint Quality => GetUInt(9); - public uint HQChance => GetUInt(10); - private uint IsShowingCollectibleInfoValue => GetUInt(11); - private uint ConditionValue => GetUInt(12); - public SeString ConditionName => GetString(13); - public SeString ConditionNameAndTooltip => GetString(14); - public uint StepCount => GetUInt(15); - public uint ResultItemId => GetUInt(16); - public uint MaxQuality => GetUInt(17); - public uint RequiredQuality => GetUInt(18); - private uint IsCollectibleValue => GetUInt(19); - public uint Collectability => GetUInt(20); - public uint MaxCollectability => GetUInt(21); - public uint CollectabilityCheckpoint1 => GetUInt(22); - public uint CollectabilityCheckpoint2 => GetUInt(23); - public uint CollectabilityCheckpoint3 => GetUInt(24); - public bool IsExpertRecipe => GetBool(25); - - public bool IsShowingCollectibleInfo => IsShowingCollectibleInfoValue != 0; - public Condition Condition => (Condition)(1 << (int)ConditionValue); - public bool IsCollectible => IsCollectibleValue != 0; - - private uint GetUInt(int i) - { - var value = this[i]; - return value->Type == ValueType.UInt ? - value->UInt : - throw new ArgumentException($"Value {i} is not a uint", nameof(i)); - } - - private bool GetBool(int i) - { - var value = this[i]; - return value->Type == ValueType.Bool ? - value->Byte != 0 : - throw new ArgumentException($"Value {i} is not a boolean", nameof(i)); - } - - private SeString GetString(int i) - { - var value = this[i]; - return value->Type switch - { - ValueType.AllocatedString or - ValueType.String => - MemoryHelper.ReadSeStringNullTerminated((nint)value->String), - _ => throw new ArgumentException($"Value {i} is not a string", nameof(i)) - }; - } - } - - private const ushort StatusInnerQuiet = 251; - private const ushort StatusWasteNot = 252; - private const ushort StatusVeneration = 2226; - private const ushort StatusGreatStrides = 254; - private const ushort StatusInnovation = 2189; - private const ushort StatusFinalAppraisal = 2190; - private const ushort StatusWasteNot2 = 257; - private const ushort StatusMuscleMemory = 2191; - private const ushort StatusManipulation = 1164; - private const ushort StatusHeartAndSoul = 2665; - - private SimulationState CreateSimulationState() - { - var player = Service.ClientState.LocalPlayer!; - var values = new AddonValues(RecipeUtils.AddonSynthesis); - var statusManager = ((Character*)player.Address)->GetStatusManager(); - - byte GetEffectStack(ushort id) - { - foreach (var status in statusManager->StatusSpan) - if (status.StatusID == id) - return status.StackCount; - return 0; - } - bool HasEffect(ushort id) - { - foreach (var status in statusManager->StatusSpan) - if (status.StatusID == id) - return true; - return false; - } - - return new(Input) - { - ActionCount = ActionCount, - StepCount = (int)values.StepCount - 1, - Progress = (int)values.Progress, - Quality = (int)values.Quality, - Durability = (int)values.Durability, - CP = (int)player.CurrentCp, - Condition = values.Condition, - ActiveEffects = new() - { - InnerQuiet = GetEffectStack(StatusInnerQuiet), - WasteNot = GetEffectStack(StatusWasteNot), - Veneration = GetEffectStack(StatusVeneration), - GreatStrides = GetEffectStack(StatusGreatStrides), - Innovation = GetEffectStack(StatusInnovation), - FinalAppraisal = GetEffectStack(StatusFinalAppraisal), - WasteNot2 = GetEffectStack(StatusWasteNot2), - MuscleMemory = GetEffectStack(StatusMuscleMemory), - Manipulation = GetEffectStack(StatusManipulation), - HeartAndSoul = HasEffect(StatusHeartAndSoul), - }, - ActionStates = ActionStates - }; - } - - private void OnActionUsed(ActionType action) - { - if (RecipeUtils.AddonSynthesis == null) - return; - var inGameState = CreateSimulationState(); - (_, var predictedState) = new SimulatorNoRandom(inGameState).Execute(inGameState, action); - QueueSolve(predictedState); - - ActionCount++; - var states = ActionStates; - states.MutateState(action.Base()); - ActionStates = states; - IsIntermediate = true; - IntermediateState = CreateSimulationState(); - } - - private static bool StatesEqualExceptSome(SimulationState a, SimulationState b) - { - b.CP = a.CP; - b.ActiveEffects = a.ActiveEffects; - return a == b; } public void Dispose() diff --git a/Craftimizer/Windows/CraftAddon.cs b/Craftimizer/Windows/CraftAddon.cs new file mode 100644 index 0000000..57cd5be --- /dev/null +++ b/Craftimizer/Windows/CraftAddon.cs @@ -0,0 +1,157 @@ +using Craftimizer.Simulator; +using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Interface.Windowing; +using Dalamud.Memory; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.UI; +using FFXIVClientStructs.FFXIV.Component.GUI; +using System; +using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType; + +namespace Craftimizer.Plugin.Windows; + +public sealed unsafe partial class Craft : Window, IDisposable +{ + // State variables, manually kept track of outside of the addon + private CharacterStats CharacterStats = null!; + private SimulationInput Input = null!; + private int ActionCount; + private ActionStates ActionStates; + + private sealed class AddonValues + { + public AddonSynthesis* Addon { get; } + public AtkValue* Values => Addon->AtkUnitBase.AtkValues; + public ushort ValueCount => Addon->AtkUnitBase.AtkValuesCount; + + public AddonValues(AddonSynthesis* addon) + { + Addon = addon; + if (ValueCount != 26) + throw new ArgumentException("AddonSynthesis must have 26 AtkValues", nameof(addon)); + } + + public unsafe AtkValue* this[int i] => Values + i; + + // Always 0? + private uint Unk0 => GetUInt(0); + // Always true? + private bool Unk1 => GetBool(1); + + public SeString ItemName => GetString(2); + public uint ItemIconId => GetUInt(3); + public uint ItemCount => GetUInt(4); + public uint Progress => GetUInt(5); + public uint MaxProgress => GetUInt(6); + public uint Durability => GetUInt(7); + public uint MaxDurability => GetUInt(8); + public uint Quality => GetUInt(9); + public uint HQChance => GetUInt(10); + private uint IsShowingCollectibleInfoValue => GetUInt(11); + private uint ConditionValue => GetUInt(12); + public SeString ConditionName => GetString(13); + public SeString ConditionNameAndTooltip => GetString(14); + public uint StepCount => GetUInt(15); + public uint ResultItemId => GetUInt(16); + public uint MaxQuality => GetUInt(17); + public uint RequiredQuality => GetUInt(18); + private uint IsCollectibleValue => GetUInt(19); + public uint Collectability => GetUInt(20); + public uint MaxCollectability => GetUInt(21); + public uint CollectabilityCheckpoint1 => GetUInt(22); + public uint CollectabilityCheckpoint2 => GetUInt(23); + public uint CollectabilityCheckpoint3 => GetUInt(24); + public bool IsExpertRecipe => GetBool(25); + + public bool IsShowingCollectibleInfo => IsShowingCollectibleInfoValue != 0; + public Condition Condition => (Condition)(1 << (int)ConditionValue); + public bool IsCollectible => IsCollectibleValue != 0; + + private uint GetUInt(int i) + { + var value = this[i]; + return value->Type == ValueType.UInt ? + value->UInt : + throw new ArgumentException($"Value {i} is not a uint", nameof(i)); + } + + private bool GetBool(int i) + { + var value = this[i]; + return value->Type == ValueType.Bool ? + value->Byte != 0 : + throw new ArgumentException($"Value {i} is not a boolean", nameof(i)); + } + + private SeString GetString(int i) + { + var value = this[i]; + return value->Type switch + { + ValueType.AllocatedString or + ValueType.String => + MemoryHelper.ReadSeStringNullTerminated((nint)value->String), + _ => throw new ArgumentException($"Value {i} is not a string", nameof(i)) + }; + } + } + + private const ushort StatusInnerQuiet = 251; + private const ushort StatusWasteNot = 252; + private const ushort StatusVeneration = 2226; + private const ushort StatusGreatStrides = 254; + private const ushort StatusInnovation = 2189; + private const ushort StatusFinalAppraisal = 2190; + private const ushort StatusWasteNot2 = 257; + private const ushort StatusMuscleMemory = 2191; + private const ushort StatusManipulation = 1164; + private const ushort StatusHeartAndSoul = 2665; + + private SimulationState GetAddonSimulationState() + { + var player = Service.ClientState.LocalPlayer!; + var values = new AddonValues(RecipeUtils.AddonSynthesis); + var statusManager = ((Character*)player.Address)->GetStatusManager(); + + byte GetEffectStack(ushort id) + { + foreach (var status in statusManager->StatusSpan) + if (status.StatusID == id) + return status.StackCount; + return 0; + } + bool HasEffect(ushort id) + { + foreach (var status in statusManager->StatusSpan) + if (status.StatusID == id) + return true; + return false; + } + + return new(Input) + { + ActionCount = ActionCount, + StepCount = (int)values.StepCount - 1, + Progress = (int)values.Progress, + Quality = (int)values.Quality, + Durability = (int)values.Durability, + CP = (int)player.CurrentCp, + Condition = values.Condition, + ActiveEffects = new() + { + InnerQuiet = GetEffectStack(StatusInnerQuiet), + WasteNot = GetEffectStack(StatusWasteNot), + Veneration = GetEffectStack(StatusVeneration), + GreatStrides = GetEffectStack(StatusGreatStrides), + Innovation = GetEffectStack(StatusInnovation), + FinalAppraisal = GetEffectStack(StatusFinalAppraisal), + WasteNot2 = GetEffectStack(StatusWasteNot2), + MuscleMemory = GetEffectStack(StatusMuscleMemory), + Manipulation = GetEffectStack(StatusManipulation), + HeartAndSoul = HasEffect(StatusHeartAndSoul), + }, + ActionStates = ActionStates + }; + } +} + diff --git a/Craftimizer/Windows/CraftSolver.cs b/Craftimizer/Windows/CraftSolver.cs new file mode 100644 index 0000000..7a7cce0 --- /dev/null +++ b/Craftimizer/Windows/CraftSolver.cs @@ -0,0 +1,96 @@ +using Craftimizer.Simulator; +using Craftimizer.Simulator.Actions; +using Dalamud.Interface.Windowing; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Craftimizer.Plugin.Windows; + +public sealed unsafe partial class Craft : Window, IDisposable +{ + private SimulationState? SolverState { get; set; } + private Task? SolverTask { get; set; } + private CancellationTokenSource? SolverTaskToken { get; set; } + private ConcurrentQueue SolverActionQueue { get; } = new(); + + // State is the state of the simulation *after* its corresponding action is executed. + private List<(ActionType Action, string Tooltip, SimulationState State)> SolverActions { get; } = new(); + private SimulatorNoRandom SolverSim { get; set; } = null!; + private SimulationState SolverLatestState => SolverActions.Count == 0 ? SolverState!.Value : SolverActions[^1].State; + + private void StopSolve() + { + if (SolverTask == null || SolverTaskToken == null) + return; + + if (!SolverTask.IsCompleted) + SolverTaskToken.Cancel(); + else + { + SolverTaskToken.Dispose(); + SolverTask.Dispose(); + + SolverTask = null; + SolverTaskToken = null; + } + } + + private void QueueSolve(SimulationState state) + { + StopSolve(); + + SolverActionQueue.Clear(); + SolverActions.Clear(); + SolverState = state; + SolverSim = new(state); + + SolverTaskToken = new(); + SolverTask = Task.Run(() => Config.SolverAlgorithm.Invoke(Config.SolverConfig, state, SolverActionQueue.Enqueue, SolverTaskToken.Token)); + } + + private void SolveTick() + { + var newState = GetNextState(); + if (SolverState == newState) + return; + + if (newState == null) + StopSolve(); + else + QueueSolve(newState.Value); + } + + private void DequeueSolver() + { + while (SolverActionQueue.TryDequeue(out var poppedAction)) + AppendSolverAction(poppedAction); + } + + private void AppendSolverAction(ActionType action) + { + var actionBase = action.Base(); + if (actionBase is BaseComboAction comboActionBase) + { + AppendSolverAction(comboActionBase.ActionTypeA); + AppendSolverAction(comboActionBase.ActionTypeB); + } + else + { + if (SolverActions.Count >= Config.SynthesisHelperStepCount) + { + StopSolve(); + return; + } + + var tooltip = actionBase.GetTooltip(SolverSim, false); + var (_, state) = SolverSim.Execute(SolverLatestState, action); + SolverActions.Add((action, tooltip, state)); + + if (SolverActions.Count >= Config.SynthesisHelperStepCount) + StopSolve(); + } + } +} diff --git a/Craftimizer/Windows/CraftState.cs b/Craftimizer/Windows/CraftState.cs new file mode 100644 index 0000000..3ba53ce --- /dev/null +++ b/Craftimizer/Windows/CraftState.cs @@ -0,0 +1,81 @@ +using Craftimizer.Simulator; +using Craftimizer.Simulator.Actions; +using Dalamud.Interface.Windowing; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; + +namespace Craftimizer.Plugin.Windows; + +public sealed unsafe partial class Craft : Window, IDisposable +{ + private ConcurrentQueue UsedActionQueue { get; set; } = new(); + private IEnumerator? StateTicker { get; set; } + + private SimulationState? GetNextState() + { + if (RecipeUtils.IsCrafting && StateTicker == null) + StateTicker = TickState(); + if (!RecipeUtils.IsCrafting && StateTicker != null) + StateTicker = null; + + if (StateTicker == null) + return null; + StateTicker.MoveNext(); + return StateTicker.Current; + } + + private IEnumerator TickState() + { + while (true) + { + SimulationState state; + + // Dequeue used actions + var sim = new SimulatorNoRandom(new()); + while (true) + { + state = GetAddonSimulationState(); + + var dequeued = false; + while (UsedActionQueue.TryDequeue(out var action)) + { + dequeued = true; + (_, state) = sim.Execute(state, action); + ActionCount++; + ActionStates.MutateState(action.Base()); + } + if (dequeued) + break; + + // If nothing is dequeued and executed, just return the addon state + yield return state; + } + + // Intermediate state, wait for addon change + var intermediateState = GetAddonSimulationState(); + while (true) + { + yield return state; + var newState = GetAddonSimulationState(); + if (!IsStateInIntermediate(newState, intermediateState)) + break; + } + } + } + + private static bool IsStateInIntermediate(SimulationState a, SimulationState b) + { + b.CP = a.CP; + b.ActiveEffects = a.ActiveEffects; + return a == b; + } + + private void OnActionUsed(ActionType action) + { + if (!RecipeUtils.IsCrafting || RecipeUtils.AddonSynthesis == null) + return; + + UsedActionQueue.Enqueue(action); + } +}