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()