Files
Craftimizer/Craftimizer/Windows/MacroEditor.cs
T
Asriel Camora a041fc6ebf Update for 7.4
2025-12-21 23:55:42 -08:00

1749 lines
74 KiB
C#

using Craftimizer.Plugin;
using Craftimizer.Simulator;
using Craftimizer.Simulator.Actions;
using Craftimizer.Utils;
using Dalamud.Game.ClientState.Statuses;
using Dalamud.Game.Text;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.ImGuiNotification;
using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing;
using Dalamud.Bindings.ImGui;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Sim = Craftimizer.Simulator.Simulator;
using SimNoRandom = Craftimizer.Simulator.SimulatorNoRandom;
using Recipe = Lumina.Excel.Sheets.Recipe;
using Dalamud.Utility;
using Craftimizer.Solver;
namespace Craftimizer.Windows;
public sealed class MacroEditor : Window, IDisposable
{
private const ImGuiWindowFlags WindowFlags = ImGuiWindowFlags.None;
private CharacterStats characterStats = null!;
public CharacterStats CharacterStats
{
get => characterStats;
private set
{
characterStats = value with
{
Craftsmanship = Math.Clamp(value.Craftsmanship, 0, 9000),
Control = Math.Clamp(value.Control, 0, 9000),
CP = Math.Clamp(value.CP, 180, 1000),
Level = Math.Clamp(value.Level, 1, 100),
};
}
}
public RecipeData RecipeData { get; private set; }
public record CrafterBuffs
{
public (int Craftsmanship, int Control) FC { get; init; }
public (uint ItemId, bool IsHQ) Food { get; init; }
public (uint ItemId, bool IsHQ) Medicine { get; init; }
public CrafterBuffs(StatusList? statuses)
{
if (statuses == null)
return;
foreach (var status in statuses)
{
if (status.StatusId == 48)
Food = FoodStatus.ResolveFoodParam(status.Param) ?? default;
else if (status.StatusId == 49)
Medicine = FoodStatus.ResolveFoodParam(status.Param) ?? default;
else if (status.StatusId == 356)
FC = FC with { Craftsmanship = status.Param / 5 };
else if (status.StatusId == 357)
FC = FC with { Control = status.Param / 5 };
}
}
}
public CrafterBuffs Buffs { get; set; }
private List<int> HQIngredientCounts { get; set; }
private int StartingQuality => RecipeData.CalculateStartingQuality(HQIngredientCounts);
private SimulatedMacro Macro { get; set; } = new();
private SimulationState State => Macro.State;
private SimulatedMacro.Reliablity Reliability => Macro.GetReliability(RecipeData);
private ActionType[] DefaultActions { get; }
private Action<IEnumerable<ActionType>>? MacroSetter { get; set; }
private BackgroundTask<int>? SolverTask { get; set; }
private bool SolverRunning => (!SolverTask?.Completed) ?? false;
private Solver.Solver? SolverObject { get; set; }
private int? SolverStartStepCount { get; 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 ITextureIcon ManipulationBadge { get; }
private ILoadedTextureIcon WellFedBadge { get; }
private ILoadedTextureIcon MedicatedBadge { get; }
private ILoadedTextureIcon InControlBadge { get; }
private ILoadedTextureIcon EatFromTheHandBadge { get; }
private IFontHandle AxisFont { get; }
private string popupSaveAsMacroName = string.Empty;
private string popupImportText = string.Empty;
private string popupImportUrl = string.Empty;
private string popupImportError = string.Empty;
private CancellationTokenSource? popupImportUrlTokenSource;
private CommunityMacros.CommunityMacro? popupImportUrlMacro;
public MacroEditor(CharacterStats characterStats, RecipeData recipeData, CrafterBuffs buffs, IEnumerable<int>? ingredientHqCounts, IEnumerable<ActionType> actions, Action<IEnumerable<ActionType>>? setter) : base("Craftimizer Macro Editor", WindowFlags)
{
CharacterStats = characterStats;
RecipeData = recipeData;
Buffs = buffs;
MacroSetter = setter;
DefaultActions = [.. actions];
HQIngredientCounts = [.. ingredientHqCounts ?? Enumerable.Repeat(0, RecipeData.Ingredients.Count)];
RecalculateState();
foreach (var action in DefaultActions)
AddStep(action);
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");
ManipulationBadge = ActionType.Manipulation.GetIcon(RecipeData.ClassJob);
WellFedBadge = IconManager.GetIcon(LuminaSheets.StatusSheet.GetRow(48)!.Icon);
MedicatedBadge = IconManager.GetIcon(LuminaSheets.StatusSheet.GetRow(49)!.Icon);
InControlBadge = IconManager.GetIcon(LuminaSheets.StatusSheet.GetRow(356)!.Icon);
EatFromTheHandBadge = IconManager.GetIcon(LuminaSheets.StatusSheet.GetRow(357)!.Icon);
AxisFont = Service.PluginInterface.UiBuilder.FontAtlas.NewGameFontHandle(new(GameFontFamilyAndSize.Axis14));
IsOpen = true;
CollapsedCondition = ImGuiCond.Appearing;
Collapsed = false;
TitleBarButtons =
[
new()
{
Icon = FontAwesomeIcon.Cog,
IconOffset = new(2, 1),
Click = _ => Service.Plugin.OpenSettingsTab("Macro Editor"),
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!")
}
];
MinWindowHeight = float.PositiveInfinity;
Service.WindowSystem.AddWindow(this);
}
private float MinWindowHeight { get; set; }
private static ReadOnlySpan<(float Scale, int MinWidth)> MinWindowWidths =>
new[]
{
(0.80f, 715),
(0.90f, 745),
(0.95f, 775),
(1.00f, 805),
(1.10f, 865),
(1.25f, 944),
(1.50f, 1128),
(2.00f, 1504),
(3.00f, 2184),
};
public override void PreDraw()
{
base.PreDraw();
var scale = ImGuiHelpers.GlobalScale;
var widths = MinWindowWidths;
var height = MinWindowWidths[^1].MinWidth;
for (var i = 0; i < widths.Length; ++i)
{
if (scale <= widths[i].Scale)
{
if (i == 0)
height = widths[i].MinWidth;
else
height = (int)float.Lerp(
widths[i - 1].MinWidth, widths[i].MinWidth,
(scale - widths[i - 1].Scale) / (widths[i].Scale - widths[i - 1].Scale)
);
break;
}
}
ImGui.SetNextWindowSizeConstraints(new Vector2(height, MinWindowHeight), new Vector2(float.PositiveInfinity));
}
public override void OnClose()
{
base.OnClose();
SolverTask?.Cancel();
}
public override void Update()
{
Macro.FlushQueue();
}
public override void Draw()
{
var modifiedInput = false;
using (var table = ImRaii.Table("params", 2, ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.SizingStretchSame))
{
if (table)
{
ImGui.TableSetupColumn("col1", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableSetupColumn("col2", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableNextColumn();
modifiedInput = DrawCharacterParams();
ImGui.TableNextColumn();
modifiedInput |= DrawRecipeParams();
}
}
if (modifiedInput)
RecalculateState();
using (var table = ImRaii.Table("macroInfo", 2, ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.SizingStretchSame))
{
if (table)
{
ImGui.TableSetupColumn("col1", ImGuiTableColumnFlags.WidthStretch, 2);
ImGui.TableSetupColumn("col2", ImGuiTableColumnFlags.WidthStretch, 3);
ImGui.TableNextColumn();
DrawActionHotbars();
ImGui.TableNextColumn();
DrawMacroInfo();
DrawMacro();
}
}
}
private bool DrawCharacterParams()
{
var oldStats = CharacterStats;
ImGuiUtils.TextCentered("Crafter");
var textClassName = RecipeData.ClassJob.GetAbbreviation();
var textClassSize = AxisFont.CalcTextSize(textClassName);
var imageSize = ImGui.GetFrameHeight();
ImGuiUtils.AlignCentered(
imageSize + 5 +
textClassSize.X);
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);
AxisFont.Text(textClassName);
using (var statsTable = ImRaii.Table("stats", 3, ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.SizingStretchSame))
{
if (statsTable)
{
ImGui.TableSetupColumn("col1", ImGuiTableColumnFlags.WidthStretch, 4.5f);
ImGui.TableSetupColumn("col2", ImGuiTableColumnFlags.WidthStretch, 3);
ImGui.TableSetupColumn("col3", ImGuiTableColumnFlags.WidthStretch, 2);
var inputWidth = ImGui.CalcTextSize(SqText.ToLevelString(9999)).X + ImGui.GetStyle().FramePadding.X * 2 + 5;
void DrawStat(string name, int value, Action<int> setter)
{
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(name);
ImGui.SameLine(0, 5);
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
var text = value.ToString();
if (ImGui.InputText($"##{name}", ref text, 8, ImGuiInputTextFlags.AutoSelectAll | ImGuiInputTextFlags.CharsDecimal))
{
setter(
int.TryParse(text, out var newLevel)
? Math.Clamp(newLevel, 0, 9999)
: 0);
}
}
ImGui.TableNextColumn();
DrawStat("Craftsmanship", CharacterStats.Craftsmanship, v => CharacterStats = CharacterStats with { Craftsmanship = v });
ImGui.TableNextColumn();
DrawStat("Control", CharacterStats.Control, v => CharacterStats = CharacterStats with { Control = v });
ImGui.TableNextColumn();
DrawStat("CP", CharacterStats.CP, v => CharacterStats = CharacterStats with { CP = v });
}
}
using (var paramTable = ImRaii.Table("params", 3, ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.SizingStretchSame))
{
if (paramTable)
{
ImGui.TableSetupColumn("col1", ImGuiTableColumnFlags.WidthFixed);
ImGui.TableSetupColumn("col2", ImGuiTableColumnFlags.WidthStretch, 3);
ImGui.TableSetupColumn("col3", ImGuiTableColumnFlags.WidthStretch, 2);
ImGui.TableNextColumn();
ImGuiUtils.AlignCentered(GetLevelEntryWidth());
{
var level = CharacterStats.Level;
if (DrawLevelEntry(ref level))
{
CharacterStats = CharacterStats with
{
Level = level
};
}
}
var disabledTint = new Vector4(0.5f, 0.5f, 0.5f, 0.75f);
var imageButtonPadding = (int)(ImGui.GetStyle().FramePadding.Y / 2f);
var imageButtonSize = imageSize - imageButtonPadding * 2;
{
var splendorousLevel = 90;
if (CharacterStats.HasSplendorousBuff && splendorousLevel > CharacterStats.Level)
CharacterStats = CharacterStats with { HasSplendorousBuff = false };
using (var d = ImRaii.Disabled(splendorousLevel > CharacterStats.Level))
{
var v = CharacterStats.HasSplendorousBuff;
var tint = v ? Vector4.One : disabledTint;
if (ImGui.ImageButton(SplendorousBadge.Handle, new Vector2(imageButtonSize), default, Vector2.One, imageButtonPadding, default, tint))
CharacterStats = CharacterStats with { HasSplendorousBuff = !v };
}
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
ImGuiUtils.Tooltip(CharacterStats.HasSplendorousBuff ? $"Splendorous Tool" : "No Splendorous Tool");
}
ImGui.SameLine(0, 5);
bool? newIsSpecialist = null;
{
var v = CharacterStats.IsSpecialist;
var specialistLevel = 55;
if (CharacterStats.IsSpecialist && specialistLevel > CharacterStats.Level)
newIsSpecialist = v = false;
using (var d = ImRaii.Disabled(specialistLevel > CharacterStats.Level))
{
var tint = new Vector4(0.99f, 0.97f, 0.62f, 1f) * (v ? Vector4.One : disabledTint);
if (ImGui.ImageButton(SpecialistBadge.Handle, new Vector2(imageButtonSize), default, Vector2.One, imageButtonPadding, default, tint))
{
v = !v;
newIsSpecialist = v;
}
}
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
ImGuiUtils.Tooltip(v ? $"Specialist" : "Not a Specialist");
}
ImGui.SameLine(0, 5);
{
var manipLevel = ActionType.Manipulation.GetActionRow(RecipeData.ClassJob).Action!.Value.ClassJobLevel;
using (var d = ImRaii.Disabled(manipLevel > CharacterStats.Level))
{
var v = CharacterStats.CanUseManipulation && manipLevel <= CharacterStats.Level;
var tint = (v || manipLevel > CharacterStats.Level) ? disabledTint : Vector4.One;
if (ImGui.ImageButton((v ? ManipulationBadge : NoManipulationBadge).Handle, new Vector2(imageButtonSize), default, Vector2.One, imageButtonPadding, default, tint))
CharacterStats = CharacterStats with { CanUseManipulation = !v };
}
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
ImGuiUtils.Tooltip(CharacterStats.CanUseManipulation && manipLevel <= CharacterStats.Level ? $"Can Use Manipulation" : "Cannot Use Manipulation");
}
ImGui.TableNextColumn();
var buffBadgeSize = new Vector2(imageSize * (WellFedBadge.AspectRatio ?? 1), imageSize);
(uint ItemId, bool HQ)? newFoodBuff = null;
ImGui.Image(WellFedBadge.Handle, buffBadgeSize);
if (ImGui.IsItemHovered())
ImGuiUtils.Tooltip("Food");
ImGui.SameLine(0, 5);
{
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
using var combo = ImRaii.Combo("##food", FormatItemBuff(Buffs.Food));
if (ImGui.IsItemHovered())
ImGuiUtils.Tooltip(FormatItemBuffDescription(Buffs.Food));
if (combo)
{
if (ImGui.Selectable("None", Buffs.Food.ItemId == 0))
newFoodBuff = (0, false);
foreach (var food in FoodStatus.OrderedFoods)
{
var row = (food.Item.RowId, false);
if (ImGui.Selectable(FormatItemBuff(row), Buffs.Food == row))
newFoodBuff = row;
if (ImGui.IsItemHovered())
ImGuiUtils.Tooltip(FormatItemBuffDescription(row));
if (food.Item.CanBeHq)
{
row = (food.Item.RowId, true);
if (ImGui.Selectable(FormatItemBuff(row), Buffs.Food == row))
newFoodBuff = row;
if (ImGui.IsItemHovered())
ImGuiUtils.Tooltip(FormatItemBuffDescription(row));
}
}
}
}
(uint ItemId, bool HQ)? newMedicineBuff = null;
ImGui.Image(MedicatedBadge.Handle, buffBadgeSize);
if (ImGui.IsItemHovered())
ImGuiUtils.Tooltip("Medicine");
ImGui.SameLine(0, 5);
{
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
using var combo = ImRaii.Combo("##medicine", FormatItemBuff(Buffs.Medicine));
if (ImGui.IsItemHovered())
ImGuiUtils.Tooltip(FormatItemBuffDescription(Buffs.Medicine));
if (combo)
{
if (ImGui.Selectable("None", Buffs.Medicine.ItemId == 0))
newMedicineBuff = (0, false);
foreach (var medicine in FoodStatus.OrderedMedicines)
{
var row = (medicine.Item.RowId, false);
if (ImGui.Selectable(FormatItemBuff(row), Buffs.Medicine == row))
newMedicineBuff = row;
if (ImGui.IsItemHovered())
ImGuiUtils.Tooltip(FormatItemBuffDescription(row));
if (medicine.Item.CanBeHq)
{
row = (medicine.Item.RowId, true);
if (ImGui.Selectable(FormatItemBuff(row), Buffs.Medicine == row))
newMedicineBuff = row;
if (ImGui.IsItemHovered())
ImGuiUtils.Tooltip(FormatItemBuffDescription(row));
}
}
}
}
ImGui.TableNextColumn();
int? newFCCraftsmanshipBuff = null;
ImGui.Image(EatFromTheHandBadge.Handle, buffBadgeSize);
var fcBuffName = "Eat from the Hand";
var fcStatName = "Craftsmanship";
if (ImGui.IsItemHovered())
ImGuiUtils.Tooltip(fcBuffName);
ImGui.SameLine(0, 5);
{
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
using var combo = ImRaii.Combo("##fcCraftsmanship", FormatFCBuff(fcBuffName, Buffs.FC.Craftsmanship));
if (ImGui.IsItemHovered())
ImGuiUtils.Tooltip(FormatFCBuffDescription(fcBuffName, fcStatName, Buffs.FC.Craftsmanship));
if (combo)
{
if (ImGui.Selectable("None", Buffs.FC.Craftsmanship == 0))
newFCCraftsmanshipBuff = 0;
for (var i = 1; i <= 3; ++i)
{
if (ImGui.Selectable(FormatFCBuff(fcBuffName, i), Buffs.FC.Craftsmanship == i))
newFCCraftsmanshipBuff = i;
if (ImGui.IsItemHovered())
ImGuiUtils.Tooltip(FormatFCBuffDescription(fcBuffName, fcStatName, i));
}
}
}
int? newFCControlBuff = null;
ImGui.Image(InControlBadge.Handle, buffBadgeSize);
fcBuffName = "In Control";
fcStatName = "Control";
if (ImGui.IsItemHovered())
ImGuiUtils.Tooltip(fcBuffName);
ImGui.SameLine(0, 5);
{
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
using var combo = ImRaii.Combo("##fcControl", FormatFCBuff(fcBuffName, Buffs.FC.Control));
if (ImGui.IsItemHovered())
ImGuiUtils.Tooltip(FormatFCBuffDescription(fcBuffName, fcStatName, Buffs.FC.Control));
if (combo)
{
if (ImGui.Selectable("None", Buffs.FC.Control == 0))
newFCControlBuff = 0;
for (var i = 1; i <= 3; ++i)
{
if (ImGui.Selectable(FormatFCBuff(fcBuffName, i), Buffs.FC.Control == i))
newFCControlBuff = i;
if (ImGui.IsItemHovered())
ImGuiUtils.Tooltip(FormatFCBuffDescription(fcBuffName, fcStatName, i));
}
}
}
if (newIsSpecialist.HasValue || newFoodBuff.HasValue || newMedicineBuff.HasValue || newFCCraftsmanshipBuff.HasValue || newFCControlBuff.HasValue)
{
var baseStat = GetBaseStats(CharacterStats);
Buffs = Buffs with
{
Food = newFoodBuff ?? Buffs.Food,
Medicine = newMedicineBuff ?? Buffs.Medicine,
FC = (newFCCraftsmanshipBuff ?? Buffs.FC.Craftsmanship, newFCControlBuff ?? Buffs.FC.Control)
};
var newStats = CharacterStats with { Craftsmanship = baseStat.Craftsmanship, Control = baseStat.Control, CP = baseStat.CP };
if (newIsSpecialist is { } isSpecialist)
{
if (isSpecialist != CharacterStats.IsSpecialist)
{
var craftsmanship = 20;
var control = 20;
var cp = 15;
if (!isSpecialist)
{
craftsmanship *= -1;
control *= -1;
cp *= -1;
}
newStats = newStats with
{
IsSpecialist = isSpecialist,
Craftsmanship = newStats.Craftsmanship + craftsmanship,
Control = newStats.Control + control,
CP = newStats.CP + cp
};
}
}
var bonus = CalculateConsumableBonus(newStats);
CharacterStats = newStats with
{
Craftsmanship = newStats.Craftsmanship + bonus.Craftsmanship,
Control = newStats.Control + bonus.Control,
CP = newStats.CP + bonus.CP
};
}
}
}
return oldStats != CharacterStats;
}
private static string FormatItemBuff((uint ItemId, bool IsHQ) input)
{
if (input.ItemId == 0)
return "None";
var name = LuminaSheets.ItemSheet.GetRowOrDefault(input.ItemId)?.Name.ToString() ?? $"Unknown ({input.ItemId})";
return input.IsHQ ? $"{name} (HQ)" : name;
}
private static string FormatItemBuffDescription((uint ItemId, bool IsHQ) input)
{
var s = new StringBuilder(FormatItemBuff(input) + "\n");
void AddStat(string name, FoodStatus.FoodStat? statNullable)
{
if (statNullable is not { } stat)
return;
var (value, max) = input.IsHQ ? (stat.ValueHQ, stat.MaxHQ) : (stat.Value, stat.Max);
if (!stat.IsRelative)
s.AppendLine($"{name} +{value}");
else
s.AppendLine($"{name} +{value}% (Max {max})");
}
if (FoodStatus.TryGetFood(input.ItemId) is { } food)
{
AddStat("Craftsmanship", food.Craftsmanship);
AddStat("Control", food.Control);
AddStat("CP", food.CP);
}
return s.ToString();
}
private static string FormatFCBuff(string name, int level)
{
if (level == 0)
return "None";
return $"{name} {new string('I', level)}";
}
private static string FormatFCBuffDescription(string name, string statName, int level)
{
if (level == 0)
return FormatFCBuff(name, level);
return $"{FormatFCBuff(name, level)}\n{statName} +{level * 5}";
}
private (int Craftsmanship, int Control, int CP) GetBaseStats(CharacterStats stats)
{
var (craftsmanship, control, cp) = (stats.Craftsmanship, stats.Control, stats.CP);
craftsmanship -= Buffs.FC.Craftsmanship * 5;
control -= Buffs.FC.Control * 5;
var food = FoodStatus.TryGetFood(Buffs.Food.ItemId);
var medicine = FoodStatus.TryGetFood(Buffs.Medicine.ItemId);
static void GetBaseStat(ref int val, bool isHq, FoodStatus.FoodStat? food, out float a, out int b)
{
a = 1;
b = 0;
if (food is { } stat)
{
if (stat.IsRelative)
{
a = (isHq ? stat.ValueHQ : stat.Value) / 100f;
b = isHq ? stat.MaxHQ : stat.Max;
}
else
val -= isHq ? stat.ValueHQ : stat.Value;
}
}
static int GetBaseStat2(int val, bool foodHq, FoodStatus.FoodStat? food, bool medicineHq, FoodStatus.FoodStat? medicine)
{
GetBaseStat(ref val, foodHq, food, out var a, out var b);
GetBaseStat(ref val, medicineHq, medicine, out var c, out var d);
return CalculateBaseStat(val, a, b, c, d);
}
craftsmanship = GetBaseStat2(craftsmanship, Buffs.Food.IsHQ, food?.Craftsmanship, Buffs.Medicine.IsHQ, medicine?.Craftsmanship);
control = GetBaseStat2(control, Buffs.Food.IsHQ, food?.Control, Buffs.Medicine.IsHQ, medicine?.Control);
cp = GetBaseStat2(cp, Buffs.Food.IsHQ, food?.CP, Buffs.Medicine.IsHQ, medicine?.CP);
return (craftsmanship, control, cp);
}
private (int Craftsmanship, int Control, int CP) CalculateConsumableBonus(CharacterStats stats)
{
int craftsmanship = 0, control = 0, cp = 0;
static int CalculateStatBonus(int val, bool isHq, FoodStatus.FoodStat? food)
{
if (food is { } stat)
{
if (stat.IsRelative)
return (int)Math.Min((isHq ? stat.ValueHQ : stat.Value) / 100f * val, isHq ? stat.MaxHQ : stat.Max);
else
return isHq ? stat.ValueHQ : stat.Value;
}
return 0;
}
var food = FoodStatus.TryGetFood(Buffs.Food.ItemId);
craftsmanship += CalculateStatBonus(stats.Craftsmanship, Buffs.Food.IsHQ, food?.Craftsmanship);
control += CalculateStatBonus(stats.Control, Buffs.Food.IsHQ, food?.Control);
cp += CalculateStatBonus(stats.CP, Buffs.Food.IsHQ, food?.CP);
var medicine = FoodStatus.TryGetFood(Buffs.Medicine.ItemId);
craftsmanship += CalculateStatBonus(stats.Craftsmanship, Buffs.Medicine.IsHQ, medicine?.Craftsmanship);
control += CalculateStatBonus(stats.Control, Buffs.Medicine.IsHQ, medicine?.Control);
cp += CalculateStatBonus(stats.CP, Buffs.Medicine.IsHQ, medicine?.CP);
craftsmanship += Buffs.FC.Craftsmanship * 5;
control += Buffs.FC.Control * 5;
return (craftsmanship, control, cp);
}
// y: output stat
// a: coefficient
// b: max value for a product
// c: coefficient
// d: max value for c product
// Implementation of https://www.desmos.com/calculator/qlj9f9qjqy for calculating x from y
private static int CalculateBaseStat(int y, float a, int b, float c, int d)
{
if (y <= 0)
return 0;
if (d / c < b / a)
(a, b, c, d) = (c, d, a, b);
var dc = d / c;
var ba = b / a;
if (dc + b + d <= y)
return y - b - d;
else if (y <= (1 + a + c) * ba)
return (int)Math.Ceiling(y / (a + c + 1));
else
return (int)Math.Ceiling((y - b) / (c + 1));
}
private readonly struct RecipeWrapper(Recipe recipe) : IEquatable<RecipeWrapper>
{
public readonly Recipe Recipe = recipe;
public bool Equals(RecipeWrapper other) =>
Recipe.RowId == other.Recipe.RowId;
public override bool Equals(object? obj)
{
return obj is RecipeWrapper other && Equals(other);
}
public override int GetHashCode()
{
return unchecked((int)Recipe.RowId);
}
}
private readonly List<RecipeWrapper> searchableRecipes = [.. LuminaSheets.RecipeSheet.Where(r => r.RecipeLevelTable.RowId != 0 && r.ItemResult.RowId != 0).Select(r => new RecipeWrapper(r))];
private bool DrawRecipeParams()
{
var oldStartingQuality = StartingQuality;
var adjustedJobLevel = RecipeData.AdjustedJobLevel;
ImGuiUtils.TextCentered("Recipe");
var textStars = new string('★', RecipeData!.Table.Stars);
var textStarsSize = Vector2.Zero;
if (!string.IsNullOrEmpty(textStars))
textStarsSize = AxisFont.CalcTextSize(textStars);
string? textLevel = null;
float textLevelSize;
if (adjustedJobLevel is { } adjJobLevel)
{
textLevelSize = GetLevelEntryWidth();
}
else
{
textLevel = SqText.LevelPrefix.ToIconChar() + SqText.ToLevelString(RecipeData.RecipeInfo.ClassJobLevel);
textLevelSize = ImGui.CalcTextSize(textLevel).X;
}
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;
var rightSideWidth =
5 + textLevelSize +
(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);
ushort? newRecipe = null;
{
var recipe = new RecipeWrapper(RecipeData.Recipe);
using var lockedFontHandle = AxisFont.Available ? AxisFont.Lock() : null;
var fontHandle = lockedFontHandle?.ImFont ?? ImGui.GetFont();
if (ImGuiUtils.SearchableCombo(
"combo",
ref recipe,
searchableRecipes,
fontHandle,
ImGui.GetContentRegionAvail().X - rightSideWidth,
r => r.Recipe.ItemResult.Value.Name.ToString(),
r => r.Recipe.RowId.ToString(),
r =>
{
ImGui.TextUnformatted($"{r.Recipe.ItemResult.Value.Name}");
var classJob = (ClassJob)r.Recipe.CraftType.RowId;
var textLevel = SqText.LevelPrefix.ToIconChar() + SqText.ToLevelString(r.Recipe.RecipeLevelTable.Value!.ClassJobLevel);
var textLevelSize = ImGui.CalcTextSize(textLevel);
ImGui.SameLine();
var imageSize = fontHandle.FontSize;
ImGuiUtils.AlignRight(
imageSize + 5 +
textLevelSize.X,
ImGui.GetContentRegionAvail().X);
var uv0 = new Vector2(6, 3);
var uv1 = uv0 + new Vector2(44);
uv0 /= new Vector2(56);
uv1 /= new Vector2(56);
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + ImGui.GetStyle().FramePadding.Y / 2);
ImGui.Image(Service.IconManager.GetIconCached(classJob.GetIconId()).Handle, new Vector2(imageSize), uv0, uv1);
ImGui.SameLine(0, 5);
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (fontHandle.FontSize - textLevelSize.Y) / 2);
ImGui.TextUnformatted(textLevel);
}))
{
newRecipe = (ushort)recipe.Recipe.RowId;
}
}
ushort? newAdjustedJobLevel = null;
ImGui.SameLine(0, 5);
if (adjustedJobLevel.HasValue)
{
var level = (int)adjustedJobLevel.Value;
if (DrawLevelEntry(ref level))
{
newAdjustedJobLevel = (ushort)level;
}
}
else
{
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");
}
using (var statsTable = ImRaii.Table("stats", 3, ImGuiTableFlags.BordersInnerV))
{
if (statsTable)
{
ImGui.TableSetupColumn("col1", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableSetupColumn("col2", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableSetupColumn("col3", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Progress");
ImGui.SameLine();
ImGuiUtils.TextRight($"{RecipeData.RecipeInfo.MaxProgress}");
ImGui.TableNextColumn();
ImGui.TextUnformatted("Quality");
ImGui.SameLine();
ImGuiUtils.TextRight($"{RecipeData.RecipeInfo.MaxQuality}");
ImGui.TableNextColumn();
ImGui.TextUnformatted("Durability");
ImGui.SameLine();
ImGuiUtils.TextRight($"{RecipeData.RecipeInfo.MaxDurability}");
}
}
using (var table = ImRaii.Table("ingredientTable", 4, ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.SizingStretchSame))
{
if (table)
{
ImGui.TableSetupColumn("col1", ImGuiTableColumnFlags.WidthStretch, 2);
ImGui.TableSetupColumn("col2", ImGuiTableColumnFlags.WidthStretch, 2);
ImGui.TableSetupColumn("col3", ImGuiTableColumnFlags.WidthStretch, 2);
ImGui.TableSetupColumn("col4", ImGuiTableColumnFlags.WidthStretch, 2);
var ingredients = RecipeData.Ingredients.GetEnumerator();
var hqCount = HQIngredientCounts.GetEnumerator();
ImGui.TableNextColumn();
DrawIngredientHQEntry(0);
DrawIngredientHQEntry(1);
ImGui.TableNextColumn();
DrawIngredientHQEntry(2);
DrawIngredientHQEntry(3);
ImGui.TableNextColumn();
DrawIngredientHQEntry(4);
DrawIngredientHQEntry(5);
ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding();
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + ImGui.GetStyle().FramePadding.Y);
ImGuiUtils.TextCentered($"Starting Quality");
ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ImGui.GetStyle().FramePadding.Y);
ImGuiUtils.TextCentered($"{StartingQuality}");
}
}
var modified = false;
if (newAdjustedJobLevel is { } jobLevel)
{
RecipeData = new((ushort)RecipeData.Recipe.RowId, jobLevel);
modified = true;
}
if (newRecipe is { } recipeId)
{
RecipeData = new(recipeId, RecipeData.AdjustedJobLevel);
HQIngredientCounts.Clear();
HQIngredientCounts.AddRange(Enumerable.Repeat(0, RecipeData.Ingredients.Count));
modified = true;
}
if (oldStartingQuality != StartingQuality)
modified = true;
return modified;
}
private void DrawIngredientHQEntry(int idx)
{
if (idx >= RecipeData.Ingredients.Count)
{
ImGui.Dummy(new(0, ImGui.GetFrameHeight()));
return;
}
var ingredient = RecipeData.Ingredients[idx];
var hqCount = HQIngredientCounts[idx];
var canHq = ingredient.Item.CanBeHq;
var icon = Service.IconManager.GetIconCached(ingredient.Item.Icon, canHq);
var imageSize = ImGui.GetFrameHeight();
using (var d = ImRaii.Disabled(!canHq))
ImGui.Image(icon.Handle, new Vector2(imageSize));
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
{
if (canHq)
{
var perItem = RecipeData.CalculateItemStartingQuality(idx, 1);
var total = RecipeData.CalculateItemStartingQuality(idx, hqCount);
ImGuiUtils.Tooltip($"{ingredient.Item.Name} {SeIconChar.HighQuality.ToIconString()}\n+{perItem} Quality/Item{(total > 0 ? $"\n+{total} Quality" : "")}");
}
else if (ingredient.Amount != 0)
ImGuiUtils.Tooltip($"{ingredient.Item.Name}");
}
ImGui.SameLine(0, 5);
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - (5 + ImGui.CalcTextSize("/").X + 5 + ImGui.CalcTextSize($"99").X));
using var d2 = ImRaii.Disabled(!canHq);
if (canHq)
{
var text = hqCount.ToString();
if (ImGui.InputText($"##ingredient{idx}", ref text, 8, ImGuiInputTextFlags.AutoSelectAll | ImGuiInputTextFlags.CharsDecimal))
{
HQIngredientCounts[idx] =
int.TryParse(text, out var newCount)
? Math.Clamp(newCount, 0, ingredient.Amount)
: 0;
}
}
else
{
var text = ingredient.Amount.ToString();
ImGui.InputText($"##ingredient{idx}", ref text, 8, ImGuiInputTextFlags.AutoSelectAll | ImGuiInputTextFlags.CharsDecimal);
}
ImGui.SameLine(0, 5);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("/");
ImGui.SameLine(0, 5);
ImGui.AlignTextToFramePadding();
ImGuiUtils.TextCentered($"{ingredient.Amount}");
}
private const int MAX_LEVEL = 100;
private static float GetLevelEntryWidth()
{
var levelTextWidth = ImGui.CalcTextSize(SqText.ToLevelString(MAX_LEVEL)).X + ImGui.GetStyle().FramePadding.X * 2 + 5;
return ImGui.CalcTextSize(SqText.LevelPrefix.ToIconString()).X + 5 + levelTextWidth;
}
private static bool DrawLevelEntry(ref int level)
{
static int LevelInputCallback(ImGuiInputTextCallbackDataPtr data)
{
if (data.EventFlag == ImGuiInputTextFlags.CallbackCharFilter)
{
if (SqText.LevelNumReplacements.TryGetValue((char)data.EventChar, out var seChar))
data.EventChar = seChar.ToIconChar();
else
return 1;
}
return 0;
}
var levelTextWidth = ImGui.CalcTextSize(SqText.ToLevelString(MAX_LEVEL)).X + ImGui.GetStyle().FramePadding.X * 2 + 5;
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(SqText.LevelPrefix.ToIconString());
ImGui.SameLine(0, 3);
ImGui.SetNextItemWidth(levelTextWidth);
var levelText = SqText.ToLevelString(level);
var textChanged = ImGui.InputText("##levelText", ref levelText, 12, ImGuiInputTextFlags.CallbackCharFilter | ImGuiInputTextFlags.AutoSelectAll, LevelInputCallback);
if (textChanged)
{
var newLevel = SqText.TryParseLevelString(levelText, out var lv)
? Math.Clamp(lv, 1, MAX_LEVEL)
: 1;
if (newLevel != level)
{
level = newLevel;
return true;
}
}
return false;
}
private void DrawActionHotbars()
{
var sim = CreateSim(State);
var imageSize = ImGui.GetFrameHeight() * 2;
var spacing = ImGui.GetStyle().ItemSpacing.Y;
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);
using var _alpha = ImRaii.PushStyle(ImGuiStyleVar.DisabledAlpha, ImGui.GetStyle().DisabledAlpha * .5f);
foreach (var category in Enum.GetValues<ActionCategory>())
{
if (category == ActionCategory.Combo)
continue;
var actions = category.GetActions();
using var panel = ImRaii2.GroupPanel(category.GetDisplayName(), -1, out var availSpace);
var itemsPerRow = (int)MathF.Floor((availSpace + spacing) / (imageSize + spacing));
var itemCount = actions.Count;
var iterCount = (int)(Math.Ceiling((float)itemCount / itemsPerRow) * itemsPerRow);
for (var i = 0; i < iterCount; i++)
{
if (i % itemsPerRow != 0)
ImGui.SameLine(0, spacing);
if (i < itemCount)
{
var actionBase = actions[i].Base();
var canUse = actionBase.CanUse(sim);
if (ImGui.ImageButton(actions[i].GetIcon(RecipeData!.ClassJob).Handle, new(imageSize), default, Vector2.One, 0, default, !canUse ? new(1, 1, 1, ImGui.GetStyle().DisabledAlpha) : Vector4.One) && !SolverRunning)
AddStep(actions[i]);
if (!canUse &&
(CharacterStats.Level < actionBase.Level ||
(actions[i] == ActionType.Manipulation && !CharacterStats.CanUseManipulation) ||
(actions[i] is ActionType.HeartAndSoul or ActionType.CarefulObservation or ActionType.QuickInnovation && !CharacterStats.IsSpecialist)
)
)
{
Vector2 v1 = ImGui.GetItemRectMin(), v2 = ImGui.GetItemRectMax();
ImGui.PushClipRect(v1, v2, true);
(v1.X, v2.X) = (v2.X, v1.X);
ImGui.GetWindowDrawList().AddLine(v1, v2, ImGui.GetColorU32(new Vector4(1, 0, 0, ImGui.GetStyle().DisabledAlpha / 2)), 5 * ImGuiHelpers.GlobalScale);
ImGui.PopClipRect();
}
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
ImGuiUtils.Tooltip($"{actions[i].GetName(RecipeData!.ClassJob)}\n{actionBase.GetTooltip(sim, true)}");
using var _padding = ImRaii.PushStyle(ImGuiStyleVar.WindowPadding, Vector2.Zero);
using (var _source = ImRaii.DragDropSource())
{
if (_source)
{
ImGuiExtras.SetDragDropPayload("macroActionInsert", actions[i]);
ImGui.ImageButton(actions[i].GetIcon(RecipeData!.ClassJob).Handle, new(imageSize));
}
}
}
else
ImGui.Dummy(new(imageSize));
}
}
var minY = ImGui.GetCursorPosY() + ImGui.GetStyle().WindowPadding.Y - ImGui.GetStyle().CellPadding.Y;
if (MinWindowHeight != minY)
MinWindowHeight = minY;
}
private void DrawMacroInfo()
{
using (var barsTable = ImRaii.Table("simBars", 2, ImGuiTableFlags.SizingStretchSame))
{
if (barsTable)
{
ImGui.TableSetupColumn("col1", ImGuiTableColumnFlags.WidthStretch, 1);
ImGui.TableSetupColumn("col2", ImGuiTableColumnFlags.WidthStretch, 2);
ImGui.TableNextColumn();
void DrawCondition(DynamicBars.DrawerParams drawerParams)
{
var (totalSize, spacing) = drawerParams;
var condition = State.Condition;
var pos = ImGui.GetCursorPos();
using (var g = ImRaii.Group())
{
var availSize = totalSize - (spacing + ImGui.GetFrameHeight());
var size = ImGui.GetFrameHeight() + spacing + ImGui.CalcTextSize(condition.Name()).X;
ImGuiUtils.AlignCentered(size, availSize);
ImGui.GetWindowDrawList().AddCircleFilled(
ImGui.GetCursorScreenPos() + new Vector2(ImGui.GetFrameHeight() / 2),
ImGui.GetFrameHeight() / 2,
ImGui.ColorConvertFloat4ToU32(new Vector4(.35f, .35f, .35f, 0) + condition.GetColor(DateTime.UtcNow.TimeOfDay)));
ImGui.Dummy(new(ImGui.GetFrameHeight()));
ImGui.SameLine(0, spacing);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(condition.Name());
}
if (ImGui.IsItemHovered())
ImGuiUtils.Tooltip(condition.Description(CharacterStats.HasSplendorousBuff));
ImGui.SetCursorPos(pos);
ImGuiUtils.AlignRight(ImGui.GetFrameHeight(), totalSize);
using (var disabled = ImRaii.Disabled(SolverRunning))
{
using var tint = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled), !Service.Configuration.ConditionRandomness);
if (ImGuiUtils.IconButtonSquare(FontAwesomeIcon.Dice))
{
Service.Configuration.ConditionRandomness ^= true;
Service.Configuration.Save();
RecalculateState();
}
}
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
ImGuiUtils.TooltipWrapped($"Condition Randomness{(!Service.Configuration.ConditionRandomness ? " (Disabled)" : string.Empty)}\n" +
"Allows the condition to fluctuate randomly like a real craft. " +
"Turns off when generating a macro.");
}
var datas = new List<DynamicBars.BarData>(3)
{
new("Durability", Colors.Durability, State.Durability, RecipeData.RecipeInfo.MaxDurability),
new("Condition", DrawCondition)
};
if (RecipeData.IsCollectable)
datas.Add(new("Collectability", Colors.Collectability, Reliability.ParamScore, State.Collectability, State.MaxCollectability, RecipeData.CollectableThresholds, $"{State.Collectability}", $"{State.MaxCollectability:0}"));
else if (RecipeData.Recipe.RequiredQuality > 0)
{
var qualityPercent = (float)State.Quality / RecipeData.Recipe.RequiredQuality * 100;
datas.Add(new("Quality %", Colors.HQ, Reliability.ParamScore, qualityPercent, 100, null, $"{qualityPercent:0}%"));
}
else if (RecipeData.RecipeInfo.MaxQuality > 0)
datas.Add(new("HQ %", Colors.HQ, Reliability.ParamScore, State.HQPercent, 100, null, $"{State.HQPercent}%"));
DynamicBars.Draw(datas);
ImGui.TableNextColumn();
datas =
[
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)
datas.RemoveAt(1);
DynamicBars.Draw(datas);
}
}
using (var barsTable = ImRaii.Table("buffBars", 2, ImGuiTableFlags.SizingStretchSame))
{
if (barsTable)
{
ImGui.TableSetupColumn("col1", ImGuiTableColumnFlags.WidthStretch, 3);
ImGui.TableSetupColumn("col2", ImGuiTableColumnFlags.WidthStretch, 1);
var buffIconHeight = ImGui.GetFrameHeight() * 1.75f;
var buffDurationShift = buffIconHeight * .2f;
var buffHeight = buffIconHeight + ImGui.GetStyle().ItemSpacing.Y + ImGui.GetTextLineHeight() - buffDurationShift;
var infoHeight = 3 * ImGui.GetTextLineHeightWithSpacing();
var panelHeight = Math.Max(buffHeight, infoHeight);
ImGui.TableNextColumn();
using (ImRaii2.GroupPanel("Buffs", -1, out _))
{
using var _font = AxisFont.Push();
ImGui.Dummy(new(0, panelHeight));
ImGui.SameLine(0, 0);
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (panelHeight - buffHeight) / 2f);
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(buffIconHeight * (icon.AspectRatio ?? 1), buffIconHeight);
ImGui.Image(icon.Handle, size);
if (!effect.IsIndefinite())
{
ImGui.SetCursorPosY(ImGui.GetCursorPosY() - buffDurationShift);
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + 1);
ImGuiUtils.TextCentered($"{effects.GetDuration(effect)}", size.X);
}
}
if (ImGui.IsItemHovered())
{
var status = effect.Status();
using var _reset = ImRaii.DefaultFont();
ImGuiUtils.Tooltip($"{status.Name}\n{status.Description}");
}
ImGui.SameLine();
}
}
ImGui.TableNextColumn();
using (ImRaii2.GroupPanel("Info", -1, out _))
{
var actions = Macro.Actions.ToArray();
var waitTime = actions.Sum(a => a.Base().MacroWaitTime);
var waitTimeOptimal = waitTime - actions.Length;
var delineationCount = actions.Count(SolverConfig.SpecialistActions.Contains);
var height = (delineationCount == 0 ? 2 : 3) * ImGui.GetTextLineHeightWithSpacing();
ImGui.Dummy(new(0, panelHeight));
ImGui.SameLine(0, 0);
using (ImRaii.Group())
{
ImGui.SetCursorPosY(ImGui.GetCursorPos().Y + (panelHeight - height) / 2f);
ImGuiUtils.TextCentered($"{actions.Length} Step{(actions.Length != 1 ? "s" : string.Empty)}");
ImGuiUtils.TextCentered($"{waitTime} sec");
if (ImGui.IsItemHovered())
ImGuiUtils.Tooltip($"Optimal Time: {waitTimeOptimal:0.#} sec");
if (delineationCount != 0)
ImGuiUtils.TextCentered($"{delineationCount} Delineation{(delineationCount != 1 ? "s" : string.Empty)}");
}
}
}
}
}
private void DrawMacro()
{
var spacing = ImGui.GetStyle().ItemSpacing.X;
var imageSize = ImGui.GetFrameHeight() * 2;
var lastState = Macro.InitialState;
using var panel = ImRaii2.GroupPanel("Macro", -1, out var availSpace);
ImGui.Dummy(new(0, imageSize));
ImGui.SameLine(0, 0);
var macroActionsHeight = ImGui.GetFrameHeightWithSpacing() * (1 + (SolverRunning ? 1 : 0));
var childHeight = ImGui.GetContentRegionAvail().Y - ImGui.GetStyle().ItemSpacing.Y * 2 - ImGui.GetStyle().CellPadding.Y - macroActionsHeight - ImGui.GetStyle().ItemSpacing.Y * 2;
using (var child = ImRaii.Child("##macroActions", new(availSpace, childHeight)))
{
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);
using var _alpha = ImRaii.PushStyle(ImGuiStyleVar.DisabledAlpha, ImGui.GetStyle().DisabledAlpha * .5f);
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 (ImGui.ImageButton(action.GetIcon(RecipeData!.ClassJob).Handle, new(imageSize), default, Vector2.One, 0, default, failedAction ? new(1, 1, 1, ImGui.GetStyle().DisabledAlpha) : Vector4.One) && !SolverRunning)
RemoveStep(i);
if (response is ActionResponse.ActionNotUnlocked ||
(
failedAction &&
(CharacterStats.Level < actionBase.Level ||
(action == ActionType.Manipulation && !CharacterStats.CanUseManipulation) ||
(action is ActionType.HeartAndSoul or ActionType.CarefulObservation && !CharacterStats.IsSpecialist)
)
)
)
{
Vector2 v1 = ImGui.GetItemRectMin(), v2 = ImGui.GetItemRectMax();
ImGui.PushClipRect(v1, v2, true);
(v1.X, v2.X) = (v2.X, v1.X);
ImGui.GetWindowDrawList().AddLine(v1, v2, ImGui.GetColorU32(new Vector4(1, 0, 0, ImGui.GetStyle().DisabledAlpha / 2)), 5 * ImGuiHelpers.GlobalScale);
ImGui.PopClipRect();
}
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
ImGuiUtils.Tooltip($"{action.GetName(RecipeData!.ClassJob)}\n{actionBase.GetTooltip(CreateSim(lastState), true)}");
if (!SolverRunning)
{
using var _padding = ImRaii.PushStyle(ImGuiStyleVar.WindowPadding, Vector2.Zero);
using (var _source = ImRaii.DragDropSource())
{
if (_source)
{
ImGuiExtras.SetDragDropPayload("macroAction", i);
ImGui.ImageButton(action.GetIcon(RecipeData!.ClassJob).Handle, new(imageSize));
}
}
using (var _target = ImRaii.DragDropTarget())
{
if (_target)
{
if (ImGuiExtras.AcceptDragDropPayload("macroAction", out int j))
Macro.Move(j, i);
else if (ImGuiExtras.AcceptDragDropPayload("macroActionInsert", out ActionType newAction))
Macro.Insert(i, newAction);
}
}
}
lastState = state;
}
}
var pos = ImGui.GetCursorScreenPos();
ImGui.Dummy(default);
ImGui.GetWindowDrawList().AddLine(pos, pos + new Vector2(availSpace, 0), ImGui.GetColorU32(ImGuiCol.Border));
ImGui.Dummy(default);
if (SolverRunning && SolverObject is { } solver)
DynamicBars.DrawProgressBar(solver, availSpace);
DrawMacroActions(availSpace);
}
private void DrawMacroActions(float availWidth)
{
var height = ImGui.GetFrameHeight();
var spacing = ImGui.GetStyle().ItemSpacing.X;
var width = availWidth - ((spacing + height) * (3 + (DefaultActions.Length > 0 ? 1 : 0))); // small buttons at the end
var halfWidth = (width - spacing) / 2f;
var quarterWidth = (halfWidth - spacing) / 2f;
using (var _disabled = ImRaii.Disabled(SolverRunning))
{
if (MacroSetter != null)
{
if (ImGui.Button("Save", new(quarterWidth, height)))
SaveMacro();
ImGui.SameLine();
if (ImGui.Button("Save As", new(quarterWidth, height)))
ShowSaveAsPopup();
}
else
{
if (ImGui.Button("Save", new(halfWidth, height)))
ShowSaveAsPopup();
}
}
DrawSaveAsPopup();
ImGui.SameLine();
if (SolverRunning)
{
if (SolverTask?.Cancelling ?? false)
{
using var _disabled = ImRaii.Disabled();
ImGui.Button("Stopping", new(halfWidth, height));
}
else
{
if (ImGui.Button("Stop", new(halfWidth, height)))
SolverTask?.Cancel();
}
}
else
{
if (ImGui.Button(SolverStartStepCount.HasValue ? "Regenerate" : "Generate", new(halfWidth, height)))
CalculateBestMacro();
}
ImGui.SameLine();
if (ImGuiUtils.IconButtonSquare(FontAwesomeIcon.Paste))
MacroCopy.Copy(Macro.Actions.ToArray());
if (ImGui.IsItemHovered())
ImGuiUtils.Tooltip("Copy to Clipboard");
ImGui.SameLine();
using (var _disabled = ImRaii.Disabled(SolverRunning))
{
if (ImGuiUtils.IconButtonSquare(FontAwesomeIcon.FileImport))
ShowImportPopup();
}
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
ImGuiUtils.Tooltip("Import Macro");
DrawImportPopup();
ImGui.SameLine();
if (DefaultActions.Length > 0)
{
using (var _disabled = ImRaii.Disabled(SolverRunning))
{
if (ImGuiUtils.IconButtonSquare(FontAwesomeIcon.Undo))
{
SolverStartStepCount = null;
Macro.Clear();
foreach (var action in DefaultActions)
AddStep(action);
}
}
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
ImGuiUtils.Tooltip("Reset");
}
ImGui.SameLine();
using (var _disabled = ImRaii.Disabled(SolverRunning))
{
if (ImGuiUtils.IconButtonSquare(FontAwesomeIcon.Trash))
{
SolverStartStepCount = null;
Macro.Clear();
}
}
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
ImGuiUtils.Tooltip("Clear");
}
private void ShowSaveAsPopup()
{
ImGui.OpenPopup($"##saveAsPopup");
popupSaveAsMacroName = string.Empty;
ImGui.SetNextWindowPos(ImGui.GetMousePos() - new Vector2(ImGui.CalcItemWidth() * .25f, ImGui.GetFrameHeight() + ImGui.GetStyle().WindowPadding.Y * 2));
}
private void DrawSaveAsPopup()
{
using var popup = ImRaii.Popup($"##saveAsPopup");
if (popup)
{
if (ImGui.IsWindowAppearing())
ImGui.SetKeyboardFocusHere();
ImGui.SetNextItemWidth(ImGui.CalcItemWidth());
if (ImGui.InputTextWithHint($"##setName", "Name", ref popupSaveAsMacroName, 100, ImGuiInputTextFlags.AutoSelectAll | ImGuiInputTextFlags.EnterReturnsTrue))
{
if (!string.IsNullOrWhiteSpace(popupSaveAsMacroName))
{
var newMacro = new Macro() { Name = popupSaveAsMacroName, Actions = Macro.Actions.ToArray() };
Service.Configuration.AddMacro(newMacro);
MacroSetter = actions =>
{
newMacro.ActionEnumerable = actions;
Service.Configuration.Save();
};
ImGui.CloseCurrentPopup();
}
}
}
}
private void ShowImportPopup()
{
ImGui.OpenPopup($"##importPopup");
popupImportText = string.Empty;
popupImportUrl = string.Empty;
popupImportError = string.Empty;
popupImportUrlMacro = null;
popupImportUrlTokenSource = null;
}
private void DrawImportPopup()
{
const string ExampleMacro = "/mlock\n/ac \"Muscle Memory\" <wait.3>\n/ac Manipulation <wait.2>\n/ac Veneration <wait.2>\n/ac \"Waste Not II\" <wait.2>\n/ac Groundwork <wait.3>\n/ac Innovation <wait.2>\n/ac \"Preparatory Touch\" <wait.3>\n/ac \"Preparatory Touch\" <wait.3>\n/ac \"Preparatory Touch\" <wait.3>\n/ac \"Preparatory Touch\" <wait.3>\n/ac \"Great Strides\" <wait.2>\n/ac \"Byregot's Blessing\" <wait.3>\n/ac \"Careful Synthesis\" <wait.3>";
const string ExampleUrl = "https://ffxivteamcraft.com/simulator/39630/35499/9XOZDZKhbVXJUIPXjM63";
ImGui.SetNextWindowPos(ImGui.GetMainViewport().GetCenter(), ImGuiCond.Always, new Vector2(0.5f));
ImGui.SetNextWindowSizeConstraints(new(400, 0), new(float.PositiveInfinity));
using var popup = ImRaii.Popup($"##importPopup", ImGuiWindowFlags.Modal | ImGuiWindowFlags.NoMove);
if (popup)
{
bool submittedText, submittedUrl;
using (var panel = ImRaii2.GroupPanel("##text", -1, out var availWidth))
{
ImGui.AlignTextToFramePadding();
ImGuiUtils.TextCentered("Paste your macro here");
{
using var font = ImRaii.PushFont(UiBuilder.MonoFont);
ImGuiUtils.InputTextMultilineWithHint("", ExampleMacro, ref popupImportText, 2048, new(availWidth, ImGui.GetTextLineHeight() * 15 + ImGui.GetStyle().FramePadding.Y), ImGuiInputTextFlags.AutoSelectAll);
}
using (var _disabled = ImRaii.Disabled(popupImportUrlTokenSource != null))
submittedText = ImGui.Button("Import", new(availWidth, 0));
}
using (var panel = ImRaii2.GroupPanel("##url", -1, out var availWidth))
{
var availOffset = ImGui.GetContentRegionAvail().X - availWidth;
ImGui.AlignTextToFramePadding();
ImGuiUtils.TextCentered("or provide a url to it");
ImGui.SameLine();
using (var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudGrey))
{
using var font = ImRaii.PushFont(UiBuilder.IconFont);
ImGuiUtils.TextRight(FontAwesomeIcon.InfoCircle.ToIconString(), ImGui.GetContentRegionAvail().X - availOffset);
}
if (ImGui.IsItemHovered())
{
using var t = ImRaii.Tooltip();
ImGui.TextUnformatted("Supported sites:");
ImGui.BulletText("ffxivteamcraft.com");
ImGui.BulletText("craftingway.app");
ImGui.TextUnformatted("More suggestions are appreciated!");
}
ImGui.SetNextItemWidth(availWidth);
submittedUrl = ImGui.InputTextWithHint("", ExampleUrl, ref popupImportUrl, 2048, ImGuiInputTextFlags.AutoSelectAll | ImGuiInputTextFlags.EnterReturnsTrue);
using (var _disabled = ImRaii.Disabled(popupImportUrlTokenSource != null))
submittedUrl = ImGui.Button("Import", new(availWidth, 0)) || submittedUrl;
}
ImGui.Dummy(default);
if (!string.IsNullOrWhiteSpace(popupImportError))
{
using (var c = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed))
ImGui.TextWrapped(popupImportError);
ImGui.Dummy(default);
}
if (ImGuiUtils.ButtonCentered("Nevermind", new(ImGui.GetContentRegionAvail().X / 2f, 0)))
{
popupImportUrlTokenSource?.Cancel();
ImGui.CloseCurrentPopup();
}
if (popupImportUrlTokenSource == null)
{
if (submittedText)
{
if (MacroImport.TryParseMacro(popupImportText) is { } parsedActions)
{
popupImportUrlTokenSource?.Cancel();
Macro.Clear();
foreach (var action in parsedActions)
AddStep(action);
Plugin.Plugin.DisplayNotification(new()
{
Content = $"Imported macro with {parsedActions.Count} step{(parsedActions.Count != 1 ? "s" : "")}",
MinimizedText = $"Imported {parsedActions.Count} step macro",
Title = "Macro Imported",
Type = NotificationType.Success
});
popupImportUrlTokenSource?.Cancel();
ImGui.CloseCurrentPopup();
}
else
popupImportError = "Could not find any actions to import. Is it a valid macro?";
}
if (submittedUrl)
{
if (MacroImport.TryParseUrl(popupImportUrl, out _))
{
popupImportUrlTokenSource = new();
popupImportUrlMacro = null;
var token = popupImportUrlTokenSource.Token;
var url = popupImportUrl;
var task = Task.Run(() => MacroImport.RetrieveUrl(url, token), token);
_ = task.ContinueWith(t =>
{
if (token == popupImportUrlTokenSource.Token)
popupImportUrlTokenSource = null;
});
_ = task.ContinueWith(t =>
{
if (token.IsCancellationRequested)
return;
try
{
t.Exception!.Flatten().Handle(ex => ex is TaskCanceledException or OperationCanceledException);
}
catch (AggregateException e)
{
if (e.InnerExceptions.Count == 1)
popupImportError = e.InnerExceptions[0].Message;
else
popupImportError = e.Message;
Log.Error(e, "Retrieving macro failed");
}
}, TaskContinuationOptions.OnlyOnFaulted);
_ = task.ContinueWith(t => popupImportUrlMacro = t.Result, TaskContinuationOptions.OnlyOnRanToCompletion);
}
else
popupImportError = "The url is not in the right format for any supported sites.";
}
if (popupImportUrlMacro is { Name: var name, Actions: var actions })
{
Macro.Clear();
foreach (var action in actions)
AddStep(action);
Plugin.Plugin.DisplayNotification(new()
{
Content = $"Imported macro \"{name}\"",
Title = "Macro Imported",
Type = NotificationType.Success
});
popupImportUrlTokenSource?.Cancel();
ImGui.CloseCurrentPopup();
}
}
}
else
{
popupImportUrlTokenSource?.Cancel();
popupImportUrlTokenSource = null;
}
}
private void CalculateBestMacro()
{
SolverTask?.Cancel();
Macro.ClearQueue();
RevertPreviousMacro();
if (Service.Configuration.ConditionRandomness)
{
Service.Configuration.ConditionRandomness = false;
Service.Configuration.Save();
RecalculateState();
}
SolverStartStepCount = Macro.Count;
var state = State;
SolverTask = new(token => CalculateBestMacroTask(state, token, Gearsets.HasDelineations()));
SolverTask.Start();
}
private int CalculateBestMacroTask(SimulationState state, CancellationToken token, bool hasDelineations)
{
var config = Service.Configuration.EditorSolverConfig;
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);
solver.OnNewAction += a => Macro.Enqueue(a);
solver.OnSuggestSolution += a => Macro.EnqueueEphemeral(a.Actions);
SolverObject = solver;
solver.Start();
var t = solver.GetTask();
_ = t.ContinueWith(_ => Macro.RemoveEphemeral());
_ = t.GetAwaiter().GetResult();
token.ThrowIfCancellationRequested();
return 0;
}
private void RevertPreviousMacro()
{
if (SolverStartStepCount is { } stepCount && stepCount < Macro.Count)
Macro.RemoveRange(stepCount, Macro.Count - stepCount);
}
private void SaveMacro()
{
MacroSetter?.Invoke(Macro.Actions);
}
private void RecalculateState()
{
Macro.InitialState = new SimulationState(new(CharacterStats, RecipeData.RecipeInfo, StartingQuality));
}
private static Sim CreateSim(in SimulationState state) =>
Service.Configuration.ConditionRandomness ? new Sim() { State = state } : new SimNoRandom() { State = state };
private void AddStep(ActionType action)
{
if (SolverRunning)
throw new InvalidOperationException("Cannot add steps while solver is running");
if (!SolverRunning)
SolverStartStepCount = null;
Macro.Add(action);
}
private void RemoveStep(int index)
{
if (SolverRunning)
throw new InvalidOperationException("Cannot remove steps while solver is running");
SolverStartStepCount = null;
Macro.RemoveAt(index);
}
public void Dispose()
{
Service.WindowSystem.RemoveWindow(this);
ExpertBadge.Dispose();
CollectibleBadge.Dispose();
CosmicExplorationBadge.Dispose();
SplendorousBadge.Dispose();
SpecialistBadge.Dispose();
NoManipulationBadge.Dispose();
WellFedBadge.Dispose();
MedicatedBadge.Dispose();
InControlBadge.Dispose();
EatFromTheHandBadge.Dispose();
AxisFont.Dispose();
}
}