diff --git a/Craftimizer/Configuration.cs b/Craftimizer/Configuration.cs index d3c59d3..012590f 100644 --- a/Craftimizer/Configuration.cs +++ b/Craftimizer/Configuration.cs @@ -1,15 +1,43 @@ +using Craftimizer.Simulator; +using Craftimizer.Simulator.Actions; using Dalamud.Configuration; +using Dalamud.Logging; using System; +using System.Collections.Generic; namespace Craftimizer.Plugin; +[Serializable] +public class Macro +{ + public string Name { get; set; } = string.Empty; + public List Actions { get; set; } = new(); +} + [Serializable] public class Configuration : IPluginConfiguration { public int Version { get; set; } = 1; - public void Save() + public bool OverrideUncraftability { get; set; } = true; + public List Macros { get; set; } = new(); + public string SimulatorType { get; set; } = typeof(Simulator.Simulator).AssemblyQualifiedName!; + + public Simulator.Simulator CreateSimulator(SimulationState state) { - Service.PluginInterface.SavePluginConfig(this); + var type = Type.GetType(SimulatorType); + if (type == null) + PluginLog.LogError($"Failed to resolve simulator type ({SimulatorType})"); + else + { + if (Activator.CreateInstance(type, state) is Simulator.Simulator sim) + return sim; + + PluginLog.LogError($"Failed to create simulator ({SimulatorType})"); + } + return new Simulator.Simulator(state); } + + public void Save() => + Service.PluginInterface.SavePluginConfig(this); } diff --git a/Craftimizer/LuminaSheets.cs b/Craftimizer/LuminaSheets.cs index 79ec981..f45f915 100644 --- a/Craftimizer/LuminaSheets.cs +++ b/Craftimizer/LuminaSheets.cs @@ -18,4 +18,5 @@ public static class LuminaSheets public static readonly ExcelSheet ItemSheetEnglish = Service.DataManager.GetExcelSheet(ClientLanguage.English)!; public static readonly ExcelSheet MateriaSheet = Service.DataManager.GetExcelSheet()!; public static readonly ExcelSheet BaseParamSheet = Service.DataManager.GetExcelSheet()!; + public static readonly ExcelSheet ItemFoodSheet = Service.DataManager.GetExcelSheet()!; } diff --git a/Craftimizer/Plugin.cs b/Craftimizer/Plugin.cs index e52d694..22c755b 100644 --- a/Craftimizer/Plugin.cs +++ b/Craftimizer/Plugin.cs @@ -1,8 +1,13 @@ using Craftimizer.Plugin.Windows; +using Craftimizer.Simulator; +using Craftimizer.Simulator.Actions; using Dalamud.Game.Command; using Dalamud.Interface.Windowing; using Dalamud.IoC; using Dalamud.Plugin; +using Lumina.Excel.GeneratedSheets; +using System.Collections.Generic; +using ClassJob = Craftimizer.Simulator.ClassJob; namespace Craftimizer.Plugin; @@ -10,22 +15,21 @@ public sealed class Plugin : IDalamudPlugin { public string Name => "Craftimizer"; - public Configuration Configuration { get; } - public WindowSystem WindowSystem { get; } = new("Craftimizer"); - public SimulatorWindow SimulatorWindow { get; } + public WindowSystem WindowSystem { get; } + public SettingsWindow SettingsWindow { get; } public CraftingLog RecipeNoteWindow { get; } + public SimulatorWindow? SimulatorWindow { get; set; } - public Plugin( - [RequiredVersion("1.0")] DalamudPluginInterface pluginInterface) + public Plugin([RequiredVersion("1.0")] DalamudPluginInterface pluginInterface) { + Service.Plugin = this; pluginInterface.Create(); + Service.Configuration = pluginInterface.GetPluginConfig() as Configuration ?? new Configuration(); - Configuration = pluginInterface.GetPluginConfig() as Configuration ?? new Configuration(); + WindowSystem = new(Name); - SimulatorWindow = new(); - WindowSystem.AddWindow(SimulatorWindow); RecipeNoteWindow = new(); - WindowSystem.AddWindow(RecipeNoteWindow); + SettingsWindow = new(); Service.CommandManager.AddHandler("/craft", new CommandInfo(OnCommand) { @@ -33,8 +37,17 @@ public sealed class Plugin : IDalamudPlugin }); Service.PluginInterface.UiBuilder.Draw += WindowSystem.Draw; - Service.PluginInterface.UiBuilder.OpenConfigUi += () => SimulatorWindow.IsOpen = true; + Service.PluginInterface.UiBuilder.OpenConfigUi += () => SettingsWindow.IsOpen = true; + } + public void OpenSimulatorWindow(Item item, SimulationInput input, ClassJob classJob, List actions) + { + if (SimulatorWindow != null) + { + SimulatorWindow.IsOpen = false; + WindowSystem.RemoveWindow(SimulatorWindow); + } + SimulatorWindow = new(item, input, classJob, actions); } public void Dispose() @@ -47,6 +60,6 @@ public sealed class Plugin : IDalamudPlugin if (command != "/craft") return; - SimulatorWindow.IsOpen = true; + SettingsWindow.IsOpen = true; } } diff --git a/Craftimizer/Service.cs b/Craftimizer/Service.cs index 21f7605..c2f3b56 100644 --- a/Craftimizer/Service.cs +++ b/Craftimizer/Service.cs @@ -7,6 +7,7 @@ using Dalamud.IoC; using Dalamud.Plugin; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.Command; +using Dalamud.Interface.Windowing; namespace Craftimizer.Plugin; @@ -22,5 +23,10 @@ public sealed class Service [PluginService] public static DataManager DataManager { get; private set; } [PluginService] public static TargetManager TargetManager { get; private set; } [PluginService] public static Condition Condition { get; private set; } + + public static Plugin Plugin { get; internal set; } + public static Configuration Configuration { get; internal set; } + public static WindowSystem WindowSystem => Plugin.WindowSystem; #pragma warning restore CS8618 + } diff --git a/Craftimizer/SimulatorUtils.cs b/Craftimizer/SimulatorUtils.cs index df4ecec..daee141 100644 --- a/Craftimizer/SimulatorUtils.cs +++ b/Craftimizer/SimulatorUtils.cs @@ -17,37 +17,50 @@ namespace Craftimizer.Plugin; internal static class ActionUtils { - private static (CraftAction? CraftAction, Action? Action) GetActionRow(this ActionType me, ClassJob classJob) + private static (CraftAction? CraftAction, Action? Action)[,] ActionRows; + + static ActionUtils() { - var actionId = me.Base().ActionId; - if (LuminaSheets.CraftActionSheet.GetRow(actionId) is CraftAction baseCraftAction) + var actionTypes = Enum.GetValues(); + var classJobs = Enum.GetValues(); + ActionRows = new (CraftAction? CraftAction, Action? Action)[actionTypes.Length, classJobs.Length]; + foreach (var actionType in actionTypes) { - return (classJob switch + var actionId = actionType.Base().ActionId; + if (LuminaSheets.CraftActionSheet.GetRow(actionId) is CraftAction baseCraftAction) { - ClassJob.Carpenter => baseCraftAction.CRP.Value!, - ClassJob.Blacksmith => baseCraftAction.BSM.Value!, - ClassJob.Armorer => baseCraftAction.ARM.Value!, - ClassJob.Goldsmith => baseCraftAction.GSM.Value!, - ClassJob.Leatherworker => baseCraftAction.LTW.Value!, - ClassJob.Weaver => baseCraftAction.WVR.Value!, - ClassJob.Alchemist => baseCraftAction.ALC.Value!, - ClassJob.Culinarian => baseCraftAction.CUL.Value!, - _ => baseCraftAction - }, null); + foreach(var classJob in classJobs) + { + ActionRows[(int)actionType, (int)classJob] = (classJob switch + { + ClassJob.Carpenter => baseCraftAction.CRP.Value!, + ClassJob.Blacksmith => baseCraftAction.BSM.Value!, + ClassJob.Armorer => baseCraftAction.ARM.Value!, + ClassJob.Goldsmith => baseCraftAction.GSM.Value!, + ClassJob.Leatherworker => baseCraftAction.LTW.Value!, + ClassJob.Weaver => baseCraftAction.WVR.Value!, + ClassJob.Alchemist => baseCraftAction.ALC.Value!, + ClassJob.Culinarian => baseCraftAction.CUL.Value!, + _ => baseCraftAction + }, null); + } + } + if (LuminaSheets.ActionSheet.GetRow(actionId) is Action baseAction) + { + var possibleActions = LuminaSheets.ActionSheet.Where(r => + r.Icon == baseAction.Icon && + r.ActionCategory.Row == baseAction.ActionCategory.Row && + r.Name.RawString.Equals(baseAction.Name.RawString, StringComparison.Ordinal)).ToArray(); + + foreach (var classJob in classJobs) + ActionRows[(int)actionType, (int)classJob] = (null, possibleActions.First(r => r.ClassJobCategory.Value?.IsClassJob(classJob) ?? false)); + } } - if (LuminaSheets.ActionSheet.GetRow(actionId) is Action baseAction) - { - return (null, - LuminaSheets.ActionSheet.First(r => - r.Icon == baseAction.Icon && - r.ActionCategory.Row == baseAction.ActionCategory.Row && - r.Name.RawString.Equals(baseAction.Name.RawString, StringComparison.Ordinal) && - (r.ClassJobCategory.Value?.IsClassJob(classJob) ?? false) - )); - } - return (null, null); } + public static (CraftAction? CraftAction, Action? Action) GetActionRow(this ActionType me, ClassJob classJob) => + ActionRows[(int)me, (int)classJob]; + public static uint GetId(this ActionType me, ClassJob classJob) { var (craftAction, action) = GetActionRow(me, classJob); diff --git a/Craftimizer/SimulatorWindow.cs b/Craftimizer/SimulatorWindow.cs deleted file mode 100644 index 96ea2ce..0000000 --- a/Craftimizer/SimulatorWindow.cs +++ /dev/null @@ -1,126 +0,0 @@ -using Craftimizer.Plugin.Utils; -using Craftimizer.Simulator; -using Craftimizer.Simulator.Actions; -using Dalamud.Interface; -using Dalamud.Interface.Windowing; -using ImGuiNET; -using Lumina.Excel.GeneratedSheets; -using System; -using System.Linq; -using System.Numerics; -using ClassJob = Craftimizer.Simulator.ClassJob; - -namespace Craftimizer.Plugin; - -public class SimulatorWindow : Window -{ - public Simulator.Simulator Simulation { get; } - private SimulationState State { get; set; } - - private bool showOnlyGuaranteedActions = true; - - public SimulatorWindow() : base("Craftimizer") - { - SizeConstraints = new WindowSizeConstraints() - { - MinimumSize = new Vector2(400, 400), - MaximumSize = new Vector2(float.MaxValue, float.MaxValue) - }; - - State = new(new( - new CharacterStats { Craftsmanship = 4041, Control = 3905, CP = 609, Level = 90, CLvl = Gearsets.CalculateCLvl(90) }, - CreateRecipeInfo(LuminaSheets.RecipeSheet.GetRow(35499)!), - 0 - )); - Simulation = new(State); - } - - public static RecipeInfo CreateRecipeInfo(Recipe recipe) - { - var recipeTable = recipe.RecipeLevelTable.Value!; - return new() { - IsExpert = recipe.IsExpert, - ClassJobLevel = recipeTable.ClassJobLevel, - RLvl = (int)recipeTable.RowId, - ConditionsFlag = recipeTable.ConditionsFlag, - MaxDurability = recipeTable.Durability * recipe.DurabilityFactor / 100, - MaxQuality = (int)recipeTable.Quality * recipe.QualityFactor / 100, - MaxProgress = recipeTable.Difficulty * recipe.DifficultyFactor / 100, - QualityModifier = recipeTable.QualityModifier, - QualityDivider = recipeTable.QualityDivider, - ProgressModifier = recipeTable.ProgressModifier, - ProgressDivider = recipeTable.ProgressDivider, - }; - } - - public override void Draw() - { - ImGui.BeginTable("CraftimizerTable", 2, ImGuiTableFlags.Resizable); - ImGui.TableSetupColumn("CraftimizerActionsColumn", ImGuiTableColumnFlags.WidthFixed, 300); - ImGui.TableNextColumn(); - ImGui.BeginChild("CraftimizerActions", Vector2.Zero, true, ImGuiWindowFlags.NoDecoration); - ImGui.Checkbox("Show only guaranteed actions", ref showOnlyGuaranteedActions); - ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero); - foreach (var category in Enum.GetValues().GroupBy(a => a.Category())) - { - var i = 0; - ImGuiUtils.BeginGroupPanel(category.Key.GetDisplayName()); - foreach (var action in category.OrderBy(a => a.Level())) - { - var baseAction = action.Base(); - if (showOnlyGuaranteedActions && baseAction.SuccessRate(Simulation) != 1) - continue; - - ImGui.BeginDisabled(!baseAction.CanUse(Simulation) || Simulation.IsComplete); - if (ImGui.ImageButton(action.GetIcon(ClassJob.Carpenter).ImGuiHandle, new Vector2(ImGui.GetFontSize() * 2))) - (_, State) = Simulation.Execute(State, action); - if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) - ImGui.SetTooltip($"{action.GetName(ClassJob.Carpenter)}\n{baseAction.GetTooltip(Simulation, true)}"); - ImGui.EndDisabled(); - if (++i % 5 != 0) - ImGui.SameLine(); - } - ImGuiUtils.EndGroupPanel(); - } - ImGui.PopStyleVar(); - ImGui.EndChild(); - ImGui.TableNextColumn(); - ImGui.BeginChild("CraftimizerSimulator", Vector2.Zero, true, ImGuiWindowFlags.NoDecoration); - ImGui.Text($"Step {State.StepCount + 1}"); - ImGui.Text(State.Condition.Name()); - if (ImGui.IsItemHovered()) - ImGui.SetTooltip(State.Condition.Description(State.Input.Stats.HasSplendorousBuff)); - ImGui.Text($"{State.HQPercent}%% HQ"); - ImGui.PushStyleColor(ImGuiCol.PlotHistogram, new Vector4(.2f, 1f, .2f, 1f)); - ImGui.ProgressBar(Math.Min((float)State.Progress / State.Input.Recipe.MaxProgress, 1f), new Vector2(200, 20), $"{State.Progress} / {State.Input.Recipe.MaxProgress}"); - ImGui.PopStyleColor(); - ImGui.PushStyleColor(ImGuiCol.PlotHistogram, new Vector4(.2f, .2f, 1f, 1f)); - ImGui.ProgressBar(Math.Min((float)State.Quality / State.Input.Recipe.MaxQuality, 1f), new Vector2(200, 20), $"{State.Quality} / {State.Input.Recipe.MaxQuality}"); - ImGui.PopStyleColor(); - ImGui.PushStyleColor(ImGuiCol.PlotHistogram, new Vector4(1f, 1f, .2f, 1f)); - ImGui.ProgressBar(Math.Clamp((float)State.Durability / State.Input.Recipe.MaxDurability, 0f, 1f), new Vector2(200, 20), $"{State.Durability} / {State.Input.Recipe.MaxDurability}"); - ImGui.PopStyleColor(); - ImGui.PushStyleColor(ImGuiCol.PlotHistogram, new Vector4(1f, .2f, 1f, 1f)); - ImGui.ProgressBar(Math.Clamp((float)State.CP / State.Input.Stats.CP, 0f, 1f), new Vector2(200, 20), $"{State.CP} / {State.Input.Stats.CP}"); - ImGui.PopStyleColor(); - ImGuiHelpers.ScaledDummy(5); - ImGui.Text($"Effects:"); - foreach (var effect in Enum.GetValues()) - { - var strength = Simulation.GetEffectStrength(effect); - var duration = Simulation.GetEffectDuration(effect); - var icon = effect.GetIcon(strength); - var h = ImGui.GetFontSize() * 1.25f; - var w = icon.Width * h / icon.Height; - ImGui.Image(icon.ImGuiHandle, new Vector2(w, h)); - ImGui.SameLine(); - ImGui.Text(effect.GetTooltip(strength, duration)); - } - ImGuiHelpers.ScaledDummy(5); - { - ImGui.Text("TODO: Action History"); - } - ImGui.EndChild(); - ImGui.EndTable(); - } -} diff --git a/Craftimizer/Utils/Gearsets.cs b/Craftimizer/Utils/Gearsets.cs index e9e45bd..5163a4d 100644 --- a/Craftimizer/Utils/Gearsets.cs +++ b/Craftimizer/Utils/Gearsets.cs @@ -9,91 +9,42 @@ using System.Linq; namespace Craftimizer.Plugin.Utils; internal static unsafe class Gearsets { - private static readonly (int CP, int Craftsmanship, int Control, bool HasSplendorous, bool HasSpecialist) BaseStats = (180, 0, 0, false, false); + public record struct GearsetStats(int CP, int Craftsmanship, int Control); + public record struct GearsetMateria(ushort Type, ushort Grade); + public record struct GearsetItem(uint itemId, bool isHq, GearsetMateria[] materia); - private const int ParamCP = 11; - private const int ParamCraftsmanship = 70; - private const int ParamControl = 71; + private static readonly GearsetStats BaseStats = new(180, 0, 0); - public static CharacterStats CalculateCharacterStats(InventoryContainer* container, int characterLevel, bool canUseManipulation) + public const int ParamCP = 11; + public const int ParamCraftsmanship = 70; + public const int ParamControl = 71; + + public static GearsetItem[] GetGearsetItems(InventoryContainer* container) { - var stats = CalculateGearsetStats(container); - return new CharacterStats - { - CP = stats.CP, - Craftsmanship = stats.Craftsmanship, - Control = stats.Control, - Level = characterLevel, - CanUseManipulation = canUseManipulation, - HasSplendorousBuff = stats.HasSplendorous, - IsSpecialist = stats.HasSpecialist, - CLvl = CalculateCLvl(characterLevel), - }; - } - - public static CharacterStats CalculateCharacterStats(RaptureGearsetModule.GearsetEntry* entry, int characterLevel, bool canUseManipulation) - { - var stats = CalculateGearsetStats(entry); - return new CharacterStats - { - CP = stats.CP, - Craftsmanship = stats.Craftsmanship, - Control = stats.Control, - Level = characterLevel, - CanUseManipulation = canUseManipulation, - HasSplendorousBuff = stats.HasSplendorous, - IsSpecialist = stats.HasSpecialist, - CLvl = CalculateCLvl(characterLevel), - }; - } - - private static (int CP, int Craftsmanship, int Control, bool HasSplendorous, bool HasSpecialist) CalculateGearsetStats(InventoryContainer* container) - { - var stats = BaseStats; + var items = new GearsetItem[(int)container->Size]; for (var i = 0; i < container->Size; ++i) { - var itemStats = CalculateGearsetItemStats(container->Items[i]); - stats.CP += itemStats.CP; - stats.Craftsmanship += itemStats.Craftsmanship; - stats.Control += itemStats.Control; - stats.HasSplendorous = stats.HasSplendorous || itemStats.HasSplendorous; - stats.HasSpecialist = stats.HasSpecialist || itemStats.HasSpecialist; + var item = container->Items[i]; + items[i] = new(item.ItemID, item.Flags.HasFlag(InventoryItem.ItemFlags.HQ), GetMaterias(item.Materia, item.MateriaGrade)); } - return stats; + return items; } - private static (int CP, int Craftsmanship, int Control, bool HasSplendorous, bool HasSpecialist) CalculateGearsetStats(RaptureGearsetModule.GearsetEntry* entry) + public static GearsetItem[] GetGearsetItems(RaptureGearsetModule.GearsetEntry* entry) { - var stats = new[] + var gearsetItems = new Span(entry->ItemsData, 14); + var items = new GearsetItem[14]; + for (var i = 0; i < 14; ++i) { - BaseStats, - CalculateGearsetItemStats(entry->MainHand), - CalculateGearsetItemStats(entry->OffHand), - CalculateGearsetItemStats(entry->Head), - CalculateGearsetItemStats(entry->Body), - CalculateGearsetItemStats(entry->Hands), - // CalculateGearsetItemStats(entry->Belt), - CalculateGearsetItemStats(entry->Legs), - CalculateGearsetItemStats(entry->Feet), - CalculateGearsetItemStats(entry->Ears), - CalculateGearsetItemStats(entry->Neck), - CalculateGearsetItemStats(entry->Wrists), - CalculateGearsetItemStats(entry->RingRight), - CalculateGearsetItemStats(entry->RightLeft), - CalculateGearsetItemStats(entry->SoulStone), - }; - return stats.Aggregate((a, b) => (a.CP + b.CP, a.Craftsmanship + b.Craftsmanship, a.Control + b.Control, a.HasSplendorous || b.HasSplendorous, a.HasSpecialist || b.HasSpecialist)); + var item = gearsetItems[i]; + items[i] = new(item.ItemID % 1000000, item.ItemID > 1000000, GetMaterias(item.Materia, item.MateriaGrade)); + } + return items; } - private static (int CP, int Craftsmanship, int Control, bool HasSplendorous, bool HasSpecialist) CalculateGearsetItemStats(InventoryItem item) => - CalculateGearsetItemStats(item.ItemID, item.Flags.HasFlag(InventoryItem.ItemFlags.HQ), item.Materia, item.MateriaGrade); - - private static (int CP, int Craftsmanship, int Control, bool HasSplendorous, bool HasSpecialist) CalculateGearsetItemStats(RaptureGearsetModule.GearsetItem item) => - CalculateGearsetItemStats(item.ItemID % 1000000, item.ItemID > 1000000, item.Materia, item.MateriaGrade); - - private static (int CP, int Craftsmanship, int Control, bool HasSplendorous, bool HasSpecialist) CalculateGearsetItemStats(uint itemId, bool isHq, ushort* materiaTypes, byte* materiaGrades) + public static GearsetStats CalculateGearsetItemStats(GearsetItem gearsetItem) { - var item = LuminaSheets.ItemSheet.GetRow(itemId)!; + var item = LuminaSheets.ItemSheet.GetRow(gearsetItem.itemId)!; int cp = 0, craftsmanship = 0, control = 0; @@ -109,28 +60,63 @@ internal static unsafe class Gearsets foreach (var statIncrease in item.UnkData59) IncreaseStat(statIncrease.BaseParam, statIncrease.BaseParamValue); - - if (isHq) - { + if (gearsetItem.isHq) foreach (var statIncrease in item.UnkData73) IncreaseStat(statIncrease.BaseParamSpecial, statIncrease.BaseParamValueSpecial); - } - for (var i = 0; i < 5; ++i) - { - if (materiaTypes[i] == 0) - continue; - var materia = LuminaSheets.MateriaSheet.GetRow(materiaTypes[i])!; - IncreaseStat((int)materia.BaseParam.Row, materia.Value[materiaGrades[i]]); + foreach(var gearsetMateria in gearsetItem.materia) + { + if (gearsetMateria.Type == 0) + continue; + + var materia = LuminaSheets.MateriaSheet.GetRow(gearsetMateria.Type)!; + IncreaseStat((int)materia.BaseParam.Row, materia.Value[gearsetMateria.Grade]); } cp = Math.Min(cp, CalculateParamCap(item, ParamCP)); craftsmanship = Math.Min(craftsmanship, CalculateParamCap(item, ParamCraftsmanship)); control = Math.Min(control, CalculateParamCap(item, ParamControl)); - return (cp, craftsmanship, control, IsSpecialistSoulCrystal(item), IsSplendorousTool(itemId)); + return new(cp, craftsmanship, control); } + public static GearsetStats CalculateGearsetStats(GearsetItem[] gearsetItems) => + gearsetItems.Select(CalculateGearsetItemStats).Aggregate(BaseStats, (a, b) => new(a.CP + b.CP, a.Craftsmanship + b.Craftsmanship, a.Control + b.Control)); + + public static CharacterStats CalculateCharacterStats(GearsetItem[] gearsetItems, int characterLevel, bool canUseManipulation) + { + var stats = CalculateGearsetStats(gearsetItems); + return new CharacterStats + { + CP = stats.CP, + Craftsmanship = stats.Craftsmanship, + Control = stats.Control, + Level = characterLevel, + CanUseManipulation = canUseManipulation, + HasSplendorousBuff = gearsetItems.Any(IsSplendorousTool), + IsSpecialist = gearsetItems.Any(IsSpecialistSoulCrystal), + CLvl = CalculateCLvl(characterLevel), + }; + } + + public static bool IsItem(GearsetItem item, uint itemId) => + item.itemId == itemId; + + public static bool IsSpecialistSoulCrystal(GearsetItem item) + { + var luminaItem = LuminaSheets.ItemSheet.GetRow(item.itemId)!; + // Soul Crystal ItemUICategory DoH Category + return luminaItem.ItemUICategory.Row != 62 && luminaItem.ClassJobUse.Value!.ClassJobCategory.Row == 33; + } + + public static bool IsSplendorousTool(GearsetItem item) => + LuminaSheets.ItemSheetEnglish.GetRow(item.itemId)!.Description.ToDalamudString().TextValue.Contains("Increases to quality are 1.75 times higher than normal when material condition is Good.", StringComparison.Ordinal); + + public static int CalculateCLvl(int characterLevel) => + characterLevel <= 80 + ? LuminaSheets.ParamGrowSheet.GetRow((uint)characterLevel)!.CraftingLevel + : (int)LuminaSheets.RecipeLevelTableSheet.First(r => r.ClassJobLevel == characterLevel).RowId; + // https://github.com/ffxiv-teamcraft/ffxiv-teamcraft/blob/24d0db2d9676f264edf53651b21005305267c84c/apps/client/src/app/modules/gearsets/materia.service.ts#L265 private static int CalculateParamCap(Item item, int paramId) { @@ -176,16 +162,11 @@ internal static unsafe class Gearsets return cap == 0 ? int.MaxValue : cap; } - private static bool IsSpecialistSoulCrystal(Item item) => - // Soul Crystal ItemUICategory DoH Category - item.ItemUICategory.Row != 62 && item.ClassJobUse.Value!.ClassJobCategory.Row == 33; - - private static bool IsSplendorousTool(uint itemId) => - LuminaSheets.ItemSheetEnglish.GetRow(itemId)!.Description.ToDalamudString().TextValue.Contains("Increases to quality are 1.75 times higher than normal when material condition is Good.", StringComparison.Ordinal); - // 38737 <= itemId && itemId <= 38744; - - public static int CalculateCLvl(int characterLevel) => - characterLevel <= 80 - ? LuminaSheets.ParamGrowSheet.GetRow((uint)characterLevel)!.CraftingLevel - : (int)LuminaSheets.RecipeLevelTableSheet.First(r => r.ClassJobLevel == characterLevel).RowId; + private static GearsetMateria[] GetMaterias(ushort* types, byte* grades) + { + var materia = new GearsetMateria[5]; + for (var i = 0; i < 5; ++i) + materia[i] = new(types[i], grades[i]); + return materia; + } } diff --git a/Craftimizer/Windows/CraftingLog.cs b/Craftimizer/Windows/CraftingLog.cs index 30ea05c..0efad5f 100644 --- a/Craftimizer/Windows/CraftingLog.cs +++ b/Craftimizer/Windows/CraftingLog.cs @@ -1,19 +1,22 @@ using Craftimizer.Plugin.Utils; using Craftimizer.Simulator; -using Craftimizer.Simulator.Actions; using Dalamud; using Dalamud.Interface; using Dalamud.Interface.Components; using Dalamud.Interface.Windowing; +using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Game.UI; using FFXIVClientStructs.FFXIV.Client.UI; -using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Client.UI.Misc; using FFXIVClientStructs.FFXIV.Component.GUI; using ImGuiNET; using Lumina.Excel.GeneratedSheets; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; using System.Numerics; using System.Runtime.InteropServices; using System.Text; @@ -26,46 +29,189 @@ public unsafe class CraftingLog : Window { private const ImGuiWindowFlags WindowFlags = ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.AlwaysAutoResize + | ImGuiWindowFlags.NoSavedSettings | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoNavFocus; + private const int LeftSideWidth = 350; + + // If relative, increase stat by Value's % (rounded down), and cap increase to Max + // If not relative, increase stat by Value, and ignore Max + [StructLayout(LayoutKind.Auto)] + private record struct FoodStat(bool IsRelative, sbyte Value, short Max, sbyte ValueHQ, short MaxHQ); + private sealed record Food(Item Item, string Name, FoodStat? Craftsmanship, FoodStat? Control, FoodStat? CP); + + private static Food[] FoodItems { get; } + private static Food[] MedicineItems { get; } + private static Random Random { get; } + + private TimeSpan FrameTime { get; set; } + private Stopwatch Stopwatch { get; } = new(); + + // Set in DrawConditions private AddonRecipeNote* Addon { get; set; } private RecipeNote* State { get; set; } private ushort RecipeId { get; set; } private Recipe Recipe { get; set; } = null!; + + // Set in CalculateRecipeStats (in DrawConditions) + private RecipeLevelTable RecipeTable { get; set; } = null!; private RecipeInfo RecipeInfo { get; set; } = null!; + private ClassJob RecipeClassJob { get; set; } + private short RecipeCharacterLevel { get; set; } + private bool RecipeCanUseManipulation { get; set; } + private int RecipeHQIngredientCount { get; set; } + private int RecipeMaxStartingQuality { get; set; } - private ClassJob RecipeClassJob => (ClassJob)Recipe.CraftType.Row; - private short RecipeCharacterLevel => PlayerState.Instance()->ClassJobLevelArray[RecipeClassJob.GetClassJobIndex()]; - private bool RecipeCanUseManipulation => ActionManager.CanUseActionOnTarget(ActionType.Manipulation.GetId(RecipeClassJob), (GameObject*)Service.ClientState.LocalPlayer!.Address); - private RecipeLevelTable RecipeTable => Recipe.RecipeLevelTable.Value!; + // Set in CalculateCharacterStats (in PreDraw) + private Gearsets.GearsetItem[] CharacterEquipment { get; set; } = null!; + private CharacterStats CharacterStatsNoConsumable { get; set; } = null!; + private Gearsets.GearsetStats CharacterConsumableBonus { get; set; } + private CharacterStats CharacterStatsConsumable { get; set; } = null!; + private CannotCraftReason CharacterCannotCraftReason { get; set; } + private SimulationInput CharacterSimulationInput { get; set; } = null!; + + // Set in UI + private int QualityNotches { get; set; } + private int StartingQuality => RecipeHQIngredientCount == 0 ? 0 : (int)((float)QualityNotches * RecipeMaxStartingQuality / RecipeHQIngredientCount); - private int startingQuality; + private Food? SelectedFood { get; set; } + private bool SelectedFoodHQ { get; set; } + + private Food? SelectedMedicine { get; set; } + private bool SelectedMedicineHQ { get; set; } + + static CraftingLog() + { + var foods = new List(); + var medicines = new List(); + foreach (var item in LuminaSheets.ItemSheet) + { + var isFood = item.ItemUICategory.Row == 46; + var isMedicine = item.ItemUICategory.Row == 44; + if (!isFood && !isMedicine) + continue; + + if (item.ItemAction.Value == null) + continue; + + if (!(item.ItemAction.Value.Type is 844 or 845 or 846)) + continue; + + var itemFood = LuminaSheets.ItemFoodSheet.GetRow(item.ItemAction.Value.Data[1]); + if (itemFood == null) + continue; + + FoodStat? craftsmanship = null, control = null, cp = null; + foreach (var stat in itemFood.UnkData1) + { + if (stat.BaseParam == 0) + continue; + var foodStat = new FoodStat(stat.IsRelative, stat.Value, stat.Max, stat.ValueHQ, stat.MaxHQ); + switch (stat.BaseParam) + { + case Gearsets.ParamCraftsmanship: craftsmanship = foodStat; break; + case Gearsets.ParamControl: control = foodStat; break; + case Gearsets.ParamCP: cp = foodStat; break; + default: continue; + } + } + + if (craftsmanship != null || control != null || cp != null) + { + var food = new Food(item, item.Name.ToDalamudString().TextValue ?? $"Unknown ({item.RowId})", craftsmanship, control, cp); + if (isFood) + foods.Add(food); + if (isMedicine) + medicines.Add(food); + } + } + foods.Sort((a, b) => b.Item.LevelItem.Row.CompareTo(a.Item.LevelItem.Row)); + medicines.Sort((a, b) => b.Item.LevelItem.Row.CompareTo(a.Item.LevelItem.Row)); + FoodItems = foods.ToArray(); + MedicineItems = medicines.ToArray(); + + Random = new(); + } public CraftingLog() : base("RecipeNoteHelper", WindowFlags, true) { + Service.WindowSystem.AddWindow(this); + IsOpen = true; } + private void CalculateRecipeStats() + { + RecipeTable = Recipe.RecipeLevelTable.Value!; + RecipeInfo = CreateRecipeInfo(Recipe); + RecipeClassJob = (ClassJob)Recipe.CraftType.Row; + RecipeCharacterLevel = PlayerState.Instance()->ClassJobLevelArray[RecipeClassJob.GetClassJobIndex()]; + RecipeCanUseManipulation = ActionManager.CanUseActionOnTarget(ActionType.Manipulation.GetId(RecipeClassJob), (GameObject*)Service.ClientState.LocalPlayer!.Address); + RecipeHQIngredientCount = Recipe.UnkData5 + .Where(i => + i != null && + i.ItemIngredient != 0 && + (LuminaSheets.ItemSheet.GetRow((uint)i.ItemIngredient)?.CanBeHq ?? false) + ).Sum(i => i.AmountIngredient); + RecipeMaxStartingQuality = (int)Math.Floor(Recipe.MaterialQualityFactor * RecipeInfo.MaxQuality / 100f); + } + + private void CalculateCharacterStats() + { + var container = InventoryManager.Instance()->GetInventoryContainer(InventoryType.EquippedItems); + if (container == null) + return; + + CharacterEquipment = Gearsets.GetGearsetItems(container); + CharacterStatsNoConsumable = Gearsets.CalculateCharacterStats(CharacterEquipment, RecipeCharacterLevel, RecipeCanUseManipulation); + CharacterConsumableBonus = CalculateConsumableBonus(CharacterStatsNoConsumable); + CharacterStatsConsumable = CharacterStatsNoConsumable with + { + Craftsmanship = CharacterStatsNoConsumable.Craftsmanship + CharacterConsumableBonus.Craftsmanship, + Control = CharacterStatsNoConsumable.Control + CharacterConsumableBonus.Control, + CP = CharacterStatsNoConsumable.CP + CharacterConsumableBonus.CP, + }; + CharacterCannotCraftReason = Service.Configuration.OverrideUncraftability ? CannotCraftReason.OK : CanCraftRecipe(CharacterEquipment, CharacterStatsConsumable); + + if (CharacterCannotCraftReason == CannotCraftReason.OK) + CharacterSimulationInput = new(CharacterStatsConsumable, RecipeInfo, StartingQuality, Random); + } + public override void Draw() { - if (Service.ClientState.LocalPlayer == null) - return; + ImGui.BeginTable("craftlog", 2, ImGuiTableFlags.BordersInnerV); + ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, LeftSideWidth); + ImGui.TableNextColumn(); DrawCraftInfo(); + ImGui.TableNextColumn(); DrawGearsets(); + + ImGui.EndTable(); + + ImGui.TextUnformatted($"{FrameTime.TotalMilliseconds:0.00}ms"); } - void DrawCraftInfo() + private void DrawCraftInfo() { - DrawRecipeInfo(); - DrawCharacterInfo(); + ImGui.BeginTable("craftinfo", 2, ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.SizingStretchSame); - DrawCraftActions(); + ImGui.TableNextColumn(); + DrawRecipeInfo(); + + ImGui.TableNextColumn(); + DrawCharacterInfo(); + ImGui.EndTable(); + + ImGui.Separator(); + + DrawCraftParameters(); + DrawMacros(); } - void DrawRecipeInfo() + private void DrawRecipeInfo() { var s = new StringBuilder(); s.AppendLine($"{RecipeClassJob.GetName()} {new string('★', RecipeTable.Stars)}"); @@ -73,46 +219,110 @@ public unsafe class CraftingLog : Window s.AppendLine($"Durability: {RecipeInfo.MaxDurability}"); s.AppendLine($"Progress: {RecipeInfo.MaxProgress}"); s.AppendLine($"Quality: {RecipeInfo.MaxQuality}"); - s.AppendLine($"Starting Quality: {startingQuality}"); ImGui.Text(s.ToString()); } - void DrawCharacterInfo() + private void DrawCharacterInfo() { - var classJob = (byte)Service.ClientState.LocalPlayer!.ClassJob.Id; - - if (!ClassJobUtils.IsClassJob(classJob, RecipeClassJob)) + if (CharacterCannotCraftReason != CannotCraftReason.OK) { - ImGui.Text("Your current class cannot craft this recipe."); + ImGui.TextWrapped(GetCannotCraftReasonText(CharacterCannotCraftReason)); return; } - var container = InventoryManager.Instance()->GetInventoryContainer(InventoryType.EquippedItems); - if (container == null) - return; + ImGui.Text(GetCharacterStatsText(CharacterStatsConsumable)); + } - var stats = Gearsets.CalculateCharacterStats(container, RecipeCharacterLevel, RecipeCanUseManipulation); + private void DrawCraftParameters() + { + ImGui.BeginDisabled(RecipeHQIngredientCount == 0); + var qualityNotches = QualityNotches; + ImGui.SetNextItemWidth(LeftSideWidth - 115); + if (ImGui.SliderInt("Starting Quality", ref qualityNotches, 0, RecipeHQIngredientCount, StartingQuality.ToString(), ImGuiSliderFlags.NoInput | ImGuiSliderFlags.AlwaysClamp)) + QualityNotches = qualityNotches; + ImGui.EndDisabled(); + + ImGui.BeginTable("craftfood", 2, ImGuiTableFlags.BordersInnerV); + + ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, LeftSideWidth - 120); + ImGui.TableNextColumn(); + + if (ImGui.BeginCombo("Food", SelectedFood?.Name ?? "None")) + { + if (ImGui.Selectable("None", SelectedFood == null)) + SelectedFood = null; + + foreach (var food in FoodItems) + if (ImGui.Selectable(food.Name, food == SelectedFood)) + SelectedFood = food; + + ImGui.EndCombo(); + } + + if (ImGui.BeginCombo("Medicine", SelectedMedicine?.Name ?? "None")) + { + if (ImGui.Selectable("None", SelectedMedicine == null)) + SelectedMedicine = null; + + foreach (var food in MedicineItems) + if (ImGui.Selectable(food.Name, food == SelectedMedicine)) + SelectedMedicine = food; + + ImGui.EndCombo(); + } + + ImGui.TableNextColumn(); var s = new StringBuilder(); - s.AppendLine($"{RecipeClassJob.GetName()}"); - s.AppendLine($"Level {stats.Level} (CLvl {stats.CLvl})"); - s.AppendLine($"Craftsmanship {stats.Craftsmanship}"); - s.AppendLine($"Control {stats.Control}"); - s.AppendLine($"CP {stats.CP}"); - if (stats.IsSpecialist) - s.AppendLine($" + Specialist"); - if (stats.HasSplendorousBuff) - s.AppendLine($" + Splendorous"); + s.AppendLine($"+{CharacterConsumableBonus.Craftsmanship} Craftsmanship"); + s.AppendLine($"+{CharacterConsumableBonus.Control} Control"); + s.AppendLine($"+{CharacterConsumableBonus.CP} CP"); ImGui.Text(s.ToString()); + + ImGui.EndTable(); } - void DrawCraftActions() + private void DrawMacros() { - ImGui.Button("Open Simulator"); - ImGui.Button("Generate a new macro"); + var padding = ImGui.GetStyle().FramePadding; + var itemPadding = ImGui.GetStyle().ItemInnerSpacing; + + var fontSize = ImGui.GetFontSize(); + var height = fontSize + (padding.Y * 2); + var width = (ImGui.GetContentRegionAvail().X / 2) - itemPadding.X; + var size = new Vector2(width, height); + + if (ImGui.Button("Open Simulator", size)) + { + Service.Plugin.OpenSimulatorWindow(Recipe.ItemResult.Value!, CharacterSimulationInput, RecipeClassJob, new()); + } + ImGui.SameLine(); + ImGui.Button("Generate a new macro", size); + + ImGui.BeginTable("macrotable", 3, ImGuiTableFlags.BordersInner); + ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, width); + foreach(var macro in Service.Configuration.Macros) + { + ImGui.TableNextRow(); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted(macro.Name); + + ImGui.TableNextColumn(); + foreach (var action in macro.Actions) + { + ImGui.Image(action.GetIcon(RecipeClassJob).ImGuiHandle, new(fontSize * 2)); + ImGui.SameLine(); + } + ImGui.Dummy(Vector2.Zero); + + ImGui.TableNextColumn(); + ImGui.Text("Sim Results Here!"); + } + ImGui.EndTable(); } - void DrawGearsets() + private void DrawGearsets() { ImGui.Text("Available Gearsets"); @@ -131,35 +341,26 @@ public unsafe class CraftingLog : Window if (!ClassJobUtils.IsClassJob(gearset->ClassJob, RecipeClassJob)) continue; - var stats = Gearsets.CalculateCharacterStats(gearset, RecipeCharacterLevel, RecipeCanUseManipulation); + var items = Gearsets.GetGearsetItems(gearset); + var stats = Gearsets.CalculateCharacterStats(items, RecipeCharacterLevel, RecipeCanUseManipulation); var gearsetId = gearset->ID + 1; - var s = new StringBuilder(); - s.AppendLine($"{SafeMemory.ReadString((nint)gearset->Name, 47)} ({gearsetId})"); - s.AppendLine($"Level {stats.Level} (CLvl {stats.CLvl})"); - s.AppendLine($"Craftsmanship {stats.Craftsmanship}"); - s.AppendLine($"Control {stats.Control}"); - s.AppendLine($"CP {stats.CP}"); - if (stats.IsSpecialist) - s.AppendLine($" + Specialist"); - if (stats.HasSplendorousBuff) - s.AppendLine($" + Splendorous"); - ImGui.Text(s.ToString()); + ImGuiUtils.BeginGroupPanel($"{SafeMemory.ReadString((nint)gearset->Name, 47)} ({gearsetId})"); + ImGui.Text(GetCharacterStatsText(stats)); ImGui.SameLine(); if (ImGuiComponents.IconButton($"SwapGearset{gearsetId}", FontAwesomeIcon.SyncAlt)) Chat.SendMessage($"/gearset change {gearsetId}"); if (ImGui.IsItemHovered()) ImGui.SetTooltip($"Swap to gearset {gearsetId}"); + ImGuiUtils.EndGroupPanel(); } } - void OnNewRecipe() - { - startingQuality = 0; - } - public override bool DrawConditions() { + if (Service.ClientState.LocalPlayer == null) + return false; + Addon = (AddonRecipeNote*)Service.GameGui.GetAddonByName("RecipeNote"); if (Addon == null) @@ -180,8 +381,7 @@ public unsafe class CraftingLog : Window if (recipeEntry == null) return false; - if (RecipeId != recipeEntry->RecipeId) - OnNewRecipe(); + var isNewRecipe = RecipeId != recipeEntry->RecipeId; RecipeId = recipeEntry->RecipeId; @@ -192,16 +392,21 @@ public unsafe class CraftingLog : Window Recipe = recipe; - RecipeInfo = SimulatorWindow.CreateRecipeInfo(Recipe); - if (!Addon->Unk258->IsVisible) return false; + if (isNewRecipe) + { + QualityNotches = 0; + CalculateRecipeStats(); + } + return base.DrawConditions(); } public override unsafe void PreDraw() { + Stopwatch.Restart(); ref var unit = ref Addon->AtkUnitBase; var scale = unit.Scale; var pos = new Vector2(unit.X, unit.Y); @@ -210,14 +415,6 @@ public unsafe class CraftingLog : Window var node = (AtkResNode*)Addon->Unk458; // unit.GetNodeById(59); var nodeParent = Addon->Unk258; // unit.GetNodeById(57); - //for (var i = 544; i <= 1960; i += 8) - //{ - // if (Marshal.ReadIntPtr((nint)Addon, i) == (nint)nodeParent) - // { - // PluginLog.LogDebug($"{i}"); - // } - //} - Position = pos + new Vector2(size.X, (nodeParent->Y + node->Y) * scale); SizeConstraints = new WindowSizeConstraints { @@ -225,6 +422,148 @@ public unsafe class CraftingLog : Window MaximumSize = new(10000, 10000) }; + CalculateCharacterStats(); + base.PreDraw(); } + + public override void PostDraw() + { + Stopwatch.Stop(); + FrameTime = Stopwatch.Elapsed; + base.PostDraw(); + } + + private Gearsets.GearsetStats CalculateConsumableBonus(CharacterStats stats) + { + static int CalculateBonus(int param, bool isHq, FoodStat? stat) + { + if (stat == null) + return 0; + + var foodStat = stat.Value; + var (value, max) = isHq ? (foodStat.ValueHQ, foodStat.MaxHQ) : (foodStat.Value, foodStat.Max); + + if (!foodStat.IsRelative) + return value; + + return Math.Min((int)MathF.Floor((float)value * param), max); + } + + Gearsets.GearsetStats ret = new(); + + if (SelectedFood != null) + { + ret.CP += CalculateBonus(stats.CP, SelectedFoodHQ, SelectedFood.CP); + ret.Craftsmanship += CalculateBonus(stats.Craftsmanship, SelectedFoodHQ, SelectedFood.Craftsmanship); + ret.Control += CalculateBonus(stats.Control, SelectedFoodHQ, SelectedFood.Control); + } + + if (SelectedMedicine != null) + { + ret.CP += CalculateBonus(stats.CP, SelectedMedicineHQ, SelectedMedicine.CP); + ret.Craftsmanship += CalculateBonus(stats.Craftsmanship, SelectedMedicineHQ, SelectedMedicine.Craftsmanship); + ret.Control += CalculateBonus(stats.Control, SelectedMedicineHQ, SelectedMedicine.Control); + } + + return ret; + } + + private enum CannotCraftReason + { + OK, + WrongClassJob, + SpecialistRequired, + RequiredItem, + RequiredStatus, + CraftsmanshipTooLow, + ControlTooLow, + } + + private CannotCraftReason CanCraftRecipe(Gearsets.GearsetItem[] items, CharacterStats stats) + { + if (!ClassJobUtils.IsClassJob((byte)Service.ClientState.LocalPlayer!.ClassJob.Id, RecipeClassJob)) + { + return CannotCraftReason.WrongClassJob; + } + + if (Recipe.IsSpecializationRequired && !stats.IsSpecialist) + return CannotCraftReason.SpecialistRequired; + + if (Recipe.ItemRequired.Row != 0) + { + if (Recipe.ItemRequired.Value != null) + { + if (!items.Any(i => Gearsets.IsItem(i, Recipe.ItemRequired.Row))) + { + return CannotCraftReason.RequiredItem; + } + } + } + + if (Recipe.StatusRequired.Row != 0) + { + if (Recipe.StatusRequired.Value != null) + { + if (!Service.ClientState.LocalPlayer.StatusList.Any(s => s.StatusId == Recipe.StatusRequired.Row)) + { + return CannotCraftReason.RequiredStatus; + } + } + } + + if (Recipe.RequiredCraftsmanship > stats.Craftsmanship) + return CannotCraftReason.CraftsmanshipTooLow; + + if (Recipe.RequiredControl > stats.Control) + return CannotCraftReason.ControlTooLow; + + return CannotCraftReason.OK; + } + + private static RecipeInfo CreateRecipeInfo(Recipe recipe) + { + var recipeTable = recipe.RecipeLevelTable.Value!; + return new() + { + IsExpert = recipe.IsExpert, + ClassJobLevel = recipeTable.ClassJobLevel, + RLvl = (int)recipeTable.RowId, + ConditionsFlag = recipeTable.ConditionsFlag, + MaxDurability = recipeTable.Durability * recipe.DurabilityFactor / 100, + MaxQuality = (int)recipeTable.Quality * recipe.QualityFactor / 100, + MaxProgress = recipeTable.Difficulty * recipe.DifficultyFactor / 100, + QualityModifier = recipeTable.QualityModifier, + QualityDivider = recipeTable.QualityDivider, + ProgressModifier = recipeTable.ProgressModifier, + ProgressDivider = recipeTable.ProgressDivider, + }; + } + + private static string GetCannotCraftReasonText(CannotCraftReason reason) => + reason switch + { + CannotCraftReason.OK => "You can craft this recipe.", + CannotCraftReason.WrongClassJob => "Your current class cannot craft this recipe.", + CannotCraftReason.SpecialistRequired => "You must be a specialist to craft this recipe.", + CannotCraftReason.RequiredItem => "You do not have the required item to craft this recipe.", + CannotCraftReason.RequiredStatus => "You do not have the required status effect to craft this recipe.", + CannotCraftReason.CraftsmanshipTooLow => "Your craftsmanship is too low to craft this recipe.", + CannotCraftReason.ControlTooLow => "Your control is too low to craft this recipe.", + _ => "Unknown reason.", + }; + + private static string GetCharacterStatsText(CharacterStats stats) + { + var s = new StringBuilder(); + s.AppendLine($"Level {stats.Level} (CLvl {stats.CLvl})"); + s.AppendLine($"Craftsmanship {stats.Craftsmanship}"); + s.AppendLine($"Control {stats.Control}"); + s.AppendLine($"CP {stats.CP}"); + if (stats.IsSpecialist) + s.AppendLine($" + Specialist"); + if (stats.HasSplendorousBuff) + s.AppendLine($" + Splendorous Tool"); + return s.ToString(); + } } diff --git a/Craftimizer/Windows/SettingsWindow.cs b/Craftimizer/Windows/SettingsWindow.cs new file mode 100644 index 0000000..911ff65 --- /dev/null +++ b/Craftimizer/Windows/SettingsWindow.cs @@ -0,0 +1,37 @@ +using Craftimizer.Simulator; +using Dalamud.Interface.Windowing; +using Dalamud.Interface; +using ImGuiNET; +using Lumina.Excel.GeneratedSheets; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Numerics; + +namespace Craftimizer.Plugin.Windows; + +public class SettingsWindow : Window +{ + private static Configuration Config => Service.Configuration; + + public SettingsWindow() : base("Craftimizer") + { + Service.WindowSystem.AddWindow(this); + + SizeConstraints = new WindowSizeConstraints() + { + MinimumSize = new Vector2(400, 400), + MaximumSize = new Vector2(float.MaxValue, float.MaxValue) + }; + Size = SizeConstraints.Value.MinimumSize; + } + + public override void Draw() + { + var val = Config.OverrideUncraftability; + if (ImGui.Checkbox("Override Uncraftability Warning", ref val)) + Config.OverrideUncraftability = val; + } +} diff --git a/Craftimizer/Windows/SimulatorWindow.cs b/Craftimizer/Windows/SimulatorWindow.cs new file mode 100644 index 0000000..c14868f --- /dev/null +++ b/Craftimizer/Windows/SimulatorWindow.cs @@ -0,0 +1,391 @@ +using Craftimizer.Simulator; +using Craftimizer.Simulator.Actions; +using Dalamud.Interface; +using Dalamud.Interface.Windowing; +using Dalamud.Logging; +using Dalamud.Plugin; +using Dalamud.Utility; +using ImGuiNET; +using ImGuiScene; +using Lumina.Excel.GeneratedSheets; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Numerics; +using ActionCategory = Craftimizer.Simulator.ActionCategory; +using ClassJob = Craftimizer.Simulator.ClassJob; + +namespace Craftimizer.Plugin.Windows; + +public class SimulatorWindow : Window +{ + private static readonly Vector2 ProgressBarSize = new(200, 20); + private static readonly Vector2 TooltipProgressBarSize = new(100, 5); + + private static readonly Vector4 ProgressColor = new(.2f, 1f, .2f, 1f); + private static readonly Vector4 QualityColor = new(.2f, .2f, 1f, 1f); + private static readonly Vector4 DurabilityColor = new(1f, 1f, .2f, 1f); + private static readonly Vector4 CPColor = new(1f, .2f, 1f, 1f); + + private static readonly Vector4 CPColorNew = new(0.38f, 0.77f, 1f, 1f); + + private static readonly Vector4 BadActionImageTint = new(1f, .5f, .5f, 1f); + private static readonly Vector4 BadActionImageColor = new(1f, .3f, .3f, 1f); + + private static readonly Vector4 BadActionTextColor = new(1f, .2f, .2f, 1f); + + private static readonly (ActionCategory Category, ActionType[] Actions)[] SortedActions; + + private TimeSpan FrameTime { get; set; } + private Stopwatch Stopwatch { get; } = new(); + + private Item Item { get; } + private SimulationInput Input { get; } + private ClassJob ClassJob { get; } + // State is the state of the simulation *after* its corresponding action is executed. + private List<(ActionType Action, string Tooltip, ActionResponse Response, SimulationState State)> Actions { get; } + private Simulator.Simulator Simulator { get; } + + private SimulationState LatestState => Actions.Count == 0 ? new(Input) : Actions[^1].State; + + private ActionType? DraggedAction { get; set; } + + static SimulatorWindow() + { + SortedActions = Enum.GetValues().GroupBy(a => a.Category()).Select(g => (g.Key, g.OrderBy(a => a.Level()).ToArray())).ToArray(); + } + + public SimulatorWindow(Item item, SimulationInput input, ClassJob classJob, List actions) : base("Simulator") + { + Service.WindowSystem.AddWindow(this); + + Item = item; + Input = input; + ClassJob = classJob; + Actions = new(); + Simulator = Service.Configuration.CreateSimulator(new(input)); + + foreach(var action in actions) + AppendAction(action); + + IsOpen = true; + } + + public override void PreDraw() + { + Stopwatch.Restart(); + + base.PreDraw(); + } + + public override void PostDraw() + { + Stopwatch.Stop(); + FrameTime = Stopwatch.Elapsed; + + base.PostDraw(); + } + + public override void Draw() + { + ImGui.BeginTable("simulatorWindow", 2, ImGuiTableFlags.Resizable); + + ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, 300); + ImGui.TableNextColumn(); + DrawActions(); + + ImGui.TableNextColumn(); + DrawSimulationInfo(); + + ImGui.EndTable(); + + ImGui.TextUnformatted($"{FrameTime.TotalMilliseconds:0.00}ms"); + } + + private void DrawActions() + { + ImGui.BeginChild("CraftimizerActions", Vector2.Zero, true, ImGuiWindowFlags.NoDecoration); + //ImGui.Checkbox("Show only guaranteed actions", ref showOnlyGuaranteedActions); + ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero); + + var actionSize = new Vector2(ImGui.GetFontSize() * 2); + foreach (var (category, actions) in SortedActions) + { + var i = 0; + ImGuiUtils.BeginGroupPanel(category.GetDisplayName()); + foreach (var action in actions) + { + var baseAction = action.Base(); + + var cannotUse = action.Level() > Input.Stats.Level || (action == ActionType.Manipulation && !Input.Stats.CanUseManipulation); + var shouldNotUse = !baseAction.CanUse(Simulator) || Simulator.IsComplete; + + ImGui.BeginDisabled(cannotUse); + + if (shouldNotUse) + ImGui.PushStyleColor(ImGuiCol.Button, BadActionImageColor); + + if (ImGui.ImageButton(action.GetIcon(ClassJob).ImGuiHandle, actionSize, Vector2.Zero, Vector2.One, -1, default, shouldNotUse ? BadActionImageTint : Vector4.One)) + AppendAction(action); + + if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) + ImGui.SetTooltip($"{action.GetName(ClassJob)}\n{baseAction.GetTooltip(Simulator, true)}"); + + if (shouldNotUse) + ImGui.PopStyleColor(); + + ImGui.EndDisabled(); + + if (++i % 5 != 0) + ImGui.SameLine(); + } + ImGuiUtils.EndGroupPanel(); + } + ImGui.PopStyleVar(); + ImGui.EndChild(); + } + + private void DrawSimulationInfo() + { + ImGui.BeginChild("simulationInfo", Vector2.Zero, true, ImGuiWindowFlags.NoDecoration); + DrawSimulationSynth(); + ImGuiHelpers.ScaledDummy(5); + DrawSimulationEffects(); + ImGuiHelpers.ScaledDummy(5); + DrawSimulationActions(); + ImGui.EndChild(); + } + + private void DrawSimulationSynth() + { + var state = LatestState; + var imageSize = new Vector2(ImGui.GetFontSize() * 2f); + + ImGui.Image(Icons.GetIconFromId(Item.Icon).ImGuiHandle, imageSize); + ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); + ImGui.SetCursorPosY(ImGui.GetFontSize()*.75f); + ImGui.TextUnformatted(Item.Name.ToDalamudString().ToString()); + var availWidth = ImGui.GetContentRegionAvail().X; + var text = $"Step {state.StepCount + 1}"; + var textWidth = ImGui.CalcTextSize(text).X; + ImGui.SameLine(availWidth - textWidth); + ImGui.AlignTextToFramePadding(); + ImGui.SetCursorPosY(ImGui.GetFontSize() * .75f); + ImGui.TextUnformatted(text); + ImGui.Separator(); + + ImGui.BeginTable("simSynth", 2); + + ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, 110); + ImGui.TableNextColumn(); + ImGuiUtils.BeginGroupPanel("Durability"); + ImGui.TextUnformatted($"{state.Durability} / {Input.Recipe.MaxDurability}"); + DrawProgressBar(state.Durability, Input.Recipe.MaxDurability, new(100, 20), CPColorNew); + ImGuiUtils.EndGroupPanel(); + + ImGuiUtils.BeginGroupPanel("Condition"); + ImGui.TextUnformatted(state.Condition.Name()); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip(state.Condition.Description(state.Input.Stats.HasSplendorousBuff)); + ImGuiUtils.EndGroupPanel(); + + ImGui.TableNextColumn(); + + ImGuiUtils.BeginGroupPanel("Progress"); + DrawProgressBar(state.Progress, Input.Recipe.MaxProgress, new(200, 20), ProgressColor); + availWidth = ImGui.GetContentRegionAvail().X; + text = $"{state.Progress} / {Input.Recipe.MaxProgress}"; + textWidth = ImGui.CalcTextSize(text).X; + ImGui.SameLine(availWidth - textWidth - 10); + ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ((ImGui.GetFrameHeight() - ImGui.GetFontSize()) / 2f)); + ImGui.TextUnformatted(text); + ImGuiUtils.EndGroupPanel(); + + ImGuiUtils.BeginGroupPanel("Quality"); + DrawProgressBar(state.Quality, Input.Recipe.MaxQuality, new(200, 20), QualityColor); + availWidth = ImGui.GetContentRegionAvail().X; + text = $"{state.Quality} / {Input.Recipe.MaxQuality}"; + textWidth = ImGui.CalcTextSize(text).X; + ImGui.SameLine(availWidth - textWidth - 10); + ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ((ImGui.GetFrameHeight() - ImGui.GetFontSize()) / 2f)); + ImGui.TextUnformatted(text); + ImGuiUtils.EndGroupPanel(); + + ImGuiUtils.BeginGroupPanel("CP"); + DrawProgressBar(state.CP, Input.Stats.CP, new(200, 20), CPColor); + availWidth = ImGui.GetContentRegionAvail().X; + text = $"{state.CP} / {Input.Stats.CP}"; + textWidth = ImGui.CalcTextSize(text).X; + ImGui.SameLine(availWidth - textWidth - 10); + ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ((ImGui.GetFrameHeight() - ImGui.GetFontSize()) / 2f)); + ImGui.TextUnformatted(text); + ImGuiUtils.EndGroupPanel(); + + ImGui.Separator(); + ImGui.TextUnformatted($"HQ {state.HQPercent}%"); + + ImGui.EndTable(); + } + + private void DrawSimulationSynthOld() + { + var state = LatestState; + + ImGui.Text($"Step {state.StepCount + 1}"); + ImGui.Text(state.Condition.Name()); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip(state.Condition.Description(state.Input.Stats.HasSplendorousBuff)); + ImGui.Text($"{state.HQPercent}%% HQ"); + DrawProgressBarOld(state.Progress, Input.Recipe.MaxProgress, ProgressColor); + DrawProgressBarOld(state.Quality, Input.Recipe.MaxQuality, QualityColor); + DrawProgressBarOld(state.Durability, Input.Recipe.MaxDurability, DurabilityColor); + DrawProgressBarOld(state.CP, Input.Stats.CP, CPColor); + } + + private void DrawSimulationEffects() + { + ImGui.Text($"Effects:"); + + var effectHeight = ImGui.GetFontSize() * 2f; + Vector2 GetEffectSize(TextureWrap icon) => new(icon.Width * effectHeight / icon.Height, effectHeight); + + foreach (var effect in Enum.GetValues()) + { + var duration = Simulator.GetEffectDuration(effect); + if (duration == 0) + continue; + + var strength = Simulator.GetEffectStrength(effect); + var icon = effect.GetIcon(strength); + + ImGui.Image(icon.ImGuiHandle, GetEffectSize(icon)); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip(effect.GetTooltip(strength, duration)); + ImGui.SameLine(); + } + ImGui.Dummy(Vector2.Zero); + } + + private void DrawSimulationActions() + { + ImGui.Text($"Actions:"); + + var actionSize = new Vector2(ImGui.GetFontSize() * 2f); + ImGui.PushStyleColor(ImGuiCol.Button, Vector4.Zero); + ImGui.PushStyleColor(ImGuiCol.ButtonActive, Vector4.Zero); + ImGui.PushStyleColor(ImGuiCol.ButtonHovered, Vector4.Zero); + for (var i = 0; i < Actions.Count; ++i) + { + var (action, tooltip, response, state) = Actions[i]; + if (ImGui.ImageButton(action.GetIcon(ClassJob).ImGuiHandle, actionSize, Vector2.Zero, Vector2.One, 0, default, response != ActionResponse.UsedAction ? BadActionImageTint : Vector4.One)) + RemoveAction(i); + if (ImGui.BeginDragDropSource()) + { + unsafe { ImGui.SetDragDropPayload("simulationAction", (nint)(void*)&i, sizeof(int)); } + ImGui.ImageButton(action.GetIcon(ClassJob).ImGuiHandle, actionSize); + ImGui.EndDragDropSource(); + } + if (ImGui.BeginDragDropTarget()) + { + var payload = ImGui.AcceptDragDropPayload("simulationAction"); + unsafe + { + if (payload.NativePtr != null) + { + int droppedIdx; + droppedIdx = *(int*)payload.Data; + var droppedAction = Actions[droppedIdx].Action; + RemoveAction(droppedIdx); + InsertAction(i, droppedAction); + } + } + } + if (ImGui.IsItemHovered()) + { + ImGui.BeginTooltip(); + var responseText = response switch + { + ActionResponse.SimulationComplete => "Recipe Complete", + ActionResponse.ActionNotUnlocked => "Action Not Unlocked", + ActionResponse.NotEnoughCP => "Not Enough CP", + ActionResponse.NoDurability => "No More Durability", + ActionResponse.CannotUseAction => "Cannot Use", + _ => string.Empty, + }; + if (response != ActionResponse.UsedAction) + ImGui.TextColored(BadActionTextColor, responseText); + ImGui.Text($"{action.GetName(ClassJob)}\n{tooltip}"); + DrawProgressBarTooltip(state.Progress, Input.Recipe.MaxProgress, ProgressColor); + DrawProgressBarTooltip(state.Quality, Input.Recipe.MaxQuality, QualityColor); + DrawProgressBarTooltip(state.Durability, Input.Recipe.MaxDurability, DurabilityColor); + DrawProgressBarTooltip(state.CP, Input.Stats.CP, CPColor); + ImGui.Text("Right Click to Remove\nDrag to Move"); + ImGui.EndTooltip(); + } + ImGui.SameLine(); + } + ImGui.PopStyleColor(3); + } + + private void AppendAction(ActionType action) + { + var tooltip = action.Base().GetTooltip(Simulator, false); + var (response, state) = Simulator.Execute(LatestState, action); + Actions.Add((action, tooltip, response, state)); + } + + private void RemoveAction(int actionIndex) + { + // Remove action + Actions.RemoveAt(actionIndex); + + // Take note of all actions afterwards + Span succeedingActions = stackalloc ActionType[Actions.Count - actionIndex]; + for (var i = 0; i < succeedingActions.Length; i++) + succeedingActions[i] = Actions[i + actionIndex].Action; + + // Remove all future actions + Actions.RemoveRange(actionIndex, succeedingActions.Length); + + // Re-execute all future actions + foreach (var action in succeedingActions) + AppendAction(action); + } + + private void InsertAction(int actionIndex, ActionType action) + { + // Take note of all actions afterwards + Span succeedingActions = stackalloc ActionType[Actions.Count - actionIndex]; + for (var i = 0; i < succeedingActions.Length; i++) + succeedingActions[i] = Actions[i + actionIndex].Action; + + // Remove all future actions + Actions.RemoveRange(actionIndex, succeedingActions.Length); + + // Execute new action + AppendAction(action); + + // Re-execute all future actions + foreach (var succeededAction in succeedingActions) + AppendAction(succeededAction); + } + + private static void DrawProgressBarTooltip(int progress, int maxProgress, Vector4 color) + { + ImGui.PushStyleColor(ImGuiCol.PlotHistogram, color); + ImGui.ProgressBar(Math.Clamp((float)progress / maxProgress, 0f, 1f), TooltipProgressBarSize); + ImGui.PopStyleColor(); + } + + private static void DrawProgressBarOld(int progress, int maxProgress, Vector4 color) => + DrawProgressBar(progress, maxProgress, ProgressBarSize, color, $"{progress} / {maxProgress}"); + + private static void DrawProgressBar(int progress, int maxProgress, Vector2 size, Vector4 color, string overlay = "") + { + ImGui.PushStyleColor(ImGuiCol.PlotHistogram, color); + ImGui.ProgressBar(Math.Clamp((float)progress / maxProgress, 0f, 1f), size, overlay); + ImGui.PopStyleColor(); + } +}