diff --git a/PlayerTags/Configuration/PluginConfigurationUI.cs b/PlayerTags/Configuration/PluginConfigurationUI.cs index 234dd34..14ff115 100644 --- a/PlayerTags/Configuration/PluginConfigurationUI.cs +++ b/PlayerTags/Configuration/PluginConfigurationUI.cs @@ -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(); diff --git a/PlayerTags/Data/Identity.cs b/PlayerTags/Data/Identity.cs index 364fa05..2ef5444 100644 --- a/PlayerTags/Data/Identity.cs +++ b/PlayerTags/Data/Identity.cs @@ -4,14 +4,14 @@ using System.Collections.Generic; namespace PlayerTags.Data { - public class Identity : IComparable + public class Identity : IComparable, IEquatable { public string Name { get; init; } public uint? WorldId { get; set; } = null; public List CustomTagIds { get; init; } = new List(); [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; } } } diff --git a/PlayerTags/Data/PluginData.cs b/PlayerTags/Data/PluginData.cs index 11c7250..3fedeac 100644 --- a/PlayerTags/Data/PluginData.cs +++ b/PlayerTags/Data/PluginData.cs @@ -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) diff --git a/PlayerTags/Features/ChatTagTargetFeature.cs b/PlayerTags/Features/ChatTagTargetFeature.cs index 37163ce..06a0266 100644 --- a/PlayerTags/Features/ChatTagTargetFeature.cs +++ b/PlayerTags/Features/ChatTagTargetFeature.cs @@ -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 { + /// + /// A feature that adds tags to chat messages. + /// public class ChatTagTargetFeature : TagTargetFeature { /// @@ -169,7 +171,7 @@ namespace PlayerTags.Features } /// - /// Adds all configured tags to chat. + /// Adds all configured tags to a chat message. /// /// The message to change. private void AddTagsToChat(SeString message) diff --git a/PlayerTags/Features/CustomTagsContextMenuFeature.cs b/PlayerTags/Features/CustomTagsContextMenuFeature.cs index cdd9bf9..79f1037 100644 --- a/PlayerTags/Features/CustomTagsContextMenuFeature.cs +++ b/PlayerTags/Features/CustomTagsContextMenuFeature.cs @@ -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 newContextMenuItems = new List(); + newContextMenuItems.Add(new OpenSubContextMenuItem("Inner1", (subContextMenuOpenedArgs2 => + { + PluginLog.Debug("Executed Inner1"); + + List newContextMenuItems = new List(); + 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 newContextMenuItems = new List(); 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("", (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 newContextMenuItems = new List(); 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("", (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); - } } } diff --git a/PlayerTags/Features/NameplatesTagTargetFeature.cs b/PlayerTags/Features/NameplateTagTargetFeature.cs similarity index 76% rename from PlayerTags/Features/NameplatesTagTargetFeature.cs rename to PlayerTags/Features/NameplateTagTargetFeature.cs index ea2b827..c3eb394 100644 --- a/PlayerTags/Features/NameplatesTagTargetFeature.cs +++ b/PlayerTags/Features/NameplateTagTargetFeature.cs @@ -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 + /// + /// A feature that adds tags to nameplates. + /// + 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; } - /// - /// Sets the strings on a nameplate. - /// - /// The player character context. - /// The name text. - /// The title text. - /// The free company text. - /// Whether the title is visible. - /// Whether the title is above the name or below it. - /// The icon id. - /// Whether the name was changed. - /// Whether the title was changed. - /// Whether the free company was changed. - 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; } } /// - /// Adds the given payload changes to the dictionary. + /// Adds the given payload changes to the specified locations. /// - /// The nameplate element to add changes to. - /// The position to add changes to. - /// The payloads to add. - /// The dictionary to add the changes to. - private void AddPayloadChanges(NameplateElement nameplateElement, TagPosition tagPosition, IEnumerable payloads, Dictionary>> nameplateChanges) + /// The nameplate element of the changes. + /// The position of the changes. + /// The payload changes to add. + /// The dictionary to add changes to. + private void AddPayloadChanges(NameplateElement nameplateElement, TagPosition tagPosition, IEnumerable payloadChanges, Dictionary>> nameplateChanges) { - if (!payloads.Any()) + if (!payloadChanges.Any()) { return; } @@ -154,25 +155,18 @@ namespace PlayerTags.Features nameplateChanges[nameplateElement] = new Dictionary>(); } - AddPayloadChanges(tagPosition, payloads, nameplateChanges[nameplateElement]); + AddPayloadChanges(tagPosition, payloadChanges, nameplateChanges[nameplateElement]); } /// - /// Adds all configured tags to the nameplate of a game object. + /// Adds tags to the nameplate of a game object. /// /// The game object context. /// The name text to change. /// The title text to change. /// The free company text to change. - /// Whether the name was changed. - /// Whether the title was changed. - /// Whether the free company was changed. - 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>> nameplateChanges = new Dictionary>>(); 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; } } } diff --git a/PlayerTags/Features/TagTargetFeature.cs b/PlayerTags/Features/TagTargetFeature.cs index f6833c2..1771f84 100644 --- a/PlayerTags/Features/TagTargetFeature.cs +++ b/PlayerTags/Features/TagTargetFeature.cs @@ -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 { + /// + /// The base of a feature that adds tags to UI elements. + /// public abstract class TagTargetFeature : IDisposable { private ActivityContext m_CurrentActivityContext; @@ -90,21 +88,26 @@ namespace PlayerTags.Features } /// - /// 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. /// /// The game object to get payloads for. /// The tag config to get payloads for. /// A list of payloads for the given tag. - protected IEnumerable GetPayloads(Tag tag, GameObject? gameObject) + protected Payload[] GetPayloads(Tag tag, GameObject? gameObject) { if (!IsTagVisible(tag, gameObject)) { - return Enumerable.Empty(); + return Array.Empty(); } return CreatePayloads(tag); } + /// + /// Creates payloads for the given tag. + /// + /// The tag to create payloads for. + /// The payloads for the given tag. private Payload[] CreatePayloads(Tag tag) { List newPayloads = new List(); diff --git a/PlayerTags/GameInterface/ContextMenus/ContextMenu.cs b/PlayerTags/GameInterface/ContextMenus/ContextMenu.cs new file mode 100644 index 0000000..0a5aecf --- /dev/null +++ b/PlayerTags/GameInterface/ContextMenus/ContextMenu.cs @@ -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); + + /// + /// Provides an interface to modify context menus. + /// + 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? m_ContextMenuOpeningHook; + + private unsafe delegate byte ContextMenuOpenedDelegate_Unmanaged(IntPtr addon, int menuSize, AtkValue* atkValueArgs); + private Hook? m_ContextMenuOpenedHook; + private Hook? m_SubContextMenuOpenedHook; + + private delegate byte ContextMenuItemSelectedDelegate_Unmanaged(IntPtr addon, int index, byte a3); + private Hook? m_ContextMenuItemSelectedHook; + + private delegate byte SubContextMenuOpeningDelegate_Unmanaged(IntPtr agent); + private Hook? 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? 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; + + /// + /// Occurs when a context menu is opened by the game. + /// + public event ContextMenuOpenedDelegate? ContextMenuOpened; + + /// + /// Whether the required hooks are in place and this instance is valid. + /// + 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(m_PluginAddressResolver.AtkValueChangeTypePtr.Value); + } + + if (m_PluginAddressResolver.AtkValueSetStringPtr.HasValue) + { + m_AtkValueSetString = Marshal.GetDelegateForFunctionPointer(m_PluginAddressResolver.AtkValueSetStringPtr.Value); + } + + if (m_PluginAddressResolver.GetAddonByIdPtr.HasValue) + { + m_GetAddonById = Marshal.GetDelegateForFunctionPointer(m_PluginAddressResolver.GetAddonByIdPtr.Value); + } + + if (m_PluginAddressResolver.OpenSubContextMenuPtr.HasValue) + { + m_OpenSubContextMenu = Marshal.GetDelegateForFunctionPointer(m_PluginAddressResolver.OpenSubContextMenuPtr.Value); + } + + if (m_PluginAddressResolver.ContextMenuOpeningPtr.HasValue) + { + m_ContextMenuOpeningHook = new Hook(m_PluginAddressResolver.ContextMenuOpeningPtr.Value, ContextMenuOpeningDetour); + m_ContextMenuOpeningHook?.Enable(); + } + + if (m_PluginAddressResolver.ContextMenuOpenedPtr.HasValue) + { + unsafe + { + m_ContextMenuOpenedHook = new Hook(m_PluginAddressResolver.ContextMenuOpenedPtr.Value, ContextMenuOpenedDetour); + } + m_ContextMenuOpenedHook?.Enable(); + } + + if (m_PluginAddressResolver.ContextMenuItemSelectedPtr.HasValue) + { + m_ContextMenuItemSelectedHook = new Hook(m_PluginAddressResolver.ContextMenuItemSelectedPtr.Value, ContextMenuItemSelectedDetour); + m_ContextMenuItemSelectedHook?.Enable(); + } + + if (m_PluginAddressResolver.SubContextMenuOpeningPtr.HasValue) + { + m_SubContextMenuOpeningHook = new Hook(m_PluginAddressResolver.SubContextMenuOpeningPtr.Value, SubContextMenuOpeningDetour); + m_SubContextMenuOpeningHook?.Enable(); + } + + if (m_PluginAddressResolver.SubContextMenuOpenedPtr.HasValue) + { + unsafe + { + m_SubContextMenuOpenedHook = new Hook(m_PluginAddressResolver.SubContextMenuOpenedPtr.Value, SubContextMenuOpenedDetour); + } + m_SubContextMenuOpenedHook?.Enable(); + } + + if (m_PluginAddressResolver.OpenInventoryContextMenuPtr.HasValue) + { + m_OpenInventoryContextMenu = Marshal.GetDelegateForFunctionPointer(m_PluginAddressResolver.OpenInventoryContextMenuPtr.Value); + } + + if (m_PluginAddressResolver.InventoryContextMenuEvent30Ptr.HasValue) + { + m_InventoryContextMenuEvent30Hook = new Hook(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 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).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); + } + } +} diff --git a/PlayerTags/GameInterface/ContextMenus/ContextMenuItem.cs b/PlayerTags/GameInterface/ContextMenus/ContextMenuItem.cs new file mode 100644 index 0000000..9e66199 --- /dev/null +++ b/PlayerTags/GameInterface/ContextMenus/ContextMenuItem.cs @@ -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(); + } + } +} diff --git a/PlayerTags/GameInterface/ContextMenus/ContextMenuOpenedArgs.cs b/PlayerTags/GameInterface/ContextMenus/ContextMenuOpenedArgs.cs new file mode 100644 index 0000000..f4e30ae --- /dev/null +++ b/PlayerTags/GameInterface/ContextMenus/ContextMenuOpenedArgs.cs @@ -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 ContextMenuItems { get; } + + public GameObjectContext? GameObjectContext { get; init; } + + public ItemContext? ItemContext { get; init; } + + public ContextMenuOpenedArgs(IntPtr addon, IntPtr agent, string? parentAddonName, IEnumerable contextMenuItems) + { + Addon = addon; + Agent = agent; + ParentAddonName = parentAddonName; + ContextMenuItems = new List(contextMenuItems); + } + } +} diff --git a/PlayerTags/GameInterface/ContextMenus/ContextMenuOpenedDelegate.cs b/PlayerTags/GameInterface/ContextMenus/ContextMenuOpenedDelegate.cs new file mode 100644 index 0000000..f09216f --- /dev/null +++ b/PlayerTags/GameInterface/ContextMenus/ContextMenuOpenedDelegate.cs @@ -0,0 +1,4 @@ +namespace PlayerTags.GameInterface.ContextMenus +{ + public delegate void ContextMenuOpenedDelegate(ContextMenuOpenedArgs args); +} diff --git a/PlayerTags/GameInterface/ContextMenus/ContextMenuReaderWriter.cs b/PlayerTags/GameInterface/ContextMenus/ContextMenuReaderWriter.cs new file mode 100644 index 0000000..9201230 --- /dev/null +++ b/PlayerTags/GameInterface/ContextMenus/ContextMenuReaderWriter.cs @@ -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 gameContextMenuItems = new List(); + 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 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() * 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}"); + } + } + } +} diff --git a/PlayerTags/GameInterface/ContextMenus/CustomContextMenuItem.cs b/PlayerTags/GameInterface/ContextMenus/CustomContextMenuItem.cs new file mode 100644 index 0000000..863cc2c --- /dev/null +++ b/PlayerTags/GameInterface/ContextMenus/CustomContextMenuItem.cs @@ -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; + } + } +} \ No newline at end of file diff --git a/PlayerTags/GameInterface/ContextMenus/CustomContextMenuItemSelectedArgs.cs b/PlayerTags/GameInterface/ContextMenus/CustomContextMenuItemSelectedArgs.cs new file mode 100644 index 0000000..45abfa4 --- /dev/null +++ b/PlayerTags/GameInterface/ContextMenus/CustomContextMenuItemSelectedArgs.cs @@ -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; + } + } +} \ No newline at end of file diff --git a/PlayerTags/GameInterface/ContextMenus/CustomContextMenuItemSelectedDelegate.cs b/PlayerTags/GameInterface/ContextMenus/CustomContextMenuItemSelectedDelegate.cs new file mode 100644 index 0000000..4040cf9 --- /dev/null +++ b/PlayerTags/GameInterface/ContextMenus/CustomContextMenuItemSelectedDelegate.cs @@ -0,0 +1,4 @@ +namespace PlayerTags.GameInterface.ContextMenus +{ + public delegate void CustomContextMenuItemSelectedDelegate(CustomContextMenuItemSelectedArgs args); +} diff --git a/PlayerTags/GameInterface/ContextMenus/FFXIVClientStructs/FFXIV/Client/UI/Addon.cs b/PlayerTags/GameInterface/ContextMenus/FFXIVClientStructs/FFXIV/Client/UI/Addon.cs new file mode 100644 index 0000000..9fa3ceb --- /dev/null +++ b/PlayerTags/GameInterface/ContextMenus/FFXIVClientStructs/FFXIV/Client/UI/Addon.cs @@ -0,0 +1,10 @@ +using System.Runtime.InteropServices; + +namespace FFXIVClientStructs.FFXIV.Client.UI +{ + [StructLayout(LayoutKind.Explicit)] + public struct Addon + { + [FieldOffset(0x1D2)] public ushort ParentAddonId; + } +} diff --git a/PlayerTags/GameInterface/ContextMenus/FFXIVClientStructs/FFXIV/Client/UI/AddonContext.cs b/PlayerTags/GameInterface/ContextMenus/FFXIVClientStructs/FFXIV/Client/UI/AddonContext.cs new file mode 100644 index 0000000..c879986 --- /dev/null +++ b/PlayerTags/GameInterface/ContextMenus/FFXIVClientStructs/FFXIV/Client/UI/AddonContext.cs @@ -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; + } +} diff --git a/PlayerTags/GameInterface/ContextMenus/FFXIVClientStructs/FFXIV/Client/UI/Agent/AgentContext.cs b/PlayerTags/GameInterface/ContextMenus/FFXIVClientStructs/FFXIV/Client/UI/Agent/AgentContext.cs new file mode 100644 index 0000000..3f36c12 --- /dev/null +++ b/PlayerTags/GameInterface/ContextMenus/FFXIVClientStructs/FFXIV/Client/UI/Agent/AgentContext.cs @@ -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; + } +} diff --git a/PlayerTags/GameInterface/ContextMenus/FFXIVClientStructs/FFXIV/Client/UI/Agent/AgentContextItemData.cs b/PlayerTags/GameInterface/ContextMenus/FFXIVClientStructs/FFXIV/Client/UI/Agent/AgentContextItemData.cs new file mode 100644 index 0000000..c6b34d1 --- /dev/null +++ b/PlayerTags/GameInterface/ContextMenus/FFXIVClientStructs/FFXIV/Client/UI/Agent/AgentContextItemData.cs @@ -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; + } +} diff --git a/PlayerTags/GameInterface/ContextMenus/FFXIVClientStructs/FFXIV/Client/UI/Agent/AgentInventoryContext.cs b/PlayerTags/GameInterface/ContextMenus/FFXIVClientStructs/FFXIV/Client/UI/Agent/AgentInventoryContext.cs new file mode 100644 index 0000000..e57581c --- /dev/null +++ b/PlayerTags/GameInterface/ContextMenus/FFXIVClientStructs/FFXIV/Client/UI/Agent/AgentInventoryContext.cs @@ -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; + } +} diff --git a/PlayerTags/GameInterface/ContextMenus/GameContextMenuItem.cs b/PlayerTags/GameInterface/ContextMenus/GameContextMenuItem.cs new file mode 100644 index 0000000..180ed91 --- /dev/null +++ b/PlayerTags/GameInterface/ContextMenus/GameContextMenuItem.cs @@ -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; + } + } +} \ No newline at end of file diff --git a/PlayerTags/GameInterface/ContextMenus/GameObjectContext.cs b/PlayerTags/GameInterface/ContextMenus/GameObjectContext.cs new file mode 100644 index 0000000..4322d25 --- /dev/null +++ b/PlayerTags/GameInterface/ContextMenus/GameObjectContext.cs @@ -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; + } + } +} diff --git a/PlayerTags/GameInterface/ContextMenus/ItemContext.cs b/PlayerTags/GameInterface/ContextMenus/ItemContext.cs new file mode 100644 index 0000000..6aaa50b --- /dev/null +++ b/PlayerTags/GameInterface/ContextMenus/ItemContext.cs @@ -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; + } + } +} diff --git a/PlayerTags/GameInterface/ContextMenus/OpenSubContextMenuItem.cs b/PlayerTags/GameInterface/ContextMenus/OpenSubContextMenuItem.cs new file mode 100644 index 0000000..fde0e12 --- /dev/null +++ b/PlayerTags/GameInterface/ContextMenus/OpenSubContextMenuItem.cs @@ -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; + } + } +} \ No newline at end of file diff --git a/PlayerTags/GameInterface/Helper.cs b/PlayerTags/GameInterface/Helper.cs new file mode 100644 index 0000000..aa2f36e --- /dev/null +++ b/PlayerTags/GameInterface/Helper.cs @@ -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; + } + } +} diff --git a/PlayerTags/GameInterface/Nameplates/Nameplate.cs b/PlayerTags/GameInterface/Nameplates/Nameplate.cs new file mode 100644 index 0000000..e9ca24a --- /dev/null +++ b/PlayerTags/GameInterface/Nameplates/Nameplate.cs @@ -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 +{ + /// + /// Provides an interface to modify nameplates. + /// + 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? m_SetPlayerNameplateHook; + + private PluginAddressResolver m_PluginAddressResolver; + + /// + /// Occurs when a player nameplate is updated by the game. + /// + public event PlayerNameplateUpdatedDelegate? PlayerNameplateUpdated; + + /// + /// Whether the required hooks are in place and this instance is valid. + /// + 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(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(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(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(namePlateInfoPtr); + + // Return the object for its object id + var objectId = namePlateInfo.ObjectID.ObjectID; + return PluginServices.ObjectTable.SearchById(objectId) as T; + } + } +} diff --git a/PlayerTags/GameInterface/Nameplates/PlayerNameplateUpdatedArgs.cs b/PlayerTags/GameInterface/Nameplates/PlayerNameplateUpdatedArgs.cs new file mode 100644 index 0000000..517410a --- /dev/null +++ b/PlayerTags/GameInterface/Nameplates/PlayerNameplateUpdatedArgs.cs @@ -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; + } + } +} diff --git a/PlayerTags/GameInterface/Nameplates/PlayerNameplateUpdatedDelegate.cs b/PlayerTags/GameInterface/Nameplates/PlayerNameplateUpdatedDelegate.cs new file mode 100644 index 0000000..0e38218 --- /dev/null +++ b/PlayerTags/GameInterface/Nameplates/PlayerNameplateUpdatedDelegate.cs @@ -0,0 +1,4 @@ +namespace PlayerTags.GameInterface.Nameplates +{ + public delegate void PlayerNameplateUpdatedDelegate(PlayerNameplateUpdatedArgs args); +} diff --git a/PlayerTags/Hooks/NameplateHooks.cs b/PlayerTags/Hooks/NameplateHooks.cs deleted file mode 100644 index da7ce3d..0000000 --- a/PlayerTags/Hooks/NameplateHooks.cs +++ /dev/null @@ -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 m_SetPlayerNameplateHook; - - public NameplateHooks(SetPlayerNameplateDelegate setPlayerNameplate) - { - m_SetPlayerNameplate = setPlayerNameplate; - - m_PluginAddressResolver = new PluginAddressResolver(); - m_PluginAddressResolver.Setup(); - - m_SetPlayerNameplateHook = new Hook(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(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(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(namePlateInfoPtr); - - // Return the object for its object id - var objectId = namePlateInfo.ObjectID.ObjectID; - return PluginServices.ObjectTable.SearchById(objectId) as T; - } - } -} diff --git a/PlayerTags/Hooks/SetPlayerNameplateDelegate.cs b/PlayerTags/Hooks/SetPlayerNameplateDelegate.cs deleted file mode 100644 index 314b6e0..0000000 --- a/PlayerTags/Hooks/SetPlayerNameplateDelegate.cs +++ /dev/null @@ -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); -} diff --git a/PlayerTags/PlayerTags.csproj b/PlayerTags/PlayerTags.csproj index 14001fc..7b5606b 100644 --- a/PlayerTags/PlayerTags.csproj +++ b/PlayerTags/PlayerTags.csproj @@ -21,7 +21,6 @@ - $(DalamudLibPath)FFXIVClientStructs.dll false diff --git a/PlayerTags/Plugin.cs b/PlayerTags/Plugin.cs index 8b59d2c..fd98d98 100644 --- a/PlayerTags/Plugin.cs +++ b/PlayerTags/Plugin.cs @@ -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()