Skip to content

Commit

Permalink
Refactor background work in recipenote, suggest macro automatically
Browse files Browse the repository at this point in the history
  • Loading branch information
WorkingRobot committed Feb 23, 2024
1 parent 1e87372 commit 2cced24
Show file tree
Hide file tree
Showing 3 changed files with 157 additions and 85 deletions.
1 change: 1 addition & 0 deletions Craftimizer/Configuration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ public class Configuration : IPluginConfiguration
public bool EnableSynthHelper { get; set; } = true;
public bool DisableSynthHelperOnMacro { get; set; } = true;
public bool ShowOptimalMacroStat { get; set; } = true;
public bool SuggestMacroAutomatically { get; set; }
public int SynthHelperStepCount { get; set; } = 5;

public bool PinSynthHelperToWindow { get; set; } = true;
Expand Down
229 changes: 144 additions & 85 deletions Craftimizer/Windows/RecipeNote.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,58 @@ public enum CraftableStatus
public CharacterStats? CharacterStats { get; private set; }
public CraftableStatus CraftStatus { get; private set; }

private CancellationTokenSource? BestMacroTokenSource { get; set; }
private Exception? BestMacroException { get; set; }
public sealed class BackgroundTask<T> : IDisposable where T : struct
{
public T? Result { get; private set; }
public Exception? Exception { get; private set; }
public bool Completed { get; private set; }

private CancellationTokenSource TokenSource { get; }
private Func<CancellationToken, T> Func { get; }

public BackgroundTask(Func<CancellationToken, T> func)
{
Func = func;
TokenSource = new();
}

public void Start()
{
var token = TokenSource.Token;
var task = Task.Run(() => Result = Func(token), token);
_ = task.ContinueWith(t =>
{
Completed = true;
});
_ = task.ContinueWith(t =>
{
if (token.IsCancellationRequested)
return;

try
{
t.Exception!.Flatten().Handle(ex => ex is TaskCanceledException or OperationCanceledException);
}
catch (AggregateException e)
{
Exception = e;
Log.Error(e, "Calculating macros failed");
}
}, TaskContinuationOptions.OnlyOnFaulted);
}

public void Cancel() =>
TokenSource.Cancel();

public void Dispose() =>
Cancel();
}

private BackgroundTask<(Macro?, SimulationState?)>? SavedMacroTask { get; set; }
private BackgroundTask<SolverSolution>? SuggestedMacroTask { get; set; }

private Solver.Solver? BestMacroSolver { get; set; }
public (Macro, SimulationState)? BestSavedMacro { get; private set; }
public bool HasSavedMacro { get; private set; }
public SolverSolution? BestSuggestedMacro { get; private set; }

private IDalamudTextureWrap ExpertBadge { get; }
private IDalamudTextureWrap CollectibleBadge { get; }
Expand Down Expand Up @@ -119,11 +165,21 @@ public override bool DrawConditions()
if (isOpen != wasOpen)
{
if (wasOpen)
BestMacroTokenSource?.Cancel();
else
{
if (!BestSuggestedMacro.HasValue && CraftStatus == CraftableStatus.OK && BestMacroTokenSource == null)
CalculateBestMacros();
SavedMacroTask?.Cancel();
SuggestedMacroTask?.Cancel();
}
else if (CraftStatus == CraftableStatus.OK)
{
if (SavedMacroTask?.Result == null && (SavedMacroTask?.Completed ?? true))
CalculateSavedMacro();
if (Service.Configuration.SuggestMacroAutomatically && SuggestedMacroTask?.Result == null && (SuggestedMacroTask?.Completed ?? true))
CalculateSuggestedMacro();
else
{
SuggestedMacroTask?.Cancel();
SuggestedMacroTask = null;
}
}
}

Expand Down Expand Up @@ -197,7 +253,16 @@ private bool ShouldDraw()
}

if (StatsChanged && CraftStatus == CraftableStatus.OK)
CalculateBestMacros();
{
CalculateSavedMacro();
if (Service.Configuration.SuggestMacroAutomatically)
CalculateSuggestedMacro();
else
{
SuggestedMacroTask?.Cancel();
SuggestedMacroTask = null;
}
}

return true;
}
Expand Down Expand Up @@ -262,22 +327,22 @@ public override void Draw()
using (var panel = ImRaii2.GroupPanel("Best Saved Macro", panelWidth, out _))
{
var stepsPanelWidthOffset = ImGui.GetContentRegionAvail().X - panelWidth;
if (BestSavedMacro is { } savedMacro)
if (SavedMacroTask?.Result is { } savedMacro && savedMacro.Item1 != null && savedMacro.Item2 != null)
{
ImGuiUtils.TextCentered(savedMacro.Item1.Name, panelWidth);
DrawMacro((savedMacro.Item1.Actions, savedMacro.Item2), null, a => { savedMacro.Item1.ActionEnumerable = a; Service.Configuration.Save(); }, stepsPanelWidthOffset, true);
DrawMacro((savedMacro.Item1.Actions, savedMacro.Item2.Value), SavedMacroTask.Exception, null, a => { savedMacro.Item1.ActionEnumerable = a; Service.Configuration.Save(); }, stepsPanelWidthOffset, true);
}
else
DrawMacro(null, null, null, stepsPanelWidthOffset, true);
DrawMacro(null, SavedMacroTask?.Exception, null, null, stepsPanelWidthOffset, true);
}

using (var panel = ImRaii2.GroupPanel("Suggested Macro", panelWidth, out _))
{
var stepsPanelWidthOffset = ImGui.GetContentRegionAvail().X - panelWidth;
if (BestSuggestedMacro is { } suggestedMacro)
DrawMacro((suggestedMacro.Actions, suggestedMacro.State), null, null, stepsPanelWidthOffset, false);
if (SuggestedMacroTask?.Result is { } suggestedMacro)
DrawMacro((suggestedMacro.Actions, suggestedMacro.State), SuggestedMacroTask.Exception, null, null, stepsPanelWidthOffset, false);
else
DrawMacro(null, BestMacroSolver, null, stepsPanelWidthOffset, false);
DrawMacro(null, SuggestedMacroTask?.Exception, BestMacroSolver, null, stepsPanelWidthOffset, false);
}

ImGuiHelpers.ScaledDummy(5);
Expand Down Expand Up @@ -582,23 +647,24 @@ private void DrawRecipeStats()
}
}

private void DrawMacro((IReadOnlyList<ActionType> Actions, SimulationState State)? macroValue, Solver.Solver? solver, Action<IEnumerable<ActionType>>? setter, float stepsAvailWidthOffset, bool isSavedMacro)
private void DrawMacro((IReadOnlyList<ActionType> Actions, SimulationState State)? macroValue, Exception? exception, Solver.Solver? solver, Action<IEnumerable<ActionType>>? setter, float stepsAvailWidthOffset, bool isSavedMacro)
{
var windowHeight = 2 * ImGui.GetFrameHeightWithSpacing();

if (macroValue is not { } macro)
{
if (isSavedMacro && !HasSavedMacro)
ImGuiUtils.TextMiddleNewLine("You have no macros!", new(ImGui.GetContentRegionAvail().X - stepsAvailWidthOffset, windowHeight + 1));
else if (BestMacroException == null)
else if (exception == null)
{
if (solver != null)
if (solver != null && SuggestedMacroTask != null)
{
var calcTextSize = ImGui.CalcTextSize("Calculating...");
var spacing = ImGui.GetStyle().ItemSpacing.X;
var fraction = Math.Clamp((float)solver.ProgressValue / solver.ProgressMax, 0, 1);
var progressColors = Colors.GetSolverProgressColors(solver.ProgressStage);

var c = ImGui.GetCursorPos();
ImGuiUtils.AlignCentered(windowHeight + spacing + calcTextSize.X, ImGui.GetContentRegionAvail().X - stepsAvailWidthOffset);

ImGuiUtils.ArcProgress(
Expand All @@ -614,17 +680,28 @@ private void DrawMacro((IReadOnlyList<ActionType> Actions, SimulationState State

ImGuiUtils.AlignMiddle(calcTextSize, new(calcTextSize.X, windowHeight));
ImGui.Text("Calculating...");
ImGui.SetCursorPos(c + new Vector2(0, windowHeight + ImGui.GetStyle().ItemSpacing.Y - 1));
}
else
ImGuiUtils.TextMiddleNewLine("Calculating...", new(ImGui.GetContentRegionAvail().X - stepsAvailWidthOffset, windowHeight + 1));
{
using var _padding = ImRaii.PushStyle(ImGuiStyleVar.FramePadding, ImGui.GetStyle().FramePadding * 2);
var size = ImGui.CalcTextSize("Generate") + ImGui.GetStyle().FramePadding * 2;
var c = ImGui.GetCursorPos();
var availSize = new Vector2(ImGui.GetContentRegionAvail().X - stepsAvailWidthOffset, windowHeight);
ImGuiUtils.AlignMiddle(size, availSize);
using var _disabled = ImRaii.Disabled(!(SuggestedMacroTask?.Completed) ?? false);
if (ImGui.Button("Generate"))
CalculateSuggestedMacro();
ImGui.SetCursorPos(c + new Vector2(0, availSize.Y + ImGui.GetStyle().ItemSpacing.Y - 1));
}
}
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());
ImGui.SetClipboardText(exception.ToString());
}
return;
}
Expand Down Expand Up @@ -856,91 +933,73 @@ private static string ResolveNpcResidentName(uint npcRowId)
return null;
}

private void CalculateBestMacros()
private void CalculateSavedMacro()
{
BestMacroTokenSource?.Cancel();
BestMacroTokenSource = new();
BestMacroSolver = null;
BestMacroException = null;
BestSavedMacro = null;
HasSavedMacro = false;
BestSuggestedMacro = null;

var token = BestMacroTokenSource.Token;
var task = Task.Run(() => CalculateBestMacrosTask(token), token);
_ = task.ContinueWith(t =>
SavedMacroTask?.Cancel();
SavedMacroTask = new(token =>
{
if (token == BestMacroTokenSource.Token)
BestMacroTokenSource = null;
});
_ = task.ContinueWith(t =>
{
if (token.IsCancellationRequested)
return;
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 SimulatorNoRandom();
List<Macro> macros = new(Service.Configuration.Macros);

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 SimulatorNoRandom();
List<Macro> macros = new(Service.Configuration.Macros);

token.ThrowIfCancellationRequested();
token.ThrowIfCancellationRequested();

HasSavedMacro = macros.Count > 0;
if (HasSavedMacro)
{
HasSavedMacro = macros.Count > 0;
if (!HasSavedMacro)
return (null, null);
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)
{
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);
})
if (failedIdx != -1)
score /= 2;
}
return (macro, outState, score);
})
.MaxBy(m => m.score);

token.ThrowIfCancellationRequested();

BestSavedMacro = (bestSaved.macro, bestSaved.outState);
return (bestSaved.macro, bestSaved.outState);
});
SavedMacroTask.Start();
}

private void CalculateSuggestedMacro()
{
SuggestedMacroTask?.Cancel();
SuggestedMacroTask = new(token =>
{
var input = new SimulationInput(CharacterStats!, RecipeData!.RecipeInfo);
var state = new SimulationState(input);
var config = Service.Configuration.SimulatorSolverConfig;

token.ThrowIfCancellationRequested();
}

var solver = new Solver.Solver(config, state) { Token = token };
solver.OnLog += Log.Debug;
BestMacroSolver = solver;
solver.Start();
var solution = solver.GetTask().GetAwaiter().GetResult();
var solver = new Solver.Solver(config, state) { Token = token };
solver.OnLog += Log.Debug;
BestMacroSolver = solver;
solver.Start();
var solution = solver.GetTask().GetAwaiter().GetResult();

token.ThrowIfCancellationRequested();

BestSuggestedMacro = solution;
token.ThrowIfCancellationRequested();

token.ThrowIfCancellationRequested();
return solution;
});
SuggestedMacroTask.Start();
}

public void Dispose()
{
SavedMacroTask?.Cancel();
SuggestedMacroTask?.Cancel();
Service.WindowSystem.RemoveWindow(this);
AxisFont?.Dispose();
}
Expand Down
12 changes: 12 additions & 0 deletions Craftimizer/Windows/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,18 @@ ref isDirty
ref isDirty
);

DrawOption(
"Automatically Suggest Macro in Crafting Log",
"(Can cause frame drops!) When navigating to a new recipe or changing your gear " +
"stats, automatically suggest a new macro (equivalent to clicking \"Generate\" " +
"in the Macro Editor). This can cause harsh frame drops on some computers or " +
"recipes when underleveled while navigating the crafting log. Turning this off " +
"provides a button to allow you to manually suggest a macro only when you need it.",
Config.SuggestMacroAutomatically,
v => Config.SuggestMacroAutomatically = v,
ref isDirty
);

DrawOption(
"Show Only One Macro Stat in Crafting Log",
"Only one stat will be shown for a macro. If a craft will be finished, quality " +
Expand Down

1 comment on commit 2cced24

@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: 2cced24 Previous: 337d42f Ratio
Craftimizer.Benchmark.Bench.Solve(State: 2C05013A, Config: 60E4EBE9) 1381667160 ns (± 5327907.242569879)
Craftimizer.Benchmark.Bench.Solve(State: 2C05013A, Config: 60E4EBE9) 1042142680 ns (± 3755024.883539389)
Craftimizer.Benchmark.Bench.Solve(State: 85B4CE7D, Config: 60E4EBE9) 1262204146.6666667 ns (± 8308493.365386209)
Craftimizer.Benchmark.Bench.Solve(State: 85B4CE7D, Config: 60E4EBE9) 892800993.3333334 ns (± 3873414.3414649907)

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

Please sign in to comment.