Implement SynthHelper
This commit is contained in:
@@ -1,19 +1,29 @@
|
|||||||
using Craftimizer.Plugin;
|
using Craftimizer.Plugin;
|
||||||
using Craftimizer.Plugin.Utils;
|
using Craftimizer.Plugin.Utils;
|
||||||
using Craftimizer.Simulator;
|
using Craftimizer.Simulator;
|
||||||
|
using Craftimizer.Simulator.Actions;
|
||||||
|
using Craftimizer.Utils;
|
||||||
|
using Dalamud.Game.ClientState.Conditions;
|
||||||
|
using Dalamud.Interface.Colors;
|
||||||
|
using Dalamud.Interface.GameFonts;
|
||||||
|
using Dalamud.Interface.Utility;
|
||||||
|
using Dalamud.Interface.Utility.Raii;
|
||||||
using Dalamud.Interface.Windowing;
|
using Dalamud.Interface.Windowing;
|
||||||
|
using Dalamud.Utility;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||||
using ImGuiNET;
|
using ImGuiNET;
|
||||||
using System;
|
using System;
|
||||||
using System.Threading;
|
using System.Collections.Generic;
|
||||||
using Craftimizer.Utils;
|
using System.Linq;
|
||||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
|
||||||
using ActionType = Craftimizer.Simulator.Actions.ActionType;
|
|
||||||
using Dalamud.Interface.Utility;
|
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using Dalamud.Game.ClientState.Conditions;
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ActionType = Craftimizer.Simulator.Actions.ActionType;
|
||||||
|
using Sim = Craftimizer.Simulator.Simulator;
|
||||||
|
using SimNoRandom = Craftimizer.Simulator.SimulatorNoRandom;
|
||||||
|
|
||||||
namespace Craftimizer.Windows;
|
namespace Craftimizer.Windows;
|
||||||
|
|
||||||
@@ -46,23 +56,30 @@ public sealed unsafe class SynthHelper : Window, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
private SimulationState currentState;
|
private SimulationState currentState;
|
||||||
|
private SimulatedMacro Macro { get; } = new();
|
||||||
|
|
||||||
private CancellationTokenSource? HelperTaskTokenSource { get; set; }
|
private CancellationTokenSource? HelperTaskTokenSource { get; set; }
|
||||||
private Exception? HelperTaskException { get; set; }
|
private Exception? HelperTaskException { get; set; }
|
||||||
|
private Solver.Solver? HelperTaskObject { get; set; }
|
||||||
|
private bool HelperTaskRunning => HelperTaskTokenSource != null;
|
||||||
|
|
||||||
|
private GameFontHandle AxisFont { get; }
|
||||||
|
|
||||||
public SynthHelper() : base("Craftimizer SynthHelper", WindowFlags)
|
public SynthHelper() : base("Craftimizer SynthHelper", WindowFlags)
|
||||||
{
|
{
|
||||||
|
AxisFont = Service.PluginInterface.UiBuilder.GetGameFontHandle(new(GameFontFamilyAndSize.Axis14));
|
||||||
|
|
||||||
Service.Plugin.Hooks.OnActionUsed += OnUseAction;
|
Service.Plugin.Hooks.OnActionUsed += OnUseAction;
|
||||||
|
|
||||||
RespectCloseHotkey = false;
|
RespectCloseHotkey = false;
|
||||||
DisableWindowSounds = true;
|
DisableWindowSounds = true;
|
||||||
ShowCloseButton = false;
|
ShowCloseButton = false;
|
||||||
IsOpen = true;
|
IsOpen = true;
|
||||||
|
|
||||||
SizeConstraints = new WindowSizeConstraints
|
SizeConstraints = new WindowSizeConstraints
|
||||||
{
|
{
|
||||||
MinimumSize = new(-1),
|
MinimumSize = new(494, -1),
|
||||||
MaximumSize = new(10000, 10000)
|
MaximumSize = new(494, 10000)
|
||||||
};
|
};
|
||||||
|
|
||||||
Service.WindowSystem.AddWindow(this);
|
Service.WindowSystem.AddWindow(this);
|
||||||
@@ -89,12 +106,14 @@ public sealed unsafe class SynthHelper : Window, IDisposable
|
|||||||
else
|
else
|
||||||
IsCrafting = false;
|
IsCrafting = false;
|
||||||
|
|
||||||
|
Macro.FlushQueue();
|
||||||
|
|
||||||
var isInCraftAction = Service.Condition[ConditionFlag.Crafting40];
|
var isInCraftAction = Service.Condition[ConditionFlag.Crafting40];
|
||||||
if (!isInCraftAction && wasInCraftAction)
|
if (!isInCraftAction && wasInCraftAction)
|
||||||
OnFinishedUsingAction();
|
OnFinishedUsingAction();
|
||||||
wasInCraftAction = isInCraftAction;
|
wasInCraftAction = isInCraftAction;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool wasOpen;
|
private bool wasOpen;
|
||||||
public override bool DrawConditions()
|
public override bool DrawConditions()
|
||||||
{
|
{
|
||||||
@@ -133,15 +152,215 @@ public sealed unsafe class SynthHelper : Window, IDisposable
|
|||||||
var scale = unit.Scale;
|
var scale = unit.Scale;
|
||||||
var pos = new Vector2(unit.X, unit.Y);
|
var pos = new Vector2(unit.X, unit.Y);
|
||||||
var size = new Vector2(unit.WindowNode->AtkResNode.Width, unit.WindowNode->AtkResNode.Height) * scale;
|
var size = new Vector2(unit.WindowNode->AtkResNode.Width, unit.WindowNode->AtkResNode.Height) * scale;
|
||||||
|
|
||||||
var node = unit.GetNodeById(46);
|
|
||||||
|
|
||||||
Position = ImGuiHelpers.MainViewport.Pos + pos + new Vector2(size.X, node->Y * scale);
|
var node = unit.GetNodeById(46);
|
||||||
|
var offset = 5;
|
||||||
|
|
||||||
|
Position = ImGuiHelpers.MainViewport.Pos + pos + new Vector2(size.X, offset * scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Draw()
|
public override void Draw()
|
||||||
{
|
{
|
||||||
ImGui.Text($"{IsCrafting} {CurrentState.Progress} {CurrentState.ActionCount} {CurrentState.Condition}");
|
DrawMacro();
|
||||||
|
|
||||||
|
DrawMacroInfo();
|
||||||
|
|
||||||
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
|
|
||||||
|
DrawMacroActions();
|
||||||
|
|
||||||
|
if (HelperTaskRunning && HelperTaskObject is { } solver)
|
||||||
|
{
|
||||||
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
|
DrawHelperTaskProgress(solver);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private SimulationState? hoveredState;
|
||||||
|
private SimulationState DisplayedState => hoveredState ?? Macro.State;
|
||||||
|
private void DrawMacro()
|
||||||
|
{
|
||||||
|
var spacing = ImGui.GetStyle().ItemSpacing.X;
|
||||||
|
var imageSize = ImGui.GetFrameHeight() * 2;
|
||||||
|
var lastState = Macro.InitialState;
|
||||||
|
hoveredState = null;
|
||||||
|
|
||||||
|
var itemsPerRow = (int)Math.Max(1, MathF.Floor((ImGui.GetContentRegionAvail().X + spacing) / (imageSize + spacing)));
|
||||||
|
|
||||||
|
using var _color = ImRaii.PushColor(ImGuiCol.Button, Vector4.Zero);
|
||||||
|
using var _color3 = ImRaii.PushColor(ImGuiCol.ButtonHovered, Vector4.Zero);
|
||||||
|
using var _color2 = ImRaii.PushColor(ImGuiCol.ButtonActive, Vector4.Zero);
|
||||||
|
for (var i = 0; i < Macro.Count; i++)
|
||||||
|
{
|
||||||
|
if (i % itemsPerRow != 0)
|
||||||
|
ImGui.SameLine(0, spacing);
|
||||||
|
var (action, response, state) = (Macro[i].Action, Macro[i].Response, Macro[i].State);
|
||||||
|
var actionBase = action.Base();
|
||||||
|
var failedAction = response != ActionResponse.UsedAction;
|
||||||
|
using var id = ImRaii.PushId(i);
|
||||||
|
if (i == 0)
|
||||||
|
{
|
||||||
|
var pos = ImGui.GetCursorScreenPos();
|
||||||
|
var offset = new Vector2(3);
|
||||||
|
ImGui.GetWindowDrawList().AddRectFilled(pos - offset, pos + new Vector2(imageSize) + offset, ImGui.GetColorU32(ImGuiColors.DalamudWhite2), 4);
|
||||||
|
}
|
||||||
|
if (ImGui.ImageButton(action.GetIcon(RecipeData!.ClassJob).ImGuiHandle, new(imageSize), default, Vector2.One, 0, default, failedAction ? new(1, 1, 1, ImGui.GetStyle().DisabledAlpha) : Vector4.One))
|
||||||
|
{
|
||||||
|
Log.Debug($"Clicked {action.GetName(RecipeData.ClassJob)} [{i}]");
|
||||||
|
if (i == 0)
|
||||||
|
Chat.SendMessage($"/ac \"{action.GetName(RecipeData.ClassJob)}\"");
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
|
||||||
|
{
|
||||||
|
ImGui.SetTooltip($"{action.GetName(RecipeData!.ClassJob)}\n" +
|
||||||
|
$"{actionBase.GetTooltip(CreateSim(lastState), true)}" +
|
||||||
|
$"{(i == 0 ? "Click to Execute" : string.Empty)}");
|
||||||
|
hoveredState = state;
|
||||||
|
}
|
||||||
|
lastState = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < 2; ++i)
|
||||||
|
{
|
||||||
|
if (Macro.Count <= i * itemsPerRow)
|
||||||
|
ImGui.Dummy(new(0, imageSize));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawMacroInfo()
|
||||||
|
{
|
||||||
|
var state = DisplayedState;
|
||||||
|
|
||||||
|
using (var panel = ImRaii2.GroupPanel("Buffs", -1, out _))
|
||||||
|
{
|
||||||
|
using var _font = ImRaii.PushFont(AxisFont.ImFont);
|
||||||
|
|
||||||
|
var iconHeight = ImGui.GetFrameHeight() * 1.75f;
|
||||||
|
var durationShift = iconHeight * .2f;
|
||||||
|
|
||||||
|
ImGui.Dummy(new(0, iconHeight + ImGui.GetStyle().ItemSpacing.Y + ImGui.GetTextLineHeight() - durationShift));
|
||||||
|
ImGui.SameLine(0, 0);
|
||||||
|
|
||||||
|
var effects = state.ActiveEffects;
|
||||||
|
foreach (var effect in Enum.GetValues<EffectType>())
|
||||||
|
{
|
||||||
|
if (!effects.HasEffect(effect))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
using (var group = ImRaii.Group())
|
||||||
|
{
|
||||||
|
var icon = effect.GetIcon(effects.GetStrength(effect));
|
||||||
|
var size = new Vector2(iconHeight * icon.Width / icon.Height, iconHeight);
|
||||||
|
|
||||||
|
ImGui.Image(icon.ImGuiHandle, size);
|
||||||
|
if (!effect.IsIndefinite())
|
||||||
|
{
|
||||||
|
ImGui.SetCursorPosY(ImGui.GetCursorPosY() - durationShift);
|
||||||
|
ImGuiUtils.TextCentered($"{effects.GetDuration(effect)}", size.X);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
{
|
||||||
|
var status = effect.Status();
|
||||||
|
using var _reset = ImRaii.DefaultFont();
|
||||||
|
ImGui.SetTooltip($"{status.Name.ToDalamudString()}\n{status.Description.ToDalamudString()}");
|
||||||
|
}
|
||||||
|
ImGui.SameLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var reliability = Macro.GetReliability(RecipeData!);
|
||||||
|
{
|
||||||
|
var mainBars = new List<DynamicBars.BarData>()
|
||||||
|
{
|
||||||
|
new("Progress", Colors.Progress, reliability.Progress, state.Progress, RecipeData!.RecipeInfo.MaxProgress),
|
||||||
|
new("Quality", Colors.Quality, reliability.Quality, state.Quality, RecipeData.RecipeInfo.MaxQuality),
|
||||||
|
new("CP", Colors.CP, state.CP, CharacterStats!.CP),
|
||||||
|
};
|
||||||
|
if (RecipeData.RecipeInfo.MaxQuality <= 0)
|
||||||
|
mainBars.RemoveAt(1);
|
||||||
|
var halfBars = new List<DynamicBars.BarData>()
|
||||||
|
{
|
||||||
|
new("Durability", Colors.Durability, state.Durability, RecipeData.RecipeInfo.MaxDurability),
|
||||||
|
};
|
||||||
|
if (RecipeData.Recipe.ItemResult.Value!.IsCollectable)
|
||||||
|
halfBars.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;
|
||||||
|
halfBars.Add(new("Quality %%", Colors.HQ, reliability.ParamScore, qualityPercent, 100, $"{qualityPercent:0}%", null));
|
||||||
|
}
|
||||||
|
else if (RecipeData.RecipeInfo.MaxQuality > 0)
|
||||||
|
halfBars.Add(new("HQ %%", Colors.HQ, reliability.ParamScore, state.HQPercent, 100, $"{state.HQPercent}%", null));
|
||||||
|
|
||||||
|
if (halfBars.Count > 1)
|
||||||
|
{
|
||||||
|
var textSize = DynamicBars.GetTextSize(mainBars.Concat(halfBars));
|
||||||
|
DynamicBars.Draw(mainBars, textSize);
|
||||||
|
using var table = ImRaii.Table($"##{nameof(SynthHelper)}_halfbars", halfBars.Count, ImGuiTableFlags.NoPadOuterX | ImGuiTableFlags.SizingStretchSame);
|
||||||
|
if (table)
|
||||||
|
{
|
||||||
|
foreach (var bar in halfBars)
|
||||||
|
{
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
DynamicBars.Draw(new[] { bar });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DynamicBars.Draw(mainBars.Concat(halfBars));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawHelperTaskProgress(Solver.Solver solver)
|
||||||
|
{
|
||||||
|
var spacing = ImGui.GetStyle().ItemSpacing.X;
|
||||||
|
var availSpace = ImGui.GetContentRegionAvail().X;
|
||||||
|
|
||||||
|
var percentWidth = ImGui.CalcTextSize("100%").X;
|
||||||
|
var progressWidth = availSpace - percentWidth - spacing;
|
||||||
|
var fraction = Math.Clamp((float)solver.ProgressValue / solver.ProgressMax, 0, 1);
|
||||||
|
using (var color = ImRaii.PushColor(ImGuiCol.PlotHistogram, ImGuiColors.DalamudGrey3))
|
||||||
|
ImGui.ProgressBar(fraction, new(progressWidth, ImGui.GetFrameHeight()), string.Empty);
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip($"Solver Progress: {solver.ProgressValue} / {solver.ProgressMax}");
|
||||||
|
ImGui.SameLine(0, spacing);
|
||||||
|
ImGui.AlignTextToFramePadding();
|
||||||
|
ImGuiUtils.TextRight($"{fraction * 100:0}%", percentWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawMacroActions()
|
||||||
|
{
|
||||||
|
if (HelperTaskRunning)
|
||||||
|
{
|
||||||
|
if (HelperTaskTokenSource?.IsCancellationRequested ?? false)
|
||||||
|
{
|
||||||
|
using var _disabled = ImRaii.Disabled();
|
||||||
|
ImGui.Button("Stopping", new(-1, 0));
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip("This might could a while, sorry! Please report\n" +
|
||||||
|
"if this takes longer than a second.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (ImGui.Button("Stop", new(-1, 0)))
|
||||||
|
HelperTaskTokenSource?.Cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (ImGui.Button("Retry", new(-1, 0)))
|
||||||
|
CalculateBestMacro();
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip("Suggest a way to finish the crafting recipe.\n" +
|
||||||
|
"Results aren't perfect, and levels of success\n" +
|
||||||
|
"can vary wildly depending on the solver's settings.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui.Button("Open in Simulator", new(-1, 0)))
|
||||||
|
Service.Plugin.OpenMacroEditor(CharacterStats!, RecipeData!, new(Service.ClientState.LocalPlayer!.StatusList), Enumerable.Empty<ActionType>(), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnStartCrafting(ushort recipeId)
|
private void OnStartCrafting(ushort recipeId)
|
||||||
@@ -183,7 +402,7 @@ public sealed unsafe class SynthHelper : Window, IDisposable
|
|||||||
if (!IsCrafting)
|
if (!IsCrafting)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
(_, CurrentState) = new SimulatorNoRandom().Execute(GetCurrentState(), action);
|
(_, CurrentState) = new SimNoRandom().Execute(GetCurrentState(), action);
|
||||||
CurrentActionCount = CurrentState.ActionCount;
|
CurrentActionCount = CurrentState.ActionCount;
|
||||||
CurrentActionStates = CurrentState.ActionStates;
|
CurrentActionStates = CurrentState.ActionStates;
|
||||||
}
|
}
|
||||||
@@ -248,13 +467,80 @@ public sealed unsafe class SynthHelper : Window, IDisposable
|
|||||||
if (!IsCrafting)
|
if (!IsCrafting)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Log.Debug("state updated!");
|
Macro.Clear();
|
||||||
|
Macro.InitialState = CurrentState;
|
||||||
|
CalculateBestMacro();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void CalculateBestMacro()
|
||||||
|
{
|
||||||
|
HelperTaskTokenSource?.Cancel();
|
||||||
|
HelperTaskTokenSource = new();
|
||||||
|
HelperTaskException = null;
|
||||||
|
Macro.ClearQueue();
|
||||||
|
Macro.Clear();
|
||||||
|
|
||||||
|
if (Service.Configuration.ConditionRandomness)
|
||||||
|
{
|
||||||
|
Service.Configuration.ConditionRandomness = false;
|
||||||
|
Service.Configuration.Save();
|
||||||
|
Macro.RecalculateState();
|
||||||
|
}
|
||||||
|
|
||||||
|
var token = HelperTaskTokenSource.Token;
|
||||||
|
var state = CurrentState;
|
||||||
|
var task = Task.Run(() => CalculateBestMacroTask(state, token), token);
|
||||||
|
_ = task.ContinueWith(t =>
|
||||||
|
{
|
||||||
|
if (token == HelperTaskTokenSource.Token)
|
||||||
|
{
|
||||||
|
HelperTaskTokenSource = null;
|
||||||
|
HelperTaskObject = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
_ = task.ContinueWith(t =>
|
||||||
|
{
|
||||||
|
if (token.IsCancellationRequested)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
t.Exception!.Flatten().Handle(ex => ex is TaskCanceledException or OperationCanceledException);
|
||||||
|
}
|
||||||
|
catch (AggregateException e)
|
||||||
|
{
|
||||||
|
HelperTaskException = e;
|
||||||
|
Log.Error(e, "Calculating macro failed");
|
||||||
|
}
|
||||||
|
}, TaskContinuationOptions.OnlyOnFaulted);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CalculateBestMacroTask(SimulationState state, CancellationToken token)
|
||||||
|
{
|
||||||
|
var config = Service.Configuration.SimulatorSolverConfig;
|
||||||
|
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
using (HelperTaskObject = new Solver.Solver(config, state) { Token = token })
|
||||||
|
{
|
||||||
|
HelperTaskObject.OnLog += Log.Debug;
|
||||||
|
HelperTaskObject.OnNewAction += Macro.Enqueue;
|
||||||
|
HelperTaskObject.Start();
|
||||||
|
_ = HelperTaskObject.GetTask().GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Sim CreateSim(in SimulationState state) =>
|
||||||
|
Service.Configuration.ConditionRandomness ? new Sim() { State = state } : new SimNoRandom() { State = state };
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Service.Plugin.Hooks.OnActionUsed -= OnUseAction;
|
Service.Plugin.Hooks.OnActionUsed -= OnUseAction;
|
||||||
|
|
||||||
Service.WindowSystem.RemoveWindow(this);
|
Service.WindowSystem.RemoveWindow(this);
|
||||||
|
|
||||||
|
AxisFont.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user