Files
Craftimizer/Craftimizer/Windows/RecipeNote.cs
T
JonKazama-Hellion b598c03e9e Apply csharpier reflow across source tree
Reformats the entire Craftimizer source tree with dotnet csharpier 1.2.6
to match the Hellion Forge house style (matches what HellionChat enforces
in its pre-push pipeline). Pure whitespace + using-block sorting; no
semantic changes.

This is a one-time noisy commit. Future code edits in this fork should
land csharpier-clean because the pre-push hook (introduced in the next
commit) runs `dotnet csharpier check Craftimizer/` as Block C of the
preflight gate.

Trade-off acknowledged: this widens the merge gap with upstream
Craftimizer should Asriel ever resume maintenance. Given the upstream
has been dormant since FFXIV 7.4 and the fork is light-rename only
(internal namespaces unchanged), the marginal cost is acceptable.
2026-05-26 20:21:21 +02:00

1606 lines
60 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
using Craftimizer.Plugin;
using Craftimizer.Simulator;
using Craftimizer.Simulator.Actions;
using Craftimizer.Solver;
using Craftimizer.Utils;
using Dalamud.Bindings.ImGui;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Components;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.ManagedFontAtlas;
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.UI;
using FFXIVClientStructs.FFXIV.Client.System.String;
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
using FFXIVClientStructs.FFXIV.Component.GUI;
using ActionType = Craftimizer.Simulator.Actions.ActionType;
using ClassJob = Craftimizer.Simulator.ClassJob;
using CSRecipeNote = FFXIVClientStructs.FFXIV.Client.Game.UI.RecipeNote;
using RecipeIngredient2 = Craftimizer.Utils.CSRecipeNote.RecipeIngredient;
namespace Craftimizer.Windows;
public sealed unsafe class RecipeNote : Window, IDisposable
{
private const ImGuiWindowFlags WindowFlagsPinned =
WindowFlagsFloating | ImGuiWindowFlags.NoSavedSettings;
private const ImGuiWindowFlags WindowFlagsFloating =
ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoFocusOnAppearing;
private const string WindowNamePinned =
"Craftimizer Crafting Log Helper###CraftimizerRecipeNote";
private const string WindowNameFloating = $"{WindowNamePinned}Floating";
public enum CraftableStatus
{
OK,
LockedClassJob,
WrongClassJob,
SpecialistRequired,
RequiredItem,
RequiredStatus,
CraftsmanshipTooLow,
ControlTooLow,
}
public AtkUnitBase* Addon { get; private set; }
public bool IsWKS { get; private set; }
public RecipeData? RecipeData { get; private set; }
public CharacterStats? CharacterStats { get; private set; }
private int StartingQuality { get; set; }
public CraftableStatus CraftStatus { get; private set; }
private BackgroundTask<(Macro?, SimulationState?)>? SavedMacroTask { get; set; }
private BackgroundTask<SolverSolution>? SuggestedMacroTask { get; set; }
private BackgroundTask<(
CommunityMacros.CommunityMacro?,
SimulationState?
)>? CommunityMacroTask { get; set; }
private Solver.Solver? BestMacroSolver { get; set; }
public bool HasSavedMacro { get; private set; }
private ILoadedTextureIcon ExpertBadge { get; }
private ILoadedTextureIcon CollectibleBadge { get; }
private ILoadedTextureIcon CosmicExplorationBadge { get; }
private ILoadedTextureIcon SplendorousBadge { get; }
private ILoadedTextureIcon SpecialistBadge { get; }
private ILoadedTextureIcon NoManipulationBadge { get; }
private IFontHandle AxisFont { get; }
public RecipeNote()
: base(WindowNamePinned)
{
ExpertBadge = IconManager.GetAssemblyTexture("Graphics.expert_badge.png");
CollectibleBadge = IconManager.GetAssemblyTexture("Graphics.collectible_badge.png");
CosmicExplorationBadge = IconManager.GetIcon(60810);
SplendorousBadge = IconManager.GetAssemblyTexture("Graphics.splendorous.png");
SpecialistBadge = IconManager.GetAssemblyTexture("Graphics.specialist.png");
NoManipulationBadge = IconManager.GetAssemblyTexture("Graphics.no_manip.png");
AxisFont = Service.PluginInterface.UiBuilder.FontAtlas.NewGameFontHandle(
new(GameFontFamilyAndSize.Axis14)
);
RespectCloseHotkey = false;
DisableWindowSounds = true;
ShowCloseButton = false;
IsOpen = true;
SizeConstraints = new WindowSizeConstraints
{
MinimumSize = new(-1),
MaximumSize = new(10000, 10000),
};
TitleBarButtons =
[
new()
{
Icon = FontAwesomeIcon.Cog,
IconOffset = new(2, 1),
Click = _ => Service.Plugin.OpenSettingsTab("Crafting Log"),
ShowTooltip = () => ImGuiUtils.Tooltip("Open Settings"),
},
new()
{
Icon = FontAwesomeIcon.Heart,
IconOffset = new(2, 1),
Click = _ => Util.OpenLink(Plugin.Plugin.SupportLink),
ShowTooltip = () => ImGuiUtils.Tooltip("Support me on Ko-fi!"),
},
];
Service.WindowSystem.AddWindow(this);
}
private bool IsCollapsed { get; set; }
private bool ShouldOpen { get; set; }
private bool WasOpen { get; set; }
private bool WasCollapsed { get; set; }
private bool ShouldCalculate => !IsCollapsed && ShouldOpen;
private bool WasCalculatable { get; set; }
public override void Update()
{
base.Update();
ShouldOpen = CalculateShouldOpen();
if (ShouldCalculate != WasCalculatable)
{
if (WasCalculatable)
{
SavedMacroTask?.Cancel();
SuggestedMacroTask?.Cancel();
CommunityMacroTask?.Cancel();
}
else if (CraftStatus == CraftableStatus.OK && !StatsChanged)
{
// If it didn't exist before or it already ran, we need to recalculate
if (SavedMacroTask?.Result == null && (SavedMacroTask?.Completed ?? true))
CalculateSavedMacro();
// If it didn't exist before or it already ran, we need to recalculate
if (
Service.Configuration.SuggestMacroAutomatically
&& SuggestedMacroTask?.Result == null
&& (SuggestedMacroTask?.Completed ?? true)
)
CalculateSuggestedMacro();
// If we don't want to suggest automatically, we should cancel and clean out the task
else if (
!Service.Configuration.SuggestMacroAutomatically
&& SuggestedMacroTask?.Result == null
)
{
SuggestedMacroTask?.Cancel();
SuggestedMacroTask = null;
}
// If it didn't exist before or it already ran, we need to recalculate
if (
Service.Configuration.ShowCommunityMacros
&& Service.Configuration.SearchCommunityMacroAutomatically
&& CommunityMacroTask?.Result == null
&& (CommunityMacroTask?.Completed ?? true)
)
CalculateCommunityMacro();
// If we don't want to search automatically, we should cancel and clean out the task
else if (
!Service.Configuration.SearchCommunityMacroAutomatically
&& CommunityMacroTask?.Result == null
)
{
CommunityMacroTask?.Cancel();
CommunityMacroTask = null;
}
}
}
if (!ShouldOpen)
{
StyleAlpha = LastAlpha = null;
LastPosition = null;
}
WasOpen = ShouldOpen;
WasCollapsed = IsCollapsed;
WasCalculatable = ShouldCalculate;
}
public override bool DrawConditions() => ShouldOpen;
private bool StatsChanged { get; set; }
private bool CalculateShouldOpen()
{
if (Service.Objects.LocalPlayer == null)
return false;
bool ShouldUseRecipeNote()
{
Addon = (AtkUnitBase*)Service.GameGui.GetAddonByName("RecipeNote").Address;
if (Addon == null)
return false;
// Check if RecipeNote addon is visible
if (Addon->WindowNode == null)
return false;
// Check if RecipeNote has a visible selected recipe
if (!Addon->GetNodeById(57)->IsVisible())
return false;
return true;
}
bool ShouldUseWKSRecipeNote()
{
Addon = (AtkUnitBase*)Service.GameGui.GetAddonByName("WKSRecipeNotebook").Address;
if (Addon == null)
return false;
// Check if WKS addon is visible
if (Addon->WindowNode == null)
return false;
// Check if WKS has a visible selected recipe
if (!Addon->GetNodeById(13)->IsVisible())
return false;
return true;
}
if (ShouldUseRecipeNote())
IsWKS = false;
else if (ShouldUseWKSRecipeNote())
IsWKS = true;
else
return false;
StatsChanged = false;
{
var instance = CSRecipeNote.Instance();
var list = instance->RecipeList;
if (list == null)
return false;
var recipeEntry = list->SelectedRecipe;
if (recipeEntry == null)
return false;
var recipeId = recipeEntry->RecipeId;
if (recipeId != RecipeData?.RecipeId)
{
RecipeData = new(recipeId);
StatsChanged = true;
}
}
Gearsets.GearsetItem[] gearItems;
{
var gearStats = Gearsets.CalculateGearsetCurrentStats();
var container = InventoryManager
.Instance()
->GetInventoryContainer(InventoryType.EquippedItems);
if (container == null)
return false;
gearItems = Gearsets.GetGearsetItems(container);
var characterStats = Gearsets.CalculateCharacterStats(
gearStats,
gearItems,
RecipeData.ClassJob.GetPlayerLevel(),
RecipeData.ClassJob.CanPlayerUseManipulation()
);
if (characterStats != CharacterStats)
{
// Re-initialize recipe data to recalculate adjusted level if needed
if (
RecipeData.AdjustedJobLevel != null
&& characterStats.Level != CharacterStats?.Level
)
{
RecipeData = new(RecipeData.RecipeId);
}
CharacterStats = characterStats;
StatsChanged = true;
}
}
var craftStatus = CalculateCraftStatus(gearItems);
if (craftStatus != CraftStatus)
{
CraftStatus = craftStatus;
StatsChanged = true;
}
var startingQuality = RecipeData.CalculateStartingQuality(CalculateIngredientHqCounts());
var qualityChanged = startingQuality != StartingQuality;
if (qualityChanged)
StartingQuality = startingQuality;
if ((StatsChanged || qualityChanged) && CraftStatus == CraftableStatus.OK)
{
// Stats changed and we are still craftable, so we need to recalculate
CalculateSavedMacro();
// If we want to suggest automatically, we should recalculate
if (Service.Configuration.SuggestMacroAutomatically)
CalculateSuggestedMacro();
// Otherwise, we should cancel and clean out the task
else
{
SuggestedMacroTask?.Cancel();
SuggestedMacroTask = null;
}
// If we want to search automatically, we should recalculate
if (
Service.Configuration.ShowCommunityMacros
&& Service.Configuration.SearchCommunityMacroAutomatically
)
CalculateCommunityMacro();
// Otherwise, we should cancel and clean out the task
else
{
CommunityMacroTask?.Cancel();
CommunityMacroTask = null;
}
}
return true;
}
private IEnumerable<int> CalculateIngredientHqCounts()
{
if (RecipeData == null)
throw new InvalidOperationException("RecipeData must not be null");
var ingredientCount = RecipeData.Ingredients.Count;
var ingredientSpan = MemoryMarshal.Cast<CSRecipeNote.RecipeIngredient, RecipeIngredient2>(
CSRecipeNote.Instance()->RecipeList->SelectedRecipe->Ingredients
);
return ingredientSpan.ToArray().Take(ingredientCount).Select(i => (int)i.HQCount);
}
private Vector2? LastPosition { get; set; }
private byte? StyleAlpha { get; set; }
private byte? LastAlpha { get; set; }
public override void PreDraw()
{
base.PreDraw();
IsCollapsed = true;
if (Service.Configuration.PinRecipeNoteToWindow)
{
ref var unit = ref *Addon;
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 newAlpha = unit.WindowNode->AtkResNode.Alpha_2;
StyleAlpha = LastAlpha ?? newAlpha;
LastAlpha = newAlpha;
uint nodeId,
nodeParentId;
if (IsWKS)
{
nodeId = 15;
nodeParentId = 13;
}
else
{
nodeId = 59;
nodeParentId = 57;
}
var node = Addon->GetNodeById(nodeId);
var nodeParent = Addon->GetNodeById(nodeParentId);
var newPosition = pos + new Vector2(size.X, (nodeParent->Y + node->Y) * scale);
Position = ImGuiHelpers.MainViewport.Pos + (LastPosition ?? newPosition);
LastPosition = newPosition;
Flags = WindowFlagsPinned;
WindowName = WindowNamePinned;
}
else
{
StyleAlpha = LastAlpha = null;
Position = LastPosition = null;
Flags = WindowFlagsFloating;
WindowName = WindowNameFloating;
}
ImGui.PushStyleVar(
ImGuiStyleVar.Alpha,
StyleAlpha.HasValue ? (StyleAlpha.Value / 255f) : 1
);
}
public override void PostDraw()
{
ImGui.PopStyleVar();
base.PostDraw();
}
public override void Draw()
{
IsCollapsed = false;
var availWidth = ImGui.GetContentRegionAvail().X;
using (
var table = ImRaii.Table(
"stats",
2,
ImGuiTableFlags.BordersInnerV
| ImGuiTableFlags.SizingFixedSame
| ImGuiTableFlags.NoSavedSettings
)
)
{
if (table)
{
if (StatsChanged)
{
ImGui.TableSetupColumn(
"",
ImGuiTableColumnFlags.WidthFixed,
150 * ImGuiHelpers.GlobalScale
);
ImGui.TableSetupColumn(
"",
ImGuiTableColumnFlags.WidthFixed,
150 * ImGuiHelpers.GlobalScale
);
}
ImGui.TableNextColumn();
DrawCharacterStats();
ImGui.TableNextColumn();
DrawRecipeStats();
// Ensure that we know the window should be the same size as this table. Any more and it'll grow slowly and won't shrink when it could
ImGui.SameLine(0, 0);
// The -1 is to account for the extra vertical separator on the right that ImGui draws for some reason
availWidth =
ImGui.GetCursorPosX()
- ImGui.GetStyle().WindowPadding.X
+ ImGui.GetStyle().CellPadding.X
- 1;
}
}
if (CraftStatus != CraftableStatus.OK)
return;
ImGui.Separator();
var panelWidth = availWidth - ImGui.GetStyle().ItemSpacing.X * 2;
{
var macroTaskResult = SavedMacroTask?.Result;
var state = new MacroTaskState()
{
Type = MacroTaskType.Saved,
Exception = SavedMacroTask?.Exception,
Started = SavedMacroTask != null,
Completed = SavedMacroTask?.Completed ?? false,
Actions = macroTaskResult?.Item1?.Actions,
MacroName = macroTaskResult?.Item1?.Name,
State = macroTaskResult?.Item2,
};
if (macroTaskResult is { } macro && macro.Item1 is { } savedMacro)
state.MacroEditorSetter = a =>
{
savedMacro.ActionEnumerable = a;
Service.Configuration.Save();
};
DrawMacro(in state, panelWidth);
}
{
var macroTaskResult = SuggestedMacroTask?.Result;
var state = new MacroTaskState()
{
Type = MacroTaskType.Suggested,
Exception = SuggestedMacroTask?.Exception,
Started = SuggestedMacroTask != null,
Completed = SuggestedMacroTask?.Completed ?? false,
Actions = macroTaskResult?.Actions,
State = macroTaskResult?.State,
Solver = BestMacroSolver,
};
DrawMacro(in state, panelWidth);
}
if (Service.Configuration.ShowCommunityMacros)
{
var macroTaskResult = CommunityMacroTask?.Result;
var state = new MacroTaskState()
{
Type = MacroTaskType.Community,
Exception = CommunityMacroTask?.Exception,
Started = CommunityMacroTask != null,
Completed = CommunityMacroTask?.Completed ?? false,
Actions = macroTaskResult?.Item1?.Actions,
MacroName = macroTaskResult?.Item1?.Name,
MacroUrl = macroTaskResult?.Item1?.Url,
State = macroTaskResult?.Item2,
};
DrawMacro(in state, panelWidth);
}
ImGuiHelpers.ScaledDummy(5);
if (ImGui.Button("View Saved Macros", new(availWidth, 0)))
Service.Plugin.OpenMacroListWindow();
if (ImGui.Button("Open in Macro Editor", new(availWidth, 0)))
Service.Plugin.OpenMacroEditor(
CharacterStats!,
RecipeData!,
new(Service.Objects.LocalPlayer!.StatusList),
CalculateIngredientHqCounts(),
[],
null
);
}
private void DrawCharacterStats()
{
ImGuiUtils.TextCentered("Crafter");
var level = RecipeData!.ClassJob.GetPlayerLevel();
{
var textClassName = RecipeData.ClassJob.GetAbbreviation();
var textClassSize = AxisFont.CalcTextSize(textClassName);
var levelText = string.Empty;
if (level != 0)
levelText = SqText.LevelPrefix.ToIconChar() + SqText.ToLevelString(level);
var imageSize = ImGui.GetFrameHeight();
bool hasSplendorous = false,
hasSpecialist = false,
shouldHaveManip = false;
if (
CraftStatus is not (CraftableStatus.LockedClassJob or CraftableStatus.WrongClassJob)
)
{
hasSplendorous = CharacterStats!.HasSplendorousBuff;
hasSpecialist = CharacterStats!.IsSpecialist;
shouldHaveManip =
!CharacterStats.CanUseManipulation
&& CharacterStats.Level >= ActionType.Manipulation.Level();
}
ImGuiUtils.AlignCentered(
imageSize
+ 5
+ textClassSize.X
+ (level == 0 ? 0 : (3 + ImGui.CalcTextSize(levelText).X))
+ (hasSplendorous ? (3 + imageSize) : 0)
+ (hasSpecialist ? (3 + imageSize) : 0)
+ (shouldHaveManip ? (3 + imageSize) : 0)
);
ImGui.AlignTextToFramePadding();
var uv0 = new Vector2(6, 3);
var uv1 = uv0 + new Vector2(44);
uv0 /= new Vector2(56);
uv1 /= new Vector2(56);
ImGui.Image(
Service.IconManager.GetIconCached(RecipeData.ClassJob.GetIconId()).Handle,
new Vector2(imageSize),
uv0,
uv1
);
ImGui.SameLine(0, 5);
if (level != 0)
{
ImGui.TextUnformatted(levelText);
ImGui.SameLine(0, 3);
}
AxisFont.Text(textClassName);
if (hasSplendorous)
{
ImGui.SameLine(0, 3);
ImGui.Image(SplendorousBadge.Handle, new Vector2(imageSize));
if (ImGui.IsItemHovered())
ImGuiUtils.Tooltip($"Splendorous Tool");
}
if (hasSpecialist)
{
ImGui.SameLine(0, 3);
ImGui.Image(
SpecialistBadge.Handle,
new Vector2(imageSize),
Vector2.Zero,
Vector2.One,
new(0.99f, 0.97f, 0.62f, 1f)
);
if (ImGui.IsItemHovered())
ImGuiUtils.Tooltip($"Specialist");
}
if (shouldHaveManip)
{
ImGui.SameLine(0, 3);
ImGui.Image(NoManipulationBadge.Handle, new Vector2(imageSize));
if (ImGui.IsItemHovered())
ImGuiUtils.Tooltip($"No Manipulation (Missing Job Quest)");
}
}
ImGui.Separator();
switch (CraftStatus)
{
case CraftableStatus.LockedClassJob:
{
ImGuiUtils.TextCentered(
$"You do not have {RecipeData.ClassJob.GetName()} unlocked."
);
ImGui.Separator();
var unlockQuest = RecipeData.ClassJob.GetUnlockQuest();
var (questGiver, questTerritory, questLocation, mapPayload) = ResolveLevelData(
unlockQuest.IssuerLocation.RowId
);
var unlockText = $"Unlock it from {questGiver}";
ImGuiUtils.AlignCentered(
ImGui.CalcTextSize(unlockText).X + 5 + ImGui.GetFrameHeight()
);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(unlockText);
ImGui.SameLine(0, 5);
if (ImGuiComponents.IconButton(FontAwesomeIcon.Flag))
Service.GameGui.OpenMapWithMapLink(mapPayload);
if (ImGui.IsItemHovered())
ImGuiUtils.Tooltip("Open in map");
ImGuiUtils.TextCentered(
$"{questTerritory} ({GetCoordinatesString(questLocation)})"
);
}
break;
case CraftableStatus.WrongClassJob:
{
ImGuiUtils.TextCentered(
$"You are not {RecipeData.ClassJob.GetNameArticle()} {RecipeData.ClassJob.GetName()}."
);
var gearsetId = GetGearsetForJob(RecipeData.ClassJob);
if (gearsetId.HasValue)
{
if (ImGuiUtils.ButtonCentered("Switch Job"))
RaptureGearsetModule.Instance()->EquipGearset(gearsetId.Value);
if (ImGui.IsItemHovered())
ImGuiUtils.Tooltip($"Swap to gearset {gearsetId + 1}");
}
else
ImGuiUtils.TextCentered(
$"You do not have any {RecipeData.ClassJob.GetName()} gearsets."
);
ImGui.Dummy(default);
}
break;
case CraftableStatus.SpecialistRequired:
{
ImGuiUtils.TextCentered($"You need to be a specialist to craft this recipe.");
var (vendorName, vendorTerritory, vendorLoation, mapPayload) = ResolveLevelData(
5891399
);
var unlockText = $"Trade a Soul of the Crafter to {vendorName}";
ImGuiUtils.AlignCentered(
ImGui.CalcTextSize(unlockText).X + 5 + ImGui.GetFrameHeight()
);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(unlockText);
ImGui.SameLine(0, 5);
if (ImGuiComponents.IconButton(FontAwesomeIcon.Flag))
Service.GameGui.OpenMapWithMapLink(mapPayload);
if (ImGui.IsItemHovered())
ImGuiUtils.Tooltip("Open in map");
ImGuiUtils.TextCentered(
$"{vendorTerritory} ({GetCoordinatesString(vendorLoation)})"
);
}
break;
case CraftableStatus.RequiredItem:
{
var item = RecipeData.Recipe.ItemRequired.Value!;
var itemName = item.Name.ToString();
var imageSize = ImGui.GetFrameHeight();
ImGuiUtils.TextCentered($"You are missing the required equipment.");
ImGuiUtils.AlignCentered(imageSize + 5 + ImGui.CalcTextSize(itemName).X);
ImGui.AlignTextToFramePadding();
ImGui.Image(
Service.IconManager.GetIconCached(item.Icon).Handle,
new(imageSize)
);
ImGui.SameLine(0, 5);
ImGui.TextUnformatted(itemName);
}
break;
case CraftableStatus.RequiredStatus:
{
var status = RecipeData.Recipe.StatusRequired.Value!;
var statusName = status.Name.ToString();
var statusIcon = Service.IconManager.GetIconCached(status.Icon);
var imageSize = new Vector2(
ImGui.GetFrameHeight() * (statusIcon.AspectRatio ?? 1),
ImGui.GetFrameHeight()
);
ImGuiUtils.TextCentered($"You are missing the required status effect.");
ImGuiUtils.AlignCentered(imageSize.X + 5 + ImGui.CalcTextSize(statusName).X);
ImGui.AlignTextToFramePadding();
ImGui.Image(statusIcon.Handle, imageSize);
ImGui.SameLine(0, 5);
ImGui.TextUnformatted(statusName);
}
break;
case CraftableStatus.CraftsmanshipTooLow:
{
ImGuiUtils.TextCentered("Your Craftsmanship is too low.");
DrawRequiredStatsTable(
CharacterStats!.Craftsmanship,
RecipeData.Recipe.RequiredCraftsmanship
);
}
break;
case CraftableStatus.ControlTooLow:
{
ImGuiUtils.TextCentered("Your Control is too low.");
DrawRequiredStatsTable(
CharacterStats!.Control,
RecipeData.Recipe.RequiredControl
);
}
break;
case CraftableStatus.OK:
{
using var table = ImRaii.Table("characterStats", 2);
if (table)
{
ImGui.TableSetupColumn(
"",
ImGuiTableColumnFlags.WidthFixed,
100 * ImGuiHelpers.GlobalScale
);
ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableNextColumn();
ImGui.TextUnformatted("Craftsmanship");
ImGui.TableNextColumn();
ImGuiUtils.TextRight($"{CharacterStats!.Craftsmanship}");
ImGui.TableNextColumn();
ImGui.TextUnformatted("Control");
ImGui.TableNextColumn();
ImGuiUtils.TextRight($"{CharacterStats.Control}");
ImGui.TableNextColumn();
ImGui.TextUnformatted("CP");
ImGui.TableNextColumn();
ImGuiUtils.TextRight($"{CharacterStats.CP}");
}
}
break;
}
using var _spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
ImGui.Dummy(Vector2.Zero);
}
private void DrawRecipeStats()
{
ImGuiUtils.TextCentered("Recipe");
{
var textStars = new string('★', RecipeData!.Table.Stars);
var textStarsSize = Vector2.Zero;
if (!string.IsNullOrEmpty(textStars))
{
textStarsSize = AxisFont.CalcTextSize(textStars);
}
var textLevel =
SqText.LevelPrefix.ToIconChar()
+ SqText.ToLevelString(
RecipeData.AdjustedJobLevel ?? RecipeData.RecipeInfo.ClassJobLevel
);
var isExpert = RecipeData.RecipeInfo.IsExpert;
var isCollectable = RecipeData.IsCollectable;
var isAdjustable = RecipeData.AdjustedJobLevel.HasValue;
var imageSize = ImGui.GetFrameHeight();
var textSize = ImGui.GetFontSize();
var badgeSize = new Vector2(textSize * (ExpertBadge.AspectRatio ?? 1), textSize);
var badgeOffset = (imageSize - badgeSize.Y) / 2;
ImGuiUtils.AlignCentered(
imageSize
+ 5
+ ImGui.CalcTextSize(textLevel).X
+ (textStarsSize != Vector2.Zero ? textStarsSize.X + 3 : 0)
+ (isAdjustable ? imageSize + 3 : 0)
+ (isCollectable ? badgeSize.X + 3 : 0)
+ (isExpert ? badgeSize.X + 3 : 0)
);
ImGui.AlignTextToFramePadding();
ImGui.Image(
Service.IconManager.GetIconCached(RecipeData.Recipe.ItemResult.Value!.Icon).Handle,
new Vector2(imageSize)
);
ImGui.SameLine(0, 5);
ImGui.TextUnformatted(textLevel);
if (textStarsSize != Vector2.Zero)
{
ImGui.SameLine(0, 3);
// Aligns better
ImGui.SetCursorPosY(ImGui.GetCursorPosY() - 1);
AxisFont.Text(textStars);
}
if (isAdjustable)
{
ImGui.SameLine(0, 3);
ImGui.Image(CosmicExplorationBadge.Handle, new(imageSize));
if (ImGui.IsItemHovered())
ImGuiUtils.Tooltip($"Cosmic Exploration");
}
if (isCollectable)
{
ImGui.SameLine(0, 3);
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + badgeOffset);
ImGui.Image(CollectibleBadge.Handle, badgeSize);
if (ImGui.IsItemHovered())
ImGuiUtils.Tooltip($"Collectible");
}
if (isExpert)
{
ImGui.SameLine(0, 3);
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + badgeOffset);
ImGui.Image(ExpertBadge.Handle, badgeSize);
if (ImGui.IsItemHovered())
ImGuiUtils.Tooltip($"Expert Recipe");
}
}
ImGui.Separator();
using var table = ImRaii.Table("recipeStats", 2);
if (table)
{
ImGui.TableSetupColumn(
"",
ImGuiTableColumnFlags.WidthFixed,
100 * ImGuiHelpers.GlobalScale
);
ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableNextColumn();
ImGui.TextUnformatted("Progress");
ImGui.TableNextColumn();
ImGuiUtils.TextRight($"{RecipeData.RecipeInfo.MaxProgress}");
ImGui.TableNextColumn();
ImGui.TextUnformatted("Quality");
ImGui.TableNextColumn();
ImGuiUtils.TextRight($"{RecipeData.RecipeInfo.MaxQuality}");
ImGui.TableNextColumn();
ImGui.TextUnformatted("Durability");
ImGui.TableNextColumn();
ImGuiUtils.TextRight($"{RecipeData.RecipeInfo.MaxDurability}");
}
}
private enum MacroTaskType
{
Saved,
Suggested,
Community,
}
private record struct MacroTaskState
{
public MacroTaskType Type;
public Exception? Exception;
public bool Started;
public bool Completed;
public IReadOnlyList<ActionType>? Actions;
public string? MacroName;
public string? MacroUrl;
public SimulationState? State;
public Solver.Solver? Solver;
public Action<IEnumerable<ActionType>>? MacroEditorSetter;
}
private void DrawMacro(in MacroTaskState state, float panelWidth)
{
var panelTitle = state.Type switch
{
MacroTaskType.Saved => "Best Saved Macro",
MacroTaskType.Suggested => "Suggested Macro",
MacroTaskType.Community => "Best Community Macro",
_ => throw new ArgumentOutOfRangeException(
nameof(state),
"state.Type must have a valid type"
),
};
using var panel = ImRaii2.GroupPanel(panelTitle, panelWidth, out _);
if (!panel.Success)
return;
var stepsAvailWidthOffset = ImGui.GetContentRegionAvail().X - panelWidth;
var windowHeight = 2 * ImGui.GetFrameHeightWithSpacing();
if (!state.Started)
{
switch (state.Type)
{
case MacroTaskType.Saved:
throw new InvalidOperationException(
"Saved macro window should always be started or completed"
);
case MacroTaskType.Suggested:
{
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);
if (ImGui.Button("Generate"))
CalculateSuggestedMacro();
if (ImGui.IsItemHovered())
ImGuiUtils.TooltipWrapped(
"Suggest a way to finish the crafting recipe. "
+ "Results aren't perfect, and levels of success "
+ "can vary wildly depending on the solver's settings."
);
ImGui.SetCursorPos(
c + new Vector2(0, availSize.Y + ImGui.GetStyle().ItemSpacing.Y)
);
break;
}
case MacroTaskType.Community:
{
using var _padding = ImRaii.PushStyle(
ImGuiStyleVar.FramePadding,
ImGui.GetStyle().FramePadding * 2
);
var size =
ImGui.CalcTextSize("Search Online") + ImGui.GetStyle().FramePadding * 2;
var c = ImGui.GetCursorPos();
var availSize = new Vector2(
ImGui.GetContentRegionAvail().X - stepsAvailWidthOffset,
windowHeight
);
ImGuiUtils.AlignMiddle(size, availSize);
if (ImGui.Button("Search Online"))
CalculateCommunityMacro();
if (ImGui.IsItemHovered())
ImGuiUtils.TooltipWrapped(
"Searches FFXIV Teamcraft to find you the best macro"
);
ImGui.SetCursorPos(
c + new Vector2(0, availSize.Y + ImGui.GetStyle().ItemSpacing.Y)
);
break;
}
}
}
else if (!state.Completed)
{
switch (state.Type)
{
case MacroTaskType.Saved:
ImGuiUtils.TextMiddleNewLine(
"Calculating...",
new(
ImGui.GetContentRegionAvail().X - stepsAvailWidthOffset,
windowHeight + 1
)
);
break;
case MacroTaskType.Suggested:
{
if (state.Solver is not { } solver)
throw new ArgumentNullException(nameof(state), "Solver should not be null");
var calcTextSize = ImGui.CalcTextSize("Calculating...");
var spacing = ImGui.GetStyle().ItemSpacing.X;
var fraction = Math.Clamp(
(float)solver.ProgressValue / solver.ProgressMax,
0,
1
);
var c = ImGui.GetCursorPos();
ImGuiUtils.AlignCentered(
windowHeight + spacing + calcTextSize.X,
ImGui.GetContentRegionAvail().X - stepsAvailWidthOffset
);
if (Service.Configuration.ProgressType == Configuration.ProgressBarType.None)
{
var textSize = ImGui.CalcTextSize($"{fraction * 100:N0}%");
var cursor = ImGui.GetCursorPos();
ImGuiUtils.AlignMiddle(textSize, new(windowHeight));
ImGui.TextUnformatted($"{fraction * 100:N0}%");
ImGui.SetCursorPos(cursor);
ImGui.Dummy(new Vector2(windowHeight + 4));
}
else
{
var progressColors = Colors.GetSolverProgressColors(solver.ProgressStage);
ImGuiUtils.ArcProgress(
solver.IsIndeterminate ? (float)-ImGui.GetTime() : fraction,
windowHeight / 2f + 2,
.5f,
ImGui.ColorConvertFloat4ToU32(progressColors.Background),
ImGui.ColorConvertFloat4ToU32(progressColors.Foreground)
);
}
if (ImGui.IsItemHovered())
DynamicBars.DrawProgressBarTooltip(solver);
ImGui.SameLine(0, spacing);
ImGuiUtils.AlignMiddle(calcTextSize, new(calcTextSize.X, windowHeight));
ImGui.TextUnformatted("Calculating...");
ImGui.SetCursorPos(
c + new Vector2(0, windowHeight + ImGui.GetStyle().ItemSpacing.Y - 1)
);
break;
}
case MacroTaskType.Community:
ImGuiUtils.TextMiddleNewLine(
"Searching...",
new(
ImGui.GetContentRegionAvail().X - stepsAvailWidthOffset,
windowHeight + 1
)
);
break;
}
}
else if (state.Exception != null)
{
ImGui.AlignTextToFramePadding();
using (var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed))
ImGuiUtils.TextCentered("An exception occurred");
if (ImGuiUtils.ButtonCentered("Copy Error Message"))
ImGui.SetClipboardText(state.Exception.ToString());
}
else if (state.Actions is not { } actions || state.State is not { } simState)
{
switch (state.Type)
{
case MacroTaskType.Saved:
ImGuiUtils.TextMiddleNewLine(
"You have no macros!",
new(
ImGui.GetContentRegionAvail().X - stepsAvailWidthOffset,
windowHeight + 1
)
);
break;
case MacroTaskType.Suggested:
// Cancelled?
break;
case MacroTaskType.Community:
ImGuiUtils.TextMiddleNewLine(
"No macros found!",
new(
ImGui.GetContentRegionAvail().X - stepsAvailWidthOffset,
windowHeight + 1
)
);
break;
}
}
else
{
if (actions.Any(a => a.Category() == ActionCategory.Combo))
throw new InvalidOperationException("Combo actions should be sanitized away");
if (state.MacroName is { } macroName)
{
using var _ = ImRaii2.TextWrapPos(panelWidth);
if (state.MacroUrl is { } macroUrl)
{
ImGuiUtils.AlignCentered(ImGui.CalcTextSize(macroName).X, panelWidth);
ImGuiUtils.Hyperlink(macroName, macroUrl, false);
}
else
ImGuiUtils.TextCentered(macroName, panelWidth);
}
using var table = ImRaii.Table(
"table",
3,
ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.SizingStretchSame
);
if (table)
{
ImGui.TableSetupColumn("desc", ImGuiTableColumnFlags.WidthFixed, 0);
ImGui.TableSetupColumn("actions", ImGuiTableColumnFlags.WidthFixed, 0);
ImGui.TableSetupColumn("steps", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableNextRow(ImGuiTableRowFlags.None, windowHeight);
ImGui.TableNextColumn();
var spacing = ImGui.GetStyle().ItemSpacing.Y;
var miniRowHeight = (windowHeight - spacing) / 2f;
{
if (Service.Configuration.ShowOptimalMacroStat)
{
var progressHeight = windowHeight;
if (
simState.Progress >= simState.Input.Recipe.MaxProgress
&& simState.Input.Recipe.MaxQuality > 0
)
{
ImGuiUtils.ArcProgress(
(float)simState.Quality / simState.Input.Recipe.MaxQuality,
progressHeight / 2f,
.5f,
ImGui.GetColorU32(ImGuiCol.TableBorderLight),
ImGui.GetColorU32(Colors.Quality)
);
if (ImGui.IsItemHovered())
ImGuiUtils.Tooltip(
$"Quality: {simState.Quality} / {simState.Input.Recipe.MaxQuality}"
);
}
else
{
ImGuiUtils.ArcProgress(
(float)simState.Progress / simState.Input.Recipe.MaxProgress,
progressHeight / 2f,
.5f,
ImGui.GetColorU32(ImGuiCol.TableBorderLight),
ImGui.GetColorU32(Colors.Progress)
);
if (ImGui.IsItemHovered())
ImGuiUtils.Tooltip(
$"Progress: {simState.Progress} / {simState.Input.Recipe.MaxProgress}"
);
}
}
else
{
ImGuiUtils.ArcProgress(
(float)simState.Progress / simState.Input.Recipe.MaxProgress,
miniRowHeight / 2f,
.5f,
ImGui.GetColorU32(ImGuiCol.TableBorderLight),
ImGui.GetColorU32(Colors.Progress)
);
if (ImGui.IsItemHovered())
ImGuiUtils.Tooltip(
$"Progress: {simState.Progress} / {simState.Input.Recipe.MaxProgress}"
);
ImGui.SameLine(0, spacing);
ImGuiUtils.ArcProgress(
(float)simState.Quality / simState.Input.Recipe.MaxQuality,
miniRowHeight / 2f,
.5f,
ImGui.GetColorU32(ImGuiCol.TableBorderLight),
ImGui.GetColorU32(Colors.Quality)
);
if (ImGui.IsItemHovered())
ImGuiUtils.Tooltip(
$"Quality: {simState.Quality} / {simState.Input.Recipe.MaxQuality}"
);
ImGuiUtils.ArcProgress(
(float)simState.Durability / simState.Input.Recipe.MaxDurability,
miniRowHeight / 2f,
.5f,
ImGui.GetColorU32(ImGuiCol.TableBorderLight),
ImGui.GetColorU32(Colors.Durability)
);
if (ImGui.IsItemHovered())
ImGuiUtils.Tooltip(
$"Remaining Durability: {simState.Durability} / {simState.Input.Recipe.MaxDurability}"
);
ImGui.SameLine(0, spacing);
ImGuiUtils.ArcProgress(
(float)simState.CP / simState.Input.Stats.CP,
miniRowHeight / 2f,
.5f,
ImGui.GetColorU32(ImGuiCol.TableBorderLight),
ImGui.GetColorU32(Colors.CP)
);
if (ImGui.IsItemHovered())
ImGuiUtils.Tooltip(
$"Remaining CP: {simState.CP} / {simState.Input.Stats.CP}"
);
}
}
ImGui.TableNextColumn();
{
if (ImGuiUtils.IconButtonSquare(FontAwesomeIcon.Edit, miniRowHeight))
Service.Plugin.OpenMacroEditor(
CharacterStats!,
RecipeData!,
new(Service.Objects.LocalPlayer!.StatusList),
CalculateIngredientHqCounts(),
actions,
state.MacroEditorSetter
);
if (ImGui.IsItemHovered())
ImGuiUtils.Tooltip("Open in Macro Editor");
if (ImGuiUtils.IconButtonSquare(FontAwesomeIcon.Paste, miniRowHeight))
MacroCopy.Copy(actions);
if (ImGui.IsItemHovered())
ImGuiUtils.Tooltip("Copy to Clipboard");
}
ImGui.TableNextColumn();
{
var itemsPerRow = (int)
MathF.Floor(
(ImGui.GetContentRegionAvail().X - stepsAvailWidthOffset + spacing)
/ (miniRowHeight + spacing)
);
var itemCount = actions.Count;
for (var i = 0; i < itemsPerRow * 2; i++)
{
if (i % itemsPerRow != 0)
ImGui.SameLine(0, spacing);
if (i < itemCount)
{
var shouldShowMore = i + 1 == itemsPerRow * 2 && i + 1 < itemCount;
if (!shouldShowMore)
{
ImGui.Image(
actions[i].GetIcon(RecipeData!.ClassJob).Handle,
new(miniRowHeight)
);
if (ImGui.IsItemHovered())
ImGuiUtils.Tooltip(actions[i].GetName(RecipeData!.ClassJob));
}
else
{
var amtMore = itemCount - itemsPerRow * 2;
var pos = ImGui.GetCursorPos();
ImGui.Image(
actions[i].GetIcon(RecipeData!.ClassJob).Handle,
new(miniRowHeight),
default,
Vector2.One,
new(1, 1, 1, .5f)
);
if (ImGui.IsItemHovered())
ImGuiUtils.Tooltip(
$"{actions[i].GetName(RecipeData!.ClassJob)}\nand {amtMore} more"
);
ImGui.SetCursorPos(pos);
ImGui
.GetWindowDrawList()
.AddRectFilled(
ImGui.GetCursorScreenPos(),
ImGui.GetCursorScreenPos() + new Vector2(miniRowHeight),
ImGui.GetColorU32(ImGuiCol.FrameBg),
miniRowHeight / 8f
);
ImGui
.GetWindowDrawList()
.AddTextClippedEx(
ImGui.GetCursorScreenPos(),
ImGui.GetCursorScreenPos() + new Vector2(miniRowHeight),
$"+{amtMore}",
null,
new(.5f),
null
);
}
}
else
ImGui.Dummy(new(miniRowHeight));
}
}
}
}
}
private static void DrawRequiredStatsTable(int current, int required)
{
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(current, required);
using var table = ImRaii.Table("requiredStats", 2);
if (table)
{
ImGui.TableSetupColumn(
"",
ImGuiTableColumnFlags.WidthFixed,
100 * ImGuiHelpers.GlobalScale
);
ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableNextColumn();
ImGui.TextUnformatted("Current");
ImGui.TableNextColumn();
ImGui.TextColored(new Vector4(0, 1, 0, 1), $"{current}");
ImGui.TableNextColumn();
ImGui.TextUnformatted("Required");
ImGui.TableNextColumn();
ImGui.TextColored(new Vector4(1, 0, 0, 1), $"{required}");
ImGui.TableNextColumn();
ImGui.TextUnformatted("You need");
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{required - current}");
}
}
private CraftableStatus CalculateCraftStatus(Gearsets.GearsetItem[] gearItems)
{
if (RecipeData!.ClassJob.GetPlayerLevel() == 0)
return CraftableStatus.LockedClassJob;
if (PlayerState.Instance()->CurrentClassJobId != RecipeData.ClassJob.GetClassJobIndex())
return CraftableStatus.WrongClassJob;
if (RecipeData.Recipe.IsSpecializationRequired && !CharacterStats!.IsSpecialist)
return CraftableStatus.SpecialistRequired;
var itemRequired = RecipeData.Recipe.ItemRequired;
if (itemRequired.RowId != 0 && itemRequired.IsValid)
{
if (!gearItems.Any(i => Gearsets.IsItem(i, itemRequired.RowId)))
return CraftableStatus.RequiredItem;
}
var statusRequired = RecipeData.Recipe.StatusRequired;
if (statusRequired.RowId != 0 && statusRequired.IsValid)
{
if (
!Service.Objects.LocalPlayer!.StatusList.Any(s =>
s.StatusId == statusRequired.RowId
)
)
return CraftableStatus.RequiredStatus;
}
if (RecipeData.Recipe.RequiredCraftsmanship > CharacterStats!.Craftsmanship)
return CraftableStatus.CraftsmanshipTooLow;
if (RecipeData.Recipe.RequiredControl > CharacterStats.Control)
return CraftableStatus.ControlTooLow;
return CraftableStatus.OK;
}
private static (
string NpcName,
string Territory,
Vector2 MapLocation,
MapLinkPayload Payload
) ResolveLevelData(uint levelRowId)
{
var level = LuminaSheets.LevelSheet.GetRow(levelRowId);
var placeName = level.Territory.Value.PlaceName.Value.Name.ToString();
var location = WorldToMap2(new(level.X, level.Z), level.Map.Value!);
return (
ResolveNpcResidentName(level.Object.RowId),
placeName,
location,
new(level.Territory.RowId, level.Map.RowId, location.X, location.Y)
);
}
private static Vector2 WorldToMap2(Vector2 worldCoordinates, Lumina.Excel.Sheets.Map map)
{
return MapUtil.WorldToMap(worldCoordinates, map.OffsetX, map.OffsetY, map.SizeFactor);
}
private static string ResolveNpcResidentName(uint npcRowId)
{
return Service.SeStringEvaluator.EvaluateObjStr(ObjectKind.EventNpc, npcRowId);
}
private static string GetCoordinatesString(Vector2 pos)
{
return $"{pos.X.ToString("0.0", CultureInfo.InvariantCulture)}, {pos.Y.ToString("0.0", CultureInfo.InvariantCulture)}";
}
private static int? GetGearsetForJob(ClassJob job)
{
var gearsetModule = RaptureGearsetModule.Instance();
var i = -1;
foreach (ref var gearset in gearsetModule->Entries)
{
i++;
if (!gearset.Flags.HasFlag(RaptureGearsetModule.GearsetFlag.Exists))
continue;
if (gearset.Id != i)
continue;
if (gearset.ClassJob != job.GetClassJobIndex())
continue;
return i;
}
return null;
}
private void CalculateSavedMacro()
{
SavedMacroTask?.Cancel();
var hasDelineations = Gearsets.HasDelineations();
SavedMacroTask = new(token =>
{
var input = new SimulationInput(
CharacterStats!,
RecipeData!.RecipeInfo,
StartingQuality
);
var state = new SimulationState(input);
var config = Service.Configuration.RecipeNoteSolverConfig;
var canUseDelineations = !Service.Configuration.CheckDelineations || hasDelineations;
if (!canUseDelineations)
config = config.FilterSpecialistActions();
var mctsConfig = new MCTSConfig(config);
var simulator = new SimulatorNoRandom();
List<Macro> macros = [.. Service.Configuration.Macros];
token.ThrowIfCancellationRequested();
if (macros.Count == 0)
return (null, null);
var bestSaved = macros
.Select(macro =>
{
var (score, outState) = CommunityMacros.CommunityMacro.CalculateScore(
macro.Actions,
simulator,
in state,
in mctsConfig
);
return (macro, outState, score);
})
.MaxBy(m => m.score);
token.ThrowIfCancellationRequested();
return (bestSaved.macro, bestSaved.outState);
});
SavedMacroTask.Start();
}
private void CalculateSuggestedMacro()
{
SuggestedMacroTask?.Cancel();
var hasDelineations = Gearsets.HasDelineations();
SuggestedMacroTask = new(token =>
{
var input = new SimulationInput(
CharacterStats!,
RecipeData!.RecipeInfo,
StartingQuality
);
var state = new SimulationState(input);
var config = Service.Configuration.RecipeNoteSolverConfig;
var canUseDelineations = !Service.Configuration.CheckDelineations || hasDelineations;
if (!canUseDelineations)
config = config.FilterSpecialistActions();
token.ThrowIfCancellationRequested();
var solver = new Solver.Solver(config, state) { Token = token };
solver.OnLog += Log.Debug;
solver.OnWarn += t => Plugin.Plugin.DisplaySolverWarning(t);
BestMacroSolver = solver;
solver.Start();
var solution = solver.GetTask().GetAwaiter().GetResult();
token.ThrowIfCancellationRequested();
return solution;
});
SuggestedMacroTask.Start();
}
public void CalculateCommunityMacro()
{
CommunityMacroTask?.Cancel();
var hasDelineations = Gearsets.HasDelineations();
CommunityMacroTask = new(token =>
{
var input = new SimulationInput(
CharacterStats!,
RecipeData!.RecipeInfo,
StartingQuality
);
var state = new SimulationState(input);
var config = Service.Configuration.RecipeNoteSolverConfig;
var canUseDelineations = !Service.Configuration.CheckDelineations || hasDelineations;
if (!canUseDelineations)
config = config.FilterSpecialistActions();
var mctsConfig = new MCTSConfig(config);
var simulator = new SimulatorNoRandom();
var macros = Service
.CommunityMacros.RetrieveRotations((int)RecipeData.Table.RowId, token)
.GetAwaiter()
.GetResult();
token.ThrowIfCancellationRequested();
if (macros.Count == 0)
return (null, null);
var bestSaved = macros
.Select(macro =>
{
var (score, outState) = CommunityMacros.CommunityMacro.CalculateScore(
macro.Actions,
simulator,
in state,
in mctsConfig
);
return (macro, outState, score);
})
.MaxBy(m => m.score);
token.ThrowIfCancellationRequested();
return (bestSaved.macro, bestSaved.outState);
});
CommunityMacroTask.Start();
}
public void Dispose()
{
SavedMacroTask?.Dispose();
SuggestedMacroTask?.Dispose();
CommunityMacroTask?.Dispose();
Service.WindowSystem.RemoveWindow(this);
AxisFont?.Dispose();
ExpertBadge.Dispose();
CollectibleBadge.Dispose();
CosmicExplorationBadge.Dispose();
SplendorousBadge.Dispose();
SpecialistBadge.Dispose();
NoManipulationBadge.Dispose();
}
}