diff --git a/Craftimizer/ImGuiUtils.cs b/Craftimizer/ImGuiUtils.cs index 0e34dc6..c50b7a7 100644 --- a/Craftimizer/ImGuiUtils.cs +++ b/Craftimizer/ImGuiUtils.cs @@ -8,9 +8,6 @@ namespace Craftimizer.Plugin; internal static class ImGuiUtils { - public static float ButtonHeight => - ImGui.CalcTextSize("A").Y + (ImGui.GetStyle().FramePadding.Y * 2); - private static readonly Stack<(Vector2 Min, Vector2 Max)> GroupPanelLabelStack = new(); // Adapted from https://github.com/ocornut/imgui/issues/1496#issuecomment-655048353 @@ -165,6 +162,15 @@ internal static class ImGuiUtils ImGui.SetCursorPosX(ImGui.GetCursorPos().X + (availWidth - width) / 2); } + public static void AlignMiddle(Vector2 size) + { + var availSize = ImGui.GetContentRegionAvail(); + if (availSize.X > size.X) + ImGui.SetCursorPosX(ImGui.GetCursorPos().X + (availSize.X - size.X) / 2); + if (availSize.Y > size.Y) + ImGui.SetCursorPosY(ImGui.GetCursorPos().Y + (availSize.Y - size.Y) / 2); + } + // https://stackoverflow.com/a/67855985 public static void TextCentered(string text) { @@ -172,6 +178,12 @@ internal static class ImGuiUtils ImGui.TextUnformatted(text); } + public static void TextMiddle(string text) + { + AlignMiddle(ImGui.CalcTextSize(text)); + ImGui.TextUnformatted(text); + } + public static bool ButtonCentered(string text) { AlignCentered(ImGui.CalcTextSize(text).X + ImGui.GetStyle().FramePadding.Y * 2); diff --git a/Craftimizer/Windows/Craft.cs b/Craftimizer/Windows/Craft.cs index 813b063..b3462f1 100644 --- a/Craftimizer/Windows/Craft.cs +++ b/Craftimizer/Windows/Craft.cs @@ -58,7 +58,7 @@ public sealed unsafe partial class Craft : Window, IDisposable var cogWidth = ImGui.CalcTextSize(FontAwesomeIcon.Cog.ToIconString()).X + (ImGui.GetStyle().FramePadding.X * 2); ImGui.PopFont(); - DrawSolveButton(new(WindowWidth - ImGui.GetStyle().ItemSpacing.X - cogWidth, ImGuiUtils.ButtonHeight)); + DrawSolveButton(new(WindowWidth - ImGui.GetStyle().ItemSpacing.X - cogWidth, ImGui.GetFrameHeight())); ImGui.SameLine(); if (ImGuiComponents.IconButton("synthSettingsButton", FontAwesomeIcon.Cog)) diff --git a/Craftimizer/Windows/RecipeNote.cs b/Craftimizer/Windows/RecipeNote.cs index 5c8778d..f4d4f3e 100644 --- a/Craftimizer/Windows/RecipeNote.cs +++ b/Craftimizer/Windows/RecipeNote.cs @@ -6,6 +6,7 @@ using Craftimizer.Solver; using Craftimizer.Utils; using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Interface; +using Dalamud.Interface.Colors; using Dalamud.Interface.Components; using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal; @@ -22,8 +23,11 @@ using FFXIVClientStructs.FFXIV.Component.GUI; using ImGuiNET; using ImGuiScene; using System; +using System.Collections.Generic; using System.Linq; using System.Numerics; +using System.Threading; +using System.Threading.Tasks; using ActionType = Craftimizer.Simulator.Actions.ActionType; using ClassJob = Craftimizer.Simulator.ClassJob; using CSRecipeNote = FFXIVClientStructs.FFXIV.Client.Game.UI.RecipeNote; @@ -55,11 +59,11 @@ public sealed unsafe class RecipeNote : Window, IDisposable public CharacterStats? CharacterStats { get; private set; } public CraftableStatus CraftStatus { get; private set; } - private TextureWrap ExpertBadge { get; } - private TextureWrap CollectibleBadge { get; } - private TextureWrap SplendorousBadge { get; } - private TextureWrap SpecialistBadge { get; } - private TextureWrap NoManipulationBadge { get; } + private CancellationTokenSource? BestMacroTokenSource { get; set; } + private Exception? BestMacroException { get; set; } + public (Macro, SimulationState)? BestSavedMacro { get; private set; } + public SolverSolution? BestSuggestedMacro { get; private set; } + private IDalamudTextureWrap ExpertBadge { get; } private IDalamudTextureWrap CollectibleBadge { get; } private IDalamudTextureWrap SplendorousBadge { get; } @@ -190,8 +194,10 @@ public sealed unsafe class RecipeNote : Window, IDisposable ImGui.Separator(); ImGuiUtils.TextCentered("Best Saved Macro"); + DrawMacro("savedMacro", BestSavedMacro == null ? null : (BestSavedMacro.Value.Item1.Actions, BestSavedMacro.Value.Item2)); ImGuiUtils.ButtonCentered("View Saved Macros"); ImGuiUtils.TextCentered("Suggested Macro"); + DrawMacro("suggestedMacro", BestSuggestedMacro == null ? null : (BestSuggestedMacro.Value.Actions, BestSuggestedMacro.Value.State)); ImGuiUtils.ButtonCentered("Open Simulator"); } @@ -210,7 +216,7 @@ public sealed unsafe class RecipeNote : Window, IDisposable var levelText = string.Empty; if (level != 0) levelText = SqText.ToLevelString(level); - var imageSize = ImGuiUtils.ButtonHeight; + var imageSize = ImGui.GetFrameHeight(); bool hasSplendorous = false, hasSpecialist = false, shouldHaveManip = false; if (CraftStatus is not (CraftableStatus.LockedClassJob or CraftableStatus.WrongClassJob)) { @@ -285,7 +291,7 @@ public sealed unsafe class RecipeNote : Window, IDisposable var (questGiver, questTerritory, questLocation, mapPayload) = ResolveLevelData(unlockQuest.IssuerLocation.Row); var unlockText = $"Unlock it from {questGiver}"; - ImGuiUtils.AlignCentered(ImGui.CalcTextSize(unlockText).X + 5 + ImGuiUtils.ButtonHeight); + ImGuiUtils.AlignCentered(ImGui.CalcTextSize(unlockText).X + 5 + ImGui.GetFrameHeight()); ImGui.AlignTextToFramePadding(); ImGui.Text(unlockText); ImGui.SameLine(0, 5); @@ -319,7 +325,7 @@ public sealed unsafe class RecipeNote : Window, IDisposable var (vendorName, vendorTerritory, vendorLoation, mapPayload) = ResolveLevelData(5891399); var unlockText = $"Trade a Soul of the Crafter to {vendorName}"; - ImGuiUtils.AlignCentered(ImGui.CalcTextSize(unlockText).X + 5 + ImGuiUtils.ButtonHeight); + ImGuiUtils.AlignCentered(ImGui.CalcTextSize(unlockText).X + 5 + ImGui.GetFrameHeight()); ImGui.AlignTextToFramePadding(); ImGui.Text(unlockText); ImGui.SameLine(0, 5); @@ -335,7 +341,7 @@ public sealed unsafe class RecipeNote : Window, IDisposable { var item = RecipeData.Recipe.ItemRequired.Value!; var itemName = item.Name.ToDalamudString().ToString(); - var imageSize = ImGuiUtils.ButtonHeight; + var imageSize = ImGui.GetFrameHeight(); ImGuiUtils.TextCentered($"You are missing the required equipment."); ImGuiUtils.AlignCentered(imageSize + 5 + ImGui.CalcTextSize(itemName).X); @@ -350,7 +356,7 @@ public sealed unsafe class RecipeNote : Window, IDisposable var status = RecipeData.Recipe.StatusRequired.Value!; var statusName = status.Name.ToDalamudString().ToString(); var statusIcon = Service.IconManager.GetIcon(status.Icon); - var imageSize = new Vector2(ImGuiUtils.ButtonHeight * statusIcon.Width / statusIcon.Height, ImGuiUtils.ButtonHeight); + var imageSize = new Vector2(ImGui.GetFrameHeight() * statusIcon.Width / statusIcon.Height, ImGui.GetFrameHeight()); ImGuiUtils.TextCentered($"You are missing the required status effect."); ImGuiUtils.AlignCentered(imageSize.X + 5 + ImGui.CalcTextSize(statusName).X); @@ -416,8 +422,8 @@ public sealed unsafe class RecipeNote : Window, IDisposable var textLevel = SqText.ToLevelString(RecipeData.RecipeInfo.ClassJobLevel); var isExpert = RecipeData.RecipeInfo.IsExpert; var isCollectable = RecipeData.Recipe.ItemResult.Value!.IsCollectable; - var imageSize = ImGuiUtils.ButtonHeight; - var textSize = ImGui.CalcTextSize("A").Y; + var imageSize = ImGui.GetFrameHeight(); + var textSize = ImGui.GetFontSize(); var badgeSize = new Vector2(textSize * ExpertBadge.Width / ExpertBadge.Height, textSize); var badgeOffset = (imageSize - badgeSize.Y) / 2; @@ -488,6 +494,31 @@ public sealed unsafe class RecipeNote : Window, IDisposable } } + private void DrawMacro(string macroName, (List Actions, SimulationState State)? macro) + { + var availWidth = ImGui.GetContentRegionAvail().X; + + using var window = ImRaii.Child(macroName, new(availWidth, 2 * ImGui.GetFrameHeight()), false); + + if (macro == null) + { + if (BestMacroException == null) + ImGuiUtils.TextMiddle("Calculating..."); + else + { + ImGui.AlignTextToFramePadding(); + using (var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed)) + ImGuiUtils.TextCentered("An exception occurred"); + if (ImGuiUtils.ButtonCentered("Copy Error Message")) + ImGui.SetClipboardText(BestMacroException.ToString()); + } + return; + } + + ImGuiUtils.TextCentered($"{macro.Value.Actions.Count} Actions"); + ImGuiUtils.TextCentered($"{macro.Value.State.Quality} Quality"); + } + private static void DrawRequiredStatsTable(int current, int required) { if (current >= required) @@ -586,7 +617,71 @@ public sealed unsafe class RecipeNote : Window, IDisposable private void CalculateBestMacros() { - throw new NotImplementedException(); + BestMacroTokenSource?.Cancel(); + BestMacroTokenSource = new(); + BestMacroException = null; + BestSavedMacro = null; + BestSuggestedMacro = null; + + var token = BestMacroTokenSource.Token; + _ = Task.Run(() => CalculateBestMacrosTask(token), token) + .ContinueWith(t => + { + if (token.IsCancellationRequested) + return; + + try + { + t.Exception!.Flatten().Handle(ex => ex is TaskCanceledException or OperationCanceledException); + } + catch (AggregateException e) + { + BestMacroException = e; + Log.Error(e, "Calculating macros failed"); + } + }, TaskContinuationOptions.OnlyOnFaulted); + } + + private void CalculateBestMacrosTask(CancellationToken token) + { + var input = new SimulationInput(CharacterStats!, RecipeData!.RecipeInfo); + var state = new SimulationState(input); + var config = Service.Configuration.SimulatorSolverConfig; + var mctsConfig = new MCTSConfig(config); + var simulator = new Solver.Simulator(state, mctsConfig.MaxStepCount); + List macros = new(Service.Configuration.Macros); + + token.ThrowIfCancellationRequested(); + + var bestSaved = macros + .Select(macro => + { + var (resp, outState, failedIdx) = simulator.ExecuteMultiple(state, macro.Actions); + outState.ActionCount = macro.Actions.Count; + var score = SimulationNode.CalculateScoreForState(outState, simulator.CompletionState, mctsConfig) ?? 0; + if (resp != ActionResponse.SimulationComplete) + { + if (failedIdx != -1) + score /= 2; + } + return (macro, outState, score); + }) + .MaxBy(m => m.score); + + token.ThrowIfCancellationRequested(); + + BestSavedMacro = (bestSaved.macro, bestSaved.outState); + + token.ThrowIfCancellationRequested(); + + var solver = new Solver.Solver(config, state) { Token = token }; + solver.OnLog += Log.Debug; + solver.Start(); + var solution = solver.GetTask().GetAwaiter().GetResult(); + + token.ThrowIfCancellationRequested(); + + BestSuggestedMacro = solution; } public void Dispose() diff --git a/Craftimizer/Windows/Settings.cs b/Craftimizer/Windows/Settings.cs index 85e333d..7fe8ce3 100644 --- a/Craftimizer/Windows/Settings.cs +++ b/Craftimizer/Windows/Settings.cs @@ -13,7 +13,7 @@ public class Settings : Window private static Configuration Config => Service.Configuration; private const int OptionWidth = 200; - private static Vector2 OptionButtonSize => new(OptionWidth, ImGuiUtils.ButtonHeight); + private static Vector2 OptionButtonSize => new(OptionWidth, ImGui.GetFrameHeight()); public const string TabGeneral = "General"; public const string TabSimulator = "Simulator"; diff --git a/Craftimizer/Windows/SimulatorDrawer.cs b/Craftimizer/Windows/SimulatorDrawer.cs index 85a8432..af6a8da 100644 --- a/Craftimizer/Windows/SimulatorDrawer.cs +++ b/Craftimizer/Windows/SimulatorDrawer.cs @@ -310,8 +310,8 @@ public sealed partial class Simulator : Window, IDisposable var totalWidth = drawParams.Total; var halfWidth = (totalWidth - ImGui.GetStyle().ItemSpacing.X) / 2f; var quarterWidth = (halfWidth - ImGui.GetStyle().ItemSpacing.X) / 2f; - var halfButtonSize = new Vector2(halfWidth, ImGuiUtils.ButtonHeight); - var quarterButtonSize = new Vector2(quarterWidth, ImGuiUtils.ButtonHeight); + var halfButtonSize = new Vector2(halfWidth, ImGui.GetFrameHeight()); + var quarterButtonSize = new Vector2(quarterWidth, ImGui.GetFrameHeight()); var conditionRandomnessText = "Condition Randomness"; var conditionRandomness = Config.ConditionRandomness; diff --git a/Solver/Solver.cs b/Solver/Solver.cs index 080c5bd..e07b5f0 100644 --- a/Solver/Solver.cs +++ b/Solver/Solver.cs @@ -87,7 +87,7 @@ public sealed class Solver : IDisposable } catch (AggregateException e) { - e.Handle(ex => ex is OperationCanceledException); + e.Flatten().Handle(ex => ex is OperationCanceledException); } catch (OperationCanceledException) {