Implement synthesis helper
I feel like it's still super janky so far.. hopefully more improvements soon
This commit is contained in:
@@ -1,14 +1,30 @@
|
||||
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 unsafe class Craft : Window
|
||||
public sealed unsafe class Craft : Window, IDisposable
|
||||
{
|
||||
private const ImGuiWindowFlags WindowFlags = ImGuiWindowFlags.NoDecoration
|
||||
| ImGuiWindowFlags.AlwaysAutoResize
|
||||
@@ -16,22 +32,91 @@ public unsafe class Craft : Window
|
||||
| ImGuiWindowFlags.NoFocusOnAppearing
|
||||
| ImGuiWindowFlags.NoNavFocus;
|
||||
|
||||
private RecipeNote RecipeUtils { get; } = new();
|
||||
private static Configuration Config => Service.Configuration;
|
||||
|
||||
private static Random Random { get; } = new();
|
||||
private static RecipeNote RecipeUtils => Service.Plugin.RecipeNote;
|
||||
|
||||
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)
|
||||
{
|
||||
Service.WindowSystem.AddWindow(this);
|
||||
Service.Plugin.Hooks.OnActionUsed += OnActionUsed;
|
||||
|
||||
IsOpen = true;
|
||||
}
|
||||
|
||||
public override void Draw()
|
||||
{
|
||||
ImGui.Text($"{CharacterStats.CP};{CharacterStats.Control};{CharacterStats.Craftsmanship}");
|
||||
while (SolverActionQueue.TryDequeue(out var poppedAction))
|
||||
AppendGeneratedAction(poppedAction);
|
||||
|
||||
DrawActions();
|
||||
|
||||
ImGui.Dummy(default);
|
||||
ImGui.BeginDisabled(!(SolverTask?.IsCompleted ?? true) || IsIntermediate);
|
||||
if (ImGui.Button("Retry"))
|
||||
QueueSolve(CreateSimulationState());
|
||||
ImGui.EndDisabled();
|
||||
}
|
||||
|
||||
private void DrawActions()
|
||||
{
|
||||
var totalWidth = 300f;
|
||||
var actionsPerRow = 5;
|
||||
|
||||
var actionSize = new Vector2((totalWidth / actionsPerRow) - (ImGui.GetStyle().ItemSpacing.X * ((actionsPerRow + 1f) / actionsPerRow)));
|
||||
ImGui.PushStyleColor(ImGuiCol.Button, Vector4.Zero);
|
||||
ImGui.PushStyleColor(ImGuiCol.ButtonActive, Vector4.Zero);
|
||||
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, Vector4.Zero);
|
||||
|
||||
ImGui.Dummy(new(0, actionSize.Y));
|
||||
ImGui.SameLine(0, 0);
|
||||
for (var i = 0; i < SolverActions.Count; ++i)
|
||||
{
|
||||
var (action, tooltip, _, state) = SolverActions[i];
|
||||
ImGui.PushID(i);
|
||||
if (ImGui.ImageButton(action.GetIcon(RecipeUtils.ClassJob).ImGuiHandle, actionSize, Vector2.Zero, Vector2.One, 0))
|
||||
{
|
||||
if (i == 0)
|
||||
Chat.SendMessage($"/ac \"{action.GetName(RecipeUtils.ClassJob)}\"");
|
||||
}
|
||||
if (ImGui.IsItemHovered())
|
||||
{
|
||||
ImGui.BeginTooltip();
|
||||
ImGui.Text($"{action.GetName(RecipeUtils.ClassJob)}\n{tooltip}");
|
||||
Simulator.DrawAllProgressTooltips(state);
|
||||
if (i == 0)
|
||||
ImGui.Text("Click to Execute");
|
||||
ImGui.EndTooltip();
|
||||
}
|
||||
ImGui.PopID();
|
||||
if (i % actionsPerRow != (actionsPerRow - 1))
|
||||
ImGui.SameLine();
|
||||
}
|
||||
|
||||
ImGui.PopStyleColor(3);
|
||||
}
|
||||
|
||||
public override void PreDraw()
|
||||
@@ -42,7 +127,7 @@ public unsafe class Craft : Window
|
||||
var pos = new Vector2(unit.X, unit.Y);
|
||||
var size = new Vector2(unit.WindowNode->AtkResNode.Width, unit.WindowNode->AtkResNode.Height) * scale;
|
||||
|
||||
var node = unit.GetNodeById(5);
|
||||
var node = unit.GetNodeById(79);
|
||||
|
||||
Position = pos + new Vector2(size.X, node->Y * scale);
|
||||
SizeConstraints = new WindowSizeConstraints
|
||||
@@ -51,14 +136,29 @@ public unsafe class Craft : Window
|
||||
MaximumSize = new(10000, 10000)
|
||||
};
|
||||
|
||||
if (Input == null)
|
||||
return;
|
||||
|
||||
var addonState = CreateSimulationState();
|
||||
if (IsIntermediate)
|
||||
{
|
||||
if (StatesEqualExceptSome(addonState, IntermediateState))
|
||||
return;
|
||||
|
||||
IsIntermediate = false;
|
||||
}
|
||||
|
||||
if (SolverState != addonState)
|
||||
QueueSolve(addonState);
|
||||
|
||||
base.PreDraw();
|
||||
}
|
||||
|
||||
private bool DrawConditionsInner()
|
||||
{
|
||||
if (!RecipeUtils.Update(out _))
|
||||
if (!RecipeUtils.HasValidRecipe)
|
||||
return false;
|
||||
return false;
|
||||
|
||||
if (RecipeUtils.AddonSynthesis == null)
|
||||
return false;
|
||||
|
||||
@@ -66,11 +166,17 @@ public unsafe class Craft : Window
|
||||
if (RecipeUtils.AddonSynthesis->AtkUnitBase.WindowNode == null)
|
||||
return false;
|
||||
|
||||
if (RecipeUtils.AddonSynthesis->AtkUnitBase.GetNodeById(79) == null)
|
||||
return false;
|
||||
|
||||
return base.DrawConditions();
|
||||
}
|
||||
|
||||
public override bool DrawConditions()
|
||||
{
|
||||
if (!Config.EnableSynthesisHelper)
|
||||
return false;
|
||||
|
||||
var ret = DrawConditionsInner();
|
||||
if (ret && !WasOpen)
|
||||
ResetSimulation();
|
||||
@@ -79,6 +185,64 @@ public unsafe class Craft : Window
|
||||
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);
|
||||
@@ -86,5 +250,178 @@ public unsafe class Craft : Window
|
||||
return;
|
||||
|
||||
CharacterStats = Gearsets.CalculateCharacterStats(Gearsets.CalculateGearsetCurrentStats(), Gearsets.GetGearsetItems(container), RecipeUtils.CharacterLevel, RecipeUtils.CanUseManipulation);
|
||||
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()
|
||||
{
|
||||
StopSolve();
|
||||
SolverTask?.Wait();
|
||||
SolverTask?.Dispose();
|
||||
SolverTaskToken?.Dispose();
|
||||
|
||||
Service.Plugin.Hooks.OnActionUsed -= OnActionUsed;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user