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:
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>();
|
||||
|
||||
682
PlayerTags/GameInterface/ContextMenus/ContextMenu.cs
Normal file
682
PlayerTags/GameInterface/ContextMenus/ContextMenu.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
28
PlayerTags/GameInterface/ContextMenus/ContextMenuItem.cs
Normal file
28
PlayerTags/GameInterface/ContextMenus/ContextMenuItem.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace PlayerTags.GameInterface.ContextMenus
|
||||
{
|
||||
public delegate void ContextMenuOpenedDelegate(ContextMenuOpenedArgs args);
|
||||
}
|
||||
465
PlayerTags/GameInterface/ContextMenus/ContextMenuReaderWriter.cs
Normal file
465
PlayerTags/GameInterface/ContextMenus/ContextMenuReaderWriter.cs
Normal 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace PlayerTags.GameInterface.ContextMenus
|
||||
{
|
||||
public delegate void CustomContextMenuItemSelectedDelegate(CustomContextMenuItemSelectedArgs args);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace FFXIVClientStructs.FFXIV.Client.UI
|
||||
{
|
||||
[StructLayout(LayoutKind.Explicit)]
|
||||
public struct Addon
|
||||
{
|
||||
[FieldOffset(0x1D2)] public ushort ParentAddonId;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
16
PlayerTags/GameInterface/ContextMenus/GameContextMenuItem.cs
Normal file
16
PlayerTags/GameInterface/ContextMenus/GameContextMenuItem.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
23
PlayerTags/GameInterface/ContextMenus/GameObjectContext.cs
Normal file
23
PlayerTags/GameInterface/ContextMenus/GameObjectContext.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
PlayerTags/GameInterface/ContextMenus/ItemContext.cs
Normal file
18
PlayerTags/GameInterface/ContextMenus/ItemContext.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
109
PlayerTags/GameInterface/Helper.cs
Normal file
109
PlayerTags/GameInterface/Helper.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
193
PlayerTags/GameInterface/Nameplates/Nameplate.cs
Normal file
193
PlayerTags/GameInterface/Nameplates/Nameplate.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace PlayerTags.GameInterface.Nameplates
|
||||
{
|
||||
public delegate void PlayerNameplateUpdatedDelegate(PlayerNameplateUpdatedArgs args);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user