diff --git a/PlayerTags/GameInterface/ContextMenus/ContextMenu.cs b/PlayerTags/GameInterface/ContextMenus/ContextMenu.cs index 5f602f1..b32bae1 100644 --- a/PlayerTags/GameInterface/ContextMenus/ContextMenu.cs +++ b/PlayerTags/GameInterface/ContextMenus/ContextMenu.cs @@ -287,6 +287,13 @@ namespace PlayerTags.GameInterface.ContextMenus if (m_SelectedOpenSubContextMenuItem != null) { title = m_SelectedOpenSubContextMenuItem.Name.TextValue; + + // Write the custom title + var titleAtkValue = &atkValues[1]; + fixed (byte* TtlePtr = m_SelectedOpenSubContextMenuItem.Name.Encode().NullTerminate()) + { + m_AtkValueSetString(titleAtkValue, TtlePtr); + } } else if (contextMenuReaderWriter.Title != null) { @@ -301,7 +308,7 @@ namespace PlayerTags.GameInterface.ContextMenus } // Get the existing items from the game. - // For inventory sub context menus, we take only the last item -- the return item. + // TODO: For inventory sub context menus, we take only the last item -- the return item. // This is because we're doing a hack to spawn a Second Tier sub context menu and then appropriating it. var contextMenuItems = contextMenuReaderWriter.Read(); if (IsInventoryContext(m_CurrentAgentContextInterface) && m_SelectedOpenSubContextMenuItem != null) @@ -309,6 +316,8 @@ namespace PlayerTags.GameInterface.ContextMenus contextMenuItems = contextMenuItems.TakeLast(1).ToArray(); } + int beforeHashCode = GetContextMenuItemsHashCode(contextMenuItems); + // Raise the event and get the context menu changes. m_CurrentContextMenuOpenedArgs = NotifyContextMenuOpened(addonContextMenu, m_CurrentAgentContextInterface, title, contextMenuOpenedDelegate, contextMenuItems); if (m_CurrentContextMenuOpenedArgs == null) @@ -316,12 +325,20 @@ namespace PlayerTags.GameInterface.ContextMenus return; } - // Write the new changes. - contextMenuReaderWriter.Write(m_CurrentContextMenuOpenedArgs, m_SelectedOpenSubContextMenuItem, m_AtkValueChangeType, m_AtkValueSetString); + int afterHashCode = GetContextMenuItemsHashCode(m_CurrentContextMenuOpenedArgs.Items); - // Update the addon. - atkValueCount = *(&addonContextMenu->AtkValuesCount) = (ushort)contextMenuReaderWriter.AtkValueCount; - atkValues = *(&addonContextMenu->AtkValues) = contextMenuReaderWriter.AtkValues; + PluginLog.Warning($"{beforeHashCode}={afterHashCode}"); + + // Only write to memory if the items were actually changed. + if (beforeHashCode != afterHashCode) + { + // Write the new changes. + contextMenuReaderWriter.Write(m_CurrentContextMenuOpenedArgs.Items, m_AtkValueChangeType, m_AtkValueSetString); + + // Update the addon. + atkValueCount = *(&addonContextMenu->AtkValuesCount) = (ushort)contextMenuReaderWriter.AtkValueCount; + atkValues = *(&addonContextMenu->AtkValues) = contextMenuReaderWriter.AtkValues; + } } private unsafe bool SubContextMenuOpeningDetour(AgentContext* agentContext) @@ -403,6 +420,19 @@ namespace PlayerTags.GameInterface.ContextMenus ContextMenuOpenedImplementation(addonContextMenu, ref atkValueCount, ref atkValues); } + private int GetContextMenuItemsHashCode(IEnumerable contextMenuItems) + { + unchecked + { + int hash = 17; + foreach (var item in contextMenuItems) + { + hash = hash * 23 + item.GetHashCode(); + } + return hash; + } + } + private unsafe ContextMenuOpenedArgs? NotifyContextMenuOpened(AddonContextMenu* addonContextMenu, AgentContextInterface* agentContextInterface, string? title, ContextMenuOpenedDelegate contextMenuOpenedDelegate, IEnumerable initialContextMenuItems) { var parentAddonName = GetParentAddonName(&addonContextMenu->AddonInterface); @@ -455,6 +485,13 @@ namespace PlayerTags.GameInterface.ContextMenus } } + // Temporarily remove the < Return item, for UX we should enforce that it is always last in the list. + var lastContextMenuItem = initialContextMenuItems.LastOrDefault(); + if (lastContextMenuItem is GameContextMenuItem gameContextMenuItem && gameContextMenuItem.SelectedAction == 102) + { + initialContextMenuItems = initialContextMenuItems.SkipLast(1); + } + var contextMenuOpenedArgs = new ContextMenuOpenedArgs((IntPtr)addonContextMenu, (IntPtr)agentContextInterface, parentAddonName, initialContextMenuItems) { Title = title, @@ -472,10 +509,14 @@ namespace PlayerTags.GameInterface.ContextMenus return null; } + // Readd the < Return item + if (lastContextMenuItem is GameContextMenuItem gameContextMenuItem1 && gameContextMenuItem1.SelectedAction == 102) + { + contextMenuOpenedArgs.Items.Add(lastContextMenuItem); + } + foreach (var contextMenuItem in contextMenuOpenedArgs.Items.ToArray()) { - contextMenuItem.Agent = (IntPtr)agentContextInterface; - // TODO: Game doesn't support nested sub context menus, but we might be able to. if (contextMenuItem is OpenSubContextMenuItem && contextMenuOpenedArgs.Title != null) { diff --git a/PlayerTags/GameInterface/ContextMenus/ContextMenuItem.cs b/PlayerTags/GameInterface/ContextMenus/ContextMenuItem.cs index 7258889..779e9ac 100644 --- a/PlayerTags/GameInterface/ContextMenus/ContextMenuItem.cs +++ b/PlayerTags/GameInterface/ContextMenus/ContextMenuItem.cs @@ -1,5 +1,6 @@ using Dalamud.Game.Text.SeStringHandling; using System; +using System.Numerics; namespace PlayerTags.GameInterface.ContextMenus { @@ -23,11 +24,6 @@ namespace PlayerTags.GameInterface.ContextMenus /// public ContextMenuItemIndicator Indicator { get; set; } = ContextMenuItemIndicator.None; - /// - /// The agent of the item. - /// - internal IntPtr Agent { get; set; } - /// /// Initializes a new instance of the class. /// @@ -41,5 +37,17 @@ namespace PlayerTags.GameInterface.ContextMenus { return Name.ToString(); } + + public override int GetHashCode() + { + unchecked + { + int hash = 17; + hash = hash * 23 + new BigInteger(Name.Encode()).GetHashCode(); + hash = hash * 23 + IsEnabled.GetHashCode(); + hash = hash * 23 + ((int)Indicator).GetHashCode(); + return hash; + } + } } } diff --git a/PlayerTags/GameInterface/ContextMenus/ContextMenuReaderWriter.cs b/PlayerTags/GameInterface/ContextMenus/ContextMenuReaderWriter.cs index cace936..cd0b200 100644 --- a/PlayerTags/GameInterface/ContextMenus/ContextMenuReaderWriter.cs +++ b/PlayerTags/GameInterface/ContextMenus/ContextMenuReaderWriter.cs @@ -263,6 +263,8 @@ namespace PlayerTags.GameInterface.ContextMenus public ContextMenuReaderWriter(AgentContextInterface* agentContextInterface, int atkValueCount, AtkValue* atkValues) { + PluginLog.Warning($"{(IntPtr)atkValues:X}"); + m_AgentContextInterface = agentContextInterface; m_AtkValueCount = atkValueCount; m_AtkValues = atkValues; @@ -330,7 +332,6 @@ namespace PlayerTags.GameInterface.ContextMenus var gameContextMenuItem = new GameContextMenuItem(name, action) { - Agent = (IntPtr)m_AgentContextInterface, IsEnabled = isEnabled, Indicator = indicator }; @@ -341,11 +342,11 @@ namespace PlayerTags.GameInterface.ContextMenus return gameContextMenuItems.ToArray(); } - public unsafe void Write(ContextMenuOpenedArgs contextMenuOpenedArgs, ContextMenuItem? selectedContextMenuItem, AtkValueChangeTypeDelegate_Unmanaged atkValueChangeType, AtkValueSetStringDelegate_Unmanaged atkValueSetString, bool allowReallocate = true) + public unsafe void Write(IEnumerable contextMenuItems, AtkValueChangeTypeDelegate_Unmanaged atkValueChangeType, AtkValueSetStringDelegate_Unmanaged atkValueSetString, bool allowReallocate = true) { if (allowReallocate) { - var newAtkValuesCount = FirstContextMenuItemIndex + (contextMenuOpenedArgs.Items.Count() * TotalDesiredAtkValuesPerContextMenuItem); + var newAtkValuesCount = FirstContextMenuItemIndex + (contextMenuItems.Count() * TotalDesiredAtkValuesPerContextMenuItem); // Allocate the new array. We have to do a little dance with the first 8 bytes which represents the array count const int arrayCountSize = 8; @@ -375,20 +376,10 @@ namespace PlayerTags.GameInterface.ContextMenus m_AtkValues = newAtkValues; } - // Set the custom title if appropriate - if (selectedContextMenuItem is OpenSubContextMenuItem) - { - var titleAtkValue = &m_AtkValues[1]; - fixed (byte* TtlePtr = selectedContextMenuItem.Name.Encode().NullTerminate()) - { - atkValueSetString(titleAtkValue, TtlePtr); - } - } - // Set the context menu item count const int contextMenuItemCountAtkValueIndex = 0; var contextMenuItemCountAtkValue = &m_AtkValues[contextMenuItemCountAtkValueIndex]; - contextMenuItemCountAtkValue->UInt = (uint)contextMenuOpenedArgs.Items.Count(); + contextMenuItemCountAtkValue->UInt = (uint)contextMenuItems.Count(); // Clear the previous arrow flags var hasPreviousIndicatorAtkValue = &m_AtkValues[HasPreviousIndicatorFlagsIndex]; @@ -398,9 +389,9 @@ namespace PlayerTags.GameInterface.ContextMenus var hasNextIndiactorFlagsAtkValue = &m_AtkValues[HasNextIndicatorFlagsIndex]; hasNextIndiactorFlagsAtkValue->UInt = 0; - for (int contextMenuItemIndex = 0; contextMenuItemIndex < contextMenuOpenedArgs.Items.Count(); ++contextMenuItemIndex) + for (int contextMenuItemIndex = 0; contextMenuItemIndex < contextMenuItems.Count(); ++contextMenuItemIndex) { - var contextMenuItem = contextMenuOpenedArgs.Items.ElementAt(contextMenuItemIndex); + var contextMenuItem = contextMenuItems.ElementAt(contextMenuItemIndex); var contextMenuItemAtkValueBaseIndex = FirstContextMenuItemIndex + (contextMenuItemIndex * SequentialAtkValuesPerContextMenuItem); @@ -500,7 +491,7 @@ namespace PlayerTags.GameInterface.ContextMenus object? value = null; if (atkValue->Type == FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Int) { - value = $"{atkValue->Int:X}"; + value = atkValue->Int; } else if (atkValue->Type == FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Bool) { diff --git a/PlayerTags/GameInterface/ContextMenus/CustomContextMenuItem.cs b/PlayerTags/GameInterface/ContextMenus/CustomContextMenuItem.cs index b87ec47..f7cf303 100644 --- a/PlayerTags/GameInterface/ContextMenus/CustomContextMenuItem.cs +++ b/PlayerTags/GameInterface/ContextMenus/CustomContextMenuItem.cs @@ -22,5 +22,15 @@ namespace PlayerTags.GameInterface.ContextMenus { ItemSelected = itemSelected; } + + public override int GetHashCode() + { + unchecked + { + int hash = base.GetHashCode(); + hash = hash * 23 + ItemSelected.GetHashCode(); + return hash; + } + } } } \ No newline at end of file diff --git a/PlayerTags/GameInterface/ContextMenus/GameContextMenuItem.cs b/PlayerTags/GameInterface/ContextMenus/GameContextMenuItem.cs index 1f47895..49805d3 100644 --- a/PlayerTags/GameInterface/ContextMenus/GameContextMenuItem.cs +++ b/PlayerTags/GameInterface/ContextMenus/GameContextMenuItem.cs @@ -22,5 +22,15 @@ namespace PlayerTags.GameInterface.ContextMenus { SelectedAction = selectedAction; } + + public override int GetHashCode() + { + unchecked + { + int hash = base.GetHashCode(); + hash = hash * 23 + SelectedAction; + return hash; + } + } } } \ No newline at end of file diff --git a/PlayerTags/GameInterface/ContextMenus/OpenSubContextMenuItem.cs b/PlayerTags/GameInterface/ContextMenus/OpenSubContextMenuItem.cs index 160ad0d..a3549e6 100644 --- a/PlayerTags/GameInterface/ContextMenus/OpenSubContextMenuItem.cs +++ b/PlayerTags/GameInterface/ContextMenus/OpenSubContextMenuItem.cs @@ -23,5 +23,15 @@ namespace PlayerTags.GameInterface.ContextMenus Opened = opened; Indicator = ContextMenuItemIndicator.Next; } + + public override int GetHashCode() + { + unchecked + { + int hash = base.GetHashCode(); + hash = hash * 23 + Opened.GetHashCode(); + return hash; + } + } } } \ No newline at end of file