From a1cdbdf5fcea804e87e8dfc7ff2b85bd7730b0ac Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Wed, 15 Nov 2023 02:53:31 -0800 Subject: [PATCH] Implement SynthHelper --- Craftimizer/Windows/SynthHelper.cs | 318 +++++++++++++++++++++++++++-- 1 file changed, 302 insertions(+), 16 deletions(-) diff --git a/Craftimizer/Windows/SynthHelper.cs b/Craftimizer/Windows/SynthHelper.cs index 40c8680..66637f9 100644 --- a/Craftimizer/Windows/SynthHelper.cs +++ b/Craftimizer/Windows/SynthHelper.cs @@ -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.Collections.Generic; +using System.Linq; +using System.Numerics; using System.Threading; -using Craftimizer.Utils; -using FFXIVClientStructs.FFXIV.Client.UI.Agent; -using FFXIVClientStructs.FFXIV.Client.Game.Character; -using FFXIVClientStructs.FFXIV.Client.Game; +using System.Threading.Tasks; using ActionType = Craftimizer.Simulator.Actions.ActionType; -using Dalamud.Interface.Utility; -using System.Numerics; -using Dalamud.Game.ClientState.Conditions; +using Sim = Craftimizer.Simulator.Simulator; +using SimNoRandom = Craftimizer.Simulator.SimulatorNoRandom; namespace Craftimizer.Windows; @@ -46,23 +56,30 @@ private SimulationState CurrentState } } 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; DisableWindowSounds = true; ShowCloseButton = false; IsOpen = true; - + SizeConstraints = new WindowSizeConstraints { - MinimumSize = new(-1), - MaximumSize = new(10000, 10000) + MinimumSize = new(494, -1), + MaximumSize = new(494, 10000) }; Service.WindowSystem.AddWindow(this); @@ -89,12 +106,14 @@ public override void Update() else IsCrafting = false; + Macro.FlushQueue(); + var isInCraftAction = Service.Condition[ConditionFlag.Crafting40]; if (!isInCraftAction && wasInCraftAction) OnFinishedUsingAction(); wasInCraftAction = isInCraftAction; } - + private bool wasOpen; public override bool DrawConditions() { @@ -133,15 +152,215 @@ public override void PreDraw() var scale = unit.Scale; 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(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()) + { + 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() + { + 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() + { + 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(), null); } private void OnStartCrafting(ushort recipeId) @@ -183,7 +402,7 @@ private void OnUseAction(ActionType action) 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 @@ private void OnStateUpdated() 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(); } }