Implement SynthHelper
This commit is contained in:
@@ -1,19 +1,29 @@
|
||||
using Craftimizer.Plugin;
|
||||
using Craftimizer.Plugin.Utils;
|
||||
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.Utility;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using ImGuiNET;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Craftimizer.Utils;
|
||||
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.Collections.Generic;
|
||||
using System.Linq;
|
||||
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;
|
||||
|
||||
@@ -46,12 +56,19 @@ public sealed unsafe class SynthHelper : Window, IDisposable
|
||||
}
|
||||
}
|
||||
private SimulationState currentState;
|
||||
private SimulatedMacro Macro { get; } = new();
|
||||
|
||||
private CancellationTokenSource? HelperTaskTokenSource { 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)
|
||||
{
|
||||
AxisFont = Service.PluginInterface.UiBuilder.GetGameFontHandle(new(GameFontFamilyAndSize.Axis14));
|
||||
|
||||
Service.Plugin.Hooks.OnActionUsed += OnUseAction;
|
||||
|
||||
RespectCloseHotkey = false;
|
||||
@@ -61,8 +78,8 @@ public sealed unsafe class SynthHelper : Window, IDisposable
|
||||
|
||||
SizeConstraints = new WindowSizeConstraints
|
||||
{
|
||||
MinimumSize = new(-1),
|
||||
MaximumSize = new(10000, 10000)
|
||||
MinimumSize = new(494, -1),
|
||||
MaximumSize = new(494, 10000)
|
||||
};
|
||||
|
||||
Service.WindowSystem.AddWindow(this);
|
||||
@@ -89,6 +106,8 @@ public sealed unsafe class SynthHelper : Window, IDisposable
|
||||
else
|
||||
IsCrafting = false;
|
||||
|
||||
Macro.FlushQueue();
|
||||
|
||||
var isInCraftAction = Service.Condition[ConditionFlag.Crafting40];
|
||||
if (!isInCraftAction && wasInCraftAction)
|
||||
OnFinishedUsingAction();
|
||||
@@ -135,13 +154,213 @@ public sealed unsafe class SynthHelper : Window, IDisposable
|
||||
var size = new Vector2(unit.WindowNode->AtkResNode.Width, unit.WindowNode->AtkResNode.Height) * scale;
|
||||
|
||||
var node = unit.GetNodeById(46);
|
||||
var offset = 5;
|
||||
|
||||
Position = ImGuiHelpers.MainViewport.Pos + pos + new Vector2(size.X, node->Y * scale);
|
||||
Position = ImGuiHelpers.MainViewport.Pos + pos + new Vector2(size.X, offset * scale);
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -183,7 +402,7 @@ public sealed unsafe class SynthHelper : Window, IDisposable
|
||||
if (!IsCrafting)
|
||||
return;
|
||||
|
||||
(_, CurrentState) = new SimulatorNoRandom().Execute(GetCurrentState(), action);
|
||||
(_, CurrentState) = new SimNoRandom().Execute(GetCurrentState(), action);
|
||||
CurrentActionCount = CurrentState.ActionCount;
|
||||
CurrentActionStates = CurrentState.ActionStates;
|
||||
}
|
||||
@@ -248,13 +467,80 @@ public sealed unsafe class SynthHelper : Window, IDisposable
|
||||
if (!IsCrafting)
|
||||
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()
|
||||
{
|
||||
Service.Plugin.Hooks.OnActionUsed -= OnUseAction;
|
||||
|
||||
Service.WindowSystem.RemoveWindow(this);
|
||||
|
||||
AxisFont.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user