From 085794837f81fdb32141166e0a5adfaf58faffce Mon Sep 17 00:00:00 2001 From: Anna Clemens Date: Tue, 27 Apr 2021 15:49:17 -0400 Subject: [PATCH] feat: add event for context menu --- XivCommon/Functions/ContextMenu.cs | 206 ++++++++++++++++++++--------- 1 file changed, 145 insertions(+), 61 deletions(-) diff --git a/XivCommon/Functions/ContextMenu.cs b/XivCommon/Functions/ContextMenu.cs index 83a714f..d220ffb 100755 --- a/XivCommon/Functions/ContextMenu.cs +++ b/XivCommon/Functions/ContextMenu.cs @@ -40,12 +40,22 @@ namespace XivCommon.Functions { private const int ActorIdOffset = 0xEF0; private const int ContentIdLowerOffset = 0xEE0; + private const int TextPointerOffset = 0xE08; + private const int WorldOffset = 0xF00; + private const int NoopContextId = 0x67; + /// + /// The delegate for context menu events. + /// + public delegate void ContextMenuEventDelegate(ContextMenuArgs args); + + public event ContextMenuEventDelegate? OpenContextMenu; + /// /// The delegate that is run when a context menu item is selected. /// - public delegate void ContextMenuItemSelectedDelegate(IntPtr addon, IntPtr agent, ContextMenuItemSelectedArgs args); + public delegate void ContextMenuItemSelectedDelegate(ContextMenuItemSelectedArgs args); private unsafe delegate byte ContextMenuOpenDelegate(IntPtr addon, int menuSize, AtkValue* atkValueArgs); @@ -69,7 +79,7 @@ namespace XivCommon.Functions { private GameFunctions Functions { get; } private ClientLanguage Language { get; } - private Dictionary> Items { get; } = new(); + private List Items { get; } = new(); private int NormalSize { get; set; } internal ContextMenu(GameFunctions functions, SigScanner scanner, ClientLanguage language, Hooks hooks) { @@ -135,27 +145,51 @@ namespace XivCommon.Functions { return Encoding.UTF8.GetString(Util.ReadTerminated(parentAddon + 8)); } + private static unsafe (uint actorId, uint contentIdLower, string? text, ushort actorWorld) GetAgentInfo(IntPtr agent) { + var actorId = *(uint*) (agent + ActorIdOffset); + var contentIdLower = *(uint*) (agent + ContentIdLowerOffset); + var textBytes = Util.ReadTerminated(Marshal.ReadIntPtr(agent + TextPointerOffset)); + var text = textBytes.Length == 0 ? null : Encoding.UTF8.GetString(textBytes); + var actorWorld = *(ushort*) (agent + WorldOffset); + return (actorId, contentIdLower, text, actorWorld); + } + private unsafe byte OpenMenuDetour(IntPtr addon, int menuSize, AtkValue* atkValueArgs) { - this.NormalSize = menuSize - 7; + this.NormalSize = (int) (&atkValueArgs[0])->UInt; var addonName = this.GetParentAddonName(addon); - if (addonName == null) { - goto Original; - } var agent = this.GetContextMenuAgent(); + var info = GetAgentInfo(agent); + this.Items.Clear(); - if (!this.Items.TryGetValue(addonName, out var registered)) { + var args = new ContextMenuArgs( + addon, + agent, + addonName, + info.actorId, + info.contentIdLower, + info.text, + info.actorWorld + ); + try { + this.OpenContextMenu?.Invoke(args); + } catch (Exception ex) { + PluginLog.LogError(ex, "Exception in OpenMenuDetour"); goto Original; } - foreach (var item in registered) { + this.Items.AddRange(args.AdditionalItems); + + for (var i = 0; i < this.Items.Count; i++) { + var item = this.Items[i]; + // set up the agent to ignore this item var menuActions = (byte*) (Marshal.ReadIntPtr(agent + MenuActionsPointerOffset) + MenuActionsOffset); - *(menuActions + menuSize) = NoopContextId; + *(menuActions + 7 + this.NormalSize + i) = NoopContextId; // set up the new menu item - var newItem = &atkValueArgs[menuSize]; + var newItem = &atkValueArgs[7 + this.NormalSize + i]; this._atkValueChangeType(newItem, ValueType.String); var name = this.Language switch { ClientLanguage.Japanese => item.NameJapanese, @@ -170,38 +204,39 @@ namespace XivCommon.Functions { } // increment the menu size - menuSize += 1; (&atkValueArgs[0])->UInt += 1; } + menuSize = 7 + (int) (&atkValueArgs[0])->UInt; + Original: return this.ContextMenuOpenHook!.Original(addon, menuSize, atkValueArgs); } - private unsafe byte ItemSelectedDetour(IntPtr addon, int index, byte a3) { + private byte ItemSelectedDetour(IntPtr addon, int index, byte a3) { var addonName = this.GetParentAddonName(addon); - if (addonName == null) { - goto Original; - } // a custom item is being clicked if (index >= this.NormalSize) { - if (!this.Items.TryGetValue(addonName, out var registered)) { - goto Original; - } - var idx = index - this.NormalSize; - if (registered.Count <= idx) { + if (this.Items.Count <= idx) { goto Original; } var agent = this.GetContextMenuAgent(); - var actorId = *(uint*) (agent + ActorIdOffset); - var contentIdLower = *(uint*) (agent + ContentIdLowerOffset); + var info = GetAgentInfo(agent); - var item = registered[idx]; + var item = this.Items[idx]; try { - item.Action(addon, agent, new ContextMenuItemSelectedArgs(actorId, contentIdLower)); + item.Action(new ContextMenuItemSelectedArgs( + addon, + agent, + addonName, + info.actorId, + info.contentIdLower, + info.text, + info.actorWorld + )); } catch (Exception ex) { PluginLog.LogError(ex, "Exception in custom context menu item"); } @@ -210,42 +245,6 @@ namespace XivCommon.Functions { Original: return this.ContextMenuItemSelectedHook!.Original(addon, index, a3); } - - /// - /// Register a menu item to appear in a context menu. - /// - /// the addon to show the item in - /// the item to be shown - public void RegisterAction(string addon, ContextMenuItem item) { - if (!this.Items.TryGetValue(addon, out var registered)) { - this.Items[addon] = new List(); - registered = this.Items[addon]; - } - - registered.Add(item); - } - - /// - /// Remove a previously-registered context menu item. - /// - /// the addon the item was registered under - /// the item to be removed - public void UnregisterAction(string addon, ContextMenuItem item) { - this.UnregisterAction(addon, item.Id); - } - - /// - /// Remove a previously-registered context menu item. - /// - /// the addon the item was registered under - /// the id of the item to be removed - public void UnregisterAction(string addon, Guid id) { - if (!this.Items.TryGetValue(addon, out var registered)) { - return; - } - - registered.RemoveAll(item => item.Id == id); - } } /// @@ -301,6 +300,21 @@ namespace XivCommon.Functions { /// Arguments for the context menu item selected delegate. /// public class ContextMenuItemSelectedArgs { + /// + /// Pointer to the context menu addon. + /// + public IntPtr Addon { get; } + + /// + /// Pointer to the context menu agent. + /// + public IntPtr Agent { get; } + + /// + /// The name of the addon containing this context menu, if any. + /// + public string? ParentAddonName { get; } + /// /// The actor ID for this context menu. May be invalid (0xE0000000). /// @@ -311,9 +325,79 @@ namespace XivCommon.Functions { /// public uint ContentIdLower { get; } - internal ContextMenuItemSelectedArgs(uint actorId, uint contentIdLower) { + /// + /// The text related to this context menu, usually an actor name. + /// + public string? Text { get; } + + /// + /// The world of the actor this context menu is for, if any. + /// + public ushort ActorWorld { get; } + + internal ContextMenuItemSelectedArgs(IntPtr addon, IntPtr agent, string? parentAddonName, uint actorId, uint contentIdLower, string? text, ushort actorWorld) { + this.Addon = addon; + this.Agent = agent; + this.ParentAddonName = parentAddonName; this.ContentIdLower = contentIdLower; this.ActorId = actorId; + this.Text = text; + this.ActorWorld = actorWorld; + } + } + + /// + /// Arguments for the context menu event. + /// + public class ContextMenuArgs { + /// + /// Pointer to the context menu addon. + /// + public IntPtr Addon { get; } + + /// + /// Pointer to the context menu agent. + /// + public IntPtr Agent { get; } + + /// + /// The name of the addon containing this context menu, if any. + /// + public string? ParentAddonName { get; } + + /// + /// The actor ID for this context menu. May be invalid (0xE0000000). + /// + public uint ActorId { get; } + + /// + /// The lower half of the content ID of the actor for this context menu. May be zero. + /// + public uint ContentIdLower { get; } + + /// + /// The text related to this context menu, usually an actor name. + /// + public string? Text { get; } + + /// + /// The world of the actor this context menu is for, if any. + /// + public ushort ActorWorld { get; } + + /// + /// Additional context menu items to add to this menu. + /// + public List AdditionalItems { get; } = new(); + + internal ContextMenuArgs(IntPtr addon, IntPtr agent, string? parentAddonName, uint actorId, uint contentIdLower, string? text, ushort actorWorld) { + this.Addon = addon; + this.Agent = agent; + this.ParentAddonName = parentAddonName; + this.ActorId = actorId; + this.ContentIdLower = contentIdLower; + this.Text = text; + this.ActorWorld = actorWorld; } } }