feat(context): begin adding inv submenus

This commit is contained in:
Anna 2021-06-24 19:34:41 -04:00
parent 6e10f72dd8
commit 5a32413c67
Signed by: anna
GPG Key ID: 0B391D8F06FCD9E0
3 changed files with 176 additions and 117 deletions

View File

@ -22,7 +22,9 @@ namespace XivCommon.Functions.ContextMenu {
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";
@ -75,6 +77,8 @@ namespace XivCommon.Functions.ContextMenu {
// 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
@ -141,10 +145,18 @@ namespace XivCommon.Functions.ContextMenu {
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!;
@ -193,6 +205,12 @@ namespace XivCommon.Functions.ContextMenu {
return;
}
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, new SomeOpenAddonThingDelegate(this.SomeOpenAddonThingDetour));
this.SomeOpenAddonThingHook.Enable();
@ -227,6 +245,11 @@ namespace XivCommon.Functions.ContextMenu {
this.ContextMenuEvent66Hook = new Hook<ContextMenuEvent66Delegate>(event66Ptr, new ContextMenuEvent66Delegate(this.ContextMenuEvent66Detour));
this.ContextMenuEvent66Hook.Enable();
}
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 />
@ -236,6 +259,7 @@ namespace XivCommon.Functions.ContextMenu {
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) {
@ -257,15 +281,17 @@ namespace XivCommon.Functions.ContextMenu {
Unknown,
}
private (AgentType agentType, IntPtr agent) GetContextMenuAgent() {
private (AgentType agentType, IntPtr agent) GetContextMenuAgent(IntPtr? agent = null) {
agent ??= this.Agent;
var agentType = AgentType.Unknown;
if (this.Agent == this.Functions.GetAgentByInternalId(9u)) {
if (agent == this.Functions.GetAgentByInternalId(9u)) {
agentType = AgentType.Normal;
} else if (this.Agent == this.Functions.GetAgentByInternalId(10u)) {
} else if (agent == this.Functions.GetAgentByInternalId(10u)) {
agentType = AgentType.Inventory;
}
return (agentType, this.Agent);
return (agentType, agent.Value);
}
private unsafe string? GetParentAddonName(IntPtr addon) {
@ -372,8 +398,6 @@ namespace XivCommon.Functions.ContextMenu {
var hasGameDisabled = menuSize - offset - this.NormalSize > 0;
var addonName = this.GetParentAddonName(addon);
var menuActions = inventory
? (byte*) (agent + InventoryMenuActionsOffset)
: (byte*) (Marshal.ReadIntPtr(agent + MenuActionsPointerOffset) + MenuActionsOffset);
@ -397,75 +421,8 @@ namespace XivCommon.Functions.ContextMenu {
nativeItems.Add(new NativeContextMenuItem(action, name, enabled, isSubMenu));
}
if (inventory) {
var info = GetInventoryAgentInfo(agent);
var args = new InventoryContextMenuOpenArgs(
addon,
agent,
addonName,
info.itemId,
info.itemAmount,
info.itemHq
);
args.Items.AddRange(nativeItems);
try {
this.OpenInventoryContextMenu?.Invoke(args);
} catch (Exception ex) {
Logger.LogError(ex, "Exception in OpenMenuDetour");
return;
}
// 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) {
if (item is InventoryContextMenuItem custom) {
custom.Agent = agent;
}
}
this.Items.AddRange(args.Items);
} else {
var info = this.GetAgentInfo(agent);
var args = new ContextMenuOpenArgs(
addon,
agent,
addonName,
info.actorId,
info.contentIdLower,
info.text,
info.actorWorld
);
args.Items.AddRange(nativeItems);
try {
this.OpenContextMenu?.Invoke(args);
} catch (Exception ex) {
Logger.LogError(ex, "Exception in OpenMenuDetour");
return;
}
// 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) {
if (item is NormalContextMenuItem custom) {
custom.Agent = agent;
}
}
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).");
if (this.PopulateItems(addon, agent, this.OpenContextMenu, this.OpenInventoryContextMenu, nativeItems)) {
return;
}
var hasCustomDisabled = this.Items.Any(item => !item.Enabled);
@ -486,7 +443,8 @@ namespace XivCommon.Functions.ContextMenu {
// set up the agent to take the appropriate action for this item
*(menuActions + offset + i) = item switch {
NativeContextMenuItem nativeItem => nativeItem.InternalAction,
ContextSubMenuItem => 0x66,
ContextSubMenuItem => ContextSubId,
InventoryContextSubMenuItem => InventoryContextSubId,
_ => inventory ? InventoryNoopContextId : NoopContextId,
};
@ -515,6 +473,100 @@ namespace XivCommon.Functions.ContextMenu {
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);
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.actorId,
info.contentIdLower,
info.text,
info.actorWorld
);
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 ContextSubMenuItem 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 {
@ -538,12 +590,19 @@ namespace XivCommon.Functions.ContextMenu {
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 ContextSubMenuItem? SubMenuItem { get; set; }
private BaseContextMenuItem? SubMenuItem { get; set; }
private byte ItemSelectedDetour(IntPtr addon, int index, byte a3) {
this.FreeSubMenuTitle();
@ -558,7 +617,10 @@ namespace XivCommon.Functions.ContextMenu {
this.SubMenuItem = sub;
break;
}
// a custom item is being clicked
case InventoryContextSubMenuItem sub: {
this.SubMenuItem = sub;
break;
}
case NormalContextMenuItem custom: {
var addonName = this.GetParentAddonName(addon);
var info = this.GetAgentInfo(custom.Agent);
@ -619,9 +681,10 @@ namespace XivCommon.Functions.ContextMenu {
this.SubMenuTitle = IntPtr.Zero;
}
private unsafe byte ContextMenuEvent66Detour(IntPtr agent) {
/// <returns>false if original should be called</returns>
private unsafe bool SubMenuInner(IntPtr agent) {
if (this.SubMenuItem == null) {
return this.ContextMenuEvent66Hook!.Original(agent);
return false;
}
// free our workaround pointer
@ -630,15 +693,7 @@ namespace XivCommon.Functions.ContextMenu {
this.Items.Clear();
try {
// this will attempt to read the header from the agent
// we don't currently update the agent with our new items, so let's just work around it
var name = this.Language switch {
ClientLanguage.Japanese => this.SubMenuItem.NameJapanese,
ClientLanguage.English => this.SubMenuItem.NameEnglish,
ClientLanguage.German => this.SubMenuItem.NameGerman,
ClientLanguage.French => this.SubMenuItem.NameFrench,
_ => this.SubMenuItem.NameEnglish,
};
var name = this.GetItemName(this.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.
@ -667,35 +722,17 @@ namespace XivCommon.Functions.ContextMenu {
var submenuArgs = (AtkValue*) (secondaryArgsPtr + 8);
var size = *(ushort*) secondaryArgsPtr;
var info = this.GetAgentInfo(agent);
var addon = this.GetAddonFromAgent(agent);
var args = new ContextMenuOpenArgs(
addon,
agent,
this.GetParentAddonName(addon),
info.actorId,
info.contentIdLower,
info.text,
info.actorWorld
);
this.SubMenuItem.Action(args);
// 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) {
if (item is NormalContextMenuItem custom) {
custom.Agent = agent;
}
var normalAction = (this.SubMenuItem as ContextSubMenuItem)?.Action;
var inventoryAction = (this.SubMenuItem as InventoryContextSubMenuItem)?.Action;
if (this.PopulateItems(addon, agent, normalAction, inventoryAction)) {
return true;
}
this.Items.AddRange(args.Items);
var booleanOffset = *(long*) (agent + *(byte*) (agent + 0x1740) * 0x678 + 0x690) != 0 ? 1 : 0;
for (var i = 0; i < args.Items.Count; i++) {
var item = args.Items[i];
for (var i = 0; i < this.Items.Count; i++) {
var item = this.Items[i];
*(ushort*) secondaryArgsPtr += 1;
var arg = &submenuArgs[size + i];
@ -712,8 +749,17 @@ namespace XivCommon.Functions.ContextMenu {
this.SubMenuItem = null;
}
return true;
}
return 0;
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

@ -8,9 +8,6 @@ namespace XivCommon.Functions.ContextMenu {
/// <summary>
/// Create a new custom context menu item that will open a submenu.
/// </summary>
/// <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 ContextSubMenuItem(SeString name, ContextMenu.ContextMenuOpenEventDelegate action) : base(name, action) {

View File

@ -0,0 +1,16 @@
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>
public InventoryContextSubMenuItem(SeString name, ContextMenu.InventoryContextMenuOpenEventDelegate action) : base(name, action) {
}
}
}