diff --git a/Glamourer/Configuration.cs b/Glamourer/Configuration.cs index 80632d0..be62f75 100644 --- a/Glamourer/Configuration.cs +++ b/Glamourer/Configuration.cs @@ -27,6 +27,7 @@ public class Configuration : IPluginConfiguration, ISavable public bool HideApplyCheckmarks { get; set; } = false; public bool SmallEquip { get; set; } = false; public bool UnlockedItemMode { get; set; } = false; + public bool EnableGameContextMenu { get; set; } = true; public MainWindow.TabType SelectedTab { get; set; } = MainWindow.TabType.Settings; public DoubleModifier DeleteDesignModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift); diff --git a/Glamourer/Glamourer.csproj b/Glamourer/Glamourer.csproj index f85d6a4..7e7bbce 100644 --- a/Glamourer/Glamourer.csproj +++ b/Glamourer/Glamourer.csproj @@ -88,6 +88,7 @@ + diff --git a/Glamourer/Gui/Tabs/SettingsTab.cs b/Glamourer/Gui/Tabs/SettingsTab.cs index a31a5a4..23af523 100644 --- a/Glamourer/Gui/Tabs/SettingsTab.cs +++ b/Glamourer/Gui/Tabs/SettingsTab.cs @@ -2,6 +2,7 @@ using System.Runtime.CompilerServices; using Dalamud.Interface; using Glamourer.Gui.Tabs.DesignTab; +using Glamourer.Interop; using Glamourer.Interop.Penumbra; using Glamourer.Services; using Glamourer.State; @@ -19,15 +20,17 @@ public class SettingsTab : ITab private readonly StateListener _stateListener; private readonly CodeService _codeService; private readonly PenumbraAutoRedraw _autoRedraw; + private readonly ContextMenuService _contextMenuService; public SettingsTab(Configuration config, DesignFileSystemSelector selector, StateListener stateListener, - CodeService codeService, PenumbraAutoRedraw autoRedraw) + CodeService codeService, PenumbraAutoRedraw autoRedraw, ContextMenuService contextMenuService) { - _config = config; - _selector = selector; - _stateListener = stateListener; - _codeService = codeService; - _autoRedraw = autoRedraw; + _config = config; + _selector = selector; + _stateListener = stateListener; + _codeService = codeService; + _autoRedraw = autoRedraw; + _contextMenuService = contextMenuService; } public ReadOnlySpan Label @@ -59,6 +62,15 @@ public class SettingsTab : ITab Checkbox("Hide Application Checkboxes", "Hide the application checkboxes in the Customization and Equipment panels of the design tab, and only show them under Application Rules.", _config.HideApplyCheckmarks, v => _config.HideApplyCheckmarks = v); + Checkbox("Enable Game Context Menus", "Whether to show a Try On via Glamourer button on context menus for equippable items.", + _config.EnableGameContextMenu, v => + { + _config.EnableGameContextMenu = v; + if (v) + _contextMenuService.Enable(); + else + _contextMenuService.Disable(); + }); if (Widget.DoubleModifierSelector("Design Deletion Modifier", "A modifier you need to hold while clicking the Delete Design button for it to take effect.", 100 * ImGuiHelpers.GlobalScale, _config.DeleteDesignModifier, v => _config.DeleteDesignModifier = v)) diff --git a/Glamourer/Interop/ContextMenuService.cs b/Glamourer/Interop/ContextMenuService.cs new file mode 100644 index 0000000..da4ce48 --- /dev/null +++ b/Glamourer/Interop/ContextMenuService.cs @@ -0,0 +1,158 @@ +using System; +using Dalamud.ContextMenu; +using Dalamud.Game.Gui; +using Dalamud.Game.Text; +using Dalamud.Game.Text.SeStringHandling; +using Glamourer.Events; +using Glamourer.Services; +using Glamourer.State; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.Interop; + +public class ContextMenuService : IDisposable +{ + public const int ItemSearchContextItemId = 0x1738; + public const int ChatLogContextItemId = 0x948; + + private readonly ItemManager _items; + private readonly DalamudContextMenu _contextMenu = new(); + private readonly StateManager _state; + private readonly ObjectManager _objects; + private readonly GameGui _gameGui; + private readonly Configuration _config; + + public ContextMenuService(ItemManager items, StateManager state, ObjectManager objects, GameGui gameGui, Configuration config) + { + _items = items; + _state = state; + _objects = objects; + _gameGui = gameGui; + _config = config; + Enable(); + } + + public void Enable() + { + _contextMenu.OnOpenGameObjectContextMenu += AddGameObjectItem; + _contextMenu.OnOpenInventoryContextMenu += AddInventoryItem; + } + + public void Disable() + { + _contextMenu.OnOpenGameObjectContextMenu -= AddGameObjectItem; + _contextMenu.OnOpenInventoryContextMenu -= AddInventoryItem; + } + + public void Dispose() + { + Disable(); + _contextMenu.Dispose(); + } + + private static readonly SeString TryOnString = new SeStringBuilder().AddUiForeground(SeIconChar.BoxedLetterG.ToIconString(), 541) + .AddText(" Try On").AddUiForegroundOff().BuiltString; + + private void AddInventoryItem(InventoryContextMenuOpenArgs args) + { + var item = CheckInventoryItem(args.ItemId); + if (item != null) + args.AddCustomItem(item); + } + + private InventoryContextMenuItem? CheckInventoryItem(uint itemId) + { + if (itemId > 500000) + itemId -= 500000; + + if (!_items.ItemService.AwaitedService.TryGetValue(itemId, EquipSlot.MainHand, out var item)) + return null; + + return new InventoryContextMenuItem(TryOnString, GetInventoryAction(item)); + } + + + private GameObjectContextMenuItem? CheckGameObjectItem(uint itemId) + { + if (itemId > 500000) + itemId -= 500000; + + if (!_items.ItemService.AwaitedService.TryGetValue(itemId, EquipSlot.MainHand, out var item)) + return null; + + return new GameObjectContextMenuItem(TryOnString, GetGameObjectAction(item)); + } + + private unsafe GameObjectContextMenuItem? CheckGameObjectItem(IntPtr agent, int offset, Func validate) + => agent != IntPtr.Zero && validate(agent) ? CheckGameObjectItem(*(uint*)(agent + offset)) : null; + + private unsafe GameObjectContextMenuItem? CheckGameObjectItem(IntPtr agent, int offset) + => agent != IntPtr.Zero ? CheckGameObjectItem(*(uint*)(agent + offset)) : null; + + private GameObjectContextMenuItem? CheckGameObjectItem(string name, int offset, Func validate) + => CheckGameObjectItem(_gameGui.FindAgentInterface(name), offset, validate); + + private void AddGameObjectItem(GameObjectContextMenuOpenArgs args) + { + var item = args.ParentAddonName switch + { + "ItemSearch" => CheckGameObjectItem(args.Agent, ItemSearchContextItemId), + "ChatLog" => CheckGameObjectItem("ChatLog", ChatLogContextItemId, ValidateChatLogContext), + _ => null, + }; + if (item != null) + args.AddCustomItem(item); + } + + private DalamudContextMenu.InventoryContextMenuItemSelectedDelegate GetInventoryAction(EquipItem item) + { + return _ => + { + var (id, playerData) = _objects.PlayerData; + if (!playerData.Valid) + return; + + if (!_state.GetOrCreate(id, playerData.Objects[0], out var state)) + return; + + var slot = item.Type.ToSlot(); + _state.ChangeEquip(state, slot, item, 0, StateChanged.Source.Manual); + if (item.Type.ValidOffhand().IsOffhandType()) + { + if (item.ModelId.Value is > 1600 and < 1651 + && _items.ItemService.AwaitedService.TryGetValue(item.ItemId, EquipSlot.Hands, out var gauntlets)) + _state.ChangeEquip(state, EquipSlot.Hands, gauntlets, 0, StateChanged.Source.Manual); + if (_items.ItemService.AwaitedService.TryGetValue(item.ItemId, EquipSlot.OffHand, out var offhand)) + _state.ChangeEquip(state, EquipSlot.OffHand, offhand, 0, StateChanged.Source.Manual); + } + }; + } + + private DalamudContextMenu.GameObjectContextMenuItemSelectedDelegate GetGameObjectAction(EquipItem item) + { + return _ => + { + var (id, playerData) = _objects.PlayerData; + if (!playerData.Valid) + return; + + if (!_state.GetOrCreate(id, playerData.Objects[0], out var state)) + return; + + var slot = item.Type.ToSlot(); + _state.ChangeEquip(state, slot, item, 0, StateChanged.Source.Manual); + if (item.Type.ValidOffhand().IsOffhandType()) + { + if (item.ModelId.Value is > 1600 and < 1651 + && _items.ItemService.AwaitedService.TryGetValue(item.ItemId, EquipSlot.Hands, out var gauntlets)) + _state.ChangeEquip(state, EquipSlot.Hands, gauntlets, 0, StateChanged.Source.Manual); + if (_items.ItemService.AwaitedService.TryGetValue(item.ItemId, EquipSlot.OffHand, out var offhand)) + _state.ChangeEquip(state, EquipSlot.OffHand, offhand, 0, StateChanged.Source.Manual); + } + }; + } + + private static unsafe bool ValidateChatLogContext(nint agent) + => *(uint*)(agent + ChatLogContextItemId + 8) == 3; +} diff --git a/Glamourer/Services/ServiceManager.cs b/Glamourer/Services/ServiceManager.cs index 2f62981..af1e108 100644 --- a/Glamourer/Services/ServiceManager.cs +++ b/Glamourer/Services/ServiceManager.cs @@ -92,7 +92,8 @@ public static class ServiceManager .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); private static IServiceCollection AddDesigns(this IServiceCollection services) => services.AddSingleton()