feat(context): add submenus

This commit is contained in:
Anna 2021-06-24 04:26:02 -04:00
parent a4e338d350
commit ed63b4aa49
Signed by: anna
GPG Key ID: 0B391D8F06FCD9E0
6 changed files with 230 additions and 34 deletions

View File

@ -7,5 +7,10 @@
/// 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

@ -21,6 +21,8 @@ namespace XivCommon.Functions.ContextMenu {
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 SetUpContextSubMenu = "E8 ?? ?? ?? ?? 44 39 A3 ?? ?? ?? ?? 0F 86 ?? ?? ?? ??";
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";
@ -46,6 +48,11 @@ namespace XivCommon.Functions.ContextMenu {
/// </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>
@ -130,6 +137,14 @@ namespace XivCommon.Functions.ContextMenu {
private Hook<ContextMenuItemSelectedInternalDelegate>? ContextMenuItemSelectedHook { get; }
private delegate byte SetUpContextSubMenuDelegate(IntPtr agent);
private readonly SetUpContextSubMenuDelegate _setUpContextSubMenu = null!;
private delegate byte ContextMenuEvent66Delegate(IntPtr agent);
private Hook<ContextMenuEvent66Delegate>? ContextMenuEvent66Hook { get; }
private unsafe delegate void AtkValueChangeTypeDelegate(AtkValue* thisPtr, ValueType type);
private readonly AtkValueChangeTypeDelegate _atkValueChangeType = null!;
@ -172,6 +187,12 @@ namespace XivCommon.Functions.ContextMenu {
return;
}
if (scanner.TryScanText(Signatures.SetUpContextSubMenu, out var setUpSubPtr, "Context Menu (set up submenu)")) {
this._setUpContextSubMenu = Marshal.GetDelegateForFunctionPointer<SetUpContextSubMenuDelegate>(setUpSubPtr);
} 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();
@ -201,6 +222,11 @@ namespace XivCommon.Functions.ContextMenu {
this.TitleContextMenuOpenHook.Enable();
}
if (scanner.TryScanText(Signatures.ContextMenuEvent66, out var event66Ptr, "Context Menu (event 66)")) {
this.ContextMenuEvent66Hook = new Hook<ContextMenuEvent66Delegate>(event66Ptr, new ContextMenuEvent66Delegate(this.ContextMenuEvent66Detour));
this.ContextMenuEvent66Hook.Enable();
}
}
/// <inheritdoc />
@ -209,6 +235,7 @@ namespace XivCommon.Functions.ContextMenu {
this.ContextMenuOpenHook?.Dispose();
this.TitleContextMenuOpenHook?.Enable();
this.ContextMenuItemSelectedHook?.Dispose();
this.ContextMenuEvent66Hook?.Dispose();
}
private IntPtr SomeOpenAddonThingDetour(IntPtr a1, IntPtr a2, IntPtr a3, uint a4, IntPtr a5, IntPtr a6, IntPtr a7, ushort a8) {
@ -217,7 +244,10 @@ namespace XivCommon.Functions.ContextMenu {
}
private unsafe byte TitleContextMenuOpenDetour(IntPtr addon, int menuSize, AtkValue* atkValueArgs) {
this.Items.Clear();
if (this.SubMenuTitle == IntPtr.Zero) {
this.Items.Clear();
}
return this.TitleContextMenuOpenHook!.Original(addon, menuSize, atkValueArgs);
}
@ -308,6 +338,7 @@ namespace XivCommon.Functions.ContextMenu {
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) {
@ -321,10 +352,15 @@ namespace XivCommon.Functions.ContextMenu {
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;
var hasGameDisabled = menuSize - ContextMenuItemOffset - this.NormalSize > 0;
// 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 addonName = this.GetParentAddonName(addon);
@ -334,19 +370,21 @@ namespace XivCommon.Functions.ContextMenu {
var nativeItems = new List<NativeContextMenuItem>();
for (var i = 0; i < this.NormalSize; i++) {
var atkItem = &atkValueArgs[ContextMenuItemOffset + i];
var atkItem = &atkValueArgs[offset + i];
var name = Util.ReadSeString((IntPtr) atkItem->String, this.SeStringManager);
var enabled = true;
if (hasGameDisabled) {
var disabledItem = &atkValueArgs[ContextMenuItemOffset + this.NormalSize + i];
var disabledItem = &atkValueArgs[offset + this.NormalSize + i];
enabled = disabledItem->Int == 0;
}
var action = *(menuActions + ContextMenuItemOffset + i);
var action = *(menuActions + offset + i);
nativeItems.Add(new NativeContextMenuItem(action, name, enabled));
var isSubMenu = (submenus & (1 << i)) > 0;
nativeItems.Add(new NativeContextMenuItem(action, name, enabled, isSubMenu));
}
if (inventory) {
@ -423,45 +461,36 @@ namespace XivCommon.Functions.ContextMenu {
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[ContextMenuItemOffset + this.Items.Count + i];
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 + ContextMenuItemOffset + i) = item switch {
*(menuActions + offset + i) = item switch {
NativeContextMenuItem nativeItem => nativeItem.InternalAction,
ContextSubMenuItem => 0x66,
_ => inventory ? InventoryNoopContextId : NoopContextId,
};
// set submenu flag
if (item.IsSubMenu) {
submenuArg->UInt |= (uint) (1 << i);
}
// set up the menu item
var newItem = &atkValueArgs[ContextMenuItemOffset + i];
var newItem = &atkValueArgs[offset + i];
this._atkValueChangeType(newItem, ValueType.String);
var name = 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,
},
NativeContextMenuItem native => native.Name,
_ => "Invalid context menu item",
};
var nameBytes = name.Encode().Terminate();
fixed (byte* nameBytesPtr = nameBytes) {
var name = this.GetItemName(item);
fixed (byte* nameBytesPtr = name.Encode().Terminate()) {
this._atkValueSetString(newItem, nameBytesPtr);
}
}
@ -473,16 +502,52 @@ namespace XivCommon.Functions.ContextMenu {
menuSize *= 2;
}
menuSize += ContextMenuItemOffset;
menuSize += offset;
}
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,
},
ContextSubMenuItem 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 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 ContextSubMenuItem sub: {
this.SubMenuItem = sub;
break;
}
// a custom item is being clicked
case NormalContextMenuItem custom: {
var addonName = this.GetParentAddonName(addon);
@ -532,5 +597,112 @@ namespace XivCommon.Functions.ContextMenu {
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;
}
private unsafe byte ContextMenuEvent66Detour(IntPtr agent) {
if (this.SubMenuItem == null) {
return this.ContextMenuEvent66Hook!.Original(agent);
}
// free our workaround pointer
this.FreeSubMenuTitle();
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,
};
// 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 size = *(ushort*) secondaryArgsPtr;
var info = this.GetAgentInfo(agent);
var args = new ContextMenuOpenArgs(
IntPtr.Zero,
agent,
string.Empty,
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;
}
}
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];
*(ushort*) secondaryArgsPtr += 1;
var arg = &submenuArgs[size + i];
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;
}
} finally {
this.SubMenuItem = null;
}
return 0;
}
}
}

View File

@ -0,0 +1,20 @@
using Dalamud.Game.Text.SeStringHandling;
namespace XivCommon.Functions.ContextMenu {
/// <summary>
/// A custom context menu item that will open a submenu
/// </summary>
public class ContextSubMenuItem : CustomContextMenuItem<ContextMenu.ContextMenuOpenEventDelegate> {
/// <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) {
this.IsSubMenu = true;
}
}
}

View File

@ -15,10 +15,11 @@ namespace XivCommon.Functions.ContextMenu {
/// </summary>
public SeString Name { get; set; }
internal NativeContextMenuItem(byte action, SeString name, bool enabled) {
internal NativeContextMenuItem(byte action, SeString name, bool enabled, bool isSubMenu) {
this.Name = name;
this.InternalAction = action;
this.Enabled = enabled;
this.IsSubMenu = isSubMenu;
}
}
}

View File

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

View File

@ -6,6 +6,7 @@
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>enable</Nullable>
<Version>2.1.1</Version>
<DebugType>full</DebugType>
</PropertyGroup>
<PropertyGroup>