Split synth helper, use ienumerator to manage state
This commit is contained in:
+10
-272
@@ -1,30 +1,14 @@
|
|||||||
using Craftimizer.Plugin.Utils;
|
using Craftimizer.Plugin.Utils;
|
||||||
using Craftimizer.Simulator;
|
|
||||||
using Craftimizer.Simulator.Actions;
|
|
||||||
using Craftimizer.Utils;
|
using Craftimizer.Utils;
|
||||||
using Dalamud.Game;
|
|
||||||
using Dalamud.Game.Text.SeStringHandling;
|
|
||||||
using Dalamud.Interface.Windowing;
|
using Dalamud.Interface.Windowing;
|
||||||
using Dalamud.Logging;
|
|
||||||
using Dalamud.Memory;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
|
||||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
|
||||||
using ImGuiNET;
|
using ImGuiNET;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Numerics;
|
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;
|
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
|
private const ImGuiWindowFlags WindowFlags = ImGuiWindowFlags.NoDecoration
|
||||||
| ImGuiWindowFlags.AlwaysAutoResize
|
| ImGuiWindowFlags.AlwaysAutoResize
|
||||||
@@ -39,26 +23,6 @@ public sealed unsafe class Craft : Window, IDisposable
|
|||||||
|
|
||||||
private bool WasOpen { get; set; }
|
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<ActionType> 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)
|
public Craft() : base("Craftimizer SynthesisHelper", WindowFlags, true)
|
||||||
{
|
{
|
||||||
Service.WindowSystem.AddWindow(this);
|
Service.WindowSystem.AddWindow(this);
|
||||||
@@ -69,15 +33,16 @@ public sealed unsafe class Craft : Window, IDisposable
|
|||||||
|
|
||||||
public override void Draw()
|
public override void Draw()
|
||||||
{
|
{
|
||||||
while (SolverActionQueue.TryDequeue(out var poppedAction))
|
SolveTick();
|
||||||
AppendGeneratedAction(poppedAction);
|
DequeueSolver();
|
||||||
|
|
||||||
DrawActions();
|
DrawActions();
|
||||||
|
|
||||||
|
ImGui.SameLine(0, 0);
|
||||||
ImGui.Dummy(default);
|
ImGui.Dummy(default);
|
||||||
ImGui.BeginDisabled(!(SolverTask?.IsCompleted ?? true) || IsIntermediate);
|
ImGui.BeginDisabled(!(SolverTask?.IsCompleted ?? true));
|
||||||
if (ImGui.Button("Retry"))
|
if (ImGui.Button("Retry"))
|
||||||
QueueSolve(CreateSimulationState());
|
QueueSolve(GetNextState()!.Value);
|
||||||
ImGui.EndDisabled();
|
ImGui.EndDisabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +60,7 @@ public sealed unsafe class Craft : Window, IDisposable
|
|||||||
ImGui.SameLine(0, 0);
|
ImGui.SameLine(0, 0);
|
||||||
for (var i = 0; i < SolverActions.Count; ++i)
|
for (var i = 0; i < SolverActions.Count; ++i)
|
||||||
{
|
{
|
||||||
var (action, tooltip, _, state) = SolverActions[i];
|
var (action, tooltip, state) = SolverActions[i];
|
||||||
ImGui.PushID(i);
|
ImGui.PushID(i);
|
||||||
if (ImGui.ImageButton(action.GetIcon(RecipeUtils.ClassJob).ImGuiHandle, actionSize, Vector2.Zero, Vector2.One, 0))
|
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)
|
if (Input == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var addonState = CreateSimulationState();
|
|
||||||
if (IsIntermediate)
|
|
||||||
{
|
|
||||||
if (StatesEqualExceptSome(addonState, IntermediateState))
|
|
||||||
return;
|
|
||||||
|
|
||||||
IsIntermediate = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SolverState != addonState)
|
|
||||||
QueueSolve(addonState);
|
|
||||||
|
|
||||||
base.PreDraw();
|
base.PreDraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,6 +112,9 @@ public sealed unsafe class Craft : Window, IDisposable
|
|||||||
if (!RecipeUtils.HasValidRecipe)
|
if (!RecipeUtils.HasValidRecipe)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
if (!RecipeUtils.IsCrafting)
|
||||||
|
return false;
|
||||||
|
|
||||||
if (RecipeUtils.AddonSynthesis == null)
|
if (RecipeUtils.AddonSynthesis == null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
@@ -185,64 +141,6 @@ public sealed unsafe class Craft : Window, IDisposable
|
|||||||
return ret;
|
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()
|
private void ResetSimulation()
|
||||||
{
|
{
|
||||||
var container = InventoryManager.Instance()->GetInventoryContainer(InventoryType.EquippedItems);
|
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);
|
Input = new(CharacterStats, RecipeUtils.Info, 0, Random);
|
||||||
ActionCount = 0;
|
ActionCount = 0;
|
||||||
ActionStates = new();
|
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()
|
public void Dispose()
|
||||||
|
|||||||
@@ -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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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<ActionType> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<ActionType> UsedActionQueue { get; set; } = new();
|
||||||
|
private IEnumerator<SimulationState>? 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<SimulationState> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user