From a5d1b1884073fdd890fa3d0e810cb3e8dd22b51c Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Fri, 23 Jun 2023 03:02:33 -0700 Subject: [PATCH] UI changes and stuff --- Craftimizer/LuminaSheets.cs | 2 + Craftimizer/SimulatorUtils.cs | 38 +++++- Craftimizer/SimulatorWindow.cs | 10 +- Craftimizer/Utils/Chat.cs | 169 +++++++++++++++++++++++ Craftimizer/Utils/Gearsets.cs | 191 ++++++++++++++++++++++++++ Craftimizer/Windows/CraftingLog.cs | 207 +++++++---------------------- 6 files changed, 445 insertions(+), 172 deletions(-) create mode 100644 Craftimizer/Utils/Chat.cs create mode 100644 Craftimizer/Utils/Gearsets.cs diff --git a/Craftimizer/LuminaSheets.cs b/Craftimizer/LuminaSheets.cs index f624e90..79ec981 100644 --- a/Craftimizer/LuminaSheets.cs +++ b/Craftimizer/LuminaSheets.cs @@ -1,5 +1,6 @@ using Lumina.Excel.GeneratedSheets; using Lumina.Excel; +using Dalamud; namespace Craftimizer.Plugin; @@ -14,6 +15,7 @@ public static class LuminaSheets public static readonly ExcelSheet AddonSheet = Service.DataManager.GetExcelSheet()!; 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 MateriaSheet = Service.DataManager.GetExcelSheet()!; public static readonly ExcelSheet BaseParamSheet = Service.DataManager.GetExcelSheet()!; } diff --git a/Craftimizer/SimulatorUtils.cs b/Craftimizer/SimulatorUtils.cs index 0548ebf..82cf307 100644 --- a/Craftimizer/SimulatorUtils.cs +++ b/Craftimizer/SimulatorUtils.cs @@ -10,7 +10,6 @@ using ClassJob = Craftimizer.Simulator.ClassJob; using Condition = Craftimizer.Simulator.Condition; using Craftimizer.Simulator; using System.Text; -using System.Runtime.CompilerServices; using System.Numerics; namespace Craftimizer.Plugin; @@ -48,6 +47,16 @@ internal static class ActionUtils return (null, null); } + public static uint GetId(this ActionType me, ClassJob classJob) + { + var (craftAction, action) = GetActionRow(me, classJob); + if (craftAction != null) + return craftAction.RowId; + if (action != null) + return action.RowId; + return 0; + } + public static string GetName(this ActionType me, ClassJob classJob) { var (craftAction, action) = GetActionRow(me, classJob); @@ -70,8 +79,31 @@ internal static class ActionUtils } } -internal static class ClassJobExtensions +internal static class ClassJobUtils { + public static byte GetClassJobIndex(this ClassJob me) => + me switch + { + ClassJob.Carpenter => 8, + ClassJob.Blacksmith => 9, + ClassJob.Armorer => 10, + ClassJob.Goldsmith => 11, + ClassJob.Leatherworker => 12, + ClassJob.Weaver => 13, + ClassJob.Alchemist => 14, + ClassJob.Culinarian => 15, + _ => 0 + }; + + // Index in the actual ClassJob sheet + public static bool IsClassJob(byte classJobIdx, ClassJob classJob) + { + var job = LuminaSheets.ClassJobSheet.GetRow(classJobIdx)!; + if (job.ClassJobCategory.Row != 33) // DoH + return false; + return (ClassJob)job.DohDolJobIndex == classJob; + } + public static bool IsClassJob(this ClassJobCategory me, ClassJob classJob) => classJob switch { @@ -134,7 +166,7 @@ internal static class ConditionUtils } } -internal static class EffectExtensions +internal static class EffectUtils { public static uint StatusId(this EffectType me) => me switch diff --git a/Craftimizer/SimulatorWindow.cs b/Craftimizer/SimulatorWindow.cs index 4530096..96ea2ce 100644 --- a/Craftimizer/SimulatorWindow.cs +++ b/Craftimizer/SimulatorWindow.cs @@ -1,3 +1,4 @@ +using Craftimizer.Plugin.Utils; using Craftimizer.Simulator; using Craftimizer.Simulator.Actions; using Dalamud.Interface; @@ -27,14 +28,14 @@ public class SimulatorWindow : Window }; State = new(new( - new CharacterStats { Craftsmanship = 4041, Control = 3905, CP = 609, Level = 90, CLvl = CalculateCLvl(90) }, + new CharacterStats { Craftsmanship = 4041, Control = 3905, CP = 609, Level = 90, CLvl = Gearsets.CalculateCLvl(90) }, CreateRecipeInfo(LuminaSheets.RecipeSheet.GetRow(35499)!), 0 )); Simulation = new(State); } - private static RecipeInfo CreateRecipeInfo(Recipe recipe) + public static RecipeInfo CreateRecipeInfo(Recipe recipe) { var recipeTable = recipe.RecipeLevelTable.Value!; return new() { @@ -52,11 +53,6 @@ public class SimulatorWindow : Window }; } - private static int CalculateCLvl(int characterLevel) => - characterLevel <= 80 - ? LuminaSheets.ParamGrowSheet.GetRow((uint)characterLevel)!.CraftingLevel - : (int)LuminaSheets.RecipeLevelTableSheet.First(r => r.ClassJobLevel == characterLevel).RowId; - public override void Draw() { ImGui.BeginTable("CraftimizerTable", 2, ImGuiTableFlags.Resizable); diff --git a/Craftimizer/Utils/Chat.cs b/Craftimizer/Utils/Chat.cs new file mode 100644 index 0000000..1098841 --- /dev/null +++ b/Craftimizer/Utils/Chat.cs @@ -0,0 +1,169 @@ +using FFXIVClientStructs.FFXIV.Client.System.Framework; +using FFXIVClientStructs.FFXIV.Client.System.Memory; +using FFXIVClientStructs.FFXIV.Client.System.String; +using System; +using System.Runtime.InteropServices; +using System.Text; + +namespace Craftimizer.Plugin.Utils; + +// https://github.com/Caraxi/SimpleTweaksPlugin/blob/0973b93931cdf8a1b01153984d62f76d998747ff/Utility/ChatHelper.cs#L17 +public static class Chat +{ + private static class Signatures + { + internal const string SendChat = "48 89 5C 24 ?? 57 48 83 EC 20 48 8B FA 48 8B D9 45 84 C9"; + internal const string SanitiseString = "E8 ?? ?? ?? ?? EB 0A 48 8D 4C 24 ?? E8 ?? ?? ?? ?? 48 8D 8D"; + } + + private delegate void ProcessChatBoxDelegate(IntPtr uiModule, IntPtr message, IntPtr unused, byte a4); + + private static ProcessChatBoxDelegate? ProcessChatBox { get; } + + private static readonly unsafe delegate* unmanaged _sanitiseString = null!; + + static Chat() + { + if (Service.SigScanner.TryScanText(Signatures.SendChat, out var processChatBoxPtr)) + { + ProcessChatBox = Marshal.GetDelegateForFunctionPointer(processChatBoxPtr); + } + + unsafe + { + if (Service.SigScanner.TryScanText(Signatures.SanitiseString, out var sanitisePtr)) + { + _sanitiseString = (delegate* unmanaged)sanitisePtr; + } + } + } + + /// + /// + /// Send a given message to the chat box. This can send chat to the server. + /// + /// + /// This method is unsafe. This method does no checking on your input and + /// may send content to the server that the normal client could not. You must + /// verify what you're sending and handle content and length to properly use + /// this. + /// + /// + /// Message to send + /// If the signature for this function could not be found + public static unsafe void SendMessageUnsafe(byte[] message) + { + if (ProcessChatBox == null) + { + throw new InvalidOperationException("Could not find signature for chat sending"); + } + + var uiModule = (IntPtr)Framework.Instance()->GetUiModule(); + + using var payload = new ChatPayload(message); + var mem1 = Marshal.AllocHGlobal(400); + Marshal.StructureToPtr(payload, mem1, false); + + ProcessChatBox(uiModule, mem1, IntPtr.Zero, 0); + + Marshal.FreeHGlobal(mem1); + } + + /// + /// + /// Send a given message to the chat box. This can send chat to the server. + /// + /// + /// This method is slightly less unsafe than . It + /// will throw exceptions for certain inputs that the client can't normally send, + /// but it is still possible to make mistakes. Use with caution. + /// + /// + /// message to send + /// If is empty, longer than 500 bytes in UTF-8, or contains invalid characters. + /// If the signature for this function could not be found + public static void SendMessage(string message) + { + var bytes = Encoding.UTF8.GetBytes(message); + if (bytes.Length == 0) + { + throw new ArgumentException("message is empty", nameof(message)); + } + + if (bytes.Length > 500) + { + throw new ArgumentException("message is longer than 500 bytes", nameof(message)); + } + + if (message.Length != SanitiseText(message).Length) + { + throw new ArgumentException("message contained invalid characters", nameof(message)); + } + + SendMessageUnsafe(bytes); + } + + /// + /// + /// Sanitises a string by removing any invalid input. + /// + /// + /// The result of this method is safe to use with + /// , provided that it is not empty or too + /// long. + /// + /// + /// text to sanitise + /// sanitised text + /// If the signature for this function could not be found + public static unsafe string SanitiseText(string text) + { + if (_sanitiseString == null) + { + throw new InvalidOperationException("Could not find signature for chat sanitisation"); + } + + var uText = Utf8String.FromString(text); + + _sanitiseString(uText, 0x27F, IntPtr.Zero); + var sanitised = uText->ToString(); + + uText->Dtor(); + IMemorySpace.Free(uText); + + return sanitised; + } + + [StructLayout(LayoutKind.Explicit)] + private readonly struct ChatPayload : IDisposable + { + [FieldOffset(0)] + private readonly IntPtr textPtr; + + [FieldOffset(16)] + private readonly ulong textLen; + + [FieldOffset(8)] + private readonly ulong unk1; + + [FieldOffset(24)] + private readonly ulong unk2; + + internal ChatPayload(byte[] stringBytes) + { + this.textPtr = Marshal.AllocHGlobal(stringBytes.Length + 30); + Marshal.Copy(stringBytes, 0, this.textPtr, stringBytes.Length); + Marshal.WriteByte(this.textPtr + stringBytes.Length, 0); + + this.textLen = (ulong)(stringBytes.Length + 1); + + this.unk1 = 64; + this.unk2 = 0; + } + + public void Dispose() + { + Marshal.FreeHGlobal(this.textPtr); + } + } +} diff --git a/Craftimizer/Utils/Gearsets.cs b/Craftimizer/Utils/Gearsets.cs new file mode 100644 index 0000000..e9e45bd --- /dev/null +++ b/Craftimizer/Utils/Gearsets.cs @@ -0,0 +1,191 @@ +using Craftimizer.Simulator; +using Dalamud.Utility; +using FFXIVClientStructs.FFXIV.Client.Game; +using FFXIVClientStructs.FFXIV.Client.UI.Misc; +using Lumina.Excel.GeneratedSheets; +using System; +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); + + private const int ParamCP = 11; + private const int ParamCraftsmanship = 70; + private const int ParamControl = 71; + + public static CharacterStats CalculateCharacterStats(InventoryContainer* container, int characterLevel, bool canUseManipulation) + { + 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; + 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; + } + return stats; + } + + private static (int CP, int Craftsmanship, int Control, bool HasSplendorous, bool HasSpecialist) CalculateGearsetStats(RaptureGearsetModule.GearsetEntry* entry) + { + var stats = new[] + { + 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)); + } + + 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) + { + var item = LuminaSheets.ItemSheet.GetRow(itemId)!; + + int cp = 0, craftsmanship = 0, control = 0; + + void IncreaseStat(int baseParam, int amount) + { + if (baseParam == ParamCP) + cp += amount; + else if (baseParam == ParamCraftsmanship) + craftsmanship += amount; + else if (baseParam == ParamControl) + control += amount; + } + + foreach (var statIncrease in item.UnkData59) + IncreaseStat(statIncrease.BaseParam, statIncrease.BaseParamValue); + + if (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]]); + } + + 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)); + } + + // 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) + { + var ilvl = item.LevelItem.Value!; + var param = LuminaSheets.BaseParamSheet.GetRow((uint)paramId)!; + + var baseValue = paramId switch + { + ParamCP => ilvl.CP, + ParamCraftsmanship => ilvl.Craftsmanship, + ParamControl => ilvl.Control, + _ => 0 + }; + // https://github.com/ffxiv-teamcraft/ffxiv-teamcraft/blob/24d0db2d9676f264edf53651b21005305267c84c/apps/data-extraction/src/extractors/items.extractor.ts#L6 + var slotMod = item.EquipSlotCategory.Row switch + { + 1 => param.oneHWpnPct, + 2 => param.OHPct, + 3 => param.HeadPct, + 4 => param.ChestPct, + 5 => param.HandsPct, + 6 => param.WaistPct, + 7 => param.LegsPct, + 8 => param.FeetPct, + 9 => param.EarringPct, + 10 => param.NecklacePct, + 11 => param.BraceletPct, + 12 => param.RingPct, + 13 => param.twoHWpnPct, + 14 => param.oneHWpnPct, + 15 => param.ChestHeadPct, + 16 => param.ChestHeadLegsFeetPct, + 18 => param.LegsFeetPct, + 19 => param.HeadChestHandsLegsFeetPct, + 20 => param.ChestLegsGlovesPct, + 21 => param.ChestLegsFeetPct, + _ => 0 + }; + var roleMod = param.MeldParam[item.BaseParamModifier]; + + // https://github.com/Caraxi/SimpleTweaksPlugin/pull/595 + var cap = (int)Math.Round((float)baseValue * slotMod / (roleMod * 10f), MidpointRounding.AwayFromZero); + 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; +} diff --git a/Craftimizer/Windows/CraftingLog.cs b/Craftimizer/Windows/CraftingLog.cs index c6a2134..66ce4c6 100644 --- a/Craftimizer/Windows/CraftingLog.cs +++ b/Craftimizer/Windows/CraftingLog.cs @@ -1,22 +1,29 @@ +using Craftimizer.Plugin.Utils; +using Craftimizer.Simulator; +using Craftimizer.Simulator.Actions; +using Dalamud.Interface; +using Dalamud.Interface.Components; using Dalamud.Interface.Windowing; 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.Linq; using System.Numerics; using System.Runtime.InteropServices; +using System.Text; +using ActionType = Craftimizer.Simulator.Actions.ActionType; +using ClassJob = Craftimizer.Simulator.ClassJob; namespace Craftimizer.Plugin.Windows; public unsafe class CraftingLog : Window { private const ImGuiWindowFlags WindowFlags = ImGuiWindowFlags.NoDecoration - | ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoNavFocus; @@ -25,6 +32,7 @@ public unsafe class CraftingLog : Window private RecipeNote* State { get; set; } private ushort SelectedRecipeId { get; set; } private Recipe SelectedRecipe { get; set; } = null!; + private RecipeInfo SelectedRecipeInfo { get; set; } = null!; public CraftingLog() : base("RecipeNoteHelper", WindowFlags, true) { @@ -33,180 +41,53 @@ public unsafe class CraftingLog : Window public override void Draw() { - ImGui.Text("Hai!! :3333"); - var inst = RaptureGearsetModule.Instance(); + if (Service.ClientState.LocalPlayer == null) + return; + + var recipeClassJob = (ClassJob)SelectedRecipe.CraftType.Row; + + var characterLevel = PlayerState.Instance()->ClassJobLevelArray[recipeClassJob.GetClassJobIndex()]; + var canUseManipulation = ActionManager.CanUseActionOnTarget(ActionType.Manipulation.GetId(recipeClassJob), (GameObject*)Service.ClientState.LocalPlayer.Address); + for (var i = 0; i < 100; i++) { var gearset = inst->Gearset[i]; if (gearset == null) continue; - if (!gearset->Flags.HasFlag(RaptureGearsetModule.GearsetFlag.Exists)) - continue; if (gearset->ID != i) continue; - var job = LuminaSheets.ClassJobSheet.GetRow(gearset->ClassJob)!; - if (job.ClassJobCategory.Row != 33) // DoH + if (!gearset->Flags.HasFlag(RaptureGearsetModule.GearsetFlag.Exists)) continue; - if (job.DohDolJobIndex != SelectedRecipe.CraftType.Row) + + if (!ClassJobUtils.IsClassJob(gearset->ClassJob, recipeClassJob)) continue; - ImGui.Text($"Supported Gearset: {gearset->ID + 1} {Marshal.PtrToStringUTF8((nint)gearset->Name)}"); - var stats = CalculateGearsetStats(gearset); - ImGui.Text($"{stats.CP} CP, {stats.Craftsmanship} Craftsmanship, {stats.Control} Control"); + + var stats = Gearsets.CalculateCharacterStats(gearset, characterLevel, canUseManipulation); + ImGui.Text($"Gearset: {gearset->ID + 1} {Marshal.PtrToStringUTF8((nint)gearset->Name)}"); + ImGui.SameLine(); + if (ImGuiComponents.IconButton($"SwapGearset{gearset->ID}", FontAwesomeIcon.SyncAlt)) + Chat.SendMessage($"/gearset change {gearset->ID + 1}"); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip($"Swap to gearset {gearset->ID + 1}"); + ImGui.Text($"{stats}"); } - - ShowCurrentGearInfo(); - } - - private void ShowCurrentGearInfo() - { - if (Service.ClientState.LocalPlayer == null) - return; - - var classJob = Service.ClientState.LocalPlayer.ClassJob.Id; - - var job = LuminaSheets.ClassJobSheet.GetRow(classJob)!; - if (job.ClassJobCategory.Row != 33) // DoH - return; - if (job.DohDolJobIndex != SelectedRecipe.CraftType.Row) - return; - - var container = InventoryManager.Instance()->GetInventoryContainer(InventoryType.EquippedItems); - if (container == null) - return; - - (int CP, int Craftsmanship, int Control) stats = (180, 0, 0); - 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; + var classJob = (byte)Service.ClientState.LocalPlayer.ClassJob.Id; + + if (!ClassJobUtils.IsClassJob(classJob, recipeClassJob)) + return; + + var container = InventoryManager.Instance()->GetInventoryContainer(InventoryType.EquippedItems); + if (container == null) + return; + + var stats = Gearsets.CalculateCharacterStats(container, characterLevel, canUseManipulation); + ImGui.Text($"Currently Equipped"); + ImGui.Text($"{stats}"); } - ImGui.Text($"Currently Equipped"); - ImGui.Text($"{stats.CP} CP, {stats.Craftsmanship} Craftsmanship, {stats.Control} Control"); - } - - private static readonly (int CP, int Craftsmanship, int Control) BaseStats = (180, 0, 0); - - private static (int CP, int Craftsmanship, int Control) CalculateGearsetStats(RaptureGearsetModule.GearsetEntry* entry) - { - var stats = new[] - { - 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)); - } - - private const int ParamCP = 11; - private const int ParamCraftsmanship = 70; - private const int ParamControl = 71; - - private static (int CP, int Craftsmanship, int Control) CalculateGearsetItemStats(InventoryItem item) => - CalculateGearsetItemStats(item.ItemID, item.Flags.HasFlag(InventoryItem.ItemFlags.HQ), item.Materia, item.MateriaGrade); - - private static (int CP, int Craftsmanship, int Control) CalculateGearsetItemStats(RaptureGearsetModule.GearsetItem item) => - CalculateGearsetItemStats(item.ItemID % 1000000, item.ItemID > 1000000, item.Materia, item.MateriaGrade); - - private static (int CP, int Craftsmanship, int Control) CalculateGearsetItemStats(uint itemId, bool isHq, ushort* materiaTypes, byte* materiaGrades) - { - var item = LuminaSheets.ItemSheet.GetRow(itemId)!; - - int cp = 0, craftsmanship = 0, control = 0; - - void IncreaseStat(int baseParam, int amount) - { - if (baseParam == ParamCP) - cp += amount; - else if (baseParam == ParamCraftsmanship) - craftsmanship += amount; - else if (baseParam == ParamControl) - control += amount; - } - - foreach(var statIncrease in item.UnkData59) - IncreaseStat(statIncrease.BaseParam, statIncrease.BaseParamValue); - - if (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]]); - } - - cp = Math.Min(cp, CalculateMateriaCap(item, ParamCP)); - craftsmanship = Math.Min(craftsmanship, CalculateMateriaCap(item, ParamCraftsmanship)); - control = Math.Min(control, CalculateMateriaCap(item, ParamControl)); - - return (cp, craftsmanship, control); - } - - // https://github.com/ffxiv-teamcraft/ffxiv-teamcraft/blob/24d0db2d9676f264edf53651b21005305267c84c/apps/client/src/app/modules/gearsets/materia.service.ts#L265 - private static int CalculateMateriaCap(Item item, int paramId) - { - var ilvl = item.LevelItem.Value!; - var param = LuminaSheets.BaseParamSheet.GetRow((uint)paramId)!; - - var baseValue = paramId switch - { - ParamCP => ilvl.CP, - ParamCraftsmanship => ilvl.Craftsmanship, - ParamControl => ilvl.Control, - _ => 0 - }; - // https://github.com/ffxiv-teamcraft/ffxiv-teamcraft/blob/24d0db2d9676f264edf53651b21005305267c84c/apps/data-extraction/src/extractors/items.extractor.ts#L6 - var slotMod = item.EquipSlotCategory.Row switch - { - 1 => param.oneHWpnPct, - 2 => param.OHPct, - 3 => param.HeadPct, - 4 => param.ChestPct, - 5 => param.HandsPct, - 6 => param.WaistPct, - 7 => param.LegsPct, - 8 => param.FeetPct, - 9 => param.EarringPct, - 10 => param.NecklacePct, - 11 => param.BraceletPct, - 12 => param.RingPct, - 13 => param.twoHWpnPct, - 14 => param.oneHWpnPct, - 15 => param.ChestHeadPct, - 16 => param.ChestHeadLegsFeetPct, - 18 => param.LegsFeetPct, - 19 => param.HeadChestHandsLegsFeetPct, - 20 => param.ChestLegsGlovesPct, - 21 => param.ChestLegsFeetPct, - _ => 0 - }; - var roleMod = param.MeldParam[item.BaseParamModifier]; - - // https://github.com/Caraxi/SimpleTweaksPlugin/pull/595 - var cap = (int)Math.Round((float)baseValue * slotMod / (roleMod * 10f), MidpointRounding.AwayFromZero); - return cap == 0 ? int.MaxValue : cap; } public override bool DrawConditions() @@ -240,6 +121,8 @@ public unsafe class CraftingLog : Window SelectedRecipe = recipe; + SelectedRecipeInfo = SimulatorWindow.CreateRecipeInfo(SelectedRecipe); + if (!Addon->Unk258->IsVisible) return false;