Merge branch 'new-context-menu-integration'
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
336
PlayerTags/Resources/Strings.Designer.cs
generated
336
PlayerTags/Resources/Strings.Designer.cs
generated
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user