Remove XivCommon; new context menu impl

- Removes XivCommon dependency
- New impl of context menus which aims to fix the issues with the XivCommon implementation
- Some documentation fixes
- Identity fixes
This commit is contained in:
r00telement
2022-01-08 19:14:15 +00:00
parent f17bde6517
commit 927ca813ff
32 changed files with 1947 additions and 319 deletions

View File

@@ -252,7 +252,7 @@ namespace PlayerTags.Configuration
if (identity.WorldId != null)
{
ImGui.SameLine();
ImGui.TextColored(new Vector4(1, 1, 1, 0.25f), $"@{identity.World}");
ImGui.TextColored(new Vector4(1, 1, 1, 0.25f), $"@{identity.WorldName}");
}
ImGui.PopStyleVar();

View File

@@ -4,14 +4,14 @@ using System.Collections.Generic;
namespace PlayerTags.Data
{
public class Identity : IComparable<Identity>
public class Identity : IComparable<Identity>, IEquatable<Identity>
{
public string Name { get; init; }
public uint? WorldId { get; set; } = null;
public List<Guid> CustomTagIds { get; init; } = new List<Guid>();
[JsonIgnore]
public string? World => WorldHelper.GetWorldName(WorldId);
public string? WorldName => WorldHelper.GetWorldName(WorldId);
public Identity(string name)
{
@@ -24,7 +24,7 @@ namespace PlayerTags.Data
if (WorldId != null)
{
str += $"@{World}";
str += $"@{WorldName}";
}
return str;
@@ -32,7 +32,70 @@ namespace PlayerTags.Data
public int CompareTo(Identity? other)
{
return ToString().CompareTo(other != null ? other.ToString() : null);
string? otherToString = null;
if (!(other is null))
{
otherToString = other.ToString();
}
return ToString().CompareTo(otherToString);
}
public override bool Equals(object? obj)
{
return obj is Identity identity && Equals(identity);
}
public bool Equals(Identity? obj)
{
if (obj is null)
{
return false;
}
return this == obj;
}
public static bool operator ==(Identity? first, Identity? second)
{
if (ReferenceEquals(first, second))
{
return true;
}
if (first is null && second is null)
{
return true;
}
if (first is null || second is null)
{
return false;
}
bool areNamesEqual = first.Name.ToLower().Trim() == second.Name.ToLower().Trim();
// If one of the worlds are null then it's technically equal as it could be promoted to the identity that does have a world
bool areWorldsEqual = first.WorldId == null || second.WorldId == null || first.WorldId == second.WorldId;
return areNamesEqual && areWorldsEqual;
}
public static bool operator !=(Identity? first, Identity? second)
{
return !(first == second);
}
public override int GetHashCode()
{
var hashCode = Name.GetHashCode();
if (WorldName != null)
{
hashCode *= 17 ^ WorldName.GetHashCode();
}
return hashCode;
}
}
}

View File

@@ -2,11 +2,11 @@
using Dalamud.Game.ClientState.Party;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using PlayerTags.Configuration;
using PlayerTags.GameInterface.ContextMenus;
using PlayerTags.PluginStrings;
using System;
using System.Collections.Generic;
using System.Linq;
using XivCommon.Functions.ContextMenu;
namespace PlayerTags.Data
{
@@ -277,16 +277,17 @@ namespace PlayerTags.Data
};
}
public Identity? GetIdentity(ContextMenuOpenArgs contextMenuOpenArgs)
public Identity? GetIdentity(ContextMenuOpenedArgs contextMenuOpenedArgs)
{
if (contextMenuOpenArgs.Text == null
|| contextMenuOpenArgs.ObjectWorld == 0
|| contextMenuOpenArgs.ObjectWorld == 65535)
if (contextMenuOpenedArgs.GameObjectContext == null
|| contextMenuOpenedArgs.GameObjectContext.Name == null
|| contextMenuOpenedArgs.GameObjectContext.WorldId == 0
|| contextMenuOpenedArgs.GameObjectContext.WorldId == 65535)
{
return null;
}
return GetIdentity(contextMenuOpenArgs.Text!.TextValue, contextMenuOpenArgs.ObjectWorld);
return GetIdentity(contextMenuOpenedArgs.GameObjectContext.Name.TextValue, contextMenuOpenedArgs.GameObjectContext.WorldId);
}
public Identity GetIdentity(PlayerCharacter playerCharacter)

View File

@@ -3,7 +3,6 @@ using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Logging;
using PlayerTags.Configuration;
using PlayerTags.Data;
using System;
@@ -12,6 +11,9 @@ using System.Linq;
namespace PlayerTags.Features
{
/// <summary>
/// A feature that adds tags to chat messages.
/// </summary>
public class ChatTagTargetFeature : TagTargetFeature
{
/// <summary>
@@ -169,7 +171,7 @@ namespace PlayerTags.Features
}
/// <summary>
/// Adds all configured tags to chat.
/// Adds all configured tags to a chat message.
/// </summary>
/// <param name="message">The message to change.</param>
private void AddTagsToChat(SeString message)

View File

@@ -1,11 +1,11 @@
using Dalamud.Logging;
using PlayerTags.Configuration;
using PlayerTags.Data;
using PlayerTags.GameInterface.ContextMenus;
using PlayerTags.Resources;
using System;
using System.Collections.Generic;
using System.Linq;
using XivCommon;
using XivCommon.Functions.ContextMenu;
namespace PlayerTags.Features
{
@@ -27,84 +27,128 @@ namespace PlayerTags.Features
"SocialList",
};
private XivCommonBase m_XivCommon;
private PluginConfiguration m_PluginConfiguration;
private PluginData m_PluginData;
public CustomTagsContextMenuFeature(XivCommonBase xivCommon, PluginConfiguration pluginConfiguration, PluginData pluginData)
private ContextMenu? m_ContextMenu;
public CustomTagsContextMenuFeature(PluginConfiguration pluginConfiguration, PluginData pluginData)
{
m_XivCommon = xivCommon;
m_PluginConfiguration = pluginConfiguration;
m_PluginData = pluginData;
m_XivCommon.Functions.ContextMenu.OpenContextMenu += ContextMenu_OpenContextMenu;
m_ContextMenu = new ContextMenu();
if (!m_ContextMenu.IsValid)
{
m_ContextMenu = null;
}
if (m_ContextMenu != null)
{
m_ContextMenu.ContextMenuOpened += ContextMenuHooks_ContextMenuOpened;
}
}
public void Dispose()
{
m_XivCommon.Functions.ContextMenu.OpenContextMenu -= ContextMenu_OpenContextMenu;
if (m_ContextMenu != null)
{
m_ContextMenu.ContextMenuOpened -= ContextMenuHooks_ContextMenuOpened;
m_ContextMenu.Dispose();
m_ContextMenu = null;
}
}
private void ContextMenu_OpenContextMenu(ContextMenuOpenArgs args)
private void ContextMenuHooks_ContextMenuOpened(ContextMenuOpenedArgs contextMenuOpenedArgs)
{
if (!m_PluginConfiguration.IsCustomTagsContextMenuEnabled || !DoesContextMenuSupportCustomTags(args))
if (contextMenuOpenedArgs.GameObjectContext != null)
{
PluginLog.Debug($"ContextMenuHooks_ContextMenuOpened {contextMenuOpenedArgs.GameObjectContext?.Id} {contextMenuOpenedArgs.GameObjectContext?.ContentIdLower} '{contextMenuOpenedArgs.GameObjectContext?.Name}' {contextMenuOpenedArgs.GameObjectContext?.WorldId}");
}
if (contextMenuOpenedArgs.ItemContext != null)
{
PluginLog.Debug($"ContextMenuHooks_ContextMenuOpened {contextMenuOpenedArgs.ItemContext?.Id} {contextMenuOpenedArgs.ItemContext?.Count} {contextMenuOpenedArgs.ItemContext?.IsHighQuality}");
}
contextMenuOpenedArgs.ContextMenuItems.Add(new CustomContextMenuItem("Root1", (itemSelectedArgs =>
{
PluginLog.Debug("Executed Root1");
})));
contextMenuOpenedArgs.ContextMenuItems.Add(new OpenSubContextMenuItem("Root2", (subContextMenuOpenedArgs =>
{
PluginLog.Debug("Executed Root2");
List<ContextMenuItem> newContextMenuItems = new List<ContextMenuItem>();
newContextMenuItems.Add(new OpenSubContextMenuItem("Inner1", (subContextMenuOpenedArgs2 =>
{
PluginLog.Debug("Executed Inner1");
List<ContextMenuItem> newContextMenuItems = new List<ContextMenuItem>();
newContextMenuItems.Add(new CustomContextMenuItem("Inner3", (itemSelectedArgs =>
{
PluginLog.Debug("Executed Inner3");
})));
subContextMenuOpenedArgs2.ContextMenuItems.InsertRange(0, newContextMenuItems);
})));
newContextMenuItems.Add(new CustomContextMenuItem("Inner2", (itemSelectedArgs =>
{
PluginLog.Debug("Executed Inner2");
})));
subContextMenuOpenedArgs.ContextMenuItems.InsertRange(0, newContextMenuItems);
})));
if (!m_PluginConfiguration.IsCustomTagsContextMenuEnabled || !SupportedAddonNames.Contains(contextMenuOpenedArgs.ParentAddonName))
{
return;
}
Identity? identity = m_PluginData.GetIdentity(args);
Identity? identity = m_PluginData.GetIdentity(contextMenuOpenedArgs);
if (identity != null)
{
var notAddedTags = m_PluginData.CustomTags.Where(customTag => !identity.CustomTagIds.Contains(customTag.CustomId.Value));
if (notAddedTags.Any())
{
args.Items.Add(new NormalContextSubMenuItem(Strings.Loc_Static_ContextMenu_AddTag, (itemArgs =>
contextMenuOpenedArgs.ContextMenuItems.Add(new OpenSubContextMenuItem(Strings.Loc_Static_ContextMenu_AddTag, (subContextMenuOpenedArgs =>
{
List<ContextMenuItem> newContextMenuItems = new List<ContextMenuItem>();
foreach (var notAddedTag in notAddedTags)
{
itemArgs.Items.Add(new NormalContextMenuItem(notAddedTag.Text.Value, (args =>
newContextMenuItems.Add(new CustomContextMenuItem(notAddedTag.Text.Value, (args =>
{
m_PluginData.AddCustomTagToIdentity(notAddedTag, identity);
m_PluginConfiguration.Save(m_PluginData);
})));
}
itemArgs.Items.Add(new NormalContextMenuItem("<Do nothing>", (args =>
{
})));
subContextMenuOpenedArgs.ContextMenuItems.InsertRange(0, newContextMenuItems);
})));
}
var addedTags = m_PluginData.CustomTags.Where(customTag => identity.CustomTagIds.Contains(customTag.CustomId.Value));
if (addedTags.Any())
{
args.Items.Add(new NormalContextSubMenuItem(Strings.Loc_Static_ContextMenu_RemoveTag, (itemArgs =>
contextMenuOpenedArgs.ContextMenuItems.Add(new OpenSubContextMenuItem(Strings.Loc_Static_ContextMenu_RemoveTag, (subContextMenuOpenedArgs =>
{
List<ContextMenuItem> newContextMenuItems = new List<ContextMenuItem>();
foreach (var addedTag in addedTags)
{
itemArgs.Items.Add(new NormalContextMenuItem(addedTag.Text.Value, (args =>
newContextMenuItems.Add(new CustomContextMenuItem(addedTag.Text.Value, (args =>
{
m_PluginData.RemoveCustomTagFromIdentity(addedTag, identity);
m_PluginConfiguration.Save(m_PluginData);
})));
}
itemArgs.Items.Add(new NormalContextMenuItem("<Do nothing>", (args =>
{
})));
subContextMenuOpenedArgs.ContextMenuItems.InsertRange(0, newContextMenuItems);
})));
}
}
}
private bool DoesContextMenuSupportCustomTags(BaseContextMenuArgs args)
{
if (args.Text == null || args.ObjectWorld == 0 || args.ObjectWorld == 65535)
{
return false;
}
return SupportedAddonNames.Contains(args.ParentAddonName);
}
}
}

View File

@@ -4,21 +4,23 @@ using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using PlayerTags.Configuration;
using PlayerTags.Data;
using PlayerTags.Hooks;
using PlayerTags.GameInterface.Nameplates;
using System;
using System.Collections.Generic;
using System.Linq;
namespace PlayerTags.Features
{
public class NameplatesTagTargetFeature : TagTargetFeature
/// <summary>
/// A feature that adds tags to nameplates.
/// </summary>
public class NameplateTagTargetFeature : TagTargetFeature
{
private PluginConfiguration m_PluginConfiguration;
private PluginData m_PluginData;
private Nameplate? m_Nameplate;
private NameplateHooks? m_NameplateHooks;
public NameplatesTagTargetFeature(PluginConfiguration pluginConfiguration, PluginData pluginData)
public NameplateTagTargetFeature(PluginConfiguration pluginConfiguration, PluginData pluginData)
{
m_PluginConfiguration = pluginConfiguration;
m_PluginData = pluginData;
@@ -38,18 +40,28 @@ namespace PlayerTags.Features
private void Hook()
{
if (m_NameplateHooks == null)
if (m_Nameplate == null)
{
m_NameplateHooks = new NameplateHooks(SetPlayerNameplate);
m_Nameplate = new Nameplate();
if (!m_Nameplate.IsValid)
{
m_Nameplate = null;
}
if (m_Nameplate != null)
{
m_Nameplate.PlayerNameplateUpdated += Nameplate_PlayerNameplateUpdated;
}
}
}
private void Unhook()
{
if (m_NameplateHooks != null)
if (m_Nameplate != null)
{
m_NameplateHooks.Dispose();
m_NameplateHooks = null;
m_Nameplate.PlayerNameplateUpdated -= Nameplate_PlayerNameplateUpdated;
m_Nameplate.Dispose();
m_Nameplate = null;
}
}
@@ -83,30 +95,18 @@ namespace PlayerTags.Features
return false;
}
/// <summary>
/// Sets the strings on a nameplate.
/// </summary>
/// <param name="playerCharacter">The player character context.</param>
/// <param name="name">The name text.</param>
/// <param name="title">The title text.</param>
/// <param name="freeCompany">The free company text.</param>
/// <param name="isTitleVisible">Whether the title is visible.</param>
/// <param name="isTitleAboveName">Whether the title is above the name or below it.</param>
/// <param name="iconId">The icon id.</param>
/// <param name="isNameChanged">Whether the name was changed.</param>
/// <param name="isTitleChanged">Whether the title was changed.</param>
/// <param name="isFreeCompanyChanged">Whether the free company was changed.</param>
private void SetPlayerNameplate(PlayerCharacter playerCharacter, SeString name, SeString title, SeString freeCompany, ref bool isTitleVisible, ref bool isTitleAboveName, ref int iconId, out bool isNameChanged, out bool isTitleChanged, out bool isFreeCompanyChanged)
private void Nameplate_PlayerNameplateUpdated(PlayerNameplateUpdatedArgs args)
{
AddTagsToNameplate(playerCharacter, name, title, freeCompany, out isNameChanged, out isTitleChanged, out isFreeCompanyChanged);
var beforeTitleHashCode = args.Title.GetHashCode();
AddTagsToNameplate(args.PlayerCharacter, args.Name, args.Title, args.FreeCompany/*, out isNameChanged, out isTitleChanged, out isFreeCompanyChanged*/);
if (m_PluginConfiguration.NameplateTitlePosition == NameplateTitlePosition.AlwaysAboveName)
{
isTitleAboveName = true;
args.IsTitleAboveName = true;
}
else if (m_PluginConfiguration.NameplateTitlePosition == NameplateTitlePosition.AlwaysBelowName)
{
isTitleAboveName = false;
args.IsTitleAboveName = false;
}
if (m_PluginConfiguration.NameplateTitleVisibility == NameplateTitleVisibility.Default)
@@ -114,15 +114,16 @@ namespace PlayerTags.Features
}
else if (m_PluginConfiguration.NameplateTitleVisibility == NameplateTitleVisibility.Always)
{
isTitleVisible = true;
args.IsTitleVisible = true;
}
else if (m_PluginConfiguration.NameplateTitleVisibility == NameplateTitleVisibility.Never)
{
isTitleVisible = false;
args.IsTitleVisible = false;
}
else if (m_PluginConfiguration.NameplateTitleVisibility == NameplateTitleVisibility.WhenHasTags)
{
isTitleVisible = isTitleChanged;
bool hasTitleChanged = beforeTitleHashCode != args.Title.GetHashCode();
args.IsTitleVisible = hasTitleChanged;
}
if (m_PluginConfiguration.NameplateFreeCompanyVisibility == NameplateFreeCompanyVisibility.Default)
@@ -130,21 +131,21 @@ namespace PlayerTags.Features
}
else if (m_PluginConfiguration.NameplateFreeCompanyVisibility == NameplateFreeCompanyVisibility.Never)
{
freeCompany.Payloads.Clear();
isFreeCompanyChanged = true;
args.FreeCompany.Payloads.Clear();
//isFreeCompanyChanged = true;
}
}
/// <summary>
/// Adds the given payload changes to the dictionary.
/// Adds the given payload changes to the specified locations.
/// </summary>
/// <param name="nameplateElement">The nameplate element to add changes to.</param>
/// <param name="tagPosition">The position to add changes to.</param>
/// <param name="payloads">The payloads to add.</param>
/// <param name="nameplateChanges">The dictionary to add the changes to.</param>
private void AddPayloadChanges(NameplateElement nameplateElement, TagPosition tagPosition, IEnumerable<Payload> payloads, Dictionary<NameplateElement, Dictionary<TagPosition, List<Payload>>> nameplateChanges)
/// <param name="nameplateElement">The nameplate element of the changes.</param>
/// <param name="tagPosition">The position of the changes.</param>
/// <param name="payloadChanges">The payload changes to add.</param>
/// <param name="nameplateChanges">The dictionary to add changes to.</param>
private void AddPayloadChanges(NameplateElement nameplateElement, TagPosition tagPosition, IEnumerable<Payload> payloadChanges, Dictionary<NameplateElement, Dictionary<TagPosition, List<Payload>>> nameplateChanges)
{
if (!payloads.Any())
if (!payloadChanges.Any())
{
return;
}
@@ -154,25 +155,18 @@ namespace PlayerTags.Features
nameplateChanges[nameplateElement] = new Dictionary<TagPosition, List<Payload>>();
}
AddPayloadChanges(tagPosition, payloads, nameplateChanges[nameplateElement]);
AddPayloadChanges(tagPosition, payloadChanges, nameplateChanges[nameplateElement]);
}
/// <summary>
/// Adds all configured tags to the nameplate of a game object.
/// Adds tags to the nameplate of a game object.
/// </summary>
/// <param name="gameObject">The game object context.</param>
/// <param name="name">The name text to change.</param>
/// <param name="title">The title text to change.</param>
/// <param name="freeCompany">The free company text to change.</param>
/// <param name="isNameChanged">Whether the name was changed.</param>
/// <param name="isTitleChanged">Whether the title was changed.</param>
/// <param name="isFreeCompanyChanged">Whether the free company was changed.</param>
private void AddTagsToNameplate(GameObject gameObject, SeString name, SeString title, SeString freeCompany, out bool isNameChanged, out bool isTitleChanged, out bool isFreeCompanyChanged)
private void AddTagsToNameplate(GameObject gameObject, SeString name, SeString title, SeString freeCompany)
{
isNameChanged = false;
isTitleChanged = false;
isFreeCompanyChanged = false;
Dictionary<NameplateElement, Dictionary<TagPosition, List<Payload>>> nameplateChanges = new Dictionary<NameplateElement, Dictionary<TagPosition, List<Payload>>>();
if (gameObject is PlayerCharacter playerCharacter)
@@ -230,17 +224,14 @@ namespace PlayerTags.Features
if (nameplateElement == NameplateElement.Name)
{
seString = name;
isNameChanged = true;
}
else if (nameplateElement == NameplateElement.Title)
{
seString = title;
isTitleChanged = true;
}
else if (nameplateElement == NameplateElement.FreeCompany)
{
seString = freeCompany;
isFreeCompanyChanged = true;
}
if (seString != null)
@@ -268,7 +259,6 @@ namespace PlayerTags.Features
{
name.Payloads.Insert(0, (new UIForegroundPayload(customTag.TextColor.InheritedValue.Value)));
name.Payloads.Add(new UIForegroundPayload(0));
isNameChanged = true;
}
if (title.Payloads.Any(payload => payload is TextPayload)
@@ -277,7 +267,6 @@ namespace PlayerTags.Features
{
title.Payloads.Insert(0, (new UIForegroundPayload(customTag.TextColor.InheritedValue.Value)));
title.Payloads.Add(new UIForegroundPayload(0));
isTitleChanged = true;
}
if (freeCompany.Payloads.Any(payload => payload is TextPayload)
@@ -286,7 +275,6 @@ namespace PlayerTags.Features
{
freeCompany.Payloads.Insert(0, (new UIForegroundPayload(customTag.TextColor.InheritedValue.Value)));
freeCompany.Payloads.Add(new UIForegroundPayload(0));
isFreeCompanyChanged = true;
}
}
}
@@ -305,7 +293,6 @@ namespace PlayerTags.Features
{
name.Payloads.Insert(0, (new UIForegroundPayload(jobTag.TextColor.InheritedValue.Value)));
name.Payloads.Add(new UIForegroundPayload(0));
isNameChanged = true;
}
if (title.Payloads.Any(payload => payload is TextPayload)
@@ -314,7 +301,6 @@ namespace PlayerTags.Features
{
title.Payloads.Insert(0, (new UIForegroundPayload(jobTag.TextColor.InheritedValue.Value)));
title.Payloads.Add(new UIForegroundPayload(0));
isTitleChanged = true;
}
if (freeCompany.Payloads.Any(payload => payload is TextPayload)
@@ -323,7 +309,6 @@ namespace PlayerTags.Features
{
freeCompany.Payloads.Insert(0, (new UIForegroundPayload(jobTag.TextColor.InheritedValue.Value)));
freeCompany.Payloads.Add(new UIForegroundPayload(0));
isFreeCompanyChanged = true;
}
}
}

View File

@@ -1,20 +1,18 @@
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Logging;
using Lumina.Excel.GeneratedSheets;
using PlayerTags.Configuration;
using PlayerTags.Data;
using PlayerTags.Inheritables;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
namespace PlayerTags.Features
{
/// <summary>
/// The base of a feature that adds tags to UI elements.
/// </summary>
public abstract class TagTargetFeature : IDisposable
{
private ActivityContext m_CurrentActivityContext;
@@ -90,21 +88,26 @@ namespace PlayerTags.Features
}
/// <summary>
/// Gets the payloads for the given game object tag. If the payloads don't yet exist then they will be created.
/// Gets the payloads for the given tag and game object depending on visibility conditions.
/// </summary>
/// <param name="gameObject">The game object to get payloads for.</param>
/// <param name="tag">The tag config to get payloads for.</param>
/// <returns>A list of payloads for the given tag.</returns>
protected IEnumerable<Payload> GetPayloads(Tag tag, GameObject? gameObject)
protected Payload[] GetPayloads(Tag tag, GameObject? gameObject)
{
if (!IsTagVisible(tag, gameObject))
{
return Enumerable.Empty<Payload>();
return Array.Empty<Payload>();
}
return CreatePayloads(tag);
}
/// <summary>
/// Creates payloads for the given tag.
/// </summary>
/// <param name="tag">The tag to create payloads for.</param>
/// <returns>The payloads for the given tag.</returns>
private Payload[] CreatePayloads(Tag tag)
{
List<Payload> newPayloads = new List<Payload>();

View File

@@ -0,0 +1,682 @@
using Dalamud.Game;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Hooking;
using Dalamud.Logging;
using FFXIVClientStructs.FFXIV.Client.System.Memory;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
namespace PlayerTags.GameInterface.ContextMenus
{
internal unsafe delegate void AtkValueChangeTypeDelegate_Unmanaged(AtkValue* thisPtr, FFXIVClientStructs.FFXIV.Component.GUI.ValueType type);
internal unsafe delegate void AtkValueSetStringDelegate_Unmanaged(AtkValue* thisPtr, byte* bytes);
/// <summary>
/// Provides an interface to modify context menus.
/// </summary>
public class ContextMenu : IDisposable
{
private class PluginAddressResolver : BaseAddressResolver
{
private const string c_AtkValueChangeType = "E8 ?? ?? ?? ?? 45 84 F6 48 8D 4C 24";
public IntPtr? AtkValueChangeTypePtr { get; private set; }
private const string c_AtkValueSetString = "E8 ?? ?? ?? ?? 41 03 ED";
public IntPtr? AtkValueSetStringPtr { get; private set; }
private const string c_GetAddonById = "E8 ?? ?? ?? ?? 8B 6B 20";
public IntPtr? GetAddonByIdPtr { get; private set; }
private const string c_OpenSubContextMenu = "E8 ?? ?? ?? ?? 44 39 A3 ?? ?? ?? ?? 0F 86";
public IntPtr? OpenSubContextMenuPtr { get; private set; }
private const string c_ContextMenuOpening = "E8 ?? ?? ?? ?? 0F B7 C0 48 83 C4 60";
public IntPtr? ContextMenuOpeningPtr { get; private set; }
private const string c_ContextMenuOpened = "48 8B C4 57 41 56 41 57 48 81 EC";
public IntPtr? ContextMenuOpenedPtr { get; private set; }
private const string c_ContextMenuItemSelected = "48 89 5C 24 ?? 55 57 41 56 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 44 24 ?? 80 B9";
public IntPtr? ContextMenuItemSelectedPtr { get; private set; }
private const string c_SubContextMenuOpening = "E8 ?? ?? ?? ?? 44 39 A3 ?? ?? ?? ?? 0F 84";
public IntPtr? SubContextMenuOpeningPtr { get; private set; }
private const string c_SubContextMenuOpened = "48 8B C4 57 41 55 41 56 48 81 EC";
public IntPtr? SubContextMenuOpenedPtr { get; private set; }
private const string c_OpenInventoryContextMenu = "44 88 44 24 ?? 88 54 24 10 53";
public IntPtr? OpenInventoryContextMenuPtr { get; private set; }
private const string c_InventoryContextMenuEvent30 = "E8 ?? ?? ?? ?? 48 83 C4 30 5B C3 8B 83";
public IntPtr? InventoryContextMenuEvent30Ptr { get; private set; }
protected override void Setup64Bit(SigScanner scanner)
{
if (scanner.TryScanText(c_AtkValueChangeType, out var atkValueChangeTypePtr))
{
AtkValueChangeTypePtr = atkValueChangeTypePtr;
}
if (scanner.TryScanText(c_AtkValueSetString, out var atkValueSetStringPtr))
{
AtkValueSetStringPtr = atkValueSetStringPtr;
}
if (scanner.TryScanText(c_GetAddonById, out var getAddonByIdPtr))
{
GetAddonByIdPtr = getAddonByIdPtr;
}
if (scanner.TryScanText(c_OpenSubContextMenu, out var openSubContextMenuPtr))
{
OpenSubContextMenuPtr = openSubContextMenuPtr;
}
if (scanner.TryScanText(c_ContextMenuOpening, out var someOpenAddonThingPtr))
{
ContextMenuOpeningPtr = someOpenAddonThingPtr;
}
if (scanner.TryScanText(c_ContextMenuOpened, out var contextMenuOpenedPtr))
{
ContextMenuOpenedPtr = contextMenuOpenedPtr;
}
if (scanner.TryScanText(c_ContextMenuItemSelected, out var contextMenuItemSelectedPtr))
{
ContextMenuItemSelectedPtr = contextMenuItemSelectedPtr;
}
if (scanner.TryScanText(c_SubContextMenuOpening, out var subContextMenuOpening))
{
SubContextMenuOpeningPtr = subContextMenuOpening;
}
if (scanner.TryScanText(c_SubContextMenuOpened, out var titleScreenContextMenuOpenedPtr))
{
SubContextMenuOpenedPtr = titleScreenContextMenuOpenedPtr;
}
if (scanner.TryScanText(c_OpenInventoryContextMenu, out var setUpInventoryContextSubMenuPtr))
{
OpenInventoryContextMenuPtr = setUpInventoryContextSubMenuPtr;
}
if (scanner.TryScanText(c_InventoryContextMenuEvent30, out var inventoryContextMenuEvent30Ptr))
{
InventoryContextMenuEvent30Ptr = inventoryContextMenuEvent30Ptr;
}
}
}
private readonly AtkValueChangeTypeDelegate_Unmanaged? m_AtkValueChangeType;
private readonly AtkValueSetStringDelegate_Unmanaged? m_AtkValueSetString;
private delegate IntPtr GetAddonByIdDelegate_Unmanaged(IntPtr raptureAtkUnitManager, ushort id);
private readonly GetAddonByIdDelegate_Unmanaged? m_GetAddonById;
private delegate byte OpenSubContextMenuDelegate_Unmanaged(IntPtr agent);
private readonly OpenSubContextMenuDelegate_Unmanaged? m_OpenSubContextMenu;
private delegate IntPtr ContextMenuOpeningDelegate_Unmanaged(IntPtr a1, IntPtr a2, IntPtr a3, uint a4, IntPtr a5, IntPtr agent, IntPtr a7, ushort a8);
private Hook<ContextMenuOpeningDelegate_Unmanaged>? m_ContextMenuOpeningHook;
private unsafe delegate byte ContextMenuOpenedDelegate_Unmanaged(IntPtr addon, int menuSize, AtkValue* atkValueArgs);
private Hook<ContextMenuOpenedDelegate_Unmanaged>? m_ContextMenuOpenedHook;
private Hook<ContextMenuOpenedDelegate_Unmanaged>? m_SubContextMenuOpenedHook;
private delegate byte ContextMenuItemSelectedDelegate_Unmanaged(IntPtr addon, int index, byte a3);
private Hook<ContextMenuItemSelectedDelegate_Unmanaged>? m_ContextMenuItemSelectedHook;
private delegate byte SubContextMenuOpeningDelegate_Unmanaged(IntPtr agent);
private Hook<SubContextMenuOpeningDelegate_Unmanaged>? m_SubContextMenuOpeningHook;
private delegate IntPtr OpenInventoryContextMenuDelegate_Unmanaged(IntPtr agent, byte hasTitle, byte zero);
private readonly OpenInventoryContextMenuDelegate_Unmanaged? m_OpenInventoryContextMenu;
private delegate void InventoryContextMenuEvent30Delegate_Unmanaged(IntPtr agent, IntPtr a2, int a3, int a4, short a5);
private Hook<InventoryContextMenuEvent30Delegate_Unmanaged>? m_InventoryContextMenuEvent30Hook;
private PluginAddressResolver m_PluginAddressResolver;
private const int MaxContextMenuItemsPerContextMenu = 32;
private IntPtr m_CurrentContextMenuAgent;
private IntPtr m_SubContextMenuTitle;
private ContextMenuOpenedArgs? m_ContextMenuOpenedArgs;
private OpenSubContextMenuItem? m_OpenSubContextMenuItem;
/// <summary>
/// Occurs when a context menu is opened by the game.
/// </summary>
public event ContextMenuOpenedDelegate? ContextMenuOpened;
/// <summary>
/// Whether the required hooks are in place and this instance is valid.
/// </summary>
public bool IsValid
{
get
{
if (!m_PluginAddressResolver.AtkValueChangeTypePtr.HasValue
|| !m_PluginAddressResolver.AtkValueSetStringPtr.HasValue
|| !m_PluginAddressResolver.GetAddonByIdPtr.HasValue
|| !m_PluginAddressResolver.OpenSubContextMenuPtr.HasValue
|| !m_PluginAddressResolver.ContextMenuOpeningPtr.HasValue
|| !m_PluginAddressResolver.ContextMenuOpenedPtr.HasValue
|| !m_PluginAddressResolver.ContextMenuItemSelectedPtr.HasValue
|| !m_PluginAddressResolver.SubContextMenuOpeningPtr.HasValue
|| !m_PluginAddressResolver.SubContextMenuOpenedPtr.HasValue
|| !m_PluginAddressResolver.OpenInventoryContextMenuPtr.HasValue
|| !m_PluginAddressResolver.InventoryContextMenuEvent30Ptr.HasValue
)
{
return false;
}
return true;
}
}
public ContextMenu()
{
m_PluginAddressResolver = new PluginAddressResolver();
m_PluginAddressResolver.Setup();
if (!IsValid)
{
return;
}
if (m_PluginAddressResolver.AtkValueChangeTypePtr.HasValue)
{
m_AtkValueChangeType = Marshal.GetDelegateForFunctionPointer<AtkValueChangeTypeDelegate_Unmanaged>(m_PluginAddressResolver.AtkValueChangeTypePtr.Value);
}
if (m_PluginAddressResolver.AtkValueSetStringPtr.HasValue)
{
m_AtkValueSetString = Marshal.GetDelegateForFunctionPointer<AtkValueSetStringDelegate_Unmanaged>(m_PluginAddressResolver.AtkValueSetStringPtr.Value);
}
if (m_PluginAddressResolver.GetAddonByIdPtr.HasValue)
{
m_GetAddonById = Marshal.GetDelegateForFunctionPointer<GetAddonByIdDelegate_Unmanaged>(m_PluginAddressResolver.GetAddonByIdPtr.Value);
}
if (m_PluginAddressResolver.OpenSubContextMenuPtr.HasValue)
{
m_OpenSubContextMenu = Marshal.GetDelegateForFunctionPointer<OpenSubContextMenuDelegate_Unmanaged>(m_PluginAddressResolver.OpenSubContextMenuPtr.Value);
}
if (m_PluginAddressResolver.ContextMenuOpeningPtr.HasValue)
{
m_ContextMenuOpeningHook = new Hook<ContextMenuOpeningDelegate_Unmanaged>(m_PluginAddressResolver.ContextMenuOpeningPtr.Value, ContextMenuOpeningDetour);
m_ContextMenuOpeningHook?.Enable();
}
if (m_PluginAddressResolver.ContextMenuOpenedPtr.HasValue)
{
unsafe
{
m_ContextMenuOpenedHook = new Hook<ContextMenuOpenedDelegate_Unmanaged>(m_PluginAddressResolver.ContextMenuOpenedPtr.Value, ContextMenuOpenedDetour);
}
m_ContextMenuOpenedHook?.Enable();
}
if (m_PluginAddressResolver.ContextMenuItemSelectedPtr.HasValue)
{
m_ContextMenuItemSelectedHook = new Hook<ContextMenuItemSelectedDelegate_Unmanaged>(m_PluginAddressResolver.ContextMenuItemSelectedPtr.Value, ContextMenuItemSelectedDetour);
m_ContextMenuItemSelectedHook?.Enable();
}
if (m_PluginAddressResolver.SubContextMenuOpeningPtr.HasValue)
{
m_SubContextMenuOpeningHook = new Hook<SubContextMenuOpeningDelegate_Unmanaged>(m_PluginAddressResolver.SubContextMenuOpeningPtr.Value, SubContextMenuOpeningDetour);
m_SubContextMenuOpeningHook?.Enable();
}
if (m_PluginAddressResolver.SubContextMenuOpenedPtr.HasValue)
{
unsafe
{
m_SubContextMenuOpenedHook = new Hook<ContextMenuOpenedDelegate_Unmanaged>(m_PluginAddressResolver.SubContextMenuOpenedPtr.Value, SubContextMenuOpenedDetour);
}
m_SubContextMenuOpenedHook?.Enable();
}
if (m_PluginAddressResolver.OpenInventoryContextMenuPtr.HasValue)
{
m_OpenInventoryContextMenu = Marshal.GetDelegateForFunctionPointer<OpenInventoryContextMenuDelegate_Unmanaged>(m_PluginAddressResolver.OpenInventoryContextMenuPtr.Value);
}
if (m_PluginAddressResolver.InventoryContextMenuEvent30Ptr.HasValue)
{
m_InventoryContextMenuEvent30Hook = new Hook<InventoryContextMenuEvent30Delegate_Unmanaged>(m_PluginAddressResolver.InventoryContextMenuEvent30Ptr.Value, InventoryContextMenuEvent30Detour);
m_InventoryContextMenuEvent30Hook?.Enable();
}
}
public void Dispose()
{
m_InventoryContextMenuEvent30Hook?.Disable();
m_SubContextMenuOpeningHook?.Disable();
m_ContextMenuItemSelectedHook?.Disable();
m_SubContextMenuOpenedHook?.Disable();
m_ContextMenuOpenedHook?.Disable();
m_ContextMenuOpeningHook?.Disable();
}
private unsafe IntPtr ContextMenuOpeningDetour(IntPtr a1, IntPtr a2, IntPtr a3, uint a4, IntPtr a5, IntPtr agent, IntPtr a7, ushort a8)
{
PluginLog.Debug($"ContextMenuOpeningDetour");
m_CurrentContextMenuAgent = agent;
return m_ContextMenuOpeningHook!.Original(a1, a2, a3, a4, a5, agent, a7, a8);
}
private unsafe byte ContextMenuOpenedDetour(IntPtr addon, int atkValueCount, AtkValue* atkValues)
{
PluginLog.Debug($"ContextMenuOpenedDetour");
if (m_ContextMenuOpenedHook == null)
{
return 0;
}
var addonContext = (AddonContext*)addon;
PluginLog.Debug($"ContextMenuOpenedDetour addonContext->IsInitialMenu={addonContext->IsInitialMenu}");
ContextMenuReaderWriter.Print(atkValueCount, atkValues);
try
{
ContextMenuOpenedImplementation(addon, ref atkValueCount, ref atkValues);
}
catch (Exception ex)
{
PluginLog.Error(ex, "ContextMenuOpenedDetour");
}
return m_ContextMenuOpenedHook.Original(addon, atkValueCount, atkValues);
}
private unsafe void ContextMenuOpenedImplementation(IntPtr addon, ref int atkValueCount, ref AtkValue* atkValues)
{
PluginLog.Debug($"ContextMenuOpenedImplementation");
if (m_AtkValueChangeType == null
|| m_AtkValueSetString == null
|| ContextMenuOpened == null
|| m_CurrentContextMenuAgent == IntPtr.Zero)
{
return;
}
ContextMenuReaderWriter contextMenuReaderWriter = new ContextMenuReaderWriter(m_CurrentContextMenuAgent, atkValueCount, atkValues);
// Read the context menu items from the game, then allow subscribers to modify them
if (m_ContextMenuOpenedArgs == null)
{
m_ContextMenuOpenedArgs = NotifyContextMenuOpened(addon, m_CurrentContextMenuAgent, ContextMenuOpened, contextMenuReaderWriter.Read());
if (m_ContextMenuOpenedArgs == null)
{
return;
}
}
contextMenuReaderWriter.Write(null, m_ContextMenuOpenedArgs.ContextMenuItems, m_AtkValueChangeType, m_AtkValueSetString);
// Update the addon
var addonContext = (AddonContext*)addon;
atkValueCount = *(&addonContext->AtkValuesCount) = (ushort)contextMenuReaderWriter.AtkValueCount;
atkValues = *(&addonContext->AtkValues) = contextMenuReaderWriter.AtkValues;
}
private byte SubContextMenuOpeningDetour(IntPtr agent)
{
PluginLog.Debug($"SubContextMenuOpeningDetour");
if (m_SubContextMenuOpeningHook == null)
{
return 0;
}
if (SubContextMenuOpeningImplementation(agent))
{
return 0x0;
}
return m_SubContextMenuOpeningHook.Original(agent);
}
private unsafe bool SubContextMenuOpeningImplementation(IntPtr agent)
{
PluginLog.Debug($"SubContextMenuOpeningImplementation {m_OpenSubContextMenuItem}");
if (m_OpenSubContextMenu == null
|| m_OpenInventoryContextMenu == null
|| m_AtkValueChangeType == null
|| m_OpenSubContextMenuItem == null)
{
return false;
}
// This isn't our own sub context menu -- don't go any further
//if (m_OpenSubContextMenuItem == null)
//{
// return false;
//}
//var a = (AgentContext*)agent;
//var s = a->SelectedIndex;
//*(&a->SelectedIndex) = 0xff;
// The important things to make this work are:
// 1. Temporary allocate a sub context menu title
// 1. Temporarily increase the atk value count by 1 so the game knows to expect at least 1 context menu item
// Other than those requirements, the data is irrelevant and will be set when the menu has actually opened.
var agentContext = (AgentContext*)agent;
if (IsInventoryContext(agent))
{
m_OpenInventoryContextMenu(agent, 0, 0);
}
else
{
m_OpenSubContextMenu(agent);
// Free any sub context menu title we've already allocated
if (m_SubContextMenuTitle != IntPtr.Zero)
{
unsafe
{
IMemorySpace.Free((void*)m_SubContextMenuTitle, (ulong)IntPtr.Size);
}
m_SubContextMenuTitle = IntPtr.Zero;
}
// Allocate a new 1 byte title. Without this, a title won't be rendered.
// The actual value doesn't matter at this point, we'll set it later.
m_SubContextMenuTitle = (IntPtr)IMemorySpace.GetUISpace()->Malloc(1, 0);
*(&agentContext->SubContextMenuTitle) = (byte*)m_SubContextMenuTitle;
}
//*(&a->SelectedIndex) = s;
var atkValues = &agentContext->ItemData->AtkValues;
// Let the game know the context menu will have at least 1 item in it
var newAtkValuesCount = agentContext->ItemData->AtkValuesCount + 1;
*(&agentContext->ItemData->AtkValuesCount) = (ushort)(newAtkValuesCount);
atkValues[0].UInt = 1;
ContextMenuReaderWriter.Print(agentContext->ItemData->AtkValuesCount, atkValues);
return true;
}
private unsafe byte SubContextMenuOpenedDetour(IntPtr addon, int atkValueCount, AtkValue* atkValues)
{
PluginLog.Debug($"SubContextMenuOpenedDetour");
if (m_SubContextMenuOpenedHook == null)
{
return 0;
}
var addonContext = (AddonContext*)addon;
PluginLog.Debug($"SubContextMenuOpenedDetour addonContext->IsInitialMenu={addonContext->IsInitialMenu}");
ContextMenuReaderWriter.Print(atkValueCount, atkValues);
try
{
SubContextMenuOpenedImplementation(addon, ref atkValueCount, ref atkValues);
}
catch (Exception ex)
{
PluginLog.Error(ex, "SubContextMenuOpenedDetour");
}
return m_SubContextMenuOpenedHook.Original(addon, atkValueCount, atkValues);
}
private unsafe void SubContextMenuOpenedImplementation(IntPtr addon, ref int atkValueCount, ref AtkValue* atkValues)
{
PluginLog.Debug($"SubContextMenuOpenedImplementation");
if (m_AtkValueSetString == null
|| m_AtkValueChangeType == null
|| ContextMenuOpened == null)
{
return;
}
ContextMenuReaderWriter contextMenuReader = new ContextMenuReaderWriter(m_CurrentContextMenuAgent, atkValueCount, atkValues);
if (m_OpenSubContextMenuItem != null)
{
m_ContextMenuOpenedArgs = NotifyContextMenuOpened(addon, m_CurrentContextMenuAgent, m_OpenSubContextMenuItem.OpenedAction, contextMenuReader.Read());
if (m_ContextMenuOpenedArgs == null)
{
return;
}
}
else
{
m_ContextMenuOpenedArgs = NotifyContextMenuOpened(addon, m_CurrentContextMenuAgent, ContextMenuOpened, contextMenuReader.Read());
if (m_ContextMenuOpenedArgs == null)
{
return;
}
}
contextMenuReader.Write(m_OpenSubContextMenuItem, m_ContextMenuOpenedArgs.ContextMenuItems, m_AtkValueChangeType, m_AtkValueSetString);
// Update the addon
var addonContext = (AddonContext*)addon;
atkValueCount = *(&addonContext->AtkValuesCount) = (ushort)contextMenuReader.AtkValueCount;
atkValues = *(&addonContext->AtkValues) = contextMenuReader.AtkValues;
}
private unsafe ContextMenuOpenedArgs? NotifyContextMenuOpened(IntPtr addon, IntPtr agent, ContextMenuOpenedDelegate contextMenuOpenedDelegate, IEnumerable<ContextMenuItem> initialContextMenuItems)
{
var parentAddonName = GetParentAddonName(addon);
ContextMenuOpenedArgs contextMenuOpenedArgs;
if (IsInventoryContext(agent))
{
var agentInventoryContext = (AgentInventoryContext*)agent;
contextMenuOpenedArgs = new ContextMenuOpenedArgs(addon, agent, parentAddonName, initialContextMenuItems)
{
ItemContext = new ItemContext(agentInventoryContext->ItemId, agentInventoryContext->ItemCount, agentInventoryContext->IsHighQuality)
};
}
else
{
var agentContext = (AgentContext*)agent;
SeString objectName;
unsafe
{
objectName = Helper.ReadSeString((IntPtr)agentContext->ObjectName.StringPtr);
}
contextMenuOpenedArgs = new ContextMenuOpenedArgs(addon, agent, parentAddonName, initialContextMenuItems)
{
GameObjectContext = new GameObjectContext(agentContext->ObjectId, agentContext->ObjectContentIdLower, objectName, agentContext->ObjectWorldId)
};
}
try
{
contextMenuOpenedDelegate.Invoke(contextMenuOpenedArgs);
}
catch (Exception ex)
{
PluginLog.LogError(ex, "NotifyContextMenuOpened");
return null;
}
foreach (var contextMenuItem in contextMenuOpenedArgs.ContextMenuItems)
{
contextMenuItem.Agent = agent;
}
if (contextMenuOpenedArgs.ContextMenuItems.Count > MaxContextMenuItemsPerContextMenu)
{
PluginLog.LogWarning($"Context menu requesting {contextMenuOpenedArgs.ContextMenuItems.Count} of max {MaxContextMenuItemsPerContextMenu} items. Resizing list to compensate.");
contextMenuOpenedArgs.ContextMenuItems.RemoveRange(MaxContextMenuItemsPerContextMenu, contextMenuOpenedArgs.ContextMenuItems.Count - MaxContextMenuItemsPerContextMenu);
}
return contextMenuOpenedArgs;
}
private unsafe byte ContextMenuItemSelectedDetour(IntPtr addon, int index, byte a3)
{
PluginLog.Debug($"ContextMenuItemSelectedDetour");
if (m_ContextMenuItemSelectedHook == null)
{
return 0;
}
try
{
ContextMenuItemSelectedImplementation(addon, index);
}
catch (Exception ex)
{
PluginLog.Error(ex, "ContextMenuItemSelectedDetour");
}
return m_ContextMenuItemSelectedHook.Original(addon, index, a3);
}
private unsafe void ContextMenuItemSelectedImplementation(IntPtr addon, int index)
{
PluginLog.Debug($"ContextMenuItemSelectedImplementation index={index}");
if (m_ContextMenuOpenedArgs == null)
{
m_ContextMenuOpenedArgs = null;
m_OpenSubContextMenuItem = null;
return;
}
// TODO: Get all the items again. Any items we don't know about (probably added by other plugins) we count and add to the index
var addonContext = (AddonContext*)addon;
ContextMenuReaderWriter.Print(addonContext->AtkValuesCount, addonContext->AtkValues);
var contextMenuItem = m_ContextMenuOpenedArgs.ContextMenuItems.ElementAtOrDefault(index);
if (contextMenuItem == null)
{
m_ContextMenuOpenedArgs = null;
m_OpenSubContextMenuItem = null;
return;
}
if (contextMenuItem is OpenSubContextMenuItem openSubContextMenuItem)
{
m_OpenSubContextMenuItem = openSubContextMenuItem;
}
else if (contextMenuItem is CustomContextMenuItem customContextMenuItem)
{
var args = new CustomContextMenuItemSelectedArgs(m_ContextMenuOpenedArgs, customContextMenuItem);
try
{
customContextMenuItem.CustomAction(args);
}
catch (Exception ex)
{
PluginLog.LogError(ex, "ContextMenuItemSelectedImplementation");
}
m_ContextMenuOpenedArgs = null;
m_OpenSubContextMenuItem = null;
}
else
{
m_ContextMenuOpenedArgs = null;
m_OpenSubContextMenuItem = null;
}
}
private void InventoryContextMenuEvent30Detour(IntPtr agent, IntPtr a2, int a3, int a4, short a5)
{
PluginLog.Debug($"InventoryContextMenuEvent30Detour");
if (m_InventoryContextMenuEvent30Hook == null)
{
return;
}
if (SubContextMenuOpeningImplementation(agent))
{
return;
}
m_InventoryContextMenuEvent30Hook.Original(agent, a2, a3, a4, a5);
}
private void SetFlag(ref uint mask, int itemIndex, bool value)
{
mask &= ~((uint)1 << itemIndex);
if (value)
{
mask |= (uint)(1 << itemIndex);
}
}
private unsafe string? GetParentAddonName(IntPtr addon)
{
if (m_GetAddonById == null)
{
return null;
}
var parentAddonId = Marshal.PtrToStructure<Addon>(addon).ParentAddonId;
if (parentAddonId == 0)
{
return null;
}
var atkStage = AtkStage.GetSingleton();
var parentAddonPtr = m_GetAddonById((IntPtr)atkStage->RaptureAtkUnitManager, parentAddonId);
return Helper.ReadString(parentAddonPtr + 8);
}
private unsafe bool IsInventoryContext(IntPtr agent)
{
if (agent == (IntPtr)AgentInventoryContext.Instance())
{
return true;
}
return false;
}
private unsafe IntPtr GetAddonFromAgent(IntPtr agent)
{
if (m_GetAddonById == null)
{
return IntPtr.Zero;
}
var agentInterface = (AgentInterface*)agent;
if (agentInterface->AddonId == 0)
{
return IntPtr.Zero;
}
return m_GetAddonById((IntPtr)AtkStage.GetSingleton()->RaptureAtkUnitManager, (ushort)agentInterface->AddonId);
}
}
}

View File

@@ -0,0 +1,28 @@
using Dalamud.Game.Text.SeStringHandling;
using System;
namespace PlayerTags.GameInterface.ContextMenus
{
public abstract class ContextMenuItem
{
public SeString Name { get; set; }
public bool IsEnabled { get; set; } = true;
public bool HasPreviousArrow { get; set; } = false;
public bool HasNextArrow { get; set; } = false;
internal IntPtr Agent { get; set; }
public ContextMenuItem(SeString name)
{
Name = name;
}
public override string ToString()
{
return Name.ToString();
}
}
}

View File

@@ -0,0 +1,29 @@
using Dalamud.Game.Text.SeStringHandling;
using System;
using System.Collections.Generic;
namespace PlayerTags.GameInterface.ContextMenus
{
public class ContextMenuOpenedArgs
{
public IntPtr Addon { get; }
public IntPtr Agent { get; }
public string? ParentAddonName { get; }
public List<ContextMenuItem> ContextMenuItems { get; }
public GameObjectContext? GameObjectContext { get; init; }
public ItemContext? ItemContext { get; init; }
public ContextMenuOpenedArgs(IntPtr addon, IntPtr agent, string? parentAddonName, IEnumerable<ContextMenuItem> contextMenuItems)
{
Addon = addon;
Agent = agent;
ParentAddonName = parentAddonName;
ContextMenuItems = new List<ContextMenuItem>(contextMenuItems);
}
}
}

View File

@@ -0,0 +1,4 @@
namespace PlayerTags.GameInterface.ContextMenus
{
public delegate void ContextMenuOpenedDelegate(ContextMenuOpenedArgs args);
}

View File

@@ -0,0 +1,465 @@
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Logging;
using FFXIVClientStructs.FFXIV.Client.System.Memory;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
namespace PlayerTags.GameInterface.ContextMenus
{
internal unsafe class ContextMenuReaderWriter
{
private IntPtr m_Agent;
private int m_AtkValueCount;
public int AtkValueCount
{
get { return m_AtkValueCount; }
}
private AtkValue* m_AtkValues;
public AtkValue* AtkValues
{
get { return m_AtkValues; }
}
public int ContextMenuItemCount
{
get { return m_AtkValues[0].Int; }
}
public bool HasTitle
{
get
{
bool isStringType =
(int)m_AtkValues[1].Type == 8
|| (int)m_AtkValues[1].Type == 38
|| m_AtkValues[1].Type == FFXIVClientStructs.FFXIV.Component.GUI.ValueType.String;
return isStringType && m_AtkValues[1].Int != 0;
}
}
public SeString? Title
{
get
{
if (HasTitle)
{
try
{
Helper.TryReadSeString((IntPtr)(&m_AtkValues[1])->String, out var str);
return str;
}
catch (Exception ex)
{
}
}
return null;
}
}
public int HasPreviousArrowFlagsIndex
{
get
{
if (HasTitle)
{
return 6;
}
return 2;
}
}
public int HasNextArrowFlagsIndex
{
get
{
if (HasTitle)
{
return 5;
}
return 3;
}
}
public int FirstContextMenuItemIndex
{
get
{
if (HasTitle)
{
return 8;
}
return 7;
}
}
public int NameIndexOffset
{
get
{
if (HasTitle && StructLayout == SubContextMenuStructLayout.Alternate)
{
return 1;
}
return 0;
}
}
public int IsDisabledIndexOffset
{
get
{
if (HasTitle && StructLayout == SubContextMenuStructLayout.Alternate)
{
return 2;
}
return ContextMenuItemCount;
}
}
public int SequentialAtkValuesPerContextMenuItem
{
get
{
if (HasTitle && StructLayout == SubContextMenuStructLayout.Alternate)
{
return 4;
}
return 1;
}
}
public int TotalDesiredAtkValuesPerContextMenuItem
{
get
{
if (HasTitle && StructLayout == SubContextMenuStructLayout.Alternate)
{
return 4;
}
return 2;
}
}
public Vector2? Position
{
get
{
if (HasTitle)
{
return new Vector2(m_AtkValues[2].Int, m_AtkValues[3].Int);
}
return null;
}
}
public enum SubContextMenuStructLayout
{
Main,
Alternate
}
public SubContextMenuStructLayout? StructLayout
{
get
{
if (HasTitle)
{
if (m_AtkValues[7].Int == 8)
{
return SubContextMenuStructLayout.Alternate;
}
else if (m_AtkValues[7].Int == 1)
{
return SubContextMenuStructLayout.Main;
}
}
return null;
}
}
private unsafe bool IsInventoryContext
{
get
{
if (m_Agent == (IntPtr)AgentInventoryContext.Instance())
{
return true;
}
return false;
}
}
public ContextMenuReaderWriter(IntPtr agent, int atkValueCount, AtkValue* atkValues)
{
m_Agent = agent;
m_AtkValueCount = atkValueCount;
m_AtkValues = atkValues;
}
public GameContextMenuItem[] Read()
{
Print();
List<GameContextMenuItem> gameContextMenuItems = new List<GameContextMenuItem>();
for (var contextMenuItemIndex = 0; contextMenuItemIndex < ContextMenuItemCount; contextMenuItemIndex++)
{
var contextMenuItemAtkValueBaseIndex = FirstContextMenuItemIndex + (contextMenuItemIndex * SequentialAtkValuesPerContextMenuItem);
PluginLog.Debug($"{contextMenuItemAtkValueBaseIndex}={FirstContextMenuItemIndex}+({contextMenuItemIndex}*{SequentialAtkValuesPerContextMenuItem})");
// Get the name
var nameAtkValue = &m_AtkValues[contextMenuItemAtkValueBaseIndex + NameIndexOffset];
if (nameAtkValue->Type == 0)
{
continue;
}
var name = Helper.ReadSeString((IntPtr)nameAtkValue->String);
// Get the enabled state. Note that SE stores this as IsDisabled, NOT IsEnabled (those heathens)
var isEnabled = true;
bool isDisabledDefined = FirstContextMenuItemIndex + ContextMenuItemCount < AtkValueCount;
if (isDisabledDefined)
{
var isDisabledAtkValue = &m_AtkValues[contextMenuItemAtkValueBaseIndex + IsDisabledIndexOffset];
isEnabled = isDisabledAtkValue->Int == 0;
}
// Get the action
byte* actions = null;
if (IsInventoryContext)
{
actions = &((AgentInventoryContext*)m_Agent)->Actions;
}
else
{
actions = &((AgentContext*)m_Agent)->ItemData->Actions;
}
byte action = *(actions + contextMenuItemAtkValueBaseIndex);
// Get the has previous arrow flag
var hasPreviousArrowFlagsAtkValue = &m_AtkValues[HasPreviousArrowFlagsIndex];
var hasPreviousArrow = HasFlag(hasPreviousArrowFlagsAtkValue->UInt, contextMenuItemIndex);
// Get the has next arrow flag
var hasNextArrowFlagsAtkValue = &m_AtkValues[HasNextArrowFlagsIndex];
var hasNextArrow = HasFlag(hasNextArrowFlagsAtkValue->UInt, contextMenuItemIndex);
var gameContextMenuItem = new GameContextMenuItem(name, action)
{
Agent = m_Agent,
IsEnabled = isEnabled,
HasPreviousArrow = hasPreviousArrow,
HasNextArrow = hasNextArrow
};
gameContextMenuItems.Add(gameContextMenuItem);
PluginLog.Debug($"Read Name={gameContextMenuItem.Name} Action={gameContextMenuItem.Action} IsEnabled={gameContextMenuItem.IsEnabled} HasPreviousArrow={gameContextMenuItem.HasPreviousArrow} HasNextArrow={gameContextMenuItem.HasNextArrow}");
}
return gameContextMenuItems.ToArray();
}
public unsafe void Write(OpenSubContextMenuItem? selectedOpenSubContextMenuItem, IEnumerable<ContextMenuItem> contextMenuItems, AtkValueChangeTypeDelegate_Unmanaged atkValueChangeType, AtkValueSetStringDelegate_Unmanaged atkValueSetString)
{
Print();
var newAtkValuesCount = FirstContextMenuItemIndex + (contextMenuItems.Count() * TotalDesiredAtkValuesPerContextMenuItem);
// Allocate the new array. We have to do a little dance with the first 8 bytes which represents the array count
const int arrayCountSize = 8;
var newAtkValuesArraySize = arrayCountSize + Marshal.SizeOf<AtkValue>() * newAtkValuesCount;
var newAtkValuesArray = (IntPtr)IMemorySpace.GetUISpace()->Malloc((ulong)newAtkValuesArraySize, 0);
if (newAtkValuesArray == IntPtr.Zero)
{
return;
}
var newAtkValues = (AtkValue*)(newAtkValuesArray + arrayCountSize);
// Zero the memory, then copy the atk values up to the first context menu item atk value
Marshal.Copy(new byte[newAtkValuesArraySize], 0, newAtkValuesArray, newAtkValuesArraySize);
Buffer.MemoryCopy(m_AtkValues, newAtkValues, newAtkValuesArraySize - arrayCountSize, (long)sizeof(AtkValue) * FirstContextMenuItemIndex);
// Free the old array
IMemorySpace.Free((void*)((IntPtr)(m_AtkValues) - arrayCountSize), arrayCountSize + (ulong)sizeof(AtkValue) * *(ulong*)((IntPtr)m_AtkValues - 8));
// Set the array count
*(ulong*)newAtkValuesArray = (ulong)newAtkValuesCount;
m_AtkValueCount = newAtkValuesCount;
m_AtkValues = newAtkValues;
// Set the title
if (selectedOpenSubContextMenuItem != null)
{
var titleAtkValue = &m_AtkValues[1];
fixed (byte* TtlePtr = selectedOpenSubContextMenuItem.Name.Encode().NullTerminate())
{
atkValueSetString(titleAtkValue, TtlePtr);
}
}
// Set the context menu item count
const int contextMenuItemCountAtkValueIndex = 0;
var contextMenuItemCountAtkValue = &m_AtkValues[contextMenuItemCountAtkValueIndex];
contextMenuItemCountAtkValue->UInt = (uint)contextMenuItems.Count();
// Clear the previous arrow flags
var hasPreviousArrowAtkValue = &m_AtkValues[HasPreviousArrowFlagsIndex];
hasPreviousArrowAtkValue->UInt = 0;
// Clear the next arrow flags
var subContextMenusFlagsAtkValue = &m_AtkValues[HasNextArrowFlagsIndex];
subContextMenusFlagsAtkValue->UInt = 0;
for (int contextMenuItemIndex = 0; contextMenuItemIndex < contextMenuItems.Count(); ++contextMenuItemIndex)
{
var contextMenuItem = contextMenuItems.ElementAt(contextMenuItemIndex);
var contextMenuItemAtkValueBaseIndex = FirstContextMenuItemIndex + (contextMenuItemIndex * SequentialAtkValuesPerContextMenuItem);
// Set the name
var nameAtkValue = &m_AtkValues[contextMenuItemAtkValueBaseIndex + NameIndexOffset];
atkValueChangeType(nameAtkValue, FFXIVClientStructs.FFXIV.Component.GUI.ValueType.String);
fixed (byte* nameBytesPtr = contextMenuItem.Name.Encode().NullTerminate())
{
atkValueSetString(nameAtkValue, nameBytesPtr);
}
// Set the enabled state. Note that SE stores this as IsDisabled, NOT IsEnabled (those heathens)
var disabledAtkValue = &m_AtkValues[contextMenuItemAtkValueBaseIndex + IsDisabledIndexOffset];
atkValueChangeType(disabledAtkValue, FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Int);
disabledAtkValue->Int = contextMenuItem.IsEnabled ? 0 : 1;
// Set the action
byte action = 0;
if (contextMenuItem is GameContextMenuItem gameContextMenuItem)
{
action = gameContextMenuItem.Action;
}
else if (contextMenuItem is CustomContextMenuItem customContextMenuItem)
{
if (IsInventoryContext)
{
action = 0xff;
}
else
{
action = 0x67;
}
}
else if (contextMenuItem is OpenSubContextMenuItem openSubContextMenuItem)
{
if (IsInventoryContext)
{
action = 0x30;
}
else
{
action = 0x66;
}
}
byte* actions = null;
if (IsInventoryContext)
{
actions = &((AgentInventoryContext*)m_Agent)->Actions;
}
else
{
actions = &((AgentContext*)m_Agent)->ItemData->Actions;
}
*(actions + FirstContextMenuItemIndex + contextMenuItemIndex) = action;
SetFlag(ref hasPreviousArrowAtkValue->UInt, contextMenuItemIndex, contextMenuItem.HasPreviousArrow);
SetFlag(ref subContextMenusFlagsAtkValue->UInt, contextMenuItemIndex, contextMenuItem.HasNextArrow);
PluginLog.Debug($"Write Name={contextMenuItem.Name} Action={action} IsEnabled={contextMenuItem.IsEnabled} HasPreviousArrow={contextMenuItem.HasPreviousArrow} HasNextArrow={contextMenuItem.HasNextArrow}");
}
Print();
}
private bool HasFlag(uint mask, int itemIndex)
{
return (mask & (1 << itemIndex)) > 0;
}
private void SetFlag(ref uint mask, int itemIndex, bool value)
{
mask &= ~((uint)1 << itemIndex);
if (value)
{
mask |= (uint)(1 << itemIndex);
}
}
public void Print()
{
Print(m_AtkValueCount, m_AtkValues);
}
public static void Print(int atkValueCount, AtkValue* atkValues)
{
PluginLog.Debug($"ContextMenuReader.Print");
for (int atkValueIndex = 0; atkValueIndex < atkValueCount; ++atkValueIndex)
{
var atkValue = &atkValues[atkValueIndex];
object? value = null;
if (atkValue->Type == FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Int)
{
value = atkValue->Int;
}
else if (atkValue->Type == FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Bool)
{
value = atkValue->Byte;
}
else if (atkValue->Type == FFXIVClientStructs.FFXIV.Component.GUI.ValueType.UInt)
{
value = atkValue->UInt;
}
else if (atkValue->Type == FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Float)
{
value = atkValue->Float;
}
else if (atkValue->Type == FFXIVClientStructs.FFXIV.Component.GUI.ValueType.String
|| (int)atkValue->Type == 38
|| (int)atkValue->Type == 8)
{
if (Helper.TryReadSeString((IntPtr)atkValue->String, out var str))
{
value = str;
}
}
else
{
value = $"{(IntPtr)atkValue->String:X}";
}
PluginLog.Debug($"atkValues[{atkValueIndex}]={(IntPtr)atkValue:X} {atkValue->Type}={value}");
}
}
}
}

View File

@@ -0,0 +1,15 @@
using Dalamud.Game.Text.SeStringHandling;
namespace PlayerTags.GameInterface.ContextMenus
{
public class CustomContextMenuItem : ContextMenuItem
{
public CustomContextMenuItemSelectedDelegate CustomAction { get; }
internal CustomContextMenuItem(SeString name, CustomContextMenuItemSelectedDelegate customAction)
: base(name)
{
CustomAction = customAction;
}
}
}

View File

@@ -0,0 +1,15 @@
namespace PlayerTags.GameInterface.ContextMenus
{
public class CustomContextMenuItemSelectedArgs
{
public ContextMenuOpenedArgs ContextMenuOpenedArgs { get; init; }
public CustomContextMenuItem SelectedItem { get; init; }
public CustomContextMenuItemSelectedArgs(ContextMenuOpenedArgs contextMenuOpenedArgs, CustomContextMenuItem selectedItem)
{
ContextMenuOpenedArgs = contextMenuOpenedArgs;
SelectedItem = selectedItem;
}
}
}

View File

@@ -0,0 +1,4 @@
namespace PlayerTags.GameInterface.ContextMenus
{
public delegate void CustomContextMenuItemSelectedDelegate(CustomContextMenuItemSelectedArgs args);
}

View File

@@ -0,0 +1,10 @@
using System.Runtime.InteropServices;
namespace FFXIVClientStructs.FFXIV.Client.UI
{
[StructLayout(LayoutKind.Explicit)]
public struct Addon
{
[FieldOffset(0x1D2)] public ushort ParentAddonId;
}
}

View File

@@ -0,0 +1,13 @@
using FFXIVClientStructs.FFXIV.Component.GUI;
using System.Runtime.InteropServices;
namespace FFXIVClientStructs.FFXIV.Client.UI
{
[StructLayout(LayoutKind.Explicit)]
public struct AddonContext
{
[FieldOffset(0x160)] public unsafe AtkValue* AtkValues;
[FieldOffset(0x1CA)] public ushort AtkValuesCount;
[FieldOffset(0x690)] public /*long*/ bool IsInitialMenu;
}
}

View File

@@ -0,0 +1,24 @@
using FFXIVClientStructs.FFXIV.Client.System.String;
using FFXIVClientStructs.FFXIV.Component.GUI;
using System;
using System.Runtime.InteropServices;
namespace FFXIVClientStructs.FFXIV.Client.UI.Agent
{
[StructLayout(LayoutKind.Explicit)]
public unsafe struct AgentContext
{
public static AgentContext* Instance() => (AgentContext*)System.Framework.Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId(AgentId.Context);
[FieldOffset(0x0)] public AgentInterface AgentInterface;
[FieldOffset(0x670)] public unsafe byte SelectedIndex;
[FieldOffset(0x690)] public byte* Unk1;
[FieldOffset(0xD08)] public byte* SubContextMenuTitle;
[FieldOffset(0xD18)] public unsafe AgentContextItemData* ItemData;
[FieldOffset(0xE08)] public Utf8String ObjectName;
[FieldOffset(0xEE0)] public uint ObjectContentIdLower;
[FieldOffset(0xEF0)] public uint ObjectId;
[FieldOffset(0xF00)] public ushort ObjectWorldId;
[FieldOffset(0x1740)] public bool IsSubContextMenu;
}
}

View File

@@ -0,0 +1,13 @@
using FFXIVClientStructs.FFXIV.Component.GUI;
using System.Runtime.InteropServices;
namespace FFXIVClientStructs.FFXIV.Client.UI.Agent
{
[StructLayout(LayoutKind.Explicit)]
public struct AgentContextItemData
{
[FieldOffset(0x0)] public ushort AtkValuesCount;
[FieldOffset(0x8)] public AtkValue AtkValues;
[FieldOffset(0x428)] public byte Actions;
}
}

View File

@@ -0,0 +1,21 @@
using FFXIVClientStructs.FFXIV.Component.GUI;
using System.Runtime.InteropServices;
namespace FFXIVClientStructs.FFXIV.Client.UI.Agent
{
[StructLayout(LayoutKind.Explicit)]
public unsafe struct AgentInventoryContext
{
public static AgentInventoryContext* Instance() => (AgentInventoryContext*)System.Framework.Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId(AgentId.InventoryContext);
[FieldOffset(0x0)] public AgentInterface AgentInterface;
[FieldOffset(0x558)] public unsafe byte Actions;
[FieldOffset(0x5F8)] public uint ItemId;
[FieldOffset(0x5FC)] public uint ItemCount;
[FieldOffset(0x604)] public bool IsHighQuality;
[FieldOffset(0x670)] public unsafe byte SelectedIndex;
[FieldOffset(0x690)] public byte* Unk1;
[FieldOffset(/*0xD08*/0x690 + 0x678)] public byte* SubContextMenuTitle;
[FieldOffset(0x1740)] public bool IsSubContextMenu;
}
}

View File

@@ -0,0 +1,16 @@
using Dalamud.Game.Text.SeStringHandling;
using System;
namespace PlayerTags.GameInterface.ContextMenus
{
public class GameContextMenuItem : ContextMenuItem
{
public byte Action { get; }
public GameContextMenuItem(SeString name, byte action)
: base(name)
{
Action = action;
}
}
}

View File

@@ -0,0 +1,23 @@
using Dalamud.Game.Text.SeStringHandling;
namespace PlayerTags.GameInterface.ContextMenus
{
public class GameObjectContext
{
public uint Id { get; }
public uint ContentIdLower { get; }
public SeString? Name { get; }
public ushort WorldId { get; }
public GameObjectContext(uint id, uint contentIdLower, SeString name, ushort worldId)
{
Id = id;
ContentIdLower = contentIdLower;
Name = name;
WorldId = worldId;
}
}
}

View File

@@ -0,0 +1,18 @@
namespace PlayerTags.GameInterface.ContextMenus
{
public class ItemContext
{
public uint Id { get; }
public uint Count { get; }
public bool IsHighQuality { get; }
public ItemContext(uint id, uint count, bool isHighQuality)
{
Id = id;
Count = count;
IsHighQuality = isHighQuality;
}
}
}

View File

@@ -0,0 +1,16 @@
using Dalamud.Game.Text.SeStringHandling;
namespace PlayerTags.GameInterface.ContextMenus
{
public class OpenSubContextMenuItem : ContextMenuItem
{
public ContextMenuOpenedDelegate OpenedAction { get; set; }
internal OpenSubContextMenuItem(SeString name, ContextMenuOpenedDelegate openedAction)
: base(name)
{
OpenedAction = openedAction;
HasNextArrow = true;
}
}
}

View File

@@ -0,0 +1,109 @@
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Logging;
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
namespace PlayerTags.GameInterface
{
public static class Helper
{
public static SeString ReadSeString(IntPtr ptr)
{
if (TryReadStringBytes(ptr, out var bytes) && bytes != null)
{
return SeString.Parse(bytes);
}
return new SeString();
}
public static bool TryReadSeString(IntPtr ptr, out SeString? seString)
{
seString = null;
if (TryReadStringBytes(ptr, out var bytes) && bytes != null)
{
seString = SeString.Parse(bytes);
return true;
}
return false;
}
public static string? ReadString(IntPtr ptr)
{
if (TryReadStringBytes(ptr, out var bytes) && bytes != null)
{
return Encoding.UTF8.GetString(bytes);
}
return null;
}
public static bool TryReadString(IntPtr ptr, out string? str)
{
str = null;
if (TryReadStringBytes(ptr, out var bytes) && bytes != null)
{
str = Encoding.UTF8.GetString(bytes);
return true;
}
return false;
}
public static bool TryReadStringBytes(IntPtr ptr, out byte[]? bytes)
{
bytes = null;
if (ptr == IntPtr.Zero)
{
return false;
}
var size = 0;
while (Marshal.ReadByte(ptr, size) != 0)
{
size++;
}
bytes = new byte[size];
Marshal.Copy(ptr, bytes, 0, size);
return true;
}
public static IntPtr Allocate(SeString seString)
{
var bytes = seString.Encode();
IntPtr pointer = Marshal.AllocHGlobal(bytes.Length + 1);
Marshal.Copy(bytes, 0, pointer, bytes.Length);
Marshal.WriteByte(pointer, bytes.Length, 0);
return pointer;
}
public static void Free(ref IntPtr ptr)
{
Marshal.FreeHGlobal(ptr);
ptr = IntPtr.Zero;
}
public static byte[] NullTerminate(this byte[] bytes)
{
if (bytes.Length == 0 || bytes[bytes.Length - 1] != 0)
{
var newBytes = new byte[bytes.Length + 1];
Array.Copy(bytes, newBytes, bytes.Length);
newBytes[^1] = 0;
return newBytes;
}
return bytes;
}
}
}

View File

@@ -0,0 +1,193 @@
using Dalamud.Game;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Hooking;
using Dalamud.Logging;
using FFXIVClientStructs.FFXIV.Client.UI;
using System;
using System.Runtime.InteropServices;
namespace PlayerTags.GameInterface.Nameplates
{
/// <summary>
/// Provides an interface to modify nameplates.
/// </summary>
public class Nameplate : IDisposable
{
private class PluginAddressResolver : BaseAddressResolver
{
private const string c_SetPlayerNameplateSignature = "48 89 5C 24 ?? 48 89 6C 24 ?? 56 57 41 54 41 56 41 57 48 83 EC 40 44 0F B6 E2";
public IntPtr? SetPlayerNameplatePtr { get; private set; }
protected override void Setup64Bit(SigScanner scanner)
{
if (scanner.TryScanText(c_SetPlayerNameplateSignature, out var setPlayerNameplatePtr))
{
SetPlayerNameplatePtr = setPlayerNameplatePtr;
}
}
}
private delegate IntPtr SetPlayerNameplateDelegate_Unmanaged(IntPtr playerNameplateObjectPtr, bool isTitleAboveName, bool isTitleVisible, IntPtr titlePtr, IntPtr namePtr, IntPtr freeCompanyPtr, int iconId);
private Hook<SetPlayerNameplateDelegate_Unmanaged>? m_SetPlayerNameplateHook;
private PluginAddressResolver m_PluginAddressResolver;
/// <summary>
/// Occurs when a player nameplate is updated by the game.
/// </summary>
public event PlayerNameplateUpdatedDelegate? PlayerNameplateUpdated;
/// <summary>
/// Whether the required hooks are in place and this instance is valid.
/// </summary>
public bool IsValid
{
get
{
if (!m_PluginAddressResolver.SetPlayerNameplatePtr.HasValue)
{
return false;
}
return true;
}
}
public Nameplate()
{
m_PluginAddressResolver = new PluginAddressResolver();
m_PluginAddressResolver.Setup();
if (!IsValid)
{
return;
}
if (m_PluginAddressResolver.SetPlayerNameplatePtr.HasValue)
{
m_SetPlayerNameplateHook = new Hook<SetPlayerNameplateDelegate_Unmanaged>(m_PluginAddressResolver.SetPlayerNameplatePtr.Value, new SetPlayerNameplateDelegate_Unmanaged(SetPlayerNameplateDetour));
m_SetPlayerNameplateHook?.Enable();
}
}
public void Dispose()
{
m_SetPlayerNameplateHook?.Disable();
}
private IntPtr SetPlayerNameplateDetour(IntPtr playerNameplateObjectPtr, bool isTitleAboveName, bool isTitleVisible, IntPtr titlePtr, IntPtr namePtr, IntPtr freeCompanyPtr, int iconId)
{
if (m_SetPlayerNameplateHook == null)
{
return IntPtr.Zero;
}
try
{
PlayerCharacter? playerCharacter = GetNameplateGameObject<PlayerCharacter>(playerNameplateObjectPtr);
if (playerCharacter != null)
{
PlayerNameplateUpdatedArgs playerNameplateOpenedArgs = new PlayerNameplateUpdatedArgs(
playerCharacter,
Helper.ReadSeString(namePtr),
Helper.ReadSeString(titlePtr),
Helper.ReadSeString(freeCompanyPtr),
isTitleVisible,
isTitleAboveName,
iconId);
var beforeNameHashCode = playerNameplateOpenedArgs.Name.GetHashCode();
var beforeTitleHashCode = playerNameplateOpenedArgs.Title.GetHashCode();
var beforeFreeCompanyHashCode = playerNameplateOpenedArgs.FreeCompany.GetHashCode();
PlayerNameplateUpdated?.Invoke(playerNameplateOpenedArgs);
IntPtr newNamePtr = namePtr;
bool hasNameChanged = beforeNameHashCode != playerNameplateOpenedArgs.Name.GetHashCode();
if (hasNameChanged)
{
newNamePtr = Helper.Allocate(playerNameplateOpenedArgs.Name);
}
IntPtr newTitlePtr = titlePtr;
bool hasTitleChanged = beforeTitleHashCode != playerNameplateOpenedArgs.Title.GetHashCode();
if (hasTitleChanged)
{
newTitlePtr = Helper.Allocate(playerNameplateOpenedArgs.Title);
}
IntPtr newFreeCompanyPtr = freeCompanyPtr;
bool hasFreeCompanyChanged = beforeFreeCompanyHashCode != playerNameplateOpenedArgs.FreeCompany.GetHashCode();
if (hasFreeCompanyChanged)
{
newFreeCompanyPtr = Helper.Allocate(playerNameplateOpenedArgs.FreeCompany);
}
var result = m_SetPlayerNameplateHook.Original(playerNameplateObjectPtr, isTitleAboveName, isTitleVisible, newNamePtr, newTitlePtr, newFreeCompanyPtr, iconId);
if (hasNameChanged)
{
Helper.Free(ref newNamePtr);
}
if (hasTitleChanged)
{
Helper.Free(ref newTitlePtr);
}
if (hasFreeCompanyChanged)
{
Helper.Free(ref newFreeCompanyPtr);
}
return result;
}
}
catch (Exception ex)
{
PluginLog.Error(ex, $"SetPlayerNameplateDetour");
}
return m_SetPlayerNameplateHook.Original(playerNameplateObjectPtr, isTitleAboveName, isTitleVisible, titlePtr, namePtr, freeCompanyPtr, iconId);
}
private T? GetNameplateGameObject<T>(IntPtr nameplateObjectPtr)
where T : GameObject
{
// Get the nameplate object array
var nameplateAddonPtr = PluginServices.GameGui.GetAddonByName("NamePlate", 1);
var nameplateObjectArrayPtrPtr = nameplateAddonPtr + Marshal.OffsetOf(typeof(AddonNamePlate), nameof(AddonNamePlate.NamePlateObjectArray)).ToInt32();
var nameplateObjectArrayPtr = Marshal.ReadIntPtr(nameplateObjectArrayPtrPtr);
if (nameplateObjectArrayPtr == IntPtr.Zero)
{
return null;
}
// Determine the index of the nameplate object within the nameplate object array
var namePlateObjectSize = Marshal.SizeOf(typeof(AddonNamePlate.NamePlateObject));
var namePlateObjectPtr0 = nameplateObjectArrayPtr + namePlateObjectSize * 0;
var namePlateIndex = (nameplateObjectPtr.ToInt64() - namePlateObjectPtr0.ToInt64()) / namePlateObjectSize;
if (namePlateIndex < 0 || namePlateIndex >= AddonNamePlate.NumNamePlateObjects)
{
return null;
}
// Get the nameplate info array
IntPtr nameplateInfoArrayPtr = IntPtr.Zero;
unsafe
{
var framework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance();
nameplateInfoArrayPtr = new IntPtr(&framework->GetUiModule()->GetRaptureAtkModule()->NamePlateInfoArray);
}
// Get the nameplate info for the nameplate object
var namePlateInfoPtr = new IntPtr(nameplateInfoArrayPtr.ToInt64() + Marshal.SizeOf(typeof(RaptureAtkModule.NamePlateInfo)) * namePlateIndex);
RaptureAtkModule.NamePlateInfo namePlateInfo = Marshal.PtrToStructure<RaptureAtkModule.NamePlateInfo>(namePlateInfoPtr);
// Return the object for its object id
var objectId = namePlateInfo.ObjectID.ObjectID;
return PluginServices.ObjectTable.SearchById(objectId) as T;
}
}
}

View File

@@ -0,0 +1,33 @@
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.Text.SeStringHandling;
namespace PlayerTags.GameInterface.Nameplates
{
public class PlayerNameplateUpdatedArgs
{
public PlayerCharacter PlayerCharacter { get; }
public SeString Name { get; }
public SeString Title { get; }
public SeString FreeCompany { get; }
public bool IsTitleVisible { get; set; }
public bool IsTitleAboveName { get; set; }
public int IconId { get; set; }
public PlayerNameplateUpdatedArgs(PlayerCharacter playerCharacter, SeString name, SeString title, SeString freeCompany, bool isTitleVisible, bool isTitleAboveName, int iconId)
{
PlayerCharacter = playerCharacter;
Name = name;
Title = title;
FreeCompany = freeCompany;
IsTitleVisible = isTitleVisible;
IsTitleAboveName = isTitleAboveName;
IconId = iconId;
}
}
}

View File

@@ -0,0 +1,4 @@
namespace PlayerTags.GameInterface.Nameplates
{
public delegate void PlayerNameplateUpdatedDelegate(PlayerNameplateUpdatedArgs args);
}

View File

@@ -1,191 +0,0 @@
using Dalamud.Game;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Hooking;
using Dalamud.Logging;
using FFXIVClientStructs.FFXIV.Client.UI;
using System;
using System.Runtime.InteropServices;
namespace PlayerTags.Hooks
{
public class NameplateHooks : IDisposable
{
private class PluginAddressResolver : BaseAddressResolver
{
private const string SetPlayerNameplateSignature = "48 89 5C 24 ?? 48 89 6C 24 ?? 56 57 41 54 41 56 41 57 48 83 EC 40 44 0F B6 E2";
public IntPtr SetPlayerNameplatePtr { get; private set; }
protected override void Setup64Bit(SigScanner scanner)
{
SetPlayerNameplatePtr = scanner.ScanText(SetPlayerNameplateSignature);
}
}
private delegate IntPtr SetPlayerNameplateDelegate_Unmanaged(IntPtr playerNameplateObjectPtr, bool isTitleAboveName, bool isTitleVisible, IntPtr titlePtr, IntPtr namePtr, IntPtr freeCompanyPtr, int iconId);
private SetPlayerNameplateDelegate m_SetPlayerNameplate;
private PluginAddressResolver m_PluginAddressResolver;
private Hook<SetPlayerNameplateDelegate_Unmanaged> m_SetPlayerNameplateHook;
public NameplateHooks(SetPlayerNameplateDelegate setPlayerNameplate)
{
m_SetPlayerNameplate = setPlayerNameplate;
m_PluginAddressResolver = new PluginAddressResolver();
m_PluginAddressResolver.Setup();
m_SetPlayerNameplateHook = new Hook<SetPlayerNameplateDelegate_Unmanaged>(m_PluginAddressResolver.SetPlayerNameplatePtr, new SetPlayerNameplateDelegate_Unmanaged(SetPlayerNameplateDetour));
m_SetPlayerNameplateHook.Enable();
}
public void Dispose()
{
m_SetPlayerNameplateHook.Disable();
}
private IntPtr SetPlayerNameplateDetour(IntPtr playerNameplateObjectPtrOriginal, bool isTitleAboveNameOriginal, bool isTitleVisibleOriginal, IntPtr titlePtrOriginal, IntPtr namePtrOriginal, IntPtr freeCompanyPtrOriginal, int iconIdOriginal)
{
if (m_SetPlayerNameplate != null)
{
try
{
PlayerCharacter? playerCharacter = GetNameplateGameObject<PlayerCharacter>(playerNameplateObjectPtrOriginal);
if (playerCharacter != null)
{
SeString title = ReadSeString(titlePtrOriginal);
SeString name = ReadSeString(namePtrOriginal);
SeString freeCompany = ReadSeString(freeCompanyPtrOriginal);
bool isTitleVisible = isTitleVisibleOriginal;
bool isTitleAboveName = isTitleAboveNameOriginal;
int iconId = iconIdOriginal;
bool isTitleChanged;
bool isNameChanged;
bool isFreeCompanyChanged;
m_SetPlayerNameplate(playerCharacter, name, title, freeCompany, ref isTitleVisible, ref isTitleAboveName, ref iconId, out isNameChanged, out isTitleChanged, out isFreeCompanyChanged);
IntPtr namePtr = namePtrOriginal;
if (isNameChanged)
{
namePtr = Allocate(name);
}
IntPtr titlePtr = titlePtrOriginal;
if (isTitleChanged)
{
titlePtr = Allocate(title);
}
IntPtr freeCompanyPtr = freeCompanyPtrOriginal;
if (isFreeCompanyChanged)
{
freeCompanyPtr = Allocate(freeCompany);
}
var result = m_SetPlayerNameplateHook.Original(playerNameplateObjectPtrOriginal, isTitleAboveName, isTitleVisible, titlePtr, namePtr, freeCompanyPtr, iconId);
if (isNameChanged)
{
Release(ref namePtr);
}
if (isTitleChanged)
{
Release(ref titlePtr);
}
if (isFreeCompanyChanged)
{
Release(ref freeCompanyPtr);
}
return result;
}
}
catch (Exception ex)
{
PluginLog.Error(ex, $"SetNameplateDetour encountered a critical error");
}
}
return m_SetPlayerNameplateHook.Original(playerNameplateObjectPtrOriginal, isTitleAboveNameOriginal, isTitleVisibleOriginal, titlePtrOriginal, namePtrOriginal, freeCompanyPtrOriginal, iconIdOriginal);
}
private static SeString ReadSeString(IntPtr stringPtr)
{
return SeString.Parse(ReadStringBytes(stringPtr));
}
private static byte[] ReadStringBytes(IntPtr stringPtr)
{
if (stringPtr == IntPtr.Zero)
{
return null!;
}
var size = 0;
while (Marshal.ReadByte(stringPtr, size) != 0)
{
size++;
}
var bytes = new byte[size];
Marshal.Copy(stringPtr, bytes, 0, size);
return bytes;
}
private static IntPtr Allocate(SeString seString)
{
var bytes = seString.Encode();
IntPtr pointer = Marshal.AllocHGlobal(bytes.Length + 1);
Marshal.Copy(bytes, 0, pointer, bytes.Length);
Marshal.WriteByte(pointer, bytes.Length, 0);
return pointer;
}
private static void Release(ref IntPtr ptr)
{
Marshal.FreeHGlobal(ptr);
ptr = IntPtr.Zero;
}
private T? GetNameplateGameObject<T>(IntPtr nameplateObjectPtr)
where T : GameObject
{
// Get the nameplate object array
var nameplateAddonPtr = PluginServices.GameGui.GetAddonByName("NamePlate", 1);
var nameplateObjectArrayPtrPtr = nameplateAddonPtr + Marshal.OffsetOf(typeof(AddonNamePlate), nameof(AddonNamePlate.NamePlateObjectArray)).ToInt32();
var nameplateObjectArrayPtr = Marshal.ReadIntPtr(nameplateObjectArrayPtrPtr);
if (nameplateObjectArrayPtr == IntPtr.Zero)
{
return null;
}
// Determine the index of the nameplate object within the nameplate object array
var namePlateObjectSize = Marshal.SizeOf(typeof(AddonNamePlate.NamePlateObject));
var namePlateObjectPtr0 = nameplateObjectArrayPtr + namePlateObjectSize * 0;
var namePlateIndex = (nameplateObjectPtr.ToInt64() - namePlateObjectPtr0.ToInt64()) / namePlateObjectSize;
if (namePlateIndex < 0 || namePlateIndex >= AddonNamePlate.NumNamePlateObjects)
{
return null;
}
// Get the nameplate info array
IntPtr nameplateInfoArrayPtr = IntPtr.Zero;
unsafe
{
var framework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance();
nameplateInfoArrayPtr = new IntPtr(&framework->GetUiModule()->GetRaptureAtkModule()->NamePlateInfoArray);
}
// Get the nameplate info for the nameplate object
var namePlateInfoPtr = new IntPtr(nameplateInfoArrayPtr.ToInt64() + Marshal.SizeOf(typeof(RaptureAtkModule.NamePlateInfo)) * namePlateIndex);
RaptureAtkModule.NamePlateInfo namePlateInfo = Marshal.PtrToStructure<RaptureAtkModule.NamePlateInfo>(namePlateInfoPtr);
// Return the object for its object id
var objectId = namePlateInfo.ObjectID.ObjectID;
return PluginServices.ObjectTable.SearchById(objectId) as T;
}
}
}

View File

@@ -1,7 +0,0 @@
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.Text.SeStringHandling;
namespace PlayerTags.Hooks
{
public delegate void SetPlayerNameplateDelegate(PlayerCharacter playerCharacter, SeString name, SeString title, SeString freeCompany, ref bool isTitleVisible, ref bool isTitleAboveName, ref int iconId, out bool isNameChanged, out bool isTitleChanged, out bool isFreeCompanyChanged);
}

View File

@@ -21,7 +21,6 @@
<ItemGroup>
<PackageReference Include="DalamudPackager" Version="2.1.5" />
<PackageReference Include="XivCommon" Version="4.0.0-alpha.2" />
<Reference Include="FFXIVClientStructs">
<HintPath>$(DalamudLibPath)FFXIVClientStructs.dll</HintPath>
<Private>false</Private>

View File

@@ -3,7 +3,6 @@ using Dalamud.Plugin;
using PlayerTags.Configuration;
using PlayerTags.Data;
using PlayerTags.Features;
using XivCommon;
namespace PlayerTags
{
@@ -12,15 +11,13 @@ namespace PlayerTags
public string Name => "Player Tags";
private const string c_CommandName = "/playertags";
private XivCommonBase m_XivCommon;
private PluginConfiguration m_PluginConfiguration;
private PluginData m_PluginData;
private PluginConfigurationUI m_PluginConfigurationUI;
private LinkSelfInChatFeature m_LinkSelfInChatFeature;
private CustomTagsContextMenuFeature m_CustomTagsContextMenuFeature;
private NameplatesTagTargetFeature m_NameplatesTagTargetFeature;
private NameplateTagTargetFeature m_NameplatesTagTargetFeature;
private ChatTagTargetFeature m_ChatTagTargetFeature;
public Plugin(DalamudPluginInterface pluginInterface)
@@ -31,17 +28,15 @@ namespace PlayerTags
m_PluginData = new PluginData(m_PluginConfiguration);
m_PluginConfigurationUI = new PluginConfigurationUI(m_PluginConfiguration, m_PluginData);
m_XivCommon = new XivCommonBase(XivCommon.Hooks.ContextMenu);
PluginServices.DalamudPluginInterface.UiBuilder.Draw += UiBuilder_Draw;
PluginServices.DalamudPluginInterface.UiBuilder.OpenConfigUi += UiBuilder_OpenConfigUi;
PluginServices.CommandManager.AddHandler(c_CommandName, new CommandInfo((string command, string arguments) =>
{
m_PluginConfiguration.IsVisible = true;
m_PluginConfiguration.Save(m_PluginData);
UiBuilder_OpenConfigUi();
}) { HelpMessage = "Shows the config" });
m_LinkSelfInChatFeature = new LinkSelfInChatFeature(m_PluginConfiguration, m_PluginData);
m_CustomTagsContextMenuFeature = new CustomTagsContextMenuFeature(m_XivCommon, m_PluginConfiguration, m_PluginData);
m_NameplatesTagTargetFeature = new NameplatesTagTargetFeature(m_PluginConfiguration, m_PluginData);
m_CustomTagsContextMenuFeature = new CustomTagsContextMenuFeature(m_PluginConfiguration, m_PluginData);
m_NameplatesTagTargetFeature = new NameplateTagTargetFeature(m_PluginConfiguration, m_PluginData);
m_ChatTagTargetFeature = new ChatTagTargetFeature(m_PluginConfiguration, m_PluginData);
}
@@ -54,7 +49,6 @@ namespace PlayerTags
PluginServices.CommandManager.RemoveHandler(c_CommandName);
PluginServices.DalamudPluginInterface.UiBuilder.OpenConfigUi -= UiBuilder_OpenConfigUi;
PluginServices.DalamudPluginInterface.UiBuilder.Draw -= UiBuilder_Draw;
m_XivCommon.Dispose();
}
private void UiBuilder_Draw()