Skip to content

Commit

Permalink
Implement SynthHelper
Browse files Browse the repository at this point in the history
  • Loading branch information
WorkingRobot committed Nov 15, 2023
1 parent 72befe8 commit a1cdbdf
Showing 1 changed file with 302 additions and 16 deletions.
318 changes: 302 additions & 16 deletions Craftimizer/Windows/SynthHelper.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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);
Expand All @@ -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()
{
Expand Down Expand Up @@ -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<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)
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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();
}
}

1 comment on commit a1cdbdf

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benchmark

Benchmark suite Current: a1cdbdf Previous: a8c3f34 Ratio
Craftimizer.Benchmark.Bench.Solve(State: 70E4CDA9, Config: 7102D73C) 1463432426.6666667 ns (± 3293892.2936391286)
Craftimizer.Benchmark.Bench.Solve(State: 70E4CDA9, Config: 7102D73C) 1105381928.5714285 ns (± 7710535.549735586)
Craftimizer.Benchmark.Bench.Solve(State: 98987816, Config: 7102D73C) 1418481007.142857 ns (± 4299512.688570161)
Craftimizer.Benchmark.Bench.Solve(State: 98987816, Config: 7102D73C) 1133449900 ns (± 5820127.643458849)

This comment was automatically generated by workflow using github-action-benchmark.

Please sign in to comment.