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;
}
}
}