diff --git a/Craftimizer/Craftimizer.csproj b/Craftimizer/Craftimizer.csproj index 1b2e168..c021083 100644 --- a/Craftimizer/Craftimizer.csproj +++ b/Craftimizer/Craftimizer.csproj @@ -28,6 +28,12 @@ + + + + + + @@ -50,6 +56,10 @@ $(DalamudLibPath)Dalamud.dll false + + $(DalamudLibPath)Dalamud.Interface.dll + false + $(DalamudLibPath)ImGui.NET.dll false diff --git a/Craftimizer/Graphics/collectible_badge.png b/Craftimizer/Graphics/collectible_badge.png new file mode 100644 index 0000000..5b3b890 Binary files /dev/null and b/Craftimizer/Graphics/collectible_badge.png differ diff --git a/Craftimizer/Graphics/expert.png b/Craftimizer/Graphics/expert.png new file mode 100644 index 0000000..db26b79 Binary files /dev/null and b/Craftimizer/Graphics/expert.png differ diff --git a/Craftimizer/Graphics/expert_badge.png b/Craftimizer/Graphics/expert_badge.png new file mode 100644 index 0000000..5186387 Binary files /dev/null and b/Craftimizer/Graphics/expert_badge.png differ diff --git a/Craftimizer/Graphics/no_manip.png b/Craftimizer/Graphics/no_manip.png new file mode 100644 index 0000000..d1ef9f9 Binary files /dev/null and b/Craftimizer/Graphics/no_manip.png differ diff --git a/Craftimizer/Graphics/specialist.png b/Craftimizer/Graphics/specialist.png new file mode 100644 index 0000000..2eafa00 Binary files /dev/null and b/Craftimizer/Graphics/specialist.png differ diff --git a/Craftimizer/Graphics/splendorous.png b/Craftimizer/Graphics/splendorous.png new file mode 100644 index 0000000..9817b87 Binary files /dev/null and b/Craftimizer/Graphics/splendorous.png differ diff --git a/Craftimizer/Icons.cs b/Craftimizer/Icons.cs deleted file mode 100644 index 37ebeb0..0000000 --- a/Craftimizer/Icons.cs +++ /dev/null @@ -1,19 +0,0 @@ -using ImGuiScene; -using System.Collections.Generic; - -namespace Craftimizer.Plugin; - -internal static class Icons -{ - private static readonly Dictionary Cache = new(); - - public static TextureWrap GetIconFromPath(string path) - { - if (!Cache.TryGetValue(path, out var ret)) - Cache.Add(path, ret = Service.DataManager.GetImGuiTexture(path)!); - return ret; - } - - public static TextureWrap GetIconFromId(ushort id) => - GetIconFromPath($"ui/icon/{id / 1000 * 1000:000000}/{id:000000}_hr1.tex"); -} diff --git a/Craftimizer/ImGuiUtils.cs b/Craftimizer/ImGuiUtils.cs index c895934..0e34dc6 100644 --- a/Craftimizer/ImGuiUtils.cs +++ b/Craftimizer/ImGuiUtils.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Numerics; - namespace Craftimizer.Plugin; internal static class ImGuiUtils @@ -158,4 +157,24 @@ internal static class ImGuiUtils ImGui.SetTooltip("Open in Browser"); } } + + public static void AlignCentered(float width) + { + var availWidth = ImGui.GetContentRegionAvail().X; + if (availWidth > width) + ImGui.SetCursorPosX(ImGui.GetCursorPos().X + (availWidth - width) / 2); + } + + // https://stackoverflow.com/a/67855985 + public static void TextCentered(string text) + { + AlignCentered(ImGui.CalcTextSize(text).X); + ImGui.TextUnformatted(text); + } + + public static bool ButtonCentered(string text) + { + AlignCentered(ImGui.CalcTextSize(text).X + ImGui.GetStyle().FramePadding.Y * 2); + return ImGui.Button(text); + } } diff --git a/Craftimizer/LuminaSheets.cs b/Craftimizer/LuminaSheets.cs index 67ba2e9..18301f2 100644 --- a/Craftimizer/LuminaSheets.cs +++ b/Craftimizer/LuminaSheets.cs @@ -16,6 +16,9 @@ public static class LuminaSheets public static readonly ExcelSheet ClassJobSheet = Service.DataManager.GetExcelSheet()!; public static readonly ExcelSheet ItemSheet = Service.DataManager.GetExcelSheet()!; public static readonly ExcelSheet ItemSheetEnglish = Service.DataManager.GetExcelSheet(ClientLanguage.English)!; + public static readonly ExcelSheet ENpcResidentSheet = Service.DataManager.GetExcelSheet()!; + public static readonly ExcelSheet LevelSheet = Service.DataManager.GetExcelSheet()!; + public static readonly ExcelSheet QuestSheet = Service.DataManager.GetExcelSheet()!; 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 cb31ff4..c62db6e 100644 --- a/Craftimizer/Plugin.cs +++ b/Craftimizer/Plugin.cs @@ -1,6 +1,7 @@ using Craftimizer.Plugin.Windows; using Craftimizer.Simulator; using Craftimizer.Utils; +using Craftimizer.Windows; using Dalamud.Interface.Windowing; using Dalamud.IoC; using Dalamud.Plugin; @@ -16,39 +17,35 @@ public sealed class Plugin : IDalamudPlugin public string Name => "Craftimizer"; public string Version { get; } public string Author { get; } - public string Configuration { get; } + public string BuildConfiguration { get; } public TextureWrap Icon { get; } public WindowSystem WindowSystem { get; } public Settings SettingsWindow { get; } - public CraftingLog RecipeNoteWindow { get; } + public Craftimizer.Windows.RecipeNote RecipeNoteWindow { get; } public Craft SynthesisWindow { get; } public Windows.Simulator? SimulatorWindow { get; set; } + public Configuration Configuration { get; } public Hooks Hooks { get; } - public RecipeNote RecipeNote { get; } + public Craftimizer.Utils.RecipeNote RecipeNote { get; } + public IconManager IconManager { get; } public Plugin([RequiredVersion("1.0")] DalamudPluginInterface pluginInterface) { - Service.Plugin = this; - pluginInterface.Create(); - Service.Configuration = pluginInterface.GetPluginConfig() as Configuration ?? new Configuration(); + Service.Initialize(this, pluginInterface); + + Configuration = pluginInterface.GetPluginConfig() as Configuration ?? new(); + Hooks = new(); + RecipeNote = new(); + IconManager = new(); + WindowSystem = new(Name); var assembly = Assembly.GetExecutingAssembly(); Version = assembly.GetCustomAttribute()!.InformationalVersion; Author = assembly.GetCustomAttribute()!.Company; - Configuration = assembly.GetCustomAttribute()!.Configuration; - byte[] iconData; - using (var stream = assembly.GetManifestResourceStream("Craftimizer.icon.png")!) - { - iconData = new byte[stream.Length]; - _ = stream.Read(iconData); - } - Icon = Service.PluginInterface.UiBuilder.LoadImage(iconData); - - Hooks = new(); - RecipeNote = new(); - WindowSystem = new(Name); + BuildConfiguration = assembly.GetCustomAttribute()!.Configuration; + Icon = IconManager.GetAssemblyTexture("icon.png"); SettingsWindow = new(); RecipeNoteWindow = new(); @@ -86,5 +83,6 @@ public sealed class Plugin : IDalamudPlugin SynthesisWindow.Dispose(); RecipeNote.Dispose(); Hooks.Dispose(); + IconManager.Dispose(); } } diff --git a/Craftimizer/Service.cs b/Craftimizer/Service.cs index 0c02ad0..e09304e 100644 --- a/Craftimizer/Service.cs +++ b/Craftimizer/Service.cs @@ -1,13 +1,11 @@ -using Dalamud.Data; +using Craftimizer.Utils; using Dalamud.Game; -using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects; -using Dalamud.Game.Command; -using Dalamud.Game.Gui; using Dalamud.Interface.Windowing; using Dalamud.IoC; using Dalamud.Plugin; +using Dalamud.Plugin.Services; namespace Craftimizer.Plugin; @@ -15,19 +13,26 @@ public sealed class Service { #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. [PluginService] public static DalamudPluginInterface PluginInterface { get; private set; } - [PluginService] public static CommandManager CommandManager { get; private set; } - [PluginService] public static ObjectTable Objects { get; private set; } - [PluginService] public static SigScanner SigScanner { get; private set; } - [PluginService] public static GameGui GameGui { get; private set; } - [PluginService] public static ClientState ClientState { get; private set; } - [PluginService] public static DataManager DataManager { get; private set; } - [PluginService] public static TargetManager TargetManager { get; private set; } + [PluginService] public static ICommandManager CommandManager { get; private set; } + [PluginService] public static IObjectTable Objects { get; private set; } + [PluginService] public static ISigScanner SigScanner { get; private set; } + [PluginService] public static IGameGui GameGui { get; private set; } + [PluginService] public static IClientState ClientState { get; private set; } + [PluginService] public static IDataManager DataManager { get; private set; } + [PluginService] public static ITextureProvider TextureProvider { get; private set; } + [PluginService] public static ITargetManager TargetManager { get; private set; } [PluginService] public static Condition Condition { get; private set; } [PluginService] public static Framework Framework { get; private set; } - public static Plugin Plugin { get; internal set; } - public static Configuration Configuration { get; internal set; } + public static Plugin Plugin { get; private set; } + public static Configuration Configuration => Plugin.Configuration; public static WindowSystem WindowSystem => Plugin.WindowSystem; + public static IconManager IconManager => Plugin.IconManager; #pragma warning restore CS8618 + internal static void Initialize(Plugin plugin, DalamudPluginInterface iface) + { + Plugin = plugin; + iface.Create(); + } } diff --git a/Craftimizer/SimulatorUtils.cs b/Craftimizer/SimulatorUtils.cs index 27eeb87..4d30def 100644 --- a/Craftimizer/SimulatorUtils.cs +++ b/Craftimizer/SimulatorUtils.cs @@ -2,6 +2,9 @@ using Craftimizer.Simulator; using Craftimizer.Simulator.Actions; using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Utility; +using FFXIVClientStructs.FFXIV.Client.Game; +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using FFXIVClientStructs.FFXIV.Client.Game.UI; using ImGuiScene; using Lumina.Excel.GeneratedSheets; using System; @@ -10,8 +13,10 @@ using System.Linq; using System.Numerics; using System.Text; using Action = Lumina.Excel.GeneratedSheets.Action; +using ActionType = Craftimizer.Simulator.Actions.ActionType; using ClassJob = Craftimizer.Simulator.ClassJob; using Condition = Craftimizer.Simulator.Condition; +using Status = Lumina.Excel.GeneratedSheets.Status; namespace Craftimizer.Plugin; @@ -85,11 +90,11 @@ internal static class ActionUtils { var (craftAction, action) = GetActionRow(me, classJob); if (craftAction != null) - return Icons.GetIconFromId(craftAction.Icon); + return Service.IconManager.GetIcon(craftAction.Icon); if (action != null) - return Icons.GetIconFromId(action.Icon); + return Service.IconManager.GetIcon(action.Icon); // Old "Steady Hand" action icon - return Icons.GetIconFromId(1953); + return Service.IconManager.GetIcon(1953); } public static ActionType? GetActionTypeFromId(uint actionId, ClassJob classJob, bool isCraftAction) @@ -145,12 +150,24 @@ internal static class ClassJobUtils public static sbyte GetExpArrayIdx(this ClassJob me) => LuminaSheets.ClassJobSheet.GetRow(me.GetClassJobIndex())!.ExpArrayIndex; - public static string GetName(this ClassJob classJob) + public static unsafe short GetPlayerLevel(this ClassJob me) => + PlayerState.Instance()->ClassJobLevelArray[me.GetExpArrayIdx()]; + + public static unsafe bool CanPlayerUseManipulation(this ClassJob me) => + ActionManager.CanUseActionOnTarget(ActionType.Manipulation.GetId(me), (GameObject*)Service.ClientState.LocalPlayer!.Address); + + public static string GetName(this ClassJob me) { - var job = LuminaSheets.ClassJobSheet.GetRow(classJob.GetClassJobIndex())!; + var job = LuminaSheets.ClassJobSheet.GetRow(me.GetClassJobIndex())!; return CultureInfo.InvariantCulture.TextInfo.ToTitleCase(job.Name.ToDalamudString().TextValue); } + public static Quest GetUnlockQuest(this ClassJob me) => + LuminaSheets.QuestSheet.GetRow(65720 + (uint)me) ?? throw new ArgumentException($"Could not get unlock quest for {me}", nameof(me)); + + public static ushort GetIconId(this ClassJob me) => + (ushort)(62100 + me.GetClassJobIndex()); + public static bool IsClassJob(this ClassJobCategory me, ClassJob classJob) => classJob switch { @@ -303,7 +320,7 @@ internal static class EffectUtils } public static TextureWrap GetIcon(this EffectType me, int strength) => - Icons.GetIconFromId(me.GetIconId(strength)); + Service.IconManager.GetIcon(me.GetIconId(strength)); public static string GetTooltip(this EffectType me, int strength, int duration) { diff --git a/Craftimizer/Utils/Gearsets.cs b/Craftimizer/Utils/Gearsets.cs index 86e16c5..0a6dd19 100644 --- a/Craftimizer/Utils/Gearsets.cs +++ b/Craftimizer/Utils/Gearsets.cs @@ -8,7 +8,7 @@ using System; using System.Linq; namespace Craftimizer.Plugin.Utils; -internal static unsafe class Gearsets +public static unsafe class Gearsets { public record struct GearsetStats(int CP, int Craftsmanship, int Control); public record struct GearsetMateria(ushort Type, ushort Grade); @@ -117,9 +117,12 @@ internal static unsafe class Gearsets public static bool IsSpecialistSoulCrystal(GearsetItem item) { + if (item.itemId == 0) + return false; + var luminaItem = LuminaSheets.ItemSheet.GetRow(item.itemId)!; - // Soul Crystal ItemUICategory DoH Category - return luminaItem.ItemUICategory.Row != 62 && luminaItem.ClassJobUse.Value!.ClassJobCategory.Row == 33; + // Soul Crystal ItemUICategory DoH Category + return luminaItem.ItemUICategory.Row == 62 && luminaItem.ClassJobUse.Value!.ClassJobCategory.Row == 33; } public static bool IsSplendorousTool(GearsetItem item) => diff --git a/Craftimizer/Utils/IconManager.cs b/Craftimizer/Utils/IconManager.cs new file mode 100644 index 0000000..abefac2 --- /dev/null +++ b/Craftimizer/Utils/IconManager.cs @@ -0,0 +1,66 @@ +using Craftimizer.Plugin; +using Dalamud.Interface.Internal; +using ImGuiScene; +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; + +namespace Craftimizer.Utils; + +public sealed class IconManager : IDisposable +{ + private readonly Dictionary iconCache = new(); + private readonly Dictionary textureCache = new(); + private readonly Dictionary assemblyCache = new(); + + public IDalamudTextureWrap GetIcon(uint id) + { + if (!iconCache.TryGetValue(id, out var ret)) + iconCache.Add(id, ret = Service.TextureProvider.GetIcon(id) ?? + throw new ArgumentException($"Invalid icon id {id}", nameof(id))); + return ret; + } + + public IDalamudTextureWrap GetTexture(string path) + { + if (!textureCache.TryGetValue(path, out var ret)) + textureCache.Add(path, ret = Service.TextureProvider.GetTextureFromGame(path) ?? + throw new ArgumentException($"Invalid texture {path}", nameof(path))); + return ret; + } + + public TextureWrap GetAssemblyTexture(string filename) + { + if (!assemblyCache.TryGetValue(filename, out var ret)) + assemblyCache.Add(filename, ret = GetAssemblyTextureInternal(filename)); + return ret; + } + + private static TextureWrap GetAssemblyTextureInternal(string filename) + { + var assembly = Assembly.GetExecutingAssembly(); + byte[] iconData; + using (var stream = assembly.GetManifestResourceStream($"Craftimizer.{filename}") ?? throw new InvalidDataException($"Could not load resource {filename}")) + { + iconData = new byte[stream.Length]; + _ = stream.Read(iconData); + } + return Service.PluginInterface.UiBuilder.LoadImage(iconData); + } + + public void Dispose() + { + foreach (var image in iconCache.Values) + image.Dispose(); + iconCache.Clear(); + + foreach (var image in textureCache.Values) + image.Dispose(); + textureCache.Clear(); + + foreach (var image in assemblyCache.Values) + image.Dispose(); + assemblyCache.Clear(); + } +} diff --git a/Craftimizer/Utils/RecipeNote.cs b/Craftimizer/Utils/RecipeNote.cs index 9178682..d27154b 100644 --- a/Craftimizer/Utils/RecipeNote.cs +++ b/Craftimizer/Utils/RecipeNote.cs @@ -16,6 +16,52 @@ using CSRecipeNote = FFXIVClientStructs.FFXIV.Client.Game.UI.RecipeNote; namespace Craftimizer.Utils; +public record RecipeData +{ + public ushort RecipeId { get; } + + public Recipe Recipe { get; } + public RecipeLevelTable Table { get; } + + public ClassJob ClassJob { get; } + public RecipeInfo RecipeInfo { get; } + public int HQIngredientCount { get; } + public int MaxStartingQuality { get; } + + public RecipeData(ushort recipeId) + { + RecipeId = recipeId; + + Recipe = LuminaSheets.RecipeSheet.GetRow(recipeId) ?? + throw new ArgumentException($"Invalid recipe id {recipeId}", nameof(recipeId)); + + Table = Recipe.RecipeLevelTable.Value!; + ClassJob = (ClassJob)Recipe.CraftType.Row; + RecipeInfo = new() + { + IsExpert = Recipe.IsExpert, + ClassJobLevel = Table.ClassJobLevel, + RLvl = (int)Table.RowId, + ConditionsFlag = Table.ConditionsFlag, + MaxDurability = Table.Durability * Recipe.DurabilityFactor / 100, + MaxQuality = (int)Table.Quality * Recipe.QualityFactor / 100, + MaxProgress = Table.Difficulty * Recipe.DifficultyFactor / 100, + QualityModifier = Table.QualityModifier, + QualityDivider = Table.QualityDivider, + ProgressModifier = Table.ProgressModifier, + ProgressDivider = Table.ProgressDivider, + }; + + HQIngredientCount = Recipe.UnkData5 + .Where(i => + i != null && + i.ItemIngredient != 0 && + (LuminaSheets.ItemSheet.GetRow((uint)i.ItemIngredient)?.CanBeHq ?? false) + ).Sum(i => i.AmountIngredient); + MaxStartingQuality = (int)Math.Floor(Recipe.MaterialQualityFactor * RecipeInfo.MaxQuality / 100f); + } +} + public sealed unsafe class RecipeNote : IDisposable { public AddonRecipeNote* AddonRecipe { get; private set; } diff --git a/Craftimizer/Utils/SqText.cs b/Craftimizer/Utils/SqText.cs new file mode 100644 index 0000000..b8ab740 --- /dev/null +++ b/Craftimizer/Utils/SqText.cs @@ -0,0 +1,33 @@ +using Dalamud.Game.Text; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace Craftimizer.Utils; + +public static class SqText +{ + private static ReadOnlyDictionary levelNumReplacements = new(new Dictionary + { + ['0'] = SeIconChar.Number0, + ['1'] = SeIconChar.Number1, + ['2'] = SeIconChar.Number2, + ['3'] = SeIconChar.Number3, + ['4'] = SeIconChar.Number4, + ['5'] = SeIconChar.Number5, + ['6'] = SeIconChar.Number6, + ['7'] = SeIconChar.Number7, + ['8'] = SeIconChar.Number8, + ['9'] = SeIconChar.Number9, + }); + + public static string ToLevelString(T value) where T : IBinaryInteger + { + var str = value.ToString() ?? throw new FormatException("Failed to format value"); + foreach(var (k, v) in levelNumReplacements) + str = str.Replace(k, v.ToIconChar()); + return SeIconChar.LevelEn.ToIconChar() + str; + } +} diff --git a/Craftimizer/Windows/RecipeNote.cs b/Craftimizer/Windows/RecipeNote.cs new file mode 100644 index 0000000..4685ded --- /dev/null +++ b/Craftimizer/Windows/RecipeNote.cs @@ -0,0 +1,542 @@ +using Craftimizer.Plugin; +using Craftimizer.Plugin.Utils; +using Craftimizer.Simulator; +using Craftimizer.Simulator.Actions; +using Craftimizer.Utils; +using Dalamud.Game.Text.SeStringHandling.Payloads; +using Dalamud.Interface; +using Dalamud.Interface.Components; +using Dalamud.Interface.GameFonts; +using Dalamud.Interface.Raii; +using Dalamud.Interface.Windowing; +using Dalamud.Logging; +using Dalamud.Utility; +using FFXIVClientStructs.FFXIV.Client.Game; +using FFXIVClientStructs.FFXIV.Client.Game.UI; +using FFXIVClientStructs.FFXIV.Client.UI; +using FFXIVClientStructs.FFXIV.Client.UI.Misc; +using FFXIVClientStructs.FFXIV.Component.GUI; +using ImGuiNET; +using ImGuiScene; +using System; +using System.Linq; +using System.Numerics; +using ActionType = Craftimizer.Simulator.Actions.ActionType; +using ClassJob = Craftimizer.Simulator.ClassJob; +using CSRecipeNote = FFXIVClientStructs.FFXIV.Client.Game.UI.RecipeNote; + +namespace Craftimizer.Windows; + +public sealed unsafe class RecipeNote : Window, IDisposable +{ + private const ImGuiWindowFlags WindowFlags = ImGuiWindowFlags.NoDecoration + | ImGuiWindowFlags.AlwaysAutoResize + | ImGuiWindowFlags.NoSavedSettings + | ImGuiWindowFlags.NoFocusOnAppearing + | ImGuiWindowFlags.NoNavFocus; + + public enum CraftableStatus + { + OK, + LockedClassJob, + WrongClassJob, + SpecialistRequired, + RequiredItem, + RequiredStatus, + CraftsmanshipTooLow, + ControlTooLow, + } + + public AddonRecipeNote* Addon { get; private set; } + public RecipeData? RecipeData { get; private set; } + public CharacterStats? CharacterStats { get; private set; } + public CraftableStatus CraftStatus { get; private set; } + + private TextureWrap ExpertBadge { get; } + private TextureWrap CollectibleBadge { get; } + private TextureWrap SplendorousBadge { get; } + private TextureWrap SpecialistBadge { get; } + private TextureWrap NoManipulationBadge { get; } + private GameFontHandle AxisFont { get; } + + public RecipeNote() : base("Craftimizer RecipeNode", WindowFlags, false) + { + 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"); + AxisFont = Service.PluginInterface.UiBuilder.GetGameFontHandle(new(GameFontFamilyAndSize.Axis14)); + + Service.WindowSystem.AddWindow(this); + + IsOpen = true; + } + + public override bool DrawConditions() + { + if (Service.ClientState.LocalPlayer == null) + return false; + + { + Addon = (AddonRecipeNote*)Service.GameGui.GetAddonByName("RecipeNote"); + if (Addon == null) + return false; + + // Check if RecipeNote addon is visible + if (Addon->AtkUnitBase.WindowNode == null) + return false; + + // Check if RecipeNote has a visible selected recipe + if (!Addon->Unk258->IsVisible) + return false; + } + + { + var instance = CSRecipeNote.Instance(); + + var list = instance->RecipeList; + if (list == null) + return false; + + var recipeEntry = list->SelectedRecipe; + if (recipeEntry == null) + return false; + + var recipeId = recipeEntry->RecipeId; + RecipeData = new(recipeId); + } + + Gearsets.GearsetItem[] gearItems; + { + var gearStats = Gearsets.CalculateGearsetCurrentStats(); + + var container = InventoryManager.Instance()->GetInventoryContainer(InventoryType.EquippedItems); + if (container == null) + return false; + + gearItems = Gearsets.GetGearsetItems(container); + + CharacterStats = Gearsets.CalculateCharacterStats(gearStats, gearItems, RecipeData.ClassJob.GetPlayerLevel(), RecipeData.ClassJob.CanPlayerUseManipulation()); + } + + CraftStatus = CalculateCraftStatus(gearItems); + + return true; + } + + public override void PreDraw() + { + ref var unit = ref Addon->AtkUnitBase; + var scale = unit.Scale; + var pos = new Vector2(unit.X, unit.Y); + var size = new Vector2(unit.WindowNode->AtkResNode.Width, unit.WindowNode->AtkResNode.Height) * scale; + + var node = (AtkResNode*)Addon->Unk458; // unit.GetNodeById(59); + var nodeParent = Addon->Unk258; // unit.GetNodeById(57); + + Position = pos + new Vector2(size.X, (nodeParent->Y + node->Y) * scale); + SizeConstraints = new WindowSizeConstraints + { + MinimumSize = new(-1), + MaximumSize = new(10000, 10000) + }; + } + + public override void Draw() + { + using var table = ImRaii.Table("stats", 2, ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.SizingStretchSame); + if (table) + { + ImGui.TableSetupColumn("col1", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("col2", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableNextColumn(); + DrawCharacterStats(); + ImGui.TableNextColumn(); + DrawRecipeStats(); + } + } + + private void DrawCharacterStats() + { + ImGuiUtils.TextCentered("Crafter"); + + var level = RecipeData!.ClassJob.GetPlayerLevel(); + { + var className = RecipeData.ClassJob.GetName(); + var levelText = string.Empty; + if (level != 0) + levelText = SqText.ToLevelString(level); + var imageSize = ImGuiUtils.ButtonHeight; + bool hasSplendorous = false, hasSpecialist = false, shouldHaveManip = false; + if (CraftStatus is not (CraftableStatus.LockedClassJob or CraftableStatus.WrongClassJob)) + { + hasSplendorous = CharacterStats!.HasSplendorousBuff; + hasSpecialist = CharacterStats!.IsSpecialist; + shouldHaveManip = !CharacterStats.CanUseManipulation && CharacterStats.Level >= ActionType.Manipulation.Level(); + } + + ImGuiUtils.AlignCentered( + imageSize + 5 + + ImGui.CalcTextSize(className).X + + (level == 0 ? 0 : (3 + ImGui.CalcTextSize(levelText).X)) + + (hasSplendorous ? (3 + imageSize) : 0) + + (hasSpecialist ? (3 + imageSize) : 0) + + (shouldHaveManip ? (3 + imageSize) : 0) + ); + ImGui.AlignTextToFramePadding(); + + ImGui.Image(Service.IconManager.GetIcon(RecipeData.ClassJob.GetIconId()).ImGuiHandle, new Vector2(imageSize)); + ImGui.SameLine(0, 5); + + if (level != 0) + { + ImGui.Text(levelText); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip($"CLvl {Gearsets.CalculateCLvl(level)}"); + ImGui.SameLine(0, 3); + } + + ImGui.Text(className); + + if (hasSplendorous) + { + ImGui.SameLine(0, 3); + ImGui.Image(SplendorousBadge.ImGuiHandle, new Vector2(imageSize)); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip($"Splendorous Tool"); + } + + if (hasSpecialist) + { + ImGui.SameLine(0, 3); + ImGui.Image(SpecialistBadge.ImGuiHandle, new Vector2(imageSize), Vector2.Zero, Vector2.One, new(0.99f, 0.97f, 0.62f, 1f)); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip($"Specialist"); + } + + if (shouldHaveManip) + { + ImGui.SameLine(0, 3); + ImGui.Image(NoManipulationBadge.ImGuiHandle, new Vector2(imageSize)); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip($"No Manipulation (Missing Job Quest)"); + } + } + + ImGui.Separator(); + + switch (CraftStatus) + { + case CraftableStatus.LockedClassJob: + { + ImGuiUtils.TextCentered($"You do not have {RecipeData.ClassJob.GetName().ToLowerInvariant()} unlocked."); + ImGui.Separator(); + var unlockQuest = RecipeData.ClassJob.GetUnlockQuest(); + var (questGiver, questTerritory, questLocation, mapPayload) = ResolveLevelData(unlockQuest.IssuerLocation.Row); + + var unlockText = $"Unlock it from {questGiver}"; + ImGuiUtils.AlignCentered(ImGui.CalcTextSize(unlockText).X + 5 + ImGuiUtils.ButtonHeight); + ImGui.AlignTextToFramePadding(); + ImGui.Text(unlockText); + ImGui.SameLine(0, 5); + if (ImGuiComponents.IconButton(FontAwesomeIcon.Flag)) + Service.GameGui.OpenMapWithMapLink(mapPayload); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Open in map"); + + ImGuiUtils.TextCentered($"{questTerritory} ({questLocation.X:0.0}, {questLocation.Y:0.0})"); + } + break; + case CraftableStatus.WrongClassJob: + { + ImGuiUtils.TextCentered($"You are not a {RecipeData.ClassJob.GetName().ToLowerInvariant()}."); + var gearsetId = GetGearsetForJob(RecipeData.ClassJob); + if (gearsetId.HasValue) + { + if (ImGuiUtils.ButtonCentered("Switch Job")) + Chat.SendMessage($"/gearset change {gearsetId + 1}"); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip($"Swap to gearset {gearsetId + 1}"); + } + else + ImGuiUtils.TextCentered($"You do not have any {RecipeData.ClassJob.GetName().ToLowerInvariant()} gearsets."); + } + break; + case CraftableStatus.SpecialistRequired: + { + ImGuiUtils.TextCentered($"You need to be a specialist to craft this recipe."); + + var (vendorName, vendorTerritory, vendorLoation, mapPayload) = ResolveLevelData(5891399); + + var unlockText = $"Trade a Soul of the Crafter to {vendorName}"; + ImGuiUtils.AlignCentered(ImGui.CalcTextSize(unlockText).X + 5 + ImGuiUtils.ButtonHeight); + ImGui.AlignTextToFramePadding(); + ImGui.Text(unlockText); + ImGui.SameLine(0, 5); + if (ImGuiComponents.IconButton(FontAwesomeIcon.Flag)) + Service.GameGui.OpenMapWithMapLink(mapPayload); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Open in map"); + + ImGuiUtils.TextCentered($"{vendorTerritory} ({vendorLoation.X:0.0}, {vendorLoation.Y:0.0})"); + } + break; + case CraftableStatus.RequiredItem: + { + var item = RecipeData.Recipe.ItemRequired.Value!; + var itemName = item.Name.ToDalamudString().ToString(); + var imageSize = ImGuiUtils.ButtonHeight; + + ImGuiUtils.TextCentered($"You are missing the required equipment."); + ImGuiUtils.AlignCentered(imageSize + 5 + ImGui.CalcTextSize(itemName).X); + ImGui.AlignTextToFramePadding(); + ImGui.Image(Service.IconManager.GetIcon(item.Icon).ImGuiHandle, new(imageSize)); + ImGui.SameLine(0, 5); + ImGui.Text(itemName); + } + break; + case CraftableStatus.RequiredStatus: + { + var status = RecipeData.Recipe.StatusRequired.Value!; + var statusName = status.Name.ToDalamudString().ToString(); + var statusIcon = Service.IconManager.GetIcon(status.Icon); + var imageSize = new Vector2(ImGuiUtils.ButtonHeight * statusIcon.Width / statusIcon.Height, ImGuiUtils.ButtonHeight); + + ImGuiUtils.TextCentered($"You are missing the required status effect."); + ImGuiUtils.AlignCentered(imageSize.X + 5 + ImGui.CalcTextSize(statusName).X); + ImGui.AlignTextToFramePadding(); + ImGui.Image(statusIcon.ImGuiHandle, imageSize); + ImGui.SameLine(0, 5); + ImGui.Text(statusName); + } + break; + case CraftableStatus.CraftsmanshipTooLow: + { + ImGuiUtils.TextCentered("Your Craftsmanship is too low."); + + DrawRequiredStatsTable(CharacterStats!.Craftsmanship, RecipeData.Recipe.RequiredCraftsmanship); + } + break; + case CraftableStatus.ControlTooLow: + { + ImGuiUtils.TextCentered("Your Control is too low."); + + DrawRequiredStatsTable(CharacterStats!.Control, RecipeData.Recipe.RequiredControl); + } + break; + case CraftableStatus.OK: + { + using var table = ImRaii.Table("characterStats", 2, ImGuiTableFlags.NoHostExtendX); + if (table) + { + ImGui.TableSetupColumn("ccol1", ImGuiTableColumnFlags.WidthFixed, 100); + ImGui.TableSetupColumn("ccol2", ImGuiTableColumnFlags.WidthStretch); + + ImGui.TableNextColumn(); + ImGui.Text("Craftsmanship"); + ImGui.TableNextColumn(); + ImGui.Text($"{CharacterStats!.Craftsmanship}"); + + ImGui.TableNextColumn(); + ImGui.Text("Control"); + ImGui.TableNextColumn(); + ImGui.Text($"{CharacterStats.Control}"); + + ImGui.TableNextColumn(); + ImGui.Text("CP"); + ImGui.TableNextColumn(); + ImGui.Text($"{CharacterStats.CP}"); + } + } + break; + } + } + + private void DrawRecipeStats() + { + 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 + 3, layout.Height); + } + var textLevel = SqText.ToLevelString(RecipeData.RecipeInfo.ClassJobLevel); + var isExpert = RecipeData.RecipeInfo.IsExpert; + var isCollectable = RecipeData.Recipe.ItemResult.Value!.IsCollectable; + var imageSize = ImGuiUtils.ButtonHeight; + var textSize = ImGui.CalcTextSize("A").Y; + var badgeSize = new Vector2(textSize * ExpertBadge.Width / ExpertBadge.Height, textSize); + var badgeOffset = (imageSize - badgeSize.Y) / 2; + + ImGuiUtils.AlignCentered( + imageSize + 5 + + ImGui.CalcTextSize(textLevel).X + + textStarsSize.X + + (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); + ImGui.Text(textLevel); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip($"RLvl {RecipeData.RecipeInfo.RLvl}"); + + if (textStarsSize != Vector2.Zero) + { + ImGui.SameLine(0, 3); + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (textStarsSize.Y - textSize) / 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 table = ImRaii.Table("recipeStats", 2); + if (table) + { + ImGui.TableSetupColumn("rcol1", ImGuiTableColumnFlags.WidthFixed, 100); + ImGui.TableSetupColumn("rcol2", ImGuiTableColumnFlags.WidthStretch); + + ImGui.TableNextColumn(); + ImGui.Text("Progress"); + ImGui.TableNextColumn(); + ImGui.Text($"{RecipeData.RecipeInfo.MaxProgress}"); + + ImGui.TableNextColumn(); + ImGui.Text("Quality"); + ImGui.TableNextColumn(); + ImGui.Text($"{RecipeData.RecipeInfo.MaxQuality}"); + + ImGui.TableNextColumn(); + ImGui.Text("Durability"); + ImGui.TableNextColumn(); + ImGui.Text($"{RecipeData.RecipeInfo.MaxDurability}"); + } + } + + private static void DrawRequiredStatsTable(int current, int required) + { + if (current >= required) + throw new ArgumentOutOfRangeException(nameof(current)); + + using var table = ImRaii.Table("requiredStats", 2, ImGuiTableFlags.NoHostExtendX); + if (table) + { + ImGui.TableSetupColumn("ccol1", ImGuiTableColumnFlags.WidthFixed, 100); + ImGui.TableSetupColumn("ccol2", ImGuiTableColumnFlags.WidthStretch); + + ImGui.TableNextColumn(); + ImGui.Text("Current"); + ImGui.TableNextColumn(); + ImGui.TextColored(new(0, 1, 0, 1), $"{current}"); + + ImGui.TableNextColumn(); + ImGui.Text("Required"); + ImGui.TableNextColumn(); + ImGui.TextColored(new(1, 0, 0, 1), $"{required}"); + + ImGui.TableNextColumn(); + ImGui.Text("You need"); + ImGui.TableNextColumn(); + ImGui.Text($"{required - current}"); + } + } + + private CraftableStatus CalculateCraftStatus(Gearsets.GearsetItem[] gearItems) + { + if (RecipeData!.ClassJob.GetPlayerLevel() == 0) + return CraftableStatus.LockedClassJob; + + if (PlayerState.Instance()->CurrentClassJobId != RecipeData.ClassJob.GetClassJobIndex()) + return CraftableStatus.WrongClassJob; + + if (RecipeData.Recipe.IsSpecializationRequired && !(CharacterStats!.IsSpecialist)) + return CraftableStatus.SpecialistRequired; + + var itemRequired = RecipeData.Recipe.ItemRequired; + if (itemRequired.Row != 0 && itemRequired.Value != null) + { + if (!gearItems.Any(i => Gearsets.IsItem(i, itemRequired.Row))) + return CraftableStatus.RequiredItem; + } + + var statusRequired = RecipeData.Recipe.StatusRequired; + if (statusRequired.Row != 0 && statusRequired.Value != null) + { + if (!Service.ClientState.LocalPlayer!.StatusList.Any(s => s.StatusId == statusRequired.Row)) + return CraftableStatus.RequiredStatus; + } + + if (RecipeData.Recipe.RequiredCraftsmanship > CharacterStats!.Craftsmanship) + return CraftableStatus.CraftsmanshipTooLow; + + if (RecipeData.Recipe.RequiredControl > CharacterStats.Control) + return CraftableStatus.ControlTooLow; + + return CraftableStatus.OK; + } + + private static (string NpcName, string Territory, Vector2 MapLocation, MapLinkPayload Payload) ResolveLevelData(uint levelRowId) + { + var level = LuminaSheets.LevelSheet.GetRow(levelRowId) ?? + throw new ArgumentNullException(nameof(levelRowId), $"Invalid level row {levelRowId}"); + var territory = level.Territory.Value!.PlaceName.Value!.Name.ToDalamudString().ToString(); + var location = MapUtil.WorldToMap(new(level.X, level.Z), level.Map.Value!); + + return (ResolveNpcResidentName(level.Object), territory, location, new(level.Territory.Row, level.Map.Row, location.X, location.Y)); + } + + private static string ResolveNpcResidentName(uint npcRowId) + { + var resident = LuminaSheets.ENpcResidentSheet.GetRow(npcRowId) ?? + throw new ArgumentNullException(nameof(npcRowId), $"Invalid npc row {npcRowId}"); + return resident.Singular.ToDalamudString().ToString(); + } + + private static int? GetGearsetForJob(ClassJob job) + { + var gearsetModule = RaptureGearsetModule.Instance(); + for (var i = 0; i < 100; i++) + { + var gearset = gearsetModule->Gearset[i]; + if (gearset == null) + continue; + if (!gearset->Flags.HasFlag(RaptureGearsetModule.GearsetFlag.Exists)) + continue; + if (gearset->ID != i) + continue; + if (gearset->ClassJob != job.GetClassJobIndex()) + continue; + return i; + } + return null; + } + + public void Dispose() + { + AxisFont?.Dispose(); + } +} diff --git a/Craftimizer/Windows/Settings.cs b/Craftimizer/Windows/Settings.cs index e55ada3..1088b3a 100644 --- a/Craftimizer/Windows/Settings.cs +++ b/Craftimizer/Windows/Settings.cs @@ -473,7 +473,7 @@ public class Settings : Window ImGui.Image(icon.ImGuiHandle, new(icon.Width, icon.Height)); ImGui.TableNextColumn(); - ImGui.Text($"{plugin.Name} v{plugin.Version} {plugin.Configuration}"); + ImGui.Text($"{plugin.Name} v{plugin.Version} {plugin.BuildConfiguration}"); ImGui.Text($"By {plugin.Author} ("); ImGui.SameLine(0, 0); ImGuiUtils.Hyperlink("WorkingRobot", "https://github.com/WorkingRobot"); diff --git a/Craftimizer/Windows/SimulatorDrawer.cs b/Craftimizer/Windows/SimulatorDrawer.cs index 9ad426e..acc0d57 100644 --- a/Craftimizer/Windows/SimulatorDrawer.cs +++ b/Craftimizer/Windows/SimulatorDrawer.cs @@ -133,7 +133,7 @@ public sealed partial class Simulator : Window, IDisposable { var imageSize = new Vector2(ImGui.GetFontSize() * 2.25f); - ImGui.Image(Icons.GetIconFromId(Item.Icon).ImGuiHandle, imageSize); + ImGui.Image(Service.IconManager.GetIcon(Item.Icon).ImGuiHandle, imageSize); ImGui.SameLine(); ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (imageSize.Y - ImGui.GetFontSize()) / 2f); ImGui.TextUnformatted(Item.Name.ToDalamudString().ToString());