Merge branch 'new-context-menu-integration'

This commit is contained in:
2022-06-17 12:45:03 +02:00
26 changed files with 240 additions and 1912 deletions

View File

@@ -41,6 +41,8 @@ namespace PlayerTags.Configuration
return;
}
ImGui.SetNextWindowSize(new Vector2(400, 500), ImGuiCond.FirstUseEver);
if (ImGui.Begin(Strings.Loc_Static_PluginName, ref m_PluginConfiguration.IsVisible))
{
ImGui.PushStyleColor(ImGuiCol.Text, new Vector4(1, 0.8f, 0.5f, 1));
@@ -53,9 +55,9 @@ namespace PlayerTags.Configuration
{
if (ImGui.BeginTabItem(Strings.Loc_Static_General))
{
//ImGui.Spacing();
//ImGui.Spacing();
//DrawCheckbox(nameof(m_PluginConfiguration.IsCustomTagsContextMenuEnabled), true, ref m_PluginConfiguration.IsCustomTagsContextMenuEnabled, () => m_PluginConfiguration.Save(m_PluginData));
ImGui.Spacing();
ImGui.Spacing();
DrawCheckbox(nameof(m_PluginConfiguration.IsCustomTagsContextMenuEnabled), true, ref m_PluginConfiguration.IsCustomTagsContextMenuEnabled, () => m_PluginConfiguration.Save(m_PluginData));
ImGui.Spacing();

View File

@@ -1,9 +1,10 @@
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.ContextMenu;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.ClientState.Party;
using Dalamud.Game.Gui.ContextMenus;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Logging;
using PlayerTags.Configuration;
using PlayerTags.GameInterface.ContextMenus;
using PlayerTags.PluginStrings;
using System;
using System.Collections.Generic;
@@ -278,16 +279,15 @@ namespace PlayerTags.Data
};
}
public Identity? GetIdentity(ContextMenuOpenedArgs contextMenuOpenedArgs)
public Identity? GetIdentity(GameObjectContextMenuOpenArgs contextMenuOpenedArgs)
{
if (contextMenuOpenedArgs.GameObjectContext == null
|| contextMenuOpenedArgs.GameObjectContext.Name == null
|| contextMenuOpenedArgs.GameObjectContext.WorldId == null)
if (string.IsNullOrEmpty(contextMenuOpenedArgs.Text?.TextValue)
|| contextMenuOpenedArgs.ObjectWorld == 0
|| contextMenuOpenedArgs.ObjectWorld == 65535)
{
return null;
}
return GetIdentity(contextMenuOpenedArgs.GameObjectContext.Name, contextMenuOpenedArgs.GameObjectContext.WorldId);
return GetIdentity(contextMenuOpenedArgs.Text?.TextValue ?? string.Empty, contextMenuOpenedArgs.ObjectWorld);
}
public Identity GetIdentity(PlayerCharacter playerCharacter)

View File

@@ -1,7 +1,8 @@
using Dalamud.Logging;
using Dalamud.ContextMenu;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Logging;
using PlayerTags.Configuration;
using PlayerTags.Data;
using PlayerTags.GameInterface.ContextMenus;
using PlayerTags.Resources;
using System;
using System.Collections.Generic;
@@ -32,84 +33,69 @@ namespace PlayerTags.Features
private PluginConfiguration m_PluginConfiguration;
private PluginData m_PluginData;
//private ContextMenu? m_ContextMenu;
private DalamudContextMenuBase? m_ContextMenu;
public CustomTagsContextMenuFeature(PluginConfiguration pluginConfiguration, PluginData pluginData)
{
m_PluginConfiguration = pluginConfiguration;
m_PluginData = pluginData;
//m_ContextMenu = new ContextMenu();
//if (!m_ContextMenu.IsValid)
//{
// m_ContextMenu = null;
//}
//if (m_ContextMenu != null)
//{
// m_ContextMenu.ContextMenuOpened += ContextMenuHooks_ContextMenuOpened;
//}
m_ContextMenu = new DalamudContextMenuBase();
m_ContextMenu.Functions.ContextMenu.OnOpenGameObjectContextMenu += ContextMenuHooks_ContextMenuOpened;
}
public void Dispose()
{
//if (m_ContextMenu != null)
//{
// m_ContextMenu.ContextMenuOpened -= ContextMenuHooks_ContextMenuOpened;
// m_ContextMenu.Dispose();
// m_ContextMenu = null;
//}
if (m_ContextMenu != null)
{
m_ContextMenu.Functions.ContextMenu.OnOpenGameObjectContextMenu -= ContextMenuHooks_ContextMenuOpened;
((IDisposable)m_ContextMenu).Dispose();
m_ContextMenu = null;
}
}
//private void ContextMenuHooks_ContextMenuOpened(ContextMenuOpenedArgs contextMenuOpenedArgs)
//{
// if (!m_PluginConfiguration.IsCustomTagsContextMenuEnabled
// || !SupportedAddonNames.Contains(contextMenuOpenedArgs.ParentAddonName))
// {
// return;
// }
private void ContextMenuHooks_ContextMenuOpened(GameObjectContextMenuOpenArgs contextMenuOpenedArgs)
{
if (!m_PluginConfiguration.IsCustomTagsContextMenuEnabled
|| !SupportedAddonNames.Contains(contextMenuOpenedArgs.ParentAddonName))
{
return;
}
// Identity? identity = m_PluginData.GetIdentity(contextMenuOpenedArgs);
// if (identity != null)
// {
// var notAddedTags = m_PluginData.CustomTags.Where(customTag => !identity.CustomTagIds.Contains(customTag.CustomId.Value));
// if (notAddedTags.Any())
// {
// contextMenuOpenedArgs.Items.Add(new OpenSubContextMenuItem(Strings.Loc_Static_ContextMenu_AddTag, (subContextMenuOpenedArgs =>
// {
// List<ContextMenuItem> newContextMenuItems = new List<ContextMenuItem>();
// foreach (var notAddedTag in notAddedTags)
// {
// newContextMenuItems.Add(new CustomContextMenuItem(notAddedTag.Text.Value, (args =>
// {
// m_PluginData.AddCustomTagToIdentity(notAddedTag, identity);
// m_PluginConfiguration.Save(m_PluginData);
// })));
// }
// subContextMenuOpenedArgs.Items.InsertRange(0, newContextMenuItems);
// })));
// }
Identity? identity = m_PluginData.GetIdentity(contextMenuOpenedArgs);
if (identity != null)
{
var allTags = new Dictionary<Tag, bool>();
foreach (var customTag in m_PluginData.CustomTags)
{
var isAdded = identity.CustomTagIds.Contains(customTag.CustomId.Value);
allTags.Add(customTag, isAdded);
}
var sortedTags = allTags.OrderBy(n => n.Value);
foreach (var tag in sortedTags)
{
string menuItemText;
if (tag.Value)
menuItemText = Strings.Loc_Static_ContextMenu_RemoveTag;
else
menuItemText = Strings.Loc_Static_ContextMenu_AddTag;
menuItemText = string.Format(menuItemText, tag.Key.Text.Value);
// var addedTags = m_PluginData.CustomTags.Where(customTag => identity.CustomTagIds.Contains(customTag.CustomId.Value));
// if (addedTags.Any())
// {
// contextMenuOpenedArgs.Items.Add(new OpenSubContextMenuItem(Strings.Loc_Static_ContextMenu_RemoveTag, (subContextMenuOpenedArgs =>
// {
// List<ContextMenuItem> newContextMenuItems = new List<ContextMenuItem>();
// foreach (var addedTag in addedTags)
// {
// newContextMenuItems.Add(new CustomContextMenuItem(addedTag.Text.Value, (args =>
// {
// m_PluginData.RemoveCustomTagFromIdentity(addedTag, identity);
// m_PluginConfiguration.Save(m_PluginData);
// })));
// }
// subContextMenuOpenedArgs.Items.InsertRange(0, newContextMenuItems);
// })));
// }
// }
//}
contextMenuOpenedArgs.AddCustomItem(
new GameObjectContextMenuItem(menuItemText, openedEventArgs =>
{
if (tag.Value)
m_PluginData.RemoveCustomTagFromIdentity(tag.Key, identity);
else
m_PluginData.AddCustomTagToIdentity(tag.Key, identity);
m_PluginConfiguration.Save(m_PluginData);
})
{
IsSubMenu = false
});
}
}
}
}
}

View File

@@ -1,646 +0,0 @@
using Dalamud.Game;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Hooking;
using Dalamud.Logging;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
namespace PlayerTags.GameInterface.ContextMenus
{
internal unsafe delegate void AtkValueChangeTypeDelegate_Unmanaged(AtkValue* thisPtr, FFXIVClientStructs.FFXIV.Component.GUI.ValueType type);
internal unsafe delegate void AtkValueSetStringDelegate_Unmanaged(AtkValue* thisPtr, byte* bytes);
/// <summary>
/// Provides an interface to modify context menus.
/// </summary>
public class ContextMenu : IDisposable
{
private class PluginAddressResolver : BaseAddressResolver
{
private const string c_AtkValueChangeType = "E8 ?? ?? ?? ?? 45 84 F6 48 8D 4C 24";
public IntPtr? AtkValueChangeTypePtr { get; private set; }
private const string c_AtkValueSetString = "E8 ?? ?? ?? ?? 41 03 ED";
public IntPtr? AtkValueSetStringPtr { get; private set; }
private const string c_GetAddonById = "E8 ?? ?? ?? ?? 8B 6B 20";
public IntPtr? GetAddonByIdPtr { get; private set; }
private const string c_OpenSubContextMenu = "E8 ?? ?? ?? ?? 44 39 A3 ?? ?? ?? ?? 0F 86";
public IntPtr? OpenSubContextMenuPtr { get; private set; }
private const string c_ContextMenuOpening = "E8 ?? ?? ?? ?? 0F B7 C0 48 83 C4 60";
public IntPtr? ContextMenuOpeningPtr { get; private set; }
private const string c_ContextMenuOpened = "48 8B C4 57 41 56 41 57 48 81 EC";
public IntPtr? ContextMenuOpenedPtr { get; private set; }
private const string c_ContextMenuItemSelected = "48 89 5C 24 ?? 55 57 41 56 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 44 24 ?? 80 B9";
public IntPtr? ContextMenuItemSelectedPtr { get; private set; }
private const string c_SubContextMenuOpening = "E8 ?? ?? ?? ?? 44 39 A3 ?? ?? ?? ?? 0F 84";
public IntPtr? SubContextMenuOpeningPtr { get; private set; }
private const string c_SubContextMenuOpened = "48 8B C4 57 41 55 41 56 48 81 EC";
public IntPtr? SubContextMenuOpenedPtr { get; private set; }
protected override void Setup64Bit(SigScanner scanner)
{
if (scanner.TryScanText(c_AtkValueChangeType, out var atkValueChangeTypePtr))
{
AtkValueChangeTypePtr = atkValueChangeTypePtr;
}
if (scanner.TryScanText(c_AtkValueSetString, out var atkValueSetStringPtr))
{
AtkValueSetStringPtr = atkValueSetStringPtr;
}
if (scanner.TryScanText(c_GetAddonById, out var getAddonByIdPtr))
{
GetAddonByIdPtr = getAddonByIdPtr;
}
if (scanner.TryScanText(c_OpenSubContextMenu, out var openSubContextMenuPtr))
{
OpenSubContextMenuPtr = openSubContextMenuPtr;
}
if (scanner.TryScanText(c_ContextMenuOpening, out var someOpenAddonThingPtr))
{
ContextMenuOpeningPtr = someOpenAddonThingPtr;
}
if (scanner.TryScanText(c_ContextMenuOpened, out var contextMenuOpenedPtr))
{
ContextMenuOpenedPtr = contextMenuOpenedPtr;
}
if (scanner.TryScanText(c_ContextMenuItemSelected, out var contextMenuItemSelectedPtr))
{
ContextMenuItemSelectedPtr = contextMenuItemSelectedPtr;
}
if (scanner.TryScanText(c_SubContextMenuOpening, out var subContextMenuOpening))
{
SubContextMenuOpeningPtr = subContextMenuOpening;
}
if (scanner.TryScanText(c_SubContextMenuOpened, out var titleScreenContextMenuOpenedPtr))
{
SubContextMenuOpenedPtr = titleScreenContextMenuOpenedPtr;
}
}
}
private readonly AtkValueChangeTypeDelegate_Unmanaged? m_AtkValueChangeType;
private readonly AtkValueSetStringDelegate_Unmanaged? m_AtkValueSetString;
private unsafe delegate AddonInterface* GetAddonByIdDelegate_Unmanaged(RaptureAtkUnitManager* raptureAtkUnitManager, ushort id);
private readonly GetAddonByIdDelegate_Unmanaged? m_GetAddonById;
private unsafe delegate bool OpenSubContextMenuDelegate_Unmanaged(AgentContext* agentContext);
private readonly OpenSubContextMenuDelegate_Unmanaged? m_OpenSubContextMenu;
private unsafe delegate IntPtr ContextMenuOpeningDelegate_Unmanaged(IntPtr a1, IntPtr a2, IntPtr a3, uint a4, IntPtr a5, AgentContextInterface* agentContextInterface, IntPtr a7, ushort a8);
private Hook<ContextMenuOpeningDelegate_Unmanaged>? m_ContextMenuOpeningHook;
private unsafe delegate bool ContextMenuOpenedDelegate_Unmanaged(AddonContextMenu* addonContextMenu, int menuSize, AtkValue* atkValueArgs);
private Hook<ContextMenuOpenedDelegate_Unmanaged>? m_ContextMenuOpenedHook;
private Hook<ContextMenuOpenedDelegate_Unmanaged>? m_SubContextMenuOpenedHook;
private unsafe delegate bool ContextMenuItemSelectedDelegate_Unmanaged(AddonContextMenu* addonContextMenu, int selectedIndex, byte a3);
private Hook<ContextMenuItemSelectedDelegate_Unmanaged>? m_ContextMenuItemSelectedHook;
private unsafe delegate bool SubContextMenuOpeningDelegate_Unmanaged(AgentContext* agentContext);
private Hook<SubContextMenuOpeningDelegate_Unmanaged>? m_SubContextMenuOpeningHook;
private PluginAddressResolver m_PluginAddressResolver;
private const int MaxContextMenuItemsPerContextMenu = 32;
private unsafe AgentContextInterface* m_CurrentAgentContextInterface;
private IntPtr m_CurrentSubContextMenuTitle;
private OpenSubContextMenuItem? m_SelectedOpenSubContextMenuItem;
private ContextMenuOpenedArgs? m_CurrentContextMenuOpenedArgs;
/// <summary>
/// Occurs when a context menu is opened by the game.
/// </summary>
public event ContextMenuOpenedDelegate? ContextMenuOpened;
/// <summary>
/// Whether the required hooks are in place and this instance is valid.
/// </summary>
public bool IsValid
{
get
{
if (!m_PluginAddressResolver.AtkValueChangeTypePtr.HasValue
|| !m_PluginAddressResolver.AtkValueSetStringPtr.HasValue
|| !m_PluginAddressResolver.GetAddonByIdPtr.HasValue
|| !m_PluginAddressResolver.OpenSubContextMenuPtr.HasValue
|| !m_PluginAddressResolver.ContextMenuOpeningPtr.HasValue
|| !m_PluginAddressResolver.ContextMenuOpenedPtr.HasValue
|| !m_PluginAddressResolver.ContextMenuItemSelectedPtr.HasValue
|| !m_PluginAddressResolver.SubContextMenuOpeningPtr.HasValue
|| !m_PluginAddressResolver.SubContextMenuOpenedPtr.HasValue
)
{
return false;
}
return true;
}
}
public ContextMenu()
{
m_PluginAddressResolver = new PluginAddressResolver();
m_PluginAddressResolver.Setup();
if (!IsValid)
{
return;
}
if (m_PluginAddressResolver.AtkValueChangeTypePtr.HasValue)
{
m_AtkValueChangeType = Marshal.GetDelegateForFunctionPointer<AtkValueChangeTypeDelegate_Unmanaged>(m_PluginAddressResolver.AtkValueChangeTypePtr.Value);
}
if (m_PluginAddressResolver.AtkValueSetStringPtr.HasValue)
{
m_AtkValueSetString = Marshal.GetDelegateForFunctionPointer<AtkValueSetStringDelegate_Unmanaged>(m_PluginAddressResolver.AtkValueSetStringPtr.Value);
}
if (m_PluginAddressResolver.GetAddonByIdPtr.HasValue)
{
m_GetAddonById = Marshal.GetDelegateForFunctionPointer<GetAddonByIdDelegate_Unmanaged>(m_PluginAddressResolver.GetAddonByIdPtr.Value);
}
if (m_PluginAddressResolver.OpenSubContextMenuPtr.HasValue)
{
m_OpenSubContextMenu = Marshal.GetDelegateForFunctionPointer<OpenSubContextMenuDelegate_Unmanaged>(m_PluginAddressResolver.OpenSubContextMenuPtr.Value);
}
if (m_PluginAddressResolver.ContextMenuOpeningPtr.HasValue)
{
unsafe
{
m_ContextMenuOpeningHook = new Hook<ContextMenuOpeningDelegate_Unmanaged>(m_PluginAddressResolver.ContextMenuOpeningPtr.Value, ContextMenuOpeningDetour);
}
m_ContextMenuOpeningHook?.Enable();
}
if (m_PluginAddressResolver.ContextMenuOpenedPtr.HasValue)
{
unsafe
{
m_ContextMenuOpenedHook = new Hook<ContextMenuOpenedDelegate_Unmanaged>(m_PluginAddressResolver.ContextMenuOpenedPtr.Value, ContextMenuOpenedDetour);
}
m_ContextMenuOpenedHook?.Enable();
}
if (m_PluginAddressResolver.ContextMenuItemSelectedPtr.HasValue)
{
unsafe
{
m_ContextMenuItemSelectedHook = new Hook<ContextMenuItemSelectedDelegate_Unmanaged>(m_PluginAddressResolver.ContextMenuItemSelectedPtr.Value, ContextMenuItemSelectedDetour);
}
m_ContextMenuItemSelectedHook?.Enable();
}
if (m_PluginAddressResolver.SubContextMenuOpeningPtr.HasValue)
{
unsafe
{
m_SubContextMenuOpeningHook = new Hook<SubContextMenuOpeningDelegate_Unmanaged>(m_PluginAddressResolver.SubContextMenuOpeningPtr.Value, SubContextMenuOpeningDetour);
}
m_SubContextMenuOpeningHook?.Enable();
}
if (m_PluginAddressResolver.SubContextMenuOpenedPtr.HasValue)
{
unsafe
{
m_SubContextMenuOpenedHook = new Hook<ContextMenuOpenedDelegate_Unmanaged>(m_PluginAddressResolver.SubContextMenuOpenedPtr.Value, SubContextMenuOpenedDetour);
}
m_SubContextMenuOpenedHook?.Enable();
}
}
public void Dispose()
{
m_SubContextMenuOpeningHook?.Disable();
m_ContextMenuItemSelectedHook?.Disable();
m_SubContextMenuOpenedHook?.Disable();
m_ContextMenuOpenedHook?.Disable();
m_ContextMenuOpeningHook?.Disable();
}
private unsafe IntPtr ContextMenuOpeningDetour(IntPtr a1, IntPtr a2, IntPtr a3, uint a4, IntPtr a5, AgentContextInterface* agentContextInterface, IntPtr a7, ushort a8)
{
m_CurrentAgentContextInterface = agentContextInterface;
return m_ContextMenuOpeningHook!.Original(a1, a2, a3, a4, a5, agentContextInterface, a7, a8);
}
private unsafe bool ContextMenuOpenedDetour(AddonContextMenu* addonContextMenu, int atkValueCount, AtkValue* atkValues)
{
if (m_ContextMenuOpenedHook == null)
{
return false;
}
try
{
ContextMenuOpenedImplementation(addonContextMenu, ref atkValueCount, ref atkValues);
}
catch (Exception ex)
{
PluginLog.Error(ex, "ContextMenuOpenedDetour");
}
return m_ContextMenuOpenedHook.Original(addonContextMenu, atkValueCount, atkValues);
}
private unsafe void ContextMenuOpenedImplementation(AddonContextMenu* addonContextMenu, ref int atkValueCount, ref AtkValue* atkValues)
{
if (m_AtkValueChangeType == null
|| m_AtkValueSetString == null
|| ContextMenuOpened == null
|| m_CurrentAgentContextInterface == null)
{
return;
}
ContextMenuReaderWriter contextMenuReaderWriter = new ContextMenuReaderWriter(m_CurrentAgentContextInterface, atkValueCount, atkValues);
// Check for a title.
string? title = null;
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)
{
title = contextMenuReaderWriter.Title.TextValue;
}
// Determine which event to raise.
ContextMenuOpenedDelegate contextMenuOpenedDelegate = ContextMenuOpened;
if (m_SelectedOpenSubContextMenuItem is OpenSubContextMenuItem openSubContextMenuItem)
{
contextMenuOpenedDelegate = openSubContextMenuItem.Opened;
}
// Get the existing items from the game.
// 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)
{
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)
{
return;
}
int afterHashCode = GetContextMenuItemsHashCode(m_CurrentContextMenuOpenedArgs.Items);
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)
{
if (m_SubContextMenuOpeningHook == null)
{
return false;
}
if (SubContextMenuOpeningImplementation(agentContext))
{
return true;
}
return m_SubContextMenuOpeningHook.Original(agentContext);
}
private unsafe bool SubContextMenuOpeningImplementation(AgentContext* agentContext)
{
if (m_OpenSubContextMenu == null
|| m_AtkValueChangeType == null
|| m_AtkValueSetString == null
|| !(m_SelectedOpenSubContextMenuItem is OpenSubContextMenuItem))
{
return false;
}
// The important things to make this work are:
// 1. Allocate a temporary sub context menu title. The value doesn't matter, we'll set it later.
// 2. Context menu item count must equal 1 to tell the game there is enough space for the "< Return" item.
// 3. Atk value count must equal the index of the first context menu item.
// This is enough to keep the base data, but excludes the context menu item data.
// We want to exclude context menu item data in this function because the game sometimes includes garbage items which can cause problems.
// After this function, the game adds the "< Return" item, and THEN we add our own items after that.
m_OpenSubContextMenu(agentContext);
// Allocate a new 1 byte title. This is required for the game to render the titled context menu style.
// The actual value doesn't matter at this point, we'll set it later.
GameInterfaceHelper.GameFree(ref m_CurrentSubContextMenuTitle, (ulong)IntPtr.Size);
m_CurrentSubContextMenuTitle = GameInterfaceHelper.GameUIAllocate(1);
*(&(&agentContext->AgentContextInterface)->SubContextMenuTitle) = (byte*)m_CurrentSubContextMenuTitle;
*(byte*)m_CurrentSubContextMenuTitle = 0;
// Expect at least 1 context menu item.
(&agentContext->Items->AtkValues)[0].UInt = 1;
// Expect a title. This isn't needed by the game, it's needed by ContextMenuReaderWriter which uses this to check if it's a context menu
m_AtkValueChangeType(&(&agentContext->Items->AtkValues)[1], FFXIVClientStructs.FFXIV.Component.GUI.ValueType.String);
(&agentContext->Items->AtkValues)[1].String = (byte*)0;
ContextMenuReaderWriter contextMenuReaderWriter = new ContextMenuReaderWriter(&agentContext->AgentContextInterface, agentContext->Items->AtkValueCount, &agentContext->Items->AtkValues);
*(&agentContext->Items->AtkValueCount) = (ushort)contextMenuReaderWriter.FirstContextMenuItemIndex;
return true;
}
private unsafe bool SubContextMenuOpenedDetour(AddonContextMenu* addonContextMenu, int atkValueCount, AtkValue* atkValues)
{
if (m_SubContextMenuOpenedHook == null)
{
return false;
}
try
{
SubContextMenuOpenedImplementation(addonContextMenu, ref atkValueCount, ref atkValues);
}
catch (Exception ex)
{
PluginLog.Error(ex, "SubContextMenuOpenedDetour");
}
return m_SubContextMenuOpenedHook.Original(addonContextMenu, atkValueCount, atkValues);
}
private unsafe void SubContextMenuOpenedImplementation(AddonContextMenu* addonContextMenu, ref int atkValueCount, ref AtkValue* atkValues)
{
ContextMenuOpenedImplementation(addonContextMenu, ref atkValueCount, ref atkValues);
}
private int GetContextMenuItemsHashCode(IEnumerable<ContextMenuItem> 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<ContextMenuItem> initialContextMenuItems)
{
var parentAddonName = GetParentAddonName(&addonContextMenu->AddonInterface);
InventoryItemContext? inventoryItemContext = null;
GameObjectContext? gameObjectContext = null;
if (IsInventoryContext(agentContextInterface))
{
var agentInventoryContext = (AgentInventoryContext*)agentContextInterface;
inventoryItemContext = new InventoryItemContext(agentInventoryContext->InventoryItemId, agentInventoryContext->InventoryItemCount, agentInventoryContext->InventoryItemIsHighQuality);
}
else
{
var agentContext = (AgentContext*)agentContextInterface;
uint? id = agentContext->GameObjectId;
if (id == 0)
{
id = null;
}
ulong? contentId = agentContext->GameObjectContentId;
if (contentId == 0)
{
contentId = null;
}
string? name;
unsafe
{
name = GameInterfaceHelper.ReadSeString((IntPtr)agentContext->GameObjectName.StringPtr).TextValue;
if (string.IsNullOrEmpty(name))
{
name = null;
}
}
ushort? worldId = agentContext->GameObjectWorldId;
if (worldId == 0)
{
worldId = null;
}
if (id != null
|| contentId != null
|| name != null
|| worldId != null)
{
gameObjectContext = new GameObjectContext(id, contentId, name, worldId);
}
}
// 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,
InventoryItemContext = inventoryItemContext,
GameObjectContext = gameObjectContext
};
try
{
contextMenuOpenedDelegate.Invoke(contextMenuOpenedArgs);
}
catch (Exception ex)
{
PluginLog.LogError(ex, "NotifyContextMenuOpened");
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())
{
// TODO: Game doesn't support nested sub context menus, but we might be able to.
if (contextMenuItem is OpenSubContextMenuItem && contextMenuOpenedArgs.Title != null)
{
contextMenuOpenedArgs.Items.Remove(contextMenuItem);
PluginLog.Warning($"Context menu '{contextMenuOpenedArgs.Title}' item '{contextMenuItem}' has been removed because nested sub context menus are not supported.");
}
}
if (contextMenuOpenedArgs.Items.Count > MaxContextMenuItemsPerContextMenu)
{
PluginLog.LogWarning($"Context menu requesting {contextMenuOpenedArgs.Items.Count} of max {MaxContextMenuItemsPerContextMenu} items. Resizing list to compensate.");
contextMenuOpenedArgs.Items.RemoveRange(MaxContextMenuItemsPerContextMenu, contextMenuOpenedArgs.Items.Count - MaxContextMenuItemsPerContextMenu);
}
return contextMenuOpenedArgs;
}
private unsafe bool ContextMenuItemSelectedDetour(AddonContextMenu* addonContextMenu, int selectedIndex, byte a3)
{
if (m_ContextMenuItemSelectedHook == null)
{
return false;
}
try
{
ContextMenuItemSelectedImplementation(addonContextMenu, selectedIndex);
}
catch (Exception ex)
{
PluginLog.Error(ex, "ContextMenuItemSelectedDetour");
}
return m_ContextMenuItemSelectedHook.Original(addonContextMenu, selectedIndex, a3);
}
private unsafe void ContextMenuItemSelectedImplementation(AddonContextMenu* addonContextMenu, int selectedIndex)
{
if (m_CurrentContextMenuOpenedArgs == null || selectedIndex == -1)
{
m_CurrentContextMenuOpenedArgs = null;
m_SelectedOpenSubContextMenuItem = null;
return;
}
// Read the selected item directly from the game
ContextMenuReaderWriter contextMenuReaderWriter = new ContextMenuReaderWriter(m_CurrentAgentContextInterface, addonContextMenu->AtkValuesCount, addonContextMenu->AtkValues);
var gameContextMenuItems = contextMenuReaderWriter.Read();
var gameSelectedItem = gameContextMenuItems.ElementAtOrDefault(selectedIndex);
// This should be impossible
if (gameSelectedItem == null)
{
m_CurrentContextMenuOpenedArgs = null;
m_SelectedOpenSubContextMenuItem = null;
return;
}
// Match it with the items we already know about based on its name.
// We can get into a state where we have a game item we don't recognize when another plugin has added one.
var selectedItem = m_CurrentContextMenuOpenedArgs.Items.FirstOrDefault(item => item.Name.Encode().SequenceEqual(gameSelectedItem.Name.Encode()));
m_SelectedOpenSubContextMenuItem = null;
if (selectedItem is CustomContextMenuItem customContextMenuItem)
{
try
{
var customContextMenuItemSelectedArgs = new CustomContextMenuItemSelectedArgs(m_CurrentContextMenuOpenedArgs, customContextMenuItem);
customContextMenuItem.ItemSelected(customContextMenuItemSelectedArgs);
}
catch (Exception ex)
{
PluginLog.LogError(ex, "ContextMenuItemSelectedImplementation");
}
}
else if (selectedItem is OpenSubContextMenuItem openSubContextMenuItem)
{
m_SelectedOpenSubContextMenuItem = openSubContextMenuItem;
}
m_CurrentContextMenuOpenedArgs = null;
}
private unsafe string? GetParentAddonName(AddonInterface* addonInterface)
{
if (m_GetAddonById == null)
{
return null;
}
var parentAddonId = addonInterface->ParentAddonId;
if (parentAddonId == 0)
{
return null;
}
var atkStage = AtkStage.GetSingleton();
var parentAddon = m_GetAddonById(atkStage->RaptureAtkUnitManager, parentAddonId);
return GameInterfaceHelper.ReadString((IntPtr)(&parentAddon->Name));
}
private unsafe bool IsInventoryContext(AgentContextInterface* agentContextInterface)
{
if (agentContextInterface == AgentInventoryContext.Instance())
{
return true;
}
return false;
}
private unsafe AddonInterface* GetAddonFromAgent(AgentInterface* agentInterface)
{
if (m_GetAddonById == null)
{
return null;
}
if (agentInterface->AddonId == 0)
{
return null;
}
return m_GetAddonById(AtkStage.GetSingleton()->RaptureAtkUnitManager, (ushort)agentInterface->AddonId);
}
}
}

View File

@@ -1,53 +0,0 @@
using Dalamud.Game.Text.SeStringHandling;
using System;
using System.Numerics;
namespace PlayerTags.GameInterface.ContextMenus
{
/// <summary>
/// An item in a context menu.
/// </summary>
public abstract class ContextMenuItem
{
/// <summary>
/// The name of the item.
/// </summary>
public SeString Name { get; set; }
/// <summary>
/// Whether the item is enabled. When enabled, an item is selectable.
/// </summary>
public bool IsEnabled { get; set; } = true;
/// <summary>
/// The indicator of the item.
/// </summary>
public ContextMenuItemIndicator Indicator { get; set; } = ContextMenuItemIndicator.None;
/// <summary>
/// Initializes a new instance of the <see cref="ContextMenuItem"/> class.
/// </summary>
/// <param name="name">The name of the item.</param>
public ContextMenuItem(SeString name)
{
Name = name;
}
public override string ToString()
{
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;
}
}
}
}

View File

@@ -1,21 +0,0 @@
namespace PlayerTags.GameInterface.ContextMenus
{
/// <summary>
/// An indicator displayed on a context menu item.
/// </summary>
public enum ContextMenuItemIndicator
{
/// <summary>
/// The item has no indicator.
/// </summary>
None,
/// <summary>
/// The item has a previous indicator.
/// </summary>
Previous,
/// <summary>
/// The item has a next indicator.
/// </summary>
Next
}
}

View File

@@ -1,62 +0,0 @@
using Dalamud.Game.Text.SeStringHandling;
using System;
using System.Collections.Generic;
namespace PlayerTags.GameInterface.ContextMenus
{
/// <summary>
/// Provides data for <see cref="ContextMenuOpenedDelegate"/> methods.
/// </summary>
public class ContextMenuOpenedArgs
{
/// <summary>
/// The addon associated with the context menu.
/// </summary>
public IntPtr Addon { get; }
/// <summary>
/// The agent associated with the context menu.
/// </summary>
public IntPtr Agent { get; }
/// <summary>
/// The the name of the parent addon associated with the context menu.
/// </summary>
public string? ParentAddonName { get; }
/// <summary>
/// The title of the context menu.
/// </summary>
public string? Title { get; init; }
/// <summary>
/// The items in the context menu.
/// </summary>
public List<ContextMenuItem> Items { get; }
/// <summary>
/// The game object context associated with the context menu.
/// </summary>
public GameObjectContext? GameObjectContext { get; init; }
/// <summary>
/// The item context associated with the context menu.
/// </summary>
public InventoryItemContext? InventoryItemContext { get; init; }
/// <summary>
/// Initializes a new instance of the <see cref="ContextMenuOpenedArgs"/> class.
/// </summary>
/// <param name="addon">The addon associated with the context menu.</param>
/// <param name="agent">The agent associated with the context menu.</param>
/// <param name="parentAddonName">The the name of the parent addon associated with the context menu.</param>
/// <param name="items">The items in the context menu.</param>
public ContextMenuOpenedArgs(IntPtr addon, IntPtr agent, string? parentAddonName, IEnumerable<ContextMenuItem> items)
{
Addon = addon;
Agent = agent;
ParentAddonName = parentAddonName;
Items = new List<ContextMenuItem>(items);
}
}
}

View File

@@ -1,8 +0,0 @@
namespace PlayerTags.GameInterface.ContextMenus
{
/// <summary>
/// Represents the method the <see cref="ContextMenu.ContextMenuOpened"/> event.
/// </summary>
/// <param name="args">The data associated with the <see cref="ContextMenu.ContextMenuOpened"/> event.</param>
public delegate void ContextMenuOpenedDelegate(ContextMenuOpenedArgs args);
}

View File

@@ -1,526 +0,0 @@
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Logging;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
namespace PlayerTags.GameInterface.ContextMenus
{
internal unsafe class ContextMenuReaderWriter
{
private enum SubContextMenuStructLayout
{
Main,
Alternate
}
private AgentContextInterface* m_AgentContextInterface;
private int m_AtkValueCount;
public int AtkValueCount
{
get { return m_AtkValueCount; }
}
private AtkValue* m_AtkValues;
public AtkValue* AtkValues
{
get { return m_AtkValues; }
}
public int ContextMenuItemCount
{
get { return m_AtkValues[0].Int; }
}
public bool HasTitle
{
get
{
bool isStringType =
(int)m_AtkValues[1].Type == 8
|| (int)m_AtkValues[1].Type == 38
|| m_AtkValues[1].Type == FFXIVClientStructs.FFXIV.Component.GUI.ValueType.String;
return isStringType;
}
}
public SeString? Title
{
get
{
if (HasTitle)
{
GameInterfaceHelper.TryReadSeString((IntPtr)(&m_AtkValues[1])->String, out var str);
return str;
}
return null;
}
}
public int HasPreviousIndicatorFlagsIndex
{
get
{
if (HasTitle)
{
return 6;
}
return 2;
}
}
public int HasNextIndicatorFlagsIndex
{
get
{
if (HasTitle)
{
return 5;
}
return 3;
}
}
public int FirstContextMenuItemIndex
{
get
{
if (HasTitle)
{
return 8;
}
return 7;
}
}
public int NameIndexOffset
{
get
{
if (HasTitle && StructLayout == SubContextMenuStructLayout.Alternate)
{
return 1;
}
return 0;
}
}
public int IsDisabledIndexOffset
{
get
{
if (HasTitle && StructLayout == SubContextMenuStructLayout.Alternate)
{
return 2;
}
return ContextMenuItemCount;
}
}
/// <summary>
/// 0x14000000 | action
/// </summary>
public int? MaskedActionIndexOffset
{
get
{
if (HasTitle && StructLayout == SubContextMenuStructLayout.Alternate)
{
return 3;
}
return null;
}
}
public int SequentialAtkValuesPerContextMenuItem
{
get
{
if (HasTitle && StructLayout == SubContextMenuStructLayout.Alternate)
{
return 4;
}
return 1;
}
}
public int TotalDesiredAtkValuesPerContextMenuItem
{
get
{
if (HasTitle && StructLayout == SubContextMenuStructLayout.Alternate)
{
return 4;
}
return 2;
}
}
public Vector2? Position
{
get
{
if (HasTitle)
{
return new Vector2(m_AtkValues[2].Int, m_AtkValues[3].Int);
}
return null;
}
}
public unsafe bool IsInventoryContext
{
get
{
if ((IntPtr)m_AgentContextInterface == (IntPtr)AgentInventoryContext.Instance())
{
return true;
}
return false;
}
}
private SubContextMenuStructLayout? StructLayout
{
get
{
if (HasTitle)
{
if (m_AtkValues[7].Int == 8)
{
return SubContextMenuStructLayout.Alternate;
}
else if (m_AtkValues[7].Int == 1)
{
return SubContextMenuStructLayout.Main;
}
}
return null;
}
}
public byte NoopAction
{
get
{
if (IsInventoryContext)
{
return 0xff;
}
else
{
return 0x67;
}
}
}
public byte OpenSubContextMenuAction
{
get
{
if (IsInventoryContext)
{
// This is actually the action to open the Second Tier context menu and we just hack around it
return 0x31;
}
else
{
return 0x66;
}
}
}
public byte? FirstUnhandledAction
{
get
{
if (StructLayout != null && StructLayout == SubContextMenuStructLayout.Alternate)
{
return 0x68;
}
return null;
}
}
public ContextMenuReaderWriter(AgentContextInterface* agentContextInterface, int atkValueCount, AtkValue* atkValues)
{
PluginLog.Warning($"{(IntPtr)atkValues:X}");
m_AgentContextInterface = agentContextInterface;
m_AtkValueCount = atkValueCount;
m_AtkValues = atkValues;
}
public GameContextMenuItem[] Read()
{
List<GameContextMenuItem> gameContextMenuItems = new List<GameContextMenuItem>();
for (var contextMenuItemIndex = 0; contextMenuItemIndex < ContextMenuItemCount; contextMenuItemIndex++)
{
var contextMenuItemAtkValueBaseIndex = FirstContextMenuItemIndex + (contextMenuItemIndex * SequentialAtkValuesPerContextMenuItem);
// Get the name
var nameAtkValue = &m_AtkValues[contextMenuItemAtkValueBaseIndex + NameIndexOffset];
if (nameAtkValue->Type == 0)
{
continue;
}
var name = GameInterfaceHelper.ReadSeString((IntPtr)nameAtkValue->String);
// Get the enabled state. Note that SE stores this as IsDisabled, NOT IsEnabled (those heathens)
var isEnabled = true;
bool isDisabledDefined = FirstContextMenuItemIndex + ContextMenuItemCount < AtkValueCount;
if (isDisabledDefined)
{
var isDisabledAtkValue = &m_AtkValues[contextMenuItemAtkValueBaseIndex + IsDisabledIndexOffset];
isEnabled = isDisabledAtkValue->Int == 0;
}
// Get the action
byte action = 0;
if (IsInventoryContext)
{
var actions = &((AgentInventoryContext*)m_AgentContextInterface)->Actions;
action = *(actions + contextMenuItemAtkValueBaseIndex);
}
else if (StructLayout != null && StructLayout.Value == SubContextMenuStructLayout.Alternate)
{
var redButtonActions = &((AgentContext*)m_AgentContextInterface)->Items->RedButtonActions;
action = (byte)*(redButtonActions + contextMenuItemIndex);
}
else
{
var actions = &((AgentContext*)m_AgentContextInterface)->Items->Actions;
action = *(actions + contextMenuItemAtkValueBaseIndex);
}
// Get the has previous indicator flag
var hasPreviousIndicatorFlagsAtkValue = &m_AtkValues[HasPreviousIndicatorFlagsIndex];
var hasPreviousIndicator = HasFlag(hasPreviousIndicatorFlagsAtkValue->UInt, contextMenuItemIndex);
// Get the has next indicator flag
var hasNextIndicatorlagsAtkValue = &m_AtkValues[HasNextIndicatorFlagsIndex];
var hasNextIndicator = HasFlag(hasNextIndicatorlagsAtkValue->UInt, contextMenuItemIndex);
ContextMenuItemIndicator indicator = ContextMenuItemIndicator.None;
if (hasPreviousIndicator)
{
indicator = ContextMenuItemIndicator.Previous;
}
else if (hasNextIndicator)
{
indicator = ContextMenuItemIndicator.Next;
}
var gameContextMenuItem = new GameContextMenuItem(name, action)
{
IsEnabled = isEnabled,
Indicator = indicator
};
gameContextMenuItems.Add(gameContextMenuItem);
}
return gameContextMenuItems.ToArray();
}
public unsafe void Write(IEnumerable<ContextMenuItem> contextMenuItems, AtkValueChangeTypeDelegate_Unmanaged atkValueChangeType, AtkValueSetStringDelegate_Unmanaged atkValueSetString, bool allowReallocate = true)
{
if (allowReallocate)
{
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;
var newAtkValuesArraySize = arrayCountSize + Marshal.SizeOf<AtkValue>() * newAtkValuesCount;
var newAtkValuesArray = GameInterfaceHelper.GameUIAllocate((ulong)newAtkValuesArraySize);
if (newAtkValuesArray == IntPtr.Zero)
{
return;
}
var newAtkValues = (AtkValue*)(newAtkValuesArray + arrayCountSize);
// Zero the memory, then copy the atk values up to the first context menu item atk value
Marshal.Copy(new byte[newAtkValuesArraySize], 0, newAtkValuesArray, newAtkValuesArraySize);
Buffer.MemoryCopy(m_AtkValues, newAtkValues, newAtkValuesArraySize - arrayCountSize, (long)sizeof(AtkValue) * FirstContextMenuItemIndex);
// Free the old array
IntPtr oldArray = (IntPtr)m_AtkValues - arrayCountSize;
ulong oldArrayCount = *(ulong*)oldArray;
ulong oldArraySize = arrayCountSize + ((ulong)sizeof(AtkValue) * oldArrayCount);
GameInterfaceHelper.GameFree(ref oldArray, oldArraySize);
// Set the array count
*(ulong*)newAtkValuesArray = (ulong)newAtkValuesCount;
m_AtkValueCount = newAtkValuesCount;
m_AtkValues = newAtkValues;
}
// Set the context menu item count
const int contextMenuItemCountAtkValueIndex = 0;
var contextMenuItemCountAtkValue = &m_AtkValues[contextMenuItemCountAtkValueIndex];
contextMenuItemCountAtkValue->UInt = (uint)contextMenuItems.Count();
// Clear the previous arrow flags
var hasPreviousIndicatorAtkValue = &m_AtkValues[HasPreviousIndicatorFlagsIndex];
hasPreviousIndicatorAtkValue->UInt = 0;
// Clear the next arrow flags
var hasNextIndiactorFlagsAtkValue = &m_AtkValues[HasNextIndicatorFlagsIndex];
hasNextIndiactorFlagsAtkValue->UInt = 0;
for (int contextMenuItemIndex = 0; contextMenuItemIndex < contextMenuItems.Count(); ++contextMenuItemIndex)
{
var contextMenuItem = contextMenuItems.ElementAt(contextMenuItemIndex);
var contextMenuItemAtkValueBaseIndex = FirstContextMenuItemIndex + (contextMenuItemIndex * SequentialAtkValuesPerContextMenuItem);
// Set the name
var nameAtkValue = &m_AtkValues[contextMenuItemAtkValueBaseIndex + NameIndexOffset];
atkValueChangeType(nameAtkValue, FFXIVClientStructs.FFXIV.Component.GUI.ValueType.String);
fixed (byte* nameBytesPtr = contextMenuItem.Name.Encode().NullTerminate())
{
atkValueSetString(nameAtkValue, nameBytesPtr);
}
// Set the enabled state. Note that SE stores this as IsDisabled, NOT IsEnabled (those heathens)
var disabledAtkValue = &m_AtkValues[contextMenuItemAtkValueBaseIndex + IsDisabledIndexOffset];
atkValueChangeType(disabledAtkValue, FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Int);
disabledAtkValue->Int = contextMenuItem.IsEnabled ? 0 : 1;
// Set the action
byte action = 0;
if (contextMenuItem is GameContextMenuItem gameContextMenuItem)
{
action = gameContextMenuItem.SelectedAction;
}
else if (contextMenuItem is CustomContextMenuItem customContextMenuItem)
{
action = NoopAction;
}
else if (contextMenuItem is OpenSubContextMenuItem openSubContextMenuItem)
{
action = OpenSubContextMenuAction;
}
if (IsInventoryContext)
{
var actions = &((AgentInventoryContext*)m_AgentContextInterface)->Actions;
*(actions + FirstContextMenuItemIndex + contextMenuItemIndex) = action;
}
else if (StructLayout != null && StructLayout.Value == SubContextMenuStructLayout.Alternate && FirstUnhandledAction != null)
{
// Some weird placeholder goes here
var actions = &((AgentContext*)m_AgentContextInterface)->Items->Actions;
*(actions + FirstContextMenuItemIndex + contextMenuItemIndex) = (byte)(FirstUnhandledAction.Value + contextMenuItemIndex);
// Make sure there's one of these function pointers for every item.
// The function needs to be the same, so we just copy the first one into every index.
var unkFunctionPointers = &((AgentContext*)m_AgentContextInterface)->Items->UnkFunctionPointers;
*(unkFunctionPointers + FirstContextMenuItemIndex + contextMenuItemIndex) = *(unkFunctionPointers + FirstContextMenuItemIndex);
// The real action goes here
var redButtonActions = &((AgentContext*)m_AgentContextInterface)->Items->RedButtonActions;
*(redButtonActions + contextMenuItemIndex) = action;
}
else
{
var actions = &((AgentContext*)m_AgentContextInterface)->Items->Actions;
*(actions + FirstContextMenuItemIndex + contextMenuItemIndex) = action;
}
if (contextMenuItem.Indicator == ContextMenuItemIndicator.Previous)
{
SetFlag(ref hasPreviousIndicatorAtkValue->UInt, contextMenuItemIndex, true);
}
else if (contextMenuItem.Indicator == ContextMenuItemIndicator.Next)
{
SetFlag(ref hasNextIndiactorFlagsAtkValue->UInt, contextMenuItemIndex, true);
}
}
}
private bool HasFlag(uint mask, int itemIndex)
{
return (mask & (1 << itemIndex)) > 0;
}
private void SetFlag(ref uint mask, int itemIndex, bool value)
{
mask &= ~((uint)1 << itemIndex);
if (value)
{
mask |= (uint)(1 << itemIndex);
}
}
public void Log()
{
Log(m_AtkValueCount, m_AtkValues);
}
public static void Log(int atkValueCount, AtkValue* atkValues)
{
PluginLog.Debug($"ContextMenuReader.Log");
for (int atkValueIndex = 0; atkValueIndex < atkValueCount; ++atkValueIndex)
{
var atkValue = &atkValues[atkValueIndex];
object? value = null;
if (atkValue->Type == FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Int)
{
value = atkValue->Int;
}
else if (atkValue->Type == FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Bool)
{
value = atkValue->Byte;
}
else if (atkValue->Type == FFXIVClientStructs.FFXIV.Component.GUI.ValueType.UInt)
{
value = atkValue->UInt;
}
else if (atkValue->Type == FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Float)
{
value = atkValue->Float;
}
else if (atkValue->Type == FFXIVClientStructs.FFXIV.Component.GUI.ValueType.String
|| (int)atkValue->Type == 38
|| (int)atkValue->Type == 8)
{
if (GameInterfaceHelper.TryReadSeString((IntPtr)atkValue->String, out var str))
{
value = str;
}
}
else
{
value = $"{(IntPtr)atkValue->String:X}";
}
PluginLog.Debug($"atkValues[{atkValueIndex}]={(IntPtr)atkValue:X} {atkValue->Type}={value}");
}
}
}
}

View File

@@ -1,36 +0,0 @@
using Dalamud.Game.Text.SeStringHandling;
namespace PlayerTags.GameInterface.ContextMenus
{
/// <summary>
/// An item in a context menu with a user defined action.
/// </summary>
public class CustomContextMenuItem : ContextMenuItem
{
/// <summary>
/// The action that will be called when the item is selected.
/// </summary>
public CustomContextMenuItemSelectedDelegate ItemSelected { get; }
/// <summary>
/// Initializes a new instance of the <see cref="CustomContextMenuItem"/> class.
/// </summary>
/// <param name="name">The name of the item.</param>
/// <param name="itemSelected">The action that will be called when the item is selected.</param>
internal CustomContextMenuItem(SeString name, CustomContextMenuItemSelectedDelegate itemSelected)
: base(name)
{
ItemSelected = itemSelected;
}
public override int GetHashCode()
{
unchecked
{
int hash = base.GetHashCode();
hash = hash * 23 + ItemSelected.GetHashCode();
return hash;
}
}
}
}

View File

@@ -1,29 +0,0 @@
namespace PlayerTags.GameInterface.ContextMenus
{
/// <summary>
/// Provides data for <see cref="CustomContextMenuItemSelectedDelegate"/> methods.
/// </summary>
public class CustomContextMenuItemSelectedArgs
{
/// <summary>
/// The currently opened context menu.
/// </summary>
public ContextMenuOpenedArgs ContextMenuOpenedArgs { get; init; }
/// <summary>
/// The selected item within the currently opened context menu.
/// </summary>
public CustomContextMenuItem SelectedItem { get; init; }
/// <summary>
/// Initializes a new instance of the <see cref="CustomContextMenuItemSelectedArgs"/> class.
/// </summary>
/// <param name="contextMenuOpenedArgs">The currently opened context menu.</param>
/// <param name="selectedItem">The selected item within the currently opened context menu.</param>
public CustomContextMenuItemSelectedArgs(ContextMenuOpenedArgs contextMenuOpenedArgs, CustomContextMenuItem selectedItem)
{
ContextMenuOpenedArgs = contextMenuOpenedArgs;
SelectedItem = selectedItem;
}
}
}

View File

@@ -1,8 +0,0 @@
namespace PlayerTags.GameInterface.ContextMenus
{
/// <summary>
/// Represents the method that handles when a <see cref="CustomContextMenuItem"/> is selected.
/// </summary>
/// <param name="args">The data associated with the selected <see cref="CustomContextMenuItem"/>.</param>
public delegate void CustomContextMenuItemSelectedDelegate(CustomContextMenuItemSelectedArgs args);
}

View File

@@ -1,14 +0,0 @@
using FFXIVClientStructs.FFXIV.Component.GUI;
using System.Runtime.InteropServices;
namespace FFXIVClientStructs.FFXIV.Client.UI
{
[StructLayout(LayoutKind.Explicit)]
public struct AddonContextMenu
{
[FieldOffset(0x0)] public unsafe AddonInterface AddonInterface;
[FieldOffset(0x160)] public unsafe AtkValue* AtkValues;
[FieldOffset(0x1CA)] public ushort AtkValuesCount;
[FieldOffset(0x690)] public bool IsInitialMenu;
}
}

View File

@@ -1,11 +0,0 @@
using System.Runtime.InteropServices;
namespace FFXIVClientStructs.FFXIV.Client.UI
{
[StructLayout(LayoutKind.Explicit)]
public unsafe struct AddonInterface
{
[FieldOffset(0x8)] public byte Name;
[FieldOffset(0x1D2)] public ushort ParentAddonId;
}
}

View File

@@ -1,20 +0,0 @@
using FFXIVClientStructs.FFXIV.Client.System.String;
using FFXIVClientStructs.FFXIV.Component.GUI;
using System.Runtime.InteropServices;
namespace FFXIVClientStructs.FFXIV.Client.UI.Agent
{
[StructLayout(LayoutKind.Explicit)]
public unsafe struct AgentContext
{
public static AgentContext* Instance() => (AgentContext*)System.Framework.Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId(AgentId.Context);
[FieldOffset(0x0)] public AgentInterface AgentInterface;
[FieldOffset(0x0)] public AgentContextInterface AgentContextInterface;
[FieldOffset(0xD18)] public unsafe AgentContextMenuItems* Items;
[FieldOffset(0xE08)] public Utf8String GameObjectName;
[FieldOffset(0xEE0)] public ulong GameObjectContentId;
[FieldOffset(0xEF0)] public uint GameObjectId;
[FieldOffset(0xF00)] public ushort GameObjectWorldId;
}
}

View File

@@ -1,15 +0,0 @@
using FFXIVClientStructs.FFXIV.Component.GUI;
using System.Runtime.InteropServices;
namespace FFXIVClientStructs.FFXIV.Client.UI.Agent
{
[StructLayout(LayoutKind.Explicit)]
public unsafe struct AgentContextInterface
{
[FieldOffset(0x0)] public AgentInterface AgentInterface;
[FieldOffset(0x670)] public unsafe byte SelectedIndex;
[FieldOffset(0x690)] public byte* Unk1;
[FieldOffset(0xD08)] public byte* SubContextMenuTitle;
[FieldOffset(0x1740)] public bool IsSubContextMenu;
}
}

View File

@@ -1,15 +0,0 @@
using FFXIVClientStructs.FFXIV.Component.GUI;
using System.Runtime.InteropServices;
namespace FFXIVClientStructs.FFXIV.Client.UI.Agent
{
[StructLayout(LayoutKind.Explicit)]
public struct AgentContextMenuItems
{
[FieldOffset(0x0)] public ushort AtkValueCount;
[FieldOffset(0x8)] public AtkValue AtkValues;
[FieldOffset(0x428)] public byte Actions;
[FieldOffset(0x450)] public ulong UnkFunctionPointers;
[FieldOffset(0x598)] public ulong RedButtonActions;
}
}

View File

@@ -1,24 +0,0 @@
using FFXIVClientStructs.FFXIV.Component.GUI;
using System.Runtime.InteropServices;
namespace FFXIVClientStructs.FFXIV.Client.UI.Agent
{
[StructLayout(LayoutKind.Explicit)]
public unsafe struct AgentInventoryContext
{
public static AgentInventoryContext* Instance() => (AgentInventoryContext*)System.Framework.Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId(AgentId.InventoryContext);
[FieldOffset(0x0)] public AgentInterface AgentInterface;
[FieldOffset(0x0)] public AgentContextInterface AgentContextInterface;
[FieldOffset(0x2C)] public uint FirstContextMenuItemAtkValueIndex;
[FieldOffset(0x30)] public uint ContextMenuItemCount;
[FieldOffset(0x38)] public AtkValue AtkValues;
[FieldOffset(0x558)] public unsafe byte Actions;
[FieldOffset(0x5A8)] public uint UnkFlags;
[FieldOffset(0x5B0)] public uint PositionX;
[FieldOffset(0x5B4)] public uint PositionY;
[FieldOffset(0x5F8)] public uint InventoryItemId;
[FieldOffset(0x5FC)] public uint InventoryItemCount;
[FieldOffset(0x604)] public bool InventoryItemIsHighQuality;
}
}

View File

@@ -1,36 +0,0 @@
using Dalamud.Game.Text.SeStringHandling;
namespace PlayerTags.GameInterface.ContextMenus
{
/// <summary>
/// An item in a context menu that with a specific game action.
/// </summary>
public class GameContextMenuItem : ContextMenuItem
{
/// <summary>
/// The game action that will be handled when the item is selected.
/// </summary>
public byte SelectedAction { get; }
/// <summary>
/// Initializes a new instance of the <see cref="GameContextMenuItem"/> class.
/// </summary>
/// <param name="name">The name of the item.</param>
/// <param name="selectedAction">The game action that will be handled when the item is selected.</param>
public GameContextMenuItem(SeString name, byte selectedAction)
: base(name)
{
SelectedAction = selectedAction;
}
public override int GetHashCode()
{
unchecked
{
int hash = base.GetHashCode();
hash = hash * 23 + SelectedAction;
return hash;
}
}
}
}

View File

@@ -1,45 +0,0 @@
using Dalamud.Game.Text.SeStringHandling;
namespace PlayerTags.GameInterface.ContextMenus
{
/// <summary>
/// Provides game object context to a context menu.
/// </summary>
public class GameObjectContext
{
/// <summary>
/// The id of the game object.
/// </summary>
public uint? Id { get; }
/// <summary>
/// The content id of the game object.
/// </summary>
public ulong? ContentId { get; }
/// <summary>
/// The name of the game object.
/// </summary>
public string? Name { get; }
/// <summary>
/// The world id of the game object.
/// </summary>
public ushort? WorldId { get; }
/// <summary>
/// Initializes a new instance of the <see cref="GameObjectContext"/> class.
/// </summary>
/// <param name="id">The id of the game object.</param>
/// <param name="contentId">The lower content id of the game object.</param>
/// <param name="name">The name of the game object.</param>
/// <param name="worldId">The world id of the game object.</param>
public GameObjectContext(uint? id, ulong? contentId, string? name, ushort? worldId)
{
Id = id;
ContentId = contentId;
Name = name;
WorldId = worldId;
}
}
}

View File

@@ -1,36 +0,0 @@
namespace PlayerTags.GameInterface.ContextMenus
{
/// <summary>
/// Provides inventory item context to a context menu.
/// </summary>
public class InventoryItemContext
{
/// <summary>
/// The id of the item.
/// </summary>
public uint Id { get; }
/// <summary>
/// The count of the item in the stack.
/// </summary>
public uint Count { get; }
/// <summary>
/// Whether the item is high quality.
/// </summary>
public bool IsHighQuality { get; }
/// <summary>
/// Initializes a new instance of the <see cref="InventoryItemContext"/> class.
/// </summary>
/// <param name="id">The id of the item.</param>
/// <param name="count">The count of the item in the stack.</param>
/// <param name="isHighQuality">Whether the item is high quality.</param>
public InventoryItemContext(uint id, uint count, bool isHighQuality)
{
Id = id;
Count = count;
IsHighQuality = isHighQuality;
}
}
}

View File

@@ -1,37 +0,0 @@
using Dalamud.Game.Text.SeStringHandling;
namespace PlayerTags.GameInterface.ContextMenus
{
/// <summary>
/// An item in a context menu that can open a sub context menu.
/// </summary>
public class OpenSubContextMenuItem : ContextMenuItem
{
/// <summary>
/// The action that will be called when the item is selected.
/// </summary>
public ContextMenuOpenedDelegate Opened { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="OpenSubContextMenuItem"/> class.
/// </summary>
/// <param name="name">The name of the item.</param>
/// <param name="opened">The action that will be called when the item is selected.</param>
internal OpenSubContextMenuItem(SeString name, ContextMenuOpenedDelegate opened)
: base(name)
{
Opened = opened;
Indicator = ContextMenuItemIndicator.Next;
}
public override int GetHashCode()
{
unchecked
{
int hash = base.GetHashCode();
hash = hash * 23 + Opened.GetHashCode();
return hash;
}
}
}
}

View File

@@ -5,7 +5,7 @@
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net5.0-windows</TargetFramework>
<TargetFramework>net5.0-windows7</TargetFramework>
<Platforms>x64</Platforms>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
@@ -20,6 +20,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dalamud.ContextMenu" Version="1.0.0" />
<PackageReference Include="DalamudPackager" Version="2.1.7" />
<Reference Include="FFXIVClientStructs">
<HintPath>$(DalamudLibPath)FFXIVClientStructs.dll</HintPath>

View File

@@ -5,6 +5,7 @@ using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Party;
using Dalamud.Game.Command;
using Dalamud.Game.Gui;
using Dalamud.Game.Gui.ContextMenus;
using Dalamud.IoC;
using Dalamud.Plugin;

File diff suppressed because it is too large Load Diff

View File

@@ -136,10 +136,10 @@
<value>Player</value>
</data>
<data name="Loc_Static_ContextMenu_AddTag" xml:space="preserve">
<value>Add tag</value>
<value>Add tag: {0}</value>
</data>
<data name="Loc_Static_ContextMenu_RemoveTag" xml:space="preserve">
<value>Remove tag</value>
<value>Remove tag: {0}</value>
</data>
<data name="Loc_Static_Nameplates" xml:space="preserve">
<value>Nameplates</value>
@@ -363,7 +363,6 @@
<data name="Loc_IsIconVisibleInNameplates_Description" xml:space="preserve">
<value>Whether the icon will be shown in nameplates.</value>
</data>
<data name="Loc_Text" xml:space="preserve">
<value>Text</value>
</data>
@@ -384,7 +383,7 @@
</data>
<data name="Loc_IsTextItalic" xml:space="preserve">
<value>Italic</value>
</data>
</data>
<data name="Loc_IsTextItalic_Description" xml:space="preserve">
<value>Whether the text will be italic.</value>
</data>
@@ -400,7 +399,6 @@
<data name="Loc_IsTextVisibleInNameplates_Description" xml:space="preserve">
<value>Whether the text will be shown in nameplates.</value>
</data>
<data name="Loc_TagPositionInChat" xml:space="preserve">
<value>Position in chat</value>
</data>
@@ -419,14 +417,12 @@
<data name="Loc_TagTargetInNameplates_Description" xml:space="preserve">
<value>The element that the tag should target in nameplates.</value>
</data>
<data name="Loc_GameObjectNamesToApplyTo" xml:space="preserve">
<value>Add to players</value>
</data>
<data name="Loc_GameObjectNamesToApplyTo_Description" xml:space="preserve">
<value>A list of players to add tags to, separated by commas or semi-colons. E.g. "Cloud Strife, Tifa Lockhart".</value>
</data>
<data name="Loc_TagPosition_After" xml:space="preserve">
<value>After</value>
</data>
@@ -445,15 +441,12 @@
<data name="Loc_TagPosition_Replace_Description" xml:space="preserve">
<value>Replace the target element with the tag.</value>
</data>
<data name="Loc_IsEnabled" xml:space="preserve">
<value>Enabled</value>
</data>
<data name="Loc_CustomId" xml:space="preserve">
<value>Custom id</value>
</data>
<data name="Loc_GeneralCategory" xml:space="preserve">
<value>General properties</value>
</data>
@@ -472,7 +465,6 @@
<data name="Loc_PlayerCategory" xml:space="preserve">
<value>Player properties</value>
</data>
<data name="Loc_IsVisibleInPveDuties" xml:space="preserve">
<value>Show in pve duties</value>
</data>
@@ -491,7 +483,6 @@
<data name="Loc_IsVisibleInOverworld_Description" xml:space="preserve">
<value>Whether the tag should be visible in other circumstances for which there is no specific option.</value>
</data>
<data name="Loc_IsVisibleForSelf" xml:space="preserve">
<value>Show for self</value>
</data>
@@ -528,77 +519,66 @@
<data name="Loc_IsVisibleForOtherPlayers_Description" xml:space="preserve">
<value>Whether the tag should be visible for players in other circumstances for which there is no specific option.</value>
</data>
<data name="Loc_IsPlayersTabOrderedByProximity" xml:space="preserve">
<value>Order by proximity</value>
</data>
<data name="Loc_IsPlayersTabOrderedByProximity_Description" xml:space="preserve">
<value>Players that are closer to you will be ordered towards the top.</value>
</data>
<data name="Loc_IsPlayersTabSelfVisible" xml:space="preserve">
<value>Show self</value>
</data>
<data name="Loc_IsPlayersTabSelfVisible_Description" xml:space="preserve">
<value>Show yourself in the players list.</value>
</data>
<data name="Loc_IsPlayersTabFriendsVisible" xml:space="preserve">
<value>Show friends</value>
</data>
<data name="Loc_IsPlayersTabFriendsVisible_Description" xml:space="preserve">
<value>Show friends in the players list.</value>
</data>
<data name="Loc_IsPlayersTabPartyVisible" xml:space="preserve">
<value>Show party members</value>
</data>
<data name="Loc_IsPlayersTabPartyVisible_Description" xml:space="preserve">
<value>Show party members in the players list.</value>
</data>
<data name="Loc_IsPlayersTabAllianceVisible" xml:space="preserve">
<value>Show alliance members</value>
</data>
<data name="Loc_IsPlayersTabAllianceVisible_Description" xml:space="preserve">
<value>Show alliance members in the players list.</value>
</data>
<data name="Loc_IsPlayersTabEnemiesVisible" xml:space="preserve">
<value>Show enemies</value>
</data>
<data name="Loc_IsPlayersTabEnemiesVisible_Description" xml:space="preserve">
<value>Show enemies in the players list.</value>
</data>
<data name="Loc_IsPlayersTabOthersVisible" xml:space="preserve">
<value>Show others</value>
</data>
<data name="Loc_IsPlayersTabOthersVisible_Description" xml:space="preserve">
<value>Show others in the players list.</value>
</data>
<data name="Loc_IsTextColorAppliedToChatName" xml:space="preserve">
<value>Apply color to chat name</value>
</data>
<data name="Loc_IsTextColorAppliedToChatName_Description" xml:space="preserve">
<value>Whether the color will be applied to the name in chat.</value>
</data>
<data name="Loc_IsTextColorAppliedToNameplateName" xml:space="preserve">
<value>Apply color to nameplate name</value>
</data>
<data name="Loc_IsTextColorAppliedToNameplateName_Description" xml:space="preserve">
<value>Whether the color will be applied to the name in nameplates.</value>
</data>
<data name="Loc_IsTextColorAppliedToNameplateTitle" xml:space="preserve">
<value>Apply color to nameplate title</value>
</data>
<data name="Loc_IsTextColorAppliedToNameplateTitle_Description" xml:space="preserve">
<value>Whether the color will be applied to title in nameplates.</value>
</data>
<data name="Loc_IsTextColorAppliedToNameplateFreeCompany" xml:space="preserve">
<value>Apply color to nameplate free company</value>
</data>