refactor: remove context menu support

This commit is contained in:
Anna 2022-01-30 17:10:17 -05:00
parent 51e3ba33e1
commit 1a4a3a065c
16 changed files with 1 additions and 1108 deletions

View File

@ -1,54 +0,0 @@
using System;
using Dalamud.Game.Text.SeStringHandling;
namespace XivCommon.Functions.ContextMenu {
/// <summary>
/// The base class for context menu arguments
/// </summary>
public abstract class BaseContextMenuArgs {
/// <summary>
/// Pointer to the context menu addon.
/// </summary>
public IntPtr Addon { get; }
/// <summary>
/// Pointer to the context menu agent.
/// </summary>
public IntPtr Agent { get; }
/// <summary>
/// The name of the addon containing this context menu, if any.
/// </summary>
public string? ParentAddonName { get; }
/// <summary>
/// The object ID for this context menu. May be invalid (0xE0000000).
/// </summary>
public uint ObjectId { get; }
/// <summary>
/// The lower half of the content ID of the object for this context menu. May be zero.
/// </summary>
public uint ContentIdLower { get; }
/// <summary>
/// The text related to this context menu, usually an object name.
/// </summary>
public SeString? Text { get; }
/// <summary>
/// The world of the object this context menu is for, if any.
/// </summary>
public ushort ObjectWorld { get; }
internal BaseContextMenuArgs(IntPtr addon, IntPtr agent, string? parentAddonName, uint objectId, uint contentIdLower, SeString? text, ushort objectWorld) {
this.Addon = addon;
this.Agent = agent;
this.ParentAddonName = parentAddonName;
this.ObjectId = objectId;
this.ContentIdLower = contentIdLower;
this.Text = text;
this.ObjectWorld = objectWorld;
}
}
}

View File

@ -1,16 +0,0 @@
namespace XivCommon.Functions.ContextMenu {
/// <summary>
/// A base context menu item
/// </summary>
public abstract class BaseContextMenuItem {
/// <summary>
/// If this item should be enabled in the menu.
/// </summary>
public bool Enabled { get; set; } = true;
/// <summary>
/// If this item should have the submenu arrow in the menu.
/// </summary>
public bool IsSubMenu { get; set; }
}
}

View File

@ -1,774 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.ExceptionServices;
using System.Runtime.InteropServices;
using System.Text;
using Dalamud;
using Dalamud.Game;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Hooking;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI;
using XivCommon.Functions.ContextMenu.Inventory;
using Framework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework;
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
namespace XivCommon.Functions.ContextMenu {
/// <summary>
/// Context menu functions
/// </summary>
public class ContextMenu : IDisposable {
private static class Signatures {
internal const string SomeOpenAddonThing = "E8 ?? ?? ?? ?? 0F B7 C0 48 83 C4 60";
internal const string ContextMenuOpen = "48 8B C4 57 41 56 41 57 48 81 EC";
internal const string ContextMenuSelected = "48 89 5C 24 ?? 55 57 41 56 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 44 24 ?? 80 B9";
internal const string ContextMenuEvent66 = "E8 ?? ?? ?? ?? 44 39 A3 ?? ?? ?? ?? 0F 84";
internal const string InventoryContextMenuEvent30 = "E8 ?? ?? ?? ?? 48 83 C4 30 5B C3 8B 83";
internal const string SetUpContextSubMenu = "E8 ?? ?? ?? ?? 44 39 A3 ?? ?? ?? ?? 0F 86";
internal const string SetUpInventoryContextSubMenu = "44 88 44 24 ?? 88 54 24 10 53";
internal const string TitleContextMenuOpen = "48 8B C4 57 41 55 41 56 48 81 EC";
internal const string AtkValueChangeType = "E8 ?? ?? ?? ?? 45 84 F6 48 8D 4C 24";
internal const string AtkValueSetString = "E8 ?? ?? ?? ?? 41 03 ED";
internal const string GetAddonByInternalId = "E8 ?? ?? ?? ?? 8B 6B 20";
}
#region Offsets and other constants
private const int MaxItems = 32;
/// <summary>
/// Offset from addon to menu type
/// </summary>
private const int ParentAddonIdOffset = 0x1D2;
private const int AddonArraySizeOffset = 0x1CA;
private const int AddonArrayOffset = 0x160;
private const int ContextMenuItemOffset = 7;
/// <summary>
/// Offset from agent to actions byte array pointer (have to add the actions offset after)
/// </summary>
private const int MenuActionsPointerOffset = 0xD18;
/// <summary>
/// SetUpContextSubMenu checks this
/// </summary>
private const int BooleanOffsetCheck = 0x690;
/// <summary>
/// Offset from [MenuActionsPointer] to actions byte array
/// </summary>
private const int MenuActionsOffset = 0x428;
/// <summary>
/// Offset from inventory context agent to actions byte array
/// </summary>
private const int InventoryMenuActionsOffset = 0x558;
private const int ObjectIdOffset = 0xEF0;
private const int ContentIdLowerOffset = 0xEE0;
private const int TextPointerOffset = 0xE08;
private const int WorldOffset = 0xF00;
private const int ItemIdOffset = 0x5F8;
private const int ItemAmountOffset = 0x5FC;
private const int ItemHqOffset = 0x604;
// Found in the first function in the agent's vtable
private const byte NoopContextId = 0x67;
private const byte InventoryNoopContextId = 0xFF;
private const byte ContextSubId = 0x66;
private const byte InventoryContextSubId = 0x30;
#endregion
// 82C570 called when you click on a title menu context item
/// <summary>
/// The delegate for context menu events.
/// </summary>
public delegate void ContextMenuOpenEventDelegate(ContextMenuOpenArgs args);
/// <summary>
/// <para>
/// The event that is fired when a context menu is being prepared for opening.
/// </para>
/// <para>
/// Requires the <see cref="Hooks.ContextMenu"/> hook to be enabled.
/// </para>
/// </summary>
public event ContextMenuOpenEventDelegate? OpenContextMenu;
/// <summary>
/// The delegate for inventory context menu events.
/// </summary>
public delegate void InventoryContextMenuOpenEventDelegate(InventoryContextMenuOpenArgs args);
/// <summary>
/// <para>
/// The event that is fired when an inventory context menu is being prepared for opening.
/// </para>
/// <para>
/// Requires the <see cref="Hooks.ContextMenu"/> hook to be enabled.
/// </para>
/// </summary>
public event InventoryContextMenuOpenEventDelegate? OpenInventoryContextMenu;
/// <summary>
/// The delegate that is run when a context menu item is selected.
/// </summary>
public delegate void ContextMenuItemSelectedDelegate(ContextMenuItemSelectedArgs args);
/// <summary>
/// The delegate that is run when an inventory context menu item is selected.
/// </summary>
public delegate void InventoryContextMenuItemSelectedDelegate(InventoryContextMenuItemSelectedArgs args);
private delegate IntPtr SomeOpenAddonThingDelegate(IntPtr a1, IntPtr a2, IntPtr a3, uint a4, IntPtr a5, IntPtr a6, IntPtr a7, ushort a8);
private Hook<SomeOpenAddonThingDelegate>? SomeOpenAddonThingHook { get; }
private unsafe delegate byte ContextMenuOpenDelegate(IntPtr addon, int menuSize, AtkValue* atkValueArgs);
private delegate IntPtr GetAddonByInternalIdDelegate(IntPtr raptureAtkUnitManager, short id);
private readonly GetAddonByInternalIdDelegate _getAddonByInternalId = null!;
private Hook<ContextMenuOpenDelegate>? ContextMenuOpenHook { get; }
private Hook<ContextMenuOpenDelegate>? TitleContextMenuOpenHook { get; }
private delegate byte ContextMenuItemSelectedInternalDelegate(IntPtr addon, int index, byte a3);
private Hook<ContextMenuItemSelectedInternalDelegate>? ContextMenuItemSelectedHook { get; }
private delegate byte SetUpContextSubMenuDelegate(IntPtr agent);
private readonly SetUpContextSubMenuDelegate _setUpContextSubMenu = null!;
private delegate IntPtr SetUpInventoryContextSubMenuDelegate(IntPtr agent, byte hasTitle, byte zero);
private readonly SetUpInventoryContextSubMenuDelegate _setUpInventoryContextSubMenu = null!;
private delegate byte ContextMenuEvent66Delegate(IntPtr agent);
private Hook<ContextMenuEvent66Delegate>? ContextMenuEvent66Hook { get; }
private delegate void InventoryContextMenuEvent30Delegate(IntPtr agent, IntPtr a2, int a3, int a4, short a5);
private Hook<InventoryContextMenuEvent30Delegate>? InventoryContextMenuEvent30Hook { get; }
private unsafe delegate void AtkValueChangeTypeDelegate(AtkValue* thisPtr, ValueType type);
private readonly AtkValueChangeTypeDelegate _atkValueChangeType = null!;
private unsafe delegate void AtkValueSetStringDelegate(AtkValue* thisPtr, byte* bytes);
private readonly AtkValueSetStringDelegate _atkValueSetString = null!;
private GameFunctions Functions { get; }
private ClientLanguage Language { get; }
private IntPtr Agent { get; set; } = IntPtr.Zero;
private List<BaseContextMenuItem> Items { get; } = new();
private int NormalSize { get; set; }
internal ContextMenu(GameFunctions functions, SigScanner scanner, ClientLanguage language, Hooks hooks) {
this.Functions = functions;
this.Language = language;
if (!hooks.HasFlag(Hooks.ContextMenu)) {
return;
}
if (scanner.TryScanText(Signatures.AtkValueChangeType, out var changeTypePtr, "Context Menu (change type)")) {
this._atkValueChangeType = Marshal.GetDelegateForFunctionPointer<AtkValueChangeTypeDelegate>(changeTypePtr);
} else {
return;
}
if (scanner.TryScanText(Signatures.AtkValueSetString, out var setStringPtr, "Context Menu (set string)")) {
this._atkValueSetString = Marshal.GetDelegateForFunctionPointer<AtkValueSetStringDelegate>(setStringPtr);
} else {
return;
}
if (scanner.TryScanText(Signatures.GetAddonByInternalId, out var getAddonPtr, "Context Menu (get addon)")) {
this._getAddonByInternalId = Marshal.GetDelegateForFunctionPointer<GetAddonByInternalIdDelegate>(getAddonPtr);
} else {
return;
}
if (scanner.TryScanText(Signatures.SetUpContextSubMenu, out var setUpSubPtr, "Context Menu (set up submenu)")) {
this._setUpContextSubMenu = Marshal.GetDelegateForFunctionPointer<SetUpContextSubMenuDelegate>(setUpSubPtr);
} else {
return;
}
// TODO: uncomment when inv submenus
// if (scanner.TryScanText(Signatures.SetUpInventoryContextSubMenu, out var setUpInvSubPtr, "Context Menu (set up inventory submenu)")) {
// this._setUpInventoryContextSubMenu = Marshal.GetDelegateForFunctionPointer<SetUpInventoryContextSubMenuDelegate>(setUpInvSubPtr);
// } else {
// return;
// }
if (scanner.TryScanText(Signatures.SomeOpenAddonThing, out var thingPtr, "Context Menu (some OpenAddon thing)")) {
this.SomeOpenAddonThingHook = new Hook<SomeOpenAddonThingDelegate>(thingPtr, this.SomeOpenAddonThingDetour);
this.SomeOpenAddonThingHook.Enable();
} else {
return;
}
if (scanner.TryScanText(Signatures.ContextMenuOpen, out var openPtr, "Context Menu open")) {
unsafe {
this.ContextMenuOpenHook = new Hook<ContextMenuOpenDelegate>(openPtr, this.OpenMenuDetour);
}
this.ContextMenuOpenHook.Enable();
} else {
return;
}
if (scanner.TryScanText(Signatures.ContextMenuSelected, out var selectedPtr, "Context Menu selected")) {
this.ContextMenuItemSelectedHook = new Hook<ContextMenuItemSelectedInternalDelegate>(selectedPtr, this.ItemSelectedDetour);
this.ContextMenuItemSelectedHook.Enable();
}
if (scanner.TryScanText(Signatures.TitleContextMenuOpen, out var titleOpenPtr, "Context Menu (title menu open)")) {
unsafe {
this.TitleContextMenuOpenHook = new Hook<ContextMenuOpenDelegate>(titleOpenPtr, this.TitleContextMenuOpenDetour);
}
this.TitleContextMenuOpenHook.Enable();
}
if (scanner.TryScanText(Signatures.ContextMenuEvent66, out var event66Ptr, "Context Menu (event 66)")) {
this.ContextMenuEvent66Hook = new Hook<ContextMenuEvent66Delegate>(event66Ptr, this.ContextMenuEvent66Detour);
this.ContextMenuEvent66Hook.Enable();
}
// TODO: uncomment when inv submenus
// if (scanner.TryScanText(Signatures.InventoryContextMenuEvent30, out var event30Ptr, "Context Menu (inv event 30)")) {
// this.InventoryContextMenuEvent30Hook = new Hook<InventoryContextMenuEvent30Delegate>(event30Ptr, new InventoryContextMenuEvent30Delegate(this.InventoryContextMenuEvent30Detour));
// this.InventoryContextMenuEvent30Hook.Enable();
// }
}
/// <inheritdoc />
public void Dispose() {
this.SomeOpenAddonThingHook?.Dispose();
this.ContextMenuOpenHook?.Dispose();
this.TitleContextMenuOpenHook?.Dispose();
this.ContextMenuItemSelectedHook?.Dispose();
this.ContextMenuEvent66Hook?.Dispose();
this.InventoryContextMenuEvent30Hook?.Dispose();
}
private IntPtr SomeOpenAddonThingDetour(IntPtr a1, IntPtr a2, IntPtr a3, uint a4, IntPtr a5, IntPtr a6, IntPtr a7, ushort a8) {
this.Agent = a6;
return this.SomeOpenAddonThingHook!.Original(a1, a2, a3, a4, a5, a6, a7, a8);
}
private unsafe byte TitleContextMenuOpenDetour(IntPtr addon, int menuSize, AtkValue* atkValueArgs) {
if (this.SubMenuTitle == IntPtr.Zero) {
this.Items.Clear();
}
return this.TitleContextMenuOpenHook!.Original(addon, menuSize, atkValueArgs);
}
private enum AgentType {
Normal,
Inventory,
Unknown,
}
private unsafe (AgentType agentType, IntPtr agent) GetContextMenuAgent(IntPtr? agent = null) {
agent ??= this.Agent;
IntPtr GetAgent(AgentId id) {
return (IntPtr) Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId(id);
}
var agentType = AgentType.Unknown;
if (agent == GetAgent(AgentId.Context)) {
agentType = AgentType.Normal;
} else if (agent == GetAgent(AgentId.InventoryContext)) {
agentType = AgentType.Inventory;
}
return (agentType, agent.Value);
}
private unsafe string? GetParentAddonName(IntPtr addon) {
var parentAddonId = Marshal.ReadInt16(addon + ParentAddonIdOffset);
if (parentAddonId == 0) {
return null;
}
var stage = AtkStage.GetSingleton();
var parentAddon = this._getAddonByInternalId((IntPtr) stage->RaptureAtkUnitManager, parentAddonId);
return Encoding.UTF8.GetString(Util.ReadTerminated(parentAddon + 8));
}
private unsafe IntPtr GetAddonFromAgent(IntPtr agent) {
var addonId = *(byte*) (agent + 0x20);
if (addonId == 0) {
return IntPtr.Zero;
}
var stage = AtkStage.GetSingleton();
return this._getAddonByInternalId((IntPtr) stage->RaptureAtkUnitManager, addonId);
}
private unsafe (uint objectId, uint contentIdLower, SeString? text, ushort objectWorld) GetAgentInfo(IntPtr agent) {
var objectId = *(uint*) (agent + ObjectIdOffset);
var contentIdLower = *(uint*) (agent + ContentIdLowerOffset);
var textBytes = Util.ReadTerminated(Marshal.ReadIntPtr(agent + TextPointerOffset));
var text = textBytes.Length == 0 ? null : SeString.Parse(textBytes);
var objectWorld = *(ushort*) (agent + WorldOffset);
return (objectId, contentIdLower, text, objectWorld);
}
private static unsafe (uint itemId, uint itemAmount, bool itemHq) GetInventoryAgentInfo(IntPtr agent) {
var itemId = *(uint*) (agent + ItemIdOffset);
var itemAmount = *(uint*) (agent + ItemAmountOffset);
var itemHq = *(byte*) (agent + ItemHqOffset) == 1;
return (itemId, itemAmount, itemHq);
}
[HandleProcessCorruptedStateExceptions]
private unsafe byte OpenMenuDetour(IntPtr addon, int menuSize, AtkValue* atkValueArgs) {
try {
this.OpenMenuDetourInner(addon, ref menuSize, ref atkValueArgs);
} catch (Exception ex) {
Logger.LogError(ex, "Exception in OpenMenuDetour");
}
return this.ContextMenuOpenHook!.Original(addon, menuSize, atkValueArgs);
}
private unsafe AtkValue* ExpandContextMenuArray(IntPtr addon) {
const ulong newItemCount = MaxItems * 2 + ContextMenuItemOffset;
var oldArray = *(AtkValue**) (addon + AddonArrayOffset);
var oldArrayItemCount = *(ushort*) (addon + AddonArraySizeOffset);
// if the array has enough room, don't reallocate
if (oldArrayItemCount >= newItemCount) {
return oldArray;
}
// reallocate
var size = (ulong) sizeof(AtkValue) * newItemCount + 8;
var newArray = this.Functions.UiAlloc.Alloc(size);
// zero new memory
Marshal.Copy(new byte[size], 0, newArray, (int) size);
// update size and pointer
*(ulong*) newArray = newItemCount;
*(void**) (addon + AddonArrayOffset) = (void*) (newArray + 8);
*(ushort*) (addon + AddonArraySizeOffset) = (ushort) newItemCount;
// copy old memory if existing
if (oldArray != null) {
Buffer.MemoryCopy(oldArray, (void*) (newArray + 8), size, (ulong) sizeof(AtkValue) * oldArrayItemCount);
this.Functions.UiAlloc.Free((IntPtr) oldArray - 8);
}
return (AtkValue*) (newArray + 8);
}
private unsafe void OpenMenuDetourInner(IntPtr addon, ref int menuSize, ref AtkValue* atkValueArgs) {
this.Items.Clear();
this.FreeSubMenuTitle();
var (agentType, agent) = this.GetContextMenuAgent();
if (agent == IntPtr.Zero) {
return;
}
if (agentType == AgentType.Unknown) {
return;
}
atkValueArgs = this.ExpandContextMenuArray(addon);
var inventory = agentType == AgentType.Inventory;
var offset = ContextMenuItemOffset + (inventory ? 0 : *(long*) (agent + BooleanOffsetCheck) != 0 ? 1 : 0);
this.NormalSize = (int) (&atkValueArgs[0])->UInt;
// idx 3 is bitmask of indices that are submenus
var submenuArg = &atkValueArgs[3];
var submenus = (int) submenuArg->UInt;
var hasGameDisabled = menuSize - offset - this.NormalSize > 0;
var menuActions = inventory
? (byte*) (agent + InventoryMenuActionsOffset)
: (byte*) (Marshal.ReadIntPtr(agent + MenuActionsPointerOffset) + MenuActionsOffset);
var nativeItems = new List<NativeContextMenuItem>();
for (var i = 0; i < this.NormalSize; i++) {
var atkItem = &atkValueArgs[offset + i];
var name = Util.ReadSeString((IntPtr) atkItem->String);
var enabled = true;
if (hasGameDisabled) {
var disabledItem = &atkValueArgs[offset + this.NormalSize + i];
enabled = disabledItem->Int == 0;
}
var action = *(menuActions + offset + i);
var isSubMenu = (submenus & (1 << i)) > 0;
nativeItems.Add(new NativeContextMenuItem(action, name, enabled, isSubMenu));
}
if (this.PopulateItems(addon, agent, this.OpenContextMenu, this.OpenInventoryContextMenu, nativeItems)) {
return;
}
var hasCustomDisabled = this.Items.Any(item => !item.Enabled);
var hasAnyDisabled = hasGameDisabled || hasCustomDisabled;
// clear all submenu flags
submenuArg->UInt = 0;
for (var i = 0; i < this.Items.Count; i++) {
var item = this.Items[i];
if (hasAnyDisabled) {
var disabledArg = &atkValueArgs[offset + this.Items.Count + i];
this._atkValueChangeType(disabledArg, ValueType.Int);
disabledArg->Int = item.Enabled ? 0 : 1;
}
// set up the agent to take the appropriate action for this item
*(menuActions + offset + i) = item switch {
NativeContextMenuItem nativeItem => nativeItem.InternalAction,
NormalContextSubMenuItem => ContextSubId,
// TODO: uncomment when inv submenus
// InventoryContextSubMenuItem => InventoryContextSubId,
_ => inventory ? InventoryNoopContextId : NoopContextId,
};
// set submenu flag
if (item.IsSubMenu) {
submenuArg->UInt |= (uint) (1 << i);
}
// set up the menu item
var newItem = &atkValueArgs[offset + i];
this._atkValueChangeType(newItem, ValueType.String);
var name = this.GetItemName(item);
fixed (byte* nameBytesPtr = name.Encode().Terminate()) {
this._atkValueSetString(newItem, nameBytesPtr);
}
}
(&atkValueArgs[0])->UInt = (uint) this.Items.Count;
menuSize = (int) (&atkValueArgs[0])->UInt;
if (hasAnyDisabled) {
menuSize *= 2;
}
menuSize += offset;
}
/// <returns>true on error</returns>
private bool PopulateItems(IntPtr addon, IntPtr agent, ContextMenuOpenEventDelegate? normalAction, InventoryContextMenuOpenEventDelegate? inventoryAction, IReadOnlyCollection<NativeContextMenuItem>? nativeItems = null) {
var (agentType, _) = this.GetContextMenuAgent(agent);
if (agentType == AgentType.Unknown) {
return true;
}
var inventory = agentType == AgentType.Inventory;
var parentAddonName = this.GetParentAddonName(addon);
if (inventory) {
var info = GetInventoryAgentInfo(agent);
var args = new InventoryContextMenuOpenArgs(
addon,
agent,
parentAddonName,
info.itemId,
info.itemAmount,
info.itemHq
);
if (nativeItems != null) {
args.Items.AddRange(nativeItems);
}
try {
inventoryAction?.Invoke(args);
} catch (Exception ex) {
Logger.LogError(ex, "Exception in OpenMenuDetour");
return true;
}
// remove any NormalContextMenuItems that may have been added - these will crash the game
args.Items.RemoveAll(item => item is NormalContextMenuItem);
// set the agent of any remaining custom items
foreach (var item in args.Items) {
switch (item) {
case InventoryContextMenuItem custom:
custom.Agent = agent;
break;
case InventoryContextSubMenuItem custom:
custom.Agent = agent;
break;
}
}
this.Items.AddRange(args.Items);
} else {
var info = this.GetAgentInfo(agent);
var args = new ContextMenuOpenArgs(
addon,
agent,
parentAddonName,
info.objectId,
info.contentIdLower,
info.text,
info.objectWorld
);
if (nativeItems != null) {
args.Items.AddRange(nativeItems);
}
try {
normalAction?.Invoke(args);
} catch (Exception ex) {
Logger.LogError(ex, "Exception in OpenMenuDetour");
return true;
}
// remove any InventoryContextMenuItems that may have been added - these will crash the game
args.Items.RemoveAll(item => item is InventoryContextMenuItem);
// set the agent of any remaining custom items
foreach (var item in args.Items) {
switch (item) {
case NormalContextMenuItem custom:
custom.Agent = agent;
break;
case NormalContextSubMenuItem custom:
custom.Agent = agent;
break;
}
}
this.Items.AddRange(args.Items);
}
if (this.Items.Count > MaxItems) {
var toRemove = this.Items.Count - MaxItems;
this.Items.RemoveRange(MaxItems, toRemove);
Logger.LogWarning($"Context menu item limit ({MaxItems}) exceeded. Removing {toRemove} item(s).");
}
return false;
}
private SeString GetItemName(BaseContextMenuItem item) {
return item switch {
NormalContextMenuItem custom => this.Language switch {
ClientLanguage.Japanese => custom.NameJapanese,
ClientLanguage.English => custom.NameEnglish,
ClientLanguage.German => custom.NameGerman,
ClientLanguage.French => custom.NameFrench,
_ => custom.NameEnglish,
},
InventoryContextMenuItem custom => this.Language switch {
ClientLanguage.Japanese => custom.NameJapanese,
ClientLanguage.English => custom.NameEnglish,
ClientLanguage.German => custom.NameGerman,
ClientLanguage.French => custom.NameFrench,
_ => custom.NameEnglish,
},
NormalContextSubMenuItem custom => this.Language switch {
ClientLanguage.Japanese => custom.NameJapanese,
ClientLanguage.English => custom.NameEnglish,
ClientLanguage.German => custom.NameGerman,
ClientLanguage.French => custom.NameFrench,
_ => custom.NameEnglish,
},
InventoryContextSubMenuItem custom => this.Language switch {
ClientLanguage.Japanese => custom.NameJapanese,
ClientLanguage.English => custom.NameEnglish,
ClientLanguage.German => custom.NameGerman,
ClientLanguage.French => custom.NameFrench,
_ => custom.NameEnglish,
},
NativeContextMenuItem native => native.Name,
_ => "Invalid context menu item",
};
}
private BaseContextMenuItem? SubMenuItem { get; set; }
private byte ItemSelectedDetour(IntPtr addon, int index, byte a3) {
this.FreeSubMenuTitle();
if (index < 0 || index >= this.Items.Count) {
goto Original;
}
var item = this.Items[index];
switch (item) {
case NormalContextSubMenuItem sub: {
this.SubMenuItem = sub;
break;
}
case InventoryContextSubMenuItem sub: {
this.SubMenuItem = sub;
break;
}
case NormalContextMenuItem custom: {
var addonName = this.GetParentAddonName(addon);
var info = this.GetAgentInfo(custom.Agent);
var args = new ContextMenuItemSelectedArgs(
addon,
custom.Agent,
addonName,
info.objectId,
info.contentIdLower,
info.text,
info.objectWorld
);
try {
custom.Action(args);
} catch (Exception ex) {
Logger.LogError(ex, "Exception in custom context menu item");
}
break;
}
case InventoryContextMenuItem custom: {
var addonName = this.GetParentAddonName(addon);
var info = GetInventoryAgentInfo(custom.Agent);
var args = new InventoryContextMenuItemSelectedArgs(
addon,
custom.Agent,
addonName,
info.itemId,
info.itemAmount,
info.itemHq
);
try {
custom.Action(args);
} catch (Exception ex) {
Logger.LogError(ex, "Exception in custom context menu item");
}
break;
}
}
Original:
return this.ContextMenuItemSelectedHook!.Original(addon, index, a3);
}
private IntPtr SubMenuTitle { get; set; } = IntPtr.Zero;
private void FreeSubMenuTitle() {
if (this.SubMenuTitle == IntPtr.Zero) {
return;
}
this.Functions.UiAlloc.Free(this.SubMenuTitle);
this.SubMenuTitle = IntPtr.Zero;
}
/// <returns>false if original should be called</returns>
private unsafe bool SubMenuInner(IntPtr agent) {
if (this.SubMenuItem == null) {
return false;
}
var subMenuItem = this.SubMenuItem;
this.SubMenuItem = null;
// free our workaround pointer
this.FreeSubMenuTitle();
this.Items.Clear();
var name = this.GetItemName(subMenuItem);
// Since the game checks the agent's AtkValue array for the submenu title, and since we
// don't update that array, we need to work around this check.
// First, we will convince the game to make the submenu title pointer null by telling it
// that an invalid index was selected.
// Second, we will replace the null pointer with our own pointer.
// Third, we will restore the original selected index.
// step 1
var selectedIdx = (byte*) (agent + 0x670);
var wasSelected = *selectedIdx;
*selectedIdx = 0xFF;
this._setUpContextSubMenu(agent);
// step 2 (see SetUpContextSubMenu)
var nameBytes = name.Encode().Terminate();
this.SubMenuTitle = this.Functions.UiAlloc.Alloc((ulong) nameBytes.Length);
Marshal.Copy(nameBytes, 0, this.SubMenuTitle, nameBytes.Length);
var v10 = agent + 0x678 * *(byte*) (agent + 0x1740) + 0x28;
*(byte**) (v10 + 0x668) = (byte*) this.SubMenuTitle;
// step 3
*selectedIdx = wasSelected;
var secondaryArgsPtr = Marshal.ReadIntPtr(agent + MenuActionsPointerOffset);
var submenuArgs = (AtkValue*) (secondaryArgsPtr + 8);
var addon = this.GetAddonFromAgent(agent);
var normalAction = (subMenuItem as NormalContextSubMenuItem)?.Action;
var inventoryAction = (subMenuItem as InventoryContextSubMenuItem)?.Action;
if (this.PopulateItems(addon, agent, normalAction, inventoryAction)) {
return true;
}
var booleanOffset = *(long*) (agent + *(byte*) (agent + 0x1740) * 0x678 + 0x690) != 0 ? 1 : 0;
for (var i = 0; i < this.Items.Count; i++) {
var item = this.Items[i];
*(ushort*) secondaryArgsPtr += 1;
var arg = &submenuArgs[ContextMenuItemOffset + i + 1];
this._atkValueChangeType(arg, ValueType.String);
var itemName = this.GetItemName(item);
fixed (byte* namePtr = itemName.Encode().Terminate()) {
this._atkValueSetString(arg, namePtr);
}
// set action to no-op
*(byte*) (secondaryArgsPtr + booleanOffset + i + ContextMenuItemOffset + 0x428) = NoopContextId;
}
return true;
}
private byte ContextMenuEvent66Detour(IntPtr agent) {
return this.SubMenuInner(agent) ? (byte) 0 : this.ContextMenuEvent66Hook!.Original(agent);
}
private void InventoryContextMenuEvent30Detour(IntPtr agent, IntPtr a2, int a3, int a4, short a5) {
if (!this.SubMenuInner(agent)) {
this.InventoryContextMenuEvent30Hook!.Original(agent, a2, a3, a4, a5);
}
}
}
}

View File

@ -1,12 +0,0 @@
using System;
using Dalamud.Game.Text.SeStringHandling;
namespace XivCommon.Functions.ContextMenu {
/// <summary>
/// Arguments for the context menu item selected delegate.
/// </summary>
public class ContextMenuItemSelectedArgs : BaseContextMenuArgs {
internal ContextMenuItemSelectedArgs(IntPtr addon, IntPtr agent, string? parentAddonName, uint objectId, uint contentIdLower, SeString? text, ushort objectWorld) : base(addon, agent, parentAddonName, objectId, contentIdLower, text, objectWorld) {
}
}
}

View File

@ -1,18 +0,0 @@
using System;
using System.Collections.Generic;
using Dalamud.Game.Text.SeStringHandling;
namespace XivCommon.Functions.ContextMenu {
/// <summary>
/// Arguments for the context menu event.
/// </summary>
public class ContextMenuOpenArgs : BaseContextMenuArgs {
/// <summary>
/// Context menu items in this menu.
/// </summary>
public List<BaseContextMenuItem> Items { get; } = new();
internal ContextMenuOpenArgs(IntPtr addon, IntPtr agent, string? parentAddonName, uint objectId, uint contentIdLower, SeString? text, ushort objectWorld) : base(addon, agent, parentAddonName, objectId, contentIdLower, text, objectWorld) {
}
}
}

View File

@ -1,51 +0,0 @@
using System;
using Dalamud.Game.Text.SeStringHandling;
namespace XivCommon.Functions.ContextMenu {
/// <summary>
/// A custom context menu item
/// </summary>
public abstract class CustomContextMenuItem<T> : BaseContextMenuItem
where T : Delegate {
internal IntPtr Agent { get; set; }
/// <summary>
/// The name of the context item to be shown for English clients.
/// </summary>
public SeString NameEnglish { get; set; }
/// <summary>
/// The name of the context item to be shown for Japanese clients.
/// </summary>
public SeString NameJapanese { get; set; }
/// <summary>
/// The name of the context item to be shown for French clients.
/// </summary>
public SeString NameFrench { get; set; }
/// <summary>
/// The name of the context item to be shown for German clients.
/// </summary>
public SeString NameGerman { get; set; }
/// <summary>
/// The action to perform when this item is clicked.
/// </summary>
public T Action { get; set; }
/// <summary>
/// Create a new context menu item.
/// </summary>
/// <param name="name">the English name of the item, copied to other languages</param>
/// <param name="action">the action to perform on click</param>
internal CustomContextMenuItem(SeString name, T action) {
this.NameEnglish = name;
this.NameJapanese = name;
this.NameFrench = name;
this.NameGerman = name;
this.Action = action;
}
}
}

View File

@ -1,47 +0,0 @@
using System;
namespace XivCommon.Functions.ContextMenu.Inventory {
/// <summary>
/// The base class for inventory context menu arguments
/// </summary>
public abstract class BaseInventoryContextMenuArgs {
/// <summary>
/// Pointer to the context menu addon.
/// </summary>
public IntPtr Addon { get; }
/// <summary>
/// Pointer to the context menu agent.
/// </summary>
public IntPtr Agent { get; }
/// <summary>
/// The name of the addon containing this context menu, if any.
/// </summary>
public string? ParentAddonName { get; }
/// <summary>
/// The ID of the item this context menu is for.
/// </summary>
public uint ItemId { get; }
/// <summary>
/// The amount of the item this context menu is for.
/// </summary>
public uint ItemAmount { get; }
/// <summary>
/// If the item this context menu is for is high-quality.
/// </summary>
public bool ItemHq { get; }
internal BaseInventoryContextMenuArgs(IntPtr addon, IntPtr agent, string? parentAddonName, uint itemId, uint itemAmount, bool itemHq) {
this.Addon = addon;
this.Agent = agent;
this.ParentAddonName = parentAddonName;
this.ItemId = itemId;
this.ItemAmount = itemAmount;
this.ItemHq = itemHq;
}
}
}

View File

@ -1,16 +0,0 @@
using Dalamud.Game.Text.SeStringHandling;
namespace XivCommon.Functions.ContextMenu.Inventory {
/// <summary>
/// A custom context menu item for inventory items.
/// </summary>
public class InventoryContextMenuItem : CustomContextMenuItem<ContextMenu.InventoryContextMenuItemSelectedDelegate> {
/// <summary>
/// Create a new context menu item for inventory items.
/// </summary>
/// <param name="name">the English name of the item, copied to other languages</param>
/// <param name="action">the action to perform on click</param>
public InventoryContextMenuItem(SeString name, ContextMenu.InventoryContextMenuItemSelectedDelegate action) : base(name, action) {
}
}
}

View File

@ -1,11 +0,0 @@
using System;
namespace XivCommon.Functions.ContextMenu.Inventory {
/// <summary>
/// The arguments for when an inventory context menu item is selected
/// </summary>
public class InventoryContextMenuItemSelectedArgs : BaseInventoryContextMenuArgs {
internal InventoryContextMenuItemSelectedArgs(IntPtr addon, IntPtr agent, string? parentAddonName, uint itemId, uint itemAmount, bool itemHq) : base(addon, agent, parentAddonName, itemId, itemAmount, itemHq) {
}
}
}

View File

@ -1,17 +0,0 @@
using System;
using System.Collections.Generic;
namespace XivCommon.Functions.ContextMenu.Inventory {
/// <summary>
/// The arguments for when an inventory context menu is opened
/// </summary>
public class InventoryContextMenuOpenArgs : BaseInventoryContextMenuArgs {
/// <summary>
/// Context menu items in this menu.
/// </summary>
public List<BaseContextMenuItem> Items { get; } = new();
internal InventoryContextMenuOpenArgs(IntPtr addon, IntPtr agent, string? parentAddonName, uint itemId, uint itemAmount, bool itemHq) : base(addon, agent, parentAddonName, itemId, itemAmount, itemHq) {
}
}
}

View File

@ -1,18 +0,0 @@
using System;
using Dalamud.Game.Text.SeStringHandling;
namespace XivCommon.Functions.ContextMenu.Inventory {
/// <summary>
/// A custom inventory context menu item that will open a submenu
/// </summary>
public class InventoryContextSubMenuItem : CustomContextMenuItem<ContextMenu.InventoryContextMenuOpenEventDelegate> {
/// <summary>
/// Create a new context menu item for inventory items that will open a submenu.
/// </summary>
/// <param name="name">the English name of the item, copied to other languages</param>
/// <param name="action">the action to perform on click</param>
[Obsolete("Inventory context submenus do not work yet", true)]
public InventoryContextSubMenuItem(SeString name, ContextMenu.InventoryContextMenuOpenEventDelegate action) : base(name, action) {
}
}
}

View File

@ -1,25 +0,0 @@
using Dalamud.Game.Text.SeStringHandling;
namespace XivCommon.Functions.ContextMenu {
/// <summary>
/// A native context menu item
/// </summary>
public sealed class NativeContextMenuItem : BaseContextMenuItem {
/// <summary>
/// The action code to be used in the context menu agent for this item.
/// </summary>
public byte InternalAction { get; }
/// <summary>
/// The name of the context item.
/// </summary>
public SeString Name { get; set; }
internal NativeContextMenuItem(byte action, SeString name, bool enabled, bool isSubMenu) {
this.Name = name;
this.InternalAction = action;
this.Enabled = enabled;
this.IsSubMenu = isSubMenu;
}
}
}

View File

@ -1,16 +0,0 @@
using Dalamud.Game.Text.SeStringHandling;
namespace XivCommon.Functions.ContextMenu {
/// <summary>
/// A custom normal context menu item
/// </summary>
public class NormalContextMenuItem : CustomContextMenuItem<ContextMenu.ContextMenuItemSelectedDelegate> {
/// <summary>
/// Create a new custom context menu item.
/// </summary>
/// <param name="name">the English name of the item, copied to other languages</param>
/// <param name="action">the action to perform on click</param>
public NormalContextMenuItem(SeString name, ContextMenu.ContextMenuItemSelectedDelegate action) : base(name, action) {
}
}
}

View File

@ -1,17 +0,0 @@
using Dalamud.Game.Text.SeStringHandling;
namespace XivCommon.Functions.ContextMenu {
/// <summary>
/// A custom context menu item that will open a submenu
/// </summary>
public class NormalContextSubMenuItem : CustomContextMenuItem<ContextMenu.ContextMenuOpenEventDelegate> {
/// <summary>
/// Create a new custom context menu item that will open a submenu.
/// </summary>
/// <param name="name">the English name of the item, copied to other languages</param>
/// <param name="action">the action to perform on click</param>
public NormalContextSubMenuItem(SeString name, ContextMenu.ContextMenuOpenEventDelegate action) : base(name, action) {
this.IsSubMenu = true;
}
}
}

View File

@ -1,13 +1,11 @@
using System;
using Dalamud.Game;
using Dalamud.Game.ClientState;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.Gui;
using Dalamud.Game.Gui.PartyFinder;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI;
using XivCommon.Functions;
using XivCommon.Functions.ContextMenu;
using XivCommon.Functions.FriendList;
using XivCommon.Functions.Housing;
using XivCommon.Functions.NamePlates;
@ -55,11 +53,6 @@ namespace XivCommon {
/// </summary>
public ChatBubbles ChatBubbles { get; }
/// <summary>
/// Context menu functions
/// </summary>
public ContextMenu ContextMenu { get; }
/// <summary>
/// Tooltip events
/// </summary>
@ -94,7 +87,6 @@ namespace XivCommon {
this.Framework = Util.GetService<Dalamud.Game.Framework>();
this.GameGui = Util.GetService<GameGui>();
var clientState = Util.GetService<ClientState>();
var objectTable = Util.GetService<ObjectTable>();
var partyFinderGui = Util.GetService<PartyFinderGui>();
var scanner = Util.GetService<SigScanner>();
@ -106,7 +98,6 @@ namespace XivCommon {
this.Examine = new Examine(scanner);
this.Talk = new Talk(scanner, hooks.HasFlag(Hooks.Talk));
this.ChatBubbles = new ChatBubbles(objectTable, scanner, hooks.HasFlag(Hooks.ChatBubbles));
this.ContextMenu = new ContextMenu(this, scanner, clientState.ClientLanguage, hooks);
this.Tooltips = new Tooltips(scanner, this.GameGui, hooks.HasFlag(Hooks.Tooltips));
this.NamePlates = new NamePlates(this, scanner, hooks.HasFlag(Hooks.NamePlates));
this.DutyFinder = new DutyFinder(scanner);
@ -119,7 +110,6 @@ namespace XivCommon {
public void Dispose() {
this.NamePlates.Dispose();
this.Tooltips.Dispose();
this.ContextMenu.Dispose();
this.ChatBubbles.Dispose();
this.Talk.Dispose();
this.BattleTalk.Dispose();

View File

@ -58,12 +58,7 @@ namespace XivCommon {
/// </summary>
ChatBubbles = 1 << 5,
/// <summary>
/// The context menu hooks.
///
/// This hook is used in order to enable context menu functions.
/// </summary>
ContextMenu = 1 << 6,
// 1 << 6 used to be ContextMenu
/// <summary>
/// The name plate hooks.