Files
Craftimizer/Craftimizer/Windows/MacroEditor.cs
T
2023-11-13 22:26:00 -08:00

1661 lines
71 KiB
C#

using Craftimizer.Plugin;
using Craftimizer.Plugin.Utils;
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.Internal;
using Dalamud.Interface.Internal.Notifications;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing;
using Dalamud.Utility;
using ImGuiNET;
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;
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, 90),
CLvl = Gearsets.CalculateCLvl(value.Level),
};
}
}
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 readonly record struct SimulationReliablity
{
public sealed class ParamReliability
{
private List<int> DataList { get; }
private ImGuiUtils.ViolinData? ViolinData { get; set; }
public int Max { get; private set; }
public int Min { get; private set; }
public float Median { get; private set; }
public float Average { get; private set; }
public ParamReliability()
{
DataList = new();
}
public void Add(int value)
{
DataList.Add(value);
}
public void FinalizeData()
{
if (DataList.Count == 0)
{
Average = Median = Max = Min = 0;
return;
}
Max = DataList.Max();
Min = DataList.Min();
if (DataList.Count % 2 == 0)
Median = (float)DataList.Order().Skip(DataList.Count / 2 - 1).Take(2).Average();
else
Median = DataList.Order().ElementAt(DataList.Count / 2);
Average = (float)DataList.Average();
}
public ImGuiUtils.ViolinData? GetViolinData(float barMax, int resolution, double bandwidth) =>
ViolinData ??=
Min != Max ?
new(DataList, 0, barMax, resolution, bandwidth) :
null;
}
public readonly ParamReliability Progress = new();
public readonly ParamReliability Quality = new();
// Param is either collectability, quality, or hq%, depending on the recipe
public readonly ParamReliability Param = new();
public SimulationReliablity(in SimulationState startState, IEnumerable<ActionType> actions, int iterCount, RecipeData recipeData)
{
Func<SimulationState, int> getParam;
if (recipeData.Recipe.ItemResult.Value!.IsCollectable)
getParam = s => s.Collectability;
else if (recipeData.Recipe.RequiredQuality > 0)
getParam = s => s.Quality;
else if (recipeData.RecipeInfo.MaxQuality > 0)
getParam = s => s.HQPercent;
else
getParam = s => 0;
for (var i = 0; i < iterCount; ++i)
{
var sim = new Sim(startState);
var (_, state, _) = sim.ExecuteMultiple(startState, actions);
Progress.Add(state.Progress);
Quality.Add(state.Quality);
Param.Add(getParam(state));
}
Progress.FinalizeData();
Quality.FinalizeData();
Param.FinalizeData();
}
}
private sealed record SimulatedActionStep
{
public ActionType Action { get; }
// State *after* executing the action
public ActionResponse Response { get; private set; }
public SimulationState State { get; private set; }
private SimulationReliablity? Reliability { get; set; }
public SimulatedActionStep(ActionType action, Sim sim, in SimulationState lastState, out SimulationState newState)
{
Action = action;
newState = Recalculate(sim, lastState);
}
public SimulationState Recalculate(Sim sim, in SimulationState lastState)
{
(Response, State) = sim.Execute(lastState, Action);
Reliability = null;
return State;
}
public SimulationReliablity GetReliability(in SimulationState initialState, IEnumerable<ActionType> actionSet, RecipeData recipeData) =>
Reliability ??=
new(initialState, actionSet, Service.Configuration.ReliabilitySimulationCount, recipeData);
};
private List<SimulatedActionStep> Macro { get; set; } = new();
private SimulationState InitialState { get; set; }
private SimulationState State => Macro.Count > 0 ? Macro[^1].State : InitialState;
private SimulationReliablity Reliability => Macro.Count > 0 ? Macro[^1].GetReliability(InitialState, Macro.Select(m => m.Action), RecipeData) : new(InitialState, Array.Empty<ActionType>(), 0, RecipeData);
private ActionType[] DefaultActions { get; }
private Action<IEnumerable<ActionType>>? MacroSetter { get; set; }
private CancellationTokenSource? SolverTokenSource { get; set; }
private Exception? SolverException { get; set; }
private int? SolverStartStepCount { get; set; }
private bool SolverRunning => SolverTokenSource != null;
private IDalamudTextureWrap ExpertBadge { get; }
private IDalamudTextureWrap CollectibleBadge { get; }
private IDalamudTextureWrap SplendorousBadge { get; }
private IDalamudTextureWrap SpecialistBadge { get; }
private IDalamudTextureWrap NoManipulationBadge { get; }
private IDalamudTextureWrap ManipulationBadge { get; }
private IDalamudTextureWrap WellFedBadge { get; }
private IDalamudTextureWrap MedicatedBadge { get; }
private IDalamudTextureWrap InControlBadge { get; }
private IDalamudTextureWrap EatFromTheHandBadge { get; }
private GameFontHandle 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 MacroImport.RetrievedMacro? popupImportUrlMacro;
public MacroEditor(CharacterStats characterStats, RecipeData recipeData, CrafterBuffs buffs, IEnumerable<ActionType> actions, Action<IEnumerable<ActionType>>? setter) : base("Craftimizer Macro Editor", WindowFlags)
{
CharacterStats = characterStats;
RecipeData = recipeData;
Buffs = buffs;
MacroSetter = setter;
DefaultActions = actions.ToArray();
HQIngredientCounts = new();
HQIngredientCounts.AddRange(Enumerable.Repeat(0, RecipeData.Ingredients.Count));
RecalculateState();
foreach (var action in DefaultActions)
AddStep(action);
ExpertBadge = Service.IconManager.GetAssemblyTexture("Graphics.expert_badge.png");
CollectibleBadge = Service.IconManager.GetAssemblyTexture("Graphics.collectible_badge.png");
SplendorousBadge = Service.IconManager.GetAssemblyTexture("Graphics.splendorous.png");
SpecialistBadge = Service.IconManager.GetAssemblyTexture("Graphics.specialist.png");
NoManipulationBadge = Service.IconManager.GetAssemblyTexture("Graphics.no_manip.png");
ManipulationBadge = ActionType.Manipulation.GetIcon(RecipeData.ClassJob);
WellFedBadge = Service.IconManager.GetIcon(LuminaSheets.StatusSheet.GetRow(48)!.Icon);
MedicatedBadge = Service.IconManager.GetIcon(LuminaSheets.StatusSheet.GetRow(49)!.Icon);
InControlBadge = Service.IconManager.GetIcon(LuminaSheets.StatusSheet.GetRow(356)!.Icon);
EatFromTheHandBadge = Service.IconManager.GetIcon(LuminaSheets.StatusSheet.GetRow(357)!.Icon);
AxisFont = Service.PluginInterface.UiBuilder.GetGameFontHandle(new(GameFontFamilyAndSize.Axis14));
IsOpen = true;
CollapsedCondition = ImGuiCond.Appearing;
Collapsed = false;
SizeConstraints = new() { MinimumSize = new(821, 750), MaximumSize = new(float.PositiveInfinity) };
Service.WindowSystem.AddWindow(this);
}
public override void OnClose()
{
SolverTokenSource?.Cancel();
}
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();
ImGui.Separator();
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();
Vector2 textClassSize;
{
var layout = AxisFont.LayoutBuilder(textClassName).Build();
textClassSize = new(layout.Width, layout.Height);
}
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.GetIcon(RecipeData.ClassJob.GetIconId()).ImGuiHandle, 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.Text(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();
var levelTextWidth = ImGui.CalcTextSize(SqText.ToLevelString(99)).X + ImGui.GetStyle().FramePadding.X * 2 + 5;
ImGuiUtils.AlignCentered(
ImGui.CalcTextSize(SqText.LevelPrefix.ToIconString()).X + 5 +
levelTextWidth);
ImGui.AlignTextToFramePadding();
ImGui.Text(SqText.LevelPrefix.ToIconString());
ImGui.SameLine(0, 3);
ImGui.SetNextItemWidth(levelTextWidth);
var levelText = SqText.ToLevelString(CharacterStats.Level);
bool textChanged;
unsafe
{
textChanged = ImGui.InputText("##levelText", ref levelText, 8, ImGuiInputTextFlags.CallbackCharFilter | ImGuiInputTextFlags.AutoSelectAll, LevelInputCallback);
}
if (textChanged)
CharacterStats = CharacterStats with
{
Level =
SqText.TryParseLevelString(levelText, out var newLevel)
? Math.Clamp(newLevel, 1, 90)
: 1
};
if (ImGui.IsItemHovered())
ImGui.SetTooltip($"CLvl {Gearsets.CalculateCLvl(CharacterStats.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 v = CharacterStats.HasSplendorousBuff;
var tint = v ? Vector4.One : disabledTint;
if (ImGui.ImageButton(SplendorousBadge.ImGuiHandle, new Vector2(imageButtonSize), default, Vector2.One, imageButtonPadding, default, tint))
CharacterStats = CharacterStats with { HasSplendorousBuff = !v };
if (ImGui.IsItemHovered())
ImGui.SetTooltip(CharacterStats.HasSplendorousBuff ? $"Splendorous Tool" : "No Splendorous Tool");
}
ImGui.SameLine(0, 5);
bool? newIsSpecialist = null;
{
var v = CharacterStats.IsSpecialist;
var tint = new Vector4(0.99f, 0.97f, 0.62f, 1f) * (v ? Vector4.One : disabledTint);
if (ImGui.ImageButton(SpecialistBadge.ImGuiHandle, new Vector2(imageButtonSize), default, Vector2.One, imageButtonPadding, default, tint))
{
v = !v;
newIsSpecialist = v;
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip(v ? $"Specialist" : "Not a Specialist");
}
ImGui.SameLine(0, 5);
{
var manipLevel = ActionType.Manipulation.GetActionRow(RecipeData.ClassJob).Action!.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.ImGuiHandle : NoManipulationBadge.ImGuiHandle, new Vector2(imageButtonSize), default, Vector2.One, imageButtonPadding, default, tint))
CharacterStats = CharacterStats with { CanUseManipulation = !v };
}
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
ImGui.SetTooltip(CharacterStats.CanUseManipulation && manipLevel <= CharacterStats.Level ? $"Can Use Manipulation" : "Cannot Use Manipulation");
}
ImGui.TableNextColumn();
(uint ItemId, bool HQ)? newFoodBuff = null;
var buffImageSize = new Vector2(imageSize * WellFedBadge.Width / WellFedBadge.Height, imageSize);
ImGui.Image(WellFedBadge.ImGuiHandle, buffImageSize);
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Food");
ImGui.SameLine(0, 5);
{
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
using var combo = ImRaii.Combo("##food", FormatItemBuff(Buffs.Food));
if (ImGui.IsItemHovered())
ImGui.SetTooltip(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())
ImGui.SetTooltip(FormatItemBuffDescription(row));
if (food.Item.CanBeHq)
{
row = (food.Item.RowId, true);
if (ImGui.Selectable(FormatItemBuff(row), Buffs.Food == row))
newFoodBuff = row;
if (ImGui.IsItemHovered())
ImGui.SetTooltip(FormatItemBuffDescription(row));
}
}
}
}
(uint ItemId, bool HQ)? newMedicineBuff = null;
buffImageSize = new Vector2(imageSize * MedicatedBadge.Width / MedicatedBadge.Height, imageSize);
ImGui.Image(MedicatedBadge.ImGuiHandle, buffImageSize);
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Medicine");
ImGui.SameLine(0, 5);
{
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
using var combo = ImRaii.Combo("##medicine", FormatItemBuff(Buffs.Medicine));
if (ImGui.IsItemHovered())
ImGui.SetTooltip(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())
ImGui.SetTooltip(FormatItemBuffDescription(row));
if (medicine.Item.CanBeHq)
{
row = (medicine.Item.RowId, true);
if (ImGui.Selectable(FormatItemBuff(row), Buffs.Medicine == row))
newMedicineBuff = row;
if (ImGui.IsItemHovered())
ImGui.SetTooltip(FormatItemBuffDescription(row));
}
}
}
}
ImGui.TableNextColumn();
int? newFCCraftsmanshipBuff = null;
buffImageSize = new Vector2(imageSize * MedicatedBadge.Width / MedicatedBadge.Height, imageSize);
ImGui.Image(EatFromTheHandBadge.ImGuiHandle, buffImageSize);
var fcBuffName = "Eat from the Hand";
var fcStatName = "Craftsmanship";
if (ImGui.IsItemHovered())
ImGui.SetTooltip(fcBuffName);
ImGui.SameLine(0, 5);
{
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
using var combo = ImRaii.Combo("##fcCraftsmanship", FormatFCBuff(fcBuffName, Buffs.FC.Craftsmanship));
if (ImGui.IsItemHovered())
ImGui.SetTooltip(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())
ImGui.SetTooltip(FormatFCBuffDescription(fcBuffName, fcStatName, i));
}
}
}
int? newFCControlBuff = null;
buffImageSize = new Vector2(imageSize * MedicatedBadge.Width / MedicatedBadge.Height, imageSize);
ImGui.Image(InControlBadge.ImGuiHandle, buffImageSize);
fcBuffName = "In Control";
fcStatName = "Control";
if (ImGui.IsItemHovered())
ImGui.SetTooltip(fcBuffName);
ImGui.SameLine(0, 5);
{
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
using var combo = ImRaii.Combo("##fcControl", FormatFCBuff(fcBuffName, Buffs.FC.Control));
if (ImGui.IsItemHovered())
ImGui.SetTooltip(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())
ImGui.SetTooltip(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 unsafe int LevelInputCallback(ImGuiInputTextCallbackData* 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;
}
private static string FormatItemBuff((uint ItemId, bool IsHQ) input)
{
if (input.ItemId == 0)
return "None";
var name = LuminaSheets.ItemSheet.GetRow(input.ItemId)?.Name.ToDalamudString().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 bool DrawRecipeParams()
{
var oldStartingQuality = StartingQuality;
ImGuiUtils.TextCentered("Recipe");
var textStars = new string('★', RecipeData!.Table.Stars);
var textStarsSize = Vector2.Zero;
if (!string.IsNullOrEmpty(textStars))
{
var layout = AxisFont.LayoutBuilder(textStars).Build();
textStarsSize = new(layout.Width, layout.Height);
}
var textLevel = SqText.LevelPrefix.ToIconChar() + SqText.ToLevelString(RecipeData.RecipeInfo.ClassJobLevel);
var isExpert = RecipeData.RecipeInfo.IsExpert;
var isCollectable = RecipeData.Recipe.ItemResult.Value!.IsCollectable;
var imageSize = ImGui.GetFrameHeight();
var textSize = ImGui.GetFontSize();
var badgeSize = new Vector2(textSize * ExpertBadge.Width / ExpertBadge.Height, textSize);
var badgeOffset = (imageSize - badgeSize.Y) / 2;
var rightSideWidth =
5 + ImGui.CalcTextSize(textLevel).X +
(textStarsSize != Vector2.Zero ? textStarsSize.X + 3 : 0) +
(isCollectable ? badgeSize.X + 3 : 0) +
(isExpert ? badgeSize.X + 3 : 0);
ImGui.AlignTextToFramePadding();
ImGui.Image(Service.IconManager.GetIcon(RecipeData.Recipe.ItemResult.Value!.Icon).ImGuiHandle, new Vector2(imageSize));
ImGui.SameLine(0, 5);
ushort? newRecipe = null;
{
var recipe = RecipeData.Recipe;
if (ImGuiUtils.SearchableCombo(
"combo",
ref recipe,
LuminaSheets.RecipeSheet.Where(r => r.RecipeLevelTable.Row != 0 && r.ItemResult.Row != 0),
AxisFont.ImFont,
ImGui.GetContentRegionAvail().X - rightSideWidth,
r => r.ItemResult.Value!.Name.ToDalamudString().ToString(),
r => r.RowId.ToString(),
r =>
{
ImGui.TextUnformatted($"{r.ItemResult.Value!.Name.ToDalamudString()}");
var classJob = (ClassJob)r.CraftType.Row;
var textLevel = SqText.LevelPrefix.ToIconChar() + SqText.ToLevelString(r.RecipeLevelTable.Value!.ClassJobLevel);
var textLevelSize = ImGui.CalcTextSize(textLevel);
ImGui.SameLine();
var imageSize = AxisFont.ImFont.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.GetIcon(classJob.GetIconId()).ImGuiHandle, new Vector2(imageSize), uv0, uv1);
ImGui.SameLine(0, 5);
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (AxisFont.ImFont.FontSize - textLevelSize.Y) / 2);
ImGui.Text(textLevel);
}))
{
newRecipe = (ushort)recipe.RowId;
}
}
ImGui.SameLine(0, 5);
ImGui.Text(textLevel);
if (ImGui.IsItemHovered())
ImGui.SetTooltip($"RLvl {RecipeData.RecipeInfo.RLvl}");
if (textStarsSize != Vector2.Zero)
{
ImGui.SameLine(0, 3);
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (imageSize - textStarsSize.Y) / 2);
AxisFont.Text(textStars);
}
if (isCollectable)
{
ImGui.SameLine(0, 3);
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + badgeOffset);
ImGui.Image(CollectibleBadge.ImGuiHandle, badgeSize);
if (ImGui.IsItemHovered())
ImGui.SetTooltip($"Collectible");
}
if (isExpert)
{
ImGui.SameLine(0, 3);
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + badgeOffset);
ImGui.Image(ExpertBadge.ImGuiHandle, badgeSize);
if (ImGui.IsItemHovered())
ImGui.SetTooltip($"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.Text("Progress");
ImGui.SameLine();
ImGuiUtils.TextRight($"{RecipeData.RecipeInfo.MaxProgress}");
ImGui.TableNextColumn();
ImGui.Text("Quality");
ImGui.SameLine();
ImGuiUtils.TextRight($"{RecipeData.RecipeInfo.MaxQuality}");
ImGui.TableNextColumn();
ImGui.Text("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}");
}
}
if (newRecipe is { } recipeId)
{
RecipeData = new(recipeId);
HQIngredientCounts.Clear();
HQIngredientCounts.AddRange(Enumerable.Repeat(0, RecipeData.Ingredients.Count));
return true;
}
return oldStartingQuality != StartingQuality;
}
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.GetHqIcon(ingredient.Item.Icon, canHq);
var imageSize = ImGui.GetFrameHeight();
using (var d = ImRaii.Disabled(!canHq))
ImGui.Image(icon.ImGuiHandle, new Vector2(imageSize));
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
{
if (canHq)
{
var perItem = RecipeData.CalculateItemStartingQuality(idx, 1);
var total = RecipeData.CalculateItemStartingQuality(idx, hqCount);
ImGui.SetTooltip($"{ingredient.Item.Name.ToDalamudString()} {SeIconChar.HighQuality.ToIconString()}\n+{perItem} Quality/Item{(total > 0 ? $"\n+{total} Quality" : "")}");
}
else
ImGui.SetTooltip($"{ingredient.Item.Name.ToDalamudString()}");
}
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.Text("/");
ImGui.SameLine(0, 5);
ImGui.AlignTextToFramePadding();
ImGuiUtils.TextCentered($"{ingredient.Amount}");
}
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).ImGuiHandle, new(imageSize), default, Vector2.One, 0, default, !canUse ? new(1, 1, 1, ImGui.GetStyle().DisabledAlpha) : Vector4.One))
AddStep(actions[i]);
if (!canUse &&
(CharacterStats.Level < actionBase.Level ||
(actions[i] == ActionType.Manipulation && !CharacterStats.CanUseManipulation) ||
(actions[i] 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);
ImGui.PopClipRect();
}
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
ImGui.SetTooltip($"{actions[i].GetName(RecipeData!.ClassJob)}\n{actionBase.GetTooltip(sim, true)}");
}
else
ImGui.Dummy(new(imageSize));
}
}
var minY = ImGui.GetCursorPosY() + ImGui.GetStyle().WindowPadding.Y - ImGui.GetStyle().CellPadding.Y;
if (SizeConstraints!.Value.MinimumSize.Y != minY)
SizeConstraints = SizeConstraints.Value with { MinimumSize = SizeConstraints.Value.MinimumSize with { Y = 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();
var datas = new List<BarData>(3)
{
new("Durability", Colors.Durability, null, State.Durability, RecipeData.RecipeInfo.MaxDurability, null, null),
new("Condition", default, null, 0, 0, null, State.Condition)
};
if (RecipeData.Recipe.ItemResult.Value!.IsCollectable)
datas.Add(new("Collectability", Colors.HQ, Reliability.Param, State.Collectability, State.MaxCollectability, $"{State.Collectability}", null));
else if (RecipeData.Recipe.RequiredQuality > 0)
datas.Add(new("Quality %", Colors.HQ, Reliability.Param, State.Quality, RecipeData.Recipe.RequiredQuality, $"{(float)State.Quality / RecipeData.Recipe.RequiredQuality * 100:0}%", null));
else if (RecipeData.RecipeInfo.MaxQuality > 0)
datas.Add(new("HQ %", Colors.HQ, Reliability.Param, State.HQPercent, 100, $"{State.HQPercent}%", null));
DrawBars(datas);
ImGui.TableNextColumn();
datas = new List<BarData>(3)
{
new("Progress", Colors.Progress, Reliability.Progress, State.Progress, RecipeData.RecipeInfo.MaxProgress, null, null),
new("Quality", Colors.Quality, Reliability.Quality, State.Quality, RecipeData.RecipeInfo.MaxQuality, null, null),
new("CP", Colors.CP, null, State.CP, CharacterStats.CP, null, null)
};
if (RecipeData.RecipeInfo.MaxQuality <= 0)
datas.RemoveAt(1);
DrawBars(datas);
}
}
using (var panel = ImRaii2.GroupPanel("Buffs", -1, out _))
{
using var _font = ImRaii.PushFont(AxisFont.ImFont);
var iconHeight = ImGui.GetFrameHeight() * 1.75f;
var durationShift = iconHeight * .2f;
ImGui.Dummy(new(0, iconHeight + ImGui.GetStyle().ItemSpacing.Y + ImGui.GetTextLineHeight() - durationShift));
ImGui.SameLine(0, 0);
var effects = State.ActiveEffects;
foreach (var effect in Enum.GetValues<EffectType>())
{
if (!effects.HasEffect(effect))
continue;
using (var group = ImRaii.Group())
{
var icon = effect.GetIcon(effects.GetStrength(effect));
var size = new Vector2(iconHeight * icon.Width / icon.Height, iconHeight);
ImGui.Image(icon.ImGuiHandle, size);
if (!effect.IsIndefinite())
{
ImGui.SetCursorPosY(ImGui.GetCursorPosY() - durationShift);
ImGuiUtils.TextCentered($"{effects.GetDuration(effect)}", size.X);
}
}
if (ImGui.IsItemHovered())
{
var status = effect.Status();
using var _reset = ImRaii.DefaultFont();
ImGui.SetTooltip($"{status.Name.ToDalamudString()}\n{status.Description.ToDalamudString()}");
}
ImGui.SameLine();
}
}
}
private readonly record struct BarData(string Name, Vector4 Color, SimulationReliablity.ParamReliability? Reliability, float Value, float Max, string? Caption, Condition? Condition);
private void DrawBars(IEnumerable<BarData> bars)
{
var spacing = ImGui.GetStyle().ItemSpacing.X;
var totalSize = ImGui.GetContentRegionAvail().X;
totalSize -= 2 * spacing;
var textSize = bars.Max(b =>
{
if (b.Caption is { } caption)
return ImGui.CalcTextSize(caption).X;
// max (sp/2) "/" (sp/2) max
return Math.Max(ImGui.CalcTextSize($"{b.Value:0}").X, ImGui.CalcTextSize($"{b.Max:0}").X) * 2
+ spacing
+ ImGui.CalcTextSize("/").X;
});
var maxSize = (textSize - 2 * spacing - ImGui.CalcTextSize("/").X) / 2;
var barSize = totalSize - textSize - spacing;
foreach (var bar in bars)
{
using var panel = ImRaii2.GroupPanel(bar.Name, totalSize, out _);
if (bar.Condition is { } condition)
{
using (var g = ImRaii.Group())
{
var size = ImGui.GetFrameHeight() + spacing + ImGui.CalcTextSize(condition.Name()).X;
ImGuiUtils.AlignCentered(size, totalSize);
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.Text(condition.Name());
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip(condition.Description(CharacterStats.HasSplendorousBuff));
}
else
{
var pos = ImGui.GetCursorPos();
using (var color = ImRaii.PushColor(ImGuiCol.PlotHistogram, bar.Color))
ImGui.ProgressBar(Math.Clamp(bar.Value / bar.Max, 0, 1), new(barSize, ImGui.GetFrameHeight()), string.Empty);
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenOverlapped))
{
if (bar.Reliability is { } reliability)
{
if (reliability.GetViolinData(bar.Max, (int)(barSize / 5), 0.02) is { } violinData)
{
ImGui.SetCursorPos(pos);
ImGuiUtils.ViolinPlot(violinData, new(barSize, ImGui.GetFrameHeight()));
if (ImGui.IsItemHovered())
ImGui.SetTooltip(
$"Min: {reliability.Min}\n" +
$"Med: {reliability.Median:0.##}\n" +
$"Avg: {reliability.Average:0.##}\n" +
$"Max: {reliability.Max}");
}
}
}
ImGui.SameLine(0, spacing);
ImGui.AlignTextToFramePadding();
if (bar.Caption is { } caption)
ImGuiUtils.TextRight(caption, textSize);
else
{
ImGuiUtils.TextRight($"{bar.Value:0}", maxSize);
ImGui.SameLine(0, spacing / 2);
ImGui.Text("/");
ImGui.SameLine(0, spacing / 2);
ImGuiUtils.TextRight($"{bar.Max:0}", maxSize);
}
}
}
}
private void DrawMacro()
{
var spacing = ImGui.GetStyle().ItemSpacing.X;
var imageSize = ImGui.GetFrameHeight() * 2;
var lastState = InitialState;
using var panel = ImRaii2.GroupPanel("Macro", -1, out var availSpace);
ImGui.Dummy(new(0, imageSize));
ImGui.SameLine(0, 0);
var macroActionsHeight = ImGui.GetFrameHeightWithSpacing();
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);
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).ImGuiHandle, new(imageSize), default, Vector2.One, 0, default, failedAction ? new(1, 1, 1, ImGui.GetStyle().DisabledAlpha) : Vector4.One))
RemoveStep(i);
if (response is ActionResponse.ActionNotUnlocked)
{
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);
ImGui.PopClipRect();
}
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
{
var sim = CreateSim(lastState);
ImGui.SetTooltip($"{action.GetName(RecipeData!.ClassJob)}\n{actionBase.GetTooltip(sim, true)}");
}
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);
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 (SolverTokenSource?.IsCancellationRequested ?? false)
{
using var _disabled = ImRaii.Disabled();
ImGui.Button("Stopping", new(halfWidth, height));
if (ImGui.IsItemHovered())
ImGui.SetTooltip("This might could a while, sorry! Please report\n" +
"if this takes longer than a second.");
}
else
{
if (ImGui.Button("Stop", new(halfWidth, height)))
SolverTokenSource?.Cancel();
}
}
else
{
if (ImGui.Button(SolverStartStepCount.HasValue ? "Regenerate" : "Generate", new(halfWidth, height)))
CalculateBestMacro();
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Suggest a way to finish the crafting recipe.\n" +
"Results aren't perfect, and levels of success\n" +
"can vary wildly depending on the solver's settings.");
}
ImGui.SameLine();
if (ImGuiUtils.IconButtonSquare(FontAwesomeIcon.Paste))
Service.Plugin.CopyMacro(Macro.Select(s => s.Action).ToArray());
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Copy to Clipboard");
ImGui.SameLine();
using (var _disabled = ImRaii.Disabled(SolverRunning))
{
if (ImGuiUtils.IconButtonSquare(FontAwesomeIcon.FileImport))
ShowImportPopup();
}
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
ImGui.SetTooltip("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))
ImGui.SetTooltip("Reset");
}
ImGui.SameLine();
using (var _disabled = ImRaii.Disabled(SolverRunning))
{
if (ImGuiUtils.IconButtonSquare(FontAwesomeIcon.Trash))
{
SolverStartStepCount = null;
Macro.Clear();
}
}
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
ImGui.SetTooltip("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.Select(s => s.Action).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.Text("Supported sites:");
ImGui.BulletText("ffxivteamcraft.com");
ImGui.BulletText("craftingway.app");
ImGui.Text("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);
Service.PluginInterface.UiBuilder.AddNotification($"Imported macro with {parsedActions.Count} step{(parsedActions.Count != 1 ? "s" : "")}", "Craftimizer Macro Imported", 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);
Service.PluginInterface.UiBuilder.AddNotification($"Imported macro \"{name}\"", "Craftimizer Macro Imported", NotificationType.Success);
popupImportUrlTokenSource?.Cancel();
ImGui.CloseCurrentPopup();
}
}
}
else
{
popupImportUrlTokenSource?.Cancel();
popupImportUrlTokenSource = null;
}
}
private void CalculateBestMacro()
{
SolverTokenSource?.Cancel();
SolverTokenSource = new();
SolverException = null;
RevertPreviousMacro();
SolverStartStepCount = Macro.Count;
var token = SolverTokenSource.Token;
var state = State;
var task = Task.Run(() => CalculateBestMacroTask(state, token), token);
_ = task.ContinueWith(t =>
{
if (token == SolverTokenSource.Token)
SolverTokenSource = null;
});
_ = task.ContinueWith(t =>
{
if (token.IsCancellationRequested)
return;
try
{
t.Exception!.Flatten().Handle(ex => ex is TaskCanceledException or OperationCanceledException);
}
catch (AggregateException e)
{
SolverException = e;
Log.Error(e, "Calculating macro failed");
}
}, TaskContinuationOptions.OnlyOnFaulted);
}
private void CalculateBestMacroTask(SimulationState state, CancellationToken token)
{
var config = Service.Configuration.SimulatorSolverConfig;
token.ThrowIfCancellationRequested();
var solver = new Solver.Solver(config, state) { Token = token };
solver.OnLog += Log.Debug;
solver.OnNewAction += a => AddStep(a, isSolver: true);
solver.Start();
_ = solver.GetTask().GetAwaiter().GetResult();
token.ThrowIfCancellationRequested();
}
private void RevertPreviousMacro()
{
if (SolverStartStepCount is { } stepCount && stepCount < Macro.Count)
Macro.RemoveRange(stepCount, Macro.Count - stepCount);
}
private void SaveMacro()
{
MacroSetter?.Invoke(Macro.Select(s => s.Action));
}
private void RecalculateState()
{
InitialState = new SimulationState(new(CharacterStats, RecipeData.RecipeInfo, StartingQuality));
var sim = CreateSim(InitialState);
var lastState = InitialState;
for (var i = 0; i < Macro.Count; i++)
lastState = Macro[i].Recalculate(sim, lastState);
}
private Sim CreateSim(in SimulationState state) =>
Service.Configuration.ConditionRandomness ? new Sim(state) : new SimNoRandom(state);
private void AddStep(ActionType action, int index = -1, bool isSolver = false)
{
if (index < -1 || index >= Macro.Count)
throw new ArgumentOutOfRangeException(nameof(index));
if (!isSolver && SolverRunning)
throw new InvalidOperationException("Cannot add steps while solver is running");
if (!SolverRunning)
SolverStartStepCount = null;
if (index == -1)
{
var sim = CreateSim(State);
Macro.Add(new(action, sim, State, out _));
}
else
{
var state = index == 0 ? InitialState : Macro[index - 1].State;
var sim = CreateSim(state);
Macro.Insert(index, new(action, sim, state, out state));
for (var i = index + 1; i < Macro.Count; i++)
state = Macro[i].Recalculate(sim, state);
}
}
private void RemoveStep(int index)
{
if (index < 0 || index >= Macro.Count)
throw new ArgumentOutOfRangeException(nameof(index));
if (SolverRunning)
throw new InvalidOperationException("Cannot remove steps while solver is running");
SolverStartStepCount = null;
Macro.RemoveAt(index);
var state = index == 0 ? InitialState : Macro[index - 1].State;
var sim = CreateSim(state);
for (var i = index; i < Macro.Count; i++)
state = Macro[i].Recalculate(sim, state);
}
public void Dispose()
{
Service.WindowSystem.RemoveWindow(this);
AxisFont.Dispose();
}
}