diff --git a/Glamourer/Events/EquipmentLoading.cs b/Glamourer/Events/EquipmentLoading.cs deleted file mode 100644 index 93d2656..0000000 --- a/Glamourer/Events/EquipmentLoading.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using Glamourer.Interop.Structs; -using OtterGui.Classes; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; - -namespace Glamourer.Events; - -/// -/// Triggered when a game object updates an equipment piece in its model data. -/// -/// Parameter is the character updating. -/// Parameter is the equipment slot changed. -/// Parameter is the model values to change the equipment piece to. -/// -/// -public sealed class EquipmentLoading : EventWrapper, EquipmentLoading.Priority> -{ - public enum Priority - { - /// - StateListener = 0, - } - - public EquipmentLoading() - : base(nameof(EquipmentLoading)) - { } - - public void Invoke(Actor actor, EquipSlot slot, CharacterArmor armor) - => Invoke(this, actor, slot, armor); -} diff --git a/Glamourer/Events/MovedEquipment.cs b/Glamourer/Events/MovedEquipment.cs new file mode 100644 index 0000000..4548575 --- /dev/null +++ b/Glamourer/Events/MovedEquipment.cs @@ -0,0 +1,28 @@ +using System; +using OtterGui.Classes; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.Events; + +/// +/// Triggered when a game object updates an equipment piece in its model data. +/// +/// Parameter is an array of slots updated and corresponding item ids and stains. +/// +/// +public sealed class MovedEquipment : EventWrapper, MovedEquipment.Priority> +{ + public enum Priority + { + /// + StateListener = 0, + } + + public MovedEquipment() + : base(nameof(MovedEquipment)) + { } + + public void Invoke((EquipSlot, uint, StainId)[] items) + => Invoke(this, items); +} diff --git a/Glamourer/Gui/Customization/CustomizationDrawer.Color.cs b/Glamourer/Gui/Customization/CustomizationDrawer.Color.cs index 200cca6..2d47a34 100644 --- a/Glamourer/Gui/Customization/CustomizationDrawer.Color.cs +++ b/Glamourer/Gui/Customization/CustomizationDrawer.Color.cs @@ -20,8 +20,7 @@ public partial class CustomizationDrawer using (var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 2 * ImGuiHelpers.GlobalScale, current < 0)) { - // Print 1-based index instead of 0. - if (ImGui.ColorButton($"{current + 1}##color", color, ImGuiColorEditFlags.None, _framedIconSize)) + if (ImGui.ColorButton($"{current}##color", color, ImGuiColorEditFlags.None, _framedIconSize)) ImGui.OpenPopup(ColorPickerPopupName); } @@ -65,7 +64,7 @@ public partial class CustomizationDrawer for (var i = 0; i < _currentCount; ++i) { var custom = _set.Data(_currentIndex, i, _customize[CustomizeIndex.Face]); - if (ImGui.ColorButton((i + 1).ToString(), ImGui.ColorConvertU32ToFloat4(custom.Color))) + if (ImGui.ColorButton(i.ToString(), ImGui.ColorConvertU32ToFloat4(custom.Color))) { UpdateValue(custom.Value); ImGui.CloseCurrentPopup(); diff --git a/Glamourer/Gui/Equipment/EquipmentDrawer.cs b/Glamourer/Gui/Equipment/EquipmentDrawer.cs index 92e2c8f..1f8034a 100644 --- a/Glamourer/Gui/Equipment/EquipmentDrawer.cs +++ b/Glamourer/Gui/Equipment/EquipmentDrawer.cs @@ -31,6 +31,9 @@ public class EquipmentDrawer private readonly TextureService _textures; private readonly Configuration _config; + private float _requiredComboWidthUnscaled; + private float _requiredComboWidth; + public EquipmentDrawer(DataManager gameData, ItemManager items, CodeService codes, TextureService textures, Configuration config) { _items = items; @@ -60,6 +63,13 @@ public class EquipmentDrawer { _iconSize = new Vector2(2 * ImGui.GetFrameHeight() + ImGui.GetStyle().ItemSpacing.Y); _comboLength = DefaultWidth * ImGuiHelpers.GlobalScale; + if (_requiredComboWidthUnscaled == 0) + { + _requiredComboWidthUnscaled = _items.ItemService.AwaitedService.AllItems(true).Concat(_items.ItemService.AwaitedService.AllItems(false)) + .Max(i => ImGui.CalcTextSize($"{i.Item2.Name} ({i.Item2.ModelString})").X) / ImGuiHelpers.GlobalScale; + } + + _requiredComboWidth = _requiredComboWidthUnscaled * ImGuiHelpers.GlobalScale; } private bool VerifyRestrictedGear(EquipSlot slot, EquipItem gear, Gender gender, Race race) @@ -171,7 +181,7 @@ public class EquipmentDrawer label = combo.Label; using var disabled = ImRaii.Disabled(locked); - if (!combo.Draw(weapon.Name, weapon.ItemId, small ? _comboLength - ImGui.GetFrameHeight() : _comboLength)) + if (!combo.Draw(weapon.Name, weapon.ItemId, small ? _comboLength - ImGui.GetFrameHeight() : _comboLength, _requiredComboWidth)) return false; weapon = combo.CurrentSelection; @@ -189,7 +199,7 @@ public class EquipmentDrawer label = combo.Label; using var disabled = ImRaii.Disabled(locked); - var change = combo.Draw(weapon.Name, weapon.ItemId, small ? _comboLength - ImGui.GetFrameHeight() : _comboLength); + var change = combo.Draw(weapon.Name, weapon.ItemId, small ? _comboLength - ImGui.GetFrameHeight() : _comboLength, _requiredComboWidth); if (change) weapon = combo.CurrentSelection; @@ -225,7 +235,7 @@ public class EquipmentDrawer label = combo.Label; armor = current; using var disabled = ImRaii.Disabled(locked); - var change = combo.Draw(armor.Name, armor.ItemId, small ? _comboLength - ImGui.GetFrameHeight() : _comboLength); + var change = combo.Draw(armor.Name, armor.ItemId, small ? _comboLength - ImGui.GetFrameHeight() : _comboLength, _requiredComboWidth); if (change) armor = combo.CurrentSelection; diff --git a/Glamourer/Gui/Equipment/ItemCombo.cs b/Glamourer/Gui/Equipment/ItemCombo.cs index 9052894..3ced7c4 100644 --- a/Glamourer/Gui/Equipment/ItemCombo.cs +++ b/Glamourer/Gui/Equipment/ItemCombo.cs @@ -19,6 +19,7 @@ public sealed class ItemCombo : FilterComboCache public readonly string Label; private uint _currentItem; + private float _innerWidth; public ItemCombo(DataManager gameData, ItemManager items, EquipSlot slot, TextureService textures) : base(() => GetItems(items, slot)) @@ -46,12 +47,16 @@ public sealed class ItemCombo : FilterComboCache return base.UpdateCurrentSelected(CurrentSelectionIdx); } - public bool Draw(string previewName, uint previewIdx, float width) + public bool Draw(string previewName, uint previewIdx, float width, float innerWidth) { + _innerWidth = innerWidth; _currentItem = previewIdx; return Draw($"##{Label}", previewName, string.Empty, width, ImGui.GetTextLineHeightWithSpacing()); } + protected override float GetFilterWidth() + => _innerWidth - 2 * ImGui.GetStyle().FramePadding.X; + protected override bool DrawSelectable(int globalIdx, bool selected) { var obj = Items[globalIdx]; diff --git a/Glamourer/Gui/Equipment/WeaponCombo.cs b/Glamourer/Gui/Equipment/WeaponCombo.cs index 5d6085a..75d2adb 100644 --- a/Glamourer/Gui/Equipment/WeaponCombo.cs +++ b/Glamourer/Gui/Equipment/WeaponCombo.cs @@ -16,6 +16,7 @@ public sealed class WeaponCombo : FilterComboCache { public readonly string Label; private uint _currentItemId; + private float _innerWidth; public WeaponCombo(ItemManager items, FullEquipType type) : base(() => GetWeapons(items, type)) @@ -41,9 +42,13 @@ public sealed class WeaponCombo : FilterComboCache return base.UpdateCurrentSelected(CurrentSelectionIdx); } - public bool Draw(string previewName, uint previewId, float width) + protected override float GetFilterWidth() + => _innerWidth - 2 * ImGui.GetStyle().FramePadding.X; + + public bool Draw(string previewName, uint previewId, float width, float innerWidth) { _currentItemId = previewId; + _innerWidth = innerWidth; return Draw($"##{Label}", previewName, string.Empty, width, ImGui.GetTextLineHeightWithSpacing()); } diff --git a/Glamourer/Gui/PenumbraChangedItemTooltip.cs b/Glamourer/Gui/PenumbraChangedItemTooltip.cs index fe93876..62e9ba2 100644 --- a/Glamourer/Gui/PenumbraChangedItemTooltip.cs +++ b/Glamourer/Gui/PenumbraChangedItemTooltip.cs @@ -162,7 +162,7 @@ public class PenumbraChangedItemTooltip : IDisposable switch (type) { case ChangedItemType.Item: - if (!_items.ItemService.AwaitedService.TryGetValue(id, out var item)) + if (!_items.ItemService.AwaitedService.TryGetValue(id, EquipSlot.MainHand, out var item)) return; CreateTooltip(item, "[Glamourer] ", false); @@ -192,7 +192,7 @@ public class PenumbraChangedItemTooltip : IDisposable if (!Player(out var state)) return; - if (!_items.ItemService.AwaitedService.TryGetValue(id, out var item)) + if (!_items.ItemService.AwaitedService.TryGetValue(id, EquipSlot.MainHand, out var item)) return; ApplyItem(state, item); diff --git a/Glamourer/Gui/Tabs/DebugTab.cs b/Glamourer/Gui/Tabs/DebugTab.cs index 718271f..8392b57 100644 --- a/Glamourer/Gui/Tabs/DebugTab.cs +++ b/Glamourer/Gui/Tabs/DebugTab.cs @@ -9,6 +9,7 @@ using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Interface; using Dalamud.Plugin; +using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Glamourer.Api; using Glamourer.Automation; @@ -42,6 +43,7 @@ public unsafe class DebugTab : ITab private readonly UpdateSlotService _updateSlotService; private readonly WeaponService _weaponService; private readonly MetaService _metaService; + private readonly InventoryService _inventoryService; private readonly PenumbraService _penumbra; private readonly ObjectTable _objects; private readonly ObjectManager _objectManager; @@ -76,7 +78,7 @@ public unsafe class DebugTab : ITab DesignFileSystem designFileSystem, DesignManager designManager, StateManager state, Configuration config, PenumbraChangedItemTooltip penumbraTooltip, MetaService metaService, GlamourerIpc ipc, DalamudPluginInterface pluginInterface, AutoDesignManager autoDesignManager, JobService jobs, CodeService code, CustomizeUnlockManager customizeUnlocks, - ItemUnlockManager itemUnlocks, DesignConverter designConverter, DatFileService datFileService) + ItemUnlockManager itemUnlocks, DesignConverter designConverter, DatFileService datFileService, InventoryService inventoryService) { _changeCustomizeService = changeCustomizeService; _visorService = visorService; @@ -103,6 +105,7 @@ public unsafe class DebugTab : ITab _itemUnlocks = itemUnlocks; _designConverter = designConverter; _datFileService = datFileService; + _inventoryService = inventoryService; } public ReadOnlySpan Label @@ -120,6 +123,7 @@ public unsafe class DebugTab : ITab DrawDesigns(); DrawState(); DrawAutoDesigns(); + DrawInventory(); DrawUnlocks(); DrawIpc(); } @@ -860,7 +864,7 @@ public unsafe class DebugTab : ITab if (!table) return; - foreach(var (index, value) in set.NpcOptions) + foreach (var (index, value) in set.NpcOptions) { ImGuiUtil.DrawTableColumn(index.ToString()); ImGuiUtil.DrawTableColumn(value.Value.ToString()); @@ -980,6 +984,7 @@ public unsafe class DebugTab : ITab ImGui.SameLine(); } + ImGui.NewLine(); } @@ -996,6 +1001,7 @@ public unsafe class DebugTab : ITab ImGui.SameLine(); } + ImGui.NewLine(); } } @@ -1442,7 +1448,7 @@ public unsafe class DebugTab : ITab var remainder = ImGuiClip.ClippedDraw(_itemUnlocks.Unlocked, skips, t => { ImGuiUtil.DrawTableColumn(t.Key.ToString()); - if (_items.ItemService.AwaitedService.TryGetValue(t.Key, out var equip)) + if (_items.ItemService.AwaitedService.TryGetValue(t.Key, EquipSlot.MainHand, out var equip)) { ImGuiUtil.DrawTableColumn(equip.Name); ImGuiUtil.DrawTableColumn(equip.Type.ToName()); @@ -1489,7 +1495,7 @@ public unsafe class DebugTab : ITab var remainder = ImGuiClip.ClippedDraw(_itemUnlocks.Unlockable, skips, t => { ImGuiUtil.DrawTableColumn(t.Key.ToString()); - if (_items.ItemService.AwaitedService.TryGetValue(t.Key, out var equip)) + if (_items.ItemService.AwaitedService.TryGetValue(t.Key, EquipSlot.MainHand, out var equip)) { ImGuiUtil.DrawTableColumn(equip.Name); ImGuiUtil.DrawTableColumn(equip.Type.ToName()); @@ -1514,6 +1520,50 @@ public unsafe class DebugTab : ITab #endregion + #region Inventory + + private void DrawInventory() + { + if (!ImGui.CollapsingHeader("Inventory")) + return; + + var inventory = InventoryManager.Instance(); + if (inventory == null) + return; + + ImGuiUtil.CopyOnClickSelectable($"0x{(ulong)inventory:X}"); + + var equip = inventory->GetInventoryContainer(InventoryType.EquippedItems); + if (equip == null || equip->Loaded == 0) + return; + + ImGuiUtil.CopyOnClickSelectable($"0x{(ulong)equip:X}"); + + using var table = ImRaii.Table("items", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + for (var i = 0; i < equip->Size; ++i) + { + ImGuiUtil.DrawTableColumn(i.ToString()); + var item = equip->GetInventorySlot(i); + if (item == null) + { + ImGuiUtil.DrawTableColumn("NULL"); + ImGui.TableNextRow(); + } + else + { + ImGuiUtil.DrawTableColumn(item->ItemID.ToString()); + ImGuiUtil.DrawTableColumn(item->GlamourID.ToString()); + ImGui.TableNextColumn(); + ImGuiUtil.CopyOnClickSelectable($"0x{(ulong)item:X}"); + } + } + } + + #endregion + #region IPC private string _gameObjectName = string.Empty; diff --git a/Glamourer/Gui/Tabs/UnlocksTab/UnlockOverview.cs b/Glamourer/Gui/Tabs/UnlocksTab/UnlockOverview.cs index 2efa4d8..4a51af8 100644 --- a/Glamourer/Gui/Tabs/UnlocksTab/UnlockOverview.cs +++ b/Glamourer/Gui/Tabs/UnlocksTab/UnlockOverview.cs @@ -179,7 +179,7 @@ public class UnlockOverview ImGui.TextUnformatted($"{item.Type.ToName()} ({slot.ToName()})"); if (item.Type.ValidOffhand().IsOffhandType()) ImGui.TextUnformatted( - $"{item.Weapon()}{(_items.ItemService.AwaitedService.TryGetValue(item.ItemId, false, out var offhand) ? $" | {offhand.Weapon()}" : string.Empty)}"); + $"{item.Weapon()}{(_items.ItemService.AwaitedService.TryGetValue(item.ItemId, EquipSlot.OffHand, out var offhand) ? $" | {offhand.Weapon()}" : string.Empty)}"); else ImGui.TextUnformatted(slot is EquipSlot.MainHand ? $"{item.Weapon()}" : $"{item.Armor()}"); ImGui.TextUnformatted( diff --git a/Glamourer/Gui/Tabs/UnlocksTab/UnlockTable.cs b/Glamourer/Gui/Tabs/UnlocksTab/UnlockTable.cs index e0a6f11..8473ce7 100644 --- a/Glamourer/Gui/Tabs/UnlocksTab/UnlockTable.cs +++ b/Glamourer/Gui/Tabs/UnlocksTab/UnlockTable.cs @@ -242,7 +242,7 @@ public class UnlockTable : Table, IDisposable ImGuiUtil.RightAlign(item.ModelString); if (ImGui.IsItemHovered() && item.Type.ValidOffhand().IsOffhandType() - && _items.ItemService.AwaitedService.TryGetValue(item.ItemId, false, out var offhand)) + && _items.ItemService.AwaitedService.TryGetValue(item.ItemId, EquipSlot.OffHand, out var offhand)) { using var tt = ImRaii.Tooltip(); ImGui.TextUnformatted("Offhand: " + offhand.ModelString); @@ -260,7 +260,7 @@ public class UnlockTable : Table, IDisposable if (FilterRegex?.IsMatch(item.ModelString) ?? item.ModelString.Contains(FilterValue, StringComparison.OrdinalIgnoreCase)) return true; - if (item.Type.ValidOffhand().IsOffhandType() && _items.ItemService.AwaitedService.TryGetValue(item.ItemId, false, out var offhand)) + if (item.Type.ValidOffhand().IsOffhandType() && _items.ItemService.AwaitedService.TryGetValue(item.ItemId, EquipSlot.OffHand, out var offhand)) return FilterRegex?.IsMatch(offhand.ModelString) ?? offhand.ModelString.Contains(FilterValue, StringComparison.OrdinalIgnoreCase); diff --git a/Glamourer/Interop/InventoryService.cs b/Glamourer/Interop/InventoryService.cs new file mode 100644 index 0000000..fd56364 --- /dev/null +++ b/Glamourer/Interop/InventoryService.cs @@ -0,0 +1,202 @@ +using System; +using System.Collections.Generic; +using Dalamud.Hooking; +using FFXIVClientStructs.FFXIV.Client.Game; +using FFXIVClientStructs.FFXIV.Client.UI.Misc; +using Glamourer.Events; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.Interop; + +public unsafe class InventoryService : IDisposable +{ + private readonly MovedEquipment _event; + private readonly List<(EquipSlot, uint, StainId)> _itemList = new(12); + + public InventoryService(MovedEquipment @event) + { + _event = @event; + _moveItemHook = Hook.FromAddress((nint)InventoryManager.MemberFunctionPointers.MoveItemSlot, MoveItemDetour); + _equipGearsetHook = + Hook.FromAddress((nint)RaptureGearsetModule.MemberFunctionPointers.EquipGearset, EquipGearSetDetour); + _moveItemHook.Enable(); + _equipGearsetHook.Enable(); + } + + public void Dispose() + { + _moveItemHook.Dispose(); + _equipGearsetHook.Dispose(); + } + + private delegate int EquipGearsetDelegate(RaptureGearsetModule* module, int gearsetId, byte glamourPlateId); + + private readonly Hook _equipGearsetHook; + + private int EquipGearSetDetour(RaptureGearsetModule* module, int gearsetId, byte glamourPlateId) + { + var ret = _equipGearsetHook.Original(module, gearsetId, glamourPlateId); + Glamourer.Log.Excessive($"[InventoryService] Applied gear set {gearsetId} with glamour plate {glamourPlateId} (Returned {ret})"); + if (ret == 0) + { + var entry = module->GetGearset(gearsetId); + if (entry == null) + return ret; + + if (glamourPlateId == 0) + glamourPlateId = entry->GlamourSetLink; + + _itemList.Clear(); + + + if (glamourPlateId != 0) + { + void Add(EquipSlot slot, uint glamourId, StainId glamourStain, ref RaptureGearsetModule.GearsetItem item) + { + if (item.ItemID == 0) + _itemList.Add((slot, 0, 0)); + else if (glamourId != 0) + _itemList.Add((slot, glamourId, glamourStain)); + else if (item.GlamourId != 0) + _itemList.Add((slot, item.GlamourId, item.Stain)); + else + _itemList.Add((slot, item.ItemID, item.Stain)); + } + + var plate = MirageManager.Instance()->GlamourPlatesSpan[glamourPlateId - 1]; + Add(EquipSlot.MainHand, plate.ItemIds[0], plate.StainIds[0], ref entry->MainHand); + Add(EquipSlot.OffHand, plate.ItemIds[1], plate.StainIds[10], ref entry->OffHand); + Add(EquipSlot.Head, plate.ItemIds[2], plate.StainIds[2], ref entry->Head); + Add(EquipSlot.Body, plate.ItemIds[3], plate.StainIds[3], ref entry->Body); + Add(EquipSlot.Hands, plate.ItemIds[4], plate.StainIds[4], ref entry->Hands); + Add(EquipSlot.Legs, plate.ItemIds[5], plate.StainIds[5], ref entry->Legs); + Add(EquipSlot.Feet, plate.ItemIds[6], plate.StainIds[6], ref entry->Feet); + Add(EquipSlot.Ears, plate.ItemIds[7], plate.StainIds[7], ref entry->Ears); + Add(EquipSlot.Neck, plate.ItemIds[8], plate.StainIds[8], ref entry->Neck); + Add(EquipSlot.Wrists, plate.ItemIds[9], plate.StainIds[9], ref entry->Wrists); + Add(EquipSlot.RFinger, plate.ItemIds[10], plate.StainIds[10], ref entry->RingRight); + Add(EquipSlot.LFinger, plate.ItemIds[11], plate.StainIds[11], ref entry->RightLeft); + } + else + { + void Add(EquipSlot slot, ref RaptureGearsetModule.GearsetItem item) + { + if (item.ItemID == 0) + _itemList.Add((slot, 0, 0)); + else if (item.GlamourId != 0) + _itemList.Add((slot, item.GlamourId, item.Stain)); + else + _itemList.Add((slot, item.ItemID, item.Stain)); + } + + Add(EquipSlot.MainHand, ref entry->MainHand); + Add(EquipSlot.OffHand, ref entry->OffHand); + Add(EquipSlot.Head, ref entry->Head); + Add(EquipSlot.Body, ref entry->Body); + Add(EquipSlot.Hands, ref entry->Hands); + Add(EquipSlot.Legs, ref entry->Legs); + Add(EquipSlot.Feet, ref entry->Feet); + Add(EquipSlot.Ears, ref entry->Ears); + Add(EquipSlot.Neck, ref entry->Neck); + Add(EquipSlot.Wrists, ref entry->Wrists); + Add(EquipSlot.RFinger, ref entry->RingRight); + Add(EquipSlot.LFinger, ref entry->RightLeft); + } + + _event.Invoke(_itemList.ToArray()); + } + + return ret; + } + + private delegate int MoveItemDelegate(InventoryManager* manager, InventoryType sourceContainer, ushort sourceSlot, + InventoryType targetContainer, ushort targetSlot, byte unk); + + private readonly Hook _moveItemHook; + + private int MoveItemDetour(InventoryManager* manager, InventoryType sourceContainer, ushort sourceSlot, + InventoryType targetContainer, ushort targetSlot, byte unk) + { + var ret = _moveItemHook.Original(manager, sourceContainer, sourceSlot, targetContainer, targetSlot, unk); + Glamourer.Log.Excessive($"[InventoryService] Moved {sourceContainer} {sourceSlot} {targetContainer} {targetSlot} (Returned {ret})"); + if (ret == 0) + { + if (InvokeSource(sourceContainer, sourceSlot, out var source)) + if (InvokeTarget(manager, targetContainer, targetSlot, out var target)) + _event.Invoke(new[] + { + source, + target, + }); + else + _event.Invoke(new[] + { + source, + }); + else if (InvokeTarget(manager, targetContainer, targetSlot, out var target)) + _event.Invoke(new[] + { + target, + }); + } + + return ret; + } + + private static bool InvokeSource(InventoryType sourceContainer, uint sourceSlot, out (EquipSlot, uint, StainId) tuple) + { + tuple = default; + if (sourceContainer is not InventoryType.EquippedItems) + return false; + + var slot = GetSlot(sourceSlot); + if (slot is EquipSlot.Unknown) + return false; + + tuple = (slot, 0u, 0); + return true; + } + + private static bool InvokeTarget(InventoryManager* manager, InventoryType targetContainer, uint targetSlot, + out (EquipSlot, uint, StainId) tuple) + { + tuple = default; + if (targetContainer is not InventoryType.EquippedItems) + return false; + + var slot = GetSlot(targetSlot); + if (slot is EquipSlot.Unknown) + return false; + + // Invoked after calling Original, so the item is already moved. + var inventory = manager->GetInventoryContainer(targetContainer); + if (inventory == null || inventory->Loaded == 0 || inventory->Size <= targetSlot) + return false; + + var item = inventory->GetInventorySlot((int)targetSlot); + if (item == null) + return false; + + tuple = (slot, item->GlamourID != 0 ? item->GlamourID : item->ItemID, item->Stain); + return true; + } + + private static EquipSlot GetSlot(uint slot) + => slot switch + { + 0 => EquipSlot.MainHand, + 1 => EquipSlot.OffHand, + 2 => EquipSlot.Head, + 3 => EquipSlot.Body, + 4 => EquipSlot.Hands, + 6 => EquipSlot.Legs, + 7 => EquipSlot.Feet, + 8 => EquipSlot.Ears, + 9 => EquipSlot.Neck, + 10 => EquipSlot.Wrists, + 11 => EquipSlot.RFinger, + 12 => EquipSlot.LFinger, + _ => EquipSlot.Unknown, + }; +} diff --git a/Glamourer/Interop/UpdateSlotService.cs b/Glamourer/Interop/UpdateSlotService.cs index 95cbfaa..916b6fe 100644 --- a/Glamourer/Interop/UpdateSlotService.cs +++ b/Glamourer/Interop/UpdateSlotService.cs @@ -1,7 +1,6 @@ using System; using Dalamud.Hooking; using Dalamud.Utility.Signatures; -using FFXIVClientStructs.FFXIV.Client.Game.Character; using Glamourer.Events; using Glamourer.Interop.Structs; using Penumbra.GameData.Enums; @@ -11,24 +10,18 @@ namespace Glamourer.Interop; public unsafe class UpdateSlotService : IDisposable { - public readonly SlotUpdating SlotUpdatingEvent; - public readonly EquipmentLoading EquipmentLoadingEvent; + public readonly SlotUpdating SlotUpdatingEvent; - public UpdateSlotService(SlotUpdating slotUpdating, EquipmentLoading equipmentLoadingEvent) + public UpdateSlotService(SlotUpdating slotUpdating) { - SlotUpdatingEvent = slotUpdating; - EquipmentLoadingEvent = equipmentLoadingEvent; + SlotUpdatingEvent = slotUpdating; SignatureHelper.Initialise(this); _flagSlotForUpdateHook.Enable(); - _loadEquipmentHook = - Hook.FromAddress((nint) DrawDataContainer.MemberFunctionPointers.LoadEquipment, LoadEquipmentDetour); - _loadEquipmentHook.Enable(); } public void Dispose() { _flagSlotForUpdateHook.Dispose(); - _loadEquipmentHook.Dispose(); } public void UpdateSlot(Model drawObject, EquipSlot slot, CharacterArmor data) @@ -53,10 +46,6 @@ public unsafe class UpdateSlotService : IDisposable [Signature(Sigs.FlagSlotForUpdate, DetourName = nameof(FlagSlotForUpdateDetour))] private readonly Hook _flagSlotForUpdateHook = null!; - private delegate void LoadEquipmentDelegateIntern(DrawDataContainer* drawDataContainer, uint slotIdx, CharacterArmor data, bool force); - - private readonly Hook _loadEquipmentHook = null!; - private ulong FlagSlotForUpdateDetour(nint drawObject, uint slotIdx, CharacterArmor* data) { var slot = slotIdx.ToEquipSlot(); @@ -66,14 +55,6 @@ public unsafe class UpdateSlotService : IDisposable return returnValue == ulong.MaxValue ? _flagSlotForUpdateHook.Original(drawObject, slotIdx, data) : returnValue; } - private void LoadEquipmentDetour(DrawDataContainer* drawDataContainer, uint slotIdx, CharacterArmor data, bool force) - { - var slot = slotIdx.ToEquipSlot(); - EquipmentLoadingEvent.Invoke(drawDataContainer->Parent, slot, data); - Glamourer.Log.Excessive($"[LoadEquipment] Called with 0x{(ulong)drawDataContainer:X} for slot {slot} with {data} ({force})."); - _loadEquipmentHook.Original(drawDataContainer, slotIdx, data, force); - } - private ulong FlagSlotForUpdateInterop(Model drawObject, EquipSlot slot, CharacterArmor armor) => _flagSlotForUpdateHook.Original(drawObject.Address, slot.ToIndex(), &armor); } diff --git a/Glamourer/Interop/WeaponService.cs b/Glamourer/Interop/WeaponService.cs index 9b3f7d8..dec17ea 100644 --- a/Glamourer/Interop/WeaponService.cs +++ b/Glamourer/Interop/WeaponService.cs @@ -4,10 +4,8 @@ using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Game.Character; using Glamourer.Events; using Glamourer.Interop.Structs; -using ImGuiNET; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; -using static FFXIVClientStructs.FFXIV.Client.UI.UIModule; namespace Glamourer.Interop; @@ -75,14 +73,14 @@ public unsafe class WeaponService : IDisposable switch (slot) { case EquipSlot.MainHand: - _loadWeaponHook.Original(&character.AsCharacter->DrawData, 0, weapon.Value, 0, 0, 1, 0); + _loadWeaponHook.Original(&character.AsCharacter->DrawData, 0, weapon.Value, 1, 0, 1, 0); return; case EquipSlot.OffHand: - _loadWeaponHook.Original(&character.AsCharacter->DrawData, 1, weapon.Value, 0, 0, 1, 0); + _loadWeaponHook.Original(&character.AsCharacter->DrawData, 1, weapon.Value, 1, 0, 1, 0); return; case EquipSlot.BothHand: - _loadWeaponHook.Original(&character.AsCharacter->DrawData, 0, weapon.Value, 0, 0, 1, 0); - _loadWeaponHook.Original(&character.AsCharacter->DrawData, 1, CharacterWeapon.Empty.Value, 0, 0, 1, 0); + _loadWeaponHook.Original(&character.AsCharacter->DrawData, 0, weapon.Value, 1, 0, 1, 0); + _loadWeaponHook.Original(&character.AsCharacter->DrawData, 1, CharacterWeapon.Empty.Value, 1, 0, 1, 0); return; // function can also be called with '2', but does not seem to ever be. } diff --git a/Glamourer/Services/ItemManager.cs b/Glamourer/Services/ItemManager.cs index 1ccfcc3..dc2a1bf 100644 --- a/Glamourer/Services/ItemManager.cs +++ b/Glamourer/Services/ItemManager.cs @@ -74,7 +74,7 @@ public class ItemManager : IDisposable if (itemId == SmallclothesId(slot)) return SmallClothesItem(slot); - if (!ItemService.AwaitedService.TryGetValue(itemId, slot is not EquipSlot.OffHand, out var item)) + if (!ItemService.AwaitedService.TryGetValue(itemId, slot, out var item)) return new EquipItem(string.Intern($"Unknown #{itemId}"), itemId, 0, 0, 0, 0, 0); if (item.Type.ToSlot() != slot) @@ -88,7 +88,7 @@ public class ItemManager : IDisposable if (itemId == NothingId(type)) return NothingItem(type); - if (!ItemService.AwaitedService.TryGetValue(itemId, type is FullEquipType.Shield, out var item)) + if (!ItemService.AwaitedService.TryGetValue(itemId, type is FullEquipType.Shield ? EquipSlot.MainHand : EquipSlot.OffHand, out var item)) return new EquipItem(string.Intern($"Unknown #{itemId}"), itemId, 0, 0, 0, 0, 0); if (item.Type != type) diff --git a/Glamourer/Services/ServiceManager.cs b/Glamourer/Services/ServiceManager.cs index 6a49232..2f62981 100644 --- a/Glamourer/Services/ServiceManager.cs +++ b/Glamourer/Services/ServiceManager.cs @@ -61,7 +61,6 @@ public static class ServiceManager private static IServiceCollection AddEvents(this IServiceCollection services) => services.AddSingleton() .AddSingleton() - .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() @@ -69,7 +68,8 @@ public static class ServiceManager .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); private static IServiceCollection AddData(this IServiceCollection services) => services.AddSingleton() @@ -91,7 +91,8 @@ public static class ServiceManager .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); private static IServiceCollection AddDesigns(this IServiceCollection services) => services.AddSingleton() diff --git a/Glamourer/Services/ServiceWrapper.cs b/Glamourer/Services/ServiceWrapper.cs index b8292a2..a0c150d 100644 --- a/Glamourer/Services/ServiceWrapper.cs +++ b/Glamourer/Services/ServiceWrapper.cs @@ -75,8 +75,8 @@ public abstract class AsyncServiceWrapper : IDisposable public sealed class IdentifierService : AsyncServiceWrapper { - public IdentifierService(DalamudPluginInterface pi, DataManager data) - : base(nameof(IdentifierService), () => Penumbra.GameData.GameData.GetIdentifier(pi, data)) + public IdentifierService(DalamudPluginInterface pi, DataManager data, ItemService itemService) + : base(nameof(IdentifierService), () => Penumbra.GameData.GameData.GetIdentifier(pi, data, itemService.AwaitedService)) { } } diff --git a/Glamourer/State/StateListener.cs b/Glamourer/State/StateListener.cs index 0e45427..1694d9f 100644 --- a/Glamourer/State/StateListener.cs +++ b/Glamourer/State/StateListener.cs @@ -2,6 +2,7 @@ using Glamourer.Automation; using Glamourer.Customization; using Glamourer.Events; +using Glamourer.Interop; using Glamourer.Interop.Penumbra; using Glamourer.Interop.Structs; using Glamourer.Services; @@ -22,12 +23,12 @@ public class StateListener : IDisposable { private readonly Configuration _config; private readonly ActorService _actors; + private readonly ObjectManager _objects; private readonly StateManager _manager; private readonly StateApplier _applier; private readonly ItemManager _items; private readonly PenumbraService _penumbra; private readonly SlotUpdating _slotUpdating; - private readonly EquipmentLoading _equipmentLoading; private readonly WeaponLoading _weaponLoading; private readonly HeadGearVisibilityChanged _headGearVisibility; private readonly VisorStateChanged _visorState; @@ -35,9 +36,11 @@ public class StateListener : IDisposable private readonly AutoDesignApplier _autoDesignApplier; private readonly FunModule _funModule; private readonly HumanModelList _humans; + private readonly MovedEquipment _movedEquipment; private ActorIdentifier _creatingIdentifier = ActorIdentifier.Invalid; - private ActorState? _creatingState = null; + private ActorState? _creatingState; + private CharacterWeapon _lastFistOffhand = CharacterWeapon.Empty; public bool Enabled { @@ -48,7 +51,7 @@ public class StateListener : IDisposable public StateListener(StateManager manager, ItemManager items, PenumbraService penumbra, ActorService actors, Configuration config, SlotUpdating slotUpdating, WeaponLoading weaponLoading, VisorStateChanged visorState, WeaponVisibilityChanged weaponVisibility, HeadGearVisibilityChanged headGearVisibility, AutoDesignApplier autoDesignApplier, FunModule funModule, HumanModelList humans, - EquipmentLoading equipmentLoading, StateApplier applier) + StateApplier applier, MovedEquipment movedEquipment, ObjectManager objects) { _manager = manager; _items = items; @@ -63,8 +66,9 @@ public class StateListener : IDisposable _autoDesignApplier = autoDesignApplier; _funModule = funModule; _humans = humans; - _equipmentLoading = equipmentLoading; _applier = applier; + _movedEquipment = movedEquipment; + _objects = objects; if (Enabled) Subscribe(); @@ -168,40 +172,36 @@ public class StateListener : IDisposable (_, armor.Value) = _items.RestrictedGear.ResolveRestricted(armor, slot, customize.Race, customize.Gender); } - /// - /// The game object does not actually invoke changes when the model id is identical, - /// so we need to handle that case too. - /// - private void OnEquipmentLoading(Actor actor, EquipSlot slot, CharacterArmor armor) + private void OnMovedEquipment((EquipSlot, uint, StainId)[] items) { - if (!actor.Model.Valid || armor != actor.GetArmor(slot)) + _objects.Update(); + var (identifier, objects) = _objects.PlayerData; + if (!identifier.IsValid || !_manager.TryGetValue(identifier, out var state)) return; - if (!actor.Identifier(_actors.AwaitedService, out var identifier) - || !_manager.TryGetValue(identifier, out var state) - || !state.BaseData.IsHuman) - return; - - if (state.ModelData.Armor(slot) == armor) - return; - - var setItem = state[slot, false] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc; - var setStain = state[slot, true] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc; - switch (setItem, setStain) + foreach (var (slot, item, stain) in items) { - case (true, true): - _manager.ChangeEquip(state, slot, state.BaseData.Item(slot), state.BaseData.Stain(slot), StateChanged.Source.Manual); - state[slot, false] = StateChanged.Source.Game; - state[slot, true] = StateChanged.Source.Game; - break; - case (true, false): - _manager.ChangeItem(state, slot, state.BaseData.Item(slot), StateChanged.Source.Manual); - state[slot, false] = StateChanged.Source.Game; - break; - case (false, true): - _manager.ChangeStain(state, slot, state.BaseData.Stain(slot), StateChanged.Source.Manual); - state[slot, true] = StateChanged.Source.Game; - break; + var currentItem = state.BaseData.Item(slot); + var model = state.ModelData.Weapon(slot); + var current = currentItem.Weapon(state.BaseData.Stain(slot)); + if (model.Value == current.Value || !_items.ItemService.AwaitedService.TryGetValue(item, EquipSlot.MainHand, out var changedItem)) + continue; + + var changed = changedItem.Weapon(stain); + if (current.Value == changed.Value && state[slot, false] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc) + { + _manager.ChangeItem(state, slot, currentItem, StateChanged.Source.Game); + switch (slot) + { + case EquipSlot.MainHand: + case EquipSlot.OffHand: + _applier.ChangeWeapon(objects, slot, currentItem, stain); + break; + default: + _applier.ChangeArmor(objects, slot, current.ToArmor(), state.ModelData.IsHatVisible()); + break; + } + } } } @@ -212,6 +212,13 @@ public class StateListener : IDisposable /// private void OnWeaponLoading(Actor actor, EquipSlot slot, Ref weapon) { + // Fist weapon gauntlet hack. + if (slot is EquipSlot.OffHand && weapon.Value.Variant == 0 && weapon.Value.Set.Value != 0 && _lastFistOffhand.Set.Value != 0) + { + weapon.Value = _lastFistOffhand; + _lastFistOffhand = CharacterWeapon.Empty; + } + if (!actor.Identifier(_actors.AwaitedService, out var identifier) || !_manager.TryGetValue(identifier, out var state)) return; @@ -229,7 +236,7 @@ public class StateListener : IDisposable else apply = true; - if (state[slot, false] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc) + if (state[slot, true] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc) _manager.ChangeStain(state, slot, state.BaseData.Stain(slot), StateChanged.Source.Game); else apply = true; @@ -249,6 +256,11 @@ public class StateListener : IDisposable else if (actorWeapon.Set.Value != 0) actorWeapon = actorWeapon.With(newWeapon.Stain); } + + // Fist Weapon Offhand hack. + if (slot is EquipSlot.MainHand && weapon.Value.Set.Value is > 1600 and < 1651) + _lastFistOffhand = new CharacterWeapon((SetId)(weapon.Value.Set.Value + 50), weapon.Value.Type, weapon.Value.Variant, + weapon.Value.Stain); } /// Update base data for a single changed equipment slot. @@ -257,7 +269,7 @@ public class StateListener : IDisposable var actorArmor = actor.GetArmor(slot); // The actor armor does not correspond to the model armor, thus the actor is transformed. // This also prevents it from changing values due to hat state. - if (actorArmor.Value != armor.Value) + if (actorArmor.Value != armor.Value && armor.Set.Value != actor.GetOffhand().Set.Value) return UpdateState.Transformed; var baseData = state.BaseData.Armor(slot); @@ -491,7 +503,7 @@ public class StateListener : IDisposable _penumbra.CreatingCharacterBase += OnCreatingCharacterBase; _penumbra.CreatedCharacterBase += OnCreatedCharacterBase; _slotUpdating.Subscribe(OnSlotUpdating, SlotUpdating.Priority.StateListener); - _equipmentLoading.Subscribe(OnEquipmentLoading, EquipmentLoading.Priority.StateListener); + _movedEquipment.Subscribe(OnMovedEquipment, MovedEquipment.Priority.StateListener); _weaponLoading.Subscribe(OnWeaponLoading, WeaponLoading.Priority.StateListener); _visorState.Subscribe(OnVisorChange, VisorStateChanged.Priority.StateListener); _headGearVisibility.Subscribe(OnHeadGearVisibilityChange, HeadGearVisibilityChanged.Priority.StateListener); @@ -503,7 +515,7 @@ public class StateListener : IDisposable _penumbra.CreatingCharacterBase -= OnCreatingCharacterBase; _penumbra.CreatedCharacterBase -= OnCreatedCharacterBase; _slotUpdating.Unsubscribe(OnSlotUpdating); - _equipmentLoading.Unsubscribe(OnEquipmentLoading); + _movedEquipment.Unsubscribe(OnMovedEquipment); _weaponLoading.Unsubscribe(OnWeaponLoading); _visorState.Unsubscribe(OnVisorChange); _headGearVisibility.Unsubscribe(OnHeadGearVisibilityChange); @@ -515,7 +527,7 @@ public class StateListener : IDisposable if (_creatingState == null) return; - _applier.ChangeHatState(new ActorData(gameObject, _creatingIdentifier.ToName()), _creatingState.ModelData.IsHatVisible()); + _applier.ChangeHatState(new ActorData(gameObject, _creatingIdentifier.ToName()), _creatingState.ModelData.IsHatVisible()); _applier.ChangeWeaponState(new ActorData(gameObject, _creatingIdentifier.ToName()), _creatingState.ModelData.IsWeaponVisible()); } } diff --git a/Glamourer/State/StateManager.cs b/Glamourer/State/StateManager.cs index dff7868..bfa3e4c 100644 --- a/Glamourer/State/StateManager.cs +++ b/Glamourer/State/StateManager.cs @@ -169,6 +169,7 @@ public class StateManager : IReadOnlyDictionary main = actor.GetMainhand(); off = actor.GetOffhand(); + FistWeaponHack(ref ret, ref main, ref off); ret.SetVisor(actor.AsCharacter->DrawData.IsVisorToggled); } @@ -190,6 +191,19 @@ public class StateManager : IReadOnlyDictionary return ret; } + /// This is hardcoded in the game. + private void FistWeaponHack(ref DesignData ret, ref CharacterWeapon mainhand, ref CharacterWeapon offhand) + { + if (mainhand.Set.Value is < 1601 or >= 1651) + return; + + var gauntlets = _items.Identify(EquipSlot.Hands, offhand.Set, 0, (byte)offhand.Variant); + offhand.Set = (SetId)(mainhand.Set.Value + 50); + offhand.Variant = mainhand.Variant; + offhand.Type = mainhand.Type; + ret.SetItem(EquipSlot.Hands, gauntlets); + } + #region Change Values /// Turn an actor human. @@ -433,7 +447,8 @@ public class StateManager : IReadOnlyDictionary if (!GetOrCreate(actor, out var state)) return; - ApplyAll(state, !actor.Model.IsHuman || Customize.Compare(actor.Model.GetCustomize(), state.ModelData.Customize).RequiresRedraw(), false); + ApplyAll(state, !actor.Model.IsHuman || Customize.Compare(actor.Model.GetCustomize(), state.ModelData.Customize).RequiresRedraw(), + false); } public void DeleteState(ActorIdentifier identifier) diff --git a/Glamourer/Unlocks/ItemUnlockManager.cs b/Glamourer/Unlocks/ItemUnlockManager.cs index 68acd5b..6e2bed1 100644 --- a/Glamourer/Unlocks/ItemUnlockManager.cs +++ b/Glamourer/Unlocks/ItemUnlockManager.cs @@ -107,7 +107,7 @@ public class ItemUnlockManager : ISavable, IDisposable private bool AddItem(uint itemId, long time) { itemId = HandleHq(itemId); - if (!_items.ItemService.AwaitedService.TryGetValue(itemId, out var equip) || !_unlocked.TryAdd(equip.ItemId, time)) + if (!_items.ItemService.AwaitedService.TryGetValue(itemId, EquipSlot.MainHand, out var equip) || !_unlocked.TryAdd(equip.ItemId, time)) return false; _event.Invoke(ObjectUnlocked.Type.Item, equip.ItemId, DateTimeOffset.FromUnixTimeMilliseconds(time)); @@ -278,7 +278,7 @@ public class ItemUnlockManager : ISavable, IDisposable private void Load() { var version = UnlockDictionaryHelpers.Load(ToFilename(_saveService.FileNames), _unlocked, - id => _items.ItemService.AwaitedService.TryGetValue(id, out _), "item"); + id => _items.ItemService.AwaitedService.TryGetValue(id, EquipSlot.MainHand, out _), "item"); UpdateModels(version); } @@ -291,7 +291,7 @@ public class ItemUnlockManager : ISavable, IDisposable var cabinet = gameData.GetExcelSheet()!; foreach (var row in cabinet) { - if (items.ItemService.AwaitedService.TryGetValue(row.Item.Row, out var item)) + if (items.ItemService.AwaitedService.TryGetValue(row.Item.Row, EquipSlot.MainHand, out var item)) ret.TryAdd(item.ItemId, new UnlockRequirements(row.RowId, 0, 0, 0, UnlockType.Cabinet)); } @@ -299,7 +299,7 @@ public class ItemUnlockManager : ISavable, IDisposable var gilShop = gameData.GetExcelSheet()!; foreach (var row in gilShopItem) { - if (!items.ItemService.AwaitedService.TryGetValue(row.Item.Row, out var item)) + if (!items.ItemService.AwaitedService.TryGetValue(row.Item.Row, EquipSlot.MainHand, out var item)) continue; var quest1 = row.QuestRequired[0].Row; @@ -332,7 +332,7 @@ public class ItemUnlockManager : ISavable, IDisposable foreach (var (item, time) in _unlocked.ToArray()) { - if (!_items.ItemService.AwaitedService.TryGetValue(item, out var equip)) + if (!_items.ItemService.AwaitedService.TryGetValue(item, EquipSlot.MainHand, out var equip)) continue; var ident = _identifier.AwaitedService.Identify(equip.ModelId, equip.WeaponType, equip.Variant, equip.Type.ToSlot());